Add casbin, rhia and upgrade getrandom to 0.2.10
diff --git a/README.md b/README.md
index 46e0ac2..67c7838 100644
--- a/README.md
+++ b/README.md
@@ -5,14 +5,16 @@
Below list the crates:
+- [casbin-rs](https://github.com/casbin/casbin-rs/tree/2bd7ef5723cbf6da58a4905f389bd1a29359243f)
- [csv](https://github.com/BurntSushi/rust-csv/tree/574ae1ff64693b42ae0ce153926d9a0a5d546936)
- gbdt
-- [getrandom](https://github.com/rust-random/getrandom/tree/0830ba66be8a5c019fc5ced5747c9d0a023e4d3e)
+- [getrandom](https://github.com/rust-random/getrandom/tree/2e483d68aaa57168a84489349d6473b492e05478)
- [image](https://github.com/image-rs/image/tree/2a79d33e663d27e402c76bfc6aa5ca78b1cc9895)
- [mio](https://github.com/tokio-rs/mio/tree/7ed74bf478230a0cfa7543901f6be6df8bb3602e)
- [num_cpus](https://github.com/seanmonstar/num_cpus/tree/e437b9d9083d717692e35d917de8674a7987dd06)
- [rand](https://github.com/rust-random/rand/tree/3543f4b0258ecec04be570bbe9dc6e50d80bd3c1)
- [rayon](https://github.com/rayon-rs/rayon/tree/3883630e0bcdcfd152fad36352893662a5bb380e)
+- [rhai](https://github.com/rhaiscript/rhai/tree/fd162ab99f9e8cf34fb36fee07ac8e52520e7c49)
- [ring](https://github.com/briansmith/ring/tree/9cc0d45f4d8521f467bb3a621e74b1535e118188)
- [rustface](https://github.com/atomashpolskiy/rustface/tree/93c97ed7d0fa1cc3553f5483d865292cc37ceb98)
- [rustls](https://github.com/rustls/rustls/tree/92600efb4f6cc25bfe0c133b0b922d915ed826e3)
diff --git a/casbin-rs/.github/dependabot.yml b/casbin-rs/.github/dependabot.yml
new file mode 100644
index 0000000..f6093ce
--- /dev/null
+++ b/casbin-rs/.github/dependabot.yml
@@ -0,0 +1,12 @@
+version: 2
+updates:
+- package-ecosystem: cargo
+ directory: "/"
+ schedule:
+ interval: daily
+ time: "13:00"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: tokio
+ versions:
+ - 1.1.0
diff --git a/casbin-rs/.github/workflows/changelog.yml b/casbin-rs/.github/workflows/changelog.yml
new file mode 100644
index 0000000..5508090
--- /dev/null
+++ b/casbin-rs/.github/workflows/changelog.yml
@@ -0,0 +1,29 @@
+name: Changelog Generator
+
+on:
+ release:
+ types: [created, edited]
+
+jobs:
+ generate_changelog_file:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Setup ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 2.7
+
+ - name: Install github_changelog_generator
+ run: gem install github_changelog_generator
+
+ - name: Gen changelog
+ run: github_changelog_generator -t ${{ secrets.GITHUB_TOKEN }} -u casbin -p casbin-rs
+
+ - name: Commit changes
+ uses: stefanzweifel/git-auto-commit-action@v4
+ with:
+ commit_message: 'changelog: auto update by ci'
+ file_pattern: CHANGELOG.md
diff --git a/casbin-rs/.github/workflows/ci.yml b/casbin-rs/.github/workflows/ci.yml
new file mode 100644
index 0000000..b43952e
--- /dev/null
+++ b/casbin-rs/.github/workflows/ci.yml
@@ -0,0 +1,103 @@
+name: CI
+
+on:
+ pull_request:
+ push:
+ branches:
+ - master
+
+jobs:
+ build:
+ name: Auto Build CI
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macOS-latest]
+ rust: [nightly, beta, stable]
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@master
+
+ - name: Install Rust toolchain ${{ matrix.rust }}
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: ${{ matrix.rust }}
+ components: rustfmt, clippy
+ override: true
+
+ - name: Install wasm32-unknown-unknown for ${{ matrix.rust }}
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: ${{ matrix.rust }}
+ target: wasm32-unknown-unknown
+ override: true
+
+ # Work around https://github.com/actions/cache/issues/403 by using GNU tar
+ # instead of BSD tar.
+ - name: Install GNU tar
+ if: matrix.os == 'macOS-latest'
+ run: |
+ brew install gnu-tar
+ echo PATH="/usr/local/opt/gnu-tar/libexec/gnubin:$PATH" >> $GITHUB_ENV
+
+ - name: Cache cargo registry
+ uses: actions/cache@v1
+ with:
+ path: ~/.cargo/registry
+ key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.toml') }}-${{ secrets.CACHE_VERSION }}
+
+ - name: Cache cargo index
+ uses: actions/cache@v1
+ with:
+ path: ~/.cargo/git
+ key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-index-${{ hashFiles('**/Cargo.toml') }}-${{ secrets.CACHE_VERSION }}
+
+ - name: Cache cargo build
+ uses: actions/cache@v1
+ with:
+ path: target
+ key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.toml') }}-${{ secrets.CACHE_VERSION }}
+
+ - name: Release build async-std
+ uses: actions-rs/cargo@v1
+ with:
+ command: build
+ args: --release --no-default-features --features runtime-async-std,cached,glob,ip,watcher,logging,incremental,explain
+
+ - name: Release build tokio
+ uses: actions-rs/cargo@v1
+ with:
+ command: build
+ args: --release --no-default-features --features runtime-tokio,cached,glob,ip,watcher,logging,incremental,explain
+
+ - name: Cargo Test For All Features Using async-std
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --no-default-features --features runtime-async-std,amortized,cached,glob,ip,watcher,logging,incremental,explain
+
+ - name: Cargo Test For All Features Using tokio
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --no-default-features --features runtime-tokio,amortized,cached,glob,ip,watcher,logging,incremental,explain
+
+ - name: Cargo Check Wasm
+ uses: actions-rs/cargo@v1
+ with:
+ command: check
+ args: --target wasm32-unknown-unknown --no-default-features --features runtime-async-std,amortized,cached,glob,ip,watcher,logging,incremental
+
+ - name: Clippy warnings
+ uses: actions-rs/cargo@v1
+ with:
+ command: clippy
+ args: -- -D warnings
+
+ - name: Cargo Fmt Check
+ uses: actions-rs/cargo@v1
+ with:
+ command: fmt
+ args: --all -- --check
diff --git a/casbin-rs/.github/workflows/coverage.yml b/casbin-rs/.github/workflows/coverage.yml
new file mode 100644
index 0000000..475dc1c
--- /dev/null
+++ b/casbin-rs/.github/workflows/coverage.yml
@@ -0,0 +1,34 @@
+name: Coverage
+
+on:
+ pull_request:
+ push:
+ branches:
+ - master
+
+jobs:
+ cover:
+ name: Auto Codecov Coverage
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@master
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ override: true
+
+ - name: Run cargo-tarpaulin
+ uses: actions-rs/tarpaulin@v0.1
+ with:
+ version: '0.22.0'
+ args: --no-default-features --features runtime-async-std,cached,glob,ip,watcher,logging,incremental,explain
+
+ - name: Upload to codecov.io
+ uses: codecov/codecov-action@v1
+ with:
+ token: ${{secrets.CODECOV_TOKEN}}
diff --git a/casbin-rs/.github/workflows/pull_request.yml b/casbin-rs/.github/workflows/pull_request.yml
new file mode 100644
index 0000000..c829630
--- /dev/null
+++ b/casbin-rs/.github/workflows/pull_request.yml
@@ -0,0 +1,31 @@
+on:
+ pull_request_target:
+ branches: [ master ]
+name: Benchmarks
+jobs:
+ runBenchmark:
+ name: run benchmark
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ with:
+ ref: ${{github.event.pull_request.head.ref}}
+ repository: ${{github.event.pull_request.head.repo.full_name}}
+ - uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ profile: minimal
+ - name: Cache cargo
+ uses: actions/cache@v2.1.4
+ with:
+ path: |
+ target
+ ~/.cargo/git
+ ~/.cargo/registry
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+ - uses: smrpn/criterion-compare-action@move_to_actions
+ with:
+ cwd: benches
+ token: ${{ secrets.GITHUB_TOKEN }}
+
diff --git a/casbin-rs/.github/workflows/release.yml b/casbin-rs/.github/workflows/release.yml
new file mode 100644
index 0000000..007faf3
--- /dev/null
+++ b/casbin-rs/.github/workflows/release.yml
@@ -0,0 +1,45 @@
+name: Auto Release
+
+on:
+ push:
+ # Sequence of patterns matched against refs/tags
+ tags:
+ - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
+
+jobs:
+ release:
+ name: Auto Release by Tags
+ runs-on: ubuntu-18.04
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@master
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ default: true
+
+ - name: Cargo Login
+ uses: actions-rs/cargo@v1
+ with:
+ command: login
+ args: -- ${{ secrets.CARGO_TOKEN }}
+
+ - name: Cargo Publish
+ uses: actions-rs/cargo@v1
+ with:
+ command: publish
+
+ - name: GitHub Release
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
+ with:
+ tag_name: ${{ github.ref }}
+ release_name: Release ${{ github.ref }}
+ draft: false
+ prerelease: false
diff --git a/casbin-rs/.github/workflows/rustdoc.yml b/casbin-rs/.github/workflows/rustdoc.yml
new file mode 100644
index 0000000..234c4ad
--- /dev/null
+++ b/casbin-rs/.github/workflows/rustdoc.yml
@@ -0,0 +1,37 @@
+name: Deploy Docs to GitHub Pages
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ release:
+ name: GitHub Pages
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v1
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ profile: minimal
+ override: true
+ components: rustfmt, rust-src
+
+ - name: Build Documentation
+ uses: actions-rs/cargo@v1
+ with:
+ command: doc
+ args: --all --no-deps
+
+ - name: Deploy Documentation
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
+ publish_branch: gh-pages
+ publish_dir: ./target/doc
+ keep_files: true
diff --git a/casbin-rs/.gitignore b/casbin-rs/.gitignore
new file mode 100644
index 0000000..50be133
--- /dev/null
+++ b/casbin-rs/.gitignore
@@ -0,0 +1,23 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+
+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
+Cargo.lock
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+.vscode
+
+.idea/
+*.iml
+
+# bench
+output.txt
+before
+after
+
+# design
+design.txt
diff --git a/casbin-rs/.vimspector.json b/casbin-rs/.vimspector.json
new file mode 100644
index 0000000..4ca0788
--- /dev/null
+++ b/casbin-rs/.vimspector.json
@@ -0,0 +1,20 @@
+{
+ "configurations": {
+ "rust - launch": {
+ "adapter": "CodeLLDB",
+ "configuration": {
+ "type": "lldb",
+ "request": "launch",
+ "program": "${Executable}",
+ "args": ["*${Args}"],
+ "sourceLanguages": ["rust"]
+ },
+ "breakpoints": {
+ "exception": {
+ "cpp_throw": "Y",
+ "cpp_catch": "N"
+ }
+ }
+ }
+ }
+}
diff --git a/casbin-rs/CHANGELOG.md b/casbin-rs/CHANGELOG.md
new file mode 100644
index 0000000..ed0edb9
--- /dev/null
+++ b/casbin-rs/CHANGELOG.md
@@ -0,0 +1,498 @@
+# Changelog
+
+## [Unreleased](https://github.com/casbin/casbin-rs/tree/HEAD)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v2.0.5...HEAD)
+
+**Implemented enhancements:**
+
+- Support policy.csv comment or YAML file adapter [\#213](https://github.com/casbin/casbin-rs/issues/213)
+
+## [v2.0.5](https://github.com/casbin/casbin-rs/tree/v2.0.5) (2020-12-24)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v2.0.3...v2.0.5)
+
+**Implemented enhancements:**
+
+- Add actix-casbin-auth to Actix official middleware list [\#92](https://github.com/casbin/casbin-rs/issues/92)
+
+**Fixed bugs:**
+
+- not currently running on the Tokio runtime on tonic [\#221](https://github.com/casbin/casbin-rs/issues/221)
+- CSV loader deletes double quotes [\#214](https://github.com/casbin/casbin-rs/issues/214)
+
+**Closed issues:**
+
+- Broken enforce with json string in 2.0 [\#210](https://github.com/casbin/casbin-rs/issues/210)
+
+**Merged pull requests:**
+
+- revert tokio upgrade [\#223](https://github.com/casbin/casbin-rs/pull/223) ([GopherJ](https://github.com/GopherJ))
+- fix\(csv\): shouldn't delete inner double quotes [\#216](https://github.com/casbin/casbin-rs/pull/216) ([GopherJ](https://github.com/GopherJ))
+- feat: switch to lru [\#212](https://github.com/casbin/casbin-rs/pull/212) ([PsiACE](https://github.com/PsiACE))
+- upgrade versions of rhai & tokio [\#211](https://github.com/casbin/casbin-rs/pull/211) ([PsiACE](https://github.com/PsiACE))
+
+## [v2.0.3](https://github.com/casbin/casbin-rs/tree/v2.0.3) (2020-10-19)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v2.0.2...v2.0.3)
+
+## [v2.0.2](https://github.com/casbin/casbin-rs/tree/v2.0.2) (2020-09-19)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v2.0.1...v2.0.2)
+
+**Closed issues:**
+
+- setup clog-cli CI [\#205](https://github.com/casbin/casbin-rs/issues/205)
+
+**Merged pull requests:**
+
+- fix: wasm checking in CI [\#207](https://github.com/casbin/casbin-rs/pull/207) ([GopherJ](https://github.com/GopherJ))
+- Automatic change log generation. [\#206](https://github.com/casbin/casbin-rs/pull/206) ([PsiACE](https://github.com/PsiACE))
+
+## [v2.0.1](https://github.com/casbin/casbin-rs/tree/v2.0.1) (2020-08-30)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v2.0.0...v2.0.1)
+
+## [v2.0.0](https://github.com/casbin/casbin-rs/tree/v2.0.0) (2020-08-30)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v1.1.3...v2.0.0)
+
+**Implemented enhancements:**
+
+- support serializable struct to be passed as ABAC parameters [\#199](https://github.com/casbin/casbin-rs/issues/199)
+- pattern support in role manager [\#192](https://github.com/casbin/casbin-rs/issues/192)
+
+**Merged pull requests:**
+
+- Release v2.0.0 [\#204](https://github.com/casbin/casbin-rs/pull/204) ([GopherJ](https://github.com/GopherJ))
+
+## [v1.1.3](https://github.com/casbin/casbin-rs/tree/v1.1.3) (2020-08-26)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v1.1.2...v1.1.3)
+
+**Implemented enhancements:**
+
+- structured logging [\#190](https://github.com/casbin/casbin-rs/issues/190)
+- add `EnforcerBuilder` type? [\#174](https://github.com/casbin/casbin-rs/issues/174)
+- casbin cache [\#171](https://github.com/casbin/casbin-rs/issues/171)
+- Make a Casbin middleware for Rocket.rs [\#93](https://github.com/casbin/casbin-rs/issues/93)
+- GSOC: Shared Redis TTL cache [\#83](https://github.com/casbin/casbin-rs/issues/83)
+
+## [v1.1.2](https://github.com/casbin/casbin-rs/tree/v1.1.2) (2020-07-20)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v1.1.1...v1.1.2)
+
+**Implemented enhancements:**
+
+- clear\_policy in adapter and enforcer [\#193](https://github.com/casbin/casbin-rs/issues/193)
+- Improve casbin-rs bench [\#109](https://github.com/casbin/casbin-rs/issues/109)
+
+**Fixed bugs:**
+
+- allowing the parsing of policy file to deal with commas inside columns [\#184](https://github.com/casbin/casbin-rs/issues/184)
+
+**Closed issues:**
+
+- re-exports rhai & add IEnforcer [\#197](https://github.com/casbin/casbin-rs/issues/197)
+- Filter should work with dynamic values \(&str instead of &'static str\) [\#195](https://github.com/casbin/casbin-rs/issues/195)
+
+**Merged pull requests:**
+
+- feat: re-exports rhai & add IEnforcer && bump version [\#198](https://github.com/casbin/casbin-rs/pull/198) ([GopherJ](https://github.com/GopherJ))
+- Change Filter definition to support dynamic filter. [\#196](https://github.com/casbin/casbin-rs/pull/196) ([bodymindarts](https://github.com/bodymindarts))
+
+## [v1.1.1](https://github.com/casbin/casbin-rs/tree/v1.1.1) (2020-07-18)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v1.1.0...v1.1.1)
+
+**Merged pull requests:**
+
+- Fix unhandled dquote [\#188](https://github.com/casbin/casbin-rs/pull/188) ([GopherJ](https://github.com/GopherJ))
+- fix: add casbin-cpp to supported languages. [\#185](https://github.com/casbin/casbin-rs/pull/185) ([divy9881](https://github.com/divy9881))
+
+## [v1.1.0](https://github.com/casbin/casbin-rs/tree/v1.1.0) (2020-07-14)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v1.0.0...v1.1.0)
+
+**Implemented enhancements:**
+
+- add cache for g function [\#175](https://github.com/casbin/casbin-rs/issues/175)
+
+**Closed issues:**
+
+- test issue-label-bot [\#178](https://github.com/casbin/casbin-rs/issues/178)
+- setup issue-label-bot [\#177](https://github.com/casbin/casbin-rs/issues/177)
+- GSOC & Non-GSOC: gitter team link [\#80](https://github.com/casbin/casbin-rs/issues/80)
+
+**Merged pull requests:**
+
+- Cache g [\#183](https://github.com/casbin/casbin-rs/pull/183) ([GopherJ](https://github.com/GopherJ))
+- Update rhai requirement from 0.16.1 to 0.17.0 [\#182](https://github.com/casbin/casbin-rs/pull/182) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
+- Update rhai requirement from 0.15.1 to 0.16.1 [\#179](https://github.com/casbin/casbin-rs/pull/179) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
+
+## [v1.0.0](https://github.com/casbin/casbin-rs/tree/v1.0.0) (2020-06-18)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.9.3...v1.0.0)
+
+**Implemented enhancements:**
+
+- Make effector a stream to optimize enforcing speed by quick return [\#125](https://github.com/casbin/casbin-rs/issues/125)
+- split code into multiple features [\#97](https://github.com/casbin/casbin-rs/issues/97)
+- automate package release [\#90](https://github.com/casbin/casbin-rs/issues/90)
+
+**Closed issues:**
+
+- casbin cache [\#170](https://github.com/casbin/casbin-rs/issues/170)
+- Explain enforcement by informing matched rules [\#141](https://github.com/casbin/casbin-rs/issues/141)
+- Dont rebuild all role links [\#138](https://github.com/casbin/casbin-rs/issues/138)
+
+**Merged pull requests:**
+
+- Cleanup [\#173](https://github.com/casbin/casbin-rs/pull/173) ([GopherJ](https://github.com/GopherJ))
+- add os matrix for CI [\#167](https://github.com/casbin/casbin-rs/pull/167) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.9.3](https://github.com/casbin/casbin-rs/tree/v0.9.3) (2020-05-25)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.9.2...v0.9.3)
+
+**Closed issues:**
+
+- Add AuthN & AuthZ sections for Rust in our awesome-auth list [\#160](https://github.com/casbin/casbin-rs/issues/160)
+
+**Merged pull requests:**
+
+- Change enforce and enforce\_mut to non-async. [\#166](https://github.com/casbin/casbin-rs/pull/166) ([schungx](https://github.com/schungx))
+- remove explain relevant code when feature has been disabled [\#164](https://github.com/casbin/casbin-rs/pull/164) ([GopherJ](https://github.com/GopherJ))
+- Speed improvements [\#163](https://github.com/casbin/casbin-rs/pull/163) ([schungx](https://github.com/schungx))
+- Use eval\_expression to restrict to expressions only. [\#161](https://github.com/casbin/casbin-rs/pull/161) ([schungx](https://github.com/schungx))
+
+## [v0.9.2](https://github.com/casbin/casbin-rs/tree/v0.9.2) (2020-05-13)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.9.1...v0.9.2)
+
+## [v0.9.1](https://github.com/casbin/casbin-rs/tree/v0.9.1) (2020-05-13)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.9.0...v0.9.1)
+
+## [v0.9.0](https://github.com/casbin/casbin-rs/tree/v0.9.0) (2020-05-13)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.8.7...v0.9.0)
+
+## [v0.8.7](https://github.com/casbin/casbin-rs/tree/v0.8.7) (2020-05-13)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.8.6...v0.8.7)
+
+**Merged pull requests:**
+
+- Fix/cargo toml version [\#156](https://github.com/casbin/casbin-rs/pull/156) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.8.6](https://github.com/casbin/casbin-rs/tree/v0.8.6) (2020-05-13)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.8.5...v0.8.6)
+
+## [v0.8.5](https://github.com/casbin/casbin-rs/tree/v0.8.5) (2020-05-13)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.8.4...v0.8.5)
+
+**Merged pull requests:**
+
+- fix: exec module not found [\#155](https://github.com/casbin/casbin-rs/pull/155) ([GopherJ](https://github.com/GopherJ))
+- fix: semantic-release/exec-not-found [\#154](https://github.com/casbin/casbin-rs/pull/154) ([GopherJ](https://github.com/GopherJ))
+- fix: semantic release [\#153](https://github.com/casbin/casbin-rs/pull/153) ([GopherJ](https://github.com/GopherJ))
+- add status log [\#152](https://github.com/casbin/casbin-rs/pull/152) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.8.4](https://github.com/casbin/casbin-rs/tree/v0.8.4) (2020-05-12)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.8.3...v0.8.4)
+
+**Merged pull requests:**
+
+- fix cached enforcer mgmt event log [\#151](https://github.com/casbin/casbin-rs/pull/151) ([GopherJ](https://github.com/GopherJ))
+- general improving [\#150](https://github.com/casbin/casbin-rs/pull/150) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.8.3](https://github.com/casbin/casbin-rs/tree/v0.8.3) (2020-05-12)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.8.2...v0.8.3)
+
+## [v0.8.2](https://github.com/casbin/casbin-rs/tree/v0.8.2) (2020-05-12)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.8.1...v0.8.2)
+
+**Merged pull requests:**
+
+- Explain [\#149](https://github.com/casbin/casbin-rs/pull/149) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.8.1](https://github.com/casbin/casbin-rs/tree/v0.8.1) (2020-05-12)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.8.0...v0.8.1)
+
+**Merged pull requests:**
+
+- Revert "Effector stream" [\#147](https://github.com/casbin/casbin-rs/pull/147) ([GopherJ](https://github.com/GopherJ))
+- Revert "Fix: CI" [\#146](https://github.com/casbin/casbin-rs/pull/146) ([GopherJ](https://github.com/GopherJ))
+- finish effector stream [\#145](https://github.com/casbin/casbin-rs/pull/145) ([GopherJ](https://github.com/GopherJ))
+- Fix: CI [\#143](https://github.com/casbin/casbin-rs/pull/143) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.8.0](https://github.com/casbin/casbin-rs/tree/v0.8.0) (2020-05-11)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.7.6...v0.8.0)
+
+**Merged pull requests:**
+
+- Effector stream [\#142](https://github.com/casbin/casbin-rs/pull/142) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.7.6](https://github.com/casbin/casbin-rs/tree/v0.7.6) (2020-05-11)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.7.5...v0.7.6)
+
+## [v0.7.5](https://github.com/casbin/casbin-rs/tree/v0.7.5) (2020-05-11)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.7.4...v0.7.5)
+
+**Merged pull requests:**
+
+- Move build role links to internal [\#140](https://github.com/casbin/casbin-rs/pull/140) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.7.4](https://github.com/casbin/casbin-rs/tree/v0.7.4) (2020-05-10)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.7.2...v0.7.4)
+
+**Merged pull requests:**
+
+- Incremental build rolelinks [\#139](https://github.com/casbin/casbin-rs/pull/139) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.7.2](https://github.com/casbin/casbin-rs/tree/v0.7.2) (2020-05-10)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.7.1...v0.7.2)
+
+**Merged pull requests:**
+
+- Split code into features [\#137](https://github.com/casbin/casbin-rs/pull/137) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.7.1](https://github.com/casbin/casbin-rs/tree/v0.7.1) (2020-05-08)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.7.0...v0.7.1)
+
+**Merged pull requests:**
+
+- Fix: ClearCache log wasn't trigger [\#135](https://github.com/casbin/casbin-rs/pull/135) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.7.0](https://github.com/casbin/casbin-rs/tree/v0.7.0) (2020-05-08)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.6.2...v0.7.0)
+
+**Implemented enhancements:**
+
+- switch to smol when it's ready [\#130](https://github.com/casbin/casbin-rs/issues/130)
+
+**Closed issues:**
+
+- GSOC: logger system [\#84](https://github.com/casbin/casbin-rs/issues/84)
+- GSOC: actix actor,actix middleware [\#82](https://github.com/casbin/casbin-rs/issues/82)
+
+**Merged pull requests:**
+
+- Simple logger [\#134](https://github.com/casbin/casbin-rs/pull/134) ([GopherJ](https://github.com/GopherJ))
+- remove circular link caused by pattern matching func [\#133](https://github.com/casbin/casbin-rs/pull/133) ([GopherJ](https://github.com/GopherJ))
+- make enforce immutable and add enforce\_mut [\#132](https://github.com/casbin/casbin-rs/pull/132) ([GopherJ](https://github.com/GopherJ))
+- fix stackoverflow when circular link detected [\#131](https://github.com/casbin/casbin-rs/pull/131) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.6.2](https://github.com/casbin/casbin-rs/tree/v0.6.2) (2020-05-01)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.6.1...v0.6.2)
+
+**Merged pull requests:**
+
+- fix get\_implicit\_users\_for\_permission api [\#129](https://github.com/casbin/casbin-rs/pull/129) ([GopherJ](https://github.com/GopherJ))
+- upgrade rhai to 0.13.0 [\#128](https://github.com/casbin/casbin-rs/pull/128) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.6.1](https://github.com/casbin/casbin-rs/tree/v0.6.1) (2020-04-25)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.6.0...v0.6.1)
+
+**Merged pull requests:**
+
+- add simple quick return [\#126](https://github.com/casbin/casbin-rs/pull/126) ([GopherJ](https://github.com/GopherJ))
+- add get\_all\_policy, get\_all\_grouping\_policy [\#124](https://github.com/casbin/casbin-rs/pull/124) ([GopherJ](https://github.com/GopherJ))
+- POC try scaling abac rules [\#121](https://github.com/casbin/casbin-rs/pull/121) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.6.0](https://github.com/casbin/casbin-rs/tree/v0.6.0) (2020-04-18)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.5.2...v0.6.0)
+
+**Closed issues:**
+
+- Implement FilteredAdapter [\#79](https://github.com/casbin/casbin-rs/issues/79)
+- Roadmap for casbin-rs@1.0.0 [\#5](https://github.com/casbin/casbin-rs/issues/5)
+
+**Merged pull requests:**
+
+- Stable filtered adapter [\#120](https://github.com/casbin/casbin-rs/pull/120) ([GopherJ](https://github.com/GopherJ))
+- Share engine [\#117](https://github.com/casbin/casbin-rs/pull/117) ([GopherJ](https://github.com/GopherJ))
+- deactivate script functon, extra i8, i16...i128, and float math [\#116](https://github.com/casbin/casbin-rs/pull/116) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.5.2](https://github.com/casbin/casbin-rs/tree/v0.5.2) (2020-04-15)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.5.1...v0.5.2)
+
+**Closed issues:**
+
+- add github-action-benchmark [\#104](https://github.com/casbin/casbin-rs/issues/104)
+
+**Merged pull requests:**
+
+- use raw engine && bump version [\#115](https://github.com/casbin/casbin-rs/pull/115) ([GopherJ](https://github.com/GopherJ))
+- add remove\_filtered\_policy details && enable\_auto\_notify\_watcher func… [\#113](https://github.com/casbin/casbin-rs/pull/113) ([GopherJ](https://github.com/GopherJ))
+- Update rhai requirement from 0.11.1 to 0.12.0 [\#111](https://github.com/casbin/casbin-rs/pull/111) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
+- Improve/benchmark [\#110](https://github.com/casbin/casbin-rs/pull/110) ([GopherJ](https://github.com/GopherJ))
+- add benchmarks [\#108](https://github.com/casbin/casbin-rs/pull/108) ([GopherJ](https://github.com/GopherJ))
+- Add workflow for benchmark. [\#107](https://github.com/casbin/casbin-rs/pull/107) ([PsiACE](https://github.com/PsiACE))
+
+## [v0.5.1](https://github.com/casbin/casbin-rs/tree/v0.5.1) (2020-04-12)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.5.0...v0.5.1)
+
+**Closed issues:**
+
+- considering trait inheritance [\#99](https://github.com/casbin/casbin-rs/issues/99)
+- Implement ABAC model [\#78](https://github.com/casbin/casbin-rs/issues/78)
+
+**Merged pull requests:**
+
+- Improve/watcher [\#106](https://github.com/casbin/casbin-rs/pull/106) ([GopherJ](https://github.com/GopherJ))
+- Implement ABAC [\#102](https://github.com/casbin/casbin-rs/pull/102) ([xcaptain](https://github.com/xcaptain))
+- use lazy static to avoid re-complilation of regex [\#96](https://github.com/casbin/casbin-rs/pull/96) ([DevinR528](https://github.com/DevinR528))
+
+## [v0.5.0](https://github.com/casbin/casbin-rs/tree/v0.5.0) (2020-04-10)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.4.4...v0.5.0)
+
+**Closed issues:**
+
+- GSOC: tokio runtime, fully async adapter [\#81](https://github.com/casbin/casbin-rs/issues/81)
+
+**Merged pull requests:**
+
+- Improve/inheritance [\#103](https://github.com/casbin/casbin-rs/pull/103) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.4.4](https://github.com/casbin/casbin-rs/tree/v0.4.4) (2020-04-08)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.4.3...v0.4.4)
+
+**Closed issues:**
+
+- support pattern in role manager [\#94](https://github.com/casbin/casbin-rs/issues/94)
+- improve error handling [\#85](https://github.com/casbin/casbin-rs/issues/85)
+- Missing async support \(async-std + async adapter\) [\#43](https://github.com/casbin/casbin-rs/issues/43)
+
+**Merged pull requests:**
+
+- remove inMatch because rhai starts to support in operator since 0.11.1 [\#98](https://github.com/casbin/casbin-rs/pull/98) ([GopherJ](https://github.com/GopherJ))
+- add support of pattern [\#95](https://github.com/casbin/casbin-rs/pull/95) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.4.3](https://github.com/casbin/casbin-rs/tree/v0.4.3) (2020-04-06)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.4.2...v0.4.3)
+
+**Closed issues:**
+
+- implementation of `TryIntoAdapter` and `TryIntoModel` trait [\#70](https://github.com/casbin/casbin-rs/issues/70)
+- Missing logger support [\#51](https://github.com/casbin/casbin-rs/issues/51)
+
+**Merged pull requests:**
+
+- Improve/error handling [\#91](https://github.com/casbin/casbin-rs/pull/91) ([GopherJ](https://github.com/GopherJ))
+- upgrade rhai to 0.11.1 to make Error Send, Sync [\#89](https://github.com/casbin/casbin-rs/pull/89) ([GopherJ](https://github.com/GopherJ))
+- remove clones and string allocs, use macro for model retreval [\#87](https://github.com/casbin/casbin-rs/pull/87) ([DevinR528](https://github.com/DevinR528))
+
+## [v0.4.2](https://github.com/casbin/casbin-rs/tree/v0.4.2) (2020-04-05)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/v0.4.1...v0.4.2)
+
+**Closed issues:**
+
+- Missing watcher support \(with a proper lock\) [\#46](https://github.com/casbin/casbin-rs/issues/46)
+
+**Merged pull requests:**
+
+- add TryIntoAdapter & TryIntoModel [\#86](https://github.com/casbin/casbin-rs/pull/86) ([GopherJ](https://github.com/GopherJ))
+
+## [v0.4.1](https://github.com/casbin/casbin-rs/tree/v0.4.1) (2020-04-05)
+
+[Full Changelog](https://github.com/casbin/casbin-rs/compare/9e7ebbddb5b92b6aad27412fadd82115793ea07a...v0.4.1)
+
+**Implemented enhancements:**
+
+- Database transactions similar api `addPolicies`, `removePolicies` [\#55](https://github.com/casbin/casbin-rs/issues/55)
+- Diesel adapter is calling for contribution! [\#35](https://github.com/casbin/casbin-rs/issues/35)
+- Add a new from file api for model [\#32](https://github.com/casbin/casbin-rs/issues/32)
+- Consider default implement some APIs for Enforcer [\#31](https://github.com/casbin/casbin-rs/issues/31)
+- Rename the package name [\#30](https://github.com/casbin/casbin-rs/issues/30)
+- Refactor API structure [\#29](https://github.com/casbin/casbin-rs/issues/29)
+- Proper error handling \(eliminate unwrap calls\) [\#24](https://github.com/casbin/casbin-rs/issues/24)
+- Role inheritance problem in rbac.rs [\#7](https://github.com/casbin/casbin-rs/issues/7)
+
+**Fixed bugs:**
+
+- Relation to Devolutions/casbin-rs? [\#23](https://github.com/casbin/casbin-rs/issues/23)
+
+**Closed issues:**
+
+- covert rule: Vec\<&str\> to rule: Vec\<String\> [\#73](https://github.com/casbin/casbin-rs/issues/73)
+- Add Casbin-RS to our feature set table [\#71](https://github.com/casbin/casbin-rs/issues/71)
+- Consider using the subslice patterns [\#67](https://github.com/casbin/casbin-rs/issues/67)
+- Add unit tests for `addPolicies` and `removePolicies` [\#65](https://github.com/casbin/casbin-rs/issues/65)
+- Add an example to Actix web examples repo [\#63](https://github.com/casbin/casbin-rs/issues/63)
+- Automatically publish package to crate.io when a new git tag is created [\#58](https://github.com/casbin/casbin-rs/issues/58)
+- Add crates badge that linked to: https://crates.io/crates/casbin [\#54](https://github.com/casbin/casbin-rs/issues/54)
+- Missing cache support [\#45](https://github.com/casbin/casbin-rs/issues/45)
+- Use github actions to auto generate rust doc [\#33](https://github.com/casbin/casbin-rs/issues/33)
+- Add "Installation" and "Get started" in README. [\#27](https://github.com/casbin/casbin-rs/issues/27)
+- Should we stick with rust clippy [\#15](https://github.com/casbin/casbin-rs/issues/15)
+
+**Merged pull requests:**
+
+- add critcmp & insert into adapter before model [\#77](https://github.com/casbin/casbin-rs/pull/77) ([GopherJ](https://github.com/GopherJ))
+- add glob\_match, make enforce function take only reference [\#76](https://github.com/casbin/casbin-rs/pull/76) ([GopherJ](https://github.com/GopherJ))
+- Refactor: modify Vec\<&str\> to Vec\<String\> \(\#73\) [\#75](https://github.com/casbin/casbin-rs/pull/75) ([hackerchai](https://github.com/hackerchai))
+- Benchmark [\#68](https://github.com/casbin/casbin-rs/pull/68) ([DevinR528](https://github.com/DevinR528))
+- Added Unit Tests for `addPolicies` and `removePolicies` [\#66](https://github.com/casbin/casbin-rs/pull/66) ([drholmie](https://github.com/drholmie))
+- Create release.yml [\#62](https://github.com/casbin/casbin-rs/pull/62) ([PsiACE](https://github.com/PsiACE))
+- Update rustdoc workflow. [\#61](https://github.com/casbin/casbin-rs/pull/61) ([PsiACE](https://github.com/PsiACE))
+- fix cache tests for runtime-tokio [\#60](https://github.com/casbin/casbin-rs/pull/60) ([GopherJ](https://github.com/GopherJ))
+- Feature/model trait and add policies [\#59](https://github.com/casbin/casbin-rs/pull/59) ([GopherJ](https://github.com/GopherJ))
+- Build docs [\#57](https://github.com/casbin/casbin-rs/pull/57) ([PsiACE](https://github.com/PsiACE))
+- Add badges. [\#56](https://github.com/casbin/casbin-rs/pull/56) ([PsiACE](https://github.com/PsiACE))
+- add async/await support [\#53](https://github.com/casbin/casbin-rs/pull/53) ([GopherJ](https://github.com/GopherJ))
+- Add some keywords and readme and also optimize --release compiles [\#52](https://github.com/casbin/casbin-rs/pull/52) ([omid](https://github.com/omid))
+- Feature/watcher and cache [\#50](https://github.com/casbin/casbin-rs/pull/50) ([GopherJ](https://github.com/GopherJ))
+- fix more rbac apis \(add missing domain parameter\) & add prelude module [\#49](https://github.com/casbin/casbin-rs/pull/49) ([GopherJ](https://github.com/GopherJ))
+- fix DefaultRoleManager::get\_users && add domain paramter to get\_roles… [\#48](https://github.com/casbin/casbin-rs/pull/48) ([GopherJ](https://github.com/GopherJ))
+- using IndexSet to store policy in assertion [\#47](https://github.com/casbin/casbin-rs/pull/47) ([GopherJ](https://github.com/GopherJ))
+- replace Box by Arc\<RwLock\> && add get\_model, get\_mut\_model for Model.… [\#44](https://github.com/casbin/casbin-rs/pull/44) ([GopherJ](https://github.com/GopherJ))
+- Improve/add function and remove gg2 gg3 [\#42](https://github.com/casbin/casbin-rs/pull/42) ([GopherJ](https://github.com/GopherJ))
+- fix typo in load\_assertion [\#41](https://github.com/casbin/casbin-rs/pull/41) ([zupzup](https://github.com/zupzup))
+- add in operator [\#40](https://github.com/casbin/casbin-rs/pull/40) ([GopherJ](https://github.com/GopherJ))
+- apply new error type to enforcer,model,config... [\#39](https://github.com/casbin/casbin-rs/pull/39) ([GopherJ](https://github.com/GopherJ))
+- remove Send & Sync marker to make it easier to construct error types [\#37](https://github.com/casbin/casbin-rs/pull/37) ([GopherJ](https://github.com/GopherJ))
+- make CasbinResult public [\#36](https://github.com/casbin/casbin-rs/pull/36) ([GopherJ](https://github.com/GopherJ))
+- update some metadata [\#34](https://github.com/casbin/casbin-rs/pull/34) ([xcaptain](https://github.com/xcaptain))
+- Add some get started doc [\#28](https://github.com/casbin/casbin-rs/pull/28) ([xcaptain](https://github.com/xcaptain))
+- Add multi-thread support [\#26](https://github.com/casbin/casbin-rs/pull/26) ([xcaptain](https://github.com/xcaptain))
+- make g function support 2 or 3 parameters [\#22](https://github.com/casbin/casbin-rs/pull/22) ([xcaptain](https://github.com/xcaptain))
+- Optimize role manager and add more rbac apis [\#20](https://github.com/casbin/casbin-rs/pull/20) ([xcaptain](https://github.com/xcaptain))
+- add error handling [\#19](https://github.com/casbin/casbin-rs/pull/19) ([xcaptain](https://github.com/xcaptain))
+- add auto\_save and auto\_build\_role\_links option in enforcer [\#17](https://github.com/casbin/casbin-rs/pull/17) ([xcaptain](https://github.com/xcaptain))
+- resolve clippy warnings and add clippy to travis [\#16](https://github.com/casbin/casbin-rs/pull/16) ([GopherJ](https://github.com/GopherJ))
+- Feature/enforcer rbac api [\#14](https://github.com/casbin/casbin-rs/pull/14) ([xcaptain](https://github.com/xcaptain))
+- add unit tests for model and implement ip match [\#13](https://github.com/casbin/casbin-rs/pull/13) ([GopherJ](https://github.com/GopherJ))
+- use Rc\<RefCell\<Role\>\> to store roles [\#9](https://github.com/casbin/casbin-rs/pull/9) ([xcaptain](https://github.com/xcaptain))
+- add travis-ci and codecov badges [\#6](https://github.com/casbin/casbin-rs/pull/6) ([xcaptain](https://github.com/xcaptain))
+- \[add\]: load\_model && load\_model\_from\_text [\#4](https://github.com/casbin/casbin-rs/pull/4) ([GopherJ](https://github.com/GopherJ))
+- \[add\]: support for parsing config files with multiple lines [\#3](https://github.com/casbin/casbin-rs/pull/3) ([GopherJ](https://github.com/GopherJ))
+- add a basic parsing functions for ini config file [\#2](https://github.com/casbin/casbin-rs/pull/2) ([xcaptain](https://github.com/xcaptain))
+- add basic matching function for casbin [\#1](https://github.com/casbin/casbin-rs/pull/1) ([xcaptain](https://github.com/xcaptain))
+
+
+
+\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
diff --git a/casbin-rs/Cargo.toml b/casbin-rs/Cargo.toml
new file mode 100644
index 0000000..90af46d
--- /dev/null
+++ b/casbin-rs/Cargo.toml
@@ -0,0 +1,98 @@
+[package]
+authors = ["Joey <joey.xf@gmail.com>", "Cheng JIANG <jiang.cheng@vip.163.com>"]
+description = "An authorization library that supports access control models like ACL, RBAC, ABAC."
+edition = "2021"
+homepage = "https://casbin.org/"
+keywords = ["auth", "authorization", "rbac", "acl", "abac"]
+license = "Apache-2.0"
+name = "casbin"
+readme = "README.md"
+repository = "https://github.com/casbin/casbin-rs"
+version = "2.0.9"
+
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+
+[dependencies]
+async-std = { version = "1.10.0", optional = true }
+
+async-trait = "0.1.52"
+globset = { version = "0.4.8", optional = true }
+ritelinked = { version = "0.3.2", default-features = false, features = [
+ "ahash",
+ "inline-more",
+] }
+ip_network = { version = "0.4.1", optional = true }
+once_cell = "1.9.0"
+mini-moka = { version = "0.10", optional = true }
+parking_lot = "0.12.0"
+regex = "1.5.4"
+petgraph = "0.6"
+fixedbitset = "0.4"
+rhai = { version = "1.14.0", features = [
+ "sync",
+ "only_i32",
+ "no_function",
+ "no_float",
+ "no_optimize",
+ "no_module",
+ "serde",
+ "unchecked",
+] }
+serde = "1.0.136"
+slog = { version = "2.7.0", optional = true }
+slog-async = { version = "2.7.0", optional = true }
+slog-term = { version = "2.9.0", optional = true }
+thiserror = "1.0.30"
+tokio = { version = "1.17.0", optional = true, default-features = false }
+tokio-stream = { version = "0.1.8", optional = true, default-features = false }
+
+[features]
+default = ["runtime-tokio", "incremental"]
+
+amortized = ["ritelinked/ahash-amortized", "ritelinked/inline-more-amortized"]
+cached = ["mini-moka"]
+explain = []
+glob = ["globset"]
+incremental = []
+ip = ["ip_network"]
+logging = ["slog", "slog-term", "slog-async"]
+runtime-async-std = ["async-std"]
+runtime-tokio = ["tokio/fs", "tokio/io-util"]
+runtime-teaclave = ["tokio/io-util"]
+watcher = []
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+getrandom = { version = "0.2.6", features = ["js"] }
+
+[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
+async-std = { version = "1.9.0", features = ["attributes"] }
+serde = { version = "1.0.136", features = ["derive"] }
+
+[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
+async-std = { version = "1.10.0", features = ["attributes"] }
+serde = { version = "1.0.136", features = ["derive"] }
+tokio = { version = "1.17.0", features = ["full"] }
+
+[profile.release]
+codegen-units = 1
+lto = true
+opt-level = 3
+
+[profile.dev]
+split-debuginfo = "unpacked"
+
+[profile.bench]
+lto = true
+opt-level = 3
+
+[[bench]]
+name = "benchmark"
+harness = false
+
+[dev-dependencies]
+criterion = { version = "0.3.5", features = ["html_reports"] }
+
+[lib]
+bench = false
diff --git a/casbin-rs/LICENSE b/casbin-rs/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/casbin-rs/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/casbin-rs/README.md b/casbin-rs/README.md
new file mode 100644
index 0000000..ac30e08
--- /dev/null
+++ b/casbin-rs/README.md
@@ -0,0 +1,257 @@
+# casbin-rs
+
+[](https://github.com/casbin/casbin-rs/commits/master)
+[](https://crates.io/crates/casbin)
+[](https://crates.io/crates/casbin)
+[](https://docs.rs/casbin)
+[](https://github.com/casbin/casbin-rs/actions)
+[](https://codecov.io/gh/casbin/casbin-rs)
+[](https://gitter.im/casbin/lobby)
+[](https://forum.casbin.org/)
+
+💖 [**Looking for an open-source identity and access management solution like Okta, Auth0, Keycloak ? Learn more about: Casdoor**](https://casdoor.org/)
+
+<a href="https://casdoor.org/"><img src="https://user-images.githubusercontent.com/3787410/147868267-6ac74908-5654-4f9c-ac79-8852af9ff925.png" alt="casdoor" style="width: 50%; height: 50%"/></a>
+
+**News**: still worry about how to write the correct Casbin policy? ``Casbin online editor`` is coming to help! Try it at: https://casbin.org/editor/
+
+
+
+**Casbin-RS** is a powerful and efficient open-source access control library for Rust projects. It provides support for enforcing authorization based on various [access control models](https://en.wikipedia.org/wiki/Computer_security_model).
+
+## All the languages supported by Casbin:
+
+[](https://github.com/casbin/casbin) | [](https://github.com/casbin/jcasbin) | [](https://github.com/casbin/node-casbin) | [](https://github.com/php-casbin/php-casbin)
+----|----|----|----
+[Casbin](https://github.com/casbin/casbin) | [jCasbin](https://github.com/casbin/jcasbin) | [node-Casbin](https://github.com/casbin/node-casbin) | [PHP-Casbin](https://github.com/php-casbin/php-casbin)
+production-ready | production-ready | production-ready | production-ready
+
+[](https://github.com/casbin/pycasbin) | [](https://github.com/casbin-net/Casbin.NET) | [](https://github.com/casbin/casbin-cpp) | [](https://github.com/casbin/casbin-rs)
+----|----|----|----
+[PyCasbin](https://github.com/casbin/pycasbin) | [Casbin.NET](https://github.com/casbin-net/Casbin.NET) | [Casbin-CPP](https://github.com/casbin/casbin-cpp) | [Casbin-RS](https://github.com/casbin/casbin-rs)
+production-ready | production-ready | beta-test | production-ready
+
+## Installation
+
+Add this package to `Cargo.toml` of your project. (Check https://crates.io/crates/casbin for right version)
+
+```toml
+[dependencies]
+casbin = { version = "2.0.9", default-features = false, features = ["runtime-async-std", "logging", "incremental"] }
+tokio = { version = "1.10.0", features = ["fs", "io-util"] }
+```
+
+**Warning**: `tokio v1.0` or later is supported from `casbin v2.0.6`, we recommend that you upgrade the relevant components to ensure that they work properly. The last version that supports `tokio v0.2` is `casbin v2.0.5` , you can choose according to your needs.
+
+## Get started
+
+1. New a Casbin enforcer with a model file and a policy file:
+
+```rust
+
+use casbin::prelude::*;
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ let mut e = Enforcer::new("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv").await?;
+ e.enable_log(true);
+
+ e.enforce(("alice", "domain1", "data1", "read"))?;
+ Ok(())
+}
+```
+
+2. Add an enforcement hook into your code right before the access happens:
+
+ ```rust
+ let sub = "alice"; // the user that wants to access a resource.
+ let obj = "data1"; // the resource that is going to be accessed.
+ let act = "read"; // the operation that the user performs on the resource.
+
+ if let Ok(authorized) = e.enforce((sub, obj, act)) {
+ if authorized {
+ // permit alice to read data1
+ } else {
+ // deny the request
+ }
+ } else {
+ // error occurs
+ }
+ ```
+💡 Please note that the Enforcer instance is not thread-safe, so in order to use it
+in a environment where multiple threads might access it, you have to protect it
+using an RwLock like so: `let e = Arc::new(RwLock::new(e));`.
+
+## Table of contents
+
+- [Supported models](#supported-models)
+- [How it works?](#how-it-works)
+- [Features](#features)
+- [Documentation](#documentation)
+- [Online editor](#online-editor)
+- [Tutorials](#tutorials)
+- [Policy management](#policy-management)
+- [Policy persistence](#policy-persistence)
+- [Role manager](#role-manager)
+- [Examples](#examples)
+- [Middlewares](#middlewares)
+- [Our adopters](#our-adopters)
+
+## Supported models
+
+1. [**ACL (Access Control List)**](https://en.wikipedia.org/wiki/Access_control_list)
+2. **ACL with [superuser](https://en.wikipedia.org/wiki/Superuser)**
+3. **ACL without users**: especially useful for systems that don't have authentication or user log-ins.
+4. **ACL without resources**: some scenarios may target for a type of resources instead of an individual resource by using permissions like ``write-article``, ``read-log``. It doesn't control the access to a specific article or log.
+5. **[RBAC (Role-Based Access Control)](https://en.wikipedia.org/wiki/Role-based_access_control)**
+6. **RBAC with resource roles**: both users and resources can have roles (or groups) at the same time.
+7. **RBAC with domains/tenants**: users can have different role sets for different domains/tenants.
+8. **[ABAC (Attribute-Based Access Control)](https://en.wikipedia.org/wiki/Attribute-Based_Access_Control)**: syntax sugar like ``resource.Owner`` can be used to get the attribute for a resource.
+9. **[RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer)**: supports paths like ``/res/*``, ``/res/:id`` and HTTP methods like ``GET``, ``POST``, ``PUT``, ``DELETE``.
+10. **Deny-override**: both allow and deny authorizations are supported, deny overrides the allow.
+11. **Priority**: the policy rules can be prioritized like firewall rules.
+
+## How it works?
+
+In casbin-rs, an access control model is abstracted into a CONF file based on the **PERM metamodel (Policy, Effect, Request, Matchers)**. So switching or upgrading the authorization mechanism for a project is just as simple as modifying a configuration. You can customize your own access control model by combining the available models. For example, you can get RBAC roles and ABAC attributes together inside one model and share one set of policy rules.
+
+The most basic and simplest model in casbin-rs is ACL. ACL's model CONF is:
+
+```ini
+# Request definition
+[request_definition]
+r = sub, obj, act
+
+# Policy definition
+[policy_definition]
+p = sub, obj, act
+
+# Policy effect
+[policy_effect]
+e = some(where (p.eft == allow))
+
+# Matchers
+[matchers]
+m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
+```
+
+An example policy for ACL model is like:
+
+```
+p, alice, data1, read
+p, bob, data2, write
+```
+
+It means:
+
+- alice can read data1
+- bob can write data2
+
+## Features
+
+What casbin-rs does:
+
+1. enforce the policy in the classic ``{subject, object, action}`` form or a customized form as you defined, both allow and deny authorizations are supported.
+2. handle the storage of the access control model and its policy.
+3. manage the role-user mappings and role-role mappings (aka role hierarchy in RBAC).
+4. support built-in superuser like ``root`` or ``administrator``. A superuser can do anything without explict permissions.
+5. multiple built-in operators to support the rule matching. For example, ``keyMatch`` can map a resource key ``/foo/bar`` to the pattern ``/foo*``.
+
+What casbin-rs does NOT do:
+
+1. authentication (aka verify ``username`` and ``password`` when a user logs in)
+2. manage the list of users or roles. I believe it's more convenient for the project itself to manage these entities. Users usually have their passwords, and casbin-rs is not designed as a password container. However, casbin-rs stores the user-role mapping for the RBAC scenario.
+
+## Documentation
+
+https://casbin.org/docs/en/overview
+
+## Online editor
+
+You can also use the online editor (http://casbin.org/editor/) to write your casbin-rs model and policy in your web browser. It provides functionality such as ``syntax highlighting`` and ``code completion``, just like an IDE for a programming language.
+
+## Tutorials
+
+https://casbin.org/docs/en/tutorials
+
+## Policy management
+
+casbin-rs provides two sets of APIs to manage permissions:
+
+- [Management API](https://github.com/casbin/casbin-rs/blob/master/src/management_api.rs): the primitive API that provides full support for casbin-rs policy management. See [here](https://github.com/casbin/casbin-rs/blob/master/src/management_api.rs) for examples.
+- [RBAC API](https://github.com/casbin/casbin-rs/blob/master/src/rbac_api.rs): a more friendly API for RBAC. This API is a subset of Management API. The RBAC users could use this API to simplify the code. See [here](https://github.com/casbin/casbin-rs/blob/master/src/rbac_api.rs) for examples.
+
+We also provide a web-based UI for model management and policy management:
+
+
+
+
+
+## Policy persistence
+
+* https://casbin.org/docs/en/adapters
+* There also is [example code on how to preload an existing policy into an adapter](https://github.com/casbin-rs/examples/blob/master/actix-middleware-example/src/main.rs#L73).
+
+## Role manager
+
+https://casbin.org/docs/en/role-managers
+
+## Examples
+
+Model | Model file | Policy file
+----|------|----
+ACL | [basic_model.conf](https://github.com/casbin/casbin-rs/blob/master/examples/basic_model.conf) | [basic_policy.csv](https://github.com/casbin/casbin-rs/blob/master/examples/basic_policy.csv)
+ACL with superuser | [basic_model_with_root.conf](https://github.com/casbin/casbin-rs/blob/master/examples/basic_with_root_model.conf) | [basic_policy.csv](https://github.com/casbin/casbin-rs/blob/master/examples/basic_policy.csv)
+ACL without users | [basic_model_without_users.conf](https://github.com/casbin/casbin-rs/blob/master/examples/basic_without_users_model.conf) | [basic_policy_without_users.csv](https://github.com/casbin/casbin-rs/blob/master/examples/basic_without_users_policy.csv)
+ACL without resources | [basic_model_without_resources.conf](https://github.com/casbin/casbin-rs/blob/master/examples/basic_without_resources_model.conf) | [basic_policy_without_resources.csv](https://github.com/casbin/casbin-rs/blob/master/examples/basic_without_resources_policy.csv)
+RBAC | [rbac_model.conf](https://github.com/casbin/casbin-rs/blob/master/examples/rbac_model.conf) | [rbac_policy.csv](https://github.com/casbin/casbin-rs/blob/master/examples/rbac_policy.csv)
+RBAC with resource roles | [rbac_model_with_resource_roles.conf](https://github.com/casbin/casbin-rs/blob/master/examples/rbac_with_resource_roles_model.conf) | [rbac_policy_with_resource_roles.csv](https://github.com/casbin/casbin-rs/blob/master/examples/rbac_with_resource_roles_policy.csv)
+RBAC with domains/tenants | [rbac_model_with_domains.conf](https://github.com/casbin/casbin-rs/blob/master/examples/rbac_with_domains_model.conf) | [rbac_policy_with_domains.csv](https://github.com/casbin/casbin-rs/blob/master/examples/rbac_with_domains_policy.csv)
+ABAC | [abac_model.conf](https://github.com/casbin/casbin-rs/blob/master/examples/abac_model.conf) | N/A
+RESTful | [keymatch_model.conf](https://github.com/casbin/casbin-rs/blob/master/examples/keymatch_model.conf) | [keymatch_policy.csv](https://github.com/casbin/casbin-rs/blob/master/examples/keymatch_policy.csv)
+Deny-override | [rbac_model_with_deny.conf](https://github.com/casbin/casbin-rs/blob/master/examples/rbac_with_deny_model.conf) | [rbac_policy_with_deny.csv](https://github.com/casbin/casbin-rs/blob/master/examples/rbac_with_deny_policy.csv)
+Priority | [priority_model.conf](https://github.com/casbin/casbin-rs/blob/master/examples/priority_model.conf) | [priority_policy.csv](https://github.com/casbin/casbin-rs/blob/master/examples/priority_policy.csv)
+
+## Middlewares
+
+Authz middlewares for web frameworks: https://casbin.org/docs/en/middlewares
+
+## Our adopters
+
+https://casbin.org/docs/en/adopters
+
+## Contributors
+
+This project exists thanks to all the people who contribute.
+<a href="https://github.com/casbin/casbin-rs/graphs/contributors"><img src="https://opencollective.com/casbin-rs/contributors.svg?width=890&button=false" /></a>
+
+## Backers
+
+Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/casbin#backer)]
+
+<a href="https://opencollective.com/casbin#backers" target="_blank"><img src="https://opencollective.com/casbin/backers.svg?width=890"></a>
+
+## Sponsors
+
+Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/casbin#sponsor)]
+
+<a href="https://opencollective.com/casbin/sponsor/0/website" target="_blank"><img src="https://opencollective.com/casbin/sponsor/0/avatar.svg"></a>
+<a href="https://opencollective.com/casbin/sponsor/1/website" target="_blank"><img src="https://opencollective.com/casbin/sponsor/1/avatar.svg"></a>
+<a href="https://opencollective.com/casbin/sponsor/2/website" target="_blank"><img src="https://opencollective.com/casbin/sponsor/2/avatar.svg"></a>
+<a href="https://opencollective.com/casbin/sponsor/3/website" target="_blank"><img src="https://opencollective.com/casbin/sponsor/3/avatar.svg"></a>
+<a href="https://opencollective.com/casbin/sponsor/4/website" target="_blank"><img src="https://opencollective.com/casbin/sponsor/4/avatar.svg"></a>
+<a href="https://opencollective.com/casbin/sponsor/5/website" target="_blank"><img src="https://opencollective.com/casbin/sponsor/5/avatar.svg"></a>
+<a href="https://opencollective.com/casbin/sponsor/6/website" target="_blank"><img src="https://opencollective.com/casbin/sponsor/6/avatar.svg"></a>
+<a href="https://opencollective.com/casbin/sponsor/7/website" target="_blank"><img src="https://opencollective.com/casbin/sponsor/7/avatar.svg"></a>
+<a href="https://opencollective.com/casbin/sponsor/8/website" target="_blank"><img src="https://opencollective.com/casbin/sponsor/8/avatar.svg"></a>
+<a href="https://opencollective.com/casbin/sponsor/9/website" target="_blank"><img src="https://opencollective.com/casbin/sponsor/9/avatar.svg"></a>
+
+## License
+
+This project is licensed under the [Apache 2.0 license](LICENSE).
+
+## Contact
+
+If you have any issues or feature requests, please contact us. PR is welcomed.
+- https://github.com/casbin/casbin-rs/issues
+- Tencent QQ group: [546057381](//shang.qq.com/wpa/qunwpa?idkey=8ac8b91fc97ace3d383d0035f7aa06f7d670fd8e8d4837347354a31c18fac885)
diff --git a/casbin-rs/benches/benchmark.rs b/casbin-rs/benches/benchmark.rs
new file mode 100644
index 0000000..3e74565
--- /dev/null
+++ b/casbin-rs/benches/benchmark.rs
@@ -0,0 +1,654 @@
+use casbin::prelude::*;
+
+extern crate criterion;
+
+use criterion::{criterion_group, criterion_main, Criterion};
+
+fn await_future<F, T>(future: F) -> T
+where
+ F: std::future::Future<Output = T>,
+{
+ #[cfg(feature = "runtime-async-std")]
+ {
+ async_std::task::block_on(future)
+ }
+
+ #[cfg(feature = "runtime-tokio")]
+ {
+ tokio::runtime::Runtime::new().unwrap().block_on(future)
+ }
+}
+
+// To save a new baseline to compare against run
+// `cargo bench > before`
+// on the master branch.
+//
+// Then install cargo-benchcmp
+// `cargo install cargo-benchcmp --force`
+//
+// Then to compare your changes switch to your branch and run
+// `cargo bench > after`
+//
+// And compare with
+// cargo benchcmp before after
+
+fn raw_enforce(r: [&str; 3]) -> bool {
+ let policies = [["alice", "data1", "read"], ["bob", "data2", "write"]];
+ for policy in &policies {
+ if policy == &r {
+ return true;
+ }
+ }
+ return false;
+}
+
+fn b_benchmark_raw(c: &mut Criterion) {
+ c.bench_function("benchmark_raw", |b| {
+ b.iter(|| {
+ raw_enforce(["alice", "data1", "read"]);
+ })
+ });
+}
+
+fn b_benchmark_basic_model(c: &mut Criterion) {
+ let e = await_future(Enforcer::new(
+ "examples/basic_model.conf",
+ "examples/basic_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark_basic_model", |b| {
+ b.iter(|| {
+ e.enforce(("alice", "data1", "read")).unwrap();
+ })
+ });
+}
+
+#[cfg(feature = "cached")]
+fn b_benchmark_cached_basic_model(c: &mut Criterion) {
+ let mut e = await_future(CachedEnforcer::new(
+ "examples/basic_model.conf",
+ "examples/basic_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark_cached_basic_model", |b| {
+ b.iter(|| {
+ e.enforce_mut(("alice", "data1", "read")).unwrap();
+ })
+ });
+}
+
+fn b_benchmark_rbac_model(c: &mut Criterion) {
+ let e = await_future(Enforcer::new(
+ "examples/rbac_model.conf",
+ "examples/rbac_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark_rbac_model", |b| {
+ b.iter(|| {
+ e.enforce(("alice", "data2", "read")).unwrap();
+ })
+ });
+}
+
+#[cfg(feature = "cached")]
+fn b_benchmark_cached_rbac_model(c: &mut Criterion) {
+ let mut e = await_future(CachedEnforcer::new(
+ "examples/rbac_model.conf",
+ "examples/rbac_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark_cached_rbac_model", |b| {
+ b.iter(|| {
+ e.enforce_mut(("alice", "data2", "read")).unwrap();
+ })
+ });
+}
+
+fn b_benchmark_role_manager_small(c: &mut Criterion) {
+ let mut e =
+ await_future(Enforcer::new("examples/rbac_model.conf", ())).unwrap();
+
+ e.enable_auto_build_role_links(false);
+
+ // 100 roles, 10 resources.
+ await_future(
+ e.add_policies(
+ (0..100_u64)
+ .map(|i| {
+ vec![
+ format!("group{}", i),
+ format!("data{}", i / 10),
+ "read".to_owned(),
+ ]
+ })
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ // 1000 users.
+ await_future(
+ e.add_grouping_policies(
+ (0..1000)
+ .map(|i| vec![format!("user{}", i), format!("group{}", i / 10)])
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ e.build_role_links().unwrap();
+
+ let rm = e.get_role_manager();
+ c.bench_function("benchmark_role_manager_small", |b| {
+ b.iter(|| {
+ (0..100_u64).for_each(|i| {
+ rm.write().has_link("user501", &format!("group{}", i), None);
+ })
+ })
+ });
+}
+
+fn b_benchmark_rbac_model_small(c: &mut Criterion) {
+ let mut e =
+ await_future(Enforcer::new("examples/rbac_model.conf", ())).unwrap();
+
+ e.enable_auto_build_role_links(false);
+
+ // 100 roles, 10 resources.
+ await_future(
+ e.add_policies(
+ (0..100_u64)
+ .map(|i| {
+ vec![
+ format!("group{}", i),
+ format!("data{}", i / 10),
+ "read".to_owned(),
+ ]
+ })
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ // 1000 users.
+ await_future(
+ e.add_grouping_policies(
+ (0..1000)
+ .map(|i| vec![format!("user{}", i), format!("group{}", i / 10)])
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ e.build_role_links().unwrap();
+ c.bench_function("benchmark_role_manager_small", |b| {
+ b.iter(|| e.enforce(("user501", "data9", "read")).unwrap())
+ });
+}
+
+#[cfg(feature = "cached")]
+fn b_benchmark_cached_rbac_model_small(c: &mut Criterion) {
+ let mut e =
+ await_future(CachedEnforcer::new("examples/rbac_model.conf", ()))
+ .unwrap();
+
+ e.enable_auto_build_role_links(false);
+
+ // 100 roles, 10 resources.
+ await_future(
+ e.add_policies(
+ (0..100_u64)
+ .map(|i| {
+ vec![
+ format!("group{}", i),
+ format!("data{}", i / 10),
+ "read".to_owned(),
+ ]
+ })
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ // 1000 users.
+ await_future(
+ e.add_grouping_policies(
+ (0..1000)
+ .map(|i| vec![format!("user{}", i), format!("group{}", i / 10)])
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ e.build_role_links().unwrap();
+ c.bench_function("benchmark_cached_rbac_model_small", |b| {
+ b.iter(|| e.enforce_mut(("user501", "data9", "read")).unwrap())
+ });
+}
+
+fn b_benchmark_role_manager_medium(c: &mut Criterion) {
+ let mut e =
+ await_future(Enforcer::new("examples/rbac_model.conf", ())).unwrap();
+
+ e.enable_auto_build_role_links(false);
+
+ // 1000 roles, 100 resources.
+ await_future(
+ e.add_policies(
+ (0..1000_u64)
+ .map(|i| {
+ vec![
+ format!("group{}", i),
+ format!("data{}", i / 10),
+ "read".to_owned(),
+ ]
+ })
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ // 10000 users.
+ await_future(
+ e.add_grouping_policies(
+ (0..10000)
+ .map(|i| vec![format!("user{}", i), format!("group{}", i / 10)])
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ e.build_role_links().unwrap();
+
+ let rm = e.get_role_manager();
+ c.bench_function("benchmark_role_manager_medium", |b| {
+ b.iter(|| {
+ (0..1000_u64).for_each(|i| {
+ rm.write()
+ .has_link("user5001", &format!("group{}", i), None);
+ })
+ })
+ });
+}
+
+fn b_benchmark_rbac_model_medium(c: &mut Criterion) {
+ let mut e =
+ await_future(Enforcer::new("examples/rbac_model.conf", ())).unwrap();
+
+ e.enable_auto_build_role_links(false);
+
+ // 1000 roles, 100 resources.
+ await_future(
+ e.add_policies(
+ (0..1000_u64)
+ .map(|i| {
+ vec![
+ format!("group{}", i),
+ format!("data{}", i / 10),
+ "read".to_owned(),
+ ]
+ })
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ // 10000 users.
+ await_future(
+ e.add_grouping_policies(
+ (0..10000)
+ .map(|i| vec![format!("user{}", i), format!("group{}", i / 10)])
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ e.build_role_links().unwrap();
+ c.bench_function("benchmark_rbac_model_medium", |b| {
+ b.iter(|| e.enforce(("user5001", "data15", "read")).unwrap())
+ });
+}
+
+#[cfg(feature = "cached")]
+fn b_benchmark_cached_rbac_model_medium(c: &mut Criterion) {
+ let mut e =
+ await_future(CachedEnforcer::new("examples/rbac_model.conf", ()))
+ .unwrap();
+
+ e.enable_auto_build_role_links(false);
+
+ // 1000 roles, 100 resources.
+ await_future(
+ e.add_policies(
+ (0..1000_u64)
+ .map(|i| {
+ vec![
+ format!("group{}", i),
+ format!("data{}", i / 10),
+ "read".to_owned(),
+ ]
+ })
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ // 10000 users.
+ await_future(
+ e.add_grouping_policies(
+ (0..10000)
+ .map(|i| vec![format!("user{}", i), format!("group{}", i / 10)])
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ e.build_role_links().unwrap();
+ c.bench_function("benchmark_cached_rbac_model_medium", |b| {
+ b.iter(|| e.enforce_mut(("user5001", "data15", "read")).unwrap())
+ });
+}
+
+fn b_benchmark_role_manager_large(c: &mut Criterion) {
+ let mut e =
+ await_future(Enforcer::new("examples/rbac_model.conf", ())).unwrap();
+
+ e.enable_auto_build_role_links(false);
+
+ // 10000 roles, 1000 resources.
+ await_future(
+ e.add_policies(
+ (0..10000_u64)
+ .map(|i| {
+ vec![
+ format!("group{}", i),
+ format!("data{}", i / 10),
+ "read".to_owned(),
+ ]
+ })
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ // 100000 users.
+ await_future(
+ e.add_grouping_policies(
+ (0..100000)
+ .map(|i| vec![format!("user{}", i), format!("group{}", i / 10)])
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ e.build_role_links().unwrap();
+
+ let rm = e.get_role_manager();
+ c.bench_function("benchmark_role_manager_large", |b| {
+ b.iter(|| {
+ (0..10000_u64).for_each(|i| {
+ rm.write()
+ .has_link("user50001", &format!("group{}", i), None);
+ })
+ })
+ });
+}
+
+fn b_benchmark_rbac_model_large(c: &mut Criterion) {
+ let mut e =
+ await_future(Enforcer::new("examples/rbac_model.conf", ())).unwrap();
+
+ e.enable_auto_build_role_links(false);
+
+ // 10000 roles, 1000 resources.
+ await_future(
+ e.add_policies(
+ (0..10000_u64)
+ .map(|i| {
+ vec![
+ format!("group{}", i),
+ format!("data{}", i / 10),
+ "read".to_owned(),
+ ]
+ })
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ // 100000 users.
+ await_future(
+ e.add_grouping_policies(
+ (0..100000)
+ .map(|i| vec![format!("user{}", i), format!("group{}", i / 10)])
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ e.build_role_links().unwrap();
+ c.bench_function("b_benchmark_rbac_model_large", |b| {
+ b.iter(|| e.enforce(("user50001", "data1500", "read")).unwrap())
+ });
+}
+
+#[cfg(feature = "cached")]
+fn b_benchmark_cached_rbac_model_large(c: &mut Criterion) {
+ let mut e =
+ await_future(CachedEnforcer::new("examples/rbac_model.conf", ()))
+ .unwrap();
+
+ e.enable_auto_build_role_links(false);
+
+ // 10000 roles, 1000 resources.
+ await_future(
+ e.add_policies(
+ (0..10000_u64)
+ .map(|i| {
+ vec![
+ format!("group{}", i),
+ format!("data{}", i / 10),
+ "read".to_owned(),
+ ]
+ })
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ // 100000 users.
+ await_future(
+ e.add_grouping_policies(
+ (0..100000)
+ .map(|i| vec![format!("user{}", i), format!("group{}", i / 10)])
+ .collect::<Vec<Vec<String>>>(),
+ ),
+ )
+ .unwrap();
+
+ e.build_role_links().unwrap();
+ c.bench_function("benchmark_cached_rbac_model_large", |b| {
+ b.iter(|| e.enforce_mut(("user50001", "data1500", "read")).unwrap())
+ });
+}
+
+fn b_benchmark_rbac_with_resource_roles(c: &mut Criterion) {
+ let e = await_future(Enforcer::new(
+ "examples/rbac_with_resource_roles_model.conf",
+ "examples/rbac_with_resource_roles_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark_rbac_with_resource_roles", |b| {
+ b.iter(|| e.enforce(("alice", "data1", "read")).unwrap())
+ });
+}
+
+#[cfg(feature = "cached")]
+fn b_benchmark_cached_rbac_with_resource_roles(c: &mut Criterion) {
+ let mut e = await_future(CachedEnforcer::new(
+ "examples/rbac_with_resource_roles_model.conf",
+ "examples/rbac_with_resource_roles_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark_cached_rbac_with_resource_roles", |b| {
+ b.iter(|| e.enforce_mut(("alice", "data1", "read")).unwrap());
+ });
+}
+
+fn b_benchmark_rbac_model_with_domains(c: &mut Criterion) {
+ let e = await_future(Enforcer::new(
+ "examples/rbac_with_domains_model.conf",
+ "examples/rbac_with_domains_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark_rbac_model_with_domains", |b| {
+ b.iter(|| e.enforce(("alice", "domain1", "data1", "read")).unwrap())
+ });
+}
+
+#[cfg(feature = "cached")]
+fn b_benchmark_cached_rbac_model_with_domains(c: &mut Criterion) {
+ let mut e = await_future(CachedEnforcer::new(
+ "examples/rbac_with_domains_model.conf",
+ "examples/rbac_with_domains_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark_cached_rbac_model_with_domains", |b| {
+ b.iter(|| {
+ e.enforce_mut(("alice", "domain1", "data1", "read"))
+ .unwrap()
+ })
+ });
+}
+
+fn b_benchmark_abac_model(c: &mut Criterion) {
+ use serde::Serialize;
+
+ let e =
+ await_future(Enforcer::new("examples/abac_model.conf", ())).unwrap();
+
+ #[derive(Serialize, Hash)]
+ struct Book<'a> {
+ owner: &'a str,
+ }
+ c.bench_function("benchmark_abac_model", |b| {
+ b.iter(|| {
+ e.enforce(("alice", Book { owner: "alice" }, "read"))
+ .unwrap()
+ })
+ });
+}
+
+#[cfg(feature = "cached")]
+fn b_benchmark_cached_abac_model(c: &mut Criterion) {
+ use serde::Serialize;
+ let mut e =
+ await_future(CachedEnforcer::new("examples/abac_model.conf", ()))
+ .unwrap();
+
+ #[derive(Serialize, Hash)]
+ struct Book<'a> {
+ owner: &'a str,
+ }
+ c.bench_function("benchmark_cached_abac_model", |b| {
+ b.iter(|| {
+ e.enforce_mut(("alice", Book { owner: "alice" }, "read"))
+ .unwrap()
+ })
+ });
+}
+
+fn b_benchmark_key_match(c: &mut Criterion) {
+ let e = await_future(Enforcer::new(
+ "examples/keymatch_model.conf",
+ "examples/keymatch_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark_key_match", |b| {
+ b.iter(|| {
+ e.enforce(("alice", "/alice_data/resource1", "GET"))
+ .unwrap()
+ })
+ });
+}
+
+#[cfg(feature = "cached")]
+fn b_benchmark_cached_key_match(c: &mut Criterion) {
+ let mut e = await_future(CachedEnforcer::new(
+ "examples/keymatch_model.conf",
+ "examples/keymatch_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark_cached_key_match", |b| {
+ b.iter(|| {
+ e.enforce_mut(("alice", "/alice_data/resource1", "GET"))
+ .unwrap()
+ })
+ });
+}
+
+fn b_benchmark_rbac_with_deny(c: &mut Criterion) {
+ let e = await_future(Enforcer::new(
+ "examples/rbac_with_deny_model.conf",
+ "examples/rbac_with_deny_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark_rbac_with_deny", |b| {
+ b.iter(|| e.enforce(("alice", "data1", "read")).unwrap())
+ });
+}
+
+#[cfg(feature = "cached")]
+fn b_benchmark_cached_rbac_with_deny(c: &mut Criterion) {
+ let mut e = await_future(CachedEnforcer::new(
+ "examples/rbac_with_deny_model.conf",
+ "examples/rbac_with_deny_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark_cached_rbac_with_deny", |b| {
+ b.iter(|| e.enforce_mut(("alice", "data1", "read")).unwrap())
+ });
+}
+
+fn b_benchmark_priority_model(c: &mut Criterion) {
+ let e = await_future(Enforcer::new(
+ "examples/priority_model.conf",
+ "examples/priority_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark priority model", |b| {
+ b.iter(|| e.enforce(("alice", "data1", "read")).unwrap())
+ });
+}
+
+#[cfg(feature = "cached")]
+fn b_benchmark_cached_priority_model(c: &mut Criterion) {
+ let mut e = await_future(CachedEnforcer::new(
+ "examples/priority_model.conf",
+ "examples/priority_policy.csv",
+ ))
+ .unwrap();
+ c.bench_function("benchmark_cached_priority_model", |b| {
+ b.iter(|| e.enforce_mut(("alice", "data1", "read")).unwrap())
+ });
+}
+
+criterion_group!(
+ benches,
+ b_benchmark_raw,
+ b_benchmark_basic_model,
+ b_benchmark_rbac_model,
+ b_benchmark_role_manager_small,
+ b_benchmark_rbac_model_small,
+ b_benchmark_role_manager_large,
+ b_benchmark_role_manager_medium,
+ b_benchmark_rbac_model_medium,
+ b_benchmark_rbac_model_large,
+ b_benchmark_rbac_with_resource_roles,
+ b_benchmark_rbac_model_with_domains,
+ b_benchmark_abac_model,
+ b_benchmark_key_match,
+ b_benchmark_rbac_with_deny,
+ b_benchmark_priority_model,
+);
+criterion_main!(benches);
diff --git a/casbin-rs/casbin-logo.png b/casbin-rs/casbin-logo.png
new file mode 100644
index 0000000..7e5d1ec
--- /dev/null
+++ b/casbin-rs/casbin-logo.png
Binary files differ
diff --git a/casbin-rs/examples/abac_model.conf b/casbin-rs/examples/abac_model.conf
new file mode 100644
index 0000000..50e3281
--- /dev/null
+++ b/casbin-rs/examples/abac_model.conf
@@ -0,0 +1,11 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub == r.obj.owner
\ No newline at end of file
diff --git a/casbin-rs/examples/basic_inverse_policy.csv b/casbin-rs/examples/basic_inverse_policy.csv
new file mode 100644
index 0000000..276c440
--- /dev/null
+++ b/casbin-rs/examples/basic_inverse_policy.csv
@@ -0,0 +1,2 @@
+p, alice, data1, write
+p, bob, data2, read
\ No newline at end of file
diff --git a/casbin-rs/examples/basic_model.conf b/casbin-rs/examples/basic_model.conf
new file mode 100644
index 0000000..cee2566
--- /dev/null
+++ b/casbin-rs/examples/basic_model.conf
@@ -0,0 +1,11 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
diff --git a/casbin-rs/examples/basic_policy.csv b/casbin-rs/examples/basic_policy.csv
new file mode 100644
index 0000000..57aaa97
--- /dev/null
+++ b/casbin-rs/examples/basic_policy.csv
@@ -0,0 +1,2 @@
+p, alice, data1, read
+p, bob, data2, write
\ No newline at end of file
diff --git a/casbin-rs/examples/basic_with_root_model.conf b/casbin-rs/examples/basic_with_root_model.conf
new file mode 100644
index 0000000..fd8a2fe
--- /dev/null
+++ b/casbin-rs/examples/basic_with_root_model.conf
@@ -0,0 +1,11 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root" # root is the super user
diff --git a/casbin-rs/examples/basic_without_resources_model.conf b/casbin-rs/examples/basic_without_resources_model.conf
new file mode 100644
index 0000000..f61bd71
--- /dev/null
+++ b/casbin-rs/examples/basic_without_resources_model.conf
@@ -0,0 +1,11 @@
+[request_definition]
+r = sub, act
+
+[policy_definition]
+p = sub, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub == p.sub && r.act == p.act
\ No newline at end of file
diff --git a/casbin-rs/examples/basic_without_resources_policy.csv b/casbin-rs/examples/basic_without_resources_policy.csv
new file mode 100644
index 0000000..c861941
--- /dev/null
+++ b/casbin-rs/examples/basic_without_resources_policy.csv
@@ -0,0 +1,2 @@
+p, alice, read
+p, bob, write
\ No newline at end of file
diff --git a/casbin-rs/examples/basic_without_users_model.conf b/casbin-rs/examples/basic_without_users_model.conf
new file mode 100644
index 0000000..1fe5993
--- /dev/null
+++ b/casbin-rs/examples/basic_without_users_model.conf
@@ -0,0 +1,11 @@
+[request_definition]
+r = obj, act
+
+[policy_definition]
+p = obj, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.obj == p.obj && r.act == p.act
\ No newline at end of file
diff --git a/casbin-rs/examples/basic_without_users_policy.csv b/casbin-rs/examples/basic_without_users_policy.csv
new file mode 100644
index 0000000..79048da
--- /dev/null
+++ b/casbin-rs/examples/basic_without_users_policy.csv
@@ -0,0 +1,2 @@
+p, data1, read
+p, data2, write
\ No newline at end of file
diff --git a/casbin-rs/examples/error/error_model.conf b/casbin-rs/examples/error/error_model.conf
new file mode 100644
index 0000000..59304db
--- /dev/null
+++ b/casbin-rs/examples/error/error_model.conf
@@ -0,0 +1,11 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition
+p = sub, obj, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
\ No newline at end of file
diff --git a/casbin-rs/examples/error/error_policy.csv b/casbin-rs/examples/error/error_policy.csv
new file mode 100644
index 0000000..e490c7d
--- /dev/null
+++ b/casbin-rs/examples/error/error_policy.csv
@@ -0,0 +1,2 @@
+p, alice, data1, read
+bob, data2, write
\ No newline at end of file
diff --git a/casbin-rs/examples/ipmatch_model.conf b/casbin-rs/examples/ipmatch_model.conf
new file mode 100644
index 0000000..26e4b01
--- /dev/null
+++ b/casbin-rs/examples/ipmatch_model.conf
@@ -0,0 +1,11 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = ipMatch(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
\ No newline at end of file
diff --git a/casbin-rs/examples/ipmatch_policy.csv b/casbin-rs/examples/ipmatch_policy.csv
new file mode 100644
index 0000000..ca678a9
--- /dev/null
+++ b/casbin-rs/examples/ipmatch_policy.csv
@@ -0,0 +1,2 @@
+p, 192.168.2.0/24, data1, read
+p, 10.0.0.0/16, data2, write
\ No newline at end of file
diff --git a/casbin-rs/examples/keymatch2_model.conf b/casbin-rs/examples/keymatch2_model.conf
new file mode 100644
index 0000000..944123d
--- /dev/null
+++ b/casbin-rs/examples/keymatch2_model.conf
@@ -0,0 +1,11 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)
\ No newline at end of file
diff --git a/casbin-rs/examples/keymatch2_policy.csv b/casbin-rs/examples/keymatch2_policy.csv
new file mode 100644
index 0000000..941a48f
--- /dev/null
+++ b/casbin-rs/examples/keymatch2_policy.csv
@@ -0,0 +1,2 @@
+p, alice, /alice_data/:resource, GET
+p, alice, /alice_data2/:id/using/:resId, GET
\ No newline at end of file
diff --git a/casbin-rs/examples/keymatch_custom_model.conf b/casbin-rs/examples/keymatch_custom_model.conf
new file mode 100644
index 0000000..1cad8bf
--- /dev/null
+++ b/casbin-rs/examples/keymatch_custom_model.conf
@@ -0,0 +1,11 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub == p.sub && keyMatchCustom(r.obj, p.obj) && regexMatch(r.act, p.act)
\ No newline at end of file
diff --git a/casbin-rs/examples/keymatch_model.conf b/casbin-rs/examples/keymatch_model.conf
new file mode 100644
index 0000000..4f86ba8
--- /dev/null
+++ b/casbin-rs/examples/keymatch_model.conf
@@ -0,0 +1,11 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)
\ No newline at end of file
diff --git a/casbin-rs/examples/keymatch_policy.csv b/casbin-rs/examples/keymatch_policy.csv
new file mode 100644
index 0000000..4b1b6d1
--- /dev/null
+++ b/casbin-rs/examples/keymatch_policy.csv
@@ -0,0 +1,7 @@
+p, alice, /alice_data/*, GET
+p, alice, /alice_data/resource1, POST
+
+p, bob, /alice_data/resource2, GET
+p, bob, /bob_data/*, POST
+
+p, cathy, /cathy_data, (GET)|(POST)
diff --git a/casbin-rs/examples/priority_indeterminate_policy.csv b/casbin-rs/examples/priority_indeterminate_policy.csv
new file mode 100644
index 0000000..974aa27
--- /dev/null
+++ b/casbin-rs/examples/priority_indeterminate_policy.csv
@@ -0,0 +1 @@
+p, alice, data1, read, indeterminate
\ No newline at end of file
diff --git a/casbin-rs/examples/priority_model.conf b/casbin-rs/examples/priority_model.conf
new file mode 100644
index 0000000..ece1562
--- /dev/null
+++ b/casbin-rs/examples/priority_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act, eft
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = priority(p.eft) || deny
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
\ No newline at end of file
diff --git a/casbin-rs/examples/priority_policy.csv b/casbin-rs/examples/priority_policy.csv
new file mode 100644
index 0000000..1ec5e5a
--- /dev/null
+++ b/casbin-rs/examples/priority_policy.csv
@@ -0,0 +1,12 @@
+p, alice, data1, read, allow
+p, data1_deny_group, data1, read, deny
+p, data1_deny_group, data1, write, deny
+p, alice, data1, write, allow
+
+g, alice, data1_deny_group
+
+p, data2_allow_group, data2, read, allow
+p, bob, data2, read, deny
+p, bob, data2, write, deny
+
+g, bob, data2_allow_group
\ No newline at end of file
diff --git a/casbin-rs/examples/rbac_basic_role_model.conf b/casbin-rs/examples/rbac_basic_role_model.conf
new file mode 100644
index 0000000..459a15f
--- /dev/null
+++ b/casbin-rs/examples/rbac_basic_role_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && regexMatch(r.act, p.act)
diff --git a/casbin-rs/examples/rbac_basic_role_policy.csv b/casbin-rs/examples/rbac_basic_role_policy.csv
new file mode 100644
index 0000000..2b189e0
--- /dev/null
+++ b/casbin-rs/examples/rbac_basic_role_policy.csv
@@ -0,0 +1,5 @@
+p, book_admin , /book/1, GET
+p, pen_admin , /pen/1, GET
+
+g, *, book_admin
+g, *, pen_admin
diff --git a/casbin-rs/examples/rbac_model.conf b/casbin-rs/examples/rbac_model.conf
new file mode 100644
index 0000000..71159e3
--- /dev/null
+++ b/casbin-rs/examples/rbac_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
\ No newline at end of file
diff --git a/casbin-rs/examples/rbac_model_in_multi_line.conf b/casbin-rs/examples/rbac_model_in_multi_line.conf
new file mode 100644
index 0000000..17771b6
--- /dev/null
+++ b/casbin-rs/examples/rbac_model_in_multi_line.conf
@@ -0,0 +1,15 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj \
+ && r.act == p.act
\ No newline at end of file
diff --git a/casbin-rs/examples/rbac_model_matcher_using_in_op.conf b/casbin-rs/examples/rbac_model_matcher_using_in_op.conf
new file mode 100644
index 0000000..55c8156
--- /dev/null
+++ b/casbin-rs/examples/rbac_model_matcher_using_in_op.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.obj in ["data2", "data3"]
diff --git a/casbin-rs/examples/rbac_policy.csv b/casbin-rs/examples/rbac_policy.csv
new file mode 100644
index 0000000..f93d6df
--- /dev/null
+++ b/casbin-rs/examples/rbac_policy.csv
@@ -0,0 +1,5 @@
+p, alice, data1, read
+p, bob, data2, write
+p, data2_admin, data2, read
+p, data2_admin, data2, write
+g, alice, data2_admin
\ No newline at end of file
diff --git a/casbin-rs/examples/rbac_with_deny_model.conf b/casbin-rs/examples/rbac_with_deny_model.conf
new file mode 100644
index 0000000..33749f0
--- /dev/null
+++ b/casbin-rs/examples/rbac_with_deny_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act, eft
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
\ No newline at end of file
diff --git a/casbin-rs/examples/rbac_with_deny_policy.csv b/casbin-rs/examples/rbac_with_deny_policy.csv
new file mode 100644
index 0000000..0603db8
--- /dev/null
+++ b/casbin-rs/examples/rbac_with_deny_policy.csv
@@ -0,0 +1,7 @@
+p, alice, data1, read, allow
+p, bob, data2, write, allow
+p, data2_admin, data2, read, allow
+p, data2_admin, data2, write, allow
+p, alice, data2, write, deny
+
+g, alice, data2_admin
\ No newline at end of file
diff --git a/casbin-rs/examples/rbac_with_domains_model.conf b/casbin-rs/examples/rbac_with_domains_model.conf
new file mode 100644
index 0000000..57c3721
--- /dev/null
+++ b/casbin-rs/examples/rbac_with_domains_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, dom, obj, act
+
+[policy_definition]
+p = sub, dom, obj, act
+
+[role_definition]
+g = _, _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act
\ No newline at end of file
diff --git a/casbin-rs/examples/rbac_with_domains_policy.csv b/casbin-rs/examples/rbac_with_domains_policy.csv
new file mode 100644
index 0000000..a20ccd0
--- /dev/null
+++ b/casbin-rs/examples/rbac_with_domains_policy.csv
@@ -0,0 +1,6 @@
+p, admin,domain1,data1,read
+p, admin,domain1,data1,write
+p, admin,domain2,data2,read
+p, admin,domain2,data2,write
+g, alice,admin,domain1
+g, bob,admin,domain2
diff --git a/casbin-rs/examples/rbac_with_hierarchy_policy.csv b/casbin-rs/examples/rbac_with_hierarchy_policy.csv
new file mode 100644
index 0000000..f722998
--- /dev/null
+++ b/casbin-rs/examples/rbac_with_hierarchy_policy.csv
@@ -0,0 +1,10 @@
+p, alice, data1, read
+p, bob, data2, write
+p, data1_admin, data1, read
+p, data1_admin, data1, write
+p, data2_admin, data2, read
+p, data2_admin, data2, write
+
+g, alice, admin
+g, admin, data1_admin
+g, admin, data2_admin
\ No newline at end of file
diff --git a/casbin-rs/examples/rbac_with_hierarchy_with_domains_policy.csv b/casbin-rs/examples/rbac_with_hierarchy_with_domains_policy.csv
new file mode 100644
index 0000000..45d9173
--- /dev/null
+++ b/casbin-rs/examples/rbac_with_hierarchy_with_domains_policy.csv
@@ -0,0 +1,11 @@
+p, role:reader, domain1, data1, read
+p, role:writer, domain1, data1, write
+
+p, alice, domain1, data2, read
+p, alice, domain2, data2, read
+
+g, role:global_admin, role:reader, domain1
+g, role:global_admin, role:writer, domain1
+
+g, alice, role:global_admin, domain1
+
diff --git a/casbin-rs/examples/rbac_with_not_deny_model.conf b/casbin-rs/examples/rbac_with_not_deny_model.conf
new file mode 100644
index 0000000..771f33d
--- /dev/null
+++ b/casbin-rs/examples/rbac_with_not_deny_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act, eft
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = !some(where (p.eft == deny))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
\ No newline at end of file
diff --git a/casbin-rs/examples/rbac_with_pattern_domain_model.conf b/casbin-rs/examples/rbac_with_pattern_domain_model.conf
new file mode 100644
index 0000000..5d04715
--- /dev/null
+++ b/casbin-rs/examples/rbac_with_pattern_domain_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, dom, obj, act
+
+[policy_definition]
+p = sub, dom, obj, act
+
+[role_definition]
+g = _, _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && regexMatch(r.act, p.act)
diff --git a/casbin-rs/examples/rbac_with_pattern_domain_policy.csv b/casbin-rs/examples/rbac_with_pattern_domain_policy.csv
new file mode 100644
index 0000000..ace0c01
--- /dev/null
+++ b/casbin-rs/examples/rbac_with_pattern_domain_policy.csv
@@ -0,0 +1,7 @@
+p, admin, domain1, data1, read
+p, admin, domain1, data1, write
+p, admin, domain2, data2, read
+p, admin, domain2, data2, write
+
+g, alice, admin, *
+g, bob, admin, domain2
diff --git a/casbin-rs/examples/rbac_with_pattern_model.conf b/casbin-rs/examples/rbac_with_pattern_model.conf
new file mode 100644
index 0000000..44507a6
--- /dev/null
+++ b/casbin-rs/examples/rbac_with_pattern_model.conf
@@ -0,0 +1,15 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+g2 = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && g2(r.obj, p.obj) && regexMatch(r.act, p.act)
diff --git a/casbin-rs/examples/rbac_with_pattern_policy.csv b/casbin-rs/examples/rbac_with_pattern_policy.csv
new file mode 100644
index 0000000..5f7267a
--- /dev/null
+++ b/casbin-rs/examples/rbac_with_pattern_policy.csv
@@ -0,0 +1,9 @@
+p, alice, /pen/1, GET
+p, alice, /pen2/1, GET
+p, book_admin, book_group, GET
+p, pen_admin, pen_group, GET
+
+g, alice, book_admin
+g, bob, pen_admin
+g2, /book/:id, book_group
+g2, /pen/:id, pen_group
diff --git a/casbin-rs/examples/rbac_with_resource_roles_model.conf b/casbin-rs/examples/rbac_with_resource_roles_model.conf
new file mode 100644
index 0000000..845bc6c
--- /dev/null
+++ b/casbin-rs/examples/rbac_with_resource_roles_model.conf
@@ -0,0 +1,15 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+g2 = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act
\ No newline at end of file
diff --git a/casbin-rs/examples/rbac_with_resource_roles_policy.csv b/casbin-rs/examples/rbac_with_resource_roles_policy.csv
new file mode 100644
index 0000000..b1d36da
--- /dev/null
+++ b/casbin-rs/examples/rbac_with_resource_roles_policy.csv
@@ -0,0 +1,7 @@
+p, alice, data1, read
+p, bob, data2, write
+p, data_group_admin, data_group, write
+
+g, alice, data_group_admin
+g2, data1, data_group
+g2, data2, data_group
\ No newline at end of file
diff --git a/casbin-rs/examples/testini.ini b/casbin-rs/examples/testini.ini
new file mode 100644
index 0000000..ccd6705
--- /dev/null
+++ b/casbin-rs/examples/testini.ini
@@ -0,0 +1,48 @@
+# test config
+debug = true
+url = act.wiki
+
+; redis config
+[redis]
+redis.key = push1,push2
+
+; mysql config
+[mysql]
+mysql.dev.host = 127.0.0.1
+mysql.dev.user = root
+mysql.dev.pass = 123456
+mysql.dev.db = test
+
+mysql.master.host = 10.0.0.1
+mysql.master.user = root
+mysql.master.pass = 89dds)2$#d
+mysql.master.db = act
+
+; math config
+[math]
+math.i64 = 64
+math.f64 = 64.1
+
+# multi-line test
+[multi1]
+name = r.sub==p.sub \
+ &&r.obj==p.obj\
+ \
+[multi2]
+name = r.sub==p.sub \
+ &&r.obj==p.obj
+
+[multi3]
+name = r.sub==p.sub \
+ &&r.obj==p.obj
+
+[multi4]
+name = \
+\
+ \
+
+[multi5]
+name = r.sub==p.sub \
+ &&r.obj==p.obj\
+ \
+
diff --git a/casbin-rs/rustfmt.toml b/casbin-rs/rustfmt.toml
new file mode 100644
index 0000000..df99c69
--- /dev/null
+++ b/casbin-rs/rustfmt.toml
@@ -0,0 +1 @@
+max_width = 80
diff --git a/casbin-rs/src/adapter/file_adapter.rs b/casbin-rs/src/adapter/file_adapter.rs
new file mode 100644
index 0000000..1671772
--- /dev/null
+++ b/casbin-rs/src/adapter/file_adapter.rs
@@ -0,0 +1,284 @@
+use crate::{
+ adapter::{Adapter, Filter},
+ error::{AdapterError, ModelError},
+ model::Model,
+ util::parse_csv_line,
+ Result,
+};
+
+#[cfg(feature = "runtime-async-std")]
+use async_std::{
+ fs::File,
+ io::prelude::*,
+ io::{BufReader, Error as IoError, ErrorKind},
+ path::Path,
+ prelude::*,
+};
+
+#[cfg(feature = "runtime-tokio")]
+use std::{
+ io::{Error as IoError, ErrorKind},
+ path::Path,
+};
+#[cfg(feature = "runtime-tokio")]
+use tokio::{
+ fs::File,
+ io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
+};
+
+use async_trait::async_trait;
+
+use std::convert::AsRef;
+use std::fmt::Write;
+
+pub struct FileAdapter<P> {
+ file_path: P,
+ is_filtered: bool,
+}
+
+type LoadPolicyFileHandler = fn(String, &mut dyn Model);
+type LoadFilteredPolicyFileHandler<'a> =
+ fn(String, &mut dyn Model, f: &Filter<'a>) -> bool;
+
+impl<P> FileAdapter<P>
+where
+ P: AsRef<Path> + Send + Sync,
+{
+ pub fn new(p: P) -> FileAdapter<P> {
+ FileAdapter {
+ file_path: p,
+ is_filtered: false,
+ }
+ }
+
+ async fn load_policy_file(
+ &self,
+ m: &mut dyn Model,
+ handler: LoadPolicyFileHandler,
+ ) -> Result<()> {
+ let f = File::open(&self.file_path).await?;
+ let mut lines = BufReader::new(f).lines();
+ #[cfg(feature = "runtime-async-std")]
+ while let Some(line) = lines.next().await {
+ handler(line?, m)
+ }
+
+ #[cfg(feature = "runtime-tokio")]
+ while let Some(line) = lines.next_line().await? {
+ handler(line, m)
+ }
+
+ Ok(())
+ }
+
+ async fn load_filtered_policy_file<'a>(
+ &self,
+ m: &mut dyn Model,
+ filter: Filter<'a>,
+ handler: LoadFilteredPolicyFileHandler<'a>,
+ ) -> Result<bool> {
+ let f = File::open(&self.file_path).await?;
+ let mut lines = BufReader::new(f).lines();
+
+ let mut is_filtered = false;
+ #[cfg(feature = "runtime-async-std")]
+ while let Some(line) = lines.next().await {
+ if handler(line?, m, &filter) {
+ is_filtered = true;
+ }
+ }
+
+ #[cfg(feature = "runtime-tokio")]
+ while let Some(line) = lines.next_line().await? {
+ if handler(line, m, &filter) {
+ is_filtered = true;
+ }
+ }
+
+ Ok(is_filtered)
+ }
+
+ async fn save_policy_file(&self, text: String) -> Result<()> {
+ let mut file = File::create(&self.file_path).await?;
+ file.write_all(text.as_bytes()).await?;
+ Ok(())
+ }
+}
+
+#[async_trait]
+impl<P> Adapter for FileAdapter<P>
+where
+ P: AsRef<Path> + Send + Sync,
+{
+ async fn load_policy(&self, m: &mut dyn Model) -> Result<()> {
+ self.load_policy_file(m, load_policy_line).await?;
+ Ok(())
+ }
+
+ async fn load_filtered_policy<'a>(
+ &mut self,
+ m: &mut dyn Model,
+ f: Filter<'a>,
+ ) -> Result<()> {
+ self.is_filtered = self
+ .load_filtered_policy_file(m, f, load_filtered_policy_line)
+ .await?;
+
+ Ok(())
+ }
+
+ async fn save_policy(&mut self, m: &mut dyn Model) -> Result<()> {
+ if self.file_path.as_ref().as_os_str().is_empty() {
+ return Err(IoError::new(
+ ErrorKind::Other,
+ "save policy failed, file path is empty",
+ )
+ .into());
+ }
+
+ let mut policies = String::new();
+ let ast_map = m.get_model().get("p").ok_or_else(|| {
+ ModelError::P("Missing policy definition in conf file".to_owned())
+ })?;
+
+ for (ptype, ast) in ast_map {
+ for rule in ast.get_policy() {
+ writeln!(policies, "{}, {}", ptype, rule.join(","))
+ .map_err(|e| AdapterError(e.into()))?;
+ }
+ }
+
+ if let Some(ast_map) = m.get_model().get("g") {
+ for (ptype, ast) in ast_map {
+ for rule in ast.get_policy() {
+ writeln!(policies, "{}, {}", ptype, rule.join(","))
+ .map_err(|e| AdapterError(e.into()))?;
+ }
+ }
+ }
+
+ self.save_policy_file(policies).await?;
+ Ok(())
+ }
+
+ async fn clear_policy(&mut self) -> Result<()> {
+ self.save_policy_file(String::new()).await?;
+ Ok(())
+ }
+
+ async fn add_policy(
+ &mut self,
+ _sec: &str,
+ _ptype: &str,
+ _rule: Vec<String>,
+ ) -> Result<bool> {
+ // this api shouldn't implement, just for convenience
+ Ok(true)
+ }
+
+ async fn add_policies(
+ &mut self,
+ _sec: &str,
+ _ptype: &str,
+ _rules: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ // this api shouldn't implement, just for convenience
+ Ok(true)
+ }
+
+ async fn remove_policy(
+ &mut self,
+ _sec: &str,
+ _ptype: &str,
+ _rule: Vec<String>,
+ ) -> Result<bool> {
+ // this api shouldn't implement, just for convenience
+ Ok(true)
+ }
+
+ async fn remove_policies(
+ &mut self,
+ _sec: &str,
+ _ptype: &str,
+ _rule: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ // this api shouldn't implement, just for convenience
+ Ok(true)
+ }
+
+ async fn remove_filtered_policy(
+ &mut self,
+ _sec: &str,
+ _ptype: &str,
+ _field_index: usize,
+ _field_values: Vec<String>,
+ ) -> Result<bool> {
+ // this api shouldn't implement, just for convenience
+ Ok(true)
+ }
+
+ fn is_filtered(&self) -> bool {
+ self.is_filtered
+ }
+}
+
+fn load_policy_line(line: String, m: &mut dyn Model) {
+ if line.is_empty() || line.starts_with('#') {
+ return;
+ }
+
+ if let Some(tokens) = parse_csv_line(line) {
+ let key = &tokens[0];
+
+ if let Some(ref sec) = key.chars().next().map(|x| x.to_string()) {
+ if let Some(ast_map) = m.get_mut_model().get_mut(sec) {
+ if let Some(ast) = ast_map.get_mut(key) {
+ ast.policy.insert(tokens[1..].to_vec());
+ }
+ }
+ }
+ }
+}
+
+fn load_filtered_policy_line(
+ line: String,
+ m: &mut dyn Model,
+ f: &Filter<'_>,
+) -> bool {
+ if line.is_empty() || line.starts_with('#') {
+ return false;
+ }
+
+ if let Some(tokens) = parse_csv_line(line) {
+ let key = &tokens[0];
+
+ let mut is_filtered = false;
+ if let Some(ref sec) = key.chars().next().map(|x| x.to_string()) {
+ if sec == "p" {
+ for (i, rule) in f.p.iter().enumerate() {
+ if !rule.is_empty() && rule != &tokens[i + 1] {
+ is_filtered = true;
+ }
+ }
+ }
+ if sec == "g" {
+ for (i, rule) in f.g.iter().enumerate() {
+ if !rule.is_empty() && rule != &tokens[i + 1] {
+ is_filtered = true;
+ }
+ }
+ }
+ if !is_filtered {
+ if let Some(ast_map) = m.get_mut_model().get_mut(sec) {
+ if let Some(ast) = ast_map.get_mut(key) {
+ ast.policy.insert(tokens[1..].to_vec());
+ }
+ }
+ }
+ }
+
+ is_filtered
+ } else {
+ false
+ }
+}
diff --git a/casbin-rs/src/adapter/memory_adapter.rs b/casbin-rs/src/adapter/memory_adapter.rs
new file mode 100644
index 0000000..6b0dab0
--- /dev/null
+++ b/casbin-rs/src/adapter/memory_adapter.rs
@@ -0,0 +1,233 @@
+use crate::{
+ adapter::{Adapter, Filter},
+ model::Model,
+ Result,
+};
+
+use async_trait::async_trait;
+use ritelinked::LinkedHashSet;
+
+#[derive(Default)]
+pub struct MemoryAdapter {
+ policy: LinkedHashSet<Vec<String>>,
+ is_filtered: bool,
+}
+
+#[async_trait]
+impl Adapter for MemoryAdapter {
+ async fn load_policy(&self, m: &mut dyn Model) -> Result<()> {
+ for line in self.policy.iter() {
+ let sec = &line[0];
+ let ptype = &line[1];
+ let rule = line[1..].to_vec().clone();
+
+ if let Some(t1) = m.get_mut_model().get_mut(sec) {
+ if let Some(t2) = t1.get_mut(ptype) {
+ t2.get_mut_policy().insert(rule);
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ async fn load_filtered_policy<'a>(
+ &mut self,
+ m: &mut dyn Model,
+ f: Filter<'a>,
+ ) -> Result<()> {
+ for line in self.policy.iter() {
+ let sec = &line[0];
+ let ptype = &line[1];
+ let rule = line[1..].to_vec().clone();
+ let mut is_filtered = false;
+
+ if sec == "p" {
+ for (i, r) in f.p.iter().enumerate() {
+ if !r.is_empty() && r != &rule[i + 1] {
+ is_filtered = true;
+ }
+ }
+ }
+ if sec == "g" {
+ for (i, r) in f.g.iter().enumerate() {
+ if !r.is_empty() && r != &rule[i + 1] {
+ is_filtered = true;
+ }
+ }
+ }
+
+ if !is_filtered {
+ if let Some(ast_map) = m.get_mut_model().get_mut(sec) {
+ if let Some(ast) = ast_map.get_mut(ptype) {
+ ast.get_mut_policy().insert(rule);
+ }
+ }
+ } else {
+ self.is_filtered = true;
+ }
+ }
+ Ok(())
+ }
+
+ async fn save_policy(&mut self, m: &mut dyn Model) -> Result<()> {
+ self.policy.clear();
+
+ if let Some(ast_map) = m.get_model().get("p") {
+ for (ptype, ast) in ast_map {
+ if let Some(sec) = ptype.chars().next().map(|x| x.to_string()) {
+ for policy in ast.get_policy() {
+ let mut rule = policy.clone();
+ rule.insert(0, ptype.clone());
+ rule.insert(0, sec.clone());
+ self.policy.insert(rule);
+ }
+ }
+ }
+ }
+
+ if let Some(ast_map) = m.get_model().get("g") {
+ for (ptype, ast) in ast_map {
+ if let Some(sec) = ptype.chars().next().map(|x| x.to_string()) {
+ for policy in ast.get_policy() {
+ let mut rule = policy.clone();
+ rule.insert(0, ptype.clone());
+ rule.insert(0, sec.clone());
+ self.policy.insert(rule);
+ }
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ async fn clear_policy(&mut self) -> Result<()> {
+ self.policy.clear();
+ self.is_filtered = false;
+ Ok(())
+ }
+
+ async fn add_policy(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ mut rule: Vec<String>,
+ ) -> Result<bool> {
+ rule.insert(0, ptype.to_owned());
+ rule.insert(0, sec.to_owned());
+
+ Ok(self.policy.insert(rule))
+ }
+
+ async fn add_policies(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rules: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ let mut all_added = true;
+ let rules: Vec<Vec<String>> = rules
+ .into_iter()
+ .map(|mut rule| {
+ rule.insert(0, ptype.to_owned());
+ rule.insert(0, sec.to_owned());
+ rule
+ })
+ .collect();
+
+ for rule in &rules {
+ if self.policy.contains(rule) {
+ all_added = false;
+ return Ok(all_added);
+ }
+ }
+ self.policy.extend(rules);
+
+ Ok(all_added)
+ }
+
+ async fn remove_policies(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rules: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ let mut all_removed = true;
+ let rules: Vec<Vec<String>> = rules
+ .into_iter()
+ .map(|mut rule| {
+ rule.insert(0, ptype.to_owned());
+ rule.insert(0, sec.to_owned());
+ rule
+ })
+ .collect();
+
+ for rule in &rules {
+ if !self.policy.contains(rule) {
+ all_removed = false;
+ return Ok(all_removed);
+ }
+ }
+ for rule in &rules {
+ self.policy.remove(rule);
+ }
+
+ Ok(all_removed)
+ }
+
+ async fn remove_policy(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ mut rule: Vec<String>,
+ ) -> Result<bool> {
+ rule.insert(0, ptype.to_owned());
+ rule.insert(0, sec.to_owned());
+
+ Ok(self.policy.remove(&rule))
+ }
+
+ async fn remove_filtered_policy(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Result<bool> {
+ if field_values.is_empty() {
+ return Ok(false);
+ }
+
+ let mut tmp = LinkedHashSet::new();
+ let mut res = false;
+ for rule in &self.policy {
+ if sec == rule[0] && ptype == rule[1] {
+ let mut matched = true;
+ for (i, field_value) in field_values.iter().enumerate() {
+ if !field_value.is_empty()
+ && &rule[field_index + i + 2] != field_value
+ {
+ matched = false;
+ break;
+ }
+ }
+
+ if matched {
+ res = true;
+ } else {
+ tmp.insert(rule.clone());
+ }
+ } else {
+ tmp.insert(rule.clone());
+ }
+ }
+ self.policy = tmp;
+
+ Ok(res)
+ }
+
+ fn is_filtered(&self) -> bool {
+ self.is_filtered
+ }
+}
diff --git a/casbin-rs/src/adapter/mod.rs b/casbin-rs/src/adapter/mod.rs
new file mode 100644
index 0000000..a60c248
--- /dev/null
+++ b/casbin-rs/src/adapter/mod.rs
@@ -0,0 +1,66 @@
+use async_trait::async_trait;
+
+#[cfg(all(not(target_arch = "wasm32"), not(feature = "runtime-teaclave")))]
+pub mod file_adapter;
+pub mod memory_adapter;
+pub mod null_adapter;
+
+#[cfg(all(
+ not(target_arch = "wasm32"),
+ not(feature = "runtime-teaclave")
+))]
+pub use file_adapter::FileAdapter;
+pub use memory_adapter::MemoryAdapter;
+pub use null_adapter::NullAdapter;
+
+use crate::{model::Model, Result};
+
+#[derive(Clone)]
+pub struct Filter<'a> {
+ pub p: Vec<&'a str>,
+ pub g: Vec<&'a str>,
+}
+
+#[async_trait]
+pub trait Adapter: Send + Sync {
+ async fn load_policy(&self, m: &mut dyn Model) -> Result<()>;
+ async fn load_filtered_policy<'a>(
+ &mut self,
+ m: &mut dyn Model,
+ f: Filter<'a>,
+ ) -> Result<()>;
+ async fn save_policy(&mut self, m: &mut dyn Model) -> Result<()>;
+ async fn clear_policy(&mut self) -> Result<()>;
+ fn is_filtered(&self) -> bool;
+ async fn add_policy(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rule: Vec<String>,
+ ) -> Result<bool>;
+ async fn add_policies(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rules: Vec<Vec<String>>,
+ ) -> Result<bool>;
+ async fn remove_policy(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rule: Vec<String>,
+ ) -> Result<bool>;
+ async fn remove_policies(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rules: Vec<Vec<String>>,
+ ) -> Result<bool>;
+ async fn remove_filtered_policy(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Result<bool>;
+}
diff --git a/casbin-rs/src/adapter/null_adapter.rs b/casbin-rs/src/adapter/null_adapter.rs
new file mode 100644
index 0000000..bd4e655
--- /dev/null
+++ b/casbin-rs/src/adapter/null_adapter.rs
@@ -0,0 +1,82 @@
+use crate::{
+ adapter::{Adapter, Filter},
+ model::Model,
+ Result,
+};
+
+use async_trait::async_trait;
+
+pub struct NullAdapter;
+
+#[async_trait]
+impl Adapter for NullAdapter {
+ async fn load_policy(&self, _m: &mut dyn Model) -> Result<()> {
+ Ok(())
+ }
+
+ async fn load_filtered_policy<'a>(
+ &mut self,
+ _m: &mut dyn Model,
+ _f: Filter<'a>,
+ ) -> Result<()> {
+ Ok(())
+ }
+
+ async fn save_policy(&mut self, _m: &mut dyn Model) -> Result<()> {
+ Ok(())
+ }
+
+ async fn clear_policy(&mut self) -> Result<()> {
+ Ok(())
+ }
+
+ async fn add_policy(
+ &mut self,
+ _sec: &str,
+ _ptype: &str,
+ _rule: Vec<String>,
+ ) -> Result<bool> {
+ Ok(true)
+ }
+
+ async fn add_policies(
+ &mut self,
+ _sec: &str,
+ _ptype: &str,
+ _rules: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ Ok(true)
+ }
+
+ async fn remove_policies(
+ &mut self,
+ _sec: &str,
+ _ptype: &str,
+ _rules: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ Ok(true)
+ }
+
+ async fn remove_policy(
+ &mut self,
+ _sec: &str,
+ _ptype: &str,
+ _rule: Vec<String>,
+ ) -> Result<bool> {
+ Ok(true)
+ }
+
+ async fn remove_filtered_policy(
+ &mut self,
+ _sec: &str,
+ _ptype: &str,
+ _field_index: usize,
+ _field_values: Vec<String>,
+ ) -> Result<bool> {
+ Ok(true)
+ }
+
+ fn is_filtered(&self) -> bool {
+ false
+ }
+}
diff --git a/casbin-rs/src/cache/default_cache.rs b/casbin-rs/src/cache/default_cache.rs
new file mode 100644
index 0000000..3a473f4
--- /dev/null
+++ b/casbin-rs/src/cache/default_cache.rs
@@ -0,0 +1,68 @@
+use crate::cache::Cache;
+use mini_moka::sync::Cache as MokaCache;
+use std::hash::Hash;
+
+pub struct DefaultCache<K, V>
+where
+ K: Eq + Hash + Send + Sync + 'static,
+ V: Send + Sync + Clone + 'static,
+{
+ cache: MokaCache<K, V>,
+}
+
+impl<K, V> DefaultCache<K, V>
+where
+ K: Eq + Hash + Send + Sync + 'static,
+ V: Send + Sync + Clone + 'static,
+{
+ pub fn new(cap: usize) -> DefaultCache<K, V> {
+ DefaultCache {
+ cache: MokaCache::new(cap as u64),
+ }
+ }
+}
+
+impl<K, V> Cache<K, V> for DefaultCache<K, V>
+where
+ K: Eq + Hash + Send + Sync + 'static,
+ V: Send + Sync + Clone + 'static,
+{
+ fn get(&self, k: &K) -> Option<V> {
+ self.cache.get(k)
+ }
+
+ fn has(&self, k: &K) -> bool {
+ self.cache.contains_key(k)
+ }
+
+ fn set(&self, k: K, v: V) {
+ self.cache.insert(k, v);
+ }
+
+ fn clear(&self) {
+ self.cache.invalidate_all();
+ }
+}
+
+#[cfg(all(test, feature = "cached"))]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_set_and_get() {
+ let cache = DefaultCache::new(1);
+
+ cache.set(vec!["alice", "/data1", "read"], false);
+ assert!(cache.get(&vec!["alice", "/data1", "read"]) == Some(false));
+ }
+
+ #[test]
+ fn test_has_and_clear() {
+ let cache = DefaultCache::new(1);
+
+ cache.set(vec!["alice", "/data1", "read"], false);
+ assert!(cache.has(&vec!["alice", "/data1", "read"]));
+ cache.clear();
+ assert!(!cache.has(&vec!["alice", "/data1", "read"]));
+ }
+}
diff --git a/casbin-rs/src/cache/mod.rs b/casbin-rs/src/cache/mod.rs
new file mode 100644
index 0000000..828fd7a
--- /dev/null
+++ b/casbin-rs/src/cache/mod.rs
@@ -0,0 +1,19 @@
+use async_trait::async_trait;
+
+use std::hash::Hash;
+
+pub mod default_cache;
+
+pub use default_cache::DefaultCache;
+
+#[async_trait]
+pub trait Cache<K, V>: Send + Sync
+where
+ K: Eq + Hash,
+ V: Clone,
+{
+ fn get(&self, k: &K) -> Option<V>;
+ fn has(&self, k: &K) -> bool;
+ fn set(&self, k: K, v: V);
+ fn clear(&self);
+}
diff --git a/casbin-rs/src/cached_api.rs b/casbin-rs/src/cached_api.rs
new file mode 100644
index 0000000..e41e7b2
--- /dev/null
+++ b/casbin-rs/src/cached_api.rs
@@ -0,0 +1,12 @@
+use crate::{cache::Cache, core_api::CoreApi};
+
+use std::hash::Hash;
+
+pub trait CachedApi<K, V>: CoreApi + Send + Sync
+where
+ K: Eq + Hash,
+ V: Clone,
+{
+ fn get_mut_cache(&mut self) -> &mut dyn Cache<K, V>;
+ fn set_cache(&mut self, cache: Box<dyn Cache<K, V>>);
+}
diff --git a/casbin-rs/src/cached_enforcer.rs b/casbin-rs/src/cached_enforcer.rs
new file mode 100644
index 0000000..3ab89b9
--- /dev/null
+++ b/casbin-rs/src/cached_enforcer.rs
@@ -0,0 +1,348 @@
+use crate::{
+ adapter::{Adapter, Filter},
+ cache::{Cache, DefaultCache},
+ cached_api::CachedApi,
+ convert::{EnforceArgs, TryIntoAdapter, TryIntoModel},
+ core_api::CoreApi,
+ effector::Effector,
+ emitter::{clear_cache, Event, EventData, EventEmitter},
+ enforcer::Enforcer,
+ model::Model,
+ rbac::RoleManager,
+ Result,
+};
+
+#[cfg(any(feature = "logging", feature = "watcher"))]
+use crate::emitter::notify_logger_and_watcher;
+
+#[cfg(feature = "watcher")]
+use crate::watcher::Watcher;
+
+#[cfg(feature = "logging")]
+use crate::logger::Logger;
+
+#[cfg(feature = "explain")]
+use crate::{error::ModelError, get_or_err};
+
+use async_trait::async_trait;
+use parking_lot::RwLock;
+use rhai::{Dynamic, ImmutableString};
+
+use std::{collections::HashMap, sync::Arc};
+
+type EventCallback = fn(&mut CachedEnforcer, EventData);
+
+pub struct CachedEnforcer {
+ enforcer: Enforcer,
+ cache: Box<dyn Cache<u64, bool>>,
+ events: HashMap<Event, Vec<EventCallback>>,
+}
+
+impl EventEmitter<Event> for CachedEnforcer {
+ fn on(&mut self, e: Event, f: fn(&mut Self, EventData)) {
+ self.events.entry(e).or_insert_with(Vec::new).push(f)
+ }
+
+ fn off(&mut self, e: Event) {
+ self.events.remove(&e);
+ }
+
+ fn emit(&mut self, e: Event, d: EventData) {
+ if let Some(cbs) = self.events.get(&e) {
+ for cb in cbs.clone().iter() {
+ cb(self, d.clone())
+ }
+ }
+ }
+}
+
+impl CachedEnforcer {
+ pub(crate) fn private_enforce(
+ &self,
+ rvals: &[Dynamic],
+ cache_key: u64,
+ ) -> Result<(bool, bool, Option<Vec<usize>>)> {
+ Ok(if let Some(authorized) = self.cache.get(&cache_key) {
+ (authorized, true, None)
+ } else {
+ let (authorized, indices) =
+ self.enforcer.private_enforce(&rvals)?;
+ self.cache.set(cache_key, authorized);
+ (authorized, false, indices)
+ })
+ }
+}
+
+#[async_trait]
+impl CoreApi for CachedEnforcer {
+ async fn new_raw<M: TryIntoModel, A: TryIntoAdapter>(
+ m: M,
+ a: A,
+ ) -> Result<CachedEnforcer> {
+ let enforcer = Enforcer::new_raw(m, a).await?;
+ let cache = Box::new(DefaultCache::new(200));
+
+ let mut cached_enforcer = CachedEnforcer {
+ enforcer,
+ cache,
+ events: HashMap::new(),
+ };
+
+ cached_enforcer.on(Event::ClearCache, clear_cache);
+
+ #[cfg(any(feature = "logging", feature = "watcher"))]
+ cached_enforcer.on(Event::PolicyChange, notify_logger_and_watcher);
+
+ Ok(cached_enforcer)
+ }
+
+ #[inline]
+ async fn new<M: TryIntoModel, A: TryIntoAdapter>(
+ m: M,
+ a: A,
+ ) -> Result<CachedEnforcer> {
+ let mut cached_enforcer = Self::new_raw(m, a).await?;
+ cached_enforcer.load_policy().await?;
+ Ok(cached_enforcer)
+ }
+
+ #[inline]
+ fn add_function(
+ &mut self,
+ fname: &str,
+ f: fn(ImmutableString, ImmutableString) -> bool,
+ ) {
+ self.enforcer.add_function(fname, f);
+ }
+
+ #[inline]
+ fn get_model(&self) -> &dyn Model {
+ self.enforcer.get_model()
+ }
+
+ #[inline]
+ fn get_mut_model(&mut self) -> &mut dyn Model {
+ self.enforcer.get_mut_model()
+ }
+
+ #[inline]
+ fn get_adapter(&self) -> &dyn Adapter {
+ self.enforcer.get_adapter()
+ }
+
+ #[inline]
+ fn get_mut_adapter(&mut self) -> &mut dyn Adapter {
+ self.enforcer.get_mut_adapter()
+ }
+
+ #[cfg(feature = "watcher")]
+ #[inline]
+ fn set_watcher(&mut self, w: Box<dyn Watcher>) {
+ self.enforcer.set_watcher(w);
+ }
+
+ #[cfg(feature = "watcher")]
+ #[inline]
+ fn get_watcher(&self) -> Option<&dyn Watcher> {
+ self.enforcer.get_watcher()
+ }
+
+ #[cfg(feature = "watcher")]
+ #[inline]
+ fn get_mut_watcher(&mut self) -> Option<&mut dyn Watcher> {
+ self.enforcer.get_mut_watcher()
+ }
+ #[inline]
+ fn get_role_manager(&self) -> Arc<RwLock<dyn RoleManager>> {
+ self.enforcer.get_role_manager()
+ }
+
+ #[inline]
+ fn set_role_manager(
+ &mut self,
+ rm: Arc<RwLock<dyn RoleManager>>,
+ ) -> Result<()> {
+ self.enforcer.set_role_manager(rm)
+ }
+
+ #[inline]
+ async fn set_model<M: TryIntoModel>(&mut self, m: M) -> Result<()> {
+ self.enforcer.set_model(m).await
+ }
+
+ #[inline]
+ async fn set_adapter<A: TryIntoAdapter>(&mut self, a: A) -> Result<()> {
+ self.enforcer.set_adapter(a).await
+ }
+
+ #[cfg(feature = "logging")]
+ #[inline]
+ fn get_logger(&self) -> &dyn Logger {
+ self.enforcer.get_logger()
+ }
+
+ #[cfg(feature = "logging")]
+ #[inline]
+ fn set_logger(&mut self, l: Box<dyn Logger>) {
+ self.enforcer.set_logger(l);
+ }
+
+ #[inline]
+ fn set_effector(&mut self, e: Box<dyn Effector>) {
+ self.enforcer.set_effector(e);
+ }
+
+ fn enforce<ARGS: EnforceArgs>(&self, rvals: ARGS) -> Result<bool> {
+ let cache_key = rvals.cache_key();
+ let rvals = rvals.try_into_vec()?;
+ #[allow(unused_variables)]
+ let (authorized, cached, indices) =
+ self.private_enforce(&rvals, cache_key)?;
+
+ #[cfg(feature = "logging")]
+ {
+ self.enforcer.get_logger().print_enforce_log(
+ rvals.iter().map(|x| x.to_string()).collect(),
+ authorized,
+ cached,
+ );
+
+ #[cfg(feature = "explain")]
+ if let Some(indices) = indices {
+ let all_rules = get_or_err!(self, "p", ModelError::P, "policy")
+ .get_policy();
+
+ let rules: Vec<String> = indices
+ .into_iter()
+ .filter_map(|y| {
+ all_rules.iter().nth(y).map(|x| x.join(", "))
+ })
+ .collect();
+
+ self.enforcer.get_logger().print_explain_log(rules);
+ }
+ }
+
+ Ok(authorized)
+ }
+
+ #[inline]
+ fn enforce_mut<ARGS: EnforceArgs>(&mut self, rvals: ARGS) -> Result<bool> {
+ self.enforce(rvals)
+ }
+
+ #[inline]
+ fn build_role_links(&mut self) -> Result<()> {
+ self.enforcer.build_role_links()
+ }
+
+ #[cfg(feature = "incremental")]
+ #[inline]
+ fn build_incremental_role_links(&mut self, d: EventData) -> Result<()> {
+ self.enforcer.build_incremental_role_links(d)
+ }
+
+ #[inline]
+ async fn load_policy(&mut self) -> Result<()> {
+ self.enforcer.load_policy().await
+ }
+
+ #[inline]
+ async fn load_filtered_policy<'a>(&mut self, f: Filter<'a>) -> Result<()> {
+ self.enforcer.load_filtered_policy(f).await
+ }
+
+ #[inline]
+ fn is_filtered(&self) -> bool {
+ self.enforcer.is_filtered()
+ }
+
+ #[inline]
+ fn is_enabled(&self) -> bool {
+ self.enforcer.is_enabled()
+ }
+
+ #[inline]
+ async fn save_policy(&mut self) -> Result<()> {
+ self.enforcer.save_policy().await
+ }
+
+ #[inline]
+ async fn clear_policy(&mut self) -> Result<()> {
+ self.enforcer.clear_policy().await
+ }
+
+ #[cfg(feature = "logging")]
+ #[inline]
+ fn enable_log(&mut self, enabled: bool) {
+ self.enforcer.enable_log(enabled);
+ }
+
+ #[inline]
+ fn enable_enforce(&mut self, enabled: bool) {
+ self.enforcer.enable_enforce(enabled);
+ }
+
+ #[inline]
+ fn enable_auto_save(&mut self, auto_save: bool) {
+ self.enforcer.enable_auto_save(auto_save);
+ }
+
+ #[inline]
+ fn enable_auto_build_role_links(&mut self, auto_build_role_links: bool) {
+ self.enforcer
+ .enable_auto_build_role_links(auto_build_role_links);
+ }
+
+ #[cfg(feature = "watcher")]
+ #[inline]
+ fn enable_auto_notify_watcher(&mut self, auto_notify_watcher: bool) {
+ self.enforcer
+ .enable_auto_notify_watcher(auto_notify_watcher);
+ }
+
+ #[inline]
+ fn has_auto_save_enabled(&self) -> bool {
+ self.enforcer.has_auto_save_enabled()
+ }
+
+ #[cfg(feature = "watcher")]
+ #[inline]
+ fn has_auto_notify_watcher_enabled(&self) -> bool {
+ self.enforcer.has_auto_notify_watcher_enabled()
+ }
+
+ #[inline]
+ fn has_auto_build_role_links_enabled(&self) -> bool {
+ self.enforcer.has_auto_build_role_links_enabled()
+ }
+}
+
+impl CachedApi<u64, bool> for CachedEnforcer {
+ fn get_mut_cache(&mut self) -> &mut dyn Cache<u64, bool> {
+ &mut *self.cache
+ }
+
+ fn set_cache(&mut self, cache: Box<dyn Cache<u64, bool>>) {
+ self.cache = cache;
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn is_send<T: Send>() -> bool {
+ true
+ }
+
+ fn is_sync<T: Sync>() -> bool {
+ true
+ }
+
+ #[test]
+ fn test_send_sync() {
+ assert!(is_send::<CachedEnforcer>());
+ assert!(is_sync::<CachedEnforcer>());
+ }
+}
diff --git a/casbin-rs/src/config.rs b/casbin-rs/src/config.rs
new file mode 100644
index 0000000..f4a39ac
--- /dev/null
+++ b/casbin-rs/src/config.rs
@@ -0,0 +1,354 @@
+use crate::Result;
+
+#[cfg(feature = "runtime-async-std")]
+use async_std::{
+ io::prelude::*,
+ io::{BufReader, Cursor, Error as IoError, ErrorKind},
+};
+
+#[cfg(all(feature = "runtime-async-std", not(target_arch = "wasm32")))]
+use async_std::{fs::File, path::Path};
+
+#[cfg(feature = "runtime-tokio")]
+use std::{io::Cursor, path::Path};
+#[cfg(any(feature = "runtime-tokio", feature = "runtime-teaclave"))]
+use tokio::io::{AsyncBufReadExt, BufReader, Error as IoError, ErrorKind};
+
+#[cfg(all(feature = "runtime-tokio", not(target_arch = "wasm32")))]
+use tokio::fs::File;
+
+use std::collections::HashMap;
+
+#[cfg(feature = "runtime-teaclave")]
+use std::io::Cursor;
+#[cfg(feature = "runtime-tokio")]
+use tokio::io::AsyncReadExt;
+
+const DEFAULT_SECTION: &str = "default";
+const DEFAULT_COMMENT: &str = "#";
+const DEFAULT_COMMENT_SEM: &str = ";";
+const DEFAULT_MULTI_LINE_SEPARATOR: &str = "\\";
+
+pub(crate) struct Config {
+ data: HashMap<String, HashMap<String, String>>,
+}
+
+impl Config {
+ #[cfg(all(not(target_arch = "wasm32"), not(feature = "runtime-teaclave")))]
+ pub(crate) async fn from_file<P: AsRef<Path>>(p: P) -> Result<Self> {
+ let mut c = Config {
+ data: HashMap::new(),
+ };
+
+ c.parse(p).await?;
+ Ok(c)
+ }
+
+ pub(crate) async fn from_str<S: AsRef<str>>(s: S) -> Result<Self> {
+ let mut c = Config {
+ data: HashMap::new(),
+ };
+
+ c.parse_buffer(&mut BufReader::new(Cursor::new(s.as_ref().as_bytes())))
+ .await?;
+ Ok(c)
+ }
+
+ #[cfg(all(not(target_arch = "wasm32"), not(feature = "runtime-teaclave")))]
+ async fn parse<P: AsRef<Path>>(&mut self, p: P) -> Result<()> {
+ let mut f = File::open(p).await?;
+ let mut c = Vec::new();
+ f.read_to_end(&mut c).await?;
+
+ let mut reader: BufReader<Cursor<&[u8]>> =
+ BufReader::new(Cursor::new(&c));
+ self.parse_buffer(&mut reader).await
+ }
+
+ async fn parse_buffer(
+ &mut self,
+ reader: &mut BufReader<Cursor<&[u8]>>,
+ ) -> Result<()> {
+ let mut section = String::new();
+
+ loop {
+ let mut line = String::new();
+ let bytes = reader.read_line(&mut line).await?;
+ if bytes == 0 {
+ // EOF reached
+ break Ok(());
+ }
+ line = line.trim().to_string();
+ if line.is_empty()
+ || line.starts_with(DEFAULT_COMMENT)
+ || line.starts_with(DEFAULT_COMMENT_SEM)
+ {
+ continue;
+ } else if line.starts_with('[') && line.ends_with(']') {
+ section = line[1..line.len() - 1].to_string();
+ } else {
+ let mut next_section = String::new();
+ while line.ends_with(DEFAULT_MULTI_LINE_SEPARATOR) {
+ line = line[..line.len() - 1].trim_end().to_string();
+
+ let mut inner_line = String::new();
+ let inner_bytes = reader.read_line(&mut inner_line).await?;
+ if inner_bytes == 0 {
+ break;
+ }
+
+ let inner_line = inner_line.trim().to_string();
+ if inner_line.is_empty()
+ || inner_line.starts_with(DEFAULT_COMMENT)
+ || inner_line.starts_with(DEFAULT_COMMENT_SEM)
+ {
+ continue;
+ }
+
+ if inner_line.starts_with('[') && inner_line.ends_with(']')
+ {
+ next_section =
+ inner_line[1..inner_line.len() - 1].to_string();
+ } else {
+ line.push_str(&inner_line);
+ }
+ }
+
+ let option_val: Vec<&str> = line
+ .trim_end_matches(|c| {
+ char::is_whitespace(c)
+ || char::to_string(&c)
+ == DEFAULT_MULTI_LINE_SEPARATOR
+ })
+ .splitn(2, '=')
+ .map(|e| e.trim())
+ .collect();
+
+ if option_val.len() != 2 {
+ return Err(IoError::new(
+ ErrorKind::Other,
+ format!("parse content error, line={}", line),
+ )
+ .into());
+ }
+
+ self.add_config(
+ section.clone(),
+ option_val[0].to_string(),
+ option_val[1].to_string(),
+ );
+
+ if !next_section.is_empty() {
+ section = next_section;
+ }
+ }
+ }
+ }
+
+ pub(crate) fn add_config(
+ &mut self,
+ mut section: String,
+ option: String,
+ value: String,
+ ) {
+ if section.is_empty() {
+ section = DEFAULT_SECTION.to_owned();
+ }
+ let section_value =
+ self.data.entry(section).or_insert_with(HashMap::new);
+
+ // if key not exists then insert, else update
+ let key_value = section_value.get_mut(&option);
+ match key_value {
+ Some(old_value) => {
+ *old_value = value;
+ }
+ None => {
+ section_value.insert(option, value);
+ }
+ }
+ }
+
+ pub fn get(&self, key: &str) -> Option<&str> {
+ let keys: Vec<String> =
+ key.to_lowercase().split("::").map(String::from).collect();
+ if keys.len() >= 2 {
+ let section = &keys[0];
+ let option = &keys[1];
+ self.data
+ .get(section)
+ .and_then(|m| m.get(option).map(|v| v.as_str()))
+ } else {
+ let section = DEFAULT_SECTION;
+ let option = &keys[0];
+ self.data
+ .get(section)
+ .and_then(|m| m.get(option).map(|v| v.as_str()))
+ }
+ }
+
+ #[allow(dead_code)]
+ pub(crate) fn set(&mut self, key: &str, value: &str) {
+ assert!(!key.is_empty(), "key can't be empty");
+ let keys: Vec<String> =
+ key.to_lowercase().split("::").map(String::from).collect();
+ if keys.len() >= 2 {
+ let section = &keys[0];
+ let option = &keys[1];
+ self.add_config(
+ section.to_owned(),
+ option.to_owned(),
+ value.to_owned(),
+ );
+ } else {
+ let section = DEFAULT_SECTION;
+ let option = &keys[0];
+ self.add_config(
+ section.to_owned(),
+ option.to_owned(),
+ value.to_owned(),
+ );
+ }
+ }
+
+ #[allow(dead_code)]
+ pub(crate) fn get_bool(&self, key: &str) -> Option<bool> {
+ self.get(key).and_then(|v| v.parse::<bool>().ok())
+ }
+
+ #[allow(dead_code)]
+ pub(crate) fn get_string(&self, key: &str) -> Option<String> {
+ self.get_str(key).map(|v| v.to_string())
+ }
+
+ pub(crate) fn get_str(&self, key: &str) -> Option<&str> {
+ self.get(key)
+ }
+
+ #[allow(dead_code)]
+ pub(crate) fn get_int(&self, key: &str) -> Option<i64> {
+ self.get(key).and_then(|v| v.parse::<i64>().ok())
+ }
+
+ #[allow(dead_code)]
+ pub(crate) fn get_float(&self, key: &str) -> Option<f64> {
+ self.get(key).and_then(|v| v.parse::<f64>().ok())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_get() {
+ let mut config =
+ Config::from_file("examples/testini.ini").await.unwrap();
+
+ assert_eq!(Some(true), config.get_bool("debug"));
+ assert_eq!(Some(64), config.get_int("math::math.i64"));
+ assert_eq!(Some(64.1), config.get_float("math::math.f64"));
+ assert_eq!(
+ Some("10.0.0.1".to_owned()),
+ config.get_string("mysql::mysql.master.host")
+ );
+
+ config.set("other::key1", "new test key");
+ assert_eq!(
+ Some("new test key".to_owned()),
+ config.get_string("other::key1")
+ );
+
+ config.set("other::key1", "test key");
+ assert_eq!(
+ Some("test key".to_owned()),
+ config.get_string("other::key1")
+ );
+
+ assert_eq!(
+ Some("r.sub==p.sub&&r.obj==p.obj".to_owned()),
+ config.get_string("multi1::name")
+ );
+ assert_eq!(
+ Some("r.sub==p.sub&&r.obj==p.obj".to_owned()),
+ config.get_string("multi2::name")
+ );
+ assert_eq!(
+ Some("r.sub==p.sub&&r.obj==p.obj".to_owned()),
+ config.get_string("multi3::name")
+ );
+ assert_eq!(Some("".to_owned()), config.get_string("multi4::name"));
+ assert_eq!(
+ Some("r.sub==p.sub&&r.obj==p.obj".to_owned()),
+ config.get_string("multi5::name")
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_from_text() {
+ let text: &str = r#"
+ # test config
+ debug = true
+ url = act.wiki
+
+ ; redis config
+ [redis]
+ redis.key = push1,push2
+
+ ; mysql config
+ [mysql]
+ mysql.dev.host = 127.0.0.1
+ mysql.dev.user = root
+ mysql.dev.pass = 123456
+ mysql.dev.db = test
+
+ mysql.master.host = 10.0.0.1
+ mysql.master.user = root
+ mysql.master.pass = 89dds)2$#d
+ mysql.master.db = act
+
+ ; math config
+ [math]
+ math.i64 = 64
+ math.f64 = 64.1
+ "#;
+
+ let mut config = Config::from_str(text).await.unwrap();
+
+ assert_eq!(Some(true), config.get_bool("debug"));
+ assert_eq!(Some(64), config.get_int("math::math.i64"));
+ assert_eq!(Some(64.1), config.get_float("math::math.f64"));
+ assert_eq!(
+ Some("10.0.0.1".to_owned()),
+ config.get_string("mysql::mysql.master.host")
+ );
+
+ config.set("other::key1", "new test key");
+ assert_eq!(
+ Some("new test key".to_owned()),
+ config.get_string("other::key1")
+ );
+
+ config.set("other::key1", "test key");
+ assert_eq!(
+ Some("test key".to_owned()),
+ config.get_string("other::key1")
+ );
+ }
+}
diff --git a/casbin-rs/src/convert.rs b/casbin-rs/src/convert.rs
new file mode 100644
index 0000000..fa71c89
--- /dev/null
+++ b/casbin-rs/src/convert.rs
@@ -0,0 +1,171 @@
+#![allow(non_snake_case)]
+use crate::{Adapter, DefaultModel, Model, NullAdapter, Result};
+
+#[cfg(all(
+ not(target_arch = "wasm32"),
+ not(feature = "runtime-teaclave")
+))]
+use crate::FileAdapter;
+
+use async_trait::async_trait;
+use rhai::{serde::to_dynamic, Dynamic};
+use serde::Serialize;
+
+use std::{
+ collections::hash_map::DefaultHasher,
+ hash::{Hash, Hasher},
+};
+
+#[async_trait]
+pub trait TryIntoModel: Send + Sync {
+ async fn try_into_model(self) -> Result<Box<dyn Model>>;
+}
+
+#[async_trait]
+pub trait TryIntoAdapter: Send + Sync {
+ async fn try_into_adapter(self) -> Result<Box<dyn Adapter>>;
+}
+
+#[async_trait]
+impl TryIntoModel for &'static str {
+ async fn try_into_model(self) -> Result<Box<dyn Model>> {
+ #[cfg(all(
+ not(target_arch = "wasm32"),
+ not(feature = "runtime-teaclave")
+ ))]
+ {
+ Ok(Box::new(DefaultModel::from_file(self).await?))
+ }
+ #[cfg(any(target_arch = "wasm32", feature = "runtime-teaclave"))]
+ {
+ Ok(Box::new(DefaultModel::from_str(self).await?))
+ }
+ }
+}
+
+#[async_trait]
+impl<T> TryIntoModel for Option<T>
+where
+ T: TryIntoModel,
+{
+ #[allow(clippy::box_default)]
+ async fn try_into_model(self) -> Result<Box<dyn Model>> {
+ if let Some(m) = self {
+ m.try_into_model().await
+ } else {
+ Ok(Box::new(DefaultModel::default()))
+ }
+ }
+}
+
+#[async_trait]
+impl TryIntoAdapter for &'static str {
+ async fn try_into_adapter(self) -> Result<Box<dyn Adapter>> {
+ #[cfg(all(
+ not(target_arch = "wasm32"),
+ not(feature = "runtime-teaclave")
+ ))]
+ {
+ Ok(Box::new(FileAdapter::new(self)))
+ }
+
+ #[cfg(any(target_arch = "wasm32", feature = "runtime-teaclave"))]
+ {
+ Ok(Box::new(NullAdapter))
+ }
+ }
+}
+
+#[async_trait]
+impl<T> TryIntoAdapter for Option<T>
+where
+ T: TryIntoAdapter,
+{
+ async fn try_into_adapter(self) -> Result<Box<dyn Adapter>> {
+ if let Some(a) = self {
+ a.try_into_adapter().await
+ } else {
+ Ok(Box::new(NullAdapter))
+ }
+ }
+}
+
+#[allow(clippy::unit_arg)]
+#[async_trait]
+impl TryIntoAdapter for () {
+ async fn try_into_adapter(self) -> Result<Box<dyn Adapter>> {
+ Ok(Box::new(NullAdapter))
+ }
+}
+
+#[async_trait]
+impl<T> TryIntoModel for T
+where
+ T: Model + 'static,
+{
+ async fn try_into_model(self) -> Result<Box<dyn Model>> {
+ Ok(Box::new(self))
+ }
+}
+
+#[async_trait]
+impl<T> TryIntoAdapter for T
+where
+ T: Adapter + 'static,
+{
+ async fn try_into_adapter(self) -> Result<Box<dyn Adapter>> {
+ Ok(Box::new(self))
+ }
+}
+
+pub trait EnforceArgs {
+ fn try_into_vec(self) -> Result<Vec<Dynamic>>;
+ fn cache_key(&self) -> u64;
+}
+
+impl EnforceArgs for Vec<String> {
+ fn try_into_vec(self) -> Result<Vec<Dynamic>> {
+ Ok(self.into_iter().map(Dynamic::from).collect())
+ }
+
+ fn cache_key(&self) -> u64 {
+ let mut hasher = DefaultHasher::new();
+ self.hash(&mut hasher);
+ hasher.finish()
+ }
+}
+
+macro_rules! impl_args {
+ ($($p:ident),*) => {
+ impl<$($p: Serialize + Hash),*> EnforceArgs for ($($p,)*)
+ {
+ fn try_into_vec(self) -> Result<Vec<Dynamic>> {
+ let ($($p,)*) = self;
+ let _v = vec![$(to_dynamic($p)?,)*];
+
+ Ok(_v)
+ }
+
+ fn cache_key(&self) -> u64 {
+ let ($($p,)*) = self;
+ let mut _hasher = DefaultHasher::new();
+
+ $($p.hash(&mut _hasher);)*
+
+ _hasher.finish()
+ }
+ }
+
+ impl_args!(@pop $($p),*);
+ };
+ (@pop) => {
+ };
+ (@pop $head:ident) => {
+ impl_args!();
+ };
+ (@pop $head:ident $(, $tail:ident)+) => {
+ impl_args!($($tail),*);
+ };
+}
+
+impl_args!(A, B, C, D, E, F, G, H, J, K, L, M, N, P, Q, R, S, T, U, V);
diff --git a/casbin-rs/src/core_api.rs b/casbin-rs/src/core_api.rs
new file mode 100644
index 0000000..5fdbb49
--- /dev/null
+++ b/casbin-rs/src/core_api.rs
@@ -0,0 +1,96 @@
+use crate::{
+ Adapter, Effector, EnforceArgs, Event, EventEmitter, Filter, Model, Result,
+ RoleManager, TryIntoAdapter, TryIntoModel,
+};
+
+#[cfg(feature = "watcher")]
+use crate::Watcher;
+
+#[cfg(feature = "logging")]
+use crate::Logger;
+
+#[cfg(feature = "incremental")]
+use crate::emitter::EventData;
+
+use async_trait::async_trait;
+use parking_lot::RwLock;
+use rhai::ImmutableString;
+
+use std::sync::Arc;
+
+#[async_trait]
+pub trait CoreApi: Send + Sync {
+ async fn new_raw<M: TryIntoModel, A: TryIntoAdapter>(
+ m: M,
+ a: A,
+ ) -> Result<Self>
+ where
+ Self: Sized;
+ async fn new<M: TryIntoModel, A: TryIntoAdapter>(
+ m: M,
+ a: A,
+ ) -> Result<Self>
+ where
+ Self: Sized;
+ fn add_function(
+ &mut self,
+ fname: &str,
+ f: fn(ImmutableString, ImmutableString) -> bool,
+ );
+ fn get_model(&self) -> &dyn Model;
+ fn get_mut_model(&mut self) -> &mut dyn Model;
+ fn get_adapter(&self) -> &dyn Adapter;
+ fn get_mut_adapter(&mut self) -> &mut dyn Adapter;
+ #[cfg(feature = "watcher")]
+ fn set_watcher(&mut self, w: Box<dyn Watcher>);
+ #[cfg(feature = "watcher")]
+ fn get_watcher(&self) -> Option<&dyn Watcher>;
+ #[cfg(feature = "watcher")]
+ fn get_mut_watcher(&mut self) -> Option<&mut dyn Watcher>;
+ fn get_role_manager(&self) -> Arc<RwLock<dyn RoleManager>>;
+ fn set_role_manager(
+ &mut self,
+ rm: Arc<RwLock<dyn RoleManager>>,
+ ) -> Result<()>;
+ #[cfg(feature = "logging")]
+ fn get_logger(&self) -> &dyn Logger;
+ #[cfg(feature = "logging")]
+ fn set_logger(&mut self, logger: Box<dyn Logger>);
+ async fn set_model<M: TryIntoModel>(&mut self, m: M) -> Result<()>
+ where
+ Self: Sized;
+ async fn set_adapter<A: TryIntoAdapter>(&mut self, a: A) -> Result<()>
+ where
+ Self: Sized;
+ fn set_effector(&mut self, e: Box<dyn Effector>);
+ fn enforce<ARGS: EnforceArgs>(&self, rvals: ARGS) -> Result<bool>
+ where
+ Self: Sized;
+ fn enforce_mut<ARGS: EnforceArgs>(&mut self, rvals: ARGS) -> Result<bool>
+ where
+ Self: Sized;
+ fn build_role_links(&mut self) -> Result<()>;
+ #[cfg(feature = "incremental")]
+ fn build_incremental_role_links(&mut self, d: EventData) -> Result<()>;
+ async fn load_policy(&mut self) -> Result<()>;
+ async fn load_filtered_policy<'a>(&mut self, f: Filter<'a>) -> Result<()>;
+ fn is_filtered(&self) -> bool;
+ fn is_enabled(&self) -> bool;
+ async fn save_policy(&mut self) -> Result<()>;
+ async fn clear_policy(&mut self) -> Result<()>;
+ #[cfg(feature = "logging")]
+ fn enable_log(&mut self, enabled: bool);
+ fn enable_auto_save(&mut self, auto_save: bool);
+ fn enable_enforce(&mut self, enabled: bool);
+ fn enable_auto_build_role_links(&mut self, auto_build_role_links: bool);
+ #[cfg(feature = "watcher")]
+ fn enable_auto_notify_watcher(&mut self, auto_notify_watcher: bool);
+ fn has_auto_save_enabled(&self) -> bool;
+ #[cfg(feature = "watcher")]
+ fn has_auto_notify_watcher_enabled(&self) -> bool;
+ fn has_auto_build_role_links_enabled(&self) -> bool;
+}
+
+pub trait IEnforcer: CoreApi + EventEmitter<Event> {}
+
+impl<T> IEnforcer for T where T: CoreApi + EventEmitter<Event> {}
diff --git a/casbin-rs/src/effector.rs b/casbin-rs/src/effector.rs
new file mode 100644
index 0000000..bc7fc4e
--- /dev/null
+++ b/casbin-rs/src/effector.rs
@@ -0,0 +1,126 @@
+use crate::push_index_if_explain;
+
+#[derive(PartialEq, Eq, Clone, Copy)]
+pub enum EffectKind {
+ Allow = 0,
+ Indeterminate = 1,
+ Deny = 2,
+}
+
+pub trait Effector: Send + Sync {
+ fn new_stream(&self, expr: &str, cap: usize) -> Box<dyn EffectorStream>;
+}
+
+pub trait EffectorStream: Send + Sync {
+ fn next(&self) -> bool;
+ #[cfg(feature = "explain")]
+ fn explain(&self) -> Option<Vec<usize>>;
+ fn push_effect(&mut self, eft: EffectKind) -> bool;
+}
+
+#[derive(Clone)]
+pub struct DefaultEffectStream {
+ done: bool,
+ res: bool,
+ expr: String,
+ idx: usize,
+ cap: usize,
+ #[cfg(feature = "explain")]
+ expl: Vec<usize>,
+}
+
+#[derive(Default)]
+pub struct DefaultEffector;
+
+impl Effector for DefaultEffector {
+ fn new_stream(&self, expr: &str, cap: usize) -> Box<dyn EffectorStream> {
+ assert!(cap > 0);
+
+ let res = match expr {
+ "some(where (p_eft == allow))"
+ | "some(where (p_eft == allow)) && !some(where (p_eft == deny))"
+ | "priority(p_eft) || deny" => false,
+ "!some(where (p_eft == deny))" => true,
+ _ => panic!("unsupported effect: `{}`", expr),
+ };
+
+ Box::new(DefaultEffectStream {
+ done: false,
+ res,
+ expr: expr.to_owned(),
+ cap,
+ idx: 0,
+ #[cfg(feature = "explain")]
+ expl: Vec::with_capacity(10),
+ })
+ }
+}
+
+impl EffectorStream for DefaultEffectStream {
+ #[inline]
+ fn next(&self) -> bool {
+ assert!(self.done);
+ self.res
+ }
+
+ #[cfg(feature = "explain")]
+ #[inline]
+ fn explain(&self) -> Option<Vec<usize>> {
+ assert!(self.done);
+ if self.expl.is_empty() {
+ None
+ } else {
+ Some(self.expl.clone())
+ }
+ }
+
+ fn push_effect(&mut self, eft: EffectKind) -> bool {
+ if self.expr == "some(where (p_eft == allow))" {
+ if eft == EffectKind::Allow {
+ self.done = true;
+ self.res = true;
+
+ push_index_if_explain!(self);
+ }
+ } else if self.expr
+ == "some(where (p_eft == allow)) && !some(where (p_eft == deny))"
+ {
+ if eft == EffectKind::Allow {
+ self.res = true;
+
+ push_index_if_explain!(self)
+ } else if eft == EffectKind::Deny {
+ self.done = true;
+ self.res = false;
+
+ push_index_if_explain!(self)
+ }
+ } else if self.expr == "!some(where (p_eft == deny))" {
+ if eft == EffectKind::Deny {
+ self.done = true;
+ self.res = false;
+
+ push_index_if_explain!(self)
+ }
+ } else if self.expr == "priority(p_eft) || deny"
+ && eft != EffectKind::Indeterminate
+ {
+ if eft == EffectKind::Allow {
+ self.res = true;
+ } else {
+ self.res = false;
+ }
+
+ self.done = true;
+ }
+
+ if self.idx + 1 == self.cap {
+ self.done = true;
+ self.idx = self.cap;
+ } else {
+ self.idx += 1;
+ }
+
+ self.done
+ }
+}
diff --git a/casbin-rs/src/emitter.rs b/casbin-rs/src/emitter.rs
new file mode 100644
index 0000000..8c7b914
--- /dev/null
+++ b/casbin-rs/src/emitter.rs
@@ -0,0 +1,115 @@
+#[cfg(any(feature = "watcher", feature = "cached", feature = "logging"))]
+use crate::core_api::CoreApi;
+
+#[cfg(feature = "cached")]
+use crate::cached_api::CachedApi;
+
+use std::{fmt, hash::Hash};
+
+#[derive(Hash, PartialEq, Eq)]
+pub enum Event {
+ PolicyChange,
+ ClearCache,
+}
+
+pub trait EventKey: Hash + PartialEq + Eq + Send + Sync {}
+impl<T> EventKey for T where T: Hash + PartialEq + Eq + Send + Sync {}
+
+#[derive(Clone)]
+pub enum EventData {
+ AddPolicy(String, String, Vec<String>),
+ AddPolicies(String, String, Vec<Vec<String>>),
+ RemovePolicy(String, String, Vec<String>),
+ RemovePolicies(String, String, Vec<Vec<String>>),
+ RemoveFilteredPolicy(String, String, Vec<Vec<String>>),
+ SavePolicy(Vec<Vec<String>>),
+ ClearPolicy,
+ ClearCache,
+}
+
+impl fmt::Display for EventData {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ use EventData::*;
+ match *self {
+ AddPolicy(ref sec, ref ptype, ref p) => write!(
+ f,
+ "Type: AddPolicy, Assertion: {}::{}, Data: {:?}",
+ sec,
+ ptype,
+ p.join(", ")
+ ),
+ AddPolicies(ref sec, ref ptype, ref p) => write!(
+ f,
+ "Type: AddPolicies, Assertion: {}::{}, Added: {}",
+ sec,
+ ptype,
+ p.len()
+ ),
+ RemovePolicy(ref sec, ref ptype, ref p) => write!(
+ f,
+ "Type: RemovePolicy, Assertion: {}::{}, Data: {:?}",
+ sec,
+ ptype,
+ p.join(", ")
+ ),
+ RemovePolicies(ref sec, ref ptype, ref p) => write!(
+ f,
+ "Type: RemovePolicies, Assertion: {}::{}, Removed: {}",
+ sec,
+ ptype,
+ p.len()
+ ),
+ RemoveFilteredPolicy(ref sec, ref ptype, ref p) => write!(
+ f,
+ "Type: RemoveFilteredPolicy, Assertion: {}::{}, Removed: {}",
+ sec,
+ ptype,
+ p.len()
+ ),
+ SavePolicy(ref p) => {
+ write!(f, "Type: SavePolicy, Saved: {}", p.len())
+ }
+ ClearPolicy => write!(f, "Type: ClearPolicy"),
+ ClearCache => write!(f, "Type: ClearCache, Data: ClearCache"),
+ }
+ }
+}
+
+pub trait EventEmitter<K>
+where
+ K: EventKey,
+{
+ fn on(&mut self, e: K, f: fn(&mut Self, EventData))
+ where
+ Self: Sized;
+ fn off(&mut self, e: K);
+ fn emit(&mut self, e: K, d: EventData);
+}
+
+#[cfg(any(feature = "logging", feature = "watcher"))]
+pub(crate) fn notify_logger_and_watcher<T: CoreApi>(e: &mut T, d: EventData) {
+ #[cfg(feature = "logging")]
+ {
+ e.get_logger().print_mgmt_log(&d);
+ }
+
+ #[cfg(feature = "watcher")]
+ {
+ if let Some(w) = e.get_mut_watcher() {
+ w.update(d);
+ }
+ }
+}
+
+#[cfg(feature = "cached")]
+#[allow(unused_variables)]
+pub(crate) fn clear_cache<T: CoreApi + CachedApi<u64, bool>>(
+ ce: &mut T,
+ d: EventData,
+) {
+ #[cfg(feature = "logging")]
+ {
+ ce.get_logger().print_mgmt_log(&d);
+ }
+ ce.get_mut_cache().clear();
+}
diff --git a/casbin-rs/src/enforcer.rs b/casbin-rs/src/enforcer.rs
new file mode 100644
index 0000000..b29b399
--- /dev/null
+++ b/casbin-rs/src/enforcer.rs
@@ -0,0 +1,1342 @@
+use crate::{
+ adapter::{Adapter, Filter},
+ convert::{EnforceArgs, TryIntoAdapter, TryIntoModel},
+ core_api::CoreApi,
+ effector::{DefaultEffector, EffectKind, Effector},
+ emitter::{Event, EventData, EventEmitter},
+ error::{ModelError, PolicyError, RequestError},
+ get_or_err,
+ management_api::MgmtApi,
+ model::{FunctionMap, Model},
+ rbac::{DefaultRoleManager, RoleManager},
+ register_g_function,
+ util::{escape_assertion, escape_eval},
+ Result,
+};
+
+#[cfg(any(feature = "logging", feature = "watcher"))]
+use crate::emitter::notify_logger_and_watcher;
+
+#[cfg(feature = "watcher")]
+use crate::watcher::Watcher;
+
+#[cfg(feature = "logging")]
+use crate::{DefaultLogger, Logger};
+
+use async_trait::async_trait;
+use once_cell::sync::Lazy;
+use parking_lot::RwLock;
+use rhai::{
+ def_package,
+ packages::{
+ ArithmeticPackage, BasicArrayPackage, BasicMapPackage, LogicPackage,
+ Package,
+ },
+ Dynamic, Engine, EvalAltResult, ImmutableString, Scope,
+};
+
+def_package! {
+ pub CasbinPackage(lib) {
+ ArithmeticPackage::init(lib);
+ LogicPackage::init(lib);
+ BasicArrayPackage::init(lib);
+ BasicMapPackage::init(lib);
+
+ lib.set_native_fn("escape_assertion", |s: ImmutableString| {
+ Ok(escape_assertion(&s))
+ });
+ }
+}
+
+static CASBIN_PACKAGE: Lazy<CasbinPackage> = Lazy::new(CasbinPackage::new);
+
+use std::{cmp::max, collections::HashMap, sync::Arc};
+
+type EventCallback = fn(&mut Enforcer, EventData);
+
+/// Enforcer is the main interface for authorization enforcement and policy management.
+pub struct Enforcer {
+ model: Box<dyn Model>,
+ adapter: Box<dyn Adapter>,
+ fm: FunctionMap,
+ eft: Box<dyn Effector>,
+ rm: Arc<RwLock<dyn RoleManager>>,
+ enabled: bool,
+ auto_save: bool,
+ auto_build_role_links: bool,
+ #[cfg(feature = "watcher")]
+ auto_notify_watcher: bool,
+ #[cfg(feature = "watcher")]
+ watcher: Option<Box<dyn Watcher>>,
+ events: HashMap<Event, Vec<EventCallback>>,
+ engine: Engine,
+ #[cfg(feature = "logging")]
+ logger: Box<dyn Logger>,
+}
+
+impl EventEmitter<Event> for Enforcer {
+ fn on(&mut self, e: Event, f: fn(&mut Self, EventData)) {
+ self.events.entry(e).or_insert_with(Vec::new).push(f)
+ }
+
+ fn off(&mut self, e: Event) {
+ self.events.remove(&e);
+ }
+
+ fn emit(&mut self, e: Event, d: EventData) {
+ if let Some(cbs) = self.events.get(&e) {
+ for cb in cbs.clone().iter() {
+ cb(self, d.clone())
+ }
+ }
+ }
+}
+
+impl Enforcer {
+ pub(crate) fn private_enforce(
+ &self,
+ rvals: &[Dynamic],
+ ) -> Result<(bool, Option<Vec<usize>>)> {
+ if !self.enabled {
+ return Ok((true, None));
+ }
+
+ let mut scope: Scope = Scope::new();
+
+ let r_ast = get_or_err!(self, "r", ModelError::R, "request");
+ let p_ast = get_or_err!(self, "p", ModelError::P, "policy");
+ let m_ast = get_or_err!(self, "m", ModelError::M, "matcher");
+ let e_ast = get_or_err!(self, "e", ModelError::E, "effector");
+
+ if r_ast.tokens.len() != rvals.len() {
+ return Err(RequestError::UnmatchRequestDefinition(
+ r_ast.tokens.len(),
+ rvals.len(),
+ )
+ .into());
+ }
+
+ for (rtoken, rval) in r_ast.tokens.iter().zip(rvals.iter()) {
+ scope.push_constant_dynamic(rtoken, rval.to_owned());
+ }
+
+ let policies = p_ast.get_policy();
+ let (policy_len, scope_len) = (policies.len(), scope.len());
+
+ let mut eft_stream =
+ self.eft.new_stream(&e_ast.value, max(policy_len, 1));
+ let m_ast_compiled = self
+ .engine
+ .compile_expression(&escape_eval(&m_ast.value))
+ .map_err(Into::<Box<EvalAltResult>>::into)?;
+
+ if policy_len == 0 {
+ for token in p_ast.tokens.iter() {
+ scope.push_constant(token, String::new());
+ }
+
+ let eval_result = self
+ .engine
+ .eval_ast_with_scope::<bool>(&mut scope, &m_ast_compiled)?;
+ let eft = if eval_result {
+ EffectKind::Allow
+ } else {
+ EffectKind::Indeterminate
+ };
+
+ eft_stream.push_effect(eft);
+
+ return Ok((eft_stream.next(), None));
+ }
+
+ for pvals in policies {
+ scope.rewind(scope_len);
+
+ if p_ast.tokens.len() != pvals.len() {
+ return Err(PolicyError::UnmatchPolicyDefinition(
+ p_ast.tokens.len(),
+ pvals.len(),
+ )
+ .into());
+ }
+ for (ptoken, pval) in p_ast.tokens.iter().zip(pvals.iter()) {
+ scope.push_constant(ptoken, pval.to_owned());
+ }
+
+ let eval_result = self
+ .engine
+ .eval_ast_with_scope::<bool>(&mut scope, &m_ast_compiled)?;
+ let eft = match p_ast.tokens.iter().position(|x| x == "p_eft") {
+ Some(j) if eval_result => {
+ let p_eft = &pvals[j];
+ if p_eft == "deny" {
+ EffectKind::Deny
+ } else if p_eft == "allow" {
+ EffectKind::Allow
+ } else {
+ EffectKind::Indeterminate
+ }
+ }
+ None if eval_result => EffectKind::Allow,
+ _ => EffectKind::Indeterminate,
+ };
+
+ if eft_stream.push_effect(eft) {
+ break;
+ }
+ }
+
+ Ok((eft_stream.next(), {
+ #[cfg(feature = "explain")]
+ {
+ eft_stream.explain()
+ }
+ #[cfg(not(feature = "explain"))]
+ {
+ None
+ }
+ }))
+ }
+
+ pub(crate) fn register_g_functions(&mut self) -> Result<()> {
+ if let Some(ast_map) = self.model.get_model().get("g") {
+ for (fname, ast) in ast_map {
+ register_g_function!(self, fname, ast);
+ }
+ }
+
+ Ok(())
+ }
+}
+
+#[async_trait]
+impl CoreApi for Enforcer {
+ #[allow(clippy::box_default)]
+ async fn new_raw<M: TryIntoModel, A: TryIntoAdapter>(
+ m: M,
+ a: A,
+ ) -> Result<Self> {
+ let model = m.try_into_model().await?;
+ let adapter = a.try_into_adapter().await?;
+ let fm = FunctionMap::default();
+ let eft = Box::new(DefaultEffector::default());
+ let rm = Arc::new(RwLock::new(DefaultRoleManager::new(10)));
+
+ let mut engine = Engine::new_raw();
+
+ engine.register_global_module(CASBIN_PACKAGE.as_shared_module());
+
+ for (key, &func) in fm.get_functions() {
+ engine.register_fn(key, func);
+ }
+
+ let mut e = Self {
+ model,
+ adapter,
+ fm,
+ eft,
+ rm,
+ enabled: true,
+ auto_save: true,
+ auto_build_role_links: true,
+ #[cfg(feature = "watcher")]
+ auto_notify_watcher: true,
+ #[cfg(feature = "watcher")]
+ watcher: None,
+ events: HashMap::new(),
+ engine,
+ #[cfg(feature = "logging")]
+ logger: Box::new(DefaultLogger::default()),
+ };
+
+ #[cfg(any(feature = "logging", feature = "watcher"))]
+ e.on(Event::PolicyChange, notify_logger_and_watcher);
+
+ e.register_g_functions()?;
+
+ Ok(e)
+ }
+
+ #[inline]
+ async fn new<M: TryIntoModel, A: TryIntoAdapter>(
+ m: M,
+ a: A,
+ ) -> Result<Self> {
+ let mut e = Self::new_raw(m, a).await?;
+ e.load_policy().await?;
+ Ok(e)
+ }
+
+ #[inline]
+ fn add_function(
+ &mut self,
+ fname: &str,
+ f: fn(ImmutableString, ImmutableString) -> bool,
+ ) {
+ self.fm.add_function(fname, f);
+ self.engine.register_fn(fname, f);
+ }
+
+ #[inline]
+ fn get_model(&self) -> &dyn Model {
+ &*self.model
+ }
+
+ #[inline]
+ fn get_mut_model(&mut self) -> &mut dyn Model {
+ &mut *self.model
+ }
+
+ #[inline]
+ fn get_adapter(&self) -> &dyn Adapter {
+ &*self.adapter
+ }
+
+ #[inline]
+ fn get_mut_adapter(&mut self) -> &mut dyn Adapter {
+ &mut *self.adapter
+ }
+
+ #[cfg(feature = "watcher")]
+ #[inline]
+ fn set_watcher(&mut self, w: Box<dyn Watcher>) {
+ self.watcher = Some(w);
+ }
+
+ #[cfg(feature = "logging")]
+ #[inline]
+ fn get_logger(&self) -> &dyn Logger {
+ &*self.logger
+ }
+
+ #[cfg(feature = "logging")]
+ #[inline]
+ fn set_logger(&mut self, l: Box<dyn Logger>) {
+ self.logger = l;
+ }
+
+ #[cfg(feature = "watcher")]
+ #[inline]
+ fn get_watcher(&self) -> Option<&dyn Watcher> {
+ if let Some(ref watcher) = self.watcher {
+ Some(&**watcher)
+ } else {
+ None
+ }
+ }
+
+ #[cfg(feature = "watcher")]
+ #[inline]
+ fn get_mut_watcher(&mut self) -> Option<&mut dyn Watcher> {
+ if let Some(ref mut watcher) = self.watcher {
+ Some(&mut **watcher)
+ } else {
+ None
+ }
+ }
+
+ #[inline]
+ fn get_role_manager(&self) -> Arc<RwLock<dyn RoleManager>> {
+ Arc::clone(&self.rm)
+ }
+
+ #[inline]
+ fn set_role_manager(
+ &mut self,
+ rm: Arc<RwLock<dyn RoleManager>>,
+ ) -> Result<()> {
+ self.rm = rm;
+ if self.auto_build_role_links {
+ self.build_role_links()?;
+ }
+
+ self.register_g_functions()
+ }
+
+ async fn set_model<M: TryIntoModel>(&mut self, m: M) -> Result<()> {
+ self.model = m.try_into_model().await?;
+ self.load_policy().await?;
+ Ok(())
+ }
+
+ async fn set_adapter<A: TryIntoAdapter>(&mut self, a: A) -> Result<()> {
+ self.adapter = a.try_into_adapter().await?;
+ self.load_policy().await?;
+ Ok(())
+ }
+
+ #[inline]
+ fn set_effector(&mut self, e: Box<dyn Effector>) {
+ self.eft = e;
+ }
+
+ /// Enforce decides whether a "subject" can access a "object" with the operation "action",
+ /// input parameters are usually: (sub, obj, act).
+ ///
+ /// # Examples
+ /// ```
+ /// use casbin::prelude::*;
+ /// #[cfg(feature = "runtime-async-std")]
+ /// #[async_std::main]
+ /// async fn main() -> Result<()> {
+ /// let mut e = Enforcer::new("examples/basic_model.conf", "examples/basic_policy.csv").await?;
+ /// assert_eq!(true, e.enforce(("alice", "data1", "read"))?);
+ /// Ok(())
+ /// }
+ ///
+ /// #[cfg(feature = "runtime-tokio")]
+ /// #[tokio::main]
+ /// async fn main() -> Result<()> {
+ /// let mut e = Enforcer::new("examples/basic_model.conf", "examples/basic_policy.csv").await?;
+ /// assert_eq!(true, e.enforce(("alice", "data1", "read"))?);
+ ///
+ /// Ok(())
+ /// }
+ /// #[cfg(all(not(feature = "runtime-async-std"), not(feature = "runtime-tokio")))]
+ /// fn main() {}
+ /// ```
+ fn enforce<ARGS: EnforceArgs>(&self, rvals: ARGS) -> Result<bool> {
+ let rvals = rvals.try_into_vec()?;
+ #[allow(unused_variables)]
+ let (authorized, indices) = self.private_enforce(&rvals)?;
+
+ #[cfg(feature = "logging")]
+ {
+ self.logger.print_enforce_log(
+ rvals.iter().map(|x| x.to_string()).collect(),
+ authorized,
+ false,
+ );
+
+ #[cfg(feature = "explain")]
+ if let Some(indices) = indices {
+ let all_rules = get_or_err!(self, "p", ModelError::P, "policy")
+ .get_policy();
+
+ let rules: Vec<String> = indices
+ .into_iter()
+ .filter_map(|y| {
+ all_rules.iter().nth(y).map(|x| x.join(", "))
+ })
+ .collect();
+
+ self.logger.print_explain_log(rules);
+ }
+ }
+
+ Ok(authorized)
+ }
+
+ fn enforce_mut<ARGS: EnforceArgs>(&mut self, rvals: ARGS) -> Result<bool> {
+ self.enforce(rvals)
+ }
+
+ fn build_role_links(&mut self) -> Result<()> {
+ self.rm.write().clear();
+ self.model.build_role_links(Arc::clone(&self.rm))?;
+
+ Ok(())
+ }
+
+ #[cfg(feature = "incremental")]
+ fn build_incremental_role_links(&mut self, d: EventData) -> Result<()> {
+ self.model
+ .build_incremental_role_links(Arc::clone(&self.rm), d)?;
+
+ Ok(())
+ }
+
+ async fn load_policy(&mut self) -> Result<()> {
+ self.model.clear_policy();
+ self.adapter.load_policy(&mut *self.model).await?;
+
+ if self.auto_build_role_links {
+ self.build_role_links()?;
+ }
+
+ Ok(())
+ }
+
+ async fn load_filtered_policy<'a>(&mut self, f: Filter<'a>) -> Result<()> {
+ self.model.clear_policy();
+ self.adapter
+ .load_filtered_policy(&mut *self.model, f)
+ .await?;
+
+ if self.auto_build_role_links {
+ self.build_role_links()?;
+ }
+
+ Ok(())
+ }
+
+ #[inline]
+ fn is_filtered(&self) -> bool {
+ self.adapter.is_filtered()
+ }
+
+ #[inline]
+ fn is_enabled(&self) -> bool {
+ self.enabled
+ }
+
+ async fn save_policy(&mut self) -> Result<()> {
+ assert!(!self.is_filtered(), "cannot save filtered policy");
+
+ self.adapter.save_policy(&mut *self.model).await?;
+
+ let mut policies = self.get_all_policy();
+ let gpolicies = self.get_all_grouping_policy();
+
+ policies.extend(gpolicies);
+
+ #[cfg(any(feature = "logging", feature = "watcher"))]
+ self.emit(Event::PolicyChange, EventData::SavePolicy(policies));
+
+ Ok(())
+ }
+
+ #[inline]
+ async fn clear_policy(&mut self) -> Result<()> {
+ if self.auto_save {
+ self.adapter.clear_policy().await?;
+ }
+ self.model.clear_policy();
+
+ #[cfg(any(feature = "logging", feature = "watcher"))]
+ self.emit(Event::PolicyChange, EventData::ClearPolicy);
+
+ Ok(())
+ }
+
+ #[inline]
+ fn enable_enforce(&mut self, enabled: bool) {
+ self.enabled = enabled;
+
+ #[cfg(feature = "logging")]
+ self.logger.print_status_log(enabled);
+ }
+
+ #[cfg(feature = "logging")]
+ #[inline]
+ fn enable_log(&mut self, enabled: bool) {
+ self.logger.enable_log(enabled);
+ }
+
+ #[inline]
+ fn enable_auto_save(&mut self, auto_save: bool) {
+ self.auto_save = auto_save;
+ }
+
+ #[inline]
+ fn enable_auto_build_role_links(&mut self, auto_build_role_links: bool) {
+ self.auto_build_role_links = auto_build_role_links;
+ }
+
+ #[cfg(feature = "watcher")]
+ #[inline]
+ fn enable_auto_notify_watcher(&mut self, auto_notify_watcher: bool) {
+ if !auto_notify_watcher {
+ self.off(Event::PolicyChange);
+ } else {
+ self.on(Event::PolicyChange, notify_logger_and_watcher);
+ }
+
+ self.auto_notify_watcher = auto_notify_watcher;
+ }
+
+ #[inline]
+ fn has_auto_save_enabled(&self) -> bool {
+ self.auto_save
+ }
+
+ #[cfg(feature = "watcher")]
+ #[inline]
+ fn has_auto_notify_watcher_enabled(&self) -> bool {
+ self.auto_notify_watcher
+ }
+
+ #[inline]
+ fn has_auto_build_role_links_enabled(&self) -> bool {
+ self.auto_build_role_links
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::prelude::*;
+
+ fn is_send<T: Send>() -> bool {
+ true
+ }
+
+ fn is_sync<T: Sync>() -> bool {
+ true
+ }
+
+ #[test]
+ fn test_send_sync() {
+ assert!(is_send::<Enforcer>());
+ assert!(is_sync::<Enforcer>());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_enforcer_swap_adapter_type() {
+ let mut m = DefaultModel::default();
+ m.add_def("r", "r", "sub, obj, act");
+ m.add_def("p", "p", "sub, obj, act");
+ m.add_def("e", "e", "some(where (p.eft == allow))");
+ m.add_def(
+ "m",
+ "m",
+ "r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)",
+ );
+
+ let file = FileAdapter::new("examples/basic_policy.csv");
+ let mem = MemoryAdapter::default();
+ let mut e = Enforcer::new(m, file).await.unwrap();
+ // this should fail since FileAdapter has basically no add_policy
+ assert!(e
+ .adapter
+ .add_policy(
+ "p",
+ "p",
+ vec!["alice".into(), "data".into(), "read".into()]
+ )
+ .await
+ .unwrap());
+ e.set_adapter(mem).await.unwrap();
+ // this passes since our MemoryAdapter has a working add_policy method
+ assert!(e
+ .adapter
+ .add_policy(
+ "p",
+ "p",
+ vec!["alice".into(), "data".into(), "read".into()]
+ )
+ .await
+ .unwrap())
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_key_match_model_in_memory() {
+ let mut m = DefaultModel::default();
+ m.add_def("r", "r", "sub, obj, act");
+ m.add_def("p", "p", "sub, obj, act");
+ m.add_def("e", "e", "some(where (p.eft == allow))");
+ m.add_def(
+ "m",
+ "m",
+ "r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)",
+ );
+
+ let adapter = FileAdapter::new("examples/keymatch_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+ assert_eq!(
+ true,
+ e.enforce(("alice", "/alice_data/resource1", "GET"))
+ .unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("alice", "/alice_data/resource1", "POST"))
+ .unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("alice", "/alice_data/resource2", "GET"))
+ .unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "/alice_data/resource2", "POST"))
+ .unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "/bob_data/resource1", "GET")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "/bob_data/resource1", "POST")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "/bob_data/resource2", "GET")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "/bob_data/resource2", "POST")).unwrap()
+ );
+
+ assert_eq!(
+ false,
+ e.enforce(("bob", "/alice_data/resource1", "GET")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("bob", "/alice_data/resource1", "POST")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("bob", "/alice_data/resource2", "GET")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("bob", "/alice_data/resource2", "POST")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("bob", "/bob_data/resource1", "GET")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("bob", "/bob_data/resource1", "POST")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("bob", "/bob_data/resource2", "GET")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("bob", "/bob_data/resource2", "POST")).unwrap()
+ );
+
+ assert_eq!(true, e.enforce(("cathy", "/cathy_data", "GET")).unwrap());
+ assert_eq!(true, e.enforce(("cathy", "/cathy_data", "POST")).unwrap());
+ assert_eq!(
+ false,
+ e.enforce(("cathy", "/cathy_data", "DELETE")).unwrap()
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_key_match_model_in_memory_deny() {
+ let mut m = DefaultModel::default();
+ m.add_def("r", "r", "sub, obj, act");
+ m.add_def("p", "p", "sub, obj, act");
+ m.add_def("e", "e", "!some(where (p.eft == deny))");
+ m.add_def(
+ "m",
+ "m",
+ "r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)",
+ );
+
+ let adapter = FileAdapter::new("examples/keymatch_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+ assert_eq!(
+ true,
+ e.enforce(("alice", "/alice_data/resource2", "POST"))
+ .unwrap()
+ );
+ }
+
+ use crate::RbacApi;
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_rbac_model_in_memory_indeterminate() {
+ let mut m = DefaultModel::default();
+ m.add_def("r", "r", "sub, obj, act");
+ m.add_def("p", "p", "sub, obj, act");
+ m.add_def("g", "g", "_, _");
+ m.add_def("e", "e", "some(where (p.eft == allow))");
+ m.add_def(
+ "m",
+ "m",
+ "g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act",
+ );
+
+ let adapter = MemoryAdapter::default();
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+ e.add_permission_for_user(
+ "alice",
+ vec!["data1", "invalid"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ assert_eq!(false, e.enforce(("alice", "data1", "read")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_rbac_model_in_memory() {
+ let mut m = DefaultModel::default();
+ m.add_def("r", "r", "sub, obj, act");
+ m.add_def("p", "p", "sub, obj, act");
+ m.add_def("g", "g", "_, _");
+ m.add_def("e", "e", "some(where (p.eft == allow))");
+ m.add_def(
+ "m",
+ "m",
+ "g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act",
+ );
+
+ let adapter = MemoryAdapter::default();
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+ e.add_permission_for_user(
+ "alice",
+ vec!["data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_permission_for_user(
+ "bob",
+ vec!["data2", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_permission_for_user(
+ "data2_admin",
+ vec!["data2", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_permission_for_user(
+ "data2_admin",
+ vec!["data2", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_role_for_user("alice", "data2_admin", None)
+ .await
+ .unwrap();
+
+ assert_eq!(true, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data1", "write")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_not_used_rbac_model_in_memory() {
+ let mut m = DefaultModel::default();
+ m.add_def("r", "r", "sub, obj, act");
+ m.add_def("p", "p", "sub, obj, act");
+ m.add_def("g", "g", "_, _");
+ m.add_def("e", "e", "some(where (p.eft == allow))");
+ m.add_def(
+ "m",
+ "m",
+ "g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act",
+ );
+
+ let adapter = MemoryAdapter::default();
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+ e.add_permission_for_user(
+ "alice",
+ vec!["data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_permission_for_user(
+ "bob",
+ vec!["data2", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(true, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data2", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data2", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data2", "write")).unwrap());
+ }
+
+ #[cfg(feature = "ip")]
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_ip_match_model() {
+ let m = DefaultModel::from_file("examples/ipmatch_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/ipmatch_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert!(e.enforce(("192.168.2.123", "data1", "read")).unwrap());
+
+ assert!(e.enforce(("10.0.0.5", "data2", "write")).unwrap());
+
+ assert!(!e.enforce(("192.168.2.123", "data1", "write")).unwrap());
+ assert!(!e.enforce(("192.168.2.123", "data2", "read")).unwrap());
+ assert!(!e.enforce(("192.168.2.123", "data2", "write")).unwrap());
+
+ assert!(!e.enforce(("192.168.0.123", "data1", "read")).unwrap());
+ assert!(!e.enforce(("192.168.0.123", "data1", "write")).unwrap());
+ assert!(!e.enforce(("192.168.0.123", "data2", "read")).unwrap());
+ assert!(!e.enforce(("192.168.0.123", "data2", "write")).unwrap());
+
+ assert!(!e.enforce(("10.0.0.5", "data1", "read")).unwrap());
+ assert!(!e.enforce(("10.0.0.5", "data1", "write")).unwrap());
+ assert!(!e.enforce(("10.0.0.5", "data2", "read")).unwrap());
+
+ assert!(!e.enforce(("192.168.0.1", "data1", "read")).unwrap());
+ assert!(!e.enforce(("192.168.0.1", "data1", "write")).unwrap());
+ assert!(!e.enforce(("192.168.0.1", "data2", "read")).unwrap());
+ assert!(!e.enforce(("192.168.0.1", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_enable_auto_save() {
+ let m = DefaultModel::from_file("examples/basic_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/basic_policy.csv");
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+ e.enable_auto_save(false);
+ e.remove_policy(
+ vec!["alice", "data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.load_policy().await.unwrap();
+
+ assert_eq!(true, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data2", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data2", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data2", "write")).unwrap());
+
+ e.enable_auto_save(true);
+ e.remove_policy(
+ vec!["alice", "data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.load_policy().await.unwrap();
+ assert_eq!(true, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data2", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data2", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_role_links() {
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = MemoryAdapter::default();
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+ e.enable_auto_build_role_links(false);
+ e.build_role_links().unwrap();
+ assert_eq!(false, e.enforce(("user501", "data9", "read")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_get_and_set_model() {
+ let m1 = DefaultModel::from_file("examples/basic_model.conf")
+ .await
+ .unwrap();
+ let adapter1 = FileAdapter::new("examples/basic_policy.csv");
+ let mut e = Enforcer::new(m1, adapter1).await.unwrap();
+
+ assert_eq!(false, e.enforce(("root", "data1", "read")).unwrap());
+
+ let m2 = DefaultModel::from_file("examples/basic_with_root_model.conf")
+ .await
+ .unwrap();
+ let adapter2 = FileAdapter::new("examples/basic_policy.csv");
+ let e2 = Enforcer::new(m2, adapter2).await.unwrap();
+
+ e.model = e2.model;
+ assert_eq!(true, e.enforce(("root", "data1", "read")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_get_and_set_adapter_in_mem() {
+ let m1 = DefaultModel::from_file("examples/basic_model.conf")
+ .await
+ .unwrap();
+ let adapter1 = FileAdapter::new("examples/basic_policy.csv");
+ let mut e = Enforcer::new(m1, adapter1).await.unwrap();
+
+ assert_eq!(true, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data1", "write")).unwrap());
+
+ let m2 = DefaultModel::from_file("examples/basic_model.conf")
+ .await
+ .unwrap();
+ let adapter2 = FileAdapter::new("examples/basic_inverse_policy.csv");
+ let e2 = Enforcer::new(m2, adapter2).await.unwrap();
+
+ e.adapter = e2.adapter;
+ e.load_policy().await.unwrap();
+ assert_eq!(false, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data1", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_keymatch_custom_model() {
+ use crate::model::key_match;
+
+ let m1 = DefaultModel::from_file("examples/keymatch_custom_model.conf")
+ .await
+ .unwrap();
+ let adapter1 = FileAdapter::new("examples/keymatch_policy.csv");
+ let mut e = Enforcer::new(m1, adapter1).await.unwrap();
+
+ e.add_function(
+ "keyMatchCustom",
+ |s1: ImmutableString, s2: ImmutableString| key_match(&s1, &s2),
+ );
+
+ assert_eq!(
+ true,
+ e.enforce(("alice", "/alice_data/123", "GET")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("alice", "/alice_data/resource1", "POST"))
+ .unwrap()
+ );
+
+ assert_eq!(
+ true,
+ e.enforce(("bob", "/alice_data/resource2", "GET")).unwrap()
+ );
+
+ assert_eq!(
+ true,
+ e.enforce(("bob", "/bob_data/resource1", "POST")).unwrap()
+ );
+
+ assert_eq!(true, e.enforce(("cathy", "/cathy_data", "GET")).unwrap());
+ assert_eq!(true, e.enforce(("cathy", "/cathy_data", "POST")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_filtered_file_adapter() {
+ let mut e = Enforcer::new(
+ "examples/rbac_with_domains_model.conf",
+ "examples/rbac_with_domains_policy.csv",
+ )
+ .await
+ .unwrap();
+
+ let filter = Filter {
+ p: vec!["", "domain1"],
+ g: vec!["", "", "domain1"],
+ };
+
+ e.load_filtered_policy(filter).await.unwrap();
+ assert_eq!(
+ e.enforce(("alice", "domain1", "data1", "read")).unwrap(),
+ true
+ );
+ assert!(e.enforce(("alice", "domain1", "data1", "write")).unwrap());
+ assert!(!e.enforce(("alice", "domain1", "data2", "read")).unwrap());
+ assert!(!e.enforce(("alice", "domain1", "data2", "write")).unwrap());
+ assert!(!e.enforce(("bob", "domain2", "data2", "read")).unwrap());
+ assert!(!e.enforce(("bob", "domain2", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_set_role_manager() {
+ let mut e = Enforcer::new(
+ "examples/rbac_with_domains_model.conf",
+ "examples/rbac_with_domains_policy.csv",
+ )
+ .await
+ .unwrap();
+
+ let new_rm = Arc::new(RwLock::new(DefaultRoleManager::new(10)));
+
+ e.set_role_manager(new_rm).unwrap();
+
+ assert!(e.enforce(("alice", "domain1", "data1", "read")).unwrap(),);
+ assert!(e.enforce(("alice", "domain1", "data1", "write")).unwrap());
+ assert!(e.enforce(("bob", "domain2", "data2", "read")).unwrap());
+ assert!(e.enforce(("bob", "domain2", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_policy_abac1() {
+ use serde::Serialize;
+
+ let mut m = DefaultModel::default();
+ m.add_def("r", "r", "sub, obj, act");
+ m.add_def("p", "p", "sub_rule, obj, act");
+ m.add_def("e", "e", "some(where (p.eft == allow))");
+ m.add_def(
+ "m",
+ "m",
+ "eval(p.sub_rule) && r.obj == p.obj && r.act == p.act",
+ );
+
+ let a = MemoryAdapter::default();
+
+ let mut e = Enforcer::new(m, a).await.unwrap();
+
+ e.add_policy(
+ vec!["r.sub.age > 18", "/data1", "read"]
+ .into_iter()
+ .map(|x| x.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+
+ #[derive(Serialize, Hash)]
+ pub struct Person<'a> {
+ name: &'a str,
+ age: u8,
+ }
+
+ assert_eq!(
+ e.enforce((
+ Person {
+ name: "alice",
+ age: 16
+ },
+ "/data1",
+ "read"
+ ))
+ .unwrap(),
+ false
+ );
+ assert_eq!(
+ e.enforce((
+ Person {
+ name: "bob",
+ age: 19
+ },
+ "/data1",
+ "read"
+ ))
+ .unwrap(),
+ true
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_policy_abac2() {
+ use serde::Serialize;
+
+ let mut m = DefaultModel::default();
+ m.add_def("r", "r", "sub, obj, act");
+ m.add_def("p", "p", "sub, obj, act");
+ m.add_def("e", "e", "some(where (p.eft == allow))");
+ m.add_def("g", "g", "_, _");
+ m.add_def(
+ "m",
+ "m",
+ "(g(r.sub, p.sub) || eval(p.sub) == true) && r.act == p.act",
+ );
+
+ let a = MemoryAdapter::default();
+
+ let mut e = Enforcer::new(m, a).await.unwrap();
+
+ e.add_policy(
+ vec![r#""admin""#, "post", "write"]
+ .into_iter()
+ .map(|x| x.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+
+ e.add_policy(
+ vec!["r.sub == r.obj.author", "post", "write"]
+ .into_iter()
+ .map(|x| x.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+
+ e.add_grouping_policy(
+ vec!["alice", r#""admin""#]
+ .into_iter()
+ .map(|x| x.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+
+ #[derive(Serialize, Hash)]
+ pub struct Post<'a> {
+ author: &'a str,
+ }
+
+ assert_eq!(
+ e.enforce(("alice", Post { author: "bob" }, "write"))
+ .unwrap(),
+ true
+ );
+
+ assert_eq!(
+ e.enforce(("bob", Post { author: "bob" }, "write")).unwrap(),
+ true
+ );
+ }
+}
diff --git a/casbin-rs/src/error.rs b/casbin-rs/src/error.rs
new file mode 100644
index 0000000..2c2673a
--- /dev/null
+++ b/casbin-rs/src/error.rs
@@ -0,0 +1,90 @@
+use rhai::{EvalAltResult, ParseError};
+use thiserror::Error;
+
+use std::{error::Error as StdError, io::Error as IoError};
+
+/// ModelError represents any type of errors in model configuration
+#[derive(Error, Debug)]
+pub enum ModelError {
+ #[error("Invalid request definition: `{0}`")]
+ R(String),
+ #[error("Invalid policy definition: `{0}`")]
+ P(String),
+ #[error("Unsupported effect: `{0}`")]
+ E(String),
+ #[error("Invalid matcher: `{0}`")]
+ M(String),
+ #[error("Other: `{0}`")]
+ Other(String),
+}
+
+/// RequestError represents any type of errors in coming request
+#[derive(Error, Debug)]
+pub enum RequestError {
+ #[error("Request doesn't match request definition. expected length: {0}, found length {1}")]
+ UnmatchRequestDefinition(usize, usize),
+}
+
+/// PolicyError represents any type of errors in policy
+#[derive(Error, Debug)]
+pub enum PolicyError {
+ #[error("Policy doesn't match policy definition. expected length: {0}, found length {1}")]
+ UnmatchPolicyDefinition(usize, usize),
+}
+
+/// RBAC error represents any type of errors in RBAC role manager
+#[derive(Error, Debug)]
+pub enum RbacError {
+ #[error("Role `{0}` not found")]
+ NotFound(String),
+}
+/// AdapterError error represents any type of errors in adapter's execution
+#[derive(Error, Debug)]
+#[error("Adapter error: {0:?}")]
+pub struct AdapterError(pub Box<dyn StdError + Send + Sync>);
+
+/// General casbin error
+#[derive(Error, Debug)]
+pub enum Error {
+ #[error("Casbin Io Error: `{0:?}`")]
+ IoError(#[from] IoError),
+
+ #[error("Casbin Model Error: `{0:?}`")]
+ ModelError(#[from] ModelError),
+
+ #[error("Casbin Policy Error: `{0:?}`")]
+ PolicyError(#[from] PolicyError),
+
+ #[error("Casbin Role Manager Error: `{0:?}`")]
+ RbacError(#[from] RbacError),
+
+ #[error("Casbin Evaluation Error: `{0:?}`")]
+ RhaiError(#[from] Box<EvalAltResult>),
+ #[error("Casbin Parse Error: `{0:?}`")]
+ RhaiParseError(#[from] ParseError),
+
+ #[error("Casbin Request Error: `{0:?}`")]
+ RequestError(#[from] RequestError),
+
+ #[error("Casbin Adapter Error: `{0:?}`")]
+ AdapterError(#[from] AdapterError),
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn is_send<T: Send>() -> bool {
+ true
+ }
+
+ fn is_sync<T: Sync>() -> bool {
+ true
+ }
+
+ #[test]
+ fn test_send_sync() {
+ assert!(is_send::<Error>());
+ assert!(is_sync::<Error>());
+ }
+}
diff --git a/casbin-rs/src/internal_api.rs b/casbin-rs/src/internal_api.rs
new file mode 100644
index 0000000..b3dde4b
--- /dev/null
+++ b/casbin-rs/src/internal_api.rs
@@ -0,0 +1,462 @@
+use crate::{core_api::IEnforcer, Result};
+
+#[cfg(any(
+ feature = "watcher",
+ feature = "cached",
+ feature = "logging",
+ feature = "incremental"
+))]
+use crate::emitter::EventData;
+
+#[cfg(any(feature = "watcher", feature = "cached", feature = "logging",))]
+use crate::emitter::Event;
+
+use async_trait::async_trait;
+
+#[async_trait]
+pub trait InternalApi: IEnforcer {
+ async fn add_policy_internal(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rule: Vec<String>,
+ ) -> Result<bool>;
+ async fn add_policies_internal(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rules: Vec<Vec<String>>,
+ ) -> Result<bool>;
+ async fn remove_policy_internal(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rule: Vec<String>,
+ ) -> Result<bool>;
+ async fn remove_policies_internal(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rules: Vec<Vec<String>>,
+ ) -> Result<bool>;
+ async fn remove_filtered_policy_internal(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Result<(bool, Vec<Vec<String>>)>;
+}
+
+#[async_trait]
+impl<T> InternalApi for T
+where
+ T: IEnforcer,
+{
+ async fn add_policy_internal(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rule: Vec<String>,
+ ) -> Result<bool> {
+ if self.has_auto_save_enabled()
+ && !self
+ .get_mut_adapter()
+ .add_policy(sec, ptype, rule.clone())
+ .await?
+ {
+ return Ok(false);
+ }
+
+ let rule_added = self.get_mut_model().add_policy(sec, ptype, {
+ #[cfg(any(
+ feature = "watcher",
+ feature = "logging",
+ feature = "incremental"
+ ))]
+ {
+ rule.clone()
+ }
+ #[cfg(all(
+ not(feature = "watcher"),
+ not(feature = "logging"),
+ not(feature = "incremental")
+ ))]
+ {
+ rule
+ }
+ });
+ #[cfg(any(feature = "watcher", feature = "logging"))]
+ {
+ let event_data =
+ EventData::AddPolicy(sec.to_owned(), ptype.to_owned(), {
+ #[cfg(feature = "incremental")]
+ {
+ rule.clone()
+ }
+ #[cfg(not(feature = "incremental"))]
+ {
+ rule
+ }
+ });
+ #[cfg(feature = "watcher")]
+ {
+ if rule_added && self.has_auto_notify_watcher_enabled() {
+ self.emit(Event::PolicyChange, event_data);
+ }
+ }
+ #[cfg(not(feature = "watcher"))]
+ {
+ if rule_added {
+ self.emit(Event::PolicyChange, event_data);
+ }
+ }
+ }
+ #[cfg(feature = "cached")]
+ {
+ if rule_added {
+ self.emit(Event::ClearCache, EventData::ClearCache);
+ }
+ }
+ if sec != "g" || !self.has_auto_build_role_links_enabled() {
+ return Ok(rule_added);
+ }
+ #[cfg(not(feature = "incremental"))]
+ {
+ self.build_role_links()?;
+ }
+ #[cfg(feature = "incremental")]
+ {
+ self.build_incremental_role_links(EventData::AddPolicy(
+ sec.to_owned(),
+ ptype.to_owned(),
+ rule,
+ ))?;
+ }
+
+ Ok(rule_added)
+ }
+
+ async fn add_policies_internal(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rules: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ if self.has_auto_save_enabled()
+ && !self
+ .get_mut_adapter()
+ .add_policies(sec, ptype, rules.clone())
+ .await?
+ {
+ return Ok(false);
+ }
+
+ let rules_added = self.get_mut_model().add_policies(sec, ptype, {
+ #[cfg(any(
+ feature = "watcher",
+ feature = "logging",
+ feature = "incremental"
+ ))]
+ {
+ rules.clone()
+ }
+ #[cfg(all(
+ not(feature = "watcher"),
+ not(feature = "logging"),
+ not(feature = "incremental")
+ ))]
+ {
+ rules
+ }
+ });
+ #[cfg(any(feature = "watcher", feature = "logging"))]
+ {
+ let event_data =
+ EventData::AddPolicies(sec.to_owned(), ptype.to_owned(), {
+ #[cfg(feature = "incremental")]
+ {
+ rules.clone()
+ }
+ #[cfg(not(feature = "incremental"))]
+ {
+ rules
+ }
+ });
+ #[cfg(feature = "watcher")]
+ {
+ if rules_added && self.has_auto_notify_watcher_enabled() {
+ self.emit(Event::PolicyChange, event_data);
+ }
+ }
+ #[cfg(not(feature = "watcher"))]
+ {
+ if rules_added {
+ self.emit(Event::PolicyChange, event_data);
+ }
+ }
+ }
+ #[cfg(feature = "cached")]
+ {
+ if rules_added {
+ self.emit(Event::ClearCache, EventData::ClearCache);
+ }
+ }
+ if sec != "g" || !self.has_auto_build_role_links_enabled() {
+ return Ok(rules_added);
+ }
+ #[cfg(not(feature = "incremental"))]
+ {
+ self.build_role_links()?;
+ }
+ #[cfg(feature = "incremental")]
+ {
+ self.build_incremental_role_links(EventData::AddPolicies(
+ sec.to_owned(),
+ ptype.to_owned(),
+ rules,
+ ))?;
+ }
+
+ Ok(rules_added)
+ }
+
+ async fn remove_policy_internal(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rule: Vec<String>,
+ ) -> Result<bool> {
+ if self.has_auto_save_enabled()
+ && !self
+ .get_mut_adapter()
+ .remove_policy(sec, ptype, rule.clone())
+ .await?
+ {
+ return Ok(false);
+ }
+
+ let rule_removed = self.get_mut_model().remove_policy(sec, ptype, {
+ #[cfg(any(
+ feature = "watcher",
+ feature = "logging",
+ feature = "incremental"
+ ))]
+ {
+ rule.clone()
+ }
+ #[cfg(all(
+ not(feature = "watcher"),
+ not(feature = "logging"),
+ not(feature = "incremental")
+ ))]
+ {
+ rule
+ }
+ });
+ #[cfg(any(feature = "watcher", feature = "logging"))]
+ {
+ let event_data =
+ EventData::RemovePolicy(sec.to_owned(), ptype.to_owned(), {
+ #[cfg(feature = "incremental")]
+ {
+ rule.clone()
+ }
+ #[cfg(not(feature = "incremental"))]
+ {
+ rule
+ }
+ });
+ #[cfg(feature = "watcher")]
+ {
+ if rule_removed && self.has_auto_notify_watcher_enabled() {
+ self.emit(Event::PolicyChange, event_data);
+ }
+ }
+ #[cfg(not(feature = "watcher"))]
+ {
+ if rule_removed {
+ self.emit(Event::PolicyChange, event_data);
+ }
+ }
+ }
+ #[cfg(feature = "cached")]
+ {
+ if rule_removed {
+ self.emit(Event::ClearCache, EventData::ClearCache);
+ }
+ }
+ if sec != "g" || !self.has_auto_build_role_links_enabled() {
+ return Ok(rule_removed);
+ }
+ #[cfg(not(feature = "incremental"))]
+ {
+ self.build_role_links()?;
+ }
+ #[cfg(feature = "incremental")]
+ {
+ self.build_incremental_role_links(EventData::RemovePolicy(
+ sec.to_owned(),
+ ptype.to_owned(),
+ rule,
+ ))?;
+ }
+
+ Ok(rule_removed)
+ }
+
+ async fn remove_policies_internal(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rules: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ if self.has_auto_save_enabled()
+ && !self
+ .get_mut_adapter()
+ .remove_policies(sec, ptype, rules.clone())
+ .await?
+ {
+ return Ok(false);
+ }
+
+ let rules_removed = self.get_mut_model().remove_policies(sec, ptype, {
+ #[cfg(any(
+ feature = "watcher",
+ feature = "logging",
+ feature = "incremental"
+ ))]
+ {
+ rules.clone()
+ }
+ #[cfg(all(
+ not(feature = "watcher"),
+ not(feature = "logging"),
+ not(feature = "incremental")
+ ))]
+ {
+ rules
+ }
+ });
+ #[cfg(any(feature = "watcher", feature = "logging"))]
+ {
+ let event_data =
+ EventData::RemovePolicies(sec.to_owned(), ptype.to_owned(), {
+ #[cfg(feature = "incremental")]
+ {
+ rules.clone()
+ }
+ #[cfg(not(feature = "incremental"))]
+ {
+ rules
+ }
+ });
+ #[cfg(feature = "watcher")]
+ {
+ if rules_removed && self.has_auto_notify_watcher_enabled() {
+ self.emit(Event::PolicyChange, event_data);
+ }
+ }
+ #[cfg(not(feature = "watcher"))]
+ {
+ if rules_removed {
+ self.emit(Event::PolicyChange, event_data);
+ }
+ }
+ }
+ #[cfg(feature = "cached")]
+ {
+ if rules_removed {
+ self.emit(Event::ClearCache, EventData::ClearCache);
+ }
+ }
+ if sec != "g" || !self.has_auto_build_role_links_enabled() {
+ return Ok(rules_removed);
+ }
+ #[cfg(not(feature = "incremental"))]
+ {
+ self.build_role_links()?;
+ }
+ #[cfg(feature = "incremental")]
+ {
+ self.build_incremental_role_links(EventData::RemovePolicies(
+ sec.to_owned(),
+ ptype.to_owned(),
+ rules,
+ ))?;
+ }
+
+ Ok(rules_removed)
+ }
+
+ async fn remove_filtered_policy_internal(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Result<(bool, Vec<Vec<String>>)> {
+ if self.has_auto_save_enabled()
+ && !self
+ .get_mut_adapter()
+ .remove_filtered_policy(
+ sec,
+ ptype,
+ field_index,
+ field_values.clone(),
+ )
+ .await?
+ {
+ return Ok((false, vec![]));
+ }
+
+ let (rules_removed, rules) = self
+ .get_mut_model()
+ .remove_filtered_policy(sec, ptype, field_index, field_values);
+ #[cfg(any(feature = "watcher", feature = "logging"))]
+ {
+ let event_data = EventData::RemoveFilteredPolicy(
+ sec.to_owned(),
+ ptype.to_owned(),
+ rules.clone(),
+ );
+ #[cfg(feature = "watcher")]
+ {
+ if rules_removed && self.has_auto_notify_watcher_enabled() {
+ self.emit(Event::PolicyChange, event_data);
+ }
+ }
+ #[cfg(not(feature = "watcher"))]
+ {
+ if rules_removed {
+ self.emit(Event::PolicyChange, event_data);
+ }
+ }
+ }
+ #[cfg(feature = "cached")]
+ {
+ if rules_removed {
+ self.emit(Event::ClearCache, EventData::ClearCache);
+ }
+ }
+ if sec != "g" || !self.has_auto_build_role_links_enabled() {
+ return Ok((rules_removed, rules));
+ }
+ #[cfg(not(feature = "incremental"))]
+ {
+ self.build_role_links()?;
+ }
+ #[cfg(feature = "incremental")]
+ {
+ self.build_incremental_role_links(
+ EventData::RemoveFilteredPolicy(
+ sec.to_owned(),
+ ptype.to_owned(),
+ rules.clone(),
+ ),
+ )?;
+ }
+
+ Ok((rules_removed, rules))
+ }
+}
diff --git a/casbin-rs/src/lib.rs b/casbin-rs/src/lib.rs
new file mode 100644
index 0000000..5ae1d82
--- /dev/null
+++ b/casbin-rs/src/lib.rs
@@ -0,0 +1,70 @@
+#[cfg(all(not(feature = "logging"), feature = "explain"))]
+compile_error!(
+ "'logging' feature must be enabled along with 'explain' feature"
+);
+
+mod adapter;
+#[cfg(feature = "cached")]
+mod cache;
+#[cfg(feature = "cached")]
+mod cached_api;
+#[cfg(feature = "cached")]
+mod cached_enforcer;
+mod config;
+mod convert;
+mod core_api;
+mod effector;
+mod emitter;
+mod enforcer;
+mod internal_api;
+#[cfg(feature = "logging")]
+mod logger;
+mod macros;
+mod management_api;
+mod model;
+mod rbac;
+mod rbac_api;
+mod util;
+#[cfg(feature = "watcher")]
+mod watcher;
+
+pub mod error;
+pub mod prelude;
+
+#[cfg(all(
+ not(target_arch = "wasm32"),
+ not(feature = "runtime-teaclave")
+))]
+pub use adapter::FileAdapter;
+pub use adapter::{Adapter, Filter, MemoryAdapter, NullAdapter};
+
+#[cfg(feature = "cached")]
+pub use cache::{Cache, DefaultCache};
+#[cfg(feature = "cached")]
+pub use cached_api::CachedApi;
+#[cfg(feature = "cached")]
+pub use cached_enforcer::CachedEnforcer;
+pub use convert::{EnforceArgs, TryIntoAdapter, TryIntoModel};
+pub use core_api::{CoreApi, IEnforcer};
+pub use effector::{
+ DefaultEffectStream, DefaultEffector, EffectKind, Effector, EffectorStream,
+};
+pub use emitter::{Event, EventData, EventEmitter, EventKey};
+pub use enforcer::Enforcer;
+pub use error::Error;
+pub use internal_api::InternalApi;
+#[cfg(feature = "logging")]
+pub use logger::{DefaultLogger, Logger};
+pub use management_api::MgmtApi;
+pub use model::{function_map, Assertion, DefaultModel, Model};
+pub use rbac::{DefaultRoleManager, MatchingFn, RoleManager};
+pub use rbac_api::RbacApi;
+#[cfg(feature = "watcher")]
+pub use watcher::Watcher;
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+// re-exports
+pub mod rhai {
+ pub use rhai::*;
+}
diff --git a/casbin-rs/src/logger/default_logger.rs b/casbin-rs/src/logger/default_logger.rs
new file mode 100644
index 0000000..e36127d
--- /dev/null
+++ b/casbin-rs/src/logger/default_logger.rs
@@ -0,0 +1,79 @@
+use crate::{emitter::EventData, logger::Logger};
+
+use slog::Drain;
+use slog::Logger as SLogger;
+use slog::{info, o};
+
+pub struct DefaultLogger {
+ enabled: bool,
+ slogger: SLogger,
+}
+
+impl Default for DefaultLogger {
+ fn default() -> Self {
+ let decorator = slog_term::TermDecorator::new().build();
+ let drain = slog_term::FullFormat::new(decorator).build().fuse();
+ let drain = slog_async::Async::new(drain)
+ .chan_size(4096)
+ .overflow_strategy(slog_async::OverflowStrategy::Block)
+ .build()
+ .fuse();
+
+ let slogger = SLogger::root(drain, o!());
+
+ Self {
+ enabled: false,
+ slogger,
+ }
+ }
+}
+
+impl Logger for DefaultLogger {
+ #[inline]
+ fn enable_log(&mut self, enabled: bool) {
+ self.enabled = enabled;
+ }
+
+ #[inline]
+ fn is_enabled(&self) -> bool {
+ self.enabled
+ }
+
+ fn print_enforce_log(
+ &self,
+ rvals: Vec<String>,
+ authorized: bool,
+ cached: bool,
+ ) {
+ if !self.is_enabled() {
+ return;
+ }
+
+ info!(self.slogger, "Enforce Request"; "Request" => rvals.join(","), "Cached" => cached, "Response" => authorized);
+ }
+
+ fn print_mgmt_log(&self, e: &EventData) {
+ if !self.is_enabled() {
+ return;
+ }
+
+ info!(self.slogger, "Policy Management"; "Event" => e.to_string());
+ }
+
+ #[cfg(feature = "explain")]
+ fn print_explain_log(&self, rules: Vec<String>) {
+ if !self.is_enabled() {
+ return;
+ }
+
+ info!(self.slogger, "Hitted Policies"; "Explain" => rules.join(","));
+ }
+
+ fn print_status_log(&self, enabled: bool) {
+ if !self.is_enabled() {
+ return;
+ }
+
+ info!(self.slogger, "Status"; "Enabled" => enabled);
+ }
+}
diff --git a/casbin-rs/src/logger/mod.rs b/casbin-rs/src/logger/mod.rs
new file mode 100644
index 0000000..10dcf57
--- /dev/null
+++ b/casbin-rs/src/logger/mod.rs
@@ -0,0 +1,53 @@
+use slog::info;
+use slog::Logger as SLogger;
+
+use crate::emitter::EventData;
+
+mod default_logger;
+
+pub use default_logger::DefaultLogger;
+
+pub trait Logger: Send + Sync {
+ fn enable_log(&mut self, enabled: bool);
+ fn is_enabled(&self) -> bool;
+ fn print_enforce_log(
+ &self,
+ rvals: Vec<String>,
+ authorized: bool,
+ cached: bool,
+ );
+ fn print_mgmt_log(&self, d: &EventData);
+ #[cfg(feature = "explain")]
+ fn print_explain_log(&self, rules: Vec<String>);
+ fn print_status_log(&self, enabled: bool);
+}
+
+impl Logger for SLogger {
+ fn enable_log(&mut self, _enabled: bool) {}
+
+ fn is_enabled(&self) -> bool {
+ true
+ }
+
+ fn print_enforce_log(
+ &self,
+ rvals: Vec<String>,
+ authorized: bool,
+ cached: bool,
+ ) {
+ info!(self, "Enforce Request"; "Request" => rvals.join(","), "Cached" => cached, "Response" => authorized);
+ }
+
+ fn print_mgmt_log(&self, e: &EventData) {
+ info!(self, "Policy Management"; "Event" => e.to_string());
+ }
+
+ #[cfg(feature = "explain")]
+ fn print_explain_log(&self, rules: Vec<String>) {
+ info!(self, "Hitted Policies"; "Explain" => rules.join(","));
+ }
+
+ fn print_status_log(&self, enabled: bool) {
+ info!(self, "Status"; "Enabled" => enabled);
+ }
+}
diff --git a/casbin-rs/src/macros.rs b/casbin-rs/src/macros.rs
new file mode 100644
index 0000000..4883604
--- /dev/null
+++ b/casbin-rs/src/macros.rs
@@ -0,0 +1,64 @@
+#[macro_export]
+macro_rules! get_or_err {
+ ($this:ident, $key:expr, $err:expr, $msg:expr) => {{
+ $this
+ .get_model()
+ .get_model()
+ .get($key)
+ .ok_or_else(|| {
+ $crate::error::Error::from($err(format!(
+ "Missing {} definition in conf file",
+ $msg
+ )))
+ })?
+ .get($key)
+ .ok_or_else(|| {
+ $crate::error::Error::from($err(format!(
+ "Missing {} section in conf file",
+ $msg
+ )))
+ })?
+ }};
+}
+
+#[macro_export]
+macro_rules! register_g_function {
+ ($enforcer:ident, $fname:ident, $ast:ident) => {{
+ let rm = Arc::clone(&$enforcer.rm);
+ let count = $ast.value.matches('_').count();
+
+ if count == 2 {
+ $enforcer.engine.register_fn(
+ $fname,
+ move |arg1: ImmutableString, arg2: ImmutableString| {
+ rm.read().has_link(&arg1, &arg2, None)
+ },
+ );
+ } else if count == 3 {
+ $enforcer.engine.register_fn(
+ $fname,
+ move |arg1: ImmutableString,
+ arg2: ImmutableString,
+ arg3: ImmutableString| {
+ rm.read().has_link(&arg1, &arg2, Some(&arg3))
+ },
+ );
+ } else {
+ return Err(ModelError::P(
+ r#"the number of "_" in role definition should be at least 2"#
+ .to_owned(),
+ )
+ .into());
+ }
+ }};
+}
+
+#[macro_export]
+macro_rules! push_index_if_explain {
+ ($this:ident) => {{
+ #[cfg(feature = "explain")]
+ if $this.cap > 1 {
+ $this.expl.push($this.idx);
+ }
+ }};
+}
diff --git a/casbin-rs/src/management_api.rs b/casbin-rs/src/management_api.rs
new file mode 100644
index 0000000..1541364
--- /dev/null
+++ b/casbin-rs/src/management_api.rs
@@ -0,0 +1,1186 @@
+use crate::{InternalApi, Result};
+
+use async_trait::async_trait;
+
+#[async_trait]
+pub trait MgmtApi: InternalApi {
+ async fn add_policy(&mut self, params: Vec<String>) -> Result<bool> {
+ self.add_named_policy("p", params).await
+ }
+
+ async fn add_policies(
+ &mut self,
+ paramss: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ self.add_named_policies("p", paramss).await
+ }
+
+ async fn remove_policy(&mut self, params: Vec<String>) -> Result<bool> {
+ self.remove_named_policy("p", params).await
+ }
+
+ async fn remove_policies(
+ &mut self,
+ paramss: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ self.remove_named_policies("p", paramss).await
+ }
+
+ async fn add_named_policy(
+ &mut self,
+ ptype: &str,
+ params: Vec<String>,
+ ) -> Result<bool>;
+ async fn add_named_policies(
+ &mut self,
+ ptype: &str,
+ paramss: Vec<Vec<String>>,
+ ) -> Result<bool>;
+ async fn remove_named_policy(
+ &mut self,
+ ptype: &str,
+ params: Vec<String>,
+ ) -> Result<bool>;
+ async fn remove_named_policies(
+ &mut self,
+ ptype: &str,
+ paramss: Vec<Vec<String>>,
+ ) -> Result<bool>;
+
+ async fn add_grouping_policy(
+ &mut self,
+ params: Vec<String>,
+ ) -> Result<bool> {
+ self.add_named_grouping_policy("g", params).await
+ }
+
+ async fn add_grouping_policies(
+ &mut self,
+ paramss: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ self.add_named_grouping_policies("g", paramss).await
+ }
+
+ async fn remove_grouping_policy(
+ &mut self,
+ params: Vec<String>,
+ ) -> Result<bool> {
+ self.remove_named_grouping_policy("g", params).await
+ }
+
+ async fn remove_grouping_policies(
+ &mut self,
+ paramss: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ self.remove_named_grouping_policies("g", paramss).await
+ }
+
+ async fn add_named_grouping_policy(
+ &mut self,
+ ptype: &str,
+ params: Vec<String>,
+ ) -> Result<bool>;
+ async fn add_named_grouping_policies(
+ &mut self,
+ ptype: &str,
+ paramss: Vec<Vec<String>>,
+ ) -> Result<bool>;
+ async fn remove_named_grouping_policy(
+ &mut self,
+ ptype: &str,
+ params: Vec<String>,
+ ) -> Result<bool>;
+ async fn remove_named_grouping_policies(
+ &mut self,
+ ptype: &str,
+ paramss: Vec<Vec<String>>,
+ ) -> Result<bool>;
+
+ async fn remove_filtered_policy(
+ &mut self,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Result<bool> {
+ self.remove_filtered_named_policy("p", field_index, field_values)
+ .await
+ }
+
+ async fn remove_filtered_grouping_policy(
+ &mut self,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Result<bool> {
+ self.remove_filtered_named_grouping_policy(
+ "g",
+ field_index,
+ field_values,
+ )
+ .await
+ }
+
+ async fn remove_filtered_named_policy(
+ &mut self,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Result<bool>;
+ async fn remove_filtered_named_grouping_policy(
+ &mut self,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Result<bool>;
+
+ fn get_policy(&self) -> Vec<Vec<String>> {
+ self.get_named_policy("p")
+ }
+
+ fn get_all_policy(&self) -> Vec<Vec<String>>;
+ fn get_named_policy(&self, ptype: &str) -> Vec<Vec<String>>;
+
+ fn get_filtered_policy(
+ &self,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Vec<Vec<String>> {
+ self.get_filtered_named_policy("p", field_index, field_values)
+ }
+
+ fn get_filtered_named_policy(
+ &self,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Vec<Vec<String>>;
+
+ fn has_policy(&self, params: Vec<String>) -> bool {
+ self.has_named_policy("p", params)
+ }
+
+ fn has_named_policy(&self, ptype: &str, params: Vec<String>) -> bool;
+
+ fn get_grouping_policy(&self) -> Vec<Vec<String>> {
+ self.get_named_grouping_policy("g")
+ }
+
+ fn get_all_grouping_policy(&self) -> Vec<Vec<String>>;
+ fn get_named_grouping_policy(&self, ptype: &str) -> Vec<Vec<String>>;
+
+ fn get_filtered_grouping_policy(
+ &self,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Vec<Vec<String>> {
+ self.get_filtered_named_grouping_policy("g", field_index, field_values)
+ }
+
+ fn get_filtered_named_grouping_policy(
+ &self,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Vec<Vec<String>>;
+ fn has_grouping_policy(&self, params: Vec<String>) -> bool {
+ self.has_grouping_named_policy("g", params)
+ }
+ fn has_grouping_named_policy(
+ &self,
+ ptype: &str,
+ params: Vec<String>,
+ ) -> bool;
+
+ fn get_all_subjects(&self) -> Vec<String> {
+ self.get_all_named_subjects("p")
+ }
+
+ fn get_all_named_subjects(&self, ptype: &str) -> Vec<String>;
+
+ fn get_all_objects(&self) -> Vec<String> {
+ self.get_all_named_objects("p")
+ }
+
+ fn get_all_named_objects(&self, ptype: &str) -> Vec<String>;
+
+ fn get_all_actions(&self) -> Vec<String> {
+ self.get_all_named_actions("p")
+ }
+
+ fn get_all_roles(&self) -> Vec<String> {
+ self.get_all_named_roles("g")
+ }
+
+ fn get_all_named_actions(&self, ptype: &str) -> Vec<String>;
+
+ fn get_all_named_roles(&self, ptype: &str) -> Vec<String>;
+}
+
+#[async_trait]
+impl<T> MgmtApi for T
+where
+ T: InternalApi,
+{
+ async fn add_named_policy(
+ &mut self,
+ ptype: &str,
+ params: Vec<String>,
+ ) -> Result<bool> {
+ self.add_policy_internal("p", ptype, params).await
+ }
+
+ async fn add_named_policies(
+ &mut self,
+ ptype: &str,
+ paramss: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ self.add_policies_internal("p", ptype, paramss).await
+ }
+
+ async fn remove_named_policy(
+ &mut self,
+ ptype: &str,
+ params: Vec<String>,
+ ) -> Result<bool> {
+ self.remove_policy_internal("p", ptype, params).await
+ }
+
+ async fn remove_named_policies(
+ &mut self,
+ ptype: &str,
+ paramss: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ self.remove_policies_internal("p", ptype, paramss).await
+ }
+
+ async fn add_named_grouping_policy(
+ &mut self,
+ ptype: &str,
+ params: Vec<String>,
+ ) -> Result<bool> {
+ let rule_added = self.add_policy_internal("g", ptype, params).await?;
+ Ok(rule_added)
+ }
+
+ async fn add_named_grouping_policies(
+ &mut self,
+ ptype: &str,
+ paramss: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ let all_added = self.add_policies_internal("g", ptype, paramss).await?;
+ Ok(all_added)
+ }
+
+ async fn remove_named_grouping_policy(
+ &mut self,
+ ptype: &str,
+ params: Vec<String>,
+ ) -> Result<bool> {
+ let rule_removed =
+ self.remove_policy_internal("g", ptype, params).await?;
+ Ok(rule_removed)
+ }
+
+ async fn remove_named_grouping_policies(
+ &mut self,
+ ptype: &str,
+ paramss: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ let all_removed =
+ self.remove_policies_internal("g", ptype, paramss).await?;
+ Ok(all_removed)
+ }
+
+ async fn remove_filtered_named_grouping_policy(
+ &mut self,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Result<bool> {
+ #[allow(unused_variables)]
+ let (rule_removed, rules) = self
+ .remove_filtered_policy_internal(
+ "g",
+ ptype,
+ field_index,
+ field_values,
+ )
+ .await?;
+ Ok(rule_removed)
+ }
+
+ async fn remove_filtered_named_policy(
+ &mut self,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Result<bool> {
+ Ok(self
+ .remove_filtered_policy_internal(
+ "p",
+ ptype,
+ field_index,
+ field_values,
+ )
+ .await?
+ .0)
+ }
+
+ fn get_named_policy(&self, ptype: &str) -> Vec<Vec<String>> {
+ self.get_model().get_policy("p", ptype)
+ }
+
+ fn get_all_policy(&self) -> Vec<Vec<String>> {
+ let mut res: Vec<Vec<String>> = vec![];
+ let sec = "p";
+ if let Some(ast_map) = self.get_model().get_model().get(sec) {
+ for (ptype, ast) in ast_map {
+ res.extend(ast.get_policy().clone().into_iter().map(|mut x| {
+ x.insert(0, ptype.clone());
+ x.insert(0, sec.to_owned());
+ x
+ }))
+ }
+ }
+
+ res
+ }
+
+ fn get_filtered_named_policy(
+ &self,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Vec<Vec<String>> {
+ self.get_model().get_filtered_policy(
+ "p",
+ ptype,
+ field_index,
+ field_values,
+ )
+ }
+
+ fn has_named_policy(&self, ptype: &str, params: Vec<String>) -> bool {
+ self.get_model().has_policy("p", ptype, params)
+ }
+
+ fn get_named_grouping_policy(&self, ptype: &str) -> Vec<Vec<String>> {
+ self.get_model().get_policy("g", ptype)
+ }
+
+ fn get_all_grouping_policy(&self) -> Vec<Vec<String>> {
+ let mut res: Vec<Vec<String>> = vec![];
+ let sec = "g";
+ if let Some(ast_map) = self.get_model().get_model().get(sec) {
+ for (ptype, ast) in ast_map {
+ res.extend(ast.get_policy().clone().into_iter().map(|mut x| {
+ x.insert(0, ptype.clone());
+ x.insert(0, sec.to_owned());
+ x
+ }))
+ }
+ }
+
+ res
+ }
+
+ fn get_filtered_named_grouping_policy(
+ &self,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Vec<Vec<String>> {
+ self.get_model().get_filtered_policy(
+ "g",
+ ptype,
+ field_index,
+ field_values,
+ )
+ }
+
+ fn has_grouping_named_policy(
+ &self,
+ ptype: &str,
+ params: Vec<String>,
+ ) -> bool {
+ self.get_model().has_policy("g", ptype, params)
+ }
+
+ fn get_all_named_subjects(&self, ptype: &str) -> Vec<String> {
+ self.get_model()
+ .get_values_for_field_in_policy("p", ptype, 0)
+ }
+
+ fn get_all_named_objects(&self, ptype: &str) -> Vec<String> {
+ self.get_model()
+ .get_values_for_field_in_policy("p", ptype, 1)
+ }
+
+ fn get_all_named_actions(&self, ptype: &str) -> Vec<String> {
+ self.get_model()
+ .get_values_for_field_in_policy("p", ptype, 2)
+ }
+
+ fn get_all_named_roles(&self, ptype: &str) -> Vec<String> {
+ self.get_model()
+ .get_values_for_field_in_policy("g", ptype, 1)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::prelude::*;
+
+ fn sort_unstable<T: Ord>(mut v: Vec<T>) -> Vec<T> {
+ v.sort_unstable();
+ v
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_modify_grouping_policy_api() {
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_policy.csv");
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(vec!["data2_admin"], e.get_roles_for_user("alice", None));
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("bob", None));
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("eve", None));
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_roles_for_user("non_exist", None)
+ );
+
+ e.remove_grouping_policy(
+ vec!["alice", "data2_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_grouping_policy(
+ vec!["bob", "data1_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_grouping_policy(
+ vec!["eve", "data3_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+
+ let named_grouping_policy =
+ vec!["alice".to_string(), "data2_admin".to_string()];
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("alice", None));
+ e.add_named_grouping_policy("g", named_grouping_policy.clone())
+ .await
+ .unwrap();
+ assert_eq!(vec!["data2_admin"], e.get_roles_for_user("alice", None));
+ e.remove_named_grouping_policy("g", named_grouping_policy.clone())
+ .await
+ .unwrap();
+
+ e.remove_grouping_policy(
+ vec!["alice", "data2_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_grouping_policy(
+ vec!["bob", "data1_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_grouping_policy(
+ vec!["eve", "data3_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(vec!["bob"], e.get_users_for_role("data1_admin", None));
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_users_for_role("data2_admin", None)
+ );
+ assert_eq!(vec!["eve"], e.get_users_for_role("data3_admin", None));
+
+ e.remove_filtered_grouping_policy(
+ 0,
+ vec!["bob"].iter().map(|s| s.to_string()).collect(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("alice", None));
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("bob", None));
+ assert_eq!(vec!["data3_admin"], e.get_roles_for_user("eve", None));
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_roles_for_user("non_exist", None)
+ );
+
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_users_for_role("data1_admin", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_users_for_role("data2_admin", None)
+ );
+ assert_eq!(vec!["eve"], e.get_users_for_role("data3_admin", None));
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_modify_policy_api() {
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_policy.csv");
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(
+ vec![
+ vec!["alice", "data1", "read"],
+ vec!["bob", "data2", "write"],
+ vec!["data2_admin", "data2", "read"],
+ vec!["data2_admin", "data2", "write"],
+ ],
+ sort_unstable(e.get_policy())
+ );
+
+ e.remove_policy(
+ vec!["alice", "data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.remove_policy(
+ vec!["bob", "data2", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.remove_policy(
+ vec!["alice", "data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_policy(
+ vec!["eve", "data3", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_policy(
+ vec!["eve", "data3", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+
+ let named_policy =
+ vec!["eve".to_string(), "data3".to_string(), "read".to_string()];
+ e.remove_named_policy("p", named_policy.clone())
+ .await
+ .unwrap();
+ e.add_named_policy("p", named_policy.clone()).await.unwrap();
+
+ assert_eq!(
+ vec![
+ vec!["data2_admin", "data2", "read"],
+ vec!["data2_admin", "data2", "write"],
+ vec!["eve", "data3", "read"],
+ ],
+ sort_unstable(e.get_policy())
+ );
+
+ e.remove_filtered_policy(
+ 1,
+ vec!["data2"].iter().map(|s| s.to_string()).collect(),
+ )
+ .await
+ .unwrap();
+ assert_eq!(vec![vec!["eve", "data3", "read"],], e.get_policy());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_get_policy_api() {
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(
+ vec![
+ vec!["alice", "data1", "read"],
+ vec!["bob", "data2", "write"],
+ vec!["data2_admin", "data2", "read"],
+ vec!["data2_admin", "data2", "write"],
+ ],
+ sort_unstable(e.get_policy())
+ );
+
+ assert_eq!(
+ vec![vec!["alice", "data1", "read"]],
+ e.get_filtered_policy(
+ 0,
+ vec!["alice"].iter().map(|s| s.to_string()).collect()
+ )
+ );
+ assert_eq!(
+ vec![vec!["bob", "data2", "write"]],
+ e.get_filtered_policy(
+ 0,
+ vec!["bob"].iter().map(|s| s.to_string()).collect()
+ )
+ );
+ assert_eq!(
+ vec![
+ vec!["data2_admin", "data2", "read"],
+ vec!["data2_admin", "data2", "write"],
+ ],
+ sort_unstable(e.get_filtered_policy(
+ 0,
+ vec!["data2_admin"].iter().map(|s| s.to_string()).collect()
+ ))
+ );
+ assert_eq!(
+ vec![vec!["alice", "data1", "read"],],
+ e.get_filtered_policy(
+ 1,
+ vec!["data1"].iter().map(|s| s.to_string()).collect()
+ )
+ );
+ assert_eq!(
+ vec![
+ vec!["bob", "data2", "write"],
+ vec!["data2_admin", "data2", "read"],
+ vec!["data2_admin", "data2", "write"],
+ ],
+ sort_unstable(e.get_filtered_policy(
+ 1,
+ vec!["data2"].iter().map(|s| s.to_string()).collect()
+ ))
+ );
+ assert_eq!(
+ vec![
+ vec!["alice", "data1", "read"],
+ vec!["data2_admin", "data2", "read"],
+ ],
+ sort_unstable(e.get_filtered_policy(
+ 2,
+ vec!["read"].iter().map(|s| s.to_string()).collect()
+ ))
+ );
+ assert_eq!(
+ vec![
+ vec!["bob", "data2", "write"],
+ vec!["data2_admin", "data2", "write"],
+ ],
+ sort_unstable(e.get_filtered_policy(
+ 2,
+ vec!["write"].iter().map(|s| s.to_string()).collect()
+ ))
+ );
+ assert_eq!(
+ vec![
+ vec!["data2_admin", "data2", "read"],
+ vec!["data2_admin", "data2", "write"],
+ ],
+ sort_unstable(
+ e.get_filtered_policy(
+ 0,
+ vec!["data2_admin", "data2"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ )
+ );
+ // Note: "" (empty string) in fieldValues means matching all values.
+ assert_eq!(
+ vec![vec!["data2_admin", "data2", "read"],],
+ e.get_filtered_policy(
+ 0,
+ vec!["data2_admin", "", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ assert_eq!(
+ vec![
+ vec!["bob", "data2", "write"],
+ vec!["data2_admin", "data2", "write"],
+ ],
+ sort_unstable(
+ e.get_filtered_policy(
+ 1,
+ vec!["data2", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ )
+ );
+
+ assert_eq!(
+ true,
+ e.has_policy(
+ vec!["alice", "data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ assert_eq!(
+ true,
+ e.has_policy(
+ vec!["bob", "data2", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ assert_eq!(
+ false,
+ e.has_policy(
+ vec!["alice", "data2", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ assert_eq!(
+ false,
+ e.has_policy(
+ vec!["bob", "data3", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+
+ assert_eq!(
+ vec![vec!["alice", "data2_admin"]],
+ e.get_filtered_grouping_policy(
+ 0,
+ vec!["alice"].iter().map(|s| s.to_string()).collect()
+ )
+ );
+ let empty_policy: Vec<Vec<String>> = vec![];
+ assert_eq!(
+ empty_policy,
+ e.get_filtered_grouping_policy(
+ 0,
+ vec!["bob"].iter().map(|s| s.to_string()).collect()
+ )
+ );
+ assert_eq!(
+ empty_policy,
+ e.get_filtered_grouping_policy(
+ 1,
+ vec!["data1_admin"].iter().map(|s| s.to_string()).collect()
+ )
+ );
+ assert_eq!(
+ vec![vec!["alice", "data2_admin"],],
+ e.get_filtered_grouping_policy(
+ 1,
+ vec!["data2_admin"].iter().map(|s| s.to_string()).collect()
+ )
+ );
+ // Note: "" (empty string) in fieldValues means matching all values.
+ assert_eq!(
+ empty_policy,
+ e.get_filtered_grouping_policy(
+ 0,
+ vec!["data2_admin"].iter().map(|s| s.to_string()).collect()
+ )
+ );
+
+ assert_eq!(
+ true,
+ e.has_grouping_policy(
+ vec!["alice", "data2_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ assert_eq!(
+ false,
+ e.has_grouping_policy(
+ vec!["bob", "data2_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_get_list() {
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(
+ vec!["alice", "bob", "data2_admin"],
+ sort_unstable(e.get_all_subjects())
+ );
+ assert_eq!(vec!["data1", "data2"], sort_unstable(e.get_all_objects()));
+ assert_eq!(vec!["read", "write"], sort_unstable(e.get_all_actions()));
+ assert_eq!(vec!["data2_admin"], e.get_all_roles());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_modify_policies_api() {
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_policy.csv");
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(
+ vec![
+ vec!["alice", "data1", "read"],
+ vec!["bob", "data2", "write"],
+ vec!["data2_admin", "data2", "read"],
+ vec!["data2_admin", "data2", "write"],
+ ],
+ sort_unstable(e.get_policy())
+ );
+
+ e.remove_policies(vec![
+ vec!["alice", "data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ vec!["bob", "data2", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ ])
+ .await
+ .unwrap();
+ e.remove_policies(vec![vec!["alice", "data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()])
+ .await
+ .unwrap();
+ assert_eq!(
+ false,
+ e.has_policy(
+ vec!["alice", "data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ assert_eq!(
+ false,
+ e.has_policy(
+ vec!["bob", "data2", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ assert_eq!(
+ true,
+ e.has_policy(
+ vec!["data2_admin", "data2", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ assert_eq!(
+ true,
+ e.has_policy(
+ vec!["data2_admin", "data2", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ e.add_policies(vec![vec!["eve", "data3", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()])
+ .await
+ .unwrap();
+ e.add_policies(vec![
+ vec!["eve", "data3", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ vec!["eve", "data3", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ ])
+ .await
+ .unwrap();
+ assert_eq!(
+ false,
+ e.has_policy(
+ vec!["alice", "data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ assert_eq!(
+ false,
+ e.has_policy(
+ vec!["bob", "data2", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ assert_eq!(
+ true,
+ e.has_policy(
+ vec!["eve", "data3", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ assert_eq!(
+ true,
+ e.has_policy(
+ vec!["data2_admin", "data2", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+ assert_eq!(
+ true,
+ e.has_policy(
+ vec!["data2_admin", "data2", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ );
+
+ let named_policy =
+ vec!["eve".to_string(), "data3".to_string(), "read".to_string()];
+ e.remove_named_policies("p", vec![named_policy.clone()])
+ .await
+ .unwrap();
+ e.add_named_policies("p", vec![named_policy.clone()])
+ .await
+ .unwrap();
+
+ assert_eq!(
+ vec![
+ vec!["data2_admin", "data2", "read"],
+ vec!["data2_admin", "data2", "write"],
+ vec!["eve", "data3", "read"],
+ ],
+ sort_unstable(e.get_policy())
+ );
+
+ e.remove_filtered_policy(
+ 1,
+ vec!["data2"].iter().map(|s| s.to_string()).collect(),
+ )
+ .await
+ .unwrap();
+ assert_eq!(vec![vec!["eve", "data3", "read"],], e.get_policy());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_modify_grouping_policies_api() {
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_policy.csv");
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(vec!["data2_admin"], e.get_roles_for_user("alice", None));
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("bob", None));
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("eve", None));
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_roles_for_user("non_exist", None)
+ );
+
+ e.remove_grouping_policies(vec![vec!["alice", "data2_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()])
+ .await
+ .unwrap();
+ e.add_grouping_policies(vec![
+ vec!["bob", "data1_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ vec!["eve", "data3_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ ])
+ .await
+ .unwrap();
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("alice", None));
+ assert_eq!(vec!["data1_admin"], e.get_roles_for_user("bob", None));
+ assert_eq!(vec!["data3_admin"], e.get_roles_for_user("eve", None));
+
+ let named_grouping_policy =
+ vec!["alice".to_string(), "data2_admin".to_string()];
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("alice", None));
+ e.add_named_grouping_policies("g", vec![named_grouping_policy.clone()])
+ .await
+ .unwrap();
+ assert_eq!(vec!["data2_admin"], e.get_roles_for_user("alice", None));
+ e.remove_named_grouping_policies(
+ "g",
+ vec![named_grouping_policy.clone()],
+ )
+ .await
+ .unwrap();
+
+ e.remove_grouping_policies(vec![vec!["alice", "data2_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()])
+ .await
+ .unwrap();
+
+ e.add_grouping_policies(vec![
+ vec!["bob", "data1_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ vec!["eve", "data3_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ ])
+ .await
+ .unwrap();
+
+ assert_eq!(vec!["bob"], e.get_users_for_role("data1_admin", None));
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_users_for_role("data2_admin", None)
+ );
+ assert_eq!(vec!["eve"], e.get_users_for_role("data3_admin", None));
+
+ e.remove_filtered_grouping_policy(
+ 0,
+ vec!["bob"].iter().map(|s| s.to_string()).collect(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("alice", None));
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("bob", None));
+ assert_eq!(vec!["data3_admin"], e.get_roles_for_user("eve", None));
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_roles_for_user("non_exist", None)
+ );
+
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_users_for_role("data1_admin", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_users_for_role("data2_admin", None)
+ );
+ assert_eq!(vec!["eve"], e.get_users_for_role("data3_admin", None));
+ }
+}
diff --git a/casbin-rs/src/model/assertion.rs b/casbin-rs/src/model/assertion.rs
new file mode 100644
index 0000000..14108ab
--- /dev/null
+++ b/casbin-rs/src/model/assertion.rs
@@ -0,0 +1,146 @@
+use crate::{
+ error::{ModelError, PolicyError},
+ rbac::{DefaultRoleManager, RoleManager},
+ Result,
+};
+
+#[cfg(feature = "incremental")]
+use crate::emitter::EventData;
+
+use parking_lot::RwLock;
+use ritelinked::{LinkedHashMap, LinkedHashSet};
+
+use std::sync::Arc;
+
+pub type AssertionMap = LinkedHashMap<String, Assertion>;
+
+#[derive(Clone)]
+pub struct Assertion {
+ pub key: String,
+ pub value: String,
+ pub tokens: Vec<String>,
+ pub policy: LinkedHashSet<Vec<String>>,
+ pub rm: Arc<RwLock<dyn RoleManager>>,
+}
+
+impl Default for Assertion {
+ fn default() -> Self {
+ Assertion {
+ key: String::new(),
+ value: String::new(),
+ tokens: vec![],
+ policy: LinkedHashSet::new(),
+ rm: Arc::new(RwLock::new(DefaultRoleManager::new(0))),
+ }
+ }
+}
+
+impl Assertion {
+ #[inline]
+ pub fn get_policy(&self) -> &LinkedHashSet<Vec<String>> {
+ &self.policy
+ }
+
+ #[inline]
+ pub fn get_mut_policy(&mut self) -> &mut LinkedHashSet<Vec<String>> {
+ &mut self.policy
+ }
+
+ pub fn build_role_links(
+ &mut self,
+ rm: Arc<RwLock<dyn RoleManager>>,
+ ) -> Result<()> {
+ let count = self.value.matches('_').count();
+ if count < 2 {
+ return Err(ModelError::P(
+ r#"the number of "_" in role definition should be at least 2"#
+ .to_owned(),
+ )
+ .into());
+ }
+ for rule in &self.policy {
+ if rule.len() < count {
+ return Err(PolicyError::UnmatchPolicyDefinition(
+ count,
+ rule.len(),
+ )
+ .into());
+ }
+ if count == 2 {
+ rm.write().add_link(&rule[0], &rule[1], None);
+ } else if count == 3 {
+ rm.write().add_link(&rule[0], &rule[1], Some(&rule[2]));
+ } else if count >= 4 {
+ return Err(ModelError::P(
+ "Multiple domains are not supported".to_owned(),
+ )
+ .into());
+ }
+ }
+ self.rm = Arc::clone(&rm);
+ Ok(())
+ }
+
+ #[cfg(feature = "incremental")]
+ pub fn build_incremental_role_links(
+ &mut self,
+ rm: Arc<RwLock<dyn RoleManager>>,
+ d: EventData,
+ ) -> Result<()> {
+ let count = self.value.matches('_').count();
+ if count < 2 {
+ return Err(ModelError::P(
+ r#"the number of "_" in role definition should be at least 2"#
+ .to_owned(),
+ )
+ .into());
+ }
+
+ if let Some((insert, rules)) = match d {
+ EventData::AddPolicy(_, _, rule) => Some((true, vec![rule])),
+ EventData::AddPolicies(_, _, rules) => Some((true, rules)),
+ EventData::RemovePolicy(_, _, rule) => Some((false, vec![rule])),
+ EventData::RemovePolicies(_, _, rules) => Some((false, rules)),
+ EventData::RemoveFilteredPolicy(_, _, rules) => {
+ Some((false, rules))
+ }
+ _ => None,
+ } {
+ for rule in rules {
+ if rule.len() < count {
+ return Err(PolicyError::UnmatchPolicyDefinition(
+ count,
+ rule.len(),
+ )
+ .into());
+ }
+ if count == 2 {
+ if insert {
+ rm.write().add_link(&rule[0], &rule[1], None);
+ } else {
+ rm.write().delete_link(&rule[0], &rule[1], None)?;
+ }
+ } else if count == 3 {
+ if insert {
+ rm.write().add_link(&rule[0], &rule[1], Some(&rule[2]));
+ } else {
+ rm.write().delete_link(
+ &rule[0],
+ &rule[1],
+ Some(&rule[2]),
+ )?;
+ }
+ } else if count >= 4 {
+ return Err(ModelError::P(
+ "Multiple domains are not supported".to_owned(),
+ )
+ .into());
+ }
+ }
+
+ self.rm = Arc::clone(&rm);
+ }
+
+ Ok(())
+ }
+}
diff --git a/casbin-rs/src/model/default_model.rs b/casbin-rs/src/model/default_model.rs
new file mode 100644
index 0000000..3f8de2d
--- /dev/null
+++ b/casbin-rs/src/model/default_model.rs
@@ -0,0 +1,1102 @@
+use crate::{
+ config::Config,
+ error::ModelError,
+ model::{Assertion, AssertionMap, Model},
+ rbac::RoleManager,
+ util::*,
+ Result,
+};
+
+#[cfg(feature = "incremental")]
+use crate::emitter::EventData;
+
+use parking_lot::RwLock;
+use ritelinked::{LinkedHashMap, LinkedHashSet};
+
+#[cfg(all(feature = "runtime-async-std", not(target_arch = "wasm32")))]
+use async_std::path::Path;
+
+#[cfg(feature = "runtime-tokio")]
+use std::path::Path;
+
+use std::{collections::HashMap, sync::Arc};
+
+#[derive(Clone, Default)]
+pub struct DefaultModel {
+ pub(crate) model: HashMap<String, AssertionMap>,
+}
+
+impl DefaultModel {
+ #[cfg(all(not(target_arch = "wasm32"), not(feature = "runtime-teaclave")))]
+ pub async fn from_file<P: AsRef<Path>>(p: P) -> Result<DefaultModel> {
+ let cfg = Config::from_file(p).await?;
+
+ let mut model = DefaultModel::default();
+
+ model.load_section(&cfg, "r")?;
+ model.load_section(&cfg, "p")?;
+ model.load_section(&cfg, "e")?;
+ model.load_section(&cfg, "m")?;
+
+ model.load_section(&cfg, "g")?;
+
+ Ok(model)
+ }
+
+ #[allow(clippy::should_implement_trait)]
+ pub async fn from_str(s: &str) -> Result<DefaultModel> {
+ let cfg = Config::from_str(s).await?;
+
+ let mut model = DefaultModel::default();
+
+ model.load_section(&cfg, "r")?;
+ model.load_section(&cfg, "p")?;
+ model.load_section(&cfg, "e")?;
+ model.load_section(&cfg, "m")?;
+
+ model.load_section(&cfg, "g")?;
+
+ Ok(model)
+ }
+
+ fn load_section(&mut self, cfg: &Config, sec: &str) -> Result<()> {
+ let mut i = 1;
+
+ loop {
+ if !self.load_assertion(
+ cfg,
+ sec,
+ &format!("{}{}", sec, self.get_key_suffix(i)),
+ )? {
+ break Ok(());
+ } else {
+ i += 1;
+ }
+ }
+ }
+
+ fn load_assertion(
+ &mut self,
+ cfg: &Config,
+ sec: &str,
+ key: &str,
+ ) -> Result<bool> {
+ let sec_name = match sec {
+ "r" => "request_definition",
+ "p" => "policy_definition",
+ "g" => "role_definition",
+ "e" => "policy_effect",
+ "m" => "matchers",
+ _ => {
+ return Err(ModelError::Other(format!(
+ "Unknown section: `{}`",
+ sec
+ ))
+ .into());
+ }
+ };
+
+ if let Some(val) = cfg.get_str(&format!("{}::{}", sec_name, key)) {
+ Ok(self.add_def(sec, key, val))
+ } else {
+ Ok(false)
+ }
+ }
+
+ fn get_key_suffix(&self, i: u64) -> String {
+ if i == 1 {
+ "".to_owned()
+ } else {
+ i.to_string()
+ }
+ }
+}
+
+impl Model for DefaultModel {
+ fn add_def(&mut self, sec: &str, key: &str, value: &str) -> bool {
+ let mut ast = Assertion {
+ key: key.to_owned(),
+ value: remove_comment(value),
+ ..Default::default()
+ };
+
+ if ast.value.is_empty() {
+ return false;
+ }
+
+ if sec == "r" || sec == "p" {
+ ast.tokens = ast
+ .value
+ .split(',')
+ .map(|x| format!("{}_{}", key, x.trim()))
+ .collect();
+ } else {
+ ast.value = escape_assertion(&ast.value);
+ }
+
+ if let Some(new_model) = self.model.get_mut(sec) {
+ new_model.insert(key.to_owned(), ast);
+ } else {
+ let mut new_ast_map = LinkedHashMap::new();
+ new_ast_map.insert(key.to_owned(), ast);
+ self.model.insert(sec.to_owned(), new_ast_map);
+ }
+
+ true
+ }
+
+ #[inline]
+ fn get_model(&self) -> &HashMap<String, AssertionMap> {
+ &self.model
+ }
+
+ #[inline]
+ fn get_mut_model(&mut self) -> &mut HashMap<String, AssertionMap> {
+ &mut self.model
+ }
+
+ fn build_role_links(
+ &mut self,
+ rm: Arc<RwLock<dyn RoleManager>>,
+ ) -> Result<()> {
+ if let Some(asts) = self.model.get_mut("g") {
+ for ast in asts.values_mut() {
+ ast.build_role_links(Arc::clone(&rm))?;
+ }
+ }
+ Ok(())
+ }
+
+ #[cfg(feature = "incremental")]
+ fn build_incremental_role_links(
+ &mut self,
+ rm: Arc<RwLock<dyn RoleManager>>,
+ d: EventData,
+ ) -> Result<()> {
+ let ast = match d {
+ EventData::AddPolicy(ref sec, ref ptype, _)
+ | EventData::AddPolicies(ref sec, ref ptype, _)
+ | EventData::RemovePolicy(ref sec, ref ptype, _)
+ | EventData::RemovePolicies(ref sec, ref ptype, _)
+ | EventData::RemoveFilteredPolicy(ref sec, ref ptype, _)
+ if sec == "g" =>
+ {
+ self.model
+ .get_mut(sec)
+ .and_then(|ast_map| ast_map.get_mut(ptype))
+ }
+ _ => None,
+ };
+
+ if let Some(ast) = ast {
+ ast.build_incremental_role_links(rm, d)?;
+ }
+
+ Ok(())
+ }
+
+ fn add_policy(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rule: Vec<String>,
+ ) -> bool {
+ if let Some(ast_map) = self.model.get_mut(sec) {
+ if let Some(ast) = ast_map.get_mut(ptype) {
+ return ast.policy.insert(rule);
+ }
+ }
+ false
+ }
+
+ fn add_policies(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rules: Vec<Vec<String>>,
+ ) -> bool {
+ let mut all_added = true;
+ if let Some(ast_map) = self.model.get_mut(sec) {
+ if let Some(ast) = ast_map.get_mut(ptype) {
+ for rule in &rules {
+ if ast.policy.contains(rule) {
+ all_added = false;
+ return all_added;
+ }
+ }
+ ast.policy.extend(rules);
+ }
+ }
+ all_added
+ }
+
+ fn get_policy(&self, sec: &str, ptype: &str) -> Vec<Vec<String>> {
+ if let Some(t1) = self.model.get(sec) {
+ if let Some(t2) = t1.get(ptype) {
+ return t2.policy.iter().map(|x| x.to_owned()).collect();
+ }
+ }
+ vec![]
+ }
+
+ fn get_filtered_policy(
+ &self,
+ sec: &str,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Vec<Vec<String>> {
+ let mut res = vec![];
+ if let Some(t1) = self.model.get(sec) {
+ if let Some(t2) = t1.get(ptype) {
+ for rule in t2.policy.iter() {
+ let mut matched = true;
+ for (i, field_value) in field_values.iter().enumerate() {
+ if !field_value.is_empty()
+ && &rule[field_index + i] != field_value
+ {
+ matched = false;
+ break;
+ }
+ }
+ if matched {
+ res.push(rule.iter().map(String::from).collect());
+ }
+ }
+ }
+ }
+ res
+ }
+
+ fn has_policy(&self, sec: &str, ptype: &str, rule: Vec<String>) -> bool {
+ let policy = self.get_policy(sec, ptype);
+ for r in policy {
+ if r == rule {
+ return true;
+ }
+ }
+ false
+ }
+
+ fn get_values_for_field_in_policy(
+ &self,
+ sec: &str,
+ ptype: &str,
+ field_index: usize,
+ ) -> Vec<String> {
+ self.get_policy(sec, ptype)
+ .into_iter()
+ .fold(LinkedHashSet::new(), |mut acc, x| {
+ acc.insert(x[field_index].clone());
+ acc
+ })
+ .into_iter()
+ .collect()
+ }
+
+ fn remove_policy(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rule: Vec<String>,
+ ) -> bool {
+ if let Some(ast_map) = self.model.get_mut(sec) {
+ if let Some(ast) = ast_map.get_mut(ptype) {
+ return ast.policy.remove(&rule);
+ }
+ }
+ false
+ }
+
+ fn remove_policies(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rules: Vec<Vec<String>>,
+ ) -> bool {
+ let mut all_removed = true;
+ if let Some(ast_map) = self.model.get_mut(sec) {
+ if let Some(ast) = ast_map.get_mut(ptype) {
+ for rule in &rules {
+ if !ast.policy.contains(rule) {
+ all_removed = false;
+ return all_removed;
+ }
+ }
+ for rule in &rules {
+ ast.policy.remove(rule);
+ }
+ }
+ }
+ all_removed
+ }
+
+ fn clear_policy(&mut self) {
+ if let Some(model_p) = self.model.get_mut("p") {
+ for ast in model_p.values_mut() {
+ ast.policy.clear();
+ }
+ }
+
+ if let Some(model_g) = self.model.get_mut("g") {
+ for ast in model_g.values_mut() {
+ ast.policy.clear();
+ }
+ }
+ }
+
+ fn remove_filtered_policy(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> (bool, Vec<Vec<String>>) {
+ if field_values.is_empty() {
+ return (false, vec![]);
+ }
+
+ let mut res = false;
+ let mut rules_removed: Vec<Vec<String>> = vec![];
+ if let Some(ast_map) = self.model.get_mut(sec) {
+ if let Some(ast) = ast_map.get_mut(ptype) {
+ for rule in ast.policy.iter() {
+ let mut matched = true;
+ for (i, field_value) in field_values.iter().enumerate() {
+ if !field_value.is_empty()
+ && &rule[field_index + i] != field_value
+ {
+ matched = false;
+ break;
+ }
+ }
+ if matched {
+ res = true;
+ rules_removed.push(rule.clone());
+ }
+ }
+ if res && !rules_removed.is_empty() {
+ for rule in rules_removed.iter() {
+ ast.policy.remove(rule);
+ }
+ }
+ }
+ }
+ (res, rules_removed)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::prelude::*;
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_basic_model() {
+ let m = DefaultModel::from_file("examples/basic_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/basic_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert!(e.enforce(("alice", "data1", "read")).unwrap());
+ assert!(!e.enforce(("alice", "data1", "write")).unwrap());
+ assert!(!e.enforce(("alice", "data2", "read")).unwrap());
+ assert!(!e.enforce(("alice", "data2", "write")).unwrap());
+ assert!(!e.enforce(("bob", "data1", "read")).unwrap());
+ assert!(!e.enforce(("bob", "data1", "write")).unwrap());
+ assert!(!e.enforce(("bob", "data2", "read")).unwrap());
+ assert!(e.enforce(("bob", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_basic_model_no_policy() {
+ let m = DefaultModel::from_file("examples/basic_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = MemoryAdapter::default();
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert!(!e.enforce(("alice", "data1", "read")).unwrap());
+ assert!(!e.enforce(("alice", "data1", "write")).unwrap());
+ assert!(!e.enforce(("alice", "data2", "read")).unwrap());
+ assert!(!e.enforce(("alice", "data2", "write")).unwrap());
+ assert!(!e.enforce(("bob", "data1", "read")).unwrap());
+ assert!(!e.enforce(("bob", "data1", "write")).unwrap());
+ assert!(!e.enforce(("bob", "data2", "read")).unwrap());
+ assert!(!e.enforce(("bob", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_basic_model_with_root() {
+ let m = DefaultModel::from_file("examples/basic_with_root_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/basic_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert!(e.enforce(("alice", "data1", "read")).unwrap());
+ assert!(e.enforce(("bob", "data2", "write")).unwrap());
+ assert!(e.enforce(("root", "data1", "read")).unwrap());
+ assert!(e.enforce(("root", "data1", "write")).unwrap());
+ assert!(e.enforce(("root", "data2", "read")).unwrap());
+ assert!(e.enforce(("root", "data2", "write")).unwrap());
+ assert!(!e.enforce(("alice", "data1", "write")).unwrap());
+ assert!(!e.enforce(("alice", "data2", "read")).unwrap());
+ assert!(!e.enforce(("alice", "data2", "write")).unwrap());
+ assert!(!e.enforce(("bob", "data1", "read")).unwrap());
+ assert!(!e.enforce(("bob", "data1", "write")).unwrap());
+ assert!(!e.enforce(("bob", "data2", "read")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_basic_model_with_root_no_policy() {
+ let m = DefaultModel::from_file("examples/basic_with_root_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = MemoryAdapter::default();
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert!(!e.enforce(("alice", "data1", "read")).unwrap());
+ assert!(!e.enforce(("bob", "data2", "write")).unwrap());
+ assert!(e.enforce(("root", "data1", "read")).unwrap());
+ assert!(e.enforce(("root", "data1", "write")).unwrap());
+ assert!(e.enforce(("root", "data2", "read")).unwrap());
+ assert!(e.enforce(("root", "data2", "write")).unwrap());
+ assert!(!e.enforce(("alice", "data1", "write")).unwrap());
+ assert!(!e.enforce(("alice", "data2", "read")).unwrap());
+ assert!(!e.enforce(("alice", "data2", "write")).unwrap());
+ assert!(!e.enforce(("bob", "data1", "read")).unwrap());
+ assert!(!e.enforce(("bob", "data1", "write")).unwrap());
+ assert!(!e.enforce(("bob", "data2", "read")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_basic_model_without_users() {
+ let m =
+ DefaultModel::from_file("examples/basic_without_users_model.conf")
+ .await
+ .unwrap();
+
+ let adapter =
+ FileAdapter::new("examples/basic_without_users_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert!(e.enforce(("data1", "read")).unwrap());
+ assert!(!e.enforce(("data1", "write")).unwrap());
+ assert!(!e.enforce(("data2", "read")).unwrap());
+ assert!(e.enforce(("data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_basic_model_without_resources() {
+ let m = DefaultModel::from_file(
+ "examples/basic_without_resources_model.conf",
+ )
+ .await
+ .unwrap();
+
+ let adapter =
+ FileAdapter::new("examples/basic_without_resources_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert!(e.enforce(("alice", "read")).unwrap());
+ assert!(e.enforce(("bob", "write")).unwrap());
+ assert!(!e.enforce(("alice", "write")).unwrap());
+ assert!(!e.enforce(("bob", "read")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_rbac_model() {
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(true, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data1", "write")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_rbac_model_with_resource_roles() {
+ let m = DefaultModel::from_file(
+ "examples/rbac_with_resource_roles_model.conf",
+ )
+ .await
+ .unwrap();
+
+ let adapter =
+ FileAdapter::new("examples/rbac_with_resource_roles_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(true, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_rbac_model_with_domains() {
+ let m =
+ DefaultModel::from_file("examples/rbac_with_domains_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_with_domains_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(
+ true,
+ e.enforce(("alice", "domain1", "data1", "read")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("alice", "domain1", "data1", "write")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "domain1", "data2", "read")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "domain1", "data2", "write")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("bob", "domain2", "data1", "read")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("bob", "domain2", "data1", "write")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("bob", "domain2", "data2", "read")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("bob", "domain2", "data2", "write")).unwrap()
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_rbac_model_with_domains_runtime() {
+ let m =
+ DefaultModel::from_file("examples/rbac_with_domains_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = MemoryAdapter::default();
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+ e.add_policy(
+ vec!["admin", "domain1", "data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_policy(
+ vec!["admin", "domain1", "data1", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_policy(
+ vec!["admin", "domain2", "data2", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_policy(
+ vec!["admin", "domain2", "data2", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+
+ e.add_grouping_policy(
+ vec!["alice", "admin", "domain1"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_grouping_policy(
+ vec!["bob", "admin", "domain2"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(
+ true,
+ e.enforce(("alice", "domain1", "data1", "read")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("alice", "domain1", "data1", "write")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "domain1", "data2", "read")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "domain1", "data2", "write")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("bob", "domain2", "data1", "read")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("bob", "domain2", "data1", "write")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("bob", "domain2", "data2", "read")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("bob", "domain2", "data2", "write")).unwrap()
+ );
+
+ assert_eq!(
+ true,
+ e.remove_filtered_policy(
+ 1,
+ vec!["domain1", "data1"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap()
+ );
+
+ assert_eq!(
+ false,
+ e.enforce(("alice", "domain1", "data1", "read")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "domain1", "data1", "write")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "domain1", "data2", "read")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "domain1", "data2", "write")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("bob", "domain2", "data1", "read")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("bob", "domain2", "data1", "write")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("bob", "domain2", "data2", "read")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("bob", "domain2", "data2", "write")).unwrap()
+ );
+
+ assert_eq!(
+ true,
+ e.remove_policy(
+ vec!["admin", "domain2", "data2", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap()
+ );
+
+ assert_eq!(
+ false,
+ e.enforce(("alice", "domain1", "data1", "read")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "domain1", "data1", "write")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "domain1", "data2", "read")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("alice", "domain1", "data2", "write")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("bob", "domain2", "data1", "read")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("bob", "domain2", "data1", "write")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.enforce(("bob", "domain2", "data2", "read")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("bob", "domain2", "data2", "write")).unwrap()
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_rbac_model_with_domains_at_runtime_mock_adapter() {
+ let m =
+ DefaultModel::from_file("examples/rbac_with_domains_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_with_domains_policy.csv");
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+
+ e.add_policy(
+ vec!["admin", "domain3", "data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ e.add_grouping_policy(
+ vec!["alice", "admin", "domain3"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(
+ true,
+ e.enforce(("alice", "domain3", "data1", "read")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("alice", "domain1", "data1", "read")).unwrap()
+ );
+
+ e.remove_filtered_policy(
+ 1,
+ vec!["domain1", "data1"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ assert_eq!(
+ false,
+ e.enforce(("alice", "domain1", "data1", "read")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("bob", "domain2", "data2", "read")).unwrap()
+ );
+
+ e.remove_policy(
+ vec!["admin", "domain2", "data2", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+ assert_eq!(
+ false,
+ e.enforce(("bob", "domain2", "data2", "read")).unwrap()
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_rbac_model_with_deny() {
+ let m = DefaultModel::from_file("examples/rbac_with_deny_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_with_deny_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(true, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data1", "write")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data2", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_rbac_model_with_not_deny() {
+ let m =
+ DefaultModel::from_file("examples/rbac_with_not_deny_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_with_deny_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(false, e.enforce(("alice", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_rbac_model_with_custom_data() {
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_policy.csv");
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+
+ e.add_grouping_policy(
+ vec!["bob", "data2_admin", "custom_data"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(true, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data1", "write")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "write")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data2", "write")).unwrap());
+
+ e.remove_grouping_policy(
+ vec!["bob", "data2_admin", "custom_data"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(true, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data1", "write")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_rbac_model_using_in_op() {
+ let m = DefaultModel::from_file(
+ "examples/rbac_model_matcher_using_in_op.conf",
+ )
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(true, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data1", "write")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data2", "write")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "write")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("guest", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data3", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data3", "read")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_abac() {
+ use serde::Serialize;
+
+ let m = DefaultModel::from_file("examples/abac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = MemoryAdapter::default();
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ #[derive(Serialize, Hash)]
+ pub struct Book<'a> {
+ owner: &'a str,
+ }
+
+ assert_eq!(
+ false,
+ e.enforce(("alice", Book { owner: "bob" }, "read")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.enforce(("alice", Book { owner: "alice" }, "read"))
+ .unwrap()
+ );
+ }
+}
diff --git a/casbin-rs/src/model/function_map.rs b/casbin-rs/src/model/function_map.rs
new file mode 100644
index 0000000..01285e0
--- /dev/null
+++ b/casbin-rs/src/model/function_map.rs
@@ -0,0 +1,257 @@
+#[cfg(all(feature = "runtime-async-std", feature = "ip"))]
+use async_std::net::IpAddr;
+
+#[cfg(all(feature = "runtime-tokio", feature = "ip"))]
+use std::net::IpAddr;
+
+#[cfg(feature = "glob")]
+use globset::GlobBuilder;
+#[cfg(feature = "ip")]
+use ip_network::IpNetwork;
+use once_cell::sync::Lazy;
+use regex::Regex;
+use rhai::ImmutableString;
+
+static MAT_B: Lazy<Regex> = Lazy::new(|| Regex::new(r":[^/]*").unwrap());
+static MAT_P: Lazy<Regex> = Lazy::new(|| Regex::new(r"\{[^/]*\}").unwrap());
+
+use std::{borrow::Cow, collections::HashMap};
+
+pub struct FunctionMap {
+ pub(crate) fm:
+ HashMap<String, fn(ImmutableString, ImmutableString) -> bool>,
+}
+
+impl Default for FunctionMap {
+ fn default() -> FunctionMap {
+ let mut fm: HashMap<
+ String,
+ fn(ImmutableString, ImmutableString) -> bool,
+ > = HashMap::new();
+ fm.insert(
+ "keyMatch".to_owned(),
+ |s1: ImmutableString, s2: ImmutableString| key_match(&s1, &s2),
+ );
+ fm.insert(
+ "keyMatch2".to_owned(),
+ |s1: ImmutableString, s2: ImmutableString| key_match2(&s1, &s2),
+ );
+ fm.insert(
+ "keyMatch3".to_owned(),
+ |s1: ImmutableString, s2: ImmutableString| key_match3(&s1, &s2),
+ );
+ fm.insert(
+ "regexMatch".to_owned(),
+ |s1: ImmutableString, s2: ImmutableString| regex_match(&s1, &s2),
+ );
+
+ #[cfg(feature = "glob")]
+ fm.insert(
+ "globMatch".to_owned(),
+ |s1: ImmutableString, s2: ImmutableString| glob_match(&s1, &s2),
+ );
+
+ #[cfg(feature = "ip")]
+ fm.insert(
+ "ipMatch".to_owned(),
+ |s1: ImmutableString, s2: ImmutableString| ip_match(&s1, &s2),
+ );
+
+ FunctionMap { fm }
+ }
+}
+
+impl FunctionMap {
+ #[inline]
+ pub fn add_function(
+ &mut self,
+ fname: &str,
+ f: fn(ImmutableString, ImmutableString) -> bool,
+ ) {
+ self.fm.insert(fname.to_owned(), f);
+ }
+
+ #[inline]
+ pub fn get_functions(
+ &self,
+ ) -> impl Iterator<Item = (&String, &fn(ImmutableString, ImmutableString) -> bool)>
+ {
+ self.fm.iter()
+ }
+}
+
+// key_match determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain *
+// For example, "/foo/bar" matches "/foo/*"
+pub fn key_match(key1: &str, key2: &str) -> bool {
+ if let Some(i) = key2.find('*') {
+ if key1.len() > i {
+ return key1[..i] == key2[..i];
+ }
+ key1[..] == key2[..i]
+ } else {
+ key1 == key2
+ }
+}
+
+// key_match2 determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *
+// For example, "/foo/bar" matches "/foo/*", "/resource1" matches "/:resource"
+pub fn key_match2(key1: &str, key2: &str) -> bool {
+ let mut key2: Cow<str> = if key2.contains("/*") {
+ key2.replace("/*", "/.*").into()
+ } else {
+ key2.into()
+ };
+
+ key2 = MAT_B.replace_all(&key2, "[^/]+").to_string().into();
+
+ regex_match(key1, &format!("^{}$", key2))
+}
+
+// key_match3 determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *
+// For example, "/foo/bar" matches "/foo/*", "/resource1" matches "/{resource}"
+pub fn key_match3(key1: &str, key2: &str) -> bool {
+ let mut key2: Cow<str> = if key2.contains("/*") {
+ key2.replace("/*", "/.*").into()
+ } else {
+ key2.into()
+ };
+
+ key2 = MAT_P.replace_all(&key2, "[^/]+").to_string().into();
+
+ regex_match(key1, &format!("^{}$", key2))
+}
+
+// regex_match determines whether key1 matches the pattern of key2 in regular expression.
+pub fn regex_match(key1: &str, key2: &str) -> bool {
+ Regex::new(key2).unwrap().is_match(key1)
+}
+
+// ip_match determines whether IP address ip1 matches the pattern of IP address ip2, ip2 can be an IP address or a CIDR pattern.
+// For example, "192.168.2.123" matches "192.168.2.0/24"
+#[cfg(feature = "ip")]
+pub fn ip_match(key1: &str, key2: &str) -> bool {
+ let key2_split = key2.splitn(2, '/').collect::<Vec<&str>>();
+ let ip_addr2 = key2_split[0];
+
+ if let (Ok(ip_addr1), Ok(ip_addr2)) =
+ (key1.parse::<IpAddr>(), ip_addr2.parse::<IpAddr>())
+ {
+ if key2_split.len() == 2 {
+ match key2_split[1].parse::<u8>() {
+ Ok(ip_netmask) => {
+ match IpNetwork::new_truncate(ip_addr2, ip_netmask) {
+ Ok(ip_network) => ip_network.contains(ip_addr1),
+ Err(err) => panic!("invalid ip network {}", err),
+ }
+ }
+ _ => panic!("invalid netmask {}", key2_split[1]),
+ }
+ } else {
+ if let (IpAddr::V4(ip_addr1_new), IpAddr::V6(ip_addr2_new)) =
+ (ip_addr1, ip_addr2)
+ {
+ if let Some(ip_addr2_new) = ip_addr2_new.to_ipv4() {
+ return ip_addr2_new == ip_addr1_new;
+ }
+ }
+
+ ip_addr1 == ip_addr2
+ }
+ } else {
+ panic!("invalid argument {} {}", key1, key2)
+ }
+}
+
+// glob_match determines whether key1 matches the pattern of key2 using glob pattern
+#[cfg(feature = "glob")]
+pub fn glob_match(key1: &str, key2: &str) -> bool {
+ GlobBuilder::new(key2)
+ .literal_separator(true)
+ .build()
+ .unwrap()
+ .compile_matcher()
+ .is_match(key1)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_key_match() {
+ assert!(key_match("/foo/bar", "/foo/*"));
+ assert!(!key_match("/bar/foo", "/foo/*"));
+ assert!(key_match("/bar", "/ba*"));
+ }
+
+ #[test]
+ fn test_key_match2() {
+ assert!(key_match2("/foo/bar", "/foo/*"));
+ assert!(key_match2("/foo/bar/baz", "/foo/*"));
+ assert!(key_match2("/foo/baz", "/foo/:bar"));
+ assert!(key_match2("/foo/baz", "/:foo/:bar"));
+ assert!(key_match2("/foo/baz/foo", "/foo/:bar/foo"));
+ assert!(!key_match2("/baz", "/foo"));
+
+ // GH Issue #282
+ assert!(key_match2("/foo/bar", "/foo/:"));
+ assert!(!key_match2("/foo/bar/baz", "/foo/:"));
+ assert!(key_match2("/foo/bar/baz", "/foo/:/baz"));
+ assert!(!key_match2("/foo/bar", "/foo/:/baz"));
+ }
+
+ #[test]
+ fn test_regex_match() {
+ assert!(regex_match("foobar", "^foo*"));
+ assert!(!regex_match("barfoo", "^foo*"));
+ }
+
+ #[test]
+ fn test_key_match3() {
+ assert!(key_match3("/foo/bar", "/foo/*"));
+ assert!(key_match3("/foo/bar/baz", "/foo/*"));
+ assert!(key_match3("/foo/baz", "/foo/{bar}"));
+ assert!(key_match3("/foo/baz/foo", "/foo/{bar}/foo"));
+ assert!(!key_match3("/baz", "/foo"));
+
+ // GH Issue #282
+ assert!(key_match3("/foo/bar", "/foo/{}"));
+ assert!(key_match3("/foo/{}", "/foo/{}"));
+ assert!(!key_match3("/foo/bar/baz", "/foo/{}"));
+ assert!(!key_match3("/foo/bar", "/foo/{}/baz"));
+ assert!(key_match3("/foo/bar/baz", "/foo/{}/baz"));
+ }
+
+ #[cfg(feature = "ip")]
+ #[test]
+ fn test_ip_match() {
+ assert!(ip_match("::1", "::0:1"));
+ assert!(ip_match("192.168.1.1", "192.168.1.1"));
+ assert!(ip_match("127.0.0.1", "::ffff:127.0.0.1"));
+ assert!(ip_match("192.168.2.123", "192.168.2.0/24"));
+ assert!(!ip_match("::1", "127.0.0.2"));
+ assert!(!ip_match("192.168.2.189", "192.168.1.134/26"));
+ }
+
+ #[cfg(feature = "ip")]
+ #[test]
+ #[should_panic]
+ fn test_ip_match_panic_1() {
+ assert!(ip_match("I am alice", "127.0.0.1"));
+ }
+
+ #[cfg(feature = "ip")]
+ #[test]
+ #[should_panic]
+ fn test_ip_match_panic_2() {
+ assert!(ip_match("127.0.0.1", "I am alice"));
+ }
+
+ #[cfg(feature = "glob")]
+ #[test]
+ fn test_glob_match() {
+ assert!(glob_match("/abc/123", "/abc/*"));
+ assert!(!glob_match("/abc/123/456", "/abc/*"));
+ assert!(glob_match("/abc/123/456", "/abc/**"));
+ }
+}
diff --git a/casbin-rs/src/model/mod.rs b/casbin-rs/src/model/mod.rs
new file mode 100644
index 0000000..970d91c
--- /dev/null
+++ b/casbin-rs/src/model/mod.rs
@@ -0,0 +1,75 @@
+use crate::{rbac::RoleManager, Result};
+
+#[cfg(feature = "incremental")]
+use crate::emitter::EventData;
+
+use parking_lot::RwLock;
+
+use std::{collections::HashMap, sync::Arc};
+
+mod assertion;
+mod default_model;
+pub mod function_map;
+
+pub use assertion::{Assertion, AssertionMap};
+pub use default_model::DefaultModel;
+pub use function_map::*;
+
+pub trait Model: Send + Sync {
+ fn add_def(&mut self, sec: &str, key: &str, value: &str) -> bool;
+ fn get_model(&self) -> &HashMap<String, AssertionMap>;
+ fn get_mut_model(&mut self) -> &mut HashMap<String, AssertionMap>;
+ fn build_role_links(
+ &mut self,
+ rm: Arc<RwLock<dyn RoleManager>>,
+ ) -> Result<()>;
+ #[cfg(feature = "incremental")]
+ fn build_incremental_role_links(
+ &mut self,
+ rm: Arc<RwLock<dyn RoleManager>>,
+ d: EventData,
+ ) -> Result<()>;
+ fn add_policy(&mut self, sec: &str, ptype: &str, rule: Vec<String>)
+ -> bool;
+ fn add_policies(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rules: Vec<Vec<String>>,
+ ) -> bool;
+ fn get_policy(&self, sec: &str, ptype: &str) -> Vec<Vec<String>>;
+ fn get_filtered_policy(
+ &self,
+ sec: &str,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> Vec<Vec<String>>;
+ fn has_policy(&self, sec: &str, ptype: &str, rule: Vec<String>) -> bool;
+ fn get_values_for_field_in_policy(
+ &self,
+ sec: &str,
+ ptype: &str,
+ field_index: usize,
+ ) -> Vec<String>;
+ fn remove_policy(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rule: Vec<String>,
+ ) -> bool;
+ fn remove_policies(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ rules: Vec<Vec<String>>,
+ ) -> bool;
+ fn clear_policy(&mut self);
+ fn remove_filtered_policy(
+ &mut self,
+ sec: &str,
+ ptype: &str,
+ field_index: usize,
+ field_values: Vec<String>,
+ ) -> (bool, Vec<Vec<String>>);
+}
diff --git a/casbin-rs/src/prelude.rs b/casbin-rs/src/prelude.rs
new file mode 100644
index 0000000..62bf04e
--- /dev/null
+++ b/casbin-rs/src/prelude.rs
@@ -0,0 +1,17 @@
+pub use crate::{
+ CoreApi, DefaultModel, Enforcer, Event, EventData, EventEmitter, Filter,
+ IEnforcer, InternalApi, MemoryAdapter, MgmtApi, Model, NullAdapter,
+ RbacApi, Result, TryIntoAdapter, TryIntoModel,
+};
+
+#[cfg(all(
+ not(target_arch = "wasm32"),
+ not(feature = "runtime-teaclave")
+))]
+pub use crate::FileAdapter;
+
+#[cfg(feature = "cached")]
+pub use crate::{CachedApi, CachedEnforcer};
+
+#[cfg(feature = "watcher")]
+pub use crate::Watcher;
diff --git a/casbin-rs/src/rbac/default_role_manager.rs b/casbin-rs/src/rbac/default_role_manager.rs
new file mode 100644
index 0000000..3e632c5
--- /dev/null
+++ b/casbin-rs/src/rbac/default_role_manager.rs
@@ -0,0 +1,793 @@
+use crate::{
+ error::RbacError,
+ rbac::{MatchingFn, RoleManager},
+ Result,
+};
+use petgraph::stable_graph::{NodeIndex, StableDiGraph};
+use std::collections::{hash_map::Entry, HashMap, HashSet};
+
+#[cfg(feature = "cached")]
+use crate::cache::{Cache, DefaultCache};
+
+#[cfg(feature = "cached")]
+use std::{
+ collections::hash_map::DefaultHasher,
+ hash::{Hash, Hasher},
+};
+
+const DEFAULT_DOMAIN: &str = "DEFAULT";
+
+pub struct DefaultRoleManager {
+ all_domains: HashMap<String, StableDiGraph<String, EdgeVariant>>,
+ all_domains_indices: HashMap<String, HashMap<String, NodeIndex<u32>>>,
+ #[cfg(feature = "cached")]
+ cache: DefaultCache<u64, bool>,
+ max_hierarchy_level: usize,
+ role_matching_fn: Option<MatchingFn>,
+ domain_matching_fn: Option<MatchingFn>,
+}
+
+#[derive(Debug)]
+enum EdgeVariant {
+ Link,
+ Match,
+}
+
+impl DefaultRoleManager {
+ pub fn new(max_hierarchy_level: usize) -> Self {
+ DefaultRoleManager {
+ all_domains: HashMap::new(),
+ all_domains_indices: HashMap::new(),
+ max_hierarchy_level,
+ #[cfg(feature = "cached")]
+ cache: DefaultCache::new(50),
+ role_matching_fn: None,
+ domain_matching_fn: None,
+ }
+ }
+
+ fn get_or_create_role(
+ &mut self,
+ name: &str,
+ domain: Option<&str>,
+ ) -> NodeIndex<u32> {
+ let domain = domain.unwrap_or(DEFAULT_DOMAIN);
+
+ let graph = self
+ .all_domains
+ .entry(domain.into())
+ .or_insert_with(StableDiGraph::new);
+
+ let role_entry = self
+ .all_domains_indices
+ .entry(domain.into())
+ .or_insert_with(HashMap::new)
+ .entry(name.into());
+
+ let vacant_entry = match role_entry {
+ Entry::Occupied(e) => return *e.get(),
+ Entry::Vacant(e) => e,
+ };
+
+ let new_role_id = graph.add_node(name.into());
+ vacant_entry.insert(new_role_id);
+
+ if let Some(role_matching_fn) = self.role_matching_fn {
+ let mut added = false;
+
+ let node_ids: Vec<_> =
+ graph.node_indices().filter(|&i| graph[i] != name).collect();
+
+ for existing_role_id in node_ids {
+ added |= link_if_matches(
+ graph,
+ role_matching_fn,
+ new_role_id,
+ existing_role_id,
+ );
+
+ added |= link_if_matches(
+ graph,
+ role_matching_fn,
+ existing_role_id,
+ new_role_id,
+ );
+ }
+
+ if added {
+ #[cfg(feature = "cached")]
+ self.cache.clear();
+ }
+ }
+
+ new_role_id
+ }
+
+ fn matched_domains(&self, domain: Option<&str>) -> Vec<String> {
+ let domain = domain.unwrap_or(DEFAULT_DOMAIN);
+ if let Some(domain_matching_fn) = self.domain_matching_fn {
+ self.all_domains
+ .keys()
+ .filter_map(|key| {
+ if domain_matching_fn(domain, key) {
+ Some(key.to_owned())
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<String>>()
+ } else {
+ self.all_domains
+ .get(domain)
+ .map_or(vec![], |_| vec![domain.to_owned()])
+ }
+ }
+
+ fn domain_has_role(&self, name: &str, domain: Option<&str>) -> bool {
+ let matched_domains = self.matched_domains(domain);
+
+ matched_domains.iter().any(|domain| {
+ // try to find direct match of role
+ if self.all_domains_indices[domain].contains_key(name) {
+ true
+ } else if let Some(role_matching_fn) = self.role_matching_fn {
+ // else if role_matching_fn is set, iterate all graph nodes and try to find matching role
+ let graph = &self.all_domains[domain];
+
+ graph
+ .node_weights()
+ .any(|role| role_matching_fn(name, role))
+ } else {
+ false
+ }
+ })
+ }
+}
+
+/// link node of `not_pattern_id` to `maybe_pattern_id` if
+/// `not_pattern` matches `maybe_pattern`'s pattern and
+/// there doesn't exist a match edge yet
+fn link_if_matches(
+ graph: &mut StableDiGraph<String, EdgeVariant>,
+ role_matching_fn: fn(&str, &str) -> bool,
+ not_pattern_id: NodeIndex<u32>,
+ maybe_pattern_id: NodeIndex<u32>,
+) -> bool {
+ let not_pattern = &graph[not_pattern_id];
+ let maybe_pattern = &graph[maybe_pattern_id];
+
+ if !role_matching_fn(maybe_pattern, not_pattern) {
+ return false;
+ }
+
+ let add_edge =
+ if let Some(idx) = graph.find_edge(not_pattern_id, maybe_pattern_id) {
+ !matches!(graph[idx], EdgeVariant::Match)
+ } else {
+ true
+ };
+
+ if add_edge {
+ graph.add_edge(not_pattern_id, maybe_pattern_id, EdgeVariant::Match);
+
+ true
+ } else {
+ false
+ }
+}
+
+impl RoleManager for DefaultRoleManager {
+ fn clear(&mut self) {
+ self.all_domains_indices.clear();
+ self.all_domains.clear();
+ #[cfg(feature = "cached")]
+ self.cache.clear();
+ }
+
+ fn add_link(&mut self, name1: &str, name2: &str, domain: Option<&str>) {
+ if name1 == name2 {
+ return;
+ }
+
+ let role1 = self.get_or_create_role(name1, domain);
+ let role2 = self.get_or_create_role(name2, domain);
+
+ let graph = self
+ .all_domains
+ .get_mut(domain.unwrap_or(DEFAULT_DOMAIN))
+ .unwrap();
+
+ let add_link = if let Some(edge) = graph.find_edge(role1, role2) {
+ !matches!(graph[edge], EdgeVariant::Link)
+ } else {
+ true
+ };
+
+ if add_link {
+ graph.add_edge(role1, role2, EdgeVariant::Link);
+
+ #[cfg(feature = "cached")]
+ self.cache.clear()
+ }
+ }
+
+ fn matching_fn(
+ &mut self,
+ role_matching_fn: Option<MatchingFn>,
+ domain_matching_fn: Option<MatchingFn>,
+ ) {
+ self.domain_matching_fn = domain_matching_fn;
+ self.role_matching_fn = role_matching_fn;
+ }
+
+ fn delete_link(
+ &mut self,
+ name1: &str,
+ name2: &str,
+ domain: Option<&str>,
+ ) -> Result<()> {
+ if !self.domain_has_role(name1, domain)
+ || !self.domain_has_role(name2, domain)
+ {
+ return Err(
+ RbacError::NotFound(format!("{} OR {}", name1, name2)).into()
+ );
+ }
+
+ let role1 = self.get_or_create_role(name1, domain);
+ let role2 = self.get_or_create_role(name2, domain);
+
+ let graph = self
+ .all_domains
+ .get_mut(domain.unwrap_or(DEFAULT_DOMAIN))
+ .unwrap();
+
+ if let Some(edge_index) = graph.find_edge(role1, role2) {
+ graph.remove_edge(edge_index).unwrap();
+
+ #[cfg(feature = "cached")]
+ self.cache.clear();
+ }
+
+ Ok(())
+ }
+
+ fn has_link(&self, name1: &str, name2: &str, domain: Option<&str>) -> bool {
+ if name1 == name2 {
+ return true;
+ }
+
+ #[cfg(feature = "cached")]
+ let cache_key = {
+ let mut hasher = DefaultHasher::new();
+ name1.hash(&mut hasher);
+ name2.hash(&mut hasher);
+ domain.unwrap_or(DEFAULT_DOMAIN).hash(&mut hasher);
+ hasher.finish()
+ };
+
+ #[cfg(feature = "cached")]
+ if let Some(res) = self.cache.get(&cache_key) {
+ return res;
+ }
+
+ let matched_domains = self.matched_domains(domain);
+
+ let mut res = false;
+
+ for domain in matched_domains {
+ let graph = self.all_domains.get(&domain).unwrap();
+ let indices = self.all_domains_indices.get(&domain).unwrap();
+
+ let role1 = if let Some(role1) = indices.get(name1) {
+ Some(*role1)
+ } else {
+ graph.node_indices().find(|&i| {
+ let role_name = &graph[i];
+
+ role_name == name1
+ || self
+ .role_matching_fn
+ .map(|f| f(name1, role_name))
+ .unwrap_or_default()
+ })
+ };
+
+ let role1 = if let Some(role1) = role1 {
+ role1
+ } else {
+ continue;
+ };
+
+ let mut bfs = matching_bfs::Bfs::new(
+ graph,
+ role1,
+ self.max_hierarchy_level,
+ self.role_matching_fn.is_some(),
+ );
+
+ while let Some(node) = bfs.next(graph) {
+ let role_name = &graph[node];
+
+ if role_name == name2
+ || self
+ .role_matching_fn
+ .map(|f| f(role_name, name2))
+ .unwrap_or_default()
+ {
+ res = true;
+ break;
+ }
+ }
+ }
+
+ #[cfg(feature = "cached")]
+ self.cache.set(cache_key, res);
+
+ res
+ }
+
+ fn get_roles(&self, name: &str, domain: Option<&str>) -> Vec<String> {
+ let matched_domains = self.matched_domains(domain);
+
+ let res = matched_domains.into_iter().fold(
+ HashSet::new(),
+ |mut set, domain| {
+ let graph = &self.all_domains[&domain];
+
+ if let Some(role_node) = graph.node_indices().find(|&i| {
+ graph[i] == name
+ || self.role_matching_fn.unwrap_or(|_, _| false)(
+ name, &graph[i],
+ )
+ }) {
+ let neighbors = matching_bfs::bfs_iterator(
+ graph,
+ role_node,
+ self.role_matching_fn.is_some(),
+ )
+ .map(|i| graph[i].clone());
+
+ set.extend(neighbors);
+ }
+
+ set
+ },
+ );
+ res.into_iter().collect()
+ }
+
+ fn get_users(&self, name: &str, domain: Option<&str>) -> Vec<String> {
+ let matched_domains = self.matched_domains(domain);
+
+ let res = matched_domains.into_iter().fold(
+ HashSet::new(),
+ |mut set, domain| {
+ let graph = &self.all_domains[&domain];
+
+ if let Some(role_node) = graph.node_indices().find(|&i| {
+ graph[i] == name
+ || self
+ .role_matching_fn
+ .map(|f| f(name, &graph[i]))
+ .unwrap_or_default()
+ }) {
+ let neighbors = graph
+ .neighbors_directed(
+ role_node,
+ petgraph::Direction::Incoming,
+ )
+ .map(|i| graph[i].clone());
+
+ set.extend(neighbors);
+ }
+
+ set
+ },
+ );
+
+ res.into_iter().collect()
+ }
+}
+
+mod matching_bfs {
+ use super::EdgeVariant;
+ use fixedbitset::FixedBitSet;
+ use petgraph::graph::NodeIndex;
+ use petgraph::stable_graph::StableDiGraph;
+ use petgraph::visit::{EdgeRef, VisitMap, Visitable};
+ use std::collections::VecDeque;
+
+ #[derive(Clone)]
+ pub(super) struct Bfs {
+ /// The queue of nodes to visit
+ pub queue: VecDeque<NodeIndex<u32>>,
+ /// The map of discovered nodes
+ pub discovered: FixedBitSet,
+ /// Maximum depth
+ pub max_depth: usize,
+ /// Consider `Match` edges
+ pub with_pattern_matching: bool,
+
+ /// Current depth
+ depth: usize,
+ /// Number of elements until next depth is reached
+ depth_elements_remaining: usize,
+ }
+
+ impl Bfs {
+ /// Create a new **Bfs**, using the graph's visitor map, and put **start**
+ /// in the stack of nodes to visit.
+ pub fn new(
+ graph: &StableDiGraph<String, EdgeVariant>,
+ start: NodeIndex<u32>,
+ max_depth: usize,
+ with_pattern_matching: bool,
+ ) -> Self {
+ let mut discovered = graph.visit_map();
+ discovered.visit(start);
+
+ let mut queue = VecDeque::new();
+ queue.push_front(start);
+
+ Bfs {
+ queue,
+ discovered,
+ max_depth,
+ with_pattern_matching,
+ depth: 0,
+ depth_elements_remaining: 1,
+ }
+ }
+
+ /// Return the next node in the bfs, or **None** if the traversal is done.
+ pub fn next(
+ &mut self,
+ graph: &StableDiGraph<String, EdgeVariant>,
+ ) -> Option<NodeIndex<u32>> {
+ if self.max_depth <= self.depth {
+ return None;
+ }
+
+ if let Some(node) = self.queue.pop_front() {
+ self.update_depth();
+
+ let mut counter = 0;
+ for succ in
+ bfs_iterator(graph, node, self.with_pattern_matching)
+ {
+ if self.discovered.visit(succ) {
+ self.queue.push_back(succ);
+ counter += 1;
+ }
+ }
+
+ self.depth_elements_remaining += counter;
+
+ Some(node)
+ } else {
+ None
+ }
+ }
+
+ fn update_depth(&mut self) {
+ self.depth_elements_remaining -= 1;
+ if self.depth_elements_remaining == 0 {
+ self.depth += 1
+ }
+ }
+ }
+
+ pub(super) fn bfs_iterator(
+ graph: &StableDiGraph<String, EdgeVariant>,
+ node: NodeIndex<u32>,
+ with_matches: bool,
+ ) -> Box<dyn Iterator<Item = NodeIndex<u32>> + '_> {
+ // outgoing LINK edges of node
+ let outgoing_direct_edge = graph
+ .edges_directed(node, petgraph::Direction::Outgoing)
+ .filter_map(|edge| match *edge.weight() {
+ EdgeVariant::Link => Some(edge.target()),
+ EdgeVariant::Match => None,
+ });
+
+ if !with_matches {
+ return Box::new(outgoing_direct_edge);
+ }
+
+ // x := outgoing LINK edges of node
+ // outgoing_match_edge : outgoing MATCH edges of x FOR ALL x
+ let outgoing_match_edge = graph
+ .edges_directed(node, petgraph::Direction::Outgoing)
+ .filter(|edge| matches!(*edge.weight(), EdgeVariant::Link))
+ .flat_map(move |edge| {
+ graph
+ .edges_directed(
+ edge.target(),
+ petgraph::Direction::Outgoing,
+ )
+ .filter_map(|edge| match *edge.weight() {
+ EdgeVariant::Match => Some(edge.target()),
+ EdgeVariant::Link => None,
+ })
+ });
+
+ // x := incoming MATCH edges of node
+ // sibling_matched_by := outgoing LINK edges of x FOR ALL x
+ let sibling_matched_by = graph
+ .edges_directed(node, petgraph::Direction::Incoming)
+ .filter(|edge| matches!(*edge.weight(), EdgeVariant::Match))
+ .flat_map(move |edge| {
+ graph
+ .edges_directed(
+ edge.source(),
+ petgraph::Direction::Outgoing,
+ )
+ .filter_map(|edge| match *edge.weight() {
+ EdgeVariant::Link => Some(edge.target()),
+ EdgeVariant::Match => None,
+ })
+ });
+
+ Box::new(
+ outgoing_direct_edge
+ .chain(outgoing_match_edge)
+ .chain(sibling_matched_by),
+ )
+ }
+
+ #[cfg(test)]
+ mod test {
+ use super::*;
+ use petgraph::stable_graph::StableDiGraph;
+
+ #[test]
+ fn test_max_depth() {
+ let mut deps = StableDiGraph::<String, EdgeVariant>::new();
+ let pg = deps.add_node("petgraph".into());
+ let fb = deps.add_node("fixedbitset".into());
+ let qc = deps.add_node("quickcheck".into());
+ let rand = deps.add_node("rand".into());
+ let libc = deps.add_node("libc".into());
+
+ deps.extend_with_edges([
+ (pg, fb, EdgeVariant::Link),
+ (pg, qc, EdgeVariant::Link),
+ (qc, rand, EdgeVariant::Link),
+ (rand, libc, EdgeVariant::Link),
+ ]);
+
+ let mut bfs = Bfs::new(&deps, pg, 2, false);
+
+ let mut nodes = vec![];
+ while let Some(x) = bfs.next(&deps) {
+ nodes.push(x);
+ }
+
+ assert!(nodes.contains(&fb));
+ assert!(nodes.contains(&qc));
+ assert!(nodes.contains(&rand));
+ assert!(!nodes.contains(&libc));
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn sort_unstable<T: Ord>(mut v: Vec<T>) -> Vec<T> {
+ v.sort_unstable();
+ v
+ }
+
+ #[test]
+ fn test_role() {
+ let mut rm = DefaultRoleManager::new(3);
+ rm.add_link("u1", "g1", None);
+ rm.add_link("u2", "g1", None);
+ rm.add_link("u3", "g2", None);
+ rm.add_link("u4", "g2", None);
+ rm.add_link("u4", "g3", None);
+ rm.add_link("g1", "g3", None);
+
+ assert_eq!(true, rm.has_link("u1", "g1", None));
+ assert_eq!(false, rm.has_link("u1", "g2", None));
+ assert_eq!(true, rm.has_link("u1", "g3", None));
+ assert_eq!(true, rm.has_link("u2", "g1", None));
+ assert_eq!(false, rm.has_link("u2", "g2", None));
+ assert_eq!(true, rm.has_link("u2", "g3", None));
+ assert_eq!(false, rm.has_link("u3", "g1", None));
+ assert_eq!(true, rm.has_link("u3", "g2", None));
+ assert_eq!(false, rm.has_link("u3", "g3", None));
+ assert_eq!(false, rm.has_link("u4", "g1", None));
+ assert_eq!(true, rm.has_link("u4", "g2", None));
+ assert_eq!(true, rm.has_link("u4", "g3", None));
+
+ // test get_roles
+ assert_eq!(vec!["g1"], rm.get_roles("u1", None));
+ assert_eq!(vec!["g1"], rm.get_roles("u2", None));
+ assert_eq!(vec!["g2"], rm.get_roles("u3", None));
+ assert_eq!(vec!["g2", "g3"], sort_unstable(rm.get_roles("u4", None)));
+ assert_eq!(vec!["g3"], rm.get_roles("g1", None));
+ assert_eq!(vec![String::new(); 0], rm.get_roles("g2", None));
+ assert_eq!(vec![String::new(); 0], rm.get_roles("g3", None));
+
+ // test delete_link
+ rm.delete_link("g1", "g3", None).unwrap();
+ rm.delete_link("u4", "g2", None).unwrap();
+ assert_eq!(true, rm.has_link("u1", "g1", None));
+ assert_eq!(false, rm.has_link("u1", "g2", None));
+ assert_eq!(false, rm.has_link("u1", "g3", None));
+ assert_eq!(true, rm.has_link("u2", "g1", None));
+ assert_eq!(false, rm.has_link("u2", "g2", None));
+ assert_eq!(false, rm.has_link("u2", "g3", None));
+ assert_eq!(false, rm.has_link("u3", "g1", None));
+ assert_eq!(true, rm.has_link("u3", "g2", None));
+ assert_eq!(false, rm.has_link("u3", "g3", None));
+ assert_eq!(false, rm.has_link("u4", "g1", None));
+ assert_eq!(false, rm.has_link("u4", "g2", None));
+ assert_eq!(true, rm.has_link("u4", "g3", None));
+ assert_eq!(vec!["g1"], rm.get_roles("u1", None));
+ assert_eq!(vec!["g1"], rm.get_roles("u2", None));
+ assert_eq!(vec!["g2"], rm.get_roles("u3", None));
+ assert_eq!(vec!["g3"], rm.get_roles("u4", None));
+ assert_eq!(vec![String::new(); 0], rm.get_roles("g1", None));
+ assert_eq!(vec![String::new(); 0], rm.get_roles("g2", None));
+ assert_eq!(vec![String::new(); 0], rm.get_roles("g3", None));
+ }
+
+ #[test]
+ fn test_clear() {
+ let mut rm = DefaultRoleManager::new(3);
+ rm.add_link("u1", "g1", None);
+ rm.add_link("u2", "g1", None);
+ rm.add_link("u3", "g2", None);
+ rm.add_link("u4", "g2", None);
+ rm.add_link("u4", "g3", None);
+ rm.add_link("g1", "g3", None);
+
+ rm.clear();
+ assert_eq!(false, rm.has_link("u1", "g1", None));
+ assert_eq!(false, rm.has_link("u1", "g2", None));
+ assert_eq!(false, rm.has_link("u1", "g3", None));
+ assert_eq!(false, rm.has_link("u2", "g1", None));
+ assert_eq!(false, rm.has_link("u2", "g2", None));
+ assert_eq!(false, rm.has_link("u2", "g3", None));
+ assert_eq!(false, rm.has_link("u3", "g1", None));
+ assert_eq!(false, rm.has_link("u3", "g2", None));
+ assert_eq!(false, rm.has_link("u3", "g3", None));
+ assert_eq!(false, rm.has_link("u4", "g1", None));
+ assert_eq!(false, rm.has_link("u4", "g2", None));
+ assert_eq!(false, rm.has_link("u4", "g3", None));
+ }
+
+ #[test]
+ fn test_domain_role() {
+ let mut rm = DefaultRoleManager::new(3);
+ rm.add_link("u1", "g1", Some("domain1"));
+ rm.add_link("u2", "g1", Some("domain1"));
+ rm.add_link("u3", "admin", Some("domain2"));
+ rm.add_link("u4", "admin", Some("domain2"));
+ rm.add_link("u4", "admin", Some("domain1"));
+ rm.add_link("g1", "admin", Some("domain1"));
+
+ assert_eq!(true, rm.has_link("u1", "g1", Some("domain1")));
+ assert_eq!(false, rm.has_link("u1", "g1", Some("domain2")));
+ assert_eq!(true, rm.has_link("u1", "admin", Some("domain1")));
+ assert_eq!(false, rm.has_link("u1", "admin", Some("domain2")));
+
+ assert_eq!(true, rm.has_link("u2", "g1", Some("domain1")));
+ assert_eq!(false, rm.has_link("u2", "g1", Some("domain2")));
+ assert_eq!(true, rm.has_link("u2", "admin", Some("domain1")));
+ assert_eq!(false, rm.has_link("u2", "admin", Some("domain2")));
+
+ assert_eq!(false, rm.has_link("u3", "g1", Some("domain1")));
+ assert_eq!(false, rm.has_link("u3", "g1", Some("domain2")));
+ assert_eq!(false, rm.has_link("u3", "admin", Some("domain1")));
+ assert_eq!(true, rm.has_link("u3", "admin", Some("domain2")));
+
+ assert_eq!(false, rm.has_link("u4", "g1", Some("domain1")));
+ assert_eq!(false, rm.has_link("u4", "g1", Some("domain2")));
+ assert_eq!(true, rm.has_link("u4", "admin", Some("domain1")));
+ assert_eq!(true, rm.has_link("u4", "admin", Some("domain2")));
+
+ rm.delete_link("g1", "admin", Some("domain1")).unwrap();
+
+ rm.delete_link("u4", "admin", Some("domain2")).unwrap();
+
+ assert_eq!(true, rm.has_link("u1", "g1", Some("domain1")));
+ assert_eq!(false, rm.has_link("u1", "g1", Some("domain2")));
+ assert_eq!(false, rm.has_link("u1", "admin", Some("domain1")));
+ assert_eq!(false, rm.has_link("u1", "admin", Some("domain2")));
+
+ assert_eq!(true, rm.has_link("u2", "g1", Some("domain1")));
+ assert_eq!(false, rm.has_link("u2", "g1", Some("domain2")));
+ assert_eq!(false, rm.has_link("u2", "admin", Some("domain1")));
+ assert_eq!(false, rm.has_link("u2", "admin", Some("domain2")));
+
+ assert_eq!(false, rm.has_link("u3", "g1", Some("domain1")));
+ assert_eq!(false, rm.has_link("u3", "g1", Some("domain2")));
+ assert_eq!(false, rm.has_link("u3", "admin", Some("domain1")));
+ assert_eq!(true, rm.has_link("u3", "admin", Some("domain2")));
+
+ assert_eq!(false, rm.has_link("u4", "g1", Some("domain1")));
+ assert_eq!(false, rm.has_link("u4", "g1", Some("domain2")));
+ assert_eq!(true, rm.has_link("u4", "admin", Some("domain1")));
+ assert_eq!(false, rm.has_link("u4", "admin", Some("domain2")));
+ }
+
+ #[test]
+ fn test_users() {
+ let mut rm = DefaultRoleManager::new(3);
+ rm.add_link("u1", "g1", Some("domain1"));
+ rm.add_link("u2", "g1", Some("domain1"));
+
+ rm.add_link("u3", "g2", Some("domain2"));
+ rm.add_link("u4", "g2", Some("domain2"));
+
+ rm.add_link("u5", "g3", None);
+
+ assert_eq!(
+ vec!["u1", "u2"],
+ sort_unstable(rm.get_users("g1", Some("domain1")))
+ );
+ assert_eq!(
+ vec!["u3", "u4"],
+ sort_unstable(rm.get_users("g2", Some("domain2")))
+ );
+ assert_eq!(vec!["u5"], rm.get_users("g3", None));
+ }
+
+ #[test]
+ fn test_pattern_domain() {
+ use crate::model::key_match;
+ let mut rm = DefaultRoleManager::new(3);
+ rm.matching_fn(None, Some(key_match));
+ rm.add_link("u1", "g1", Some("*"));
+
+ assert!(rm.domain_has_role("u1", Some("domain2")));
+ }
+
+ #[test]
+ fn test_basic_role_matching() {
+ use crate::model::key_match;
+ let mut rm = DefaultRoleManager::new(10);
+ rm.matching_fn(Some(key_match), None);
+ rm.add_link("bob", "book_group", None);
+ rm.add_link("*", "book_group", None);
+ rm.add_link("*", "pen_group", None);
+ rm.add_link("eve", "pen_group", None);
+
+ assert!(rm.has_link("alice", "book_group", None));
+ assert!(rm.has_link("eve", "book_group", None));
+ assert!(rm.has_link("bob", "book_group", None));
+
+ assert_eq!(
+ vec!["book_group", "pen_group"],
+ sort_unstable(rm.get_roles("alice", None))
+ );
+ }
+
+ #[test]
+ fn test_basic_role_matching2() {
+ use crate::model::key_match;
+ let mut rm = DefaultRoleManager::new(10);
+ rm.matching_fn(Some(key_match), None);
+ rm.add_link("alice", "book_group", None);
+ rm.add_link("alice", "*", None);
+ rm.add_link("bob", "pen_group", None);
+
+ assert!(rm.has_link("alice", "book_group", None));
+ assert!(rm.has_link("alice", "pen_group", None));
+ assert!(rm.has_link("bob", "pen_group", None));
+ assert!(!rm.has_link("bob", "book_group", None));
+
+ assert_eq!(
+ vec!["*", "alice", "bob", "book_group", "pen_group"],
+ sort_unstable(rm.get_roles("alice", None))
+ );
+
+ assert_eq!(vec!["alice"], sort_unstable(rm.get_users("*", None)));
+ }
+}
diff --git a/casbin-rs/src/rbac/mod.rs b/casbin-rs/src/rbac/mod.rs
new file mode 100644
index 0000000..e74898a
--- /dev/null
+++ b/casbin-rs/src/rbac/mod.rs
@@ -0,0 +1,5 @@
+mod default_role_manager;
+mod role_manager;
+
+pub use default_role_manager::DefaultRoleManager;
+pub use role_manager::{MatchingFn, RoleManager};
diff --git a/casbin-rs/src/rbac/role_manager.rs b/casbin-rs/src/rbac/role_manager.rs
new file mode 100644
index 0000000..785ca08
--- /dev/null
+++ b/casbin-rs/src/rbac/role_manager.rs
@@ -0,0 +1,22 @@
+use crate::Result;
+
+pub type MatchingFn = fn(&str, &str) -> bool;
+
+pub trait RoleManager: Send + Sync {
+ fn clear(&mut self);
+ fn add_link(&mut self, name1: &str, name2: &str, domain: Option<&str>);
+ fn matching_fn(
+ &mut self,
+ role_matching_fn: Option<MatchingFn>,
+ domain_matching_fn: Option<MatchingFn>,
+ );
+ fn delete_link(
+ &mut self,
+ name1: &str,
+ name2: &str,
+ domain: Option<&str>,
+ ) -> Result<()>;
+ fn has_link(&self, name1: &str, name2: &str, domain: Option<&str>) -> bool;
+ fn get_roles(&self, name: &str, domain: Option<&str>) -> Vec<String>;
+ fn get_users(&self, name: &str, domain: Option<&str>) -> Vec<String>;
+}
diff --git a/casbin-rs/src/rbac_api.rs b/casbin-rs/src/rbac_api.rs
new file mode 100644
index 0000000..04d33ef
--- /dev/null
+++ b/casbin-rs/src/rbac_api.rs
@@ -0,0 +1,1283 @@
+use crate::{MgmtApi, Result};
+
+use async_trait::async_trait;
+
+use std::collections::HashSet;
+
+#[async_trait]
+pub trait RbacApi: MgmtApi {
+ async fn add_permission_for_user(
+ &mut self,
+ user: &str,
+ permission: Vec<String>,
+ ) -> Result<bool>;
+ async fn add_permissions_for_user(
+ &mut self,
+ user: &str,
+ permissions: Vec<Vec<String>>,
+ ) -> Result<bool>;
+ async fn add_role_for_user(
+ &mut self,
+ user: &str,
+ role: &str,
+ domain: Option<&str>,
+ ) -> Result<bool>;
+ async fn add_roles_for_user(
+ &mut self,
+ user: &str,
+ roles: Vec<String>,
+ domain: Option<&str>,
+ ) -> Result<bool>;
+ async fn delete_role_for_user(
+ &mut self,
+ user: &str,
+ role: &str,
+ domain: Option<&str>,
+ ) -> Result<bool>;
+ async fn delete_roles_for_user(
+ &mut self,
+ user: &str,
+ domain: Option<&str>,
+ ) -> Result<bool>;
+ async fn delete_user(&mut self, name: &str) -> Result<bool>;
+ async fn delete_role(&mut self, name: &str) -> Result<bool>;
+
+ async fn delete_permission(
+ &mut self,
+ permission: Vec<String>,
+ ) -> Result<bool> {
+ self.remove_filtered_policy(1, permission).await
+ }
+ async fn delete_permission_for_user(
+ &mut self,
+ user: &str,
+ permission: Vec<String>,
+ ) -> Result<bool>;
+
+ async fn delete_permissions_for_user(
+ &mut self,
+ user: &str,
+ ) -> Result<bool> {
+ self.remove_filtered_policy(
+ 0,
+ vec![user].iter().map(|s| (*s).to_string()).collect(),
+ )
+ .await
+ }
+
+ fn get_roles_for_user(
+ &mut self,
+ name: &str,
+ domain: Option<&str>,
+ ) -> Vec<String>;
+ fn get_users_for_role(
+ &self,
+ name: &str,
+ domain: Option<&str>,
+ ) -> Vec<String>;
+ fn has_role_for_user(
+ &mut self,
+ name: &str,
+ role: &str,
+ domain: Option<&str>,
+ ) -> bool;
+ fn get_permissions_for_user(
+ &self,
+ user: &str,
+ domain: Option<&str>,
+ ) -> Vec<Vec<String>>;
+ fn has_permission_for_user(
+ &self,
+ user: &str,
+ permission: Vec<String>,
+ ) -> bool;
+ fn get_implicit_roles_for_user(
+ &mut self,
+ name: &str,
+ domain: Option<&str>,
+ ) -> Vec<String>;
+ fn get_implicit_permissions_for_user(
+ &mut self,
+ name: &str,
+ domain: Option<&str>,
+ ) -> Vec<Vec<String>>;
+ async fn get_implicit_users_for_permission(
+ &self,
+ permission: Vec<String>,
+ ) -> Vec<String>;
+}
+
+#[async_trait]
+impl<T> RbacApi for T
+where
+ T: MgmtApi,
+{
+ async fn add_permission_for_user(
+ &mut self,
+ user: &str,
+ permission: Vec<String>,
+ ) -> Result<bool> {
+ let mut perm = permission;
+ perm.insert(0, user.to_string());
+ self.add_policy(perm).await
+ }
+
+ async fn add_permissions_for_user(
+ &mut self,
+ user: &str,
+ permissions: Vec<Vec<String>>,
+ ) -> Result<bool> {
+ let perms = permissions
+ .into_iter()
+ .map(|mut p| {
+ p.insert(0, user.to_string());
+ p
+ })
+ .collect();
+ self.add_policies(perms).await
+ }
+
+ async fn add_role_for_user(
+ &mut self,
+ user: &str,
+ role: &str,
+ domain: Option<&str>,
+ ) -> Result<bool> {
+ self.add_grouping_policy(if let Some(domain) = domain {
+ vec![user, role, domain]
+ .iter()
+ .map(|s| (*s).to_string())
+ .collect()
+ } else {
+ vec![user, role].iter().map(|s| (*s).to_string()).collect()
+ })
+ .await
+ }
+
+ async fn add_roles_for_user(
+ &mut self,
+ user: &str,
+ roles: Vec<String>,
+ domain: Option<&str>,
+ ) -> Result<bool> {
+ self.add_grouping_policies(
+ roles
+ .into_iter()
+ .map(|role| {
+ if let Some(domain) = domain {
+ vec![user.to_string(), role, domain.to_string()]
+ } else {
+ vec![user.to_string(), role]
+ }
+ })
+ .collect(),
+ )
+ .await
+ }
+
+ async fn delete_role_for_user(
+ &mut self,
+ user: &str,
+ role: &str,
+ domain: Option<&str>,
+ ) -> Result<bool> {
+ self.remove_grouping_policy(if let Some(domain) = domain {
+ vec![user, role, domain]
+ .iter()
+ .map(|s| (*s).to_string())
+ .collect()
+ } else {
+ vec![user, role].iter().map(|s| (*s).to_string()).collect()
+ })
+ .await
+ }
+
+ async fn delete_roles_for_user(
+ &mut self,
+ user: &str,
+ domain: Option<&str>,
+ ) -> Result<bool> {
+ self.remove_filtered_grouping_policy(
+ 0,
+ if let Some(domain) = domain {
+ vec![user, "", domain]
+ .iter()
+ .map(|s| (*s).to_string())
+ .collect()
+ } else {
+ vec![user].iter().map(|s| (*s).to_string()).collect()
+ },
+ )
+ .await
+ }
+
+ fn get_roles_for_user(
+ &mut self,
+ name: &str,
+ domain: Option<&str>,
+ ) -> Vec<String> {
+ let mut roles = vec![];
+ if let Some(t1) = self.get_mut_model().get_mut_model().get_mut("g") {
+ if let Some(t2) = t1.get_mut("g") {
+ roles = t2.rm.write().get_roles(name, domain);
+ }
+ }
+
+ roles
+ }
+
+ fn get_users_for_role(
+ &self,
+ name: &str,
+ domain: Option<&str>,
+ ) -> Vec<String> {
+ if let Some(t1) = self.get_model().get_model().get("g") {
+ if let Some(t2) = t1.get("g") {
+ return t2.rm.read().get_users(name, domain);
+ }
+ }
+ vec![]
+ }
+
+ fn has_role_for_user(
+ &mut self,
+ name: &str,
+ role: &str,
+ domain: Option<&str>,
+ ) -> bool {
+ let roles = self.get_roles_for_user(name, domain);
+ let mut has_role = false;
+ for r in roles {
+ if r == role {
+ has_role = true;
+ break;
+ }
+ }
+ has_role
+ }
+
+ async fn delete_user(&mut self, name: &str) -> Result<bool> {
+ let res1 = self
+ .remove_filtered_grouping_policy(0, vec![name.to_string()])
+ .await?;
+ let res2 = self
+ .remove_filtered_policy(0, vec![name.to_string()])
+ .await?;
+ Ok(res1 || res2)
+ }
+
+ async fn delete_role(&mut self, name: &str) -> Result<bool> {
+ let res1 = self
+ .remove_filtered_grouping_policy(1, vec![name.to_string()])
+ .await?;
+ let res2 = self
+ .remove_filtered_policy(0, vec![name.to_string()])
+ .await?;
+ Ok(res1 || res2)
+ }
+
+ async fn delete_permission_for_user(
+ &mut self,
+ user: &str,
+ permission: Vec<String>,
+ ) -> Result<bool> {
+ let mut permission = permission;
+ permission.insert(0, user.to_string());
+ self.remove_policy(permission).await
+ }
+
+ fn get_permissions_for_user(
+ &self,
+ user: &str,
+ domain: Option<&str>,
+ ) -> Vec<Vec<String>> {
+ self.get_filtered_policy(0, {
+ if let Some(domain) = domain {
+ vec![user, domain]
+ .iter()
+ .map(|s| (*s).to_string())
+ .collect()
+ } else {
+ vec![user].iter().map(|s| (*s).to_string()).collect()
+ }
+ })
+ }
+
+ fn has_permission_for_user(
+ &self,
+ user: &str,
+ permission: Vec<String>,
+ ) -> bool {
+ let mut permission = permission;
+ permission.insert(0, user.to_string());
+ self.has_policy(permission)
+ }
+
+ fn get_implicit_roles_for_user(
+ &mut self,
+ name: &str,
+ domain: Option<&str>,
+ ) -> Vec<String> {
+ let mut res: HashSet<String> = HashSet::new();
+ let mut q: Vec<String> = vec![name.to_owned()];
+ while !q.is_empty() {
+ let name = q.swap_remove(0);
+ let roles =
+ self.get_role_manager().write().get_roles(&name, domain);
+ for r in roles.into_iter() {
+ if res.insert(r.to_owned()) {
+ q.push(r);
+ }
+ }
+ }
+ res.into_iter().collect()
+ }
+
+ fn get_implicit_permissions_for_user(
+ &mut self,
+ user: &str,
+ domain: Option<&str>,
+ ) -> Vec<Vec<String>> {
+ let mut roles = self.get_implicit_roles_for_user(user, domain);
+ roles.insert(0, user.to_owned());
+
+ let mut res = vec![];
+
+ for role in roles.iter() {
+ let permissions = self.get_permissions_for_user(role, domain);
+ res.extend(permissions);
+ }
+ res
+ }
+
+ async fn get_implicit_users_for_permission(
+ &self,
+ permission: Vec<String>,
+ ) -> Vec<String> {
+ let mut subjects = self.get_all_subjects();
+ let roles = self.get_all_roles();
+
+ subjects.extend(roles.iter().flat_map(|role| {
+ self.get_role_manager().read().get_users(role, None)
+ }));
+
+ let users: Vec<String> = subjects
+ .into_iter()
+ .filter(|subject| !roles.contains(subject))
+ .collect();
+
+ let mut res: Vec<String> = vec![];
+ for user in users.iter() {
+ let mut req = permission.clone();
+ req.insert(0, user.to_string());
+ if let Ok(r) = self.enforce(req) {
+ if r && !res.contains(user) {
+ res.push(user.to_owned());
+ }
+ }
+ }
+ res
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::prelude::*;
+
+ fn sort_unstable<T: Ord>(mut v: Vec<T>) -> Vec<T> {
+ v.sort_unstable();
+ v
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_role_api() {
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_policy.csv");
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(vec!["data2_admin"], e.get_roles_for_user("alice", None));
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("bob", None));
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_roles_for_user("data2_admin", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_roles_for_user("non_exists", None)
+ );
+
+ assert_eq!(false, e.has_role_for_user("alice", "data1_admin", None));
+ assert_eq!(true, e.has_role_for_user("alice", "data2_admin", None));
+
+ e.add_role_for_user("alice", "data1_admin", None)
+ .await
+ .unwrap();
+ assert_eq!(
+ vec!["data1_admin", "data2_admin"],
+ sort_unstable(e.get_roles_for_user("alice", None))
+ );
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("bob", None));
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_roles_for_user("data2_admin", None)
+ );
+
+ e.delete_role_for_user("alice", "data1_admin", None)
+ .await
+ .unwrap();
+ assert_eq!(vec!["data2_admin"], e.get_roles_for_user("alice", None));
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("bob", None));
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_roles_for_user("data2_admin", None)
+ );
+
+ e.delete_roles_for_user("alice", None).await.unwrap();
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("alice", None));
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("bob", None));
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_roles_for_user("data2_admin", None)
+ );
+
+ e.add_roles_for_user(
+ "bob",
+ vec!["data1_admin", "data2_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ None,
+ )
+ .await
+ .unwrap();
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("alice", None));
+ assert_eq!(
+ vec!["data1_admin", "data2_admin"],
+ sort_unstable(e.get_roles_for_user("bob", None))
+ );
+
+ e.delete_roles_for_user("bob", None).await.unwrap();
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("alice", None));
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("bob", None));
+
+ e.add_role_for_user("alice", "data1_admin", None)
+ .await
+ .unwrap();
+ e.delete_user("alice").await.unwrap();
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("alice", None));
+ assert_eq!(vec![String::new(); 0], e.get_roles_for_user("bob", None));
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_roles_for_user("data2_admin", None)
+ );
+
+ e.add_role_for_user("alice", "data2_admin", None)
+ .await
+ .unwrap();
+ assert_eq!(false, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data1", "write")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("alice", "data2", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data2", "write")).unwrap());
+
+ e.delete_role("data2_admin").await.unwrap();
+ assert_eq!(false, e.enforce(("alice", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data2", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "data2", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "read")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data1", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "data2", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_role_api_threads() {
+ use parking_lot::RwLock;
+
+ use std::sync::Arc;
+ use std::thread;
+
+ #[cfg(feature = "runtime-async-std")]
+ use async_std::task;
+
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new("examples/rbac_policy.csv");
+ let e = Arc::new(RwLock::new(Enforcer::new(m, adapter).await.unwrap()));
+ let ee = e.clone();
+
+ assert_eq!(
+ vec!["data2_admin"],
+ e.write().get_roles_for_user("alice", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ e.write().get_roles_for_user("bob", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ e.write().get_roles_for_user("data2_admin", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ e.write().get_roles_for_user("non_exists", None)
+ );
+
+ assert_eq!(
+ false,
+ e.write().has_role_for_user("alice", "data1_admin", None)
+ );
+ assert_eq!(
+ true,
+ e.write().has_role_for_user("alice", "data2_admin", None)
+ );
+
+ thread::spawn(move || {
+ #[cfg(feature = "runtime-async-std")]
+ {
+ task::block_on(async move {
+ ee.write()
+ .add_role_for_user("alice", "data1_admin", None)
+ .await
+ .unwrap();
+
+ assert_eq!(
+ vec!["data1_admin", "data2_admin"],
+ sort_unstable(
+ ee.write().get_roles_for_user("alice", None)
+ )
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ ee.write().get_roles_for_user("bob", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ ee.write().get_roles_for_user("data2_admin", None)
+ );
+
+ ee.write()
+ .add_roles_for_user(
+ "bob",
+ vec!["data2_admin"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect(),
+ None,
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(
+ vec!["data1_admin", "data2_admin"],
+ sort_unstable(
+ ee.write().get_roles_for_user("alice", None)
+ )
+ );
+ assert_eq!(
+ vec!["data2_admin"],
+ ee.write().get_roles_for_user("bob", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ ee.write().get_roles_for_user("data2_admin", None)
+ );
+ });
+ }
+
+ #[cfg(feature = "runtime-tokio")]
+ {
+ tokio::runtime::Builder::new_multi_thread()
+ .enable_all()
+ .build()
+ .unwrap()
+ .block_on(async move {
+ ee.write()
+ .add_role_for_user("alice", "data1_admin", None)
+ .await
+ .unwrap();
+
+ assert_eq!(
+ vec!["data1_admin", "data2_admin"],
+ sort_unstable(
+ ee.write().get_roles_for_user("alice", None)
+ )
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ ee.write().get_roles_for_user("bob", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ ee.write().get_roles_for_user("data2_admin", None)
+ );
+
+ ee.write()
+ .add_roles_for_user(
+ "bob",
+ vec!["data2_admin".to_owned()],
+ None,
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(
+ vec!["data1_admin", "data2_admin"],
+ sort_unstable(
+ ee.write().get_roles_for_user("alice", None)
+ )
+ );
+ assert_eq!(
+ vec!["data2_admin"],
+ ee.write().get_roles_for_user("bob", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ ee.write().get_roles_for_user("data2_admin", None)
+ );
+ });
+ }
+ })
+ .join()
+ .unwrap();
+
+ e.write()
+ .delete_role_for_user("alice", "data1_admin", None)
+ .await
+ .unwrap();
+ e.write().delete_roles_for_user("bob", None).await.unwrap();
+ assert_eq!(
+ vec!["data2_admin"],
+ e.write().get_roles_for_user("alice", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ e.write().get_roles_for_user("bob", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ e.write().get_roles_for_user("data2_admin", None)
+ );
+
+ e.write()
+ .delete_roles_for_user("alice", None)
+ .await
+ .unwrap();
+ assert_eq!(
+ vec![String::new(); 0],
+ e.write().get_roles_for_user("alice", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ e.write().get_roles_for_user("bob", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ e.write().get_roles_for_user("data2_admin", None)
+ );
+
+ e.write()
+ .add_role_for_user("alice", "data1_admin", None)
+ .await
+ .unwrap();
+ e.write().delete_user("alice").await.unwrap();
+ assert_eq!(
+ vec![String::new(); 0],
+ e.write().get_roles_for_user("alice", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ e.write().get_roles_for_user("bob", None)
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ e.write().get_roles_for_user("data2_admin", None)
+ );
+
+ e.write()
+ .add_role_for_user("alice", "data2_admin", None)
+ .await
+ .unwrap();
+ assert_eq!(
+ false,
+ e.write().enforce(("alice", "data1", "read")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.write().enforce(("alice", "data1", "write")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.write().enforce(("alice", "data2", "read")).unwrap()
+ );
+ assert_eq!(
+ true,
+ e.write().enforce(("alice", "data2", "write")).unwrap()
+ );
+ assert_eq!(false, e.write().enforce(("bob", "data1", "read")).unwrap());
+ assert_eq!(
+ false,
+ e.write().enforce(("bob", "data1", "write")).unwrap()
+ );
+ assert_eq!(false, e.write().enforce(("bob", "data2", "read")).unwrap());
+ assert_eq!(true, e.write().enforce(("bob", "data2", "write")).unwrap());
+
+ e.write().delete_role("data2_admin").await.unwrap();
+ assert_eq!(
+ false,
+ e.write().enforce(("alice", "data1", "read")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.write().enforce(("alice", "data1", "write")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.write().enforce(("alice", "data2", "read")).unwrap()
+ );
+ assert_eq!(
+ false,
+ e.write().enforce(("alice", "data2", "write")).unwrap()
+ );
+ assert_eq!(false, e.write().enforce(("bob", "data1", "read")).unwrap());
+ assert_eq!(
+ false,
+ e.write().enforce(("bob", "data1", "write")).unwrap()
+ );
+ assert_eq!(false, e.write().enforce(("bob", "data2", "read")).unwrap());
+ assert_eq!(true, e.write().enforce(("bob", "data2", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_permission_api() {
+ let m = DefaultModel::from_file(
+ "examples/basic_without_resources_model.conf",
+ )
+ .await
+ .unwrap();
+
+ let adapter =
+ FileAdapter::new("examples/basic_without_resources_policy.csv");
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(true, e.enforce(("alice", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "write")).unwrap());
+
+ assert_eq!(
+ vec![vec!["alice", "read"]],
+ e.get_permissions_for_user("alice", None)
+ );
+ assert_eq!(
+ vec![vec!["bob", "write"]],
+ e.get_permissions_for_user("bob", None)
+ );
+
+ assert_eq!(
+ true,
+ e.has_permission_for_user(
+ "alice",
+ vec!["read"].iter().map(|s| s.to_string()).collect()
+ )
+ );
+ assert_eq!(
+ false,
+ e.has_permission_for_user(
+ "alice",
+ vec!["write"].iter().map(|s| s.to_string()).collect()
+ )
+ );
+ assert_eq!(
+ false,
+ e.has_permission_for_user(
+ "bob",
+ vec!["read"].iter().map(|s| s.to_string()).collect()
+ )
+ );
+ assert_eq!(
+ true,
+ e.has_permission_for_user(
+ "bob",
+ vec!["write"].iter().map(|s| s.to_string()).collect()
+ )
+ );
+
+ e.delete_permission(
+ vec!["read"].iter().map(|s| s.to_string()).collect(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(false, e.enforce(("alice", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "write")).unwrap());
+
+ e.add_permission_for_user(
+ "bob",
+ vec!["read"].iter().map(|s| s.to_string()).collect(),
+ )
+ .await
+ .unwrap();
+ e.add_permissions_for_user(
+ "eve",
+ vec![
+ vec!["read"].iter().map(|s| s.to_string()).collect(),
+ vec!["write"].iter().map(|s| s.to_string()).collect(),
+ ],
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(false, e.enforce(("alice", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "write")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "write")).unwrap());
+ assert_eq!(true, e.enforce(("eve", "read")).unwrap());
+ assert_eq!(true, e.enforce(("eve", "write")).unwrap());
+
+ e.delete_permission_for_user(
+ "bob",
+ vec!["read"].iter().map(|s| s.to_string()).collect(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(false, e.enforce(("alice", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "read")).unwrap());
+ assert_eq!(true, e.enforce(("bob", "write")).unwrap());
+ assert_eq!(true, e.enforce(("eve", "read")).unwrap());
+ assert_eq!(true, e.enforce(("eve", "write")).unwrap());
+
+ e.delete_permissions_for_user("bob").await.unwrap();
+ e.delete_permissions_for_user("eve").await.unwrap();
+
+ assert_eq!(false, e.enforce(("alice", "read")).unwrap());
+ assert_eq!(false, e.enforce(("alice", "write")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "read")).unwrap());
+ assert_eq!(false, e.enforce(("bob", "write")).unwrap());
+ assert_eq!(false, e.enforce(("eve", "read")).unwrap());
+ assert_eq!(false, e.enforce(("eve", "write")).unwrap());
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_implicit_role_api() {
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter =
+ FileAdapter::new("examples/rbac_with_hierarchy_policy.csv");
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(
+ vec![vec!["alice", "data1", "read"]],
+ e.get_permissions_for_user("alice", None)
+ );
+ assert_eq!(
+ vec![vec!["bob", "data2", "write"]],
+ e.get_permissions_for_user("bob", None)
+ );
+
+ assert_eq!(
+ vec!["admin", "data1_admin", "data2_admin"],
+ sort_unstable(e.get_implicit_roles_for_user("alice", None))
+ );
+ assert_eq!(
+ vec![String::new(); 0],
+ e.get_implicit_roles_for_user("bob", None)
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_implicit_permission_api() {
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter =
+ FileAdapter::new("examples/rbac_with_hierarchy_policy.csv");
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(
+ vec![vec!["alice", "data1", "read"]],
+ e.get_permissions_for_user("alice", None)
+ );
+ assert_eq!(
+ vec![vec!["bob", "data2", "write"]],
+ e.get_permissions_for_user("bob", None)
+ );
+
+ assert_eq!(
+ vec![
+ vec!["alice", "data1", "read"],
+ vec!["data1_admin", "data1", "read"],
+ vec!["data1_admin", "data1", "write"],
+ vec!["data2_admin", "data2", "read"],
+ vec!["data2_admin", "data2", "write"],
+ ],
+ sort_unstable(e.get_implicit_permissions_for_user("alice", None))
+ );
+ assert_eq!(
+ vec![vec!["bob", "data2", "write"]],
+ e.get_implicit_permissions_for_user("bob", None)
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_implicit_user_api() {
+ let m = DefaultModel::from_file("examples/rbac_model.conf")
+ .await
+ .unwrap();
+
+ let adapter =
+ FileAdapter::new("examples/rbac_with_hierarchy_policy.csv");
+ let e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(
+ vec!["alice"],
+ e.get_implicit_users_for_permission(
+ vec!["data1", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ .await
+ );
+ assert_eq!(
+ vec!["alice"],
+ e.get_implicit_users_for_permission(
+ vec!["data1", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ .await
+ );
+ assert_eq!(
+ vec!["alice"],
+ e.get_implicit_users_for_permission(
+ vec!["data2", "read"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ .await
+ );
+ assert_eq!(
+ vec!["alice", "bob"],
+ sort_unstable(
+ e.get_implicit_users_for_permission(
+ vec!["data2", "write"]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
+ )
+ .await
+ )
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_implicit_permission_api_with_domain() {
+ let m =
+ DefaultModel::from_file("examples/rbac_with_domains_model.conf")
+ .await
+ .unwrap();
+
+ let adapter = FileAdapter::new(
+ "examples/rbac_with_hierarchy_with_domains_policy.csv",
+ );
+ let mut e = Enforcer::new(m, adapter).await.unwrap();
+
+ assert_eq!(
+ vec![
+ vec!["alice", "domain1", "data2", "read"],
+ vec!["role:reader", "domain1", "data1", "read"],
+ vec!["role:writer", "domain1", "data1", "write"],
+ ],
+ sort_unstable(
+ e.get_implicit_permissions_for_user("alice", Some("domain1"))
+ )
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_pattern_matching_fn() {
+ let mut e = Enforcer::new(
+ "examples/rbac_with_pattern_model.conf",
+ "examples/rbac_with_pattern_policy.csv",
+ )
+ .await
+ .unwrap();
+
+ use crate::model::key_match2;
+
+ e.get_role_manager()
+ .write()
+ .matching_fn(Some(key_match2), None);
+
+ assert!(e.enforce(("alice", "/pen/1", "GET")).unwrap());
+ assert!(e.enforce(("alice", "/pen2/1", "GET")).unwrap());
+ assert!(e.enforce(("alice", "/book/1", "GET")).unwrap());
+ assert!(e.enforce(("alice", "/book/2", "GET")).unwrap());
+ assert!(e.enforce(("alice", "/pen/1", "GET")).unwrap());
+ assert!(!e.enforce(("alice", "/pen/2", "GET")).unwrap());
+ assert!(!e.enforce(("bob", "/book/1", "GET")).unwrap());
+ assert!(!e.enforce(("bob", "/book/2", "GET")).unwrap());
+ assert!(e.enforce(("bob", "/pen/1", "GET")).unwrap());
+ assert!(e.enforce(("bob", "/pen/2", "GET")).unwrap());
+
+ assert_eq!(
+ vec!["book_group"],
+ sort_unstable(e.get_implicit_roles_for_user("/book/1", None))
+ );
+
+ assert_eq!(
+ vec!["pen_group"],
+ sort_unstable(e.get_implicit_roles_for_user("/pen/1", None))
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_pattern_matching_fn_with_domain() {
+ let mut e = Enforcer::new(
+ "examples/rbac_with_pattern_domain_model.conf",
+ "examples/rbac_with_pattern_domain_policy.csv",
+ )
+ .await
+ .unwrap();
+
+ use crate::function_map::key_match;
+
+ e.get_role_manager()
+ .write()
+ .matching_fn(None, Some(key_match));
+
+ assert!(e.enforce(("alice", "domain1", "data1", "read")).unwrap());
+ assert!(e.enforce(("alice", "domain1", "data1", "write")).unwrap());
+ assert!(e.enforce(("alice", "domain2", "data2", "read")).unwrap());
+ assert!(e.enforce(("alice", "domain2", "data2", "write")).unwrap());
+
+ assert!(!e.enforce(("bob", "domain1", "data1", "read")).unwrap());
+ assert!(!e.enforce(("bob", "domain1", "data1", "write")).unwrap());
+ assert!(e.enforce(("bob", "domain2", "data2", "read")).unwrap());
+ assert!(e.enforce(("bob", "domain2", "data2", "write")).unwrap());
+
+ assert_eq!(
+ vec!["admin".to_owned()],
+ e.get_implicit_roles_for_user("alice", Some("domain3"))
+ );
+
+ assert_eq!(
+ vec!["alice".to_owned()],
+ e.get_users_for_role("admin", Some("domain3"))
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_pattern_matching_basic_role() {
+ let mut e = Enforcer::new(
+ "examples/rbac_basic_role_model.conf",
+ "examples/rbac_basic_role_policy.csv",
+ )
+ .await
+ .unwrap();
+
+ use crate::model::key_match;
+
+ e.get_role_manager()
+ .write()
+ .matching_fn(Some(key_match), None);
+
+ assert!(e.enforce(("alice", "/pen/1", "GET")).unwrap());
+ assert!(e.enforce(("alice", "/book/1", "GET")).unwrap());
+ assert!(e.enforce(("bob", "/pen/1", "GET")).unwrap());
+ assert!(e.enforce(("bob", "/book/1", "GET")).unwrap());
+
+ assert!(!e.enforce(("alice", "/pen/2", "GET")).unwrap());
+ assert!(!e.enforce(("alice", "/book/2", "GET")).unwrap());
+ assert!(!e.enforce(("bob", "/pen/2", "GET")).unwrap());
+ assert!(!e.enforce(("bob", "/book/2", "GET")).unwrap());
+
+ assert_eq!(
+ vec!["book_admin", "pen_admin"],
+ sort_unstable(e.get_implicit_roles_for_user("alice", None))
+ );
+ assert_eq!(
+ vec!["book_admin", "pen_admin"],
+ sort_unstable(e.get_implicit_roles_for_user("bob", None))
+ );
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg_attr(
+ all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+ async_std::test
+ )]
+ #[cfg_attr(
+ all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+ tokio::test
+ )]
+ async fn test_implicit_users_for_permission() {
+ let mut m = DefaultModel::default();
+ m.add_def("r", "r", "sub, obj, act");
+ m.add_def("p", "p", "sub, obj, act");
+ m.add_def("g", "g", "_, _");
+ m.add_def("e", "e", "some(where (p.eft == allow))");
+ m.add_def(
+ "m",
+ "m",
+ "g(r.sub, p.sub) && r.obj == p.obj && regexMatch(r.act, p.act)",
+ );
+
+ let a = MemoryAdapter::default();
+
+ let mut e = Enforcer::new(m, a).await.unwrap();
+
+ assert!(e
+ .add_policy(vec![
+ "role::r1".to_owned(),
+ "data1".to_owned(),
+ "(read)|(writer)".to_owned()
+ ])
+ .await
+ .unwrap());
+
+ assert!(e
+ .add_policy(vec![
+ "role::r2".to_owned(),
+ "data2".to_owned(),
+ "writer".to_owned()
+ ])
+ .await
+ .unwrap());
+
+ assert!(e
+ .add_policy(vec![
+ "user1".to_owned(),
+ "data2".to_owned(),
+ "writer".to_owned()
+ ])
+ .await
+ .unwrap());
+
+ assert!(e
+ .add_grouping_policy(vec![
+ "user2".to_owned(),
+ "role::r2".to_owned(),
+ ])
+ .await
+ .unwrap());
+
+ assert!(e
+ .add_grouping_policy(vec![
+ "user3".to_owned(),
+ "role::r2".to_owned(),
+ ])
+ .await
+ .unwrap());
+
+ assert_eq!(
+ vec!["user1".to_owned(), "user2".to_owned(), "user3".to_owned()],
+ sort_unstable(
+ e.get_implicit_users_for_permission(vec![
+ "data2".to_owned(),
+ "writer".to_owned()
+ ])
+ .await
+ )
+ );
+ }
+}
diff --git a/casbin-rs/src/util.rs b/casbin-rs/src/util.rs
new file mode 100644
index 0000000..e004588
--- /dev/null
+++ b/casbin-rs/src/util.rs
@@ -0,0 +1,185 @@
+use once_cell::sync::Lazy;
+use regex::Regex;
+
+use std::borrow::Cow;
+
+macro_rules! regex {
+ ($re:expr) => {
+ ::regex::Regex::new($re).unwrap()
+ };
+}
+
+static ESC_A: Lazy<Regex> = Lazy::new(|| regex!(r"\b(r\d*|p\d*)\."));
+#[allow(dead_code)]
+static ESC_G: Lazy<Regex> = Lazy::new(|| {
+ regex!(r"\b(g\d*)\(((?:\s*[r|p]\d*\.\w+\s*,\s*){1,2}\s*[r|p]\d*\.\w+\s*)\)")
+});
+#[cfg(not(feature = "runtime-teaclave"))]
+static ESC_C: Lazy<Regex> = Lazy::new(|| regex!(r#"(\s*"[^"]*"?|\s*[^,]*)"#));
+pub(crate) static ESC_E: Lazy<Regex> =
+ Lazy::new(|| regex!(r"\beval\(([^)]*)\)"));
+
+pub fn escape_assertion(s: &str) -> String {
+ ESC_A.replace_all(s, "${1}_").to_string()
+}
+
+pub fn remove_comment(s: &str) -> String {
+ let s = if let Some(idx) = s.find('#') {
+ &s[..idx]
+ } else {
+ s
+ };
+
+ s.trim_end().to_owned()
+}
+
+pub fn escape_eval(m: &str) -> Cow<str> {
+ ESC_E.replace_all(m, "eval(escape_assertion(${1}))")
+}
+
+#[cfg(not(feature = "runtime-teaclave"))]
+pub fn parse_csv_line<S: AsRef<str>>(line: S) -> Option<Vec<String>> {
+ let line = line.as_ref().trim();
+ if line.is_empty() || line.starts_with('#') {
+ return None;
+ }
+
+ let mut res = vec![];
+ for col in ESC_C.find_iter(line).map(|m| m.as_str().trim()) {
+ res.push({
+ if col.len() >= 2 && col.starts_with('"') && col.ends_with('"') {
+ col[1..col.len() - 1].to_owned()
+ } else {
+ col.to_owned()
+ }
+ })
+ }
+ if res.is_empty() {
+ None
+ } else {
+ Some(res)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_remove_comment() {
+ assert!(remove_comment("#").is_empty());
+ assert_eq!(
+ r#"g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.sub == "root""#,
+ remove_comment(
+ r#"g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.sub == "root" # root is the super user"#
+ )
+ );
+ }
+
+ #[test]
+ fn test_escape_assertion() {
+ let s = "g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act";
+ let exp = "g(r_sub, p_sub) && r_obj == p_obj && r_act == p_act";
+
+ assert_eq!(exp, escape_assertion(s));
+
+ let s1 = "g(r2.sub, p2.sub) && r2.obj == p2.obj && r2.act == p2.act";
+ let exp1 = "g(r2_sub, p2_sub) && r2_obj == p2_obj && r2_act == p2_act";
+
+ assert_eq!(exp1, escape_assertion(s1));
+ }
+
+ #[test]
+ fn test_csv_parse_1() {
+ assert_eq!(
+ parse_csv_line("alice, domain1, data1, action1"),
+ Some(vec![
+ "alice".to_owned(),
+ "domain1".to_owned(),
+ "data1".to_owned(),
+ "action1".to_owned()
+ ])
+ )
+ }
+
+ #[test]
+ fn test_csv_parse_2() {
+ assert_eq!(
+ parse_csv_line("alice, \"domain1, domain2\", data1 , action1"),
+ Some(vec![
+ "alice".to_owned(),
+ "domain1, domain2".to_owned(),
+ "data1".to_owned(),
+ "action1".to_owned()
+ ])
+ )
+ }
+
+ #[test]
+ fn test_csv_parse_3() {
+ assert_eq!(
+ parse_csv_line(","),
+ Some(vec!["".to_owned(), "".to_owned(),])
+ )
+ }
+
+ #[test]
+ fn test_csv_parse_4() {
+ assert_eq!(parse_csv_line(" "), None);
+ assert_eq!(parse_csv_line("#"), None);
+ assert_eq!(parse_csv_line(" #"), None);
+ }
+
+ #[test]
+ fn test_csv_parse_5() {
+ assert_eq!(
+ parse_csv_line(
+ "alice, \"domain1, domain2\", \"data1, data2\", action1"
+ ),
+ Some(vec![
+ "alice".to_owned(),
+ "domain1, domain2".to_owned(),
+ "data1, data2".to_owned(),
+ "action1".to_owned()
+ ])
+ )
+ }
+
+ #[test]
+ fn test_csv_parse_6() {
+ assert_eq!(parse_csv_line("\" "), Some(vec!["\"".to_owned()]))
+ }
+
+ #[test]
+ fn test_csv_parse_7() {
+ assert_eq!(
+ parse_csv_line("\" alice"),
+ Some(vec!["\" alice".to_owned()])
+ )
+ }
+
+ #[test]
+ fn test_csv_parse_8() {
+ assert_eq!(
+ parse_csv_line("alice, \"domain1, domain2"),
+ Some(vec!["alice".to_owned(), "\"domain1, domain2".to_owned(),])
+ )
+ }
+
+ #[test]
+ fn test_csv_parse_9() {
+ assert_eq!(parse_csv_line("\"\""), Some(vec!["".to_owned()]));
+ }
+
+ #[test]
+ fn test_csv_parse_10() {
+ assert_eq!(
+ parse_csv_line("r.sub.Status == \"ACTIVE\", /data1, read"),
+ Some(vec![
+ "r.sub.Status == \"ACTIVE\"".to_owned(),
+ "/data1".to_owned(),
+ "read".to_owned()
+ ])
+ );
+ }
+}
diff --git a/casbin-rs/src/watcher.rs b/casbin-rs/src/watcher.rs
new file mode 100644
index 0000000..07e6391
--- /dev/null
+++ b/casbin-rs/src/watcher.rs
@@ -0,0 +1,6 @@
+use crate::emitter::EventData;
+
+pub trait Watcher: Send + Sync {
+ fn set_update_callback(&mut self, cb: Box<dyn FnMut() + Send + Sync>);
+ fn update(&mut self, d: EventData);
+}
diff --git a/getrandom/.clippy.toml b/getrandom/.clippy.toml
index 492e05c..992016c 100644
--- a/getrandom/.clippy.toml
+++ b/getrandom/.clippy.toml
@@ -1 +1 @@
-msrv = "1.34"
+msrv = "1.36"
diff --git a/getrandom/.github/workflows/tests.yml b/getrandom/.github/workflows/tests.yml
index 8ca2d01..2f74f43 100644
--- a/getrandom/.github/workflows/tests.yml
+++ b/getrandom/.github/workflows/tests.yml
@@ -8,6 +8,9 @@
schedule:
- cron: "0 12 * * 1"
+permissions:
+ contents: read
+
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-Dwarnings"
@@ -15,21 +18,18 @@
jobs:
check-doc:
name: Docs, deadlinks, minimal dependencies
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v2
- - uses: actions-rs/toolchain@v1
- with:
- profile: minimal
- toolchain: nightly # Needed for -Z minimal-versions and doc_cfg
- override: true
- - uses: Swatinem/rust-cache@v1
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@nightly # Needed for -Z minimal-versions and doc_cfg
- name: Install precompiled cargo-deadlinks
run: |
- export URL=$(curl -s https://api.github.com/repos/deadlinks/cargo-deadlinks/releases/latest | jq -r '.assets[] | select(.name | contains("cargo-deadlinks-linux")) | .browser_download_url')
- wget -O /tmp/cargo-deadlinks $URL
- chmod +x /tmp/cargo-deadlinks
- mv /tmp/cargo-deadlinks ~/.cargo/bin
+ VERSION=0.8.1
+ URL="https://github.com/deadlinks/cargo-deadlinks/releases/download/${VERSION}/cargo-deadlinks-linux"
+ wget -O ~/.cargo/bin/cargo-deadlinks $URL
+ chmod +x ~/.cargo/bin/cargo-deadlinks
+ cargo deadlinks --version
+ - uses: Swatinem/rust-cache@v2
- name: Generate Docs
env:
RUSTDOCFLAGS: --cfg docsrs
@@ -38,34 +38,33 @@
cargo generate-lockfile -Z minimal-versions
cargo test --features=custom,std
+ # TODO: add aarch64-based macOS runner when it's supported by Github Actions
main-tests:
- name: Main tests
+ name: Tier 1 Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest, windows-latest]
- toolchain: [nightly, beta, stable, 1.34]
+ os: [ubuntu-22.04, windows-2022]
+ toolchain: [nightly, beta, stable, 1.36]
# Only Test macOS on stable to reduce macOS CI jobs
include:
- - os: macos-latest
+ - os: macos-12
toolchain: stable
steps:
- - uses: actions/checkout@v2
- - uses: actions-rs/toolchain@v1
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@master
with:
- profile: minimal
toolchain: ${{ matrix.toolchain }}
- override: true
- - uses: Swatinem/rust-cache@v1
+ - uses: Swatinem/rust-cache@v2
- run: cargo test
- run: cargo test --features=std
- run: cargo test --features=custom # custom should do nothing here
- if: ${{ matrix.toolchain == 'nightly' }}
- run: cargo build --benches
+ run: cargo test --benches
linux-tests:
- name: Additional Linux targets
- runs-on: ubuntu-latest
+ name: Linux Test
+ runs-on: ubuntu-22.04
strategy:
matrix:
target: [
@@ -74,47 +73,48 @@
i686-unknown-linux-musl,
]
steps:
- - uses: actions/checkout@v2
- - uses: actions-rs/toolchain@v1
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@stable
with:
- profile: minimal
- target: ${{ matrix.target }}
- toolchain: stable
- - uses: Swatinem/rust-cache@v1
+ targets: ${{ matrix.target }}
- name: Install multilib
- # update is needed to fix the 404 error on install, see:
- # https://github.com/actions/virtual-environments/issues/675
- run: |
- sudo apt-get update
- sudo apt-get install gcc-multilib
+ run: sudo apt-get update && sudo apt-get install gcc-multilib
+ - uses: Swatinem/rust-cache@v2
- run: cargo test --target=${{ matrix.target }} --features=std
- # We can only Build/Link on these targets for now.
- # TODO: Run the iOS binaries in the simulator
- # TODO: build/run aarch64-apple-darwin binaries on a x86_64 Mac
- apple-tests:
- name: Additional Apple targets
- runs-on: macos-latest
- strategy:
- matrix:
- target: [
- aarch64-apple-ios,
- x86_64-apple-ios,
- ]
+ ios-tests:
+ name: iOS Simulator Test
+ runs-on: macos-12
steps:
- - uses: actions/checkout@v2
- - uses: actions-rs/toolchain@v1
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@stable
with:
- profile: minimal
- target: ${{ matrix.target }}
- toolchain: stable
- - uses: Swatinem/rust-cache@v1
- - name: Build Tests
- run: cargo test --no-run --target=${{ matrix.target }} --features=std
+ targets: x86_64-apple-ios
+ - name: Install precompiled cargo-dinghy
+ run: |
+ VERSION=0.6.2
+ URL="https://github.com/sonos/dinghy/releases/download/${VERSION}/cargo-dinghy-macos-${VERSION}.tgz"
+ wget -O - $URL | tar -xz --strip-components=1 -C ~/.cargo/bin
+ cargo dinghy --version
+ - name: Setup Simulator
+ # Use the first installed iOS runtime and the first (i.e. oldest) supported iPhone device.
+ run: |
+ RUNTIME=$(xcrun simctl list runtimes --json | jq '.runtimes | map(select(.name | contains("iOS"))) | .[0]')
+ RUNTIME_ID=$(echo $RUNTIME | jq -r '.identifier')
+ echo "Using runtime:" $RUNTIME_ID
+ DEVICE_ID=$(echo $RUNTIME | jq -r '.supportedDeviceTypes | map(select(.productFamily == "iPhone")) | .[0].identifier')
+ echo "Using device:" $DEVICE_ID
+ SIM_ID=$(xcrun simctl create Test-iPhone $DEVICE_ID $RUNTIME_ID)
+ echo "Created simulator:" $SIM_ID
+ xcrun simctl boot $SIM_ID
+ echo "device=$SIM_ID" >> $GITHUB_ENV
+ - uses: Swatinem/rust-cache@v2
+ - name: Run tests
+ run: cargo dinghy -d ${{ env.device }} test
windows-tests:
- name: Additional Windows targets
- runs-on: windows-latest
+ name: Windows Test
+ runs-on: windows-2022
strategy:
matrix:
toolchain: [
@@ -123,217 +123,217 @@
stable-i686-msvc,
]
steps:
- - uses: actions/checkout@v2
- - name: Install toolchain
- uses: actions-rs/toolchain@v1
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@master
with:
- profile: minimal
toolchain: ${{ matrix.toolchain }}
- override: true
- - uses: Swatinem/rust-cache@v1
+ - uses: Swatinem/rust-cache@v2
- run: cargo test --features=std
cross-tests:
name: Cross Test
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
strategy:
matrix:
target: [
aarch64-unknown-linux-gnu,
- aarch64-linux-android,
+ # TODO: add Android tests back when the cross cuts a new release.
+ # See: https://github.com/cross-rs/cross/issues/1222
+ # aarch64-linux-android,
mips-unknown-linux-gnu,
+ wasm32-unknown-emscripten,
]
steps:
- - uses: actions/checkout@v2
- - uses: actions-rs/toolchain@v1
- with:
- profile: minimal
- target: ${{ matrix.target }}
- toolchain: stable
- - uses: Swatinem/rust-cache@v1
+ - uses: actions/checkout@v3
- name: Install precompiled cross
run: |
- export URL=$(curl -s https://api.github.com/repos/cross-rs/cross/releases/latest | jq -r '.assets[] | select(.name | contains("x86_64-unknown-linux-gnu.tar.gz")) | .browser_download_url')
- wget -O /tmp/binaries.tar.gz $URL
- tar -C /tmp -xzf /tmp/binaries.tar.gz
- mv /tmp/cross ~/.cargo/bin
+ VERSION=v0.2.5
+ URL=https://github.com/cross-rs/cross/releases/download/${VERSION}/cross-x86_64-unknown-linux-gnu.tar.gz
+ wget -O - $URL | tar -xz -C ~/.cargo/bin
+ cross --version
- name: Test
run: cross test --no-fail-fast --target=${{ matrix.target }} --features=std
+ macos-link:
+ name: macOS ARM64 Build/Link
+ runs-on: macos-12
+ steps:
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@nightly
+ with:
+ targets: aarch64-apple-darwin, aarch64-apple-ios
+ components: rust-src
+ - uses: Swatinem/rust-cache@v2
+ - run: cargo test --no-run --target=aarch64-apple-darwin --features=std
+ - run: cargo test --no-run --target=aarch64-apple-ios --features=std
+ - run: cargo test --no-run --target=aarch64-apple-watchos-sim -Zbuild-std --features=std
+
cross-link:
name: Cross Build/Link
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
strategy:
matrix:
target: [
- # See: https://github.com/rust-random/getrandom/issues/254
- # sparcv9-sun-solaris,
+ sparcv9-sun-solaris,
+ x86_64-unknown-illumos,
+ x86_64-unknown-freebsd,
x86_64-unknown-netbsd,
]
steps:
- - uses: actions/checkout@v2
- - uses: actions-rs/toolchain@v1
- with:
- profile: minimal
- target: ${{ matrix.target }}
- toolchain: stable
- - uses: Swatinem/rust-cache@v1
+ - uses: actions/checkout@v3
- name: Install precompiled cross
run: |
- export URL=$(curl -s https://api.github.com/repos/cross-rs/cross/releases/latest | jq -r '.assets[] | select(.name | contains("x86_64-unknown-linux-gnu.tar.gz")) | .browser_download_url')
- wget -O /tmp/binaries.tar.gz $URL
- tar -C /tmp -xzf /tmp/binaries.tar.gz
- mv /tmp/cross ~/.cargo/bin
+ VERSION=v0.2.5
+ URL=https://github.com/cross-rs/cross/releases/download/${VERSION}/cross-x86_64-unknown-linux-gnu.tar.gz
+ wget -O - $URL | tar -xz -C ~/.cargo/bin
+ cross --version
- name: Build Tests
run: cross test --no-run --target=${{ matrix.target }} --features=std
web-tests:
- name: Web tests
- runs-on: ubuntu-latest
+ name: Web Test
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: ubuntu-22.04
+ host: x86_64-unknown-linux-musl
+ - os: windows-2022
+ host: x86_64-pc-windows-msvc
+ - os: macos-12
+ host: x86_64-apple-darwin
+ runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v2
- - uses: actions-rs/toolchain@v1
- with:
- profile: minimal
- target: wasm32-unknown-unknown
- toolchain: stable
- - uses: Swatinem/rust-cache@v1
- - name: Install precompiled wasm-bindgen-test-runner
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@stable
+ - run: choco install wget
+ if: runner.os == 'Windows'
+ - name: Install precompiled wasm-pack
+ shell: bash
run: |
- export VERSION=$(cargo metadata --format-version=1 | jq -r '.packages[] | select ( .name == "wasm-bindgen" ) | .version')
- wget -O /tmp/binaries.tar.gz https://github.com/rustwasm/wasm-bindgen/releases/download/$VERSION/wasm-bindgen-$VERSION-x86_64-unknown-linux-musl.tar.gz
- tar -C /tmp -xzf /tmp/binaries.tar.gz --strip-components=1
- mv /tmp/wasm-bindgen-test-runner ~/.cargo/bin
+ VERSION=v0.11.0
+ URL=https://github.com/rustwasm/wasm-pack/releases/download/${VERSION}/wasm-pack-${VERSION}-${{ matrix.host }}.tar.gz
+ wget -O - $URL | tar -xz --strip-components=1 -C ~/.cargo/bin
+ wasm-pack --version
+ - uses: Swatinem/rust-cache@v2
- name: Test (Node)
- run: cargo test --target=wasm32-unknown-unknown --features=js
+ run: wasm-pack test --node --features=js
- name: Test (Firefox)
- env:
- GECKODRIVER: /usr/bin/geckodriver
- run: cargo test --target=wasm32-unknown-unknown --features=js,test-in-browser
+ run: wasm-pack test --headless --firefox --features=js,test-in-browser
- name: Test (Chrome)
- env:
- CHROMEDRIVER: /usr/bin/chromedriver
- run: cargo test --target=wasm32-unknown-unknown --features=js,test-in-browser
+ run: wasm-pack test --headless --chrome --features=js,test-in-browser
+ - name: Test (Edge)
+ if: runner.os == 'Windows'
+ # Edge has flaky failures: "driver failed to bind port during startup"
+ continue-on-error: true
+ run: wasm-pack test --headless --chrome --chromedriver $Env:EDGEWEBDRIVER\msedgedriver.exe --features=js,test-in-browser
+ - name: Test (Safari)
+ if: runner.os == 'macOS'
+ # Safari tests are broken: https://github.com/rustwasm/wasm-bindgen/issues/3004
+ continue-on-error: true
+ run: wasm-pack test --headless --safari --features=js,test-in-browser
- name: Test (custom getrandom)
- run: cargo test --target=wasm32-unknown-unknown --features=custom
+ run: wasm-pack test --node --features=custom
+
+ wasm64-tests:
+ name: wasm64 Build/Link
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@nightly # Need to build libstd
+ with:
+ components: rust-src
+ - uses: Swatinem/rust-cache@v2
+ - name: Build and Link tests (build-std)
+ # This target is Tier 3, so we have to build libstd ourselves.
+ # We currently cannot run these tests because wasm-bindgen-test-runner
+ # does not yet support memory64.
+ run: cargo test --no-run -Z build-std=std,panic_abort --target=wasm64-unknown-unknown --features=js
wasi-tests:
- name: WASI test
- runs-on: ubuntu-latest
+ name: WASI Test
+ runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v2
- - uses: actions-rs/toolchain@v1
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@stable
with:
- profile: minimal
- target: wasm32-wasi
- toolchain: stable
- - uses: Swatinem/rust-cache@v1
+ targets: wasm32-wasi
- name: Install precompiled wasmtime
run: |
- export URL=$(curl -s https://api.github.com/repos/bytecodealliance/wasmtime/releases/latest | jq -r '.assets[] | select(.name | contains("x86_64-linux.tar.xz")) | .browser_download_url')
- wget -O /tmp/binaries.tar.xz $URL
- tar -C /tmp -xf /tmp/binaries.tar.xz --strip-components=1
- mv /tmp/wasmtime ~/.cargo/bin
+ VERSION=v2.0.0
+ URL=https://github.com/bytecodealliance/wasmtime/releases/download/${VERSION}/wasmtime-${VERSION}-x86_64-linux.tar.xz
+ wget -O - $URL | tar -xJ --strip-components=1 -C ~/.cargo/bin
+ wasmtime --version
+ - uses: Swatinem/rust-cache@v2
- run: cargo test --target wasm32-wasi
- emscripten-tests:
- name: Emscripten tests
- runs-on: ubuntu-latest
- env:
- EMSDK_VERSION: 1.39.20 # Last emsdk compatible with Rust's LLVM 11
- steps:
- - uses: actions/checkout@v2
- - uses: actions-rs/toolchain@v1
- with:
- profile: minimal
- toolchain: stable
- - run: rustup target add wasm32-unknown-emscripten
- - run: rustup target add asmjs-unknown-emscripten
- - uses: Swatinem/rust-cache@v1
- - name: Cache emsdk
- id: cache-emsdk
- uses: actions/cache@v2
- with:
- path: ~/emsdk
- key: ${{ runner.os }}-${{ env.EMSDK_VERSION }}-emsdk
- - name: Install emsdk
- if: steps.cache-emsdk.outputs.cache-hit != 'true'
- run: |
- git clone https://github.com/emscripten-core/emsdk.git ~/emsdk
- cd ~/emsdk
- ./emsdk install $EMSDK_VERSION
- ./emsdk activate $EMSDK_VERSION
- - run: echo "$HOME/emsdk/upstream/emscripten" >> $GITHUB_PATH
- - name: wasm test
- run: cargo test --target=wasm32-unknown-emscripten --features=std
- - name: asm.js test
- run: | # Debug info breaks on asm.js
- RUSTFLAGS="$RUSTFLAGS -C debuginfo=0"
- cargo test --target=asmjs-unknown-emscripten --features=std
-
- build:
- name: Build only
- runs-on: ubuntu-latest
+ build-tier2:
+ name: Tier 2 Build
+ runs-on: ubuntu-22.04
strategy:
matrix:
target: [
- x86_64-unknown-freebsd,
- x86_64-fuchsia,
+ x86_64-unknown-fuchsia,
x86_64-unknown-redox,
x86_64-fortanix-unknown-sgx,
]
steps:
- - uses: actions/checkout@v2
- - uses: actions-rs/toolchain@v1
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@stable
with:
- profile: minimal
- target: ${{ matrix.target }}
- toolchain: nightly # Required to build libc for Redox
- override: true
- - uses: Swatinem/rust-cache@v1
+ targets: ${{ matrix.target }}
+ - uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --target=${{ matrix.target }} --features=std
- build-std:
- name: Build-only (build-std)
- runs-on: ubuntu-latest
+ build-tier3:
+ name: Tier 3 Build
+ runs-on: ubuntu-22.04
+ strategy:
+ matrix:
+ # Supported tier 3 targets without libstd support
+ target: [
+ x86_64-unknown-hermit,
+ x86_64-wrs-vxworks,
+ aarch64-kmc-solid_asp3,
+ armv6k-nintendo-3ds,
+ armv7-sony-vita-newlibeabihf,
+ riscv32imc-esp-espidf,
+ aarch64-unknown-nto-qnx710,
+ # `std` support still in progress. Can be moved up with the other
+ # apple targets after https://github.com/rust-lang/rust/pull/103503
+ aarch64-apple-tvos,
+ ]
+ include:
+ # Supported tier 3 targets with libstd support
+ - target: x86_64-unknown-openbsd
+ features: ["std"]
+ - target: x86_64-unknown-dragonfly
+ features: ["std"]
+ - target: x86_64-unknown-haiku
+ features: ["std"]
+ # Unsupported tier 3 targets to test the rdrand feature
+ - target: x86_64-unknown-uefi
+ features: ["rdrand"]
+ - target: x86_64-unknown-l4re-uclibc
+ features: ["rdrand"]
steps:
- - uses: actions/checkout@v2
- - name: Install toolchain
- uses: actions-rs/toolchain@v1
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@nightly # Required to build libcore
with:
- profile: minimal
- toolchain: nightly # Required to build libcore
components: rust-src
- override: true
- - uses: Swatinem/rust-cache@v1
- - name: Hermit (x86-64 only)
- run: cargo build -Z build-std=core --target=x86_64-unknown-hermit
- - name: UEFI (RDRAND)
- run: cargo build -Z build-std=core --features=rdrand --target=x86_64-unknown-uefi
- - name: L4Re (RDRAND)
- run: cargo build -Z build-std=core --features=rdrand --target=x86_64-unknown-l4re-uclibc
- - name: VxWorks
- run: cargo build -Z build-std=core --target=x86_64-wrs-vxworks
- - name: SOLID
- run: cargo build -Z build-std=core --target=aarch64-kmc-solid_asp3
- - name: Nintendo 3DS
- run: cargo build -Z build-std=core --target=armv6k-nintendo-3ds
+ - uses: Swatinem/rust-cache@v2
+ - run: cargo build -Z build-std=${{ contains(matrix.features, 'std') && 'std' || 'core'}} --target=${{ matrix.target }} --features="${{ join(matrix.features, ',') }}"
clippy-fmt:
name: Clippy + rustfmt
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v1
- - uses: actions-rs/toolchain@v1
+ - uses: dtolnay/rust-toolchain@stable
with:
- profile: minimal
- # https://github.com/rust-lang/rust-clippy/pull/6379 added MSRV
- # support, so we need to use nightly until this is on stable.
- toolchain: nightly
components: rustfmt, clippy
- override: true
- - uses: Swatinem/rust-cache@v1
+ - uses: Swatinem/rust-cache@v2
- name: clippy
run: cargo clippy --all --features=custom,std
- name: fmt
diff --git a/getrandom/CHANGELOG.md b/getrandom/CHANGELOG.md
index 4ab267a..1b57e18 100644
--- a/getrandom/CHANGELOG.md
+++ b/getrandom/CHANGELOG.md
@@ -4,6 +4,106 @@
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.2.10] - 2023-06-06
+### Added
+- Support for PS Vita (`armv7-sony-vita-newlibeabihf`) [#359]
+
+### Changed
+- Use getentropy from libc on Emscripten targets [#362]
+
+[#359]: https://github.com/rust-random/getrandom/pull/359
+[#362]: https://github.com/rust-random/getrandom/pull/362
+
+## [0.2.9] - 2023-04-06
+### Added
+- AIX support [#282]
+- `getrandom_uninit` function [#291]
+- `wasm64-unknown-unknown` support [#303]
+- tvOS and watchOS support [#317]
+- QNX/nto support [#325]
+- Support for `getrandom` syscall on NetBSD ≥ 10.0 [#331]
+- `RtlGenRandom` fallback for non-UWP Windows [#337]
+
+### Breaking Changes
+- Update MSRV to 1.36 [#291]
+
+### Fixed
+- Solaris/OpenBSD/Dragonfly build [#301]
+
+### Changed
+- Update MSRV to 1.36 [#291]
+- Use getentropy on Emscripten [#307]
+- Solaris: consistantly use `/dev/random` source [#310]
+- Move 3ds selection above rdrand/js/custom fallback [#312]
+- Remove buffer zeroing from Node.js implementation [#315]
+- Use `open` instead of `open64` [#326]
+- Remove #cfg from bsd_arandom.rs [#332]
+- Hermit: use `sys_read_entropy` syscall [#333]
+- Eliminate potential panic in sys_fill_exact [#334]
+- rdrand: Remove checking for 0 and !0 and instead check CPU family and do a self-test [#335]
+- Move `__getrandom_custom` definition into a const block [#344]
+- Switch the custom backend to Rust ABI [#347]
+
+[#282]: https://github.com/rust-random/getrandom/pull/282
+[#291]: https://github.com/rust-random/getrandom/pull/291
+[#301]: https://github.com/rust-random/getrandom/pull/301
+[#303]: https://github.com/rust-random/getrandom/pull/303
+[#307]: https://github.com/rust-random/getrandom/pull/307
+[#310]: https://github.com/rust-random/getrandom/pull/310
+[#312]: https://github.com/rust-random/getrandom/pull/312
+[#315]: https://github.com/rust-random/getrandom/pull/315
+[#317]: https://github.com/rust-random/getrandom/pull/317
+[#325]: https://github.com/rust-random/getrandom/pull/325
+[#326]: https://github.com/rust-random/getrandom/pull/326
+[#331]: https://github.com/rust-random/getrandom/pull/331
+[#332]: https://github.com/rust-random/getrandom/pull/332
+[#333]: https://github.com/rust-random/getrandom/pull/333
+[#334]: https://github.com/rust-random/getrandom/pull/334
+[#335]: https://github.com/rust-random/getrandom/pull/335
+[#337]: https://github.com/rust-random/getrandom/pull/337
+[#344]: https://github.com/rust-random/getrandom/pull/344
+[#347]: https://github.com/rust-random/getrandom/pull/347
+
+## [0.2.8] - 2022-10-20
+### Changed
+- The [Web Cryptography API] will now be preferred on `wasm32-unknown-unknown`
+ when using the `"js"` feature, even on Node.js [#284] [#295]
+
+### Added
+- Added benchmarks to track buffer initialization cost [#272]
+
+### Fixed
+- Use `$crate` in `register_custom_getrandom!` [#270]
+
+### Documentation
+- Add information about enabling `"js"` feature [#280]
+- Fix link to `wasm-bindgen` [#278]
+- Document the varied implementations for underlying randomness sources [#276]
+
+[Web Cryptography API]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API
+[#284]: https://github.com/rust-random/getrandom/pull/284
+[#295]: https://github.com/rust-random/getrandom/pull/295
+[#272]: https://github.com/rust-random/getrandom/pull/272
+[#270]: https://github.com/rust-random/getrandom/pull/270
+[#280]: https://github.com/rust-random/getrandom/pull/280
+[#278]: https://github.com/rust-random/getrandom/pull/278
+[#276]: https://github.com/rust-random/getrandom/pull/276
+
+## [0.2.7] - 2022-06-14
+### Changed
+- Update `wasi` dependency to `0.11` [#253]
+
+### Fixed
+- Use `AtomicPtr` instead of `AtomicUsize` for Strict Provenance compatibility. [#263]
+
+### Documentation
+- Add comments explaining use of fallback mechanisms [#257] [#260]
+
+[#263]: https://github.com/rust-random/getrandom/pull/263
+[#260]: https://github.com/rust-random/getrandom/pull/260
+[#253]: https://github.com/rust-random/getrandom/pull/253
+[#257]: https://github.com/rust-random/getrandom/pull/257
+
## [0.2.6] - 2022-03-28
### Added
- Nintendo 3DS (`armv6k-nintendo-3ds`) support [#248]
@@ -55,7 +155,7 @@
## [0.2.2] - 2021-01-19
### Changed
- Forward `rustc-dep-of-std` to dependencies. [#198]
-- Highlight feature-dependend functionality in documentation using the `doc_cfg` feature. [#200]
+- Highlight feature-dependent functionality in documentation using the `doc_cfg` feature. [#200]
[#198]: https://github.com/rust-random/getrandom/pull/198
[#200]: https://github.com/rust-random/getrandom/pull/200
@@ -185,7 +285,7 @@
## [0.1.9] - 2019-08-14 [YANKED]
### Changed
- Remove `std` dependency for opening and reading files. [#58]
-- Use `wasi` isntead of `libc` on WASI target. [#64]
+- Use `wasi` instead of `libc` on WASI target. [#64]
- By default emit a compile-time error when built for an unsupported target.
This behaviour can be disabled by using the `dummy` feature. [#71]
@@ -291,7 +391,11 @@
## [0.0.0] - 2019-01-19
Publish an empty template library.
-[0.2.5]: https://github.com/rust-random/getrandom/compare/v0.2.5...v0.2.6
+[0.2.10]: https://github.com/rust-random/getrandom/compare/v0.2.9...v0.2.10
+[0.2.9]: https://github.com/rust-random/getrandom/compare/v0.2.8...v0.2.9
+[0.2.8]: https://github.com/rust-random/getrandom/compare/v0.2.7...v0.2.8
+[0.2.7]: https://github.com/rust-random/getrandom/compare/v0.2.6...v0.2.7
+[0.2.6]: https://github.com/rust-random/getrandom/compare/v0.2.5...v0.2.6
[0.2.5]: https://github.com/rust-random/getrandom/compare/v0.2.4...v0.2.5
[0.2.4]: https://github.com/rust-random/getrandom/compare/v0.2.3...v0.2.4
[0.2.3]: https://github.com/rust-random/getrandom/compare/v0.2.2...v0.2.3
diff --git a/getrandom/Cargo.toml b/getrandom/Cargo.toml
index 9483a17..f0a0821 100644
--- a/getrandom/Cargo.toml
+++ b/getrandom/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "getrandom"
-version = "0.2.6" # Also update html_root_url in lib.rs when bumping this
+version = "0.2.10" # Also update html_root_url in lib.rs when bumping this
edition = "2018"
authors = ["The Rand Project Developers"]
license = "MIT OR Apache-2.0"
@@ -18,18 +18,18 @@
core = { version = "1.0", optional = true, package = "rustc-std-workspace-core" }
[target.'cfg(unix)'.dependencies]
-libc = { version = "0.2.120", default-features = false }
+libc = { version = "0.2.143", default-features = false }
[target.'cfg(target_vendor = "teaclave")'.dependencies]
sgx_libc = { version = "2.0.0", features = ["pthread"] }
[target.'cfg(target_os = "wasi")'.dependencies]
-wasi = "0.10"
+wasi = { version = "0.11", default-features = false }
-[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
+[target.'cfg(all(any(target_arch = "wasm32", target_arch = "wasm64"), target_os = "unknown"))'.dependencies]
wasm-bindgen = { version = "0.2.62", default-features = false, optional = true }
js-sys = { version = "0.3", optional = true }
-[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies]
+[target.'cfg(all(any(target_arch = "wasm32", target_arch = "wasm64"), target_os = "unknown"))'.dev-dependencies]
wasm-bindgen-test = "0.3.18"
[features]
@@ -37,7 +37,7 @@
std = []
# Feature to enable fallback RDRAND-based implementation on x86/x86_64
rdrand = []
-# Feature to enable JavaScript bindings on wasm32-unknown-unknown
+# Feature to enable JavaScript bindings on wasm*-unknown-unknown
js = ["wasm-bindgen", "js-sys"]
# Feature to enable custom RNG implementations
custom = []
diff --git a/getrandom/README.md b/getrandom/README.md
index df2307b..404b383 100644
--- a/getrandom/README.md
+++ b/getrandom/README.md
@@ -15,10 +15,10 @@
[License]: https://img.shields.io/crates/l/getrandom
-A Rust library for retrieving random data from (operating) system source. It is
-assumed that system always provides high-quality cryptographically secure random
+A Rust library for retrieving random data from (operating) system sources. It is
+assumed that the system always provides high-quality cryptographically secure random
data, ideally backed by hardware entropy sources. This crate derives its name
-from Linux's `getrandom` function, but is cross platform, roughly supporting
+from Linux's `getrandom` function, but is cross-platform, roughly supporting
the same set of platforms as Rust's `std` lib.
This is a low-level API. Most users should prefer using high-level random-number
@@ -52,7 +52,7 @@
## Minimum Supported Rust Version
-This crate requires Rust 1.34.0 or later.
+This crate requires Rust 1.36.0 or later.
# License
diff --git a/getrandom/SECURITY.md b/getrandom/SECURITY.md
new file mode 100644
index 0000000..19bfb9a
--- /dev/null
+++ b/getrandom/SECURITY.md
@@ -0,0 +1,13 @@
+# Security Policy
+
+## Supported Versions
+
+Security updates are applied only to the latest release.
+
+## Reporting a Vulnerability
+
+If you have discovered a security vulnerability in this project, please report it privately. **Do not disclose it as a public issue.** This gives us time to work with you to fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released.
+
+Please disclose it at [security advisory](https://github.com/rust-random/getrandom/security/advisories/new).
+
+This project is maintained by a team of volunteers on a reasonable-effort basis. As such, please give us at least 90 days to work on a fix before public exposure.
diff --git a/getrandom/benches/buffer.rs b/getrandom/benches/buffer.rs
new file mode 100644
index 0000000..b32be43
--- /dev/null
+++ b/getrandom/benches/buffer.rs
@@ -0,0 +1,71 @@
+#![feature(test, maybe_uninit_uninit_array_transpose)]
+extern crate test;
+
+use std::mem::MaybeUninit;
+
+// Call getrandom on a zero-initialized stack buffer
+#[inline(always)]
+fn bench_getrandom<const N: usize>() {
+ let mut buf = [0u8; N];
+ getrandom::getrandom(&mut buf).unwrap();
+ test::black_box(&buf as &[u8]);
+}
+
+// Call getrandom_uninit on an uninitialized stack buffer
+#[inline(always)]
+fn bench_getrandom_uninit<const N: usize>() {
+ let mut uninit = [MaybeUninit::uninit(); N];
+ let buf: &[u8] = getrandom::getrandom_uninit(&mut uninit).unwrap();
+ test::black_box(buf);
+}
+
+// We benchmark using #[inline(never)] "inner" functions for two reasons:
+// - Avoiding inlining reduces a source of variance when running benchmarks.
+// - It is _much_ easier to get the assembly or IR for the inner loop.
+//
+// For example, using cargo-show-asm (https://github.com/pacak/cargo-show-asm),
+// we can get the assembly for a particular benchmark's inner loop by running:
+// cargo asm --bench buffer --release buffer::p384::bench_getrandom::inner
+macro_rules! bench {
+ ( $name:ident, $size:expr ) => {
+ pub mod $name {
+ #[bench]
+ pub fn bench_getrandom(b: &mut test::Bencher) {
+ #[inline(never)]
+ fn inner() {
+ super::bench_getrandom::<{ $size }>()
+ }
+
+ b.bytes = $size as u64;
+ b.iter(inner);
+ }
+ #[bench]
+ pub fn bench_getrandom_uninit(b: &mut test::Bencher) {
+ #[inline(never)]
+ fn inner() {
+ super::bench_getrandom_uninit::<{ $size }>()
+ }
+
+ b.bytes = $size as u64;
+ b.iter(inner);
+ }
+ }
+ };
+}
+
+// 16 bytes (128 bits) is the size of an 128-bit AES key/nonce.
+bench!(aes128, 128 / 8);
+
+// 32 bytes (256 bits) is the seed sized used for rand::thread_rng
+// and the `random` value in a ClientHello/ServerHello for TLS.
+// This is also the size of a 256-bit AES/HMAC/P-256/Curve25519 key
+// and/or nonce.
+bench!(p256, 256 / 8);
+
+// A P-384/HMAC-384 key and/or nonce.
+bench!(p384, 384 / 8);
+
+// Initializing larger buffers is not the primary use case of this library, as
+// this should normally be done by a userspace CSPRNG. However, we have a test
+// here to see the effects of a lower (amortized) syscall overhead.
+bench!(page, 4096);
diff --git a/getrandom/benches/mod.rs b/getrandom/benches/mod.rs
deleted file mode 100644
index a93e720..0000000
--- a/getrandom/benches/mod.rs
+++ /dev/null
@@ -1,22 +0,0 @@
-#![feature(test)]
-extern crate test;
-
-#[bench]
-fn bench_64(b: &mut test::Bencher) {
- let mut buf = [0u8; 64];
- b.iter(|| {
- getrandom::getrandom(&mut buf[..]).unwrap();
- test::black_box(&buf);
- });
- b.bytes = buf.len() as u64;
-}
-
-#[bench]
-fn bench_65536(b: &mut test::Bencher) {
- let mut buf = [0u8; 65536];
- b.iter(|| {
- getrandom::getrandom(&mut buf[..]).unwrap();
- test::black_box(&buf);
- });
- b.bytes = buf.len() as u64;
-}
diff --git a/getrandom/src/3ds.rs b/getrandom/src/3ds.rs
index 6030512..87a32a1 100644
--- a/getrandom/src/3ds.rs
+++ b/getrandom/src/3ds.rs
@@ -9,8 +9,9 @@
//! Implementation for Nintendo 3DS
use crate::util_libc::sys_fill_exact;
use crate::Error;
+use core::mem::MaybeUninit;
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
sys_fill_exact(dest, |buf| unsafe {
libc::getrandom(buf.as_mut_ptr() as *mut libc::c_void, buf.len(), 0)
})
diff --git a/getrandom/src/apple-other.rs b/getrandom/src/apple-other.rs
new file mode 100644
index 0000000..8f90485
--- /dev/null
+++ b/getrandom/src/apple-other.rs
@@ -0,0 +1,27 @@
+// Copyright 2018 Developers of the Rand project.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Implementation for iOS
+use crate::Error;
+use core::{ffi::c_void, mem::MaybeUninit, ptr::null};
+
+#[link(name = "Security", kind = "framework")]
+extern "C" {
+ fn SecRandomCopyBytes(rnd: *const c_void, count: usize, bytes: *mut u8) -> i32;
+}
+
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ // Apple's documentation guarantees kSecRandomDefault is a synonym for NULL.
+ let ret = unsafe { SecRandomCopyBytes(null(), dest.len(), dest.as_mut_ptr() as *mut u8) };
+ // errSecSuccess (from SecBase.h) is always zero.
+ if ret != 0 {
+ Err(Error::IOS_SEC_RANDOM)
+ } else {
+ Ok(())
+ }
+}
diff --git a/getrandom/src/bsd_arandom.rs b/getrandom/src/bsd_arandom.rs
index f26f260..5314c48 100644
--- a/getrandom/src/bsd_arandom.rs
+++ b/getrandom/src/bsd_arandom.rs
@@ -7,10 +7,13 @@
// except according to those terms.
//! Implementation for FreeBSD and NetBSD
-use crate::{util_libc::sys_fill_exact, Error};
-use core::ptr;
+use crate::{
+ util_libc::{sys_fill_exact, Weak},
+ Error,
+};
+use core::{mem::MaybeUninit, ptr};
-fn kern_arnd(buf: &mut [u8]) -> libc::ssize_t {
+fn kern_arnd(buf: &mut [MaybeUninit<u8>]) -> libc::ssize_t {
static MIB: [libc::c_int; 2] = [libc::CTL_KERN, libc::KERN_ARND];
let mut len = buf.len();
let ret = unsafe {
@@ -30,19 +33,18 @@
}
}
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
- #[cfg(target_os = "freebsd")]
- {
- use crate::util_libc::Weak;
- static GETRANDOM: Weak = unsafe { Weak::new("getrandom\0") };
- type GetRandomFn =
- unsafe extern "C" fn(*mut u8, libc::size_t, libc::c_uint) -> libc::ssize_t;
+type GetRandomFn = unsafe extern "C" fn(*mut u8, libc::size_t, libc::c_uint) -> libc::ssize_t;
- if let Some(fptr) = GETRANDOM.ptr() {
- let func: GetRandomFn = unsafe { core::mem::transmute(fptr) };
- return sys_fill_exact(dest, |buf| unsafe { func(buf.as_mut_ptr(), buf.len(), 0) });
- }
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ // getrandom(2) was introduced in FreeBSD 12.0 and NetBSD 10.0
+ static GETRANDOM: Weak = unsafe { Weak::new("getrandom\0") };
+ if let Some(fptr) = GETRANDOM.ptr() {
+ let func: GetRandomFn = unsafe { core::mem::transmute(fptr) };
+ return sys_fill_exact(dest, |buf| unsafe {
+ func(buf.as_mut_ptr() as *mut u8, buf.len(), 0)
+ });
}
+
// Both FreeBSD and NetBSD will only return up to 256 bytes at a time, and
// older NetBSD kernels will fail on longer buffers.
for chunk in dest.chunks_mut(256) {
diff --git a/getrandom/src/custom.rs b/getrandom/src/custom.rs
index 6110b05..c9207d0 100644
--- a/getrandom/src/custom.rs
+++ b/getrandom/src/custom.rs
@@ -7,8 +7,8 @@
// except according to those terms.
//! An implementation which calls out to an externally defined function.
-use crate::Error;
-use core::num::NonZeroU32;
+use crate::{util::uninit_slice_fill_zero, Error};
+use core::{mem::MaybeUninit, num::NonZeroU32};
/// Register a function to be invoked by `getrandom` on unsupported targets.
///
@@ -76,24 +76,36 @@
#[cfg_attr(docsrs, doc(cfg(feature = "custom")))]
macro_rules! register_custom_getrandom {
($path:path) => {
- // We use an extern "C" function to get the guarantees of a stable ABI.
- #[no_mangle]
- extern "C" fn __getrandom_custom(dest: *mut u8, len: usize) -> u32 {
- let f: fn(&mut [u8]) -> Result<(), ::getrandom::Error> = $path;
- let slice = unsafe { ::core::slice::from_raw_parts_mut(dest, len) };
- match f(slice) {
- Ok(()) => 0,
- Err(e) => e.code().get(),
+ // TODO(MSRV 1.37): change to unnamed block
+ const __getrandom_internal: () = {
+ // We use Rust ABI to be safe against potential panics in the passed function.
+ #[no_mangle]
+ unsafe fn __getrandom_custom(dest: *mut u8, len: usize) -> u32 {
+ // Make sure the passed function has the type of getrandom::getrandom
+ type F = fn(&mut [u8]) -> ::core::result::Result<(), $crate::Error>;
+ let _: F = $crate::getrandom;
+ let f: F = $path;
+ let slice = ::core::slice::from_raw_parts_mut(dest, len);
+ match f(slice) {
+ Ok(()) => 0,
+ Err(e) => e.code().get(),
+ }
}
- }
+ };
};
}
#[allow(dead_code)]
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
- extern "C" {
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ extern "Rust" {
fn __getrandom_custom(dest: *mut u8, len: usize) -> u32;
}
+ // Previously we always passed a valid, initialized slice to
+ // `__getrandom_custom`. Ensure `dest` has been initialized for backward
+ // compatibility with implementations that rely on that (e.g. Rust
+ // implementations that construct a `&mut [u8]` slice from `dest` and
+ // `len`).
+ let dest = uninit_slice_fill_zero(dest);
let ret = unsafe { __getrandom_custom(dest.as_mut_ptr(), dest.len()) };
match NonZeroU32::new(ret) {
None => Ok(()),
diff --git a/getrandom/src/dragonfly.rs b/getrandom/src/dragonfly.rs
index f27e906..d3ef00a 100644
--- a/getrandom/src/dragonfly.rs
+++ b/getrandom/src/dragonfly.rs
@@ -12,14 +12,18 @@
util_libc::{sys_fill_exact, Weak},
Error,
};
+use core::mem::MaybeUninit;
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
static GETRANDOM: Weak = unsafe { Weak::new("getrandom\0") };
type GetRandomFn = unsafe extern "C" fn(*mut u8, libc::size_t, libc::c_uint) -> libc::ssize_t;
+ // getrandom(2) was introduced in DragonflyBSD 5.7
if let Some(fptr) = GETRANDOM.ptr() {
let func: GetRandomFn = unsafe { core::mem::transmute(fptr) };
- return sys_fill_exact(dest, |buf| unsafe { func(buf.as_mut_ptr(), buf.len(), 0) });
+ return sys_fill_exact(dest, |buf| unsafe {
+ func(buf.as_mut_ptr() as *mut u8, buf.len(), 0)
+ });
} else {
use_file::getrandom_inner(dest)
}
diff --git a/getrandom/src/emscripten.rs b/getrandom/src/emscripten.rs
new file mode 100644
index 0000000..30221c6
--- /dev/null
+++ b/getrandom/src/emscripten.rs
@@ -0,0 +1,13 @@
+//! Implementation for Emscripten
+use crate::{util_libc::last_os_error, Error};
+use core::mem::MaybeUninit;
+
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ // Emscripten 2.0.5 added getentropy, so we can use it unconditionally.
+ // Unlike other getentropy implementations, there is no max buffer length.
+ let ret = unsafe { libc::getentropy(dest.as_mut_ptr() as *mut libc::c_void, dest.len()) };
+ if ret < 0 {
+ return Err(last_os_error());
+ }
+ Ok(())
+}
diff --git a/getrandom/src/error.rs b/getrandom/src/error.rs
index c743ed2..10df95b 100644
--- a/getrandom/src/error.rs
+++ b/getrandom/src/error.rs
@@ -43,16 +43,19 @@
pub const FAILED_RDRAND: Error = internal_error(5);
/// RDRAND instruction unsupported on this target.
pub const NO_RDRAND: Error = internal_error(6);
- /// The browser does not have support for `self.crypto`.
+ /// The environment does not support the Web Crypto API.
pub const WEB_CRYPTO: Error = internal_error(7);
- /// The browser does not have support for `crypto.getRandomValues`.
+ /// Calling Web Crypto API `crypto.getRandomValues` failed.
pub const WEB_GET_RANDOM_VALUES: Error = internal_error(8);
/// On VxWorks, call to `randSecure` failed (random number generator is not yet initialized).
pub const VXWORKS_RAND_SECURE: Error = internal_error(11);
- /// NodeJS does not have support for the `crypto` module.
+ /// Node.js does not have the `crypto` CommonJS module.
pub const NODE_CRYPTO: Error = internal_error(12);
- /// NodeJS does not have support for `crypto.randomFillSync`.
+ /// Calling Node.js function `crypto.randomFillSync` failed.
pub const NODE_RANDOM_FILL_SYNC: Error = internal_error(13);
+ /// Called from an ES module on Node.js. This is unsupported, see:
+ /// <https://docs.rs/getrandom#nodejs-es-module-support>.
+ pub const NODE_ES_MODULE: Error = internal_error(14);
/// Codes below this point represent OS Errors (i.e. positive i32 values).
/// Codes at or above this point, but below [`Error::CUSTOM_START`] are
@@ -112,10 +115,6 @@
let idx = buf.iter().position(|&b| b == 0).unwrap_or(n);
core::str::from_utf8(&buf[..idx]).ok()
}
- } else if #[cfg(target_os = "wasi")] {
- fn os_err(errno: i32, _buf: &mut [u8]) -> Option<wasi::Error> {
- wasi::Error::from_raw_error(errno as _)
- }
} else {
fn os_err(_errno: i32, _buf: &mut [u8]) -> Option<&str> {
None
@@ -173,10 +172,11 @@
Error::FAILED_RDRAND => Some("RDRAND: failed multiple times: CPU issue likely"),
Error::NO_RDRAND => Some("RDRAND: instruction not supported"),
Error::WEB_CRYPTO => Some("Web Crypto API is unavailable"),
- Error::WEB_GET_RANDOM_VALUES => Some("Web API crypto.getRandomValues is unavailable"),
+ Error::WEB_GET_RANDOM_VALUES => Some("Calling Web API crypto.getRandomValues failed"),
Error::VXWORKS_RAND_SECURE => Some("randSecure: VxWorks RNG module is not initialized"),
- Error::NODE_CRYPTO => Some("Node.js crypto module is unavailable"),
- Error::NODE_RANDOM_FILL_SYNC => Some("Node.js API crypto.randomFillSync is unavailable"),
+ Error::NODE_CRYPTO => Some("Node.js crypto CommonJS module is unavailable"),
+ Error::NODE_RANDOM_FILL_SYNC => Some("Calling Node.js API crypto.randomFillSync failed"),
+ Error::NODE_ES_MODULE => Some("Node.js ES modules are not directly supported, see https://docs.rs/getrandom#nodejs-es-module-support"),
_ => None,
}
}
diff --git a/getrandom/src/espidf.rs b/getrandom/src/espidf.rs
index dce8a2a..d074dc4 100644
--- a/getrandom/src/espidf.rs
+++ b/getrandom/src/espidf.rs
@@ -8,13 +8,13 @@
//! Implementation for ESP-IDF
use crate::Error;
-use core::ffi::c_void;
+use core::{ffi::c_void, mem::MaybeUninit};
extern "C" {
fn esp_fill_random(buf: *mut c_void, len: usize) -> u32;
}
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
// Not that NOT enabling WiFi, BT, or the voltage noise entropy source (via `bootloader_random_enable`)
// will cause ESP-IDF to return pseudo-random numbers based on the voltage noise entropy, after the initial boot process:
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/random.html
diff --git a/getrandom/src/fuchsia.rs b/getrandom/src/fuchsia.rs
index 572ff53..5a135f3 100644
--- a/getrandom/src/fuchsia.rs
+++ b/getrandom/src/fuchsia.rs
@@ -8,13 +8,14 @@
//! Implementation for Fuchsia Zircon
use crate::Error;
+use core::mem::MaybeUninit;
#[link(name = "zircon")]
extern "C" {
fn zx_cprng_draw(buffer: *mut u8, length: usize);
}
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
- unsafe { zx_cprng_draw(dest.as_mut_ptr(), dest.len()) }
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ unsafe { zx_cprng_draw(dest.as_mut_ptr() as *mut u8, dest.len()) }
Ok(())
}
diff --git a/getrandom/src/hermit.rs b/getrandom/src/hermit.rs
new file mode 100644
index 0000000..570b03d
--- /dev/null
+++ b/getrandom/src/hermit.rs
@@ -0,0 +1,21 @@
+use crate::Error;
+use core::{cmp::min, mem::MaybeUninit, num::NonZeroU32};
+
+extern "C" {
+ fn sys_read_entropy(buffer: *mut u8, length: usize, flags: u32) -> isize;
+}
+
+pub fn getrandom_inner(mut dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ while !dest.is_empty() {
+ let res = unsafe { sys_read_entropy(dest.as_mut_ptr() as *mut u8, dest.len(), 0) };
+ if res < 0 {
+ // SAFETY: all Hermit error codes use i32 under the hood:
+ // https://github.com/hermitcore/libhermit-rs/blob/master/src/errno.rs
+ let code = unsafe { NonZeroU32::new_unchecked((-res) as u32) };
+ return Err(code.into());
+ }
+ let len = min(res as usize, dest.len());
+ dest = &mut dest[len..];
+ }
+ Ok(())
+}
diff --git a/getrandom/src/ios.rs b/getrandom/src/ios.rs
deleted file mode 100644
index 226de16..0000000
--- a/getrandom/src/ios.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2018 Developers of the Rand project.
-//
-// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
-// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
-// option. This file may not be copied, modified, or distributed
-// except according to those terms.
-
-//! Implementation for iOS
-use crate::Error;
-use core::{ffi::c_void, ptr::null};
-
-#[link(name = "Security", kind = "framework")]
-extern "C" {
- fn SecRandomCopyBytes(rnd: *const c_void, count: usize, bytes: *mut u8) -> i32;
-}
-
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
- // Apple's documentation guarantees kSecRandomDefault is a synonym for NULL.
- let ret = unsafe { SecRandomCopyBytes(null(), dest.len(), dest.as_mut_ptr()) };
- // errSecSuccess (from SecBase.h) is always zero.
- if ret != 0 {
- Err(Error::IOS_SEC_RANDOM)
- } else {
- Ok(())
- }
-}
diff --git a/getrandom/src/js.rs b/getrandom/src/js.rs
index e910f2b..d031282 100644
--- a/getrandom/src/js.rs
+++ b/getrandom/src/js.rs
@@ -8,17 +8,20 @@
use crate::Error;
extern crate std;
-use std::thread_local;
+use std::{mem::MaybeUninit, thread_local};
-use js_sys::{global, Uint8Array};
+use js_sys::{global, Function, Uint8Array};
use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
+// Size of our temporary Uint8Array buffer used with WebCrypto methods
// Maximum is 65536 bytes see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
-const BROWSER_CRYPTO_BUFFER_SIZE: usize = 256;
+const WEB_CRYPTO_BUFFER_SIZE: usize = 256;
+// Node.js's crypto.randomFillSync requires the size to be less than 2**31.
+const NODE_MAX_BUFFER_SIZE: usize = (1 << 31) - 1;
enum RngSource {
Node(NodeCrypto),
- Browser(BrowserCrypto, Uint8Array),
+ Web(WebCrypto, Uint8Array),
}
// JsValues are always per-thread, so we initialize RngSource for each thread.
@@ -27,20 +30,33 @@
static RNG_SOURCE: Result<RngSource, Error> = getrandom_init();
);
-pub(crate) fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
+pub(crate) fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
RNG_SOURCE.with(|result| {
let source = result.as_ref().map_err(|&e| e)?;
match source {
RngSource::Node(n) => {
- if n.random_fill_sync(dest).is_err() {
- return Err(Error::NODE_RANDOM_FILL_SYNC);
+ for chunk in dest.chunks_mut(NODE_MAX_BUFFER_SIZE) {
+ // SAFETY: chunk is never used directly, the memory is only
+ // modified via the Uint8Array view, which is passed
+ // directly to JavaScript. Also, crypto.randomFillSync does
+ // not resize the buffer. We know the length is less than
+ // u32::MAX because of the chunking above.
+ // Note that this uses the fact that JavaScript doesn't
+ // have a notion of "uninitialized memory", this is purely
+ // a Rust/C/C++ concept.
+ let res = n.random_fill_sync(unsafe {
+ Uint8Array::view_mut_raw(chunk.as_mut_ptr() as *mut u8, chunk.len())
+ });
+ if res.is_err() {
+ return Err(Error::NODE_RANDOM_FILL_SYNC);
+ }
}
}
- RngSource::Browser(crypto, buf) => {
+ RngSource::Web(crypto, buf) => {
// getRandomValues does not work with all types of WASM memory,
// so we initially write to browser memory to avoid exceptions.
- for chunk in dest.chunks_mut(BROWSER_CRYPTO_BUFFER_SIZE) {
+ for chunk in dest.chunks_mut(WEB_CRYPTO_BUFFER_SIZE) {
// The chunk can be smaller than buf's length, so we call to
// JS to create a smaller view of buf without allocation.
let sub_buf = buf.subarray(0, chunk.len() as u32);
@@ -48,7 +64,9 @@
if crypto.get_random_values(&sub_buf).is_err() {
return Err(Error::WEB_GET_RANDOM_VALUES);
}
- sub_buf.copy_to(chunk);
+
+ // SAFETY: `sub_buf`'s length is the same length as `chunk`
+ unsafe { sub_buf.raw_copy_to_ptr(chunk.as_mut_ptr() as *mut u8) };
}
}
};
@@ -58,25 +76,33 @@
fn getrandom_init() -> Result<RngSource, Error> {
let global: Global = global().unchecked_into();
- if is_node(&global) {
- let crypto = NODE_MODULE
- .require("crypto")
- .map_err(|_| Error::NODE_CRYPTO)?;
- return Ok(RngSource::Node(crypto));
- }
- // Assume we are in some Web environment (browser or web worker). We get
- // `self.crypto` (called `msCrypto` on IE), so we can call
- // `crypto.getRandomValues`. If `crypto` isn't defined, we assume that
- // we are in an older web browser and the OS RNG isn't available.
- let crypto = match (global.crypto(), global.ms_crypto()) {
- (c, _) if c.is_object() => c,
- (_, c) if c.is_object() => c,
- _ => return Err(Error::WEB_CRYPTO),
+ // Get the Web Crypto interface if we are in a browser, Web Worker, Deno,
+ // or another environment that supports the Web Cryptography API. This
+ // also allows for user-provided polyfills in unsupported environments.
+ let crypto = match global.crypto() {
+ // Standard Web Crypto interface
+ c if c.is_object() => c,
+ // Node.js CommonJS Crypto module
+ _ if is_node(&global) => {
+ // If module.require isn't a valid function, we are in an ES module.
+ match Module::require_fn().and_then(JsCast::dyn_into::<Function>) {
+ Ok(require_fn) => match require_fn.call1(&global, &JsValue::from_str("crypto")) {
+ Ok(n) => return Ok(RngSource::Node(n.unchecked_into())),
+ Err(_) => return Err(Error::NODE_CRYPTO),
+ },
+ Err(_) => return Err(Error::NODE_ES_MODULE),
+ }
+ }
+ // IE 11 Workaround
+ _ => match global.ms_crypto() {
+ c if c.is_object() => c,
+ _ => return Err(Error::WEB_CRYPTO),
+ },
};
- let buf = Uint8Array::new_with_length(BROWSER_CRYPTO_BUFFER_SIZE as u32);
- Ok(RngSource::Browser(crypto, buf))
+ let buf = Uint8Array::new_with_length(WEB_CRYPTO_BUFFER_SIZE as u32);
+ Ok(RngSource::Web(crypto, buf))
}
// Taken from https://www.npmjs.com/package/browser-or-node
@@ -93,29 +119,35 @@
#[wasm_bindgen]
extern "C" {
- type Global; // Return type of js_sys::global()
+ // Return type of js_sys::global()
+ type Global;
- // Web Crypto API (https://www.w3.org/TR/WebCryptoAPI/)
- #[wasm_bindgen(method, getter, js_name = "msCrypto")]
- fn ms_crypto(this: &Global) -> BrowserCrypto;
+ // Web Crypto API: Crypto interface (https://www.w3.org/TR/WebCryptoAPI/)
+ type WebCrypto;
+ // Getters for the WebCrypto API
#[wasm_bindgen(method, getter)]
- fn crypto(this: &Global) -> BrowserCrypto;
- type BrowserCrypto;
+ fn crypto(this: &Global) -> WebCrypto;
+ #[wasm_bindgen(method, getter, js_name = msCrypto)]
+ fn ms_crypto(this: &Global) -> WebCrypto;
+ // Crypto.getRandomValues()
#[wasm_bindgen(method, js_name = getRandomValues, catch)]
- fn get_random_values(this: &BrowserCrypto, buf: &Uint8Array) -> Result<(), JsValue>;
+ fn get_random_values(this: &WebCrypto, buf: &Uint8Array) -> Result<(), JsValue>;
- // We use a "module" object here instead of just annotating require() with
- // js_name = "module.require", so that Webpack doesn't give a warning. See:
- // https://github.com/rust-random/getrandom/issues/224
- type NodeModule;
- #[wasm_bindgen(js_name = module)]
- static NODE_MODULE: NodeModule;
// Node JS crypto module (https://nodejs.org/api/crypto.html)
- #[wasm_bindgen(method, catch)]
- fn require(this: &NodeModule, s: &str) -> Result<NodeCrypto, JsValue>;
type NodeCrypto;
+ // crypto.randomFillSync()
#[wasm_bindgen(method, js_name = randomFillSync, catch)]
- fn random_fill_sync(this: &NodeCrypto, buf: &mut [u8]) -> Result<(), JsValue>;
+ fn random_fill_sync(this: &NodeCrypto, buf: Uint8Array) -> Result<(), JsValue>;
+
+ // Ideally, we would just use `fn require(s: &str)` here. However, doing
+ // this causes a Webpack warning. So we instead return the function itself
+ // and manually invoke it using call1. This also lets us to check that the
+ // function actually exists, allowing for better error messages. See:
+ // https://github.com/rust-random/getrandom/issues/224
+ // https://github.com/rust-random/getrandom/issues/256
+ type Module;
+ #[wasm_bindgen(getter, static_method_of = Module, js_class = module, js_name = require, catch)]
+ fn require_fn() -> Result<JsValue, JsValue>;
// Node JS process Object (https://nodejs.org/api/process.html)
#[wasm_bindgen(method, getter)]
diff --git a/getrandom/src/lib.rs b/getrandom/src/lib.rs
index 1e521f3..f1eb7a7 100644
--- a/getrandom/src/lib.rs
+++ b/getrandom/src/lib.rs
@@ -14,26 +14,28 @@
//! | ----------------- | ------------------ | --------------
//! | Linux, Android | `*‑linux‑*` | [`getrandom`][1] system call if available, otherwise [`/dev/urandom`][2] after successfully polling `/dev/random`
//! | Windows | `*‑windows‑*` | [`BCryptGenRandom`]
-//! | macOS | `*‑apple‑darwin` | [`getentropy`][3] if available, otherwise [`/dev/random`][4] (identical to `/dev/urandom`)
-//! | iOS | `*‑apple‑ios` | [`SecRandomCopyBytes`]
+//! | macOS | `*‑apple‑darwin` | [`getentropy`][3] if available, otherwise [`/dev/urandom`][4] (identical to `/dev/random`)
+//! | iOS, tvOS, watchOS | `*‑apple‑ios`, `*-apple-tvos`, `*-apple-watchos` | [`SecRandomCopyBytes`]
//! | FreeBSD | `*‑freebsd` | [`getrandom`][5] if available, otherwise [`kern.arandom`][6]
//! | OpenBSD | `*‑openbsd` | [`getentropy`][7]
-//! | NetBSD | `*‑netbsd` | [`kern.arandom`][8]
-//! | Dragonfly BSD | `*‑dragonfly` | [`getrandom`][9] if available, otherwise [`/dev/random`][10]
+//! | NetBSD | `*‑netbsd` | [`getrandom`][16] if available, otherwise [`kern.arandom`][8]
+//! | Dragonfly BSD | `*‑dragonfly` | [`getrandom`][9] if available, otherwise [`/dev/urandom`][10] (identical to `/dev/random`)
//! | Solaris, illumos | `*‑solaris`, `*‑illumos` | [`getrandom`][11] if available, otherwise [`/dev/random`][12]
//! | Fuchsia OS | `*‑fuchsia` | [`cprng_draw`]
//! | Redox | `*‑redox` | `/dev/urandom`
-//! | Haiku | `*‑haiku` | `/dev/random` (identical to `/dev/urandom`)
-//! | Hermit | `x86_64-*-hermit` | [`RDRAND`]
+//! | Haiku | `*‑haiku` | `/dev/urandom` (identical to `/dev/random`)
+//! | Hermit | `*-hermit` | [`sys_read_entropy`]
//! | SGX | `x86_64‑*‑sgx` | [`RDRAND`]
//! | VxWorks | `*‑wrs‑vxworks‑*` | `randABytes` after checking entropy pool initialization with `randSecure`
//! | ESP-IDF | `*‑espidf` | [`esp_fill_random`]
-//! | Emscripten | `*‑emscripten` | `/dev/random` (identical to `/dev/urandom`)
+//! | Emscripten | `*‑emscripten` | [`getentropy`][13]
//! | WASI | `wasm32‑wasi` | [`random_get`]
-//! | Web Browser | `wasm32‑*‑unknown` | [`Crypto.getRandomValues`], see [WebAssembly support]
-//! | Node.js | `wasm32‑*‑unknown` | [`crypto.randomBytes`], see [WebAssembly support]
+//! | Web Browser and Node.js | `wasm*‑*‑unknown` | [`Crypto.getRandomValues`] if available, then [`crypto.randomFillSync`] if on Node.js, see [WebAssembly support]
//! | SOLID | `*-kmc-solid_*` | `SOLID_RNG_SampleRandomBytes`
//! | Nintendo 3DS | `armv6k-nintendo-3ds` | [`getrandom`][1]
+//! | PS Vita | `armv7-sony-vita-newlibeabihf` | [`getentropy`][13]
+//! | QNX Neutrino | `*‑nto-qnx*` | [`/dev/urandom`][14] (identical to `/dev/random`)
+//! | AIX | `*-ibm-aix` | [`/dev/urandom`][15]
//!
//! There is no blanket implementation on `unix` targets that reads from
//! `/dev/urandom`. This ensures all supported targets are using the recommended
@@ -72,11 +74,37 @@
//! that you are building for an environment containing JavaScript, and will
//! call the appropriate methods. Both web browser (main window and Web Workers)
//! and Node.js environments are supported, invoking the methods
-//! [described above](#supported-targets) using the
-//! [wasm-bindgen](https://github.com/rust-lang/rust-bindgen) toolchain.
+//! [described above](#supported-targets) using the [`wasm-bindgen`] toolchain.
+//!
+//! To enable the `js` Cargo feature, add the following to the `dependencies`
+//! section in your `Cargo.toml` file:
+//! ```toml
+//! [dependencies]
+//! getrandom = { version = "0.2", features = ["js"] }
+//! ```
+//!
+//! This can be done even if `getrandom` is not a direct dependency. Cargo
+//! allows crates to enable features for indirect dependencies.
+//!
+//! This feature should only be enabled for binary, test, or benchmark crates.
+//! Library crates should generally not enable this feature, leaving such a
+//! decision to *users* of their library. Also, libraries should not introduce
+//! their own `js` features *just* to enable `getrandom`'s `js` feature.
//!
//! This feature has no effect on targets other than `wasm32-unknown-unknown`.
//!
+//! #### Node.js ES module support
+//!
+//! Node.js supports both [CommonJS modules] and [ES modules]. Due to
+//! limitations in wasm-bindgen's [`module`] support, we cannot directly
+//! support ES Modules running on Node.js. However, on Node v15 and later, the
+//! module author can add a simple shim to support the Web Cryptography API:
+//! ```js
+//! import { webcrypto } from 'node:crypto'
+//! globalThis.crypto = webcrypto
+//! ```
+//! This crate will then use the provided `webcrypto` implementation.
+//!
//! ### Custom implementations
//!
//! The [`register_custom_getrandom!`] macro allows a user to mark their own
@@ -89,16 +117,6 @@
//! using `rdrand` and `js` Cargo features) continue using their normal
//! implementations even if a function is registered.
//!
-//! ### Indirect Dependencies
-//!
-//! If `getrandom` is not a direct dependency of your crate, you can still
-//! enable any of the above fallback behaviors by enabling the relevant
-//! feature in your root crate's `Cargo.toml`:
-//! ```toml
-//! [dependencies]
-//! getrandom = { version = "0.2", features = ["js"] }
-//! ```
-//!
//! ## Early boot
//!
//! Sometimes, early in the boot process, the OS has not collected enough
@@ -115,18 +133,27 @@
//! entropy yet. To avoid returning low-entropy bytes, we first poll
//! `/dev/random` and only switch to `/dev/urandom` once this has succeeded.
//!
+//! On OpenBSD, this kind of entropy accounting isn't available, and on
+//! NetBSD, blocking on it is discouraged. On these platforms, nonblocking
+//! interfaces are used, even when reliable entropy may not be available.
+//! On the platforms where it is used, the reliability of entropy accounting
+//! itself isn't free from controversy. This library provides randomness
+//! sourced according to the platform's best practices, but each platform has
+//! its own limits on the grade of randomness it can promise in environments
+//! with few sources of entropy.
+//!
//! ## Error handling
//!
-//! We always choose failure over returning insecure "random" bytes. In general,
-//! on supported platforms, failure is highly unlikely, though not impossible.
-//! If an error does occur, then it is likely that it will occur on every call to
-//! `getrandom`, hence after the first successful call one can be reasonably
-//! confident that no errors will occur.
+//! We always choose failure over returning known insecure "random" bytes. In
+//! general, on supported platforms, failure is highly unlikely, though not
+//! impossible. If an error does occur, then it is likely that it will occur
+//! on every call to `getrandom`, hence after the first successful call one
+//! can be reasonably confident that no errors will occur.
//!
//! [1]: http://man7.org/linux/man-pages/man2/getrandom.2.html
//! [2]: http://man7.org/linux/man-pages/man4/urandom.4.html
//! [3]: https://www.unix.com/man-page/mojave/2/getentropy/
-//! [4]: https://www.unix.com/man-page/mojave/4/random/
+//! [4]: https://www.unix.com/man-page/mojave/4/urandom/
//! [5]: https://www.freebsd.org/cgi/man.cgi?query=getrandom&manpath=FreeBSD+12.0-stable
//! [6]: https://www.freebsd.org/cgi/man.cgi?query=random&sektion=4
//! [7]: https://man.openbsd.org/getentropy.2
@@ -135,21 +162,30 @@
//! [10]: https://leaf.dragonflybsd.org/cgi/web-man?command=random§ion=4
//! [11]: https://docs.oracle.com/cd/E88353_01/html/E37841/getrandom-2.html
//! [12]: https://docs.oracle.com/cd/E86824_01/html/E54777/random-7d.html
+//! [13]: https://github.com/emscripten-core/emscripten/pull/12240
+//! [14]: https://www.qnx.com/developers/docs/7.1/index.html#com.qnx.doc.neutrino.utilities/topic/r/random.html
+//! [15]: https://www.ibm.com/docs/en/aix/7.3?topic=files-random-urandom-devices
+//! [16]: https://man.netbsd.org/getrandom.2
//!
//! [`BCryptGenRandom`]: https://docs.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptgenrandom
//! [`Crypto.getRandomValues`]: https://www.w3.org/TR/WebCryptoAPI/#Crypto-method-getRandomValues
//! [`RDRAND`]: https://software.intel.com/en-us/articles/intel-digital-random-number-generator-drng-software-implementation-guide
//! [`SecRandomCopyBytes`]: https://developer.apple.com/documentation/security/1399291-secrandomcopybytes?language=objc
//! [`cprng_draw`]: https://fuchsia.dev/fuchsia-src/zircon/syscalls/cprng_draw
-//! [`crypto.randomBytes`]: https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback
+//! [`crypto.randomFillSync`]: https://nodejs.org/api/crypto.html#cryptorandomfillsyncbuffer-offset-size
//! [`esp_fill_random`]: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/random.html#_CPPv415esp_fill_randomPv6size_t
//! [`random_get`]: https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#-random_getbuf-pointeru8-buf_len-size---errno
//! [WebAssembly support]: #webassembly-support
+//! [`wasm-bindgen`]: https://github.com/rustwasm/wasm-bindgen
+//! [`module`]: https://rustwasm.github.io/wasm-bindgen/reference/attributes/on-js-imports/module.html
+//! [CommonJS modules]: https://nodejs.org/api/modules.html
+//! [ES modules]: https://nodejs.org/api/esm.html
+//! [`sys_read_entropy`]: https://hermitcore.github.io/libhermit-rs/hermit/fn.sys_read_entropy.html
#![doc(
html_logo_url = "https://www.rust-lang.org/logos/rust-logo-128x128-blk.png",
html_favicon_url = "https://www.rust-lang.org/favicon.ico",
- html_root_url = "https://docs.rs/getrandom/0.2.6"
+ html_root_url = "https://docs.rs/getrandom/0.2.10"
)]
#![no_std]
#![warn(rust_2018_idioms, unused_lifetimes, missing_docs)]
@@ -158,6 +194,9 @@
#[macro_use]
extern crate cfg_if;
+use crate::util::{slice_as_uninit_mut, slice_assume_init_mut};
+use core::mem::MaybeUninit;
+
mod error;
mod util;
// To prevent a breaking change when targets are added, we always export the
@@ -175,11 +214,14 @@
// System-specific implementations.
//
-// These should all provide getrandom_inner with the same signature as getrandom.
+// These should all provide getrandom_inner with the signature
+// `fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error>`.
+// The function MUST fully initialize `dest` when `Ok(())` is returned.
+// The function MUST NOT ever write uninitialized bytes into `dest`,
+// regardless of what value it returns.
#[cfg(not(target_vendor = "teaclave"))]
cfg_if! {
- if #[cfg(any(target_os = "emscripten", target_os = "haiku",
- target_os = "redox"))] {
+ if #[cfg(any(target_os = "haiku", target_os = "redox", target_os = "nto", target_os = "aix"))] {
mod util_libc;
#[path = "use_file.rs"] mod imp;
} else if #[cfg(any(target_os = "android", target_os = "linux"))] {
@@ -199,8 +241,8 @@
#[path = "dragonfly.rs"] mod imp;
} else if #[cfg(target_os = "fuchsia")] {
#[path = "fuchsia.rs"] mod imp;
- } else if #[cfg(target_os = "ios")] {
- #[path = "ios.rs"] mod imp;
+ } else if #[cfg(any(target_os = "ios", target_os = "watchos", target_os = "tvos"))] {
+ #[path = "apple-other.rs"] mod imp;
} else if #[cfg(target_os = "macos")] {
mod util_libc;
mod use_file;
@@ -208,10 +250,10 @@
} else if #[cfg(target_os = "openbsd")] {
mod util_libc;
#[path = "openbsd.rs"] mod imp;
- } else if #[cfg(target_os = "wasi")] {
+ } else if #[cfg(all(target_arch = "wasm32", target_os = "wasi"))] {
#[path = "wasi.rs"] mod imp;
- } else if #[cfg(all(target_arch = "x86_64", target_os = "hermit"))] {
- #[path = "rdrand.rs"] mod imp;
+ } else if #[cfg(target_os = "hermit")] {
+ #[path = "hermit.rs"] mod imp;
} else if #[cfg(target_os = "vxworks")] {
mod util_libc;
#[path = "vxworks.rs"] mod imp;
@@ -221,23 +263,31 @@
#[path = "espidf.rs"] mod imp;
} else if #[cfg(windows)] {
#[path = "windows.rs"] mod imp;
+ } else if #[cfg(all(target_os = "horizon", target_arch = "arm"))] {
+ // We check for target_arch = "arm" because the Nintendo Switch also
+ // uses Horizon OS (it is aarch64).
+ mod util_libc;
+ #[path = "3ds.rs"] mod imp;
+ } else if #[cfg(target_os = "vita")] {
+ mod util_libc;
+ #[path = "vita.rs"] mod imp;
+ } else if #[cfg(target_os = "emscripten")] {
+ mod util_libc;
+ #[path = "emscripten.rs"] mod imp;
} else if #[cfg(all(target_arch = "x86_64", target_env = "sgx"))] {
#[path = "rdrand.rs"] mod imp;
} else if #[cfg(all(feature = "rdrand",
any(target_arch = "x86_64", target_arch = "x86")))] {
#[path = "rdrand.rs"] mod imp;
} else if #[cfg(all(feature = "js",
- target_arch = "wasm32", target_os = "unknown"))] {
+ any(target_arch = "wasm32", target_arch = "wasm64"),
+ target_os = "unknown"))] {
#[path = "js.rs"] mod imp;
- } else if #[cfg(all(target_os = "horizon", target_arch = "arm"))] {
- // We check for target_arch = "arm" because the Nintendo Switch also
- // uses Horizon OS (it is aarch64).
- mod util_libc;
- #[path = "3ds.rs"] mod imp;
} else if #[cfg(feature = "custom")] {
use custom as imp;
- } else if #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] {
- compile_error!("the wasm32-unknown-unknown target is not supported by \
+ } else if #[cfg(all(any(target_arch = "wasm32", target_arch = "wasm64"),
+ target_os = "unknown"))] {
+ compile_error!("the wasm*-unknown-unknown targets are not supported by \
default, you may need to enable the \"js\" feature. \
For more information see: \
https://docs.rs/getrandom/#webassembly-support");
@@ -260,9 +310,42 @@
/// In general, `getrandom` will be fast enough for interactive usage, though
/// significantly slower than a user-space CSPRNG; for the latter consider
/// [`rand::thread_rng`](https://docs.rs/rand/*/rand/fn.thread_rng.html).
+#[inline]
pub fn getrandom(dest: &mut [u8]) -> Result<(), Error> {
- if dest.is_empty() {
- return Ok(());
+ // SAFETY: The `&mut MaybeUninit<_>` reference doesn't escape, and
+ // `getrandom_uninit` guarantees it will never de-initialize any part of
+ // `dest`.
+ getrandom_uninit(unsafe { slice_as_uninit_mut(dest) })?;
+ Ok(())
+}
+
+/// Version of the `getrandom` function which fills `dest` with random bytes
+/// returns a mutable reference to those bytes.
+///
+/// On successful completion this function is guaranteed to return a slice
+/// which points to the same memory as `dest` and has the same length.
+/// In other words, it's safe to assume that `dest` is initialized after
+/// this function has returned `Ok`.
+///
+/// No part of `dest` will ever be de-initialized at any point, regardless
+/// of what is returned.
+///
+/// # Examples
+///
+/// ```ignore
+/// # // We ignore this test since `uninit_array` is unstable.
+/// #![feature(maybe_uninit_uninit_array)]
+/// # fn main() -> Result<(), getrandom::Error> {
+/// let mut buf = core::mem::MaybeUninit::uninit_array::<1024>();
+/// let buf: &mut [u8] = getrandom::getrandom_uninit(&mut buf)?;
+/// # Ok(()) }
+/// ```
+#[inline]
+pub fn getrandom_uninit(dest: &mut [MaybeUninit<u8>]) -> Result<&mut [u8], Error> {
+ if !dest.is_empty() {
+ imp::getrandom_inner(dest)?;
}
- imp::getrandom_inner(dest)
+ // SAFETY: `dest` has been fully initialized by `imp::getrandom_inner`
+ // since it returned `Ok`.
+ Ok(unsafe { slice_assume_init_mut(dest) })
}
diff --git a/getrandom/src/linux_android.rs b/getrandom/src/linux_android.rs
index 5508fdd..e81f1e1 100644
--- a/getrandom/src/linux_android.rs
+++ b/getrandom/src/linux_android.rs
@@ -12,8 +12,10 @@
util_libc::{last_os_error, sys_fill_exact},
{use_file, Error},
};
+use core::mem::MaybeUninit;
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ // getrandom(2) was introduced in Linux 3.17
static HAS_GETRANDOM: LazyBool = LazyBool::new();
if HAS_GETRANDOM.unsync_init(is_getrandom_available) {
sys_fill_exact(dest, |buf| unsafe {
diff --git a/getrandom/src/macos.rs b/getrandom/src/macos.rs
index 585a35a..312f9b2 100644
--- a/getrandom/src/macos.rs
+++ b/getrandom/src/macos.rs
@@ -12,16 +12,17 @@
util_libc::{last_os_error, Weak},
Error,
};
-use core::mem;
+use core::mem::{self, MaybeUninit};
type GetEntropyFn = unsafe extern "C" fn(*mut u8, libc::size_t) -> libc::c_int;
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ // getentropy(2) was added in 10.12, Rust supports 10.7+
static GETENTROPY: Weak = unsafe { Weak::new("getentropy\0") };
if let Some(fptr) = GETENTROPY.ptr() {
let func: GetEntropyFn = unsafe { mem::transmute(fptr) };
for chunk in dest.chunks_mut(256) {
- let ret = unsafe { func(chunk.as_mut_ptr(), chunk.len()) };
+ let ret = unsafe { func(chunk.as_mut_ptr() as *mut u8, chunk.len()) };
if ret != 0 {
return Err(last_os_error());
}
diff --git a/getrandom/src/openbsd.rs b/getrandom/src/openbsd.rs
index c8d28b3..7a76f61 100644
--- a/getrandom/src/openbsd.rs
+++ b/getrandom/src/openbsd.rs
@@ -8,8 +8,10 @@
//! Implementation for OpenBSD
use crate::{util_libc::last_os_error, Error};
+use core::mem::MaybeUninit;
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ // getentropy(2) was added in OpenBSD 5.6, so we can use it unconditionally.
for chunk in dest.chunks_mut(256) {
let ret = unsafe { libc::getentropy(chunk.as_mut_ptr() as *mut libc::c_void, chunk.len()) };
if ret == -1 {
diff --git a/getrandom/src/rdrand.rs b/getrandom/src/rdrand.rs
index 1df21e5..69f6a5d 100644
--- a/getrandom/src/rdrand.rs
+++ b/getrandom/src/rdrand.rs
@@ -5,10 +5,11 @@
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
-
-//! Implementation for SGX using RDRAND instruction
-use crate::Error;
-use core::mem;
+use crate::{
+ util::{slice_as_uninit, LazyBool},
+ Error,
+};
+use core::mem::{size_of, MaybeUninit};
cfg_if! {
if #[cfg(target_arch = "x86_64")] {
@@ -24,74 +25,106 @@
// Implementation Guide" - Section 5.2.1 and "Intel® 64 and IA-32 Architectures
// Software Developer’s Manual" - Volume 1 - Section 7.3.17.1.
const RETRY_LIMIT: usize = 10;
-const WORD_SIZE: usize = mem::size_of::<usize>();
#[target_feature(enable = "rdrand")]
-unsafe fn rdrand() -> Result<[u8; WORD_SIZE], Error> {
+unsafe fn rdrand() -> Option<usize> {
for _ in 0..RETRY_LIMIT {
- let mut el = mem::zeroed();
- if rdrand_step(&mut el) == 1 {
- // AMD CPUs from families 14h to 16h (pre Ryzen) sometimes fail to
- // set CF on bogus random data, so we check these values explicitly.
- // See https://github.com/systemd/systemd/issues/11810#issuecomment-489727505
- // We perform this check regardless of target to guard against
- // any implementation that incorrectly fails to set CF.
- if el != 0 && el != !0 {
- return Ok(el.to_ne_bytes());
- }
- // Keep looping in case this was a false positive.
+ let mut val = 0;
+ if rdrand_step(&mut val) == 1 {
+ return Some(val as usize);
}
}
- Err(Error::FAILED_RDRAND)
+ None
}
-// "rdrand" target feature requires "+rdrnd" flag, see https://github.com/rust-lang/rust/issues/49653.
+// "rdrand" target feature requires "+rdrand" flag, see https://github.com/rust-lang/rust/issues/49653.
#[cfg(all(target_env = "sgx", not(target_feature = "rdrand")))]
compile_error!(
- "SGX targets require 'rdrand' target feature. Enable by using -C target-feature=+rdrnd."
+ "SGX targets require 'rdrand' target feature. Enable by using -C target-feature=+rdrand."
);
-#[cfg(target_feature = "rdrand")]
-fn is_rdrand_supported() -> bool {
- true
+// Run a small self-test to make sure we aren't repeating values
+// Adapted from Linux's test in arch/x86/kernel/cpu/rdrand.c
+// Fails with probability < 2^(-90) on 32-bit systems
+#[target_feature(enable = "rdrand")]
+unsafe fn self_test() -> bool {
+ // On AMD, RDRAND returns 0xFF...FF on failure, count it as a collision.
+ let mut prev = !0; // TODO(MSRV 1.43): Move to usize::MAX
+ let mut fails = 0;
+ for _ in 0..8 {
+ match rdrand() {
+ Some(val) if val == prev => fails += 1,
+ Some(val) => prev = val,
+ None => return false,
+ };
+ }
+ fails <= 2
}
-// TODO use is_x86_feature_detected!("rdrand") when that works in core. See:
-// https://github.com/rust-lang-nursery/stdsimd/issues/464
-#[cfg(not(target_feature = "rdrand"))]
-fn is_rdrand_supported() -> bool {
- use crate::util::LazyBool;
+fn is_rdrand_good() -> bool {
+ #[cfg(not(target_feature = "rdrand"))]
+ {
+ // SAFETY: All Rust x86 targets are new enough to have CPUID, and we
+ // check that leaf 1 is supported before using it.
+ let cpuid0 = unsafe { arch::__cpuid(0) };
+ if cpuid0.eax < 1 {
+ return false;
+ }
+ let cpuid1 = unsafe { arch::__cpuid(1) };
- // SAFETY: All Rust x86 targets are new enough to have CPUID, and if CPUID
- // is supported, CPUID leaf 1 is always supported.
- const FLAG: u32 = 1 << 30;
- static HAS_RDRAND: LazyBool = LazyBool::new();
- HAS_RDRAND.unsync_init(|| unsafe { (arch::__cpuid(1).ecx & FLAG) != 0 })
-}
+ let vendor_id = [
+ cpuid0.ebx.to_le_bytes(),
+ cpuid0.edx.to_le_bytes(),
+ cpuid0.ecx.to_le_bytes(),
+ ];
+ if vendor_id == [*b"Auth", *b"enti", *b"cAMD"] {
+ let mut family = (cpuid1.eax >> 8) & 0xF;
+ if family == 0xF {
+ family += (cpuid1.eax >> 20) & 0xFF;
+ }
+ // AMD CPUs families before 17h (Zen) sometimes fail to set CF when
+ // RDRAND fails after suspend. Don't use RDRAND on those families.
+ // See https://bugzilla.redhat.com/show_bug.cgi?id=1150286
+ if family < 0x17 {
+ return false;
+ }
+ }
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
- if !is_rdrand_supported() {
- return Err(Error::NO_RDRAND);
+ const RDRAND_FLAG: u32 = 1 << 30;
+ if cpuid1.ecx & RDRAND_FLAG == 0 {
+ return false;
+ }
}
- // SAFETY: After this point, rdrand is supported, so calling the rdrand
- // functions is not undefined behavior.
- unsafe { rdrand_exact(dest) }
+ // SAFETY: We have already checked that rdrand is available.
+ unsafe { self_test() }
}
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ static RDRAND_GOOD: LazyBool = LazyBool::new();
+ if !RDRAND_GOOD.unsync_init(is_rdrand_good) {
+ return Err(Error::NO_RDRAND);
+ }
+ // SAFETY: After this point, we know rdrand is supported.
+ unsafe { rdrand_exact(dest) }.ok_or(Error::FAILED_RDRAND)
+}
+
+// TODO: make this function safe when we have feature(target_feature_11)
#[target_feature(enable = "rdrand")]
-unsafe fn rdrand_exact(dest: &mut [u8]) -> Result<(), Error> {
+unsafe fn rdrand_exact(dest: &mut [MaybeUninit<u8>]) -> Option<()> {
// We use chunks_exact_mut instead of chunks_mut as it allows almost all
// calls to memcpy to be elided by the compiler.
- let mut chunks = dest.chunks_exact_mut(WORD_SIZE);
+ let mut chunks = dest.chunks_exact_mut(size_of::<usize>());
for chunk in chunks.by_ref() {
- chunk.copy_from_slice(&rdrand()?);
+ let src = rdrand()?.to_ne_bytes();
+ chunk.copy_from_slice(slice_as_uninit(&src));
}
let tail = chunks.into_remainder();
let n = tail.len();
if n > 0 {
- tail.copy_from_slice(&rdrand()?[..n]);
+ let src = rdrand()?.to_ne_bytes();
+ tail.copy_from_slice(slice_as_uninit(&src[..n]));
}
- Ok(())
+ Some(())
}
diff --git a/getrandom/src/solaris_illumos.rs b/getrandom/src/solaris_illumos.rs
index 2d1b767..501c610 100644
--- a/getrandom/src/solaris_illumos.rs
+++ b/getrandom/src/solaris_illumos.rs
@@ -8,12 +8,11 @@
//! Implementation for the Solaris family
//!
-//! Read from `/dev/random`, with chunks of limited size (256 bytes).
//! `/dev/random` uses the Hash_DRBG with SHA512 algorithm from NIST SP 800-90A.
//! `/dev/urandom` uses the FIPS 186-2 algorithm, which is considered less
-//! secure. We choose to read from `/dev/random`.
+//! secure. We choose to read from `/dev/random` (and use GRND_RANDOM).
//!
-//! Since Solaris 11.3 and mid-2015 illumos, the `getrandom` syscall is available.
+//! Solaris 11.3 and late-2018 illumos added the getrandom(2) libc function.
//! To make sure we can compile on both Solaris and its derivatives, as well as
//! function, we check for the existence of getrandom(2) in libc by calling
//! libc::dlsym.
@@ -22,22 +21,25 @@
util_libc::{sys_fill_exact, Weak},
Error,
};
-use core::mem;
+use core::mem::{self, MaybeUninit};
-#[cfg(target_os = "illumos")]
-type GetRandomFn = unsafe extern "C" fn(*mut u8, libc::size_t, libc::c_uint) -> libc::ssize_t;
-#[cfg(target_os = "solaris")]
-type GetRandomFn = unsafe extern "C" fn(*mut u8, libc::size_t, libc::c_uint) -> libc::c_int;
+static GETRANDOM: Weak = unsafe { Weak::new("getrandom\0") };
+type GetRandomFn =
+ unsafe extern "C" fn(*mut libc::c_void, libc::size_t, libc::c_uint) -> libc::ssize_t;
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
- static GETRANDOM: Weak = unsafe { Weak::new("getrandom\0") };
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
if let Some(fptr) = GETRANDOM.ptr() {
let func: GetRandomFn = unsafe { mem::transmute(fptr) };
// 256 bytes is the lowest common denominator across all the Solaris
// derived platforms for atomically obtaining random data.
for chunk in dest.chunks_mut(256) {
sys_fill_exact(chunk, |buf| unsafe {
- func(buf.as_mut_ptr(), buf.len(), 0) as libc::ssize_t
+ // A cast is needed for the flags as libc uses the wrong type.
+ func(
+ buf.as_mut_ptr() as *mut libc::c_void,
+ buf.len(),
+ libc::GRND_RANDOM as libc::c_uint,
+ )
})?
}
Ok(())
diff --git a/getrandom/src/solid.rs b/getrandom/src/solid.rs
index dc76aac..aeccc4e 100644
--- a/getrandom/src/solid.rs
+++ b/getrandom/src/solid.rs
@@ -8,14 +8,14 @@
//! Implementation for SOLID
use crate::Error;
-use core::num::NonZeroU32;
+use core::{mem::MaybeUninit, num::NonZeroU32};
extern "C" {
pub fn SOLID_RNG_SampleRandomBytes(buffer: *mut u8, length: usize) -> i32;
}
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
- let ret = unsafe { SOLID_RNG_SampleRandomBytes(dest.as_mut_ptr(), dest.len()) };
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ let ret = unsafe { SOLID_RNG_SampleRandomBytes(dest.as_mut_ptr() as *mut u8, dest.len()) };
if ret >= 0 {
Ok(())
} else {
diff --git a/getrandom/src/teaclave.rs b/getrandom/src/teaclave.rs
index 245f5c4..ac867d1 100644
--- a/getrandom/src/teaclave.rs
+++ b/getrandom/src/teaclave.rs
@@ -9,13 +9,29 @@
//! Implementation for SGX using RDRAND instruction
extern crate sgx_trts;
-use crate::Error;
+use sgx_trts::rand::rand;
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
- // sgx_read_rand cannot take len=0, but this function does
- if dest.is_empty() {
- return Ok(());
+use core::mem::MaybeUninit;
+
+use crate::{util::slice_as_uninit, Error};
+
+// Vec can not be used under no-std, so we generate the random numbers by chunks
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ const WINDOW_SIZE: usize = 8;
+ let mut buf = [0; WINDOW_SIZE];
+
+ let mut chunks = dest.chunks_exact_mut(WINDOW_SIZE);
+ for chunk in chunks.by_ref() {
+ rand(&mut buf).or(Err(Error::UNSUPPORTED))?;
+ chunk.copy_from_slice(slice_as_uninit(&buf));
}
- sgx_trts::rand::rand(dest).or(Err(Error::UNSUPPORTED))
+ let tail = chunks.into_remainder();
+ let n = tail.len();
+ if n > 0 {
+ rand(&mut buf).or(Err(Error::UNSUPPORTED))?;
+ tail.copy_from_slice(slice_as_uninit(&buf[..n]));
+ }
+
+ Ok(())
}
diff --git a/getrandom/src/use_file.rs b/getrandom/src/use_file.rs
index 16c0216..a6ef0d2 100644
--- a/getrandom/src/use_file.rs
+++ b/getrandom/src/use_file.rs
@@ -14,34 +14,33 @@
};
use core::{
cell::UnsafeCell,
+ mem::MaybeUninit,
sync::atomic::{AtomicUsize, Ordering::Relaxed},
};
+// We prefer using /dev/urandom and only use /dev/random if the OS
+// documentation indicates that /dev/urandom is insecure.
+// On Solaris/Illumos, see src/solaris_illumos.rs
+// On Dragonfly, Haiku, macOS, and QNX Neutrino the devices are identical.
+#[cfg(any(target_os = "solaris", target_os = "illumos"))]
+const FILE_PATH: &str = "/dev/random\0";
#[cfg(any(
+ target_os = "aix",
+ target_os = "android",
+ target_os = "linux",
+ target_os = "redox",
target_os = "dragonfly",
- target_os = "emscripten",
target_os = "haiku",
target_os = "macos",
- target_os = "solaris",
- target_os = "illumos"
+ target_os = "nto",
))]
-const FILE_PATH: &str = "/dev/random\0";
-#[cfg(any(target_os = "android", target_os = "linux", target_os = "redox"))]
const FILE_PATH: &str = "/dev/urandom\0";
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
let fd = get_rng_fd()?;
- let read = |buf: &mut [u8]| unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
-
- if cfg!(target_os = "emscripten") {
- // `Crypto.getRandomValues` documents `dest` should be at most 65536 bytes.
- for chunk in dest.chunks_mut(65536) {
- sys_fill_exact(chunk, read)?;
- }
- } else {
- sys_fill_exact(dest, read)?;
- }
- Ok(())
+ sys_fill_exact(dest, |buf| unsafe {
+ libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
+ })
}
// Returns the file descriptor for the device file used to retrieve random
diff --git a/getrandom/src/util.rs b/getrandom/src/util.rs
index 06e23c2..3162afa 100644
--- a/getrandom/src/util.rs
+++ b/getrandom/src/util.rs
@@ -6,7 +6,11 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
#![allow(dead_code)]
-use core::sync::atomic::{AtomicUsize, Ordering::Relaxed};
+use core::{
+ mem::MaybeUninit,
+ ptr,
+ sync::atomic::{AtomicUsize, Ordering::Relaxed},
+};
// This structure represents a lazily initialized static usize value. Useful
// when it is preferable to just rerun initialization instead of locking.
@@ -62,3 +66,36 @@
self.0.unsync_init(|| init() as usize) != 0
}
}
+
+/// Polyfill for `maybe_uninit_slice` feature's
+/// `MaybeUninit::slice_assume_init_mut`. Every element of `slice` must have
+/// been initialized.
+#[inline(always)]
+pub unsafe fn slice_assume_init_mut<T>(slice: &mut [MaybeUninit<T>]) -> &mut [T] {
+ // SAFETY: `MaybeUninit<T>` is guaranteed to be layout-compatible with `T`.
+ &mut *(slice as *mut [MaybeUninit<T>] as *mut [T])
+}
+
+#[inline]
+pub fn uninit_slice_fill_zero(slice: &mut [MaybeUninit<u8>]) -> &mut [u8] {
+ unsafe { ptr::write_bytes(slice.as_mut_ptr(), 0, slice.len()) };
+ unsafe { slice_assume_init_mut(slice) }
+}
+
+#[inline(always)]
+pub fn slice_as_uninit<T>(slice: &[T]) -> &[MaybeUninit<T>] {
+ // SAFETY: `MaybeUninit<T>` is guaranteed to be layout-compatible with `T`.
+ // There is no risk of writing a `MaybeUninit<T>` into the result since
+ // the result isn't mutable.
+ unsafe { &*(slice as *const [T] as *const [MaybeUninit<T>]) }
+}
+
+/// View an mutable initialized array as potentially-uninitialized.
+///
+/// This is unsafe because it allows assigning uninitialized values into
+/// `slice`, which would be undefined behavior.
+#[inline(always)]
+pub unsafe fn slice_as_uninit_mut<T>(slice: &mut [T]) -> &mut [MaybeUninit<T>] {
+ // SAFETY: `MaybeUninit<T>` is guaranteed to be layout-compatible with `T`.
+ &mut *(slice as *mut [T] as *mut [MaybeUninit<T>])
+}
diff --git a/getrandom/src/util_libc.rs b/getrandom/src/util_libc.rs
index 6df1cd7..4b94144 100644
--- a/getrandom/src/util_libc.rs
+++ b/getrandom/src/util_libc.rs
@@ -6,8 +6,15 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
#![allow(dead_code)]
-use crate::{util::LazyUsize, Error};
-use core::{num::NonZeroU32, ptr::NonNull};
+use crate::Error;
+use core::{
+ cmp::min,
+ mem::MaybeUninit,
+ num::NonZeroU32,
+ ptr::NonNull,
+ sync::atomic::{fence, AtomicPtr, Ordering},
+};
+use libc::c_void;
cfg_if! {
if #[cfg(any(target_os = "netbsd", target_os = "openbsd", target_os = "android"))] {
@@ -20,12 +27,16 @@
use libc::__error as errno_location;
} else if #[cfg(target_os = "haiku")] {
use libc::_errnop as errno_location;
- } else if #[cfg(all(target_os = "horizon", target_arch = "arm"))] {
+ } else if #[cfg(target_os = "nto")] {
+ use libc::__get_errno_ptr as errno_location;
+ } else if #[cfg(any(all(target_os = "horizon", target_arch = "arm"), target_os = "vita"))] {
extern "C" {
// Not provided by libc: https://github.com/rust-lang/libc/issues/1995
fn __errno() -> *mut libc::c_int;
}
use __errno as errno_location;
+ } else if #[cfg(target_os = "aix")] {
+ use libc::_Errno as errno_location;
}
}
@@ -54,8 +65,8 @@
// - should return -1 and set errno on failure
// - should return the number of bytes written on success
pub fn sys_fill_exact(
- mut buf: &mut [u8],
- sys_fill: impl Fn(&mut [u8]) -> libc::ssize_t,
+ mut buf: &mut [MaybeUninit<u8>],
+ sys_fill: impl Fn(&mut [MaybeUninit<u8>]) -> libc::ssize_t,
) -> Result<(), Error> {
while !buf.is_empty() {
let res = sys_fill(buf);
@@ -68,7 +79,8 @@
} else {
// We don't check for EOF (ret = 0) as the data we are reading
// should be an infinite stream of random bytes.
- buf = &mut buf[(res as usize)..];
+ let len = min(res as usize, buf.len());
+ buf = &mut buf[len..];
}
}
Ok(())
@@ -76,37 +88,57 @@
// A "weak" binding to a C function that may or may not be present at runtime.
// Used for supporting newer OS features while still building on older systems.
-// F must be a function pointer of type `unsafe extern "C" fn`. Based off of the
-// weak! macro in libstd.
+// Based off of the DlsymWeak struct in libstd:
+// https://github.com/rust-lang/rust/blob/1.61.0/library/std/src/sys/unix/weak.rs#L84
+// except that the caller must manually cast self.ptr() to a function pointer.
pub struct Weak {
name: &'static str,
- addr: LazyUsize,
+ addr: AtomicPtr<c_void>,
}
impl Weak {
+ // A non-null pointer value which indicates we are uninitialized. This
+ // constant should ideally not be a valid address of a function pointer.
+ // However, if by chance libc::dlsym does return UNINIT, there will not
+ // be undefined behavior. libc::dlsym will just be called each time ptr()
+ // is called. This would be inefficient, but correct.
+ // TODO: Replace with core::ptr::invalid_mut(1) when that is stable.
+ const UNINIT: *mut c_void = 1 as *mut c_void;
+
// Construct a binding to a C function with a given name. This function is
// unsafe because `name` _must_ be null terminated.
pub const unsafe fn new(name: &'static str) -> Self {
Self {
name,
- addr: LazyUsize::new(),
+ addr: AtomicPtr::new(Self::UNINIT),
}
}
- // Return a function pointer if present at runtime. Otherwise, return null.
- pub fn ptr(&self) -> Option<NonNull<libc::c_void>> {
- let addr = self.addr.unsync_init(|| unsafe {
- libc::dlsym(libc::RTLD_DEFAULT, self.name.as_ptr() as *const _) as usize
- });
- NonNull::new(addr as *mut _)
- }
-}
-
-cfg_if! {
- if #[cfg(any(target_os = "linux", target_os = "emscripten"))] {
- use libc::open64 as open;
- } else {
- use libc::open;
+ // Return the address of a function if present at runtime. Otherwise,
+ // return None. Multiple callers can call ptr() concurrently. It will
+ // always return _some_ value returned by libc::dlsym. However, the
+ // dlsym function may be called multiple times.
+ pub fn ptr(&self) -> Option<NonNull<c_void>> {
+ // Despite having only a single atomic variable (self.addr), we still
+ // cannot always use Ordering::Relaxed, as we need to make sure a
+ // successful call to dlsym() is "ordered before" any data read through
+ // the returned pointer (which occurs when the function is called).
+ // Our implementation mirrors that of the one in libstd, meaning that
+ // the use of non-Relaxed operations is probably unnecessary.
+ match self.addr.load(Ordering::Relaxed) {
+ Self::UNINIT => {
+ let symbol = self.name.as_ptr() as *const _;
+ let addr = unsafe { libc::dlsym(libc::RTLD_DEFAULT, symbol) };
+ // Synchronizes with the Acquire fence below
+ self.addr.store(addr, Ordering::Release);
+ NonNull::new(addr)
+ }
+ addr => {
+ let func = NonNull::new(addr)?;
+ fence(Ordering::Acquire);
+ Some(func)
+ }
+ }
}
}
@@ -114,7 +146,7 @@
pub unsafe fn open_readonly(path: &str) -> Result<libc::c_int, Error> {
debug_assert_eq!(path.as_bytes().last(), Some(&0));
loop {
- let fd = open(path.as_ptr() as *const _, libc::O_RDONLY | libc::O_CLOEXEC);
+ let fd = libc::open(path.as_ptr() as *const _, libc::O_RDONLY | libc::O_CLOEXEC);
if fd >= 0 {
return Ok(fd);
}
diff --git a/getrandom/src/vita.rs b/getrandom/src/vita.rs
new file mode 100644
index 0000000..4f19b9c
--- /dev/null
+++ b/getrandom/src/vita.rs
@@ -0,0 +1,21 @@
+// Copyright 2021 Developers of the Rand project.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Implementation for PS Vita
+use crate::{util_libc::last_os_error, Error};
+use core::mem::MaybeUninit;
+
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ for chunk in dest.chunks_mut(256) {
+ let ret = unsafe { libc::getentropy(chunk.as_mut_ptr() as *mut libc::c_void, chunk.len()) };
+ if ret == -1 {
+ return Err(last_os_error());
+ }
+ }
+ Ok(())
+}
diff --git a/getrandom/src/vxworks.rs b/getrandom/src/vxworks.rs
index 6cb5d52..9b2090f 100644
--- a/getrandom/src/vxworks.rs
+++ b/getrandom/src/vxworks.rs
@@ -8,9 +8,12 @@
//! Implementation for VxWorks
use crate::{util_libc::last_os_error, Error};
-use core::sync::atomic::{AtomicBool, Ordering::Relaxed};
+use core::{
+ mem::MaybeUninit,
+ sync::atomic::{AtomicBool, Ordering::Relaxed},
+};
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
static RNG_INIT: AtomicBool = AtomicBool::new(false);
while !RNG_INIT.load(Relaxed) {
let ret = unsafe { libc::randSecure() };
@@ -25,7 +28,7 @@
// Prevent overflow of i32
for chunk in dest.chunks_mut(i32::max_value() as usize) {
- let ret = unsafe { libc::randABytes(chunk.as_mut_ptr(), chunk.len() as i32) };
+ let ret = unsafe { libc::randABytes(chunk.as_mut_ptr() as *mut u8, chunk.len() as i32) };
if ret != 0 {
return Err(last_os_error());
}
diff --git a/getrandom/src/wasi.rs b/getrandom/src/wasi.rs
index 2d413e0..9276ee7 100644
--- a/getrandom/src/wasi.rs
+++ b/getrandom/src/wasi.rs
@@ -8,16 +8,18 @@
//! Implementation for WASI
use crate::Error;
-use core::num::NonZeroU32;
+use core::{
+ mem::MaybeUninit,
+ num::{NonZeroU16, NonZeroU32},
+};
use wasi::random_get;
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
- unsafe {
- random_get(dest.as_mut_ptr(), dest.len()).map_err(|e: wasi::Error| {
- // convert wasi's Error into getrandom's NonZeroU32 error
- // SAFETY: `wasi::Error` is `NonZeroU16` internally, so `e.raw_error()`
- // will never return 0
- NonZeroU32::new_unchecked(e.raw_error() as u32).into()
- })
- }
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
+ unsafe { random_get(dest.as_mut_ptr() as *mut u8, dest.len()) }.map_err(|e| {
+ // The WASI errno will always be non-zero, but we check just in case.
+ match NonZeroU16::new(e.raw()) {
+ Some(r) => Error::from(NonZeroU32::from(r)),
+ None => Error::ERRNO_NOT_POSITIVE,
+ }
+ })
}
diff --git a/getrandom/src/windows.rs b/getrandom/src/windows.rs
index 643badd..92d7042 100644
--- a/getrandom/src/windows.rs
+++ b/getrandom/src/windows.rs
@@ -7,7 +7,7 @@
// except according to those terms.
use crate::Error;
-use core::{ffi::c_void, num::NonZeroU32, ptr};
+use core::{ffi::c_void, mem::MaybeUninit, num::NonZeroU32, ptr};
const BCRYPT_USE_SYSTEM_PREFERRED_RNG: u32 = 0x00000002;
@@ -21,19 +21,37 @@
) -> u32;
}
-pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
+// Forbidden when targetting UWP
+#[cfg(not(target_vendor = "uwp"))]
+#[link(name = "advapi32")]
+extern "system" {
+ #[link_name = "SystemFunction036"]
+ fn RtlGenRandom(RandomBuffer: *mut c_void, RandomBufferLength: u32) -> u8;
+}
+
+pub fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
// Prevent overflow of u32
for chunk in dest.chunks_mut(u32::max_value() as usize) {
+ // BCryptGenRandom was introduced in Windows Vista
let ret = unsafe {
BCryptGenRandom(
ptr::null_mut(),
- chunk.as_mut_ptr(),
+ chunk.as_mut_ptr() as *mut u8,
chunk.len() as u32,
BCRYPT_USE_SYSTEM_PREFERRED_RNG,
)
};
// NTSTATUS codes use the two highest bits for severity status.
if ret >> 30 == 0b11 {
+ // Failed. Try RtlGenRandom as a fallback.
+ #[cfg(not(target_vendor = "uwp"))]
+ {
+ let ret =
+ unsafe { RtlGenRandom(chunk.as_mut_ptr() as *mut c_void, chunk.len() as u32) };
+ if ret != 0 {
+ continue;
+ }
+ }
// We zeroize the highest bit, so the error code will reside
// inside the range designated for OS codes.
let code = ret ^ (1 << 31);
diff --git a/getrandom/tests/common/mod.rs b/getrandom/tests/common/mod.rs
index 006f230..666f7f5 100644
--- a/getrandom/tests/common/mod.rs
+++ b/getrandom/tests/common/mod.rs
@@ -12,7 +12,19 @@
getrandom_impl(&mut [0u8; 0]).unwrap();
}
+// Return the number of bits in which s1 and s2 differ
+#[cfg(not(feature = "custom"))]
+fn num_diff_bits(s1: &[u8], s2: &[u8]) -> usize {
+ assert_eq!(s1.len(), s2.len());
+ s1.iter()
+ .zip(s2.iter())
+ .map(|(a, b)| (a ^ b).count_ones() as usize)
+ .sum()
+}
+
+// Tests the quality of calling getrandom on two large buffers
#[test]
+#[cfg(not(feature = "custom"))]
fn test_diff() {
let mut v1 = [0u8; 1000];
getrandom_impl(&mut v1).unwrap();
@@ -20,13 +32,35 @@
let mut v2 = [0u8; 1000];
getrandom_impl(&mut v2).unwrap();
- let mut n_diff_bits = 0;
- for i in 0..v1.len() {
- n_diff_bits += (v1[i] ^ v2[i]).count_ones();
- }
+ // Between 3.5 and 4.5 bits per byte should differ. Probability of failure:
+ // ~ 2^(-94) = 2 * CDF[BinomialDistribution[8000, 0.5], 3500]
+ let d = num_diff_bits(&v1, &v2);
+ assert!(d > 3500);
+ assert!(d < 4500);
+}
- // Check at least 1 bit per byte differs. p(failure) < 1e-1000 with random input.
- assert!(n_diff_bits >= v1.len() as u32);
+// Tests the quality of calling getrandom repeatedly on small buffers
+#[test]
+#[cfg(not(feature = "custom"))]
+fn test_small() {
+ // For each buffer size, get at least 256 bytes and check that between
+ // 3 and 5 bits per byte differ. Probability of failure:
+ // ~ 2^(-91) = 64 * 2 * CDF[BinomialDistribution[8*256, 0.5], 3*256]
+ for size in 1..=64 {
+ let mut num_bytes = 0;
+ let mut diff_bits = 0;
+ while num_bytes < 256 {
+ let mut s1 = vec![0u8; size];
+ getrandom_impl(&mut s1).unwrap();
+ let mut s2 = vec![0u8; size];
+ getrandom_impl(&mut s2).unwrap();
+
+ num_bytes += size;
+ diff_bits += num_diff_bits(&s1, &s2);
+ }
+ assert!(diff_bits > 3 * num_bytes);
+ assert!(diff_bits < 5 * num_bytes);
+ }
}
#[test]
diff --git a/getrandom/tests/custom.rs b/getrandom/tests/custom.rs
index 62eae1d..b085094 100644
--- a/getrandom/tests/custom.rs
+++ b/getrandom/tests/custom.rs
@@ -7,13 +7,8 @@
))]
use wasm_bindgen_test::wasm_bindgen_test as test;
-#[cfg(feature = "test-in-browser")]
-wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
-use core::{
- num::NonZeroU32,
- sync::atomic::{AtomicU8, Ordering},
-};
+use core::num::NonZeroU32;
use getrandom::{getrandom, register_custom_getrandom, Error};
fn len7_err() -> Error {
@@ -21,27 +16,36 @@
}
fn super_insecure_rng(buf: &mut [u8]) -> Result<(), Error> {
+ // `getrandom` guarantees it will not call any implementation if the output
+ // buffer is empty.
+ assert!(!buf.is_empty());
// Length 7 buffers return a custom error
if buf.len() == 7 {
return Err(len7_err());
}
- // Otherwise, increment an atomic counter
- static COUNTER: AtomicU8 = AtomicU8::new(0);
+ // Otherwise, fill bytes based on input length
+ let mut start = buf.len() as u8;
for b in buf {
- *b = COUNTER.fetch_add(1, Ordering::Relaxed);
+ *b = start;
+ start = start.wrapping_mul(3);
}
Ok(())
}
register_custom_getrandom!(super_insecure_rng);
+use getrandom::getrandom as getrandom_impl;
+mod common;
+
#[test]
fn custom_rng_output() {
let mut buf = [0u8; 4];
assert_eq!(getrandom(&mut buf), Ok(()));
- assert_eq!(buf, [0, 1, 2, 3]);
+ assert_eq!(buf, [4, 12, 36, 108]);
+
+ let mut buf = [0u8; 3];
assert_eq!(getrandom(&mut buf), Ok(()));
- assert_eq!(buf, [4, 5, 6, 7]);
+ assert_eq!(buf, [3, 9, 27]);
}
#[test]
diff --git a/getrandom/tests/rdrand.rs b/getrandom/tests/rdrand.rs
index 4ff85c4..2567868 100644
--- a/getrandom/tests/rdrand.rs
+++ b/getrandom/tests/rdrand.rs
@@ -11,5 +11,10 @@
#[path = "../src/util.rs"]
mod util;
-use rdrand::getrandom_inner as getrandom_impl;
+// The rdrand implementation has the signature of getrandom_uninit(), but our
+// tests expect getrandom_impl() to have the signature of getrandom().
+fn getrandom_impl(dest: &mut [u8]) -> Result<(), Error> {
+ rdrand::getrandom_inner(unsafe { util::slice_as_uninit_mut(dest) })?;
+ Ok(())
+}
mod common;
diff --git a/rhai/.gitattributes b/rhai/.gitattributes
new file mode 100644
index 0000000..98bce04
--- /dev/null
+++ b/rhai/.gitattributes
@@ -0,0 +1 @@
+*.rhai linguist-language=JavaScript
diff --git a/rhai/.github/workflows/benchmark.yml b/rhai/.github/workflows/benchmark.yml
new file mode 100644
index 0000000..12cfee3
--- /dev/null
+++ b/rhai/.github/workflows/benchmark.yml
@@ -0,0 +1,30 @@
+name: Benchmark
+on:
+ push:
+ branches:
+ - master
+ - perf
+
+jobs:
+ benchmark:
+ name: Run Rust benchmark
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - run: rustup toolchain update nightly && rustup default nightly
+ - name: Run benchmark
+ run: cargo +nightly bench --features decimal,metadata,serde,debugging | tee output.txt
+ - name: Store benchmark result
+ uses: rhysd/github-action-benchmark@v1
+ with:
+ name: Rust Benchmark
+ tool: 'cargo'
+ output-file-path: output.txt
+ # Use personal access token instead of GITHUB_TOKEN due to https://github.community/t5/GitHub-Actions/Github-action-not-triggering-gh-pages-upon-push/td-p/26869/highlight/false
+ github-token: ${{ secrets.RHAI }}
+ auto-push: true
+ # Show alert with commit comment on detecting possible performance regression
+ alert-threshold: '200%'
+ comment-on-alert: true
+ fail-on-alert: true
+ alert-comment-cc-users: '@schungx'
diff --git a/rhai/.github/workflows/build.yml b/rhai/.github/workflows/build.yml
new file mode 100644
index 0000000..bed9d2d
--- /dev/null
+++ b/rhai/.github/workflows/build.yml
@@ -0,0 +1,195 @@
+name: Build
+
+on:
+ push:
+ branches:
+ - main
+ - master
+ pull_request: {}
+
+env:
+ RUST_MSRV: 1.61.0
+
+jobs:
+ msrv:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/cache@v1
+ with:
+ path: |
+ ~/.cargo/bin
+ ~/.cargo/registry/index
+ ~/.cargo/registry/cache
+ ~/.cargo/git/db/
+ target
+ key: "${{ runner.os }}-cargo-build-msrv-${{ hashFiles('**/Cargo.lock') }}"
+ - uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: ${{ env.RUST_MSRV }}
+ override: true
+ - uses: actions-rs/cargo@v1
+ with:
+ command: check
+
+ # typical build with various feature combinations
+ build:
+ name: Build
+ runs-on: ${{ matrix.os }}
+ continue-on-error: ${{ matrix.experimental }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest]
+ flags:
+ - ""
+ - "--features testing-environ,debugging"
+ - "--features testing-environ,metadata,serde,internals"
+ - "--features testing-environ,unchecked,serde,metadata,internals,debugging"
+ - "--features testing-environ,sync,serde,metadata,internals,debugging"
+ - "--features testing-environ,no_position,serde,metadata,internals,debugging"
+ - "--features testing-environ,no_optimize,serde,metadata,internals,debugging"
+ - "--features testing-environ,no_float,serde,metadata,internals,debugging"
+ - "--features testing-environ,f32_float,serde,metadata,internals,debugging"
+ - "--features testing-environ,decimal,serde,metadata,internals,debugging"
+ - "--features testing-environ,no_custom_syntax,serde,metadata,internals,debugging"
+ - "--features testing-environ,no_float,decimal"
+ - "--tests --features testing-environ,only_i32,serde,metadata,internals,debugging"
+ - "--features testing-environ,only_i64,serde,metadata,internals,debugging"
+ - "--features testing-environ,no_index,serde,metadata,internals,debugging"
+ - "--features testing-environ,no_object,serde,metadata,internals,debugging"
+ - "--features testing-environ,no_function,serde,metadata,internals,debugging"
+ - "--features testing-environ,no_module,serde,metadata,internals,debugging"
+ - "--features testing-environ,no_time,serde,metadata,internals,debugging"
+ - "--features testing-environ,no_closure,serde,metadata,internals,debugging"
+ - "--features testing-environ,unicode-xid-ident,serde,metadata,internals,debugging"
+ - "--features testing-environ,sync,no_time,no_function,no_float,no_position,no_optimize,no_module,no_closure,no_custom_syntax,metadata,serde,unchecked,debugging"
+ - "--features testing-environ,no_time,no_function,no_float,no_position,no_index,no_object,no_optimize,no_module,no_closure,no_custom_syntax,unchecked"
+ toolchain: [stable]
+ experimental: [false]
+ include:
+ # smoketests for different toolchains
+ - {toolchain: stable, os: windows-latest, experimental: false, flags: ""}
+ - {toolchain: stable, os: macos-latest, experimental: false, flags: ""}
+ - {toolchain: beta, os: ubuntu-latest, experimental: false, flags: ""}
+ - {toolchain: nightly, os: ubuntu-latest, experimental: true, flags: ""}
+ fail-fast: false
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Setup Toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: ${{matrix.toolchain}}
+ override: true
+ - name: Test
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: ${{matrix.flags}}
+
+ # no-std builds are a bit more extensive to test
+ no_std_build:
+ name: NoStdBuild
+ runs-on: ${{matrix.os}}
+ continue-on-error: ${{matrix.experimental}}
+ strategy:
+ matrix:
+ include:
+ - {os: ubuntu-latest, flags: "--profile unix", experimental: false}
+ - {os: windows-latest, flags: "--profile windows", experimental: true}
+ #- {os: macos-latest, flags: "--profile macos", experimental: false}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Setup Toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: nightly
+ override: true
+ - name: Build Project
+ uses: actions-rs/cargo@v1
+ with:
+ command: build
+ args: --manifest-path=no_std/no_std_test/Cargo.toml ${{matrix.flags}}
+
+ wasm:
+ name: Check Wasm build
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ flags:
+ - "--target wasm32-wasi"
+# These fail currently, future PR should fix them
+# - "--target wasm32-unknown-unknown"
+# - "--target wasm32-unknown-unknown --features wasm-bindgen"
+ - "--target wasm32-unknown-unknown --no-default-features"
+ - "--target wasm32-unknown-unknown --no-default-features --features wasm-bindgen"
+ fail-fast: false
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Setup Generic Wasm Toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ target: wasm32-unknown-unknown
+ - name: Setup Wasi Toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ target: wasm32-wasi
+ - name: Build
+ uses: actions-rs/cargo@v1
+ with:
+ command: build
+ args: ${{matrix.flags}}
+
+ rustfmt:
+ name: Check Formatting
+ runs-on: windows-latest
+ continue-on-error: true
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Setup Toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: nightly
+ override: true
+ components: rustfmt, clippy
+ - name: Run Rustfmt
+ uses: actions-rs/cargo@v1
+ with:
+ command: fmt
+ args: --all -- --check
+ - name: Run Clippy
+ uses: actions-rs/cargo@v1
+ with:
+ command: clippy
+ args: --all -- -Aclippy::all -Dclippy::perf
+
+ codegen_build:
+ name: Codegen Build
+ runs-on: ${{matrix.os}}
+ continue-on-error: ${{matrix.experimental}}
+ strategy:
+ matrix:
+ include:
+ - {toolchain: stable, os: ubuntu-latest, experimental: false, flags: "--features metadata"}
+ - {toolchain: stable, os: windows-latest, experimental: false, flags: "--features metadata"}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Setup Toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: ${{matrix.toolchain}}
+ override: true
+ - name: Build Project
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --manifest-path=codegen/Cargo.toml ${{matrix.flags}}
diff --git a/rhai/.github/workflows/ci.yaml b/rhai/.github/workflows/ci.yaml
new file mode 100644
index 0000000..6b751bc
--- /dev/null
+++ b/rhai/.github/workflows/ci.yaml
@@ -0,0 +1,25 @@
+name: Manual CI
+
+on:
+ workflow_dispatch:
+ inputs:
+ depth:
+ description: "Specify a max number of simultaneous feature flags"
+ required: true
+ type: string
+ default: "2"
+
+jobs:
+ feature_powerset:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@v1
+ with:
+ toolchain: stable
+ - uses: taiki-e/install-action@v2
+ with:
+ tool: cargo-hack@0.5.25
+ - run: cargo hack check --feature-powerset --depth ${{ inputs.depth }} --no-dev-deps --exclude-features "$FEATURE_EXCLUDE"
+ env:
+ FEATURE_EXCLUDE: "no_std stdweb wasm-bindgen f32_float only_i32 unicode-xid-ident bin-features"
diff --git a/rhai/.gitignore b/rhai/.gitignore
new file mode 100644
index 0000000..d2538ea
--- /dev/null
+++ b/rhai/.gitignore
@@ -0,0 +1,14 @@
+target/
+Cargo.lock
+.vscode/
+.cargo/
+benches/results
+clippy.toml
+Rhai.toml
+**/*.bat
+**/*.exe
+doc/rhai-sync.json
+doc/rhai.json
+.idea/
+.idea
+.idea/*
diff --git a/rhai/CHANGELOG.md b/rhai/CHANGELOG.md
new file mode 100644
index 0000000..8fb8268
--- /dev/null
+++ b/rhai/CHANGELOG.md
@@ -0,0 +1,2009 @@
+Rhai Release Notes
+==================
+
+Version 1.15.0
+==============
+
+Bug fixes
+---------
+
+* Fixes a concurrency error in static hashing keys (thanks [`garypen`](https://github.com/garypen)!).
+
+Enhancements
+------------
+
+* Expressions involving `this` should now run slightly faster due to a dedicated `AST` node `ThisPtr`.
+* A `take` function is added to the standard library to take ownership of any data (replacing with `()`) in order to avoid cloning.
+* `EvalAltResult::ErrorMismatchOutputType` now gives a better name for the requested generic type (e.g. `&str` is now `&str` and not `string`).
+
+
+Version 1.14.0
+==============
+
+This new version contains a substantial number of bug fixes for edge cases.
+
+A new syntax is supported to facilitate writing object methods in script.
+
+The code hacks that attempt to optimize branch prediction performance are removed because benchmarks do not show any material speed improvements.
+
+Bug fixes
+----------
+
+* `is_shared` is a reserved keyword and is now handled properly (e.g. it cannot be the target of a function pointer).
+* Re-optimizing an AST via `optimize_ast` with constants now works correctly for closures. Previously the hidden `Share` nodes are not removed and causes variable-not-found errors during runtime if the constants are not available in the scope.
+* Expressions such as `(v[0].func()).prop` now parse correctly.
+* Shadowed variable exports are now handled correctly.
+* Shadowed constant definitions are now optimized correctly when propagated (e.g. `const X = 1; const X = 1 + 1 + 1; X` now evaluates to 3 instead of 0).
+* Identifiers and comma's in the middle of custom syntax now register correctly.
+* Exporting an object map from a module with closures defined on properties now works correctly when those properties are called to mimic method calls in OOP-style.
+* Compiling for `thumbv6m-none-eabi` target (e.g. Raspberry Pi Pico) now completes successfully. Dependency to `no-std-compat` is now pointed to the latest repo instead of `crates.io`.
+
+New features
+------------
+
+* It is now possible to require a specific _type_ to the `this` pointer for a particular script-defined function so that it is called only when the `this` pointer contains the specified type.
+* `is_def_fn` is extended to support checking for typed methods, with syntax `is_def_fn(this_type, fn_name, arity)`
+* `Dynamic::take` is added as a short-cut for `std::mem::take(&mut value)`.
+
+Enhancements
+------------
+
+* `Engine::is_symbol_disabled` is added to test whether a particular keyword/symbol is disabled.
+* Support is added to deserialize a `Dynamic` value containing custom types or shared values back into another `Dynamic` (essentially a straight cloned copy).
+
+
+Version 1.13.0
+==============
+
+This version attempts a number of optimizations that may yield small speed improvements:
+
+* Simple operators (e.g. integer arithmetic) are inlined to avoid the overhead of a function call.
+* The tokenizer uses pre-calculated tables (generated by GNU `gperf`) for keyword recognition.
+* A black-arts trick (see `Engine::black_box`) is used to prevent LLVM from optimizing hand-tuned AST node matches back into a lookup table, which messes up branch prediction on modern CPU's.
+
+Bug fixes
+---------
+
+* Complex indexing/dotting chains now parse correctly, for example: `a[b][c[d]].e`
+* `map` and `filter` for arrays are marked `pure`. Warnings are added to the documentation of pure array methods that take `this` closures.
+* Syntax such as `foo.bar::baz` no longer panics, but returns a proper parse error.
+* Expressions such as `!inside` now parses correctly instead of as `!in` followed by `side`.
+* Custom syntax starting with symbols now works correctly and no longer raises a parse error.
+* Comparing different custom types now works correctly when the appropriate comparison operators are registered.
+* Some op-assignments, such as `x += y` where `x` and `y` are `char`, now work correctly instead of failing silently.
+* Op-assignments to bit flags or bit ranges now work correctly.
+
+Potentially breaking changes
+----------------------------
+
+* The trait method `ModuleResolver::resolve_raw` (which is a low-level API) now takes a `&mut Scope` parameter. This is a breaking change because the signature is modified, but this trait method has a default and is rarely called/implemented in practice.
+* `Module::eval_ast_as_new_raw` (a low-level API) now takes a `&mut Scope` instead of the `Scope` parameter. This is a breaking change because the `&mut` is now required.
+* `Engine::allow_loop_expressions` now correctly defaults to `true` (was erroneously `false` by default).
+
+Enhancements
+------------
+
+* `Engine::new_raw` is now `const` and runs very fast, delaying all other initialization until first use.
+* The functions `min` and `max` are added for numbers.
+* Range cases in `switch` statements now also match floating-point and decimal values. In order to support this, however, small numeric ranges cases are no longer unrolled.
+* Loading a module via `import` now gives the module access to the current scope, including variables and constants defined inside.
+* Some very simple operator calls (e.g. integer add) are inlined to avoid the overhead of a function call, resulting in a small speed improvement.
+* The tokenizer now uses table-driven keyword recognizers generated by GNU `gperf`. At least _theoretically_ it should be faster...
+* The field `isAnonymous` is added to JSON functions metadata.
+
+
+Version 1.12.0
+==============
+
+Bug fixes
+---------
+
+* Integer numbers that are too large to deserialize into `INT` now fall back to `Decimal` or `FLOAT` instead of silently truncating.
+* Parsing deeply-nested closures (e.g. `||{||{||{||{||{||{||{...}}}}}}}`) no longer panics but will be confined to the nesting limit.
+* Closures containing a single expression are now allowed in `Engine::eval_expression` etc.
+* Strings interpolation now works under `Engine::new_raw` without any standard package.
+* `Fn` now throws an error if the name is a reserved keyword as it cannot possibly map to such a function. This also disallows creating function pointers to custom operators which are defined as disabled keywords (a mouthful), but such custom operators are designed primarily to be used as operators.
+
+Breaking API changes
+--------------------
+
+* The callback for initializing a debugger instance has changed to `Fn(&Engine, Debugger) -> Debugger`. This allows more control over the initial setup of the debugger.
+* The internal macro `reify!` is no longer available publicly.
+
+Deprecated API's
+----------------
+
+* `Module::with_capacity` is deprecated.
+* The internal method `Engine::eval_statements_raw` is deprecated.
+* Array overloaded methods that take function names (as string) are deprecated in favor of using the `Fn("...")` call.
+
+Speed improvements
+------------------
+
+* The function registration mechanism is revamped to take advantage of constant generics, among others, to omit checking code where possible. This yields a 10-20% speed improvements on certain real-life, function-call-heavy workloads.
+* Functions taking function pointers as parameters, usually called with closures, now run faster because a link to the anonymous function (generated by the closure) is stored together with the function pointer itself. This allows short-circuiting the function lookup step.
+
+Net features
+------------
+
+### First class functions (sort of)
+
+* A function pointer created via a closure definition now links to the particular anonymous function itself.
+* This avoids a potentially expensive function lookup when the function pointer is called, speeding up closures.
+* Closures now also encapsulate their defining environment, so function pointers can now be freely `export`ed from modules!
+
+### `!in`
+
+* A new operator `!in` is added which maps to `!(... in ...)`.
+
+### `Engine::call_fn_with_options`
+
+* `Engine::call_fn_raw` is deprecated in favor of `Engine::call_fn_with_options` which allows setting options for the function call.
+* The options are for future-proofing the API.
+* In this version, it gains the ability to set the value of the _custom state_ (accessible via `NativeCallContext::tag`) for a function evaluation, overriding `Engine::set_default_tag`.
+
+### Compact a script for compression
+
+* `Engine::compact_script` is added which takes a valid script (it still returns parsing errors) and returns a _compacted_ version of the script with all insignificant whitespaces and all comments removed.
+* A compact script compresses better than one with liberal whitespaces and comments.
+* Unlike some uglifiers or minifiers, `Engine::compact_script` does not optimize the script in any way, nor does it rename variables.
+
+### Enhanced array API
+
+* Array methods that take a function pointer, usually a closure (e.g. `map`, `filter`, `index_of`, `reduce` etc.), can now bind the array element to `this` when calling a closure.
+* This vastly improves performance when working with arrays of large types (e.g. object maps) by avoiding unnecessary cloning.
+* `find` and `find_map` are added for arrays.
+* `for_each` is also added for arrays, allowing a closure to mutate array elements (bound to `this`) in turn.
+
+Enhancements
+------------
+
+* Optimizations have been done to key data structures to minimize size and creation time, which involves turning rarely-used fields into `Option<Box<T>>`. This resulted in some speed improvements.
+* `CallableFunction` is exported under `internals`.
+* The `TypeBuilder` type and `CustomType` trait are no longer marked as volatile.
+* `FuncArgs` is also implemented for arrays.
+* `Engine::set_XXX` API can now be chained.
+* `EvalContext::scope_mut` now returns `&mut Scope` instead of `&mut &mut Scope`.
+* Line-style doc-comments are now merged into a single string to avoid creating many strings. Block-style doc-comments continue to be independent strings.
+* Block-style doc-comments are now "un-indented" for better formatting.
+* Doc-comments on plugin modules are now captured in the module's `doc` field.
+* Expression nesting levels is refined such that it grows less excessively for common patterns.
+* The traits `Index` and `IndexMut` are added to `FnPtr`.
+* `FnPtr::iter_curry` and `FnPtr::iter_curry_mut` are added.
+* `Dynamic::deep_scan` is added to recursively scan for `Dynamic` values.
+* `>>` and `<<` operators on integers no longer throw errors when the number of bits to shift is out of bounds. Shifting by a negative number of bits simply reverses the shift direction.
+
+
+Version 1.11.0
+==============
+
+Speed improvements
+------------------
+
+* Due to a code refactor, built-in operators for standard types now run even faster, in certain cases by 20-30%.
+
+Bug fixes
+---------
+
+* `Engine::parse_json` now returns an error on unquoted keys to be consistent with JSON specifications.
+* `import` statements inside `eval` no longer cause errors in subsequent code.
+* Functions marked `global` in `import`ed modules with no alias names now work properly.
+* Incorrect loop optimizations that are too aggressive (e.g. unrolling a `do { ... } until true` with a `break` statement inside) and cause crashes are removed.
+* `Dynamic::is` now works properly for shared values.
+
+Breaking changes
+----------------
+
+* `NativeCallContext::new` is completely deprecated and unimplemented (always panics) in favor of new API's.
+
+New features
+------------
+
+### `Dynamic` detection API
+
+* New methods are added to `Dynamic` in the form of `is_XXX()` where `XXX` is a type (e.g. `is_int`, `is_unit`, `is_bool`, `is_array`).
+* This new API is to make it easier to detect the data type, instead of having to call `is::<XXX>()`.
+
+### Loop expressions
+
+* Loops (such as `loop`, `do`, `while` and `for`) can now act as _expressions_, with the `break` statement returning an optional value.
+* Normal loops return `()` as the value.
+* Loop expressions can be enabled/disabled via `Engine::set_allow_loop_expressions`
+
+### Static hashing
+
+* It is now possible to specify a fixed _seed_ for use with the `ahash` hasher, via a static function `rhai::config::hashing::set_ahash_seed` or an environment variable (`RHAI_AHASH_SEED`), in order to force static (i.e. deterministic) hashes for function signatures.
+* This is necessary when using Rhai across shared-library boundaries.
+* A build script is used to extract the environment variable (`RHAI_AHASH_SEED`, if any) and splice it into the source code before compilation.
+
+### `no_time` for no timestamps
+
+* A new feature, `no_time`, is added to disable support for timestamps.
+* This may be necessary when building for architectures without time support, such as raw WASM.
+
+### Serializable `Scope`
+
+* `Scope` is now serializable and deserializable via `serde`.
+
+### Store and recreate `NativeCallContext`
+
+* A convenient API is added to store a `NativeCallContext` into a new `NativeCallContextStore` type.
+* This allows a `NativeCallContext` to be stored and recreated later on.
+
+### Call native Rust functions in `NativeCallContext`
+
+* `NativeCallContext::call_native_fn` is added to call registered native Rust functions only.
+* `NativeCallContext::call_native_fn_raw` is added as the advanced version.
+* This is often desirable as Rust functions typically do not want a similar-named scripted function to hijack the process -- which will cause brittleness.
+
+### Custom syntax improvements
+
+* The look-ahead symbol for custom syntax now renders a string literal in quotes (instead of the generic term `string`).
+* This facilitates more accurate parsing by separating strings and identifiers.
+
+### Limits API
+
+* Methods returning maximum limits (e.g. `Engine::max_string_len`) are now available even under `unchecked`.
+* This helps avoid the proliferation of unnecessary feature flags in third-party library code.
+
+Enhancements
+------------
+
+* `parse_json` function is added to parse a JSON string into an object map.
+* `Error::ErrorNonPureMethodCallOnConstant` is added which is raised when a non-pure method is called on a constant value.
+
+
+Version 1.10.1
+==============
+
+Bug fixes
+---------
+
+* Compiling on 32-bit architectures no longer cause a compilation error.
+* Fix type-size test for 32-bit architectures without the `decimal` feature.
+
+Custom syntax with state
+------------------------
+
+* [`Engine::register_custom_syntax_with_state_raw`] is added. The custom syntax parser and implementation functions take on an additional parameter that holds a user-defined custom _state_ which should substantially simplify writing some custom parsers.
+* [`Engine::register_custom_syntax_raw`] is deprecated.
+
+
+Version 1.10.0
+==============
+
+This version introduces _Fast Operators_ mode, which is turned on by default but can be disabled via
+a new options API: `Engine::set_fast_operators`.
+
+_Fast Operators_ mode assumes that none of Rhai's built-in operators for standard data types are
+overloaded by user-registered functions. In the vast majority of cases this should be so (really,
+who overloads the `+` operator for integers anyway?).
+
+This assumption allows the `Engine` to avoid checking for overloads for every single operator call.
+This usually results in substantial speed improvements, especially for expressions.
+
+Minimum Rust Version
+--------------------
+
+The minimum Rust version is now `1.61.0` in order to use some `const` generics.
+
+Bug fixes
+---------
+
+* API for registering property getters/setters and indexers to an `Engine` now works with functions that take a first parameter of `NativeCallContext`.
+* Missing API function `Module::set_getter_setter_fn` is added.
+* To avoid subtle errors, simple optimization is used for `rhai-run`; previous it was full optimization.
+
+Deprecated API
+--------------
+
+* All versions of the `Engine::register_XXX_result` API that register a function returning `Result<T, Box<EvalAltResult>>` are now deprecated. The regular, non-`result` versions handle all functions correctly.
+
+New features
+------------
+
+### Fast operators
+
+* A new option `Engine::fast_operators` is introduced (default to `true`) to enable/disable _Fast Operators_ mode.
+
+### Fallible type iterators
+
+* For very special needs, the ability to register fallible type iterators is added.
+
+### Expressions
+
+* `if`-expressions are allowed in `Engine::eval_expression` and `Engine::compile_expression` provided that both statement blocks each contain at most a single expression.
+* `switch`-expressions are allowed in `Engine::eval_expression` and `Engine::compile_expression` provided that match actions are expressions only.
+
+Enhancements
+------------
+
+* `is_empty` method is added to arrays, BLOB's, object maps, strings and ranges.
+* `StaticModuleResolver` now stores the path in the module's `id` field.
+* `Engine::module_resolver` is added to grant access to the `Engine`'s module resolver.
+* Constants and variables now have types in generated definition files.
+
+
+Version 1.9.1
+=============
+
+This is a bug-fix version that fixes a bug.
+
+Accessing properties in _Strict Variables Mode_ no longer generates a _variable not found_ error.
+
+
+Version 1.9.0
+=============
+
+The minimum Rust version is now `1.60.0` in order to use the `dep:` syntax for dependencies.
+
+Bug fixes
+---------
+
+* `switch` cases with conditions that evaluate to constant `()` no longer optimize to `false` (should raise a type error during runtime).
+* Fixes concatenation of BLOB's and strings, where the BLOB's should be interpreted as UTF-8 encoded strings.
+* Capturing an unknown variable in a closure no longer panics.
+* Fixes panic in interpolated strings with constant expressions.
+* Using `call_fn_raw` on a function without evaluating the AST no longer panics on namespace-qualified function calls due to `import` statements not run.
+* Some reserved tokens (such as "?", "++") cannot be used in custom syntax; this is now fixed.
+
+Breaking changes
+----------------
+
+* The first closure passed to `Engine::register_debugger` now takes a single parameter which is a reference to the current `Engine`.
+
+New features
+------------
+
+### New feature flags
+
+* A new feature flag, `std`, which is enabled by default, is added due to requirements from dependency crates.
+* A new feature flag, `no_custom_syntax`, is added to remove custom syntax support from Rhai for applications that do not require it (which should be most).
+
+### Module documentation
+
+* Comment lines beginning with `//!` (requires the `metadata` feature) are now collected as the script file's _module documentation_.
+* `AST` and `Module` have methods to access and manipulate documentation.
+
+### Output definition files
+
+* An API is added to automatically generate definition files from a fully-configured `Engine`, for use with the Rhai Language Server.
+
+### Short-hand to function pointers
+
+* Using a script-defined function's name (in place of a variable) implicitly creates a function pointer to the function.
+
+### Top-level functions
+
+* Crate-level functions `rhai::eval`, `rhai::run`, `rhai::eval_file`, `rhai::run_file` are added as convenient wrappers.
+
+### CustomType trait and TypeBuilder
+
+* A new volatile API, `Engine::build_type`, enables registration of the entire API of a custom type in one go, provided that the custom type implements the `CustomType` trait (which uses `TypeBuilder` to register the API functions).
+
+### Simpler Package API
+
+* It is now easier to register packages via the `Package::register_into_engine` and `Package::register_into_engine_as` API.
+* Defining a custom package with base packages is also much easier with a new syntax - put the new base packages after a colon.
+
+Enhancements
+------------
+
+### `switch` statement
+
+* `switch` cases can now include multiple values separated by `|`.
+* Duplicated `switch` cases are now allowed.
+* The error `ParseErrorType::DuplicatedSwitchCase` is deprecated.
+* Ranges in `switch` statements that are small (currently no more than 16 items) are unrolled if possible.
+
+### Others
+
+* `EvalContext::eval_expression_tree_raw` and `Expression::eval_with_context_raw` are added to allow for not rewinding the `Scope` at the end of a statements block.
+* A new `range` function variant that takes an exclusive range with a step.
+* `as_string` is added to BLOB's to convert it into a string by interpreting it as a UTF-8 byte stream.
+* `FnAccess::is_private`, `FnAccess::is_public`, `FnNamespace::is_module_namespace` and `FnNameSpace::is_global_namespace` are added for convenience.
+* `Iterator<Item=T>` type for functions metadata is simplified to `Iterator<T>`.
+* `Scope::remove` is added to remove a variable from a `Scope`, returning its value.
+* The code base is cleaner by running it through Clippy.
+* `ParseError::err_type` and `ParseError::position` are added for convenience.
+* The source of an `AST` compiled from a script file is set to the file's path.
+* `|>` and `<|` are now reserved symbols.
+
+
+Version 1.8.0
+=============
+
+Bug fixes
+---------
+
+* Self-contained `AST` now works properly with `Engine::call_fn`.
+* Missing `to_int` from `Decimal` is added.
+* Parsing of index expressions is relaxed and many cases no longer result in an index-type error to allow for custom indexers.
+* Merging or combining a self-contained `AST` into another `AST` now works properly.
+* Plugin modules/functions no longer generate errors under `#![deny(missing_docs)]`.
+* Calling a property on a function call that returns a shared value no longer causes an error.
+* _Strict Variables Mode_ now checks for module namespaces within functions as well.
+* Module defined via `Engine::register_static_module` are now checked in _Strict Variables Mode_.
+
+Reserved Symbols
+----------------
+
+* `?`, `??`, `?.`, `?[` and `!.` are now reserved symbols.
+
+Deprecated API's
+----------------
+
+* `FnPtr::num_curried` is deprecated in favor of `FnPtr::curry().len()`.
+
+New features
+------------
+
+* The _Elvis operators_ (`?.` and `?[`) are now supported for property access, method calls and indexing.
+* The _null-coalescing operator_ (`??`) is now supported to short-circuit `()` values.
+
+Enhancements
+------------
+
+* Indexing and property access are now faster.
+* `EvalAltResult::IndexNotFound` is added to aid in raising errors for indexers.
+* `Engine::def_tag`, `Engine::def_tag_mut` and `Engine::set_tag` are added to manage a default value for the custom evaluation state, accessible via `EvalState::tag()` (which is the same as `NativeCallContext::tag()`).
+* Originally, the debugger's custom state uses the same state as `EvalState::tag()` (which is the same as `NativeCallContext::tag()`). It is now split into its own variable accessible under `Debugger::state()`.
+* Non-borrowed string keys can now be deserialized for object maps via `serde`.
+* `Scope::get` is added to get a reference to a variable's value.
+* Variable resolvers can now return a _shared_ value which can be mutated.
+
+
+Version 1.7.0
+=============
+
+Bug fixes
+---------
+
+* Compound assignments now work properly with indexers.
+* Cloning a `Scope` no longer turns all constants to mutable.
+
+Script-breaking changes
+-----------------------
+
+* _Strict Variables Mode_ no longer returns an error when an undeclared variable matches a variable/constant in the provided external `Scope`.
+
+Potentially breaking API changes
+--------------------------------
+
+* The `Engine::on_var` and `Engine::on_parse_token` API's are now marked unstable/volatile.
+* The closures passed to `Engine::on_var`, `Engine::on_def_var` and `Engine::register_debugger` take `EvalContext` instead of `&EvalContext` or `&mut EvalContext`.
+* The following enum's are marked `non_exhaustive`: `AccessMode`, `FnAccess`, `FnNamespace`, `FnMetadata`, `OptimizationLevel`
+
+New API
+-------
+
+* `Module::eval_ast_as_new_raw` is made public as a low-level API.
+* `format_map_as_json` is provided globally, which is the same as `to_json` for object maps.
+* `Engine::call_fn_raw_raw` is added to add speed to repeated function calls.
+* `Engine::eval_statements_raw` is added to evaluate a sequence of statements.
+
+New features
+------------
+
+* A custom state is provided that is persistent during the entire evaluation run. This custom state is a `Dynamic`, which can hold any data, and can be accessed by the host via `EvalContext::tag`, `EvalContext::tag_mut`, `NativeCallContext::tag` and `GlobalRuntimeState.tag`.
+
+Enhancements
+------------
+
+* Improper `switch` case condition syntax is now caught at parse time.
+* `Engine::parse_json` now natively handles nested JSON inputs (using a token remap filter) without needing to replace `{` with `#{`.
+* `to_json` is added to object maps to cheaply convert it to JSON format (`()` is mapped to `null`, all other data types must be supported by JSON)
+* `FileModuleResolver` now accepts a custom `Scope` to provide constants for optimization.
+* New variants, `Start` and `End`, are added to `DebuggerEvent` triggered at the start/end of script evaluation.
+
+
+Version 1.6.1
+=============
+
+Bug fixes
+---------
+
+* Functions with `Dynamic` parameters now work in qualified calls from `import`ed modules.
+* `rhai-repl` now compiles with the new patch version of `rustyline`.
+* `rhai_codegen` dependency is now explicitly `1.4` or higher.
+
+Script-breaking changes
+-----------------------
+
+* `split` now splits a string by whitespaces instead of splitting it into individual characters. This is more in line with common practices.
+* A new function `to_chars` for strings is added to split the string into individual characters.
+
+Enhancements
+------------
+
+* Strings are now directly iterable (via `for .. in`) yielding individual characters.
+
+
+Version 1.6.0
+=============
+
+This version, in particular, fixes a plugin macro hygiene error for the nightly compiler:
+
+```text
+error[E0425]: cannot find value `args` in this scope
+```
+
+Compiler version
+----------------
+
+* Minimum compiler version is now `1.57` due to [`smartstring`](https://crates.io/crates/smartstring) dependency.
+
+Bug fixes
+---------
+
+* Fixed macro hygiene error with nightly compiler.
+* Invalid property or method access such as `a.b::c.d` or `a.b::func()` no longer panics but properly returns a syntax error.
+* `Scope::is_constant` now returns the correct value.
+* Exporting a variable that contains a local function pointer (including anonymous function or closure) now raises a runtime error.
+* Full optimization is now skipped for method calls.
+
+New features
+------------
+
+* [Type aliases](https://doc.rust-lang.org/reference/items/type-aliases.html) in plugin modules are now used as friendly names for custom types. This makes plugin modules more self-contained when they are used to define a custom type's API.
+
+Enhancements
+------------
+
+* Variable definitions are optimized so that shadowed variables are reused as much as possible to reduce memory consumption.
+* `FnAccess` and `FnNamespace` now implement `Ord` and `PartialOrd`.
+* The `event_handler_map` example is enhanced to prevent shadowing of the state object map.
+* Separation of constants in function calls is removed as its performance benefit is dubious.
+* A function `sleep` is added to block the current thread by a specified number of seconds.
+* `Scope::set_alias` is added to export a variable under a particular alias name.
+* `starts_with` and `ends_with` are added for strings.
+* Variables in modules registered via `register_global_module` can now be accessed in the global namespace.
+* `Dynamic::into_read_only` is added to convert a `Dynamic` value into constant.
+* `Module` now holds a collection of custom types with an API.
+
+
+Version 1.5.0
+=============
+
+This version adds a debugging interface, which can be used to integrate a debugger.
+
+Based on popular demand, an option is added to throw exceptions when invalid properties are accessed on object maps (default is to return `()`).
+
+Also based on popular demand, the `REPL` tool now uses a slightly-enhanced version of [`rustyline`](https://crates.io/crates/rustyline) for line editing and history.
+
+Bug fixes
+---------
+
+* In `Scope::clone_visible`, constants are now properly cloned as constants.
+* Variables introduced inside `try` blocks are now properly cleaned up upon an exception.
+* Off-by-one error in character positions after a comment line is now fixed.
+* Globally-defined constants are now encapsulated correctly inside a loaded module and no longer spill across call boundaries.
+* Type names display is fixed.
+* Exceptions thrown inside function calls now unwrap correctly when `catch`-ed.
+* Error messages for certain invalid property accesses are fixed.
+
+Script-breaking changes
+-----------------------
+
+* For consistency with the `import` statement, the `export` statement no longer exports multiple variables.
+* Appending a BLOB to a string (via `+`, `+=`, `append` or string interpolation) now treats the BLOB as a UTF-8 encoded string.
+* Appending a string/character to a BLOB (via `+=` or `append`) now adds the string/character as a UTF-8 encoded byte stream.
+
+New features
+------------
+
+* A debugging interface is added.
+* A new bin tool, `rhai-dbg` (aka _The Rhai Debugger_), is added to showcase the debugging interface.
+* A new package, `DebuggingPackage`, is added which contains the `back_trace` function to get the current call stack anywhere in a script.
+* `Engine::set_fail_on_invalid_map_property` is added to control whether to raise an error (new `EvalAltResult::ErrorPropertyNotFound`) when invalid properties are accessed on object maps.
+* `Engine::set_allow_shadowing` is added to allow/disallow variables _shadowing_, with new errors `EvalAltResult::ErrorVariableExists` and `ParseErrorType::VariableExists`.
+* `Engine::on_def_var` allows registering a closure which can decide whether a variable definition is allow to continue, during compilation or runtime, or should fail with an error (`ParseErrorType::ForbiddenVariable` or `EvalAltResult::ErrorForbiddenVariable`).
+* A new syntax for defining custom packages is introduced that removes the need to specify the Rhai crate name (internally uses the `$crate` meta variable).
+
+Enhancements
+------------
+
+* Default features for dependencies (such as `ahash/std` and `num-traits/std`) are no longer required.
+* The `no_module` feature now eliminates large sections of code via feature gates.
+* Debug display of `AST` is improved.
+* `NativeCallContext::call_level()` is added to give the current nesting level of function calls.
+* A new feature, `bin-features`, pulls in all the required features for `bin` tools.
+* `AST` position display is improved:
+ * `Expr::start_position` is added to give the beginning of the expression (not the operator's position).
+ * `StmtBlock` and `Stmt::Block` now keep the position of the closing `}` as well.
+* `EvalAltResult::unwrap_inner` is added to access the base error inside multiple layers of wrappings (e.g. `EvalAltResult::ErrorInFunction`).
+* Yet another new syntax is introduced for `def_package!` that further simplifies the old syntax.
+* A new method `to_blob` is added to convert a string into a BLOB as UTF-8 encoded bytes.
+* A new method `to_array` is added to convert a BLOB into array of integers.
+
+REPL tool changes
+-----------------
+
+The REPL bin tool, `rhai-rpl`, has been enhanced.
+
+### Build changes
+
+* The `rustyline` feature is now required in order to build `rhai-repl`.
+* Therefore, `rhai-repl` is no longer automatically built when using a simple `cargo build` with default features.
+
+### Line editor
+
+* `rhai-repl` now uses a modified version of [`rustyline`](https://crates.io/crates/rustyline) as a line editor with history.
+* Ctrl-Enter can now be used to enter multiple lines without having to attach the `\` continuation character the end of each line.
+* Bracketed paste is supported, even on Windows (version 10 or above), so pasting code directly into `rhai-repl` is made much more convenient.
+
+### New commands
+
+* `strict` to turn on/off _Strict Variables Mode_.
+* `optimize` to turn on/off script optimization.
+* `history` to print lines history.
+* `!!`, `!`_num_, `!`_text_ and `!?`_text_ to recall a history line.
+* `keys` to print all key bindings.
+
+
+Version 1.4.1
+=============
+
+This is primarily a bug-fix version which fixes a large number of bugs.
+
+Bug fixes
+---------
+
+* Expressions such as `x = x + 1` no longer panics.
+* Padding arrays with another array via `pad` no longer loops indefinitely.
+* `chop` for arrays and BLOB's now works properly.
+* `set_bit` for bit-flags with negative index now works correctly.
+* Misnamed `params` field `name` in the JSON output of `Engine::gen_fn_metadata_to_json` is fixed (was incorrectly named `type`).
+* Fixes a potential `unsafe` violation in `for` loop.
+* Missing `to_hex`, `to_octal` and `to_binary` for `i128` and `u128` are added.
+* `remove` for arrays and BLOB's now treat negative index correctly.
+* `parse_int` now works properly for negative numbers.
+* `Engine::gen_fn_signatures` now generates signatures for external packages registered via `Engine::register_global_module`.
+* `\r\n` pairs are now recognized correctly for doc-comments.
+
+Enhancements
+------------
+
+* Formatting of return types in functions metadata info is improved.
+* Use `SmartString` for `Scope` variable names and remove `unsafe` lifetime casting.
+* Functions in the standard library now have doc-comments (which can be obtained via `Engine::gen_fn_metadata_to_json`).
+* `get` and `set` methods are added to arrays, BLOB's, object maps and strings.
+
+
+Version 1.4.0
+=============
+
+This version adds support for integer _ranges_ via the `..` and `..=` operators.
+Many standard API's are extended with range parameters where appropriate.
+
+Script-breaking changes
+-----------------------
+
+* `is` is (pun intended) now a reserved keyword to prepare for possible future type checking expressions (e.g. `x is "string"`).
+
+Breaking changes
+----------------
+
+* `LogicPackage` is removed from `CorePackage`.
+* Bit-field functions are moved into a new `BitFieldPackage` (used to be in `LogicPackage`) which makes more sense.
+
+Bug fixes
+---------
+
+* Custom syntax now works properly inside binary expressions and with method calls.
+* Hex numbers with the high-bit set now parse correctly into negative integer numbers.
+* Constructing a literal array or object map now checks for size limits for each item instead of at the very end when it is already too late.
+* Non-`INT` integer types are now treated exactly as custom types under `only_i64` and `only_i32`.
+* Calling `pad` on an array now checks for total size over limit after each item added.
+
+New features
+------------
+
+* Added support for integer _ranges_ via the `..` and `..=` operators.
+* Added `EvalAltResult::ErrorCustomSyntax` to catch errors in custom syntax, which should not happen unless an `AST` is compiled on one `Engine` but evaluated on another unrelated `Engine`.
+
+Enhancements
+------------
+
+* `BLOB`'s are refined to display in a more compact hex format.
+* A new syntax is introduced for `def_package!` that will replace the old syntax in future versions.
+* Added `NativeCallContext::call_fn` to easily call a function.
+* Doc-comments on plugin module functions are extracted into the functions' metadata.
+
+Deprecated API's
+----------------
+
+* `Expression::get_variable_name` is deprecated in favor of the new `Expression::get_string_value`.
+* The old syntax of `def_package!` is deprecated in favor of the new syntax.
+
+
+Version 1.3.0
+=============
+
+This version adds native support for `BLOB`'s (byte arrays), as well as a number of configuration
+settings to fine-tun language features.
+
+Compiler requirement
+--------------------
+
+* Minimum compiler version is now 1.51.
+
+Bug fixes
+---------
+
+* `from_dynamic` now supports deserializing `Option`.
+
+New features
+------------
+
+* `BLOB` (essentially a byte array) is added as a supported primitive value type parallel to arrays.
+* New options for `Engine` which allows disabling `if`-expressions, `switch`-expressions, statement expressions, anonymous functions and/or looping (i.e. `while`, `loop`, `do` and `for` statements):
+ * `Engine::set_allow_if_expression`
+ * `Engine::set_allow_switch_expression`
+ * `Engine::set_allow_statement_expression`
+ * `Engine::set_allow_anonymous_fn`
+ * `Engine::set_allow_looping`
+* New _strict variables_ mode for `Engine` (enabled via `Engine::set_strict_variables`) to throw parse errors on undefined variable usage. Two new parse error variants, `ParseErrorType::VariableNotFound` and `ParseErrorType::ModuleNotFound`, are added.
+
+Enhancements
+------------
+
+* Two double quotes (`""`) in a string literal now maps to `"`; two back-ticks (``` `` ```) in a literal string now maps to `` ` ``.
+* Added `Engine::register_type_with_name_raw` to register a custom type based on a fully-qualified type path.
+* Added `into_array` and `into_typed_array` for `Dynamic`.
+* Added `FnPtr::call` and `FnPtr::call_within_context` to simplify calling a function pointer.
+* A function's hashes are included in its JSON metadata to assist in debugging. Each function's `baseHash` field in the JSON object should map directly to the pre-calculated hash in the function call.
+* `Expression` now derefs to `Expr`.
+
+Deprecated and Gated API's
+--------------------------
+
+* `NativeCallContext::new` is deprecated because it is simpler to call a function pointer via `FnPtr::call`.
+* `AST::merge_filtered` and `AST::combine_filtered` are no longer exported under `no_function`.
+* `AST::new` and `AST::new_with_source` are moved under `internals`.
+* `FnPtr::call_dynamic` is deprecated in favor of `FnPtr::call_raw`.
+
+
+Version 1.2.1
+=============
+
+Bug fixes
+---------
+
+* Array methods (such as `map`) taking a closure with captures as argument now works properly.
+
+
+Version 1.2.0
+=============
+
+Bug fixes (potentially script-breaking)
+--------------------------------------
+
+* As originally intended, function calls with a bang (`!`) now operates directly on the caller's scope, allowing variables inside the scope to be mutated.
+* As originally intended, `Engine::XXX_with_scope` API's now properly propagate constants within the provided scope also to _functions_ in the script.
+* Printing of integral floating-point numbers is fixed (used to only prints `0.0`).
+* `func!()` calls now work properly under `no_closure`.
+* Fixed parsing of unary negation such that expressions like `if foo { ... } -x` parses correctly.
+
+New features
+------------
+
+* `#[cfg(...)]` attributes can now be put directly on plugin functions or function defined in a plugin module.
+* A custom syntax parser can now return a symbol starting with `$$` to inform the implementation function which syntax variant was actually parsed.
+* `AST::iter_literal_variables` is added to extract all top-level literal constant/variable definitions from a script without running it.
+* `Engine::call_fn_dynamic` is deprecated and `Engine::call_fn_raw` is added which allows keeping new variables in the custom scope.
+
+Enhancements
+------------
+
+* Array methods now avoid cloning as much as possible (although most predicates will involve cloning anyway if passed a closure).
+* Array methods that take function pointers (e.g. closures) now optionally take the function name as a string.
+* Array adds the `dedup` method.
+* Array adds a `sort` method with no parameters which sorts homogeneous arrays of built-in comparable types (e.g. `INT`).
+* Inlining is disabled for error-path functions because errors are exceptional and scripts usually fail completely when an error is encountered.
+* The `optimize` module is completely eliminated under `no_optimize`, which should yield smaller code size.
+* `NativeCallContext::position` is added to return the position of the function call.
+* `Scope::clone_visible` is added that copies only the last instance of each variable, omitting all shadowed variables.
+
+Deprecated API's
+----------------
+
+* `NativeCallContext::call_fn_dynamic_raw` is deprecated and `NativeCallContext::call_fn_raw` is added.
+* `From<EvalAltResult>` for `Result<T, Box<EvalAltResult> >` is deprecated so it will no longer be possible to do `EvalAltResult::ErrorXXXXX.into()` to convert to a `Result`; instead, `Err(EvalAltResult:ErrorXXXXX.into())` must be used. Code is clearer if errors are explicitly wrapped in `Err`.
+
+
+Version 1.1.2
+=============
+
+Bug fixes
+---------
+
+* `0.0` now prints correctly (used to print `0e0`).
+* Unary operators are now properly recognized as an expression statement.
+* Reverses a regression on string `+` operations.
+* The global namespace is now searched before packages, which is the correct behavior.
+
+
+Version 1.1.1
+=============
+
+Bug fixes
+---------
+
+* Assignment to indexing expression with dot expressions inside no longer cause a compilation error.
+* The `no_module` and `internals` features now work together without a compilation error.
+* String literal operations (such as `"hello" + ", world"`) now optimizes correctly.
+
+
+Version 1.1.0
+=============
+
+Bug fixes
+---------
+
+* Custom syntax starting with a disabled standard keyword now works properly.
+* When calling `Engine::call_fn`, new variables defined during evaluation of the body script are removed and no longer spill into the function call.
+* `NamespaceRef::new` is fixed.
+
+Enhancements
+------------
+
+### `Engine` API
+
+* `Engine::consume_XXX` methods are renamed to `Engine::run_XXX` to make meanings clearer. The `consume_XXX` API is deprecated.
+* `Engine::register_type_XXX` are now available even under `no_object`.
+* Added `Engine::on_parse_token` to allow remapping certain tokens during parsing.
+* Added `Engine::const_empty_string` to merge empty strings into a single instance.
+
+### Custom Syntax
+
+* `$symbol$` is supported in custom syntax to match any symbol.
+* Custom syntax with `$block$`, `}` or `;` as the last symbol are now self-terminating (i.e. no need to attach a terminating `;`).
+
+### `Dynamic` Values
+
+* `Dynamic::as_string` and `Dynamic::as_immutable_string` are deprecated and replaced by `into_string` and `into_immutable_string` respectively.
+* Added a number of constants to `Dynamic`.
+* Added a number of constants and `fromXXX` constant methods to `Dynamic`.
+* Added `sin`, `cos` and `tan` for `Decimal` values.
+
+### `Decimal` Values
+
+* `parse_float()`, `PI()` and `E()` now defer to `Decimal` under `no_float` if `decimal` is turned on.
+* Added `log10()` for `Decimal`.
+* `ln` for `Decimal` is now checked and won't panic.
+
+### String Values
+
+* `SmartString` now uses `LazyCompact` instead of `Compact` to minimize allocations.
+* Added `pop` for strings.
+* Added `ImmutableString::ptr_eq` to test if two strings point to the same allocation.
+* The `serde` feature of `SmartString` is turned on under `metadata` to make `Map` serializable.
+
+### `Scope` API
+
+* `Scope::set_value` now takes anything that implements `Into<Cow<str> >`.
+* Added `Scope::is_constant` to check if a variable is constant.
+* Added `Scope::set_or_push` to add a new variable only if one doesn't already exist.
+
+### `AST` API
+
+* Added `ASTNode::position`.
+* `ReturnType` is removed in favor of option flags for `Stmt::Return`.
+* `Stmt::Break` and `Stmt::Continue` are merged into `Stmt::BreakLoop` via an option flag.
+* `StaticVec` is changed to keep three items inline instead of four.
+
+
+Version 1.0.6
+=============
+
+Bug fixes
+---------
+
+* Eliminate unnecessary property write-back when accessed via a getter since property getters are assumed to be _pure_.
+* Writing to a property of an indexed valued obtained via an indexer now works properly by writing back the changed value via an index setter.
+
+Enhancements
+------------
+
+* `MultiInputsStream`, `ParseState`, `TokenIterator`, `IdentifierBuilder` and `AccessMode` are exported under the `internals` feature.
+
+
+Version 1.0.5
+=============
+
+Bug fixes
+---------
+
+* `FloatWrapper` is no longer erroneously exported under `no_float+internals`.
+* The `sign` function now works properly for float values that are `NaN`.
+
+
+Version 1.0.4
+=============
+
+* Fixed bug with `catch` variable used in `catch` block.
+
+
+Version 1.0.2
+=============
+
+Bug fixes
+---------
+
+* Fixed bug in method call followed by an array indexing.
+
+
+Version 1.0.1
+=============
+
+Bug fixes
+---------
+
+* Fixed bug in using indexing/dotting inside index bracket.
+* `while` and `loop` statements are no longer considered _pure_ (since a loop can go on forever and this is a side effect).
+
+
+Version 1.0.0
+=============
+
+The official version `1.0`.
+
+Almost the same version as `0.20.3` but with deprecated API's removed.
+
+Bug fixes
+---------
+
+* Fixed infinite loop in certain script optimizations.
+* Building for `no-std` no longer requires patching `smartstring`.
+* Parsing a lone `return` or `throw` without a semicolon at the end of a block no longer raises an error.
+
+Breaking changes
+----------------
+
+* All deprecated API's (e.g. the `RegisterFn` and `RegisterResultFn` traits) are removed.
+* `Module::set_id` is split into `Module::set_id` and `Module::clear_id` pair.
+* `begin`, `end`, `each`, `then`, `unless` are no longer reserved keywords.
+
+Enhancements
+------------
+
+* New methods `is_odd`, `is_even` for integers, and `is_zero` for all numbers.
+* `From<BTreeSet>` and `From<HashSet>` are added for `Dynamic`, which create object maps with `()` values.
+
+
+Version 0.20.3
+==============
+
+This version adds support to index into an integer number, treating it as a bit-field.
+
+Bug fixes
+---------
+
+* Fixed incorrect optimization regarding chain-indexing with non-numeric index.
+* Variable values are checked for over-sized violations after assignments and setters.
+
+Breaking changes
+----------------
+
+* To keep the API consistent, strings are no longer iterable by default. Use the `chars` method to iterate through the characters in a string.
+* `Dynamic::take_string` and `Dynamic::take_immutable_string` are renamed to `Dynamic::as_string` and `Dynamic::as_immutable_string` respectively.
+
+New features
+------------
+
+* New syntax for `for` statement to include counter variable.
+* An integer value can now be indexed to get/set a single bit.
+* The `bits` method of an integer can be used to iterate through its bits.
+* New `$bool$`, `$int$`, `$float$` and `$string$` expression types for custom syntax.
+* New methods `to_hex`, `to_octal` and `to_binary` for integer numbers.
+* New methods `to_upper`, `to_lower`, `make_upper`, `make_lower` for strings/characters.
+
+
+Version 0.20.2
+==============
+
+This version adds a number of convenience features:
+
+* Ability for a `Dynamic` to hold an `i32` _tag_ of arbitrary data
+
+* Simplifies dynamic properties access by falling back to an indexer (passing the name of the property as a string) when a property is not found.
+
+Bug fixes
+---------
+
+* Propagation of constants held in a custom scope now works properly instead of always replacing by `()`.
+
+Breaking changes
+----------------
+
+* `Engine::disable_doc_comments` is removed because doc-comments are now placed under the `metadata` feature flag.
+* Registering a custom syntax now only requires specifying whether the `Scope` is adjusted (i.e. whether variables are added or removed). There is no need to specify the number of variables added/removed.
+* Assigning to a property of a constant is now allowed and no longer raise an `EvalAltResult::ErrorAssignmentToConstant` error. This is to facilitate the Singleton pattern. Registered setter functions are automatically guarded against setters calling on constants and will continue to raise errors unless the `pure` attribute is present (for plugins).
+* If a property getter/setter is not found, an indexer with string index, if any, is tried.
+* The indexers API (`Engine::register_indexer_XXX` and `Module::set_indexer_XXX`) are now also exposed under `no_index`.
+
+New features
+------------
+
+* Each `Dynamic` value can now contain arbitrary data (type `i32`) in the form of a _tag_. This is to use up otherwise wasted space in the `Dynamic` type.
+* A new internal feature `no_smartstring` to turn off `SmartString` for those rare cases that it is needed.
+* `DynamicReadLock` and `DynamicWriteLoc` are exposed under `internals`.
+* `From< Shared< Locked<Dynamic> > >` is added for `Dynamic` mapping directly to a shared value, together with support for `Dynamic::from`.
+* An indexer with string index acts as a _fallback_ to a property getter/setter.
+
+Enhancements
+------------
+
+* Registering a custom syntax now only requires specifying whether the `Scope` is adjusted (i.e. whether variables are added or removed). This allows more flexibility for cases where the number of new variables declared depends on internal logic.
+* Putting a `pure` attribute on a plugin property/index setter now enables it to be used on constants.
+
+
+Version 0.20.1
+==============
+
+This version enables functions to access constants declared at global level via the special `global` module.
+
+Bug fixes
+---------
+
+* Fixed bug when position is zero in `insert` and `split_at` methods for arrays.
+* Indexing operations with pure index values are no longer considered pure due to the possibility of indexers.
+
+Breaking changes
+----------------
+
+* `Dynamic::is_shared` and `Dynamic::is_locked` are removed under the `no_closure` feature. They used to always return `false`.
+* `Engine::call_fn` now evaluates the `AST` before calling the function.
+* `Engine::on_progress` is disabled with `unchecked`.
+
+Enhancements
+------------
+
+* The crate [`no-std-compat`](https://crates.io/crates/no_std_compat) is used to compile for `no-std`. This removes the need to use a special `crate::stdlib` namespace for `std` imports.
+
+New features
+------------
+
+* A module called `global` is automatically created to hold global-level constants, which can then be accessed from functions.
+* A new feature `no_position` is added to turn off position tracking during parsing to squeeze out the last drop of performance.
+
+
+Version 0.20.0
+==============
+
+This version adds string interpolation with `` `... ${`` ... ``} ...` `` syntax.
+
+`switch` statement cases can now have conditions.
+
+Negative indices for arrays and strings are allowed and now count from the end (-1 = last item/character).
+
+Bug fixes
+---------
+
+* Property setter op-assignments now work properly.
+* Off-by-one bug in `Array::drain` method with range is fixed.
+
+Breaking changes
+----------------
+
+* Negative index to an array or string yields the appropriate element/character counting from the _end_.
+* The default `_` case of a `switch` statement now must be the last case, together with two new error variants: `EvalAltResult::WrongSwitchDefaultCase` and `EvalAltResult::WrongSwitchCaseCondition`.
+* `ModuleResolver` trait methods take an additional parameter `source_path` that contains the path of the current environment. This is to facilitate loading other script files always from the current directory.
+* `FileModuleResolver` now resolves relative paths under the source path if there is no base path set.
+* `FileModuleResolver::base_path` now returns `Option<&str>` which is `None` if there is no base path set.
+* Doc-comments now require the `metadata` feature.
+
+Enhancements
+------------
+
+* `Array::drain` and `Array::retain` methods with predicate now scan the array in forward order instead of in reverse.
+
+New features
+------------
+
+* String interpolation support is added via the `` `... ${`` ... ``} ...` `` syntax.
+* `FileModuleResolver` resolves relative paths under the parent path (i.e. the path holding the script that does the loading). This allows seamless cross-loading of scripts from a directory hierarchy instead of having all relative paths load from the current working directory.
+* Negative index to an array or string yields the appropriate element/character counting from the _end_.
+* `switch` statement cases can now have an optional `if` clause.
+
+
+Version 0.19.15
+===============
+
+This version replaces all internal usage of `HashMap` with `BTreeMap`, which should result
+in some speed improvement because a `BTreeMap` is leaner when the number of items held is small.
+Most, if not all, collections in Rhai hold very few data items, so this is a typical scenario of
+many tiny-sized collections.
+
+The Rhai object map type, `Map`, used to be an alias to `HashMap` and is now aliased to `BTreeMap`
+instead. This is also because, in the vast majority of usage cases, the number of properties held by
+an object map is small.
+
+`HashMap` and `BTreeMap` have almost identical public API's so this change is unlikely to break
+existing code.
+
+[`SmartString`](https://crates.io/crates/smartstring) is used to store identifiers (which tend to
+be short, fewer than 23 characters, and ASCII-based) because they can usually be stored inline.
+`Map` keys now also use [`SmartString`](https://crates.io/crates/smartstring).
+
+In addition, there is now support for line continuation in strings (put `\` at the end of line) as
+well as multi-line literal strings (wrapped by back-ticks: `` `...` ``).
+
+Finally, all function signature/metadata methods are now grouped under the umbrella `metadata` feature.
+This avoids spending precious resources maintaining metadata for functions for the vast majority of
+use cases where such information is not required.
+
+
+Bug fixes
+---------
+
+* The feature flags `no_index + no_object` now compile without errors.
+
+Breaking changes
+----------------
+
+* The traits `RegisterFn` and `RegisterResultFn` are removed. `Engine::register_fn` and `Engine::register_result_fn` are now implemented directly on `Engine`.
+* `FnPtr::call_dynamic` now takes `&NativeCallContext` instead of consuming it.
+* All `Module::set_fn_XXX` methods are removed, in favor of `Module::set_native_fn`.
+* `Array::reduce` and `Array::reduce_rev` now take a `Dynamic` as initial value instead of a function pointer.
+* `protected`, `super` are now reserved keywords.
+* The `Module::set_fn_XXX` API now take `&str` as the function name instead of `Into<String>`.
+* The _reflections_ API such as `Engine::gen_fn_signatures`, `Module::update_fn_metadata` etc. are put under the `metadata` feature gate.
+* The shebang `#!` is now a reserved symbol.
+* Shebangs at the very beginning of script files are skipped when loading them.
+* [`SmartString`](https://crates.io/crates/smartstring) is used for identifiers by default. Currently, a PR branch is pulled for `no-std` builds. The official crate will be used once `SmartString` is fixed to support `no-std`.
+* `Map` is now an alias to `BTreeMap<SmartString, Dynamic>` instead of `HashMap` because most object maps hold few properties.
+* `EvalAltResult::FnWrongDefinition` is renamed `WrongFnDefinition` for consistency.
+
+New features
+------------
+
+* Line continuation (via `\`) and multi-line literal strings (wrapped with `` ` ``) support are added.
+* Rhai scripts can now start with a shebang `#!` which is ignored.
+
+Enhancements
+------------
+
+* Replaced all `HashMap` usage with `BTreeMap` for better performance because collections in Rhai are tiny.
+* `Engine::register_result_fn` no longer requires the successful return type to be `Dynamic`. It can now be any clonable type.
+* `#[rhai_fn(return_raw)]` can now return `Result<T, Box<EvalAltResult> >` where `T` is any clonable
+ type instead of `Result<Dynamic, Box<EvalAltResult> >`.
+* `Dynamic::clone_cast` is added to simplify casting from a `&Dynamic`.
+
+
+Version 0.19.14
+===============
+
+This version runs faster due to optimizations done on AST node structures. It also fixes a number of
+panic bugs related to passing shared values as function call arguments.
+
+Bug fixes
+---------
+
+* Panic when passing a shared string into a registered function as `&str` argument is fixed.
+* Panic when calling `switch` statements on custom types is fixed.
+* Potential overflow panics in `range(from, to, step)` is fixed.
+* `&mut String` parameters in registered functions no longer panic when passed a string.
+* Some expressions involving shared variables now work properly, for example `x in shared_value`, `return shared_value`, `obj.field = shared_value` etc. Previously, the resultant value is still shared which is counter-intuitive.
+* Errors in native Rust functions now contain the correct function call positions.
+* Fixed error types in `EvalAltResult::ErrorMismatchDataType` which were swapped.
+
+Breaking changes
+----------------
+
+* `Dynamic::as_str` is removed because it does not properly handle shared values.
+* Zero step in the `range` function now raises an error instead of creating an infinite stream.
+* Error variable captured by `catch` is now an _object map_ containing error fields.
+* `EvalAltResult::clear_position` is renamed `EvalAltResult::take_position` and returns the position taken.
+* `private` functions in an `AST` can now be called with `call_fn` etc.
+* `NativeCallContext::call_fn_dynamic_raw` no longer has the `pub_only` parameter.
+* `Module::update_fn_metadata` input parameter is changed.
+* Function keywords (e.g. `type_of`, `eval`, `Fn`) can no longer be overloaded. It is more trouble than worth. To disable these keywords, use `Engine::disable_symbol`.
+* `is_def_var` and `is_def_fn` are now reserved keywords.
+* `Engine::id` field is removed because it is never used.
+* `num-traits` is now a required dependency.
+* The `in` operator is now implemented on top of the `contains` function and is no longer restricted to a few specific types.
+* `EvalAltResult::ErrorInExpr` is removed because the `in` operator now calls `contains`.
+* The methods `AST::walk`, `Expr::walk`, `Stmt::walk` and `ASTNode::walk` and the callbacks they take now return `bool` to optionally terminate the recursive walk.
+
+Enhancements
+------------
+
+* Layout of AST nodes is optimized to reduce redirections, so speed is improved.
+* Function calls are more optimized and should now run faster.
+* `range` function now supports negative step and decreasing streams (i.e. to < from).
+* More information is provided to the error variable captured by the `catch` statement in an _object map_.
+* Previously, `private` functions in an `AST` cannot be called with `call_fn` etc. This is inconvenient when trying to call a function inside a script which also serves as a loadable module exporting part (but not all) of the functions. Now, all functions (`private` or not) can be called in an `AST`. The `private` keyword is relegated to preventing a function from being exported.
+* `Dynamic::as_unit` just for completeness sake.
+* `bytes` method added for strings to get length quickly (if the string is ASCII-only).
+* `FileModuleResolver` can now enable/disable caching.
+* Recursively walking an `AST` can now be terminated in the middle.
+
+
+Version 0.19.13
+===============
+
+This version introduces functions with `Dynamic` parameters acting as wildcards.
+
+Bug fixes
+---------
+
+* Bug in `Position::is_beginning_of_line` is fixed.
+
+Breaking changes
+----------------
+
+* For plugin functions, constants passed to methods (i.e. `&mut` parameter) now raise an error unless the functions are marked with `#[rhai_fn(pure)]`.
+* Visibility (i.e. `pub` or not) for generated _plugin_ modules now follow the visibility of the underlying module.
+* Comparison operators between the sames types or different _numeric_ types now throw errors when they're not defined instead of returning the default. Only comparing between _different_ types will return the default.
+* Default stack-overflow and top-level expression nesting limits for release builds are lowered to 64 from 128.
+* `Engine::call_fn_dynamic` takes an additional parameter to optionally evaluate the given `AST` before calling the function.
+
+New features
+------------
+
+* Functions are now allowed to have `Dynamic` arguments.
+* `#[rhai_fn(pure)]` attribute to mark a plugin function with `&mut` parameter as _pure_ so constants can be passed to it. Without it, passing a constant value into the `&mut` parameter will now raise an error.
+
+Enhancements
+------------
+
+* Built-in operators between `FLOAT`/[`Decimal`](https://crates.io/crates/rust_decimal) and `INT` are now implemented for more speed under those cases.
+* Error position in `eval` statements is now wrapped in an `EvalAltResult::ErrorInFunctionCall`.
+* `Position` now implements `Add` and `AddAssign`.
+* `Scope` now implements `IntoIterator`.
+* Strings now have the `-`/`-=` operators and the `remove` method to delete a sub-string/character.
+* Strings now have the `split_rev` method and variations of `split` with maximum number of segments.
+* Arrays now have the `split` method.
+* Comparisons between `FLOAT`/[`Decimal`](https://crates.io/crates/rust_decimal) and `INT` are now built in.
+* Comparisons between string and `char` are now built in.
+* `Engine::call_fn_dynamic` can now optionally evaluate the given `AST` before calling the function.
+
+
+Version 0.19.12
+===============
+
+This version is an incremental release with a number of enhancements and bug fixes.
+
+Notice that there are a number of breaking changes, especially with regards to replacing the `~`
+exponential operator with `**`, and the addition of the `decimal` feature that turns on
+[`Decimal`](https://crates.io/crates/rust_decimal) support.
+
+Bug fixes
+---------
+
+* Empty statements (i.e. statements with only one `;`) now parse correctly and no longer hang.
+* `continue`, `break` and `return` statements no longer panic inside a `try .. catch` block.
+* `round` function for `f64` is now implemented correctly.
+
+Breaking changes
+----------------
+
+* In order to be consistent with other scripting languages:
+ * the power/exponentiation operator is changed from `~` to `**`; `~` is now a reserved symbol
+ * the power/exponentiation operator now binds to the right
+ * trigonometry functions now take radians and return radians instead of degrees
+* `Dynamic::into_shared` is no longer available under `no_closure`. It used to panic.
+* `Token::is_operator` is renamed to `Token::is_symbol`.
+* `AST::clone_functions_only_filtered`, `AST::merge_filtered`, `AST::combine_filtered` and `AST::retain_functions` now take `Fn` instead of `FnMut` as the filter predicate.
+
+New features
+------------
+
+* Scientific notation is supported for floating-point number literals.
+* A new feature, `decimal`, enables the [`Decimal`](https://crates.io/crates/rust_decimal) data type. When both `no_float` and `decimal` features are enabled, floating-point literals parse to `Decimal`.
+
+Enhancements
+------------
+
+* Functions resolution cache is used in more cases, making repeated function calls faster.
+* Added `atan(x, y)` and `hypot(x, y)` to `BasicMathPackage`.
+* Added standard arithmetic operators between `FLOAT`/[`Decimal`](https://crates.io/crates/rust_decimal) and `INT`.
+
+
+Version 0.19.11
+===============
+
+This version streamlines compiling for WASM.
+
+Rust compiler minimum version is raised to 1.49.
+
+Bug fixes
+---------
+
+* Parameters passed to plugin module functions were sometimes erroneously consumed. This is now fixed.
+* Fixes compilation errors in `metadata` feature build.
+* Stacking `!` operators now work properly.
+* Off-by-one error in `insert` method for arrays is fixed.
+* Invalid property access now throws the appropriate error instead of panics.
+
+Breaking changes
+----------------
+
+* Rust compiler requirement raised to 1.49.
+* `NativeCallContext::new` taker an additional parameter containing the name of the function called.
+* `Engine::set_doc_comments` is renamed `Engine::enable_doc_comments`.
+
+New features
+------------
+
+* Two new features, `wasm-bindgen` and `stdweb`, to specify the JS interop layer for WASM builds. `wasm-bindgen` used to be required.
+
+Enhancements
+------------
+
+* `ahash` is used to hash function call parameters. This should yield speed improvements.
+* `Dynamic` and `ImmutableString` now implement `serde::Serialize` and `serde::Deserialize`.
+* `NativeCallContext` has a new field containing the name of the function called, useful when the same Rust function is registered under multiple names in Rhai.
+* New functions `PI()` and `E()` to return mathematical constants, and `to_radians` and `to_degrees` to convert between radians and degrees.
+
+
+Version 0.19.10
+===============
+
+Bug fixes
+---------
+
+* `no_std` feature now compiles correctly (bug introduced in `0.19.9`).
+* Bug in `FileModuleResolver::clear_cache_for_path` path mapping fixed.
+* Some optimizer fringe cases are fixed - related to constants propagation when the evil `eval` is present.
+
+Breaking changes
+----------------
+
+* The error variant `EvalAltResult::ErrorInFunctionCall` has a new parameter holding the _source_ of the function.
+* `ParseErrorType::WrongFnDefinition` is renamed `FnWrongDefinition`.
+* Redefining an existing function within the same script now throws a new `ParseErrorType::FnDuplicatedDefinition`. This is to prevent accidental overwriting an earlier function definition.
+* `AST::set_source` is now split into `AST::set_source` and `AST::clear_source`.
+
+New features
+------------
+
+* `Engine::compile_into_self_contained` compiles a script into an `AST` and _eagerly_ resolves all `import` statements with string literal paths. The resolved modules are directly embedded into the `AST`. When the `AST` is later evaluated, `import` statements directly yield the pre-resolved modules without going through the resolution process once again.
+* `AST::walk`, `Stmt::walk` and `Expr::walk` internal API's to recursively walk an `AST`.
+
+Enhancements
+------------
+
+* Source information is provided when there is an error within a call to a function defined in another module.
+* Source information is provided to the `NativeCallContext` for native Rust functions.
+* `EvalAltResult::clear_position` to clear the position information of an error - useful when only the message is needed and the position doesn't need to be printed out.
+* A new optional function `resolve_ast` is added to the `ModuleResolver` trait for advanced usage.
+
+
+Version 0.19.9
+==============
+
+This version fixes a bug introduced in `0.19.8` which breaks property access
+within closures.
+
+It also removes the confusing differences between _packages_ and _modules_
+by unifying the terminology and API under the global umbrella of _modules_.
+
+Bug fixes
+---------
+
+* Fix bug when accessing properties in closures.
+* Fix bug when accessing a deep index with a function call.
+* Fix bug that sometimes allow assigning to an invalid l-value.
+* Fix off-by-one error with `Engine::set_max_call_levels`.
+
+Breaking changes
+----------------
+
+* `Engine::load_package` is renamed `Engine::register_global_module` and now must explicitly pass a shared [`Module`].
+* `Engine::register_module` is renamed `Engine::register_static_module` and now must explicitly pass a shared [`Module`].
+* `Package::get` is renamed `Package::as_shared_module`.
+* `Engine::set_module_resolver` now takes a straight module resolver instead of an `Option`. To disable module resolving, use the new `DummyModuleResolver`.
+
+Enhancements
+------------
+
+* `Scope` is now `Clone + Hash`.
+* `Engine::register_static_module` now supports sub-module paths (e.g. `foo::bar::baz`).
+* `Engine::register_custom_operator` now accepts reserved symbols.
+* `Engine::register_custom_operator` now returns an error if given a precedence of zero.
+* The examples `repl` and `rhai_runner` are moved into `bin` and renamed `rhai-repl` and `rhai-run` respectively.
+
+
+Version 0.19.8
+==============
+
+This version makes it easier to generate documentation for a Rhai code base.
+
+Each function defined in an `AST` can optionally attach _doc-comments_ (which, as in Rust,
+are comments prefixed by either `///` or `/**`). Doc-comments allow third-party tools to
+automatically generate documentation for functions defined in a Rhai script.
+
+A new API, `Engine::gen_fn_metadata_to_json` and `Engine::gen_fn_metadata_with_ast_to_json`,
+paired with the new `metadata` feature, exports the full list of functions metadata
+(including those in an `AST`) as a JSON document.
+
+There are also a sizable number of bug fixes.
+
+Bug fixes
+---------
+
+* Unary prefix operators `-`, `+` and `!` now bind correctly when applied to an expression. Previously, `-x.len` is parsed as `(-x).len` which is obviously counter-intuitive.
+* Indexing of namespace-qualified variables now work properly, such as `path::to::var[x]`.
+* Constants are no longer propagated by the optimizer if shadowed by a non-constant variable.
+* A constant passed as the `this` parameter to Rhai functions now throws an error if assigned to.
+* Generic type parameter of `Engine::register_iterator` is `IntoIterator` instead of `Iterator`.
+* Fixes parsing of block comments ending with `**/` or inner blocks starting with `//*`.
+
+Breaking changes
+----------------
+
+* `Engine::on_progress` now takes `u64` instead of `&u64`.
+* The closure for `Engine::on_debug` now takes two additional parameters: `source: Option<&str>` and `pos: Position`.
+* `AST::iter_functions` now returns `ScriptFnMetadata`.
+* The parser function passed to `Engine::register_custom_syntax_raw` now takes an additional parameter containing the _look-ahead_ symbol.
+
+New features
+------------
+
+* `AST::iter_functions` now returns `ScriptFnMetadata` which includes, among others, _doc-comments_ for functions prefixed by `///` or `/**`.
+* _Doc-comments_ can be enabled/disabled with the new `Engine::set_doc_comments` method.
+* A new feature `metadata` is added that pulls in `serde_json` and enables `Engine::gen_fn_metadata_to_json` and `Engine::gen_fn_metadata_with_ast_to_json` which exports the full list of functions metadata (including those inside an `AST`) in JSON format.
+* `Engine::on_debug` provides two additional parameters: `source: Option<&str>` and `pos: Position`, containing the current source (if any) and position of the `debug` statement.
+* `NativeCallContext` and `EvalContext` both expose `source()` which returns the current source, if any.
+
+Enhancements
+------------
+
+* A functions lookup cache is added to make function call resolution faster.
+* Capturing a constant variable in a closure is now supported, with no cloning.
+* A _look-ahead_ symbol is provided to custom syntax parsers, which can be used to parse variable-length symbol streams.
+
+
+Version 0.19.7
+==============
+
+Bug fixes
+---------
+
+* Fixes compilation errors with certain feature flag combinations.
+
+Enhancements
+------------
+
+* Property getters/setters and indexers defined in a plugin module are by default `#[rhai_fn(global)]`.
+* `to_debug` is a new standard function for converting a value into debug format.
+* Arrays and object maps now print values using `to_debug` (if available).
+
+
+Version 0.19.6
+==============
+
+This version adds the `switch` statement.
+
+It also allows exposing selected module functions (usually methods) to the global namespace.
+This is very convenient when encapsulating the API of a custom Rust type into a module while having methods
+and iterators registered on the custom type work normally.
+
+A new `gen_fn_signatures` API enables enumerating the registered functions of an `Engine` for documentation purposes.
+It also prepares the way for a future reflection API.
+
+Bug fixes
+---------
+
+* Custom syntax that introduces a shadowing variable now works properly.
+
+Breaking changes
+----------------
+
+* `Module::set_fn`, `Module::set_raw_fn` and `Module::set_fn_XXX_mut` all take an additional parameter of `FnNamespace`.
+* `Module::set_fn` takes a further parameter with a list of parameter names/types plus the function return type, if any.
+* `Module::get_sub_module_mut` is removed.
+* `begin`, `end`, `unless` are now reserved keywords.
+* `EvalPackage` is removed in favor of `Engine::disable_symbol`.
+
+New features
+------------
+
+* New `switch` statement.
+* New `do ... while` and `do ... until` statements.
+* New `Engine::gen_fn_signatures`, `Module::gen_fn_signatures` and `PackagesCollection::gen_fn_signatures` to generate a list of signatures for functions registered.
+* New `Engine::register_static_module` to register a module as a sub-module in the global namespace.
+* New `set_exported_global_fn!` macro to register a plugin function and expose it to the global namespace.
+* `Module::set_fn_XXX_mut` can expose a module function to the global namespace. This is convenient when registering an API for a custom type.
+* `Module::set_getter_fn`, `Module::set_setter_fn`, `Module::set_indexer_get_fn`, `Module::set_indexer_set_fn` all expose the function to the global namespace by default. This is convenient when registering an API for a custom type.
+* New `Module::update_fn_metadata` to update a module function's parameter names and types.
+* New `#[rhai_fn(global)]` and `#[rhai_fn(internal)]` attributes to determine whether a function defined in a plugin module should be exposed to the global namespace. This is convenient when defining an API for a custom type.
+* New `get_fn_metadata_list` to get the metadata of all script-defined functions in scope.
+
+Enhancements
+------------
+
+* New constants under `Dynamic` including `UNIT`, `TRUE`, `FALSE`, `ZERO`, `ONE` etc.
+* Floating-point numbers ending with a decimal point without a trailing `0` are supported.
+
+
+Version 0.19.5
+==============
+
+This version fixes a bug that prevents compilation with the `internals` feature.
+It also speeds up importing modules.
+
+Bug fixes
+---------
+
+* Fixes compilation error when using the `internals` feature. Bug introduced in `0.19.4`.
+* Importing script files recursively no longer panics.
+
+Breaking changes
+----------------
+
+* Modules imported at global level can now be accessed in functions.
+* `ModuleResolver::resolve` now returns `Shared<Module>` for better resources sharing when loading modules.
+* `ParseErrorType::DuplicatedExport` is removed as multiple `export`'s are now allowed.
+
+Enhancements
+------------
+
+* Modules imported via `import` statements at global level can now be used in functions. There is no longer any need to re-`import` the modules at the beginning of each function block.
+* Modules imported via `import` statements are encapsulated into the `AST` when loading a module from a script file.
+* `export` keyword can now be tagged onto `let` and `const` statements as a short-hand, e.g.: `export let x = 42;`
+* Variables can now be `export`-ed multiple times under different names.
+* `index_of`, `==` and `!=` are defined for arrays.
+* `==` and `!=` are defined for object maps.
+
+
+Version 0.19.4
+==============
+
+This version basically cleans up the code structure in preparation for a potential `1.0` release in the future.
+Most scripts should see a material speed increase.
+
+This version also adds a low-level API for more flexibility when defining custom syntax.
+
+Bug fixes
+---------
+
+* Fixes `Send + Sync` for `EvalAltResult` under the `sync` feature. Bug introduced with `0.19.3`.
+
+Breaking changes
+----------------
+
+* Custom syntax can no longer start with a keyword (even a _reserved_ one), even if it has been disabled. That is to avoid breaking scripts later when the keyword is no longer disabled.
+
+Changes to Error Handling
+------------------------
+
+* `EvalAltResult::ErrorAssignmentToUnknownLHS` is moved to `ParseError::AssignmentToInvalidLHS`. `ParseError::AssignmentToCopy` is removed.
+* `EvalAltResult::ErrorDataTooLarge` is simplified.
+* `Engine::on_progress` closure signature now returns `Option<Dynamic>` with the termination value passed on to `EvalAltResult::ErrorTerminated`.
+* `ParseErrorType::BadInput` now wraps a `LexError` instead of a text string.
+
+New features
+------------
+
+* `f32_float` feature to set `FLOAT` to `f32`.
+* Low-level API for custom syntax allowing more flexibility in designing the syntax.
+* `Module::fill_with` to poly-fill a module with another.
+* Scripts terminated via `Engine::on_progress` can now pass on a value as a termination token.
+
+Enhancements
+------------
+
+* Essential AST structures like `Expr` and `Stmt` are packed into smaller sizes (16 bytes and 32 bytes on 64-bit), stored inline for more cache friendliness, and de-`Box`ed as much as possible.
+* `Scope` is optimized for cache friendliness.
+
+
+Version 0.19.3
+==============
+
+This version streamlines some of the advanced API's, and adds the `try` ... `catch` statement
+to catch exceptions.
+
+Breaking changes
+----------------
+
+* `EvalAltResult::ErrorReadingScriptFile` is removed in favor of the new `EvalAltResult::ErrorSystem`.
+* `EvalAltResult::ErrorLoopBreak` is renamed to `EvalAltResult::LoopBreak`.
+* `Engine::register_raw_fn` and `FnPtr::call_dynamic` function signatures have changed.
+* Callback signatures to `Engine::on_var` and `Engine::register_custom_syntax` have changed.
+* `EvalAltResult::ErrorRuntime` now wraps a `Dynamic` instead of a string.
+* Default call stack depth for `debug` builds is reduced to 8 (from 12) because it keeps overflowing the stack in GitHub CI!
+* Keyword `thread` is reserved.
+
+New features
+------------
+
+* The plugins system is enhanced to support functions taking a `NativeCallContext` as the first parameter.
+* `throw` statement can now throw any value instead of just text strings.
+* New `try` ... `catch` statement to catch exceptions.
+
+Enhancements
+------------
+
+* Calling `eval` or `Fn` in method-call style, which is an error, is now caught during parsing.
+* `func!()` call style is valid even under `no_closure` feature.
+
+
+Version 0.19.2
+==============
+
+Bug fix on call module functions.
+
+
+Version 0.19.1
+==============
+
+This version adds a variable resolver with the ability to short-circuit variable access,
+plus a whole bunch of array methods.
+
+Breaking changes
+----------------
+
+* `AST::iter_functions` now returns an iterator instead of taking a closure.
+* `Module::get_script_function_by_signature` renamed to `Module::get_script_fn` and returns `&<Shared<ScriptFnDef> >`.
+* `Module::num_fn`, `Module::num_var` and `Module::num_iter` are removed and merged into `Module::count`.
+* The `merge_namespaces` parameter to `Module::eval_ast_as_new` is removed and now defaults to `true`.
+* `GlobalFileModuleResolver` is removed because its performance gain over the `FileModuleResolver` is no longer very significant.
+* The following `EvalAltResult` variants are removed and merged into `EvalAltResult::ErrorMismatchDataType`: `ErrorCharMismatch`, `ErrorNumericIndexExpr`, `ErrorStringIndexExpr`, `ErrorImportExpr`, `ErrorLogicGuard`, `ErrorBooleanArgMismatch`
+* `Scope::iter_raw` returns an iterator with an additional field indicating whether the variable is constant or not.
+* `rhai::ser` and `rhai::de` namespaces are merged into `rhai::serde`.
+* New reserved symbols: `++`, `--`, `..`, `...`.
+* Callback signature for custom syntax implementation function is changed to allow for more flexibility.
+* Default call stack depth for `debug` builds is reduced to 12 (from 16).
+* Precedence for `~` is raised, while `in` is moved below logic comparison operators.
+
+New features
+------------
+
+* New `Engine::on_var` to register a _variable resolver_.
+* `const` statements can now take any expression (or none at all) instead of only constant values.
+* `OptimizationLevel::Simple` now eagerly evaluates built-in binary operators of primary types (if not overloaded).
+* `is_def_var()` to detect if variable is defined, and `is_def_fn()` to detect if script function is defined.
+* `Dynamic::from(&str)` now constructs a `Dynamic` with a copy of the string as value.
+* `AST::combine` and `AST::combine_filtered` allows combining two `AST`'s without creating a new one.
+* `map`, `filter`, `reduce`, `reduce_rev`, `some`, `all`, `extract`, `splice`, `chop` and `sort` functions for arrays.
+* New `Module::set_iterable` and `Module::set_iterator` to define type iterators more easily. `Engine::register_iterator` is changed to use the simpler version.
+
+Enhancements
+------------
+
+* Many one-liners and few-liners are now marked `#[inline]` or `[inline(always)]`, just in case it helps when LTO is not turned on.
+
+
+Version 0.19.0
+==============
+
+The major new feature for this version is _Plugins_ support, powered by procedural macros.
+Plugins make it extremely easy to develop and register Rust functions with an `Engine`.
+
+Bug fixes
+---------
+
+* `if` statement with an empty `true` block would not evaluate the `false` block. This is now fixed.
+* Fixes a bug in `Module::set_fn_4_mut`.
+* Module API's now properly handle `&str` and `String` parameters.
+* Indexers are available under `no_object`.
+* Registered operator-assignment functions (e.g. `+=`) now work correctly.
+
+Breaking changes
+----------------
+
+* `Engine::register_set_result` and `Engine::register_indexer_set_result` now take a function that returns `Result<(), Box<EvalAltResult> >`.
+* `Engine::register_indexer_XXX` and `Module::set_indexer_XXX` panic when the type is `Array`, `Map` or `String`.
+* `EvalAltResult` has a new variant `ErrorInModule` which holds errors when loading an external module.
+* `Module::eval_ast_as_new` now takes an extra boolean parameter, indicating whether to encapsulate the entire module into a separate namespace.
+* Functions in `FileModuleResolver` loaded modules now can cross-call each other in addition to functions in the global namespace. For the old behavior, use `MergingFileModuleResolver` instead.
+* New `EvalAltResult::ErrorInModule` variant capturing errors when loading a module from a script file.
+
+New features
+------------
+
+* Plugins support via procedural macros.
+* Scripted functions are allowed in packages.
+* `parse_int` and `parse_float` functions for parsing numbers; `split` function for splitting strings.
+* `AST::iter_functions` and `Module::iter_script_fn_info` to iterate functions.
+* Functions iteration functions for `AST` and `Module` now take `FnMut` instead of `Fn`.
+* New `FileModuleResolver` that encapsulates the entire `AST` of the module script, allowing function cross-calling. The old version is renamed `MergingFileModuleResolver`.
+* `+` and `-` operators for timestamps to increment/decrement by seconds.
+
+
+Version 0.18.3
+==============
+
+Bug fixes
+---------
+
+* `Engine::compile_expression`, `Engine::eval_expression` etc. no longer parse anonymous functions and closures.
+* Imported modules now work inside closures.
+* Closures that capture now work under `no_object`.
+
+New features
+------------
+
+* Adds `Module::combine_flatten` to combine two modules while flattening to the root level.
+
+
+Version 0.18.2
+==============
+
+Bug fixes
+---------
+
+* Fixes bug that prevents calling functions in closures.
+* Fixes bug that erroneously consumes the first argument to a namespace-qualified function call.
+
+Breaking changes
+----------------
+
+* `Module::contains_fn` and `Module::get_script_fn` no longer take the `public_only` parameter.
+
+New features
+------------
+
+* Adds `Engine::register_get_result`, `Engine::register_set_result`, `Engine::register_indexer_get_result`, `Engine::register_indexer_set_result` API.
+* Adds `Module::combine` to combine two modules.
+* `Engine::parse_json` now also accepts a JSON object starting with `#{`.
+
+
+Version 0.18.1
+==============
+
+This version adds:
+
+* Anonymous functions (in Rust closure syntax). Simplifies creation of single-use ad-hoc functions.
+* Currying of function pointers.
+* Closures - auto-currying of anonymous functions to capture shared variables from the external scope. Use the `no_closure` feature to disable sharing values and capturing.
+* Binding the `this` pointer in a function pointer `call`.
+* Capturing call scope via `func!(...)` syntax.
+
+New features
+------------
+
+* `call` can now be called function-call style for function pointers - this is to handle builds with `no_object`.
+* Reserve language keywords, such as `print`, `eval`, `call`, `this` etc.
+* `x.call(f, ...)` allows binding `x` to `this` for the function referenced by the function pointer `f`.
+* Anonymous functions are supported in the syntax of a Rust closure, e.g. `|x, y, z| x + y - z`.
+* Custom syntax now works even without the `internals` feature.
+* Currying of function pointers is supported via the new `curry` keyword.
+* Automatic currying of anonymous functions to capture shared variables from the external scope.
+* Capturing of the calling scope for function call via the `func!(...)` syntax.
+* `Module::set_indexer_get_set_fn` is added as a short-hand of both `Module::set_indexer_get_fn` and `Module::set_indexer_set_fn`.
+* New `unicode-xid-ident` feature to allow [Unicode Standard Annex #31](http://www.unicode.org/reports/tr31/) for identifiers.
+* `Scope::iter_raw` returns an iterator with a reference to the underlying `Dynamic` value (which may be shared).
+
+Breaking changes
+----------------
+
+* Language keywords are now _reserved_ (even when disabled) and they can no longer be used as variable names.
+* Function signature for defining custom syntax is simplified.
+* `Engine::register_raw_fn_XXX` API shortcuts are removed.
+* `PackagesCollection::get_fn`, `PackagesCollection::contains_fn`, `Module::get_fn` and `Module::contains_fn` now take an additional `public_only` parameter indicating whether only public functions are accepted.
+* The iterator returned by `Scope::iter` now contains a clone of the `Dynamic` value (unshared).
+* `Engine::register_global_module` takes any type that is `Into<PackageLibrary>`.
+* Error in `Engine::register_custom_syntax` is no longer `Box`-ed.
+
+Housekeeping
+------------
+
+* Most compilation warnings are eliminated via feature gates.
+
+
+Version 0.17.0
+==============
+
+This version adds:
+
+* [`serde`](https://crates.io/crates/serde) support for working with `Dynamic` values (particularly _object maps_).
+* Low-level API to register functions.
+* Surgically disable keywords and/or operators in the language.
+* Define custom operators.
+* Extend the language via custom syntax.
+
+Bug fixes
+---------
+
+* Fixed method calls in the middle of a dot chain.
+
+Breaking changes
+----------------
+
+* `EvalAltResult::ErrorMismatchOutputType` has an extra argument containing the name of the requested type.
+* `Engine::call_fn_dynamic` take an extra argument, allowing a `Dynamic` value to be bound to the `this` pointer.
+* Precedence of the `%` (modulo) operator is lowered to below bit shifts. This is to handle the case of `x < < 3 % 10`.
+
+New features
+------------
+
+* New `serde` feature to allow serializing/deserializing to/from `Dynamic` values using [`serde`](https://crates.io/crates/serde).
+ This is particularly useful when converting a Rust `struct` to a `Dynamic` _object map_ and back.
+* `Engine::disable_symbol` to surgically disable keywords and/or operators.
+* `Engine::register_custom_operator` to define a custom operator.
+* `Engine::register_custom_syntax` to define a custom syntax.
+* New low-level API `Engine::register_raw_fn`.
+* New low-level API `Module::set_raw_fn` mirroring `Engine::register_raw_fn`.
+* `AST::clone_functions_only`, `AST::clone_functions_only_filtered` and `AST::clone_statements_only` to clone only part of an `AST`.
+* The boolean `^` (XOR) operator is added.
+* `FnPtr` is exposed as the function pointer type.
+* `rhai::module_resolvers::ModuleResolversCollection` added to try a list of module resolvers.
+* It is now possible to mutate the first argument of a namespace-qualified function call when the argument is a simple variable (but not a module constant).
+* Many configuration/setting API's now returns `&mut Self` so that the calls can be chained.
+* `String` parameters in functions are supported (but inefficiently).
+
+
+Version 0.16.1
+==============
+
+Bug fix release to fix errors when compiling with features.
+
+
+Version 0.16.0
+==============
+
+The major new feature in this version is OOP - well, poor man's OOP, that is.
+
+The `README` is officially transferred to [The Rhai Book](https://rhai.rs/book).
+
+An online [Playground](https://alvinhochun.github.io/rhai-demo/) is available.
+
+Breaking changes
+----------------
+
+* The trait function `ModuleResolver::resolve` no longer takes a `Scope` as argument.
+* Functions defined in script now differentiates between using method-call style and normal function-call style.
+ The method-call style will bind the object to the `this` parameter instead of consuming the first parameter.
+* Imported modules are no longer stored in the `Scope`. `Scope::push_module` is removed.
+ Therefore, cannot rely on module imports to persist across invocations using a `Scope`.
+* `AST::retain_functions` is used for another purpose. The old `AST::retain_functions` is renamed to `AST::clear_statements`.
+
+New features
+------------
+
+* Support for _function pointers_ via `Fn(name)` and `Fn.call(...)` syntax - a poor man's first-class function.
+* Support for calling script-defined functions in method-call style with `this` binding to the object.
+* Special support in object maps for OOP.
+* Expanded the `AST` API for fine-tuned manipulation of functions.
+
+Enhancements
+------------
+
+* [The Rhai Book](https://rhai.rs/book) is online. Most content in the original `README` was transferred to the Book.
+* New feature `internals` to expose internal data structures (e.g. the AST nodes).
+
+
+Version 0.15.1
+==============
+
+This is a minor release which enables updating indexers (via registered indexer setters) and supports functions
+with `&str` parameters (maps transparently to `ImmutableString`). WASM is also a tested target.
+
+Bug fix
+-------
+
+* `let s="abc"; s[1].change_to('X');` now correctly sets the character '`X`' into '`s`' yielding `"aXc"`.
+
+Breaking changes
+----------------
+
+* Callback closure passed to `Engine::on_progress` now takes `&u64` instead of `u64` to be consistent with other callback signatures.
+* `Engine::register_indexer` is renamed to `Engine::register_indexer_get`.
+* `Module::set_indexer_fn` is renamed to `Module::set_indexer_get_fn`.
+* The tuple `ParseError` now exposes the internal fields and the `ParseError::error_type` and `ParseError::position` methods are removed. The first tuple field is the `ParseErrorType` and the second tuple field is the `Position`.
+* `Engine::call_fn_dynamic` now takes any type that implements `IntoIterator<Item = Dynamic>`.
+
+New features
+------------
+
+* Indexers are now split into getters and setters (which now support updates). The API is split into `Engine::register_indexer_get` and `Engine::register_indexer_set` with `Engine::register_indexer_get_set` being a short-hand. Similarly, `Module::set_indexer_get_fn` and `Module::set_indexer_set_fn` are added.
+* `Engine:register_fn` and `Engine:register_result_fn` accepts functions that take parameters of type `&str` (immutable string slice), which maps directly to `ImmutableString`. This is to avoid needing wrappers for functions taking string parameters.
+* Set maximum limit on data sizes: `Engine::set_max_string_size`, `Engine::set_max_array_size` and `Engine::set_max_map_size`.
+* Supports trailing commas on array literals, object map literals, function definitions and function calls.
+* Enhances support for compiling to WASM.
+
+
+Version 0.15.0
+==============
+
+This version uses immutable strings (`ImmutableString` type) and built-in operator functions (e.g. `+`, `>`, `+=`) to improve speed, plus some bug fixes.
+
+Regression fix
+--------------
+
+* Do not optimize script with `eval_expression` - it is assumed to be one-off and short.
+
+Bug fixes
+---------
+
+* Indexing with an index or dot expression now works property (it compiled wrongly before).
+ For example, `let s = "hello"; s[s.len-1] = 'x';` now works property instead of causing a runtime error.
+* `if` expressions are not supposed to be allowed when compiling for expressions only. This is fixed.
+
+Breaking changes
+----------------
+
+* `Engine::compile_XXX` functions now return `ParseError` instead of `Box<ParseError>`.
+* The `RegisterDynamicFn` trait is merged into the `RegisterResultFn` trait which now always returns `RhaiResult`.
+* Default maximum limit on levels of nested function calls is fine-tuned and set to a different value.
+* Some operator functions are now built in (see _Speed enhancements_ below), so they are available even under `Engine::new_raw`.
+* Strings are now immutable. The type `rhai::ImmutableString` is used instead of `std::string::String`. This is to avoid excessive cloning of strings. All native-Rust functions taking string parameters should switch to `rhai::ImmutableString` (which is either `Rc<String>` or `Arc<String>` depending on whether the `sync` feature is used).
+* Native Rust functions registered with the `Engine` also mutates the first argument when called in normal function-call style (previously the first argument will be passed by _value_ if not called in method-call style). Of course, if the first argument is a calculated value (e.g. result of an expression), then mutating it has no effect, but at least it is not cloned.
+* Some built-in methods (e.g. `len` for string, `floor` for `FLOAT`) now have _property_ versions in addition to methods to simplify coding.
+
+New features
+------------
+
+* Set limit on maximum level of nesting expressions and statements to avoid panics during parsing.
+* New `EvalPackage` to disable `eval`.
+* `Module::set_getter_fn`, `Module::set_setter_fn` and `Module:set_indexer_fn` to register getter/setter/indexer functions.
+* `Engine::call_fn_dynamic` for more control in calling script functions.
+
+Speed enhancements
+------------------
+
+* Common operators (e.g. `+`, `>`, `==`) now call into highly efficient built-in implementations for standard types (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and `ImmutableString`) if not overridden by a registered function. This yields a 5-10% speed benefit depending on script operator usage. Scripts running tight loops will see significant speed-up.
+* Common assignment operators (e.g. `+=`, `%=`) now call into highly efficient built-in implementations for standard types (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and `ImmutableString`) if not overridden by a registered function.
+* Implementations of common operators for standard types are removed from the `ArithmeticPackage` and `LogicPackage` (and therefore the `CorePackage`) because they are now always available, even under `Engine::new_raw`.
+* Operator-assignment statements (e.g. `+=`) are now handled directly and much faster.
+* Strings are now _immutable_ and use the `rhai::ImmutableString` type, eliminating large amounts of cloning.
+* For Native Rust functions taking a first `&mut` parameter, the first argument is passed by reference instead of by value, even if not called in method-call style. This allows many functions declared with `&mut` parameter to avoid excessive cloning. For example, if `a` is a large array, getting its length in this manner: `len(a)` used to result in a full clone of `a` before taking the length and throwing the copy away. Now, `a` is simply passed by reference, avoiding the cloning altogether.
+* A custom hasher simply passes through `u64` keys without hashing to avoid function call hash keys (which are by themselves `u64`) being hashed twice.
+
+
+Version 0.14.1
+==============
+
+The major features for this release is modules, script resource limits, and speed improvements
+(mainly due to avoiding allocations).
+
+New features
+------------
+
+* Modules and _module resolvers_ allow loading external scripts under a module namespace. A module can contain constant variables, Rust functions and Rhai functions.
+* `export` variables and `private` functions.
+* _Indexers_ for Rust types.
+* Track script evaluation progress and terminate script run.
+* Set limit on maximum number of operations allowed per script run.
+* Set limit on maximum number of modules loaded per script run.
+* A new API, `Engine::compile_scripts_with_scope`, can compile a list of script segments without needing to first concatenate them together into one large string.
+* Stepped `range` function with a custom step.
+
+Speed improvements
+------------------
+
+### `StaticVec`
+
+A script contains many lists - statements in a block, arguments to a function call etc.
+In a typical script, most of these lists tend to be short - e.g. the vast majority of function calls contain
+fewer than 4 arguments, while most statement blocks have fewer than 4-5 statements, with one or two being
+the most common. Before, dynamic `Vec`'s are used to hold these short lists for very brief periods of time,
+causing allocations churn.
+
+In this version, large amounts of allocations are avoided by converting to a `StaticVec` -
+a list type based on a static array for a small number of items (currently four) -
+wherever possible plus other tricks. Most real-life scripts should see material speed increases.
+
+### Pre-computed variable lookups
+
+Almost all variable lookups, as well as lookups in loaded modules, are now pre-computed.
+A variable's name is almost never used to search for the variable in the current scope.
+
+_Getters_ and _setter_ function names are also pre-computed and cached, so no string allocations are
+performed during a property get/set call.
+
+### Pre-computed function call hashes
+
+Lookup of all function calls, including Rust and Rhai ones, are now through pre-computed hashes.
+The function name is no longer used to search for a function, making function call dispatches
+much faster.
+
+### Large Boxes for expressions and statements
+
+The expression (`Expr`) and statement (`Stmt`) types are modified so that all of the variants contain only
+one single `Box` to a large allocated structure containing _all_ the fields. This makes the `Expr` and
+`Stmt` types very small (only one single pointer) and improves evaluation speed due to cache efficiency.
+
+Error handling
+--------------
+
+Previously, when an error occurs inside a function call, the error position reported is the function
+call site. This makes it difficult to diagnose the actual location of the error within the function.
+
+A new error variant `EvalAltResult::ErrorInFunctionCall` is added in this version.
+It wraps the internal error returned by the called function, including the error position within the function.
diff --git a/rhai/Cargo.toml b/rhai/Cargo.toml
new file mode 100644
index 0000000..14867da
--- /dev/null
+++ b/rhai/Cargo.toml
@@ -0,0 +1,159 @@
+[workspace]
+members = [".", "codegen"]
+
+[package]
+name = "rhai"
+version = "1.15.0"
+rust-version = "1.61.0"
+edition = "2018"
+resolver = "2"
+authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung", "jhwgh1968"]
+description = "Embedded scripting for Rust"
+homepage = "https://rhai.rs"
+repository = "https://github.com/rhaiscript"
+readme = "README.md"
+license = "MIT OR Apache-2.0"
+include = ["/src/**/*", "/Cargo.toml", "/README.md", "LICENSE*"]
+keywords = ["scripting", "scripting-engine", "scripting-language", "embedded"]
+categories = ["no-std", "embedded", "wasm", "parser-implementations"]
+
+[dependencies]
+smallvec = { version = "1.7", default-features = false, features = ["union", "const_new", "const_generics"] }
+ahash = { version = "0.8.2", default-features = false, features = ["compile-time-rng"] }
+num-traits = { version = "0.2", default-features = false }
+bitflags = { version = "1", default-features = false }
+smartstring = { version = "1", default-features = false }
+rhai_codegen = { version = "1.5.0", path = "codegen", default-features = false }
+
+no-std-compat = { git = "https://gitlab.com/jD91mZM2/no-std-compat", version = "0.4.1", default-features = false, features = ["alloc"], optional = true }
+libm = { version = "0.2", default-features = false, optional = true }
+hashbrown = { version = "0.13", optional = true }
+core-error = { version = "0.0", default-features = false, features = ["alloc"], optional = true }
+serde = { version = "1.0", default-features = false, features = ["derive", "alloc"], optional = true }
+serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true }
+unicode-xid = { version = "0.2", default-features = false, optional = true }
+rust_decimal = { version = "1.16", default-features = false, features = ["maths"], optional = true }
+getrandom = { version = "0.2", optional = true }
+rustyline = { version = "11", optional = true }
+document-features = { version = "0.2", optional = true }
+
+[dev-dependencies]
+rmp-serde = "1.1"
+serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
+
+[features]
+
+## Default features: `std`, uses runtime random numbers for hashing.
+default = ["std", "ahash/runtime-rng"] # ahash/runtime-rng trumps ahash/compile-time-rng
+## Standard features: uses compile-time random number for hashing.
+std = ["ahash/std", "num-traits/std", "smartstring/std"]
+
+#! ### Enable Special Functionalities
+
+ ## Require that all data types implement `Send + Sync` (for multi-threaded usage).
+ sync = []
+ ## Add support for the [`Decimal`](https://crates.io/crates/rust_decimal) data type (acts as the system floating-point type under `no_float`).
+ decimal = ["rust_decimal"]
+ ## Enable serialization/deserialization of Rhai data types via [`serde`](https://crates.io/crates/serde).
+ serde = ["dep:serde", "smartstring/serde", "smallvec/serde"]
+ ## Allow [Unicode Standard Annex #31](https://unicode.org/reports/tr31/) for identifiers.
+ unicode-xid-ident = ["unicode-xid"]
+ ## Enable functions metadata (including doc-comments); implies [`serde`](#feature-serde).
+ metadata = ["serde", "serde_json", "rhai_codegen/metadata", "smartstring/serde"]
+ ## Expose internal data structures (e.g. `AST` nodes).
+ internals = []
+ ## Enable the debugging interface (implies [`internals`](#feature-internals)).
+ debugging = ["internals"]
+ ## Features and dependencies required by `bin` tools: `decimal`, `metadata`, `serde`, `debugging` and [`rustyline`](https://crates.io/crates/rustyline).
+ bin-features = ["decimal", "metadata", "serde", "debugging", "rustyline"]
+
+#! ### System Configuration Features
+
+ ## Use `f32` instead of `f64` as the system floating-point number type.
+ f32_float = []
+ ## Use `i32` instead of `i64` for the system integer number type (useful for 32-bit architectures).
+ ## All other integer types (e.g. `u8`) are disabled.
+ only_i32 = []
+ ## Disable all integer types (e.g. `u8`) other than `i64`.
+ only_i64 = []
+
+#! ### Disable Language Features
+
+ ## Remove support for floating-point numbers.
+ no_float = []
+ ## Remove support for arrays and indexing.
+ no_index = []
+ ## Remove support for custom types, properties, method-style calls and object maps.
+ no_object = []
+ ## Remove support for time-stamps.
+ no_time = []
+ ## Remove support for script-defined functions (implies [`no_closure`](#feature-no_closure)).
+ no_function = ["no_closure"]
+ ## Remove support for capturing external variables in anonymous functions (i.e. closures).
+ no_closure = []
+ ## Remove support for loading external modules.
+ no_module = []
+ ## Remove support for custom syntax.
+ no_custom_syntax = []
+
+#! ### Performance-Related Features
+
+ ## Disable all safety checks.
+ unchecked = []
+ ## Do not track position when parsing.
+ no_position = []
+ ## Disable the script optimizer.
+ no_optimize = []
+
+#! ### Compiling for `no-std`
+
+ ## Turn on `no-std` compilation (nightly only).
+ no_std = ["no-std-compat", "num-traits/libm", "core-error", "libm", "hashbrown", "no_time"]
+
+#! ### JavaScript Interface for WASM
+
+ ## Use [`wasm-bindgen`](https://crates.io/crates/wasm-bindgen) as JavaScript interface.
+ wasm-bindgen = ["getrandom/js", "instant/wasm-bindgen"]
+ ## Use [`stdweb`](https://crates.io/crates/stdweb) as JavaScript interface.
+ stdweb = ["getrandom/js", "instant/stdweb"]
+
+#! ### Features used in testing environments only
+
+ ## Running under a testing environment.
+ testing-environ = []
+
+[[bin]]
+name = "rhai-repl"
+required-features = ["rustyline"]
+
+[[bin]]
+name = "rhai-run"
+
+[[bin]]
+name = "rhai-dbg"
+required-features = ["debugging"]
+
+[[example]]
+name = "serde"
+required-features = ["serde"]
+
+[[example]]
+name = "definitions"
+required-features = ["metadata", "internals"]
+
+[profile.release]
+lto = "fat"
+codegen-units = 1
+#opt-level = "z" # optimize for size
+#panic = 'abort' # remove stack backtrace for no-std
+
+[target.'cfg(target_family = "wasm")'.dependencies]
+instant = { version = "0.1.10" } # WASM implementation of std::time::Instant
+
+[package.metadata.docs.rs]
+features = ["document-features", "metadata", "serde", "internals", "decimal", "debugging"]
+
+[patch.crates-io]
+# Notice that a custom modified version of `rustyline` is used which supports bracketed paste on Windows.
+# This can be moved to the official version when bracketed paste is added.
+rustyline = { git = "https://github.com/schungx/rustyline", branch = "v11" }
diff --git a/rhai/LICENSE-APACHE.txt b/rhai/LICENSE-APACHE.txt
new file mode 100644
index 0000000..d9a10c0
--- /dev/null
+++ b/rhai/LICENSE-APACHE.txt
@@ -0,0 +1,176 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/rhai/LICENSE-MIT.txt b/rhai/LICENSE-MIT.txt
new file mode 100644
index 0000000..31aa793
--- /dev/null
+++ b/rhai/LICENSE-MIT.txt
@@ -0,0 +1,23 @@
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/rhai/README.md b/rhai/README.md
new file mode 100644
index 0000000..13527b5
--- /dev/null
+++ b/rhai/README.md
@@ -0,0 +1,150 @@
+Rhai - Embedded Scripting for Rust
+==================================
+
+
+[](https://github.com/rhaiscript/rhai/actions)
+[](https://github.com/rhaiscript/rhai)
+[](https://github.com/license/rhaiscript/rhai)
+[](https://crates.io/crates/rhai/)
+[](https://crates.io/crates/rhai/)
+[](https://docs.rs/rhai/)
+[](https://marketplace.visualstudio.com/items?itemName=rhaiscript.vscode-rhai)
+[](https://packagecontrol.io/packages/Rhai)
+[](https://discord.gg/HquqbYFcZ9)
+[](https://rhaiscript.zulipchat.com)
+[](https://www.reddit.com/r/Rhai)
+
+[](https://rhai.rs)
+
+Rhai is an embedded scripting language and evaluation engine for Rust that gives a safe and easy way
+to add scripting to any application.
+
+
+Targets and builds
+------------------
+
+* All CPU and O/S targets supported by Rust, including:
+ * WebAssembly (WASM)
+ * `no-std`
+* Minimum Rust version 1.61.0
+
+
+Standard features
+-----------------
+
+* Simple language similar to JavaScript+Rust with [dynamic](https://rhai.rs/book/language/dynamic.html) typing.
+* Fairly efficient evaluation (1 million iterations in 0.14 sec on a single-core 2.6 GHz Linux VM).
+* Tight integration with native Rust [functions](https://rhai.rs/book/rust/functions.html) and [types](https://rhai.rs/book/rust/custom-types.html), including [getters/setters](https://rhai.rs/book/rust/getters-setters.html), [methods](https://rhai.rs/book/rust/methods.html) and [indexers](https://rhai.rs/book/rust/indexers.html).
+* Freely pass Rust values into a script as [variables](https://rhai.rs/book/language/variables.html)/[constants](https://rhai.rs/book/language/constants.html) via an external [`Scope`](https://rhai.rs/book/engine/scope.html) - all clonable Rust types are supported; no need to implement any special trait. Or tap directly into the [variable resolution process](https://rhai.rs/book/engine/var.html).
+* Built-in support for most common [data types](https://rhai.rs/book/language/values-and-types.html) including booleans, [integers](https://rhai.rs/book/language/numbers.html), [floating-point numbers](https://rhai.rs/book/language/numbers.html) (including [`Decimal`](https://crates.io/crates/rust_decimal)), [strings](https://rhai.rs/book/language/strings-chars.html), [Unicode characters](https://rhai.rs/book/language/strings-chars.html), [arrays](https://rhai.rs/book/language/arrays.html) (including packed [byte arrays](https://rhai.rs/book/language/blobs.html)) and [object maps](https://rhai.rs/book/language/object-maps.html).
+* Easily [call a script-defined function](https://rhai.rs/book/engine/call-fn.html) from Rust.
+* Relatively little `unsafe` code (yes there are some for performance reasons).
+* Few dependencies - currently only [`smallvec`](https://crates.io/crates/smallvec), [`num-traits`](https://crates.io/crates/num-traits), [`ahash`](https://crates.io/crates/ahash), [`bitflags`](https://crates.io/crates/bitflags) and [`smartstring`](https://crates.io/crates/smartstring).
+* Re-entrant scripting engine can be made `Send + Sync` (via the `sync` feature).
+* Compile once to [AST](https://rhai.rs/book/engine/compile.html) form for repeated evaluations.
+* Scripts are [optimized](https://rhai.rs/book/engine/optimize) (useful for template-based machine-generated scripts).
+* Easy custom API development via [plugins](https://rhai.rs/book/plugins) system powered by procedural macros.
+* [Function overloading](https://rhai.rs/book/language/overload.html) and [operator overloading](https://rhai.rs/book/rust/operators.html).
+* Dynamic dispatch via [function pointers](https://rhai.rs/book/language/fn-ptr.html) with additional support for [currying](https://rhai.rs/book/language/fn-curry.html).
+* [Closures](https://rhai.rs/book/language/fn-closure.html) (anonymous functions) that can capture shared values.
+* Some syntactic support for [object-oriented programming (OOP)](https://rhai.rs/book/patterns/oop.html).
+* Organize code base with dynamically-loadable [modules](https://rhai.rs/book/language/modules), optionally [overriding the resolution process](https://rhai.rs/book/rust/modules/resolvers.html).
+* Serialization/deserialization support via [serde](https://crates.io/crates/serde) (requires the `serde` feature).
+* Support for [minimal builds](https://rhai.rs/book/start/builds/minimal.html) by excluding unneeded language [features](https://rhai.rs/book/start/features.html).
+* A [debugging](https://rhai.rs/book/engine/debugging) interface.
+
+
+Protected against attacks
+-------------------------
+
+* [_Don't Panic_](https://rhai.rs/book/safety/index.html#dont-panic-guarantee--any-panic-is-a-bug) guarantee - Any panic is a bug. Rhai subscribes to the motto that a library should never panic the host system, and is coded with this in mind.
+* [Sand-boxed](https://rhai.rs/book/safety/sandbox.html) - the scripting engine, if declared immutable, cannot mutate the containing environment unless [explicitly permitted](https://rhai.rs/book/patterns/control.html).
+* Rugged - protected against malicious attacks (such as [stack-overflow](https://rhai.rs/book/safety/max-call-stack.html), [over-sized data](https://rhai.rs/book/safety/max-string-size.html), and [runaway scripts](https://rhai.rs/book/safety/max-operations.html) etc.) that may come from untrusted third-party user-land scripts.
+* Track script evaluation [progress](https://rhai.rs/book/safety/progress.html) and manually terminate a script run.
+* Passes Miri.
+
+
+For those who actually want their own language
+----------------------------------------------
+
+* Use as a [DSL](https://rhai.rs/book/engine/dsl.html).
+* Disable certain [language features](https://rhai.rs/book/engine/options.html#language-features) such as [looping](https://rhai.rs/book/engine/disable-looping.html).
+* Further restrict the language by surgically [disabling keywords and operators](https://rhai.rs/book/engine/disable-keywords.html).
+* Define [custom operators](https://rhai.rs/book/engine/custom-op.html).
+* Extend the language with [custom syntax](https://rhai.rs/book/engine/custom-syntax.html).
+
+
+Example
+-------
+
+The [`scripts`](https://github.com/rhaiscript/rhai/tree/master/scripts) subdirectory contains sample Rhai scripts.
+
+Below is the standard _Fibonacci_ example for scripting languages:
+
+```ts
+// This Rhai script calculates the n-th Fibonacci number using a
+// really dumb algorithm to test the speed of the scripting engine.
+
+const TARGET = 28;
+const REPEAT = 5;
+const ANSWER = 317_811;
+
+fn fib(n) {
+ if n < 2 {
+ n
+ } else {
+ fib(n-1) + fib(n-2)
+ }
+}
+
+print(`Running Fibonacci(${TARGET}) x ${REPEAT} times...`);
+print("Ready... Go!");
+
+let result;
+let now = timestamp();
+
+for n in 0..REPEAT {
+ result = fib(TARGET);
+}
+
+print(`Finished. Run time = ${now.elapsed} seconds.`);
+
+print(`Fibonacci number #${TARGET} = ${result}`);
+
+if result != ANSWER {
+ print(`The answer is WRONG! Should be ${ANSWER}!`);
+}
+```
+
+Project Site
+------------
+
+[`rhai.rs`](https://rhai.rs)
+
+
+Documentation
+-------------
+
+See [_The Rhai Book_](https://rhai.rs/book) for details on the Rhai scripting engine and language.
+
+
+Playground
+----------
+
+An [_Online Playground_](https://rhai.rs/playground) is available with syntax-highlighting editor,
+powered by WebAssembly.
+
+Scripts can be evaluated directly from the editor.
+
+
+License
+-------
+
+Licensed under either of the following, at your choice:
+
+* [Apache License, Version 2.0](https://github.com/rhaiscript/rhai/blob/master/LICENSE-APACHE.txt), or
+* [MIT license](https://github.com/rhaiscript/rhai/blob/master/LICENSE-MIT.txt)
+
+Unless explicitly stated otherwise, any contribution intentionally submitted
+for inclusion in this crate, as defined in the Apache-2.0 license, shall
+be dual-licensed as above, without any additional terms or conditions.
diff --git a/rhai/benches/README.md b/rhai/benches/README.md
new file mode 100644
index 0000000..1c6d87c
--- /dev/null
+++ b/rhai/benches/README.md
@@ -0,0 +1,14 @@
+Micro Benchmarks
+================
+
+Micro benchmarks to measure the speed of Rhai.
+
+As with all micro benchmarks, take these with a grain of salt.
+
+
+How to Run
+----------
+
+```bash
+cargo +nightly bench
+```
diff --git a/rhai/benches/engine.rs b/rhai/benches/engine.rs
new file mode 100644
index 0000000..d725699
--- /dev/null
+++ b/rhai/benches/engine.rs
@@ -0,0 +1,40 @@
+#![feature(test)]
+
+///! Test evaluating expressions
+extern crate test;
+
+use rhai::{Array, Engine, Map, INT};
+use test::Bencher;
+
+#[bench]
+fn bench_engine_new(bench: &mut Bencher) {
+ bench.iter(|| Engine::new());
+}
+
+#[bench]
+fn bench_engine_new_raw(bench: &mut Bencher) {
+ bench.iter(|| Engine::new_raw());
+}
+
+#[bench]
+fn bench_engine_new_raw_core(bench: &mut Bencher) {
+ use rhai::packages::*;
+ let package = CorePackage::new();
+
+ bench.iter(|| {
+ let mut engine = Engine::new_raw();
+ package.register_into_engine(&mut engine);
+ });
+}
+
+#[bench]
+fn bench_engine_register_fn(bench: &mut Bencher) {
+ fn hello(_a: INT, _b: Array, _c: Map) -> bool {
+ true
+ }
+
+ bench.iter(|| {
+ let mut engine = Engine::new_raw();
+ engine.register_fn("hello", hello);
+ });
+}
diff --git a/rhai/benches/eval_array.rs b/rhai/benches/eval_array.rs
new file mode 100644
index 0000000..7287f22
--- /dev/null
+++ b/rhai/benches/eval_array.rs
@@ -0,0 +1,87 @@
+#![feature(test)]
+
+///! Test evaluating expressions
+extern crate test;
+
+use rhai::{Engine, OptimizationLevel};
+use test::Bencher;
+
+#[bench]
+fn bench_eval_array_small_get(bench: &mut Bencher) {
+ let script = "let x = [1, 2, 3, 4, 5]; x[3]";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_array_small_set(bench: &mut Bencher) {
+ let script = "let x = [1, 2, 3, 4, 5]; x[3] = 42;";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_array_large_get(bench: &mut Bencher) {
+ let script = r#"let x = [ 1, 2.345, "hello", true,
+ [ 1, 2, 3, [ "hey", [ "deeply", "nested" ], "jude" ] ]
+ ];
+ x[4][3][1][1]
+ "#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_array_large_set(bench: &mut Bencher) {
+ let script = r#"let x = [ 1, 2.345, "hello", true,
+ [ 1, 2, 3, [ "hey", [ "deeply", "nested" ], "jude" ] ]
+ ];
+ x[4][3][1][1] = 42
+ "#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_array_loop(bench: &mut Bencher) {
+ let script = "
+ let list = [];
+
+ for i in 0..10_000 {
+ list.push(i);
+ }
+
+ let sum = 0;
+
+ for i in list {
+ sum += i;
+ }
+ ";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
diff --git a/rhai/benches/eval_expression.rs b/rhai/benches/eval_expression.rs
new file mode 100644
index 0000000..dfcdbb0
--- /dev/null
+++ b/rhai/benches/eval_expression.rs
@@ -0,0 +1,235 @@
+#![feature(test)]
+
+///! Test evaluating expressions
+extern crate test;
+
+use rhai::{Engine, OptimizationLevel};
+use test::Bencher;
+
+#[bench]
+fn bench_eval_expression_single(bench: &mut Bencher) {
+ let script = "1";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile_expression(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_expression_number_literal(bench: &mut Bencher) {
+ let script = "2 > 1";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile_expression(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_expression_number_operators(bench: &mut Bencher) {
+ let script = "2 + 2 == 4";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile_expression(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_expression_optimized_simple(bench: &mut Bencher) {
+ let script = r#"
+ 2 > 1 &&
+ "something" != "nothing" ||
+ "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" &&
+ [array, has, spaces].len <= #{prop:name}.len &&
+ modifierTest + 1000 / 2 > (80 * 100 % 2)
+ "#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::Simple);
+ let ast = engine.compile_expression(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_expression_optimized_full(bench: &mut Bencher) {
+ let script = r#"
+ 2 > 1 &&
+ "something" != "nothing" ||
+ "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" &&
+ [array, has, spaces].len <= #{prop:name}.len &&
+ modifierTest + 1000 / 2 > (80 * 100 % 2)
+ "#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::Full);
+ let ast = engine.compile_expression(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_call_expression(bench: &mut Bencher) {
+ let script = r#"
+ 2 > 1 &&
+ "something" != "nothing" ||
+ "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" &&
+ [array, has, spaces].len <= #{prop:name}.len &&
+ modifierTest + 1000 / 2 > (80 * 100 % 2)
+ "#;
+
+ let engine = Engine::new();
+
+ bench.iter(|| engine.eval_expression::<bool>(script).unwrap());
+}
+
+#[bench]
+fn bench_eval_call(bench: &mut Bencher) {
+ let script = r#"
+ 2 > 1 &&
+ "something" != "nothing" ||
+ "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" &&
+ [array, has, spaces].len <= #{prop:name}.len &&
+ modifierTest + 1000 / 2 > (80 * 100 % 2)
+ "#;
+
+ let engine = Engine::new();
+
+ bench.iter(|| engine.eval::<bool>(script).unwrap());
+}
+
+#[bench]
+fn bench_eval_deeply_nested(bench: &mut Bencher) {
+ let script = r#"
+ (1 + 2 * 3 - 9) * 4 < 5 * 6 - 70 / 8 &&
+ (42 + 99 > 1 + 2 - 3 + 4 * 5 || 123 - 88 < 123 + 88 - 99 + 100)
+ && true
+ && !!!!!!!!false
+ "#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ bench.iter(|| engine.eval::<bool>(script).unwrap());
+}
+
+#[bench]
+fn bench_eval_loop_number(bench: &mut Bencher) {
+ let script = "
+ let s = 0;
+ for x in 0..10000 {
+ s += 1;
+ }
+ ";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_loop_strings_build(bench: &mut Bencher) {
+ let script = r#"
+ let s;
+ for x in 0..10000 {
+ s = "hello, world!" + "hello, world!";
+ }
+ "#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_loop_strings_no_build(bench: &mut Bencher) {
+ let script = r#"
+ let s;
+ for x in 0..10000 {
+ s = "hello" + "";
+ }
+ "#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_switch(bench: &mut Bencher) {
+ let script = "
+ let sum = 0;
+ let rem = 0;
+
+ for x in 0..10 {
+ rem = x % 10;
+
+ sum += switch rem {
+ 0 => 10,
+ 1 => 12,
+ 2 => 42,
+ 3 => 1,
+ 4 => 12,
+ 5 => 42,
+ 6 => 1,
+ 7 => 12,
+ 8 => 42,
+ 9 => 1,
+ }
+ }
+ ";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_nested_if(bench: &mut Bencher) {
+ let script = "
+ let sum = 0;
+ let rem = 0;
+
+ for x in 0..10 {
+ rem = x % 10;
+
+ sum += if rem == 0 { 10 }
+ else if rem == 1 { 12 }
+ else if rem == 2 { 42 }
+ else if rem == 3 { 1 }
+ else if rem == 4 { 12 }
+ else if rem == 5 { 42 }
+ else if rem == 6 { 1 }
+ else if rem == 7 { 12 }
+ else if rem == 8 { 42 }
+ else if rem == 9 { 1 };
+ }
+ ";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
diff --git a/rhai/benches/eval_map.rs b/rhai/benches/eval_map.rs
new file mode 100644
index 0000000..df49dcd
--- /dev/null
+++ b/rhai/benches/eval_map.rs
@@ -0,0 +1,71 @@
+#![feature(test)]
+
+///! Test evaluating expressions
+extern crate test;
+
+use rhai::{Engine, OptimizationLevel};
+use test::Bencher;
+
+#[bench]
+fn bench_eval_map_small_get(bench: &mut Bencher) {
+ let script = "let x = #{a:1}; x.a";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_map_small_set(bench: &mut Bencher) {
+ let script = "let x = #{a:1}; x.a = 42;";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_map_large_get(bench: &mut Bencher) {
+ let script = r#"let x = #{
+ a:1,
+ b:2.345,
+ c:"hello",
+ d: true,
+ e: #{ x: 42, "y$@#%": (), z: [ 1, 2, 3, #{}, #{ "hey": "jude" }]}
+ };
+ x["e"].z[4].hey
+ "#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_map_large_set(bench: &mut Bencher) {
+ let script = r#"let x = #{
+ a:1,
+ b:2.345,
+ c:"hello",
+ d: true,
+ e: #{ x: 42, "y$@#%": (), z: [ 1, 2, 3, #{}, #{ "hey": "jude" }]}
+ };
+ x["e"].z[4].hey = 42;
+ "#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
diff --git a/rhai/benches/eval_module.rs b/rhai/benches/eval_module.rs
new file mode 100644
index 0000000..55fb0fb
--- /dev/null
+++ b/rhai/benches/eval_module.rs
@@ -0,0 +1,53 @@
+#![feature(test)]
+
+///! Test evaluating with scope
+extern crate test;
+
+use rhai::{Engine, Module, OptimizationLevel, Scope};
+use test::Bencher;
+
+#[bench]
+fn bench_eval_module(bench: &mut Bencher) {
+ let script = "
+ fn foo(x) { x + 1 }
+ fn bar(x) { foo(x) }
+ ";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine).unwrap();
+
+ engine.register_static_module("testing", module.into());
+
+ let ast = engine
+ .compile(
+ "
+ fn foo(x) { x - 1 }
+ testing::bar(41)
+ ",
+ )
+ .unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_function_call(bench: &mut Bencher) {
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine
+ .compile(
+ "
+ fn foo(x) { x - 1 }
+ fn bar(x) { foo(x) }
+ bar(41)
+ ",
+ )
+ .unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
diff --git a/rhai/benches/eval_scope.rs b/rhai/benches/eval_scope.rs
new file mode 100644
index 0000000..2237fa8
--- /dev/null
+++ b/rhai/benches/eval_scope.rs
@@ -0,0 +1,77 @@
+#![feature(test)]
+
+///! Test evaluating with scope
+extern crate test;
+
+use rhai::{Engine, OptimizationLevel, Scope, INT};
+use test::Bencher;
+
+#[bench]
+fn bench_eval_scope_single(bench: &mut Bencher) {
+ let script = "requests_made == requests_made";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let mut scope = Scope::new();
+ scope.push("requests_made", 99 as INT);
+
+ let ast = engine.compile_expression(script).unwrap();
+
+ bench.iter(|| engine.run_ast_with_scope(&mut scope, &ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_scope_multiple(bench: &mut Bencher) {
+ let script = "requests_made > requests_succeeded";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let mut scope = Scope::new();
+ scope.push("requests_made", 99 as INT);
+ scope.push("requests_succeeded", 90 as INT);
+
+ let ast = engine.compile_expression(script).unwrap();
+
+ bench.iter(|| engine.run_ast_with_scope(&mut scope, &ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_scope_longer(bench: &mut Bencher) {
+ let script = "(requests_made * requests_succeeded / 100) >= 90";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let mut scope = Scope::new();
+ scope.push("requests_made", 99 as INT);
+ scope.push("requests_succeeded", 90 as INT);
+
+ let ast = engine.compile_expression(script).unwrap();
+
+ bench.iter(|| engine.run_ast_with_scope(&mut scope, &ast).unwrap());
+}
+
+#[bench]
+fn bench_eval_scope_complex(bench: &mut Bencher) {
+ let script = r#"
+ 2 > 1 &&
+ "something" != "nothing" ||
+ "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" &&
+ Variable_name_with_spaces <= variableName &&
+ modifierTest + 1000 / 2 > (80 * 100 % 2)
+ "#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let mut scope = Scope::new();
+ scope.push("Variable_name_with_spaces", 99 as INT);
+ scope.push("variableName", 90 as INT);
+ scope.push("modifierTest", 5 as INT);
+
+ let ast = engine.compile_expression(script).unwrap();
+
+ bench.iter(|| engine.run_ast_with_scope(&mut scope, &ast).unwrap());
+}
diff --git a/rhai/benches/eval_type.rs b/rhai/benches/eval_type.rs
new file mode 100644
index 0000000..7cee439
--- /dev/null
+++ b/rhai/benches/eval_type.rs
@@ -0,0 +1,100 @@
+#![feature(test)]
+
+///! Test evaluating expressions
+extern crate test;
+
+use rhai::{Engine, OptimizationLevel, Scope, INT};
+use test::Bencher;
+
+#[derive(Debug, Clone)]
+struct Test {
+ x: INT,
+}
+
+impl Test {
+ pub fn get_x(&mut self) -> INT {
+ self.x
+ }
+ pub fn action(&mut self) {
+ self.x = 0;
+ }
+ pub fn update(&mut self, val: INT) {
+ self.x = val;
+ }
+ pub fn get_nest(&mut self) -> Test {
+ Test { x: 9 }
+ }
+}
+
+#[bench]
+fn bench_type_field(bench: &mut Bencher) {
+ let script = "foo.field";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ engine.register_type_with_name::<Test>("Test");
+ engine.register_get("field", Test::get_x);
+
+ let ast = engine.compile_expression(script).unwrap();
+
+ let mut scope = Scope::new();
+ scope.push("foo", Test { x: 42 });
+
+ bench.iter(|| engine.run_ast_with_scope(&mut scope, &ast).unwrap());
+}
+
+#[bench]
+fn bench_type_method(bench: &mut Bencher) {
+ let script = "foo.action()";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ engine.register_type_with_name::<Test>("Test");
+ engine.register_fn("action", Test::action);
+
+ let ast = engine.compile_expression(script).unwrap();
+
+ let mut scope = Scope::new();
+ scope.push("foo", Test { x: 42 });
+
+ bench.iter(|| engine.run_ast_with_scope(&mut scope, &ast).unwrap());
+}
+
+#[bench]
+fn bench_type_method_with_params(bench: &mut Bencher) {
+ let script = "foo.update(1)";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ engine.register_type_with_name::<Test>("Test");
+ engine.register_fn("update", Test::update);
+
+ let ast = engine.compile_expression(script).unwrap();
+
+ let mut scope = Scope::new();
+ scope.push("foo", Test { x: 42 });
+
+ bench.iter(|| engine.run_ast_with_scope(&mut scope, &ast).unwrap());
+}
+
+#[bench]
+fn bench_type_method_nested(bench: &mut Bencher) {
+ let script = "foo.nest.field";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ engine.register_type_with_name::<Test>("Test");
+ engine.register_get("field", Test::get_x);
+ engine.register_get("nest", Test::get_nest);
+
+ let ast = engine.compile_expression(script).unwrap();
+
+ let mut scope = Scope::new();
+ scope.push("foo", Test { x: 42 });
+
+ bench.iter(|| engine.run_ast_with_scope(&mut scope, &ast).unwrap());
+}
diff --git a/rhai/benches/iterations.rs b/rhai/benches/iterations.rs
new file mode 100644
index 0000000..33ac414
--- /dev/null
+++ b/rhai/benches/iterations.rs
@@ -0,0 +1,78 @@
+#![feature(test)]
+
+///! Test 1,000 iterations
+extern crate test;
+
+use rhai::{Engine, OptimizationLevel, INT};
+use test::Bencher;
+
+#[bench]
+fn bench_iterations_1000(bench: &mut Bencher) {
+ let script = "
+ let x = 1_000;
+
+ while x > 0 {
+ x -= 1;
+ }
+ ";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_iterations_fibonacci(bench: &mut Bencher) {
+ let script = "
+ fn fibonacci(n) {
+ if n < 2 {
+ n
+ } else {
+ fibonacci(n-1) + fibonacci(n-2)
+ }
+ }
+
+ fibonacci(20)
+ ";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.eval_ast::<INT>(&ast).unwrap());
+}
+
+#[bench]
+fn bench_iterations_array(bench: &mut Bencher) {
+ let script = "
+ let x = [];
+ x.pad(1000, 0);
+ for i in 0..1000 { x[i] = i % 256; }
+ ";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
+
+#[bench]
+fn bench_iterations_blob(bench: &mut Bencher) {
+ let script = "
+ let x = blob(1000, 0);
+ for i in 0..1000 { x[i] = i % 256; }
+ ";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(script).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
diff --git a/rhai/benches/parsing.rs b/rhai/benches/parsing.rs
new file mode 100644
index 0000000..d45b225
--- /dev/null
+++ b/rhai/benches/parsing.rs
@@ -0,0 +1,136 @@
+#![feature(test)]
+
+///! Test parsing expressions
+extern crate test;
+
+use rhai::{Engine, OptimizationLevel};
+use test::Bencher;
+
+#[bench]
+fn bench_parse_single(bench: &mut Bencher) {
+ let script = "1";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ bench.iter(|| engine.compile_expression(script).unwrap());
+}
+
+#[bench]
+fn bench_parse_simple(bench: &mut Bencher) {
+ let script = "(requests_made * requests_succeeded / 100) >= 90";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ bench.iter(|| engine.compile_expression(script).unwrap());
+}
+
+#[bench]
+fn bench_parse_full(bench: &mut Bencher) {
+ let script = r#"
+ 2 > 1 &&
+ "something" != "nothing" ||
+ "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" &&
+ [array, has, spaces].len <= #{prop:name}.len &&
+ modifierTest + 1000 / 2 > (80 * 100 % 2)
+ "#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ bench.iter(|| engine.compile_expression(script).unwrap());
+}
+
+#[bench]
+fn bench_parse_array(bench: &mut Bencher) {
+ let script = r#"[1, 234.789, "hello", false, [ 9, 8, 7] ]"#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ bench.iter(|| engine.compile_expression(script).unwrap());
+}
+
+#[bench]
+fn bench_parse_map(bench: &mut Bencher) {
+ let script = r#"#{a: 1, b: 42, c: "hi", "dc%$& ": "strange", x: true, y: 123.456 }"#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ bench.iter(|| engine.compile_expression(script).unwrap());
+}
+
+#[bench]
+fn bench_parse_primes(bench: &mut Bencher) {
+ let script = "
+ // This script uses the Sieve of Eratosthenes to calculate prime numbers.
+
+ let now = timestamp();
+
+ const MAX_NUMBER_TO_CHECK = 10_000; // 1229 primes <= 10000
+
+ let prime_mask = [];
+ prime_mask.pad(MAX_NUMBER_TO_CHECK, true);
+
+ prime_mask[0] = false;
+ prime_mask[1] = false;
+
+ let total_primes_found = 0;
+
+ for p in 2..MAX_NUMBER_TO_CHECK {
+ if prime_mask[p] {
+ print(p);
+
+ total_primes_found += 1;
+ let i = 2 * p;
+
+ while i < MAX_NUMBER_TO_CHECK {
+ prime_mask[i] = false;
+ i += p;
+ }
+ }
+ }
+
+ print(`Total ${total_primes_found} primes.`);
+ print(`Run time = ${now.elapsed} seconds.`);
+ ";
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ bench.iter(|| engine.compile(script).unwrap());
+}
+
+#[bench]
+fn bench_parse_optimize_simple(bench: &mut Bencher) {
+ let script = r#"
+ 2 > 1 &&
+ "something" != "nothing" ||
+ "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" &&
+ [array, has, spaces].len <= #{prop:name}.len &&
+ modifierTest + 1000 / 2 > (80 * 100 % 2)
+ "#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::Simple);
+
+ bench.iter(|| engine.compile_expression(script).unwrap());
+}
+
+#[bench]
+fn bench_parse_optimize_full(bench: &mut Bencher) {
+ let script = r#"
+ 2 > 1 &&
+ "something" != "nothing" ||
+ "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" &&
+ [array, has, spaces].len <= #{prop:name}.len &&
+ modifierTest + 1000 / 2 > (80 * 100 % 2)
+ "#;
+
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::Full);
+
+ bench.iter(|| engine.compile_expression(script).unwrap());
+}
diff --git a/rhai/benches/primes.rs b/rhai/benches/primes.rs
new file mode 100644
index 0000000..40c047a
--- /dev/null
+++ b/rhai/benches/primes.rs
@@ -0,0 +1,43 @@
+#![feature(test)]
+
+///! Test evaluating expressions
+extern crate test;
+
+use rhai::{Engine, OptimizationLevel};
+use test::Bencher;
+
+// This script uses the Sieve of Eratosthenes to calculate prime numbers.
+
+const SCRIPT: &str = "
+ const MAX_NUMBER_TO_CHECK = 1_000; // 168 primes <= 1000
+
+ let prime_mask = [];
+ prime_mask.pad(MAX_NUMBER_TO_CHECK, true);
+
+ prime_mask[0] = false;
+ prime_mask[1] = false;
+
+ let total_primes_found = 0;
+
+ for p in 2..MAX_NUMBER_TO_CHECK {
+ if prime_mask[p] {
+ total_primes_found += 1;
+ let i = 2 * p;
+
+ while i < MAX_NUMBER_TO_CHECK {
+ prime_mask[i] = false;
+ i += p;
+ }
+ }
+ }
+";
+
+#[bench]
+fn bench_eval_primes(bench: &mut Bencher) {
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::None);
+
+ let ast = engine.compile(SCRIPT).unwrap();
+
+ bench.iter(|| engine.run_ast(&ast).unwrap());
+}
diff --git a/rhai/build.rs b/rhai/build.rs
new file mode 100644
index 0000000..39c1123
--- /dev/null
+++ b/rhai/build.rs
@@ -0,0 +1,26 @@
+use std::{
+ env,
+ fs::File,
+ io::{Read, Write},
+};
+
+fn main() {
+ // Tell Cargo that if the given environment variable changes, to rerun this build script.
+ println!("cargo:rerun-if-changed=build.template");
+ println!("cargo:rerun-if-env-changed=RHAI_AHASH_SEED");
+ let mut contents = String::new();
+
+ File::open("build.template")
+ .expect("cannot open `build.template`")
+ .read_to_string(&mut contents)
+ .expect("cannot read from `build.template`");
+
+ let seed = env::var("RHAI_AHASH_SEED").map_or_else(|_| "None".into(), |s| format!("Some({s})"));
+
+ contents = contents.replace("{{AHASH_SEED}}", &seed);
+
+ File::create("src/config/hashing_env.rs")
+ .expect("cannot create `config.rs`")
+ .write_all(contents.as_bytes())
+ .expect("cannot write to `config/hashing_env.rs`");
+}
diff --git a/rhai/build.template b/rhai/build.template
new file mode 100644
index 0000000..783764e
--- /dev/null
+++ b/rhai/build.template
@@ -0,0 +1,3 @@
+//! This file is automatically recreated during build time by `build.rs` from `build.template`.
+
+pub const AHASH_SEED: Option<[u64; 4]> = {{AHASH_SEED}};
diff --git a/rhai/codegen/Cargo.toml b/rhai/codegen/Cargo.toml
new file mode 100644
index 0000000..8a1495c
--- /dev/null
+++ b/rhai/codegen/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "rhai_codegen"
+version = "1.5.0"
+edition = "2018"
+resolver = "2"
+authors = ["jhwgh1968", "Stephen Chung"]
+description = "Procedural macros support package for Rhai, a scripting language and engine for Rust"
+keywords = ["scripting", "scripting-engine", "scripting-language", "embedded", "plugin"]
+categories = ["no-std", "embedded", "wasm", "parser-implementations"]
+homepage = "https://rhai.rs/book/plugins/index.html"
+repository = "https://github.com/rhaiscript/rhai"
+license = "MIT OR Apache-2.0"
+
+[lib]
+proc-macro = true
+
+[features]
+default = []
+metadata = []
+
+[dependencies]
+proc-macro2 = "1"
+syn = { version = "1.0", features = ["full", "parsing", "printing", "proc-macro", "extra-traits"] }
+quote = "1"
+
+[dev-dependencies]
+rhai = { path = "..", version = "1.12", features = ["metadata"] }
+trybuild = "1"
diff --git a/rhai/codegen/README.md b/rhai/codegen/README.md
new file mode 100644
index 0000000..075f292
--- /dev/null
+++ b/rhai/codegen/README.md
@@ -0,0 +1,10 @@
+Procedural Macros for Plugins
+=============================
+
+
+
+This crate holds procedural macros for code generation, supporting the plugins system
+for [Rhai](https://github.com/rhaiscript/rhai).
+
+This crate is automatically referenced by the [Rhai crate](https://crates.io/crates/rhai).
+Normally it should not be used directly.
diff --git a/rhai/codegen/src/attrs.rs b/rhai/codegen/src/attrs.rs
new file mode 100644
index 0000000..8e232a7
--- /dev/null
+++ b/rhai/codegen/src/attrs.rs
@@ -0,0 +1,195 @@
+use proc_macro2::{Ident, Span, TokenStream};
+use syn::{
+ parse::{ParseStream, Parser},
+ spanned::Spanned,
+};
+
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub enum ExportScope {
+ PubOnly,
+ Prefix(String),
+ All,
+}
+
+impl Default for ExportScope {
+ fn default() -> ExportScope {
+ ExportScope::PubOnly
+ }
+}
+
+pub trait ExportedParams: Sized {
+ fn parse_stream(args: ParseStream) -> syn::Result<Self>;
+ fn no_attrs() -> Self;
+ fn from_info(info: ExportInfo) -> syn::Result<Self>;
+}
+
+#[derive(Debug, Clone)]
+pub struct AttrItem {
+ pub key: Ident,
+ pub value: Option<syn::LitStr>,
+ pub span: Span,
+}
+
+#[derive(Debug, Clone)]
+pub struct ExportInfo {
+ pub item_span: Span,
+ pub items: Vec<AttrItem>,
+}
+
+pub fn parse_attr_items(args: ParseStream) -> syn::Result<ExportInfo> {
+ if args.is_empty() {
+ return Ok(ExportInfo {
+ item_span: args.span(),
+ items: Vec::new(),
+ });
+ }
+ let arg_list = args.call(syn::punctuated::Punctuated::parse_separated_nonempty)?;
+
+ parse_punctuated_items(arg_list)
+}
+
+pub fn parse_punctuated_items(
+ arg_list: syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>,
+) -> syn::Result<ExportInfo> {
+ let list_span = arg_list.span();
+
+ let mut attrs = Vec::new();
+
+ for arg in arg_list {
+ let arg_span = arg.span();
+ let (key, value) = match arg {
+ syn::Expr::Assign(syn::ExprAssign {
+ ref left,
+ ref right,
+ ..
+ }) => {
+ let attr_name = match left.as_ref() {
+ syn::Expr::Path(syn::ExprPath {
+ path: attr_path, ..
+ }) => attr_path.get_ident().cloned().ok_or_else(|| {
+ syn::Error::new(attr_path.span(), "expecting attribute name")
+ })?,
+ x => return Err(syn::Error::new(x.span(), "expecting attribute name")),
+ };
+ let attr_value = match right.as_ref() {
+ syn::Expr::Lit(syn::ExprLit {
+ lit: syn::Lit::Str(string),
+ ..
+ }) => string.clone(),
+ x => return Err(syn::Error::new(x.span(), "expecting string literal")),
+ };
+ (attr_name, Some(attr_value))
+ }
+ syn::Expr::Path(syn::ExprPath { path, .. }) => path
+ .get_ident()
+ .cloned()
+ .map(|a| (a, None))
+ .ok_or_else(|| syn::Error::new(path.span(), "expecting attribute name"))?,
+ x => return Err(syn::Error::new(x.span(), "expecting identifier")),
+ };
+ attrs.push(AttrItem {
+ key,
+ value,
+ span: arg_span,
+ });
+ }
+
+ Ok(ExportInfo {
+ item_span: list_span,
+ items: attrs,
+ })
+}
+
+pub fn outer_item_attributes<T: ExportedParams>(
+ args: TokenStream,
+ _attr_name: &str,
+) -> syn::Result<T> {
+ if args.is_empty() {
+ return Ok(T::no_attrs());
+ }
+
+ let arg_list = syn::punctuated::Punctuated::parse_separated_nonempty.parse2(args)?;
+
+ T::from_info(parse_punctuated_items(arg_list)?)
+}
+
+pub fn inner_item_attributes<T: ExportedParams>(
+ attrs: &mut Vec<syn::Attribute>,
+ attr_name: &str,
+) -> syn::Result<T> {
+ // Find the #[rhai_fn] attribute which will turn be read for function parameters.
+ if let Some(index) = attrs
+ .iter()
+ .position(|a| a.path.get_ident().map_or(false, |i| *i == attr_name))
+ {
+ let rhai_fn_attr = attrs.remove(index);
+
+ // Cannot have more than one #[rhai_fn]
+ if let Some(duplicate) = attrs
+ .iter()
+ .find(|a| a.path.get_ident().map_or(false, |i| *i == attr_name))
+ {
+ return Err(syn::Error::new(
+ duplicate.span(),
+ format!("duplicated attribute '{attr_name}'"),
+ ));
+ }
+
+ rhai_fn_attr.parse_args_with(T::parse_stream)
+ } else {
+ Ok(T::no_attrs())
+ }
+}
+
+#[cfg(feature = "metadata")]
+pub fn doc_attributes(attrs: &[syn::Attribute]) -> syn::Result<Vec<String>> {
+ // Find the #[doc] attribute which will turn be read for function documentation.
+ let mut comments = Vec::new();
+ let mut buf = String::new();
+
+ for attr in attrs {
+ if let Some(i) = attr.path.get_ident() {
+ if *i == "doc" {
+ if let syn::Meta::NameValue(syn::MetaNameValue {
+ lit: syn::Lit::Str(s),
+ ..
+ }) = attr.parse_meta()?
+ {
+ let mut line = s.value();
+
+ if line.contains('\n') {
+ // Must be a block comment `/** ... */`
+ if !buf.is_empty() {
+ comments.push(buf.clone());
+ buf.clear();
+ }
+ line.insert_str(0, "/**");
+ line.push_str("*/");
+ comments.push(line);
+ } else {
+ // Single line - assume it is `///`
+ if !buf.is_empty() {
+ buf.push('\n');
+ }
+ buf.push_str("///");
+ buf.push_str(&line);
+ }
+ }
+ }
+ }
+ }
+
+ if !buf.is_empty() {
+ comments.push(buf);
+ }
+
+ Ok(comments)
+}
+
+pub fn collect_cfg_attr(attrs: &[syn::Attribute]) -> Vec<syn::Attribute> {
+ attrs
+ .iter()
+ .filter(|&a| a.path.get_ident().map_or(false, |i| *i == "cfg"))
+ .cloned()
+ .collect()
+}
diff --git a/rhai/codegen/src/function.rs b/rhai/codegen/src/function.rs
new file mode 100644
index 0000000..27ec31c
--- /dev/null
+++ b/rhai/codegen/src/function.rs
@@ -0,0 +1,875 @@
+use proc_macro2::{Span, TokenStream};
+use quote::{quote, quote_spanned, ToTokens};
+use syn::{
+ parse::{Parse, ParseStream},
+ spanned::Spanned,
+};
+
+#[cfg(no_std)]
+use alloc::format;
+#[cfg(not(no_std))]
+use std::format;
+
+use std::borrow::Cow;
+
+use crate::attrs::{ExportInfo, ExportScope, ExportedParams};
+
+#[derive(Clone, Debug, Eq, PartialEq, Copy, Hash)]
+pub enum FnNamespaceAccess {
+ Unset,
+ Global,
+ Internal,
+}
+
+impl Default for FnNamespaceAccess {
+ fn default() -> Self {
+ Self::Unset
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Copy, Hash)]
+pub enum Index {
+ Get,
+ Set,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub enum Property {
+ Get(syn::Ident),
+ Set(syn::Ident),
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub enum FnSpecialAccess {
+ None,
+ Index(Index),
+ Property(Property),
+}
+
+impl Default for FnSpecialAccess {
+ fn default() -> FnSpecialAccess {
+ FnSpecialAccess::None
+ }
+}
+
+impl FnSpecialAccess {
+ pub fn get_fn_name(&self) -> Option<(String, String, Span)> {
+ match self {
+ FnSpecialAccess::None => None,
+ FnSpecialAccess::Property(Property::Get(ref g)) => {
+ Some((format!("{FN_GET}{g}"), g.to_string(), g.span()))
+ }
+ FnSpecialAccess::Property(Property::Set(ref s)) => {
+ Some((format!("{FN_SET}{s}"), s.to_string(), s.span()))
+ }
+ FnSpecialAccess::Index(Index::Get) => Some((
+ FN_IDX_GET.to_string(),
+ "index_get".to_string(),
+ Span::call_site(),
+ )),
+ FnSpecialAccess::Index(Index::Set) => Some((
+ FN_IDX_SET.to_string(),
+ "index_set".to_string(),
+ Span::call_site(),
+ )),
+ }
+ }
+}
+
+pub fn flatten_type_groups(ty: &syn::Type) -> &syn::Type {
+ match ty {
+ syn::Type::Group(syn::TypeGroup { ref elem, .. })
+ | syn::Type::Paren(syn::TypeParen { ref elem, .. }) => flatten_type_groups(elem.as_ref()),
+ _ => ty,
+ }
+}
+
+pub fn print_type(ty: &syn::Type) -> String {
+ ty.to_token_stream()
+ .to_string()
+ .replace(" , ", ", ")
+ .replace("& ", "&")
+ .replace(" :: ", "::")
+ .replace(" ( ", "(")
+ .replace(" ) ", ")")
+ .replace(" < ", "<")
+ .replace(" > ", ">")
+}
+
+#[derive(Debug, Default)]
+pub struct ExportedFnParams {
+ pub name: Vec<String>,
+ pub return_raw: Option<Span>,
+ pub pure: Option<Span>,
+ pub skip: bool,
+ pub special: FnSpecialAccess,
+ pub namespace: FnNamespaceAccess,
+ pub span: Option<Span>,
+}
+
+pub const FN_GET: &str = "get$";
+pub const FN_SET: &str = "set$";
+pub const FN_IDX_GET: &str = "index$get$";
+pub const FN_IDX_SET: &str = "index$set$";
+
+impl Parse for ExportedFnParams {
+ fn parse(args: ParseStream) -> syn::Result<Self> {
+ if args.is_empty() {
+ return Ok(ExportedFnParams::default());
+ }
+
+ let info = crate::attrs::parse_attr_items(args)?;
+ Self::from_info(info)
+ }
+}
+
+impl ExportedParams for ExportedFnParams {
+ fn parse_stream(args: ParseStream) -> syn::Result<Self> {
+ Self::parse(args)
+ }
+
+ fn no_attrs() -> Self {
+ Default::default()
+ }
+
+ fn from_info(info: crate::attrs::ExportInfo) -> syn::Result<Self> {
+ let ExportInfo {
+ item_span: span,
+ items: attrs,
+ } = info;
+ let mut name = Vec::new();
+ let mut return_raw = None;
+ let mut pure = None;
+ let mut skip = false;
+ let mut namespace = FnNamespaceAccess::Unset;
+ let mut special = FnSpecialAccess::None;
+ for attr in attrs {
+ let crate::attrs::AttrItem {
+ key,
+ value,
+ span: item_span,
+ } = attr;
+ match (key.to_string().as_ref(), value) {
+ ("get", None) | ("set", None) | ("name", None) => {
+ return Err(syn::Error::new(key.span(), "requires value"))
+ }
+ ("name", Some(s)) if s.value() == FN_IDX_GET => {
+ return Err(syn::Error::new(
+ item_span,
+ "use attribute 'index_get' instead",
+ ))
+ }
+ ("name", Some(s)) if s.value() == FN_IDX_SET => {
+ return Err(syn::Error::new(
+ item_span,
+ "use attribute 'index_set' instead",
+ ))
+ }
+ ("name", Some(s)) if s.value().starts_with(FN_GET) => {
+ return Err(syn::Error::new(
+ item_span,
+ format!(
+ "use attribute 'getter = \"{}\"' instead",
+ &s.value()[FN_GET.len()..]
+ ),
+ ))
+ }
+ ("name", Some(s)) if s.value().starts_with(FN_SET) => {
+ return Err(syn::Error::new(
+ item_span,
+ format!(
+ "use attribute 'setter = \"{}\"' instead",
+ &s.value()[FN_SET.len()..]
+ ),
+ ))
+ }
+ ("name", Some(s)) => name.push(s.value()),
+
+ ("index_get", Some(s))
+ | ("index_set", Some(s))
+ | ("return_raw", Some(s))
+ | ("pure", Some(s))
+ | ("skip", Some(s))
+ | ("global", Some(s))
+ | ("internal", Some(s)) => {
+ return Err(syn::Error::new(s.span(), "extraneous value"))
+ }
+
+ ("pure", None) => pure = Some(item_span),
+ ("return_raw", None) => return_raw = Some(item_span),
+ ("skip", None) => skip = true,
+ ("global", None) => match namespace {
+ FnNamespaceAccess::Unset => namespace = FnNamespaceAccess::Global,
+ FnNamespaceAccess::Global => (),
+ FnNamespaceAccess::Internal => {
+ return Err(syn::Error::new(
+ key.span(),
+ "namespace is already set to 'internal'",
+ ))
+ }
+ },
+ ("internal", None) => match namespace {
+ FnNamespaceAccess::Unset => namespace = FnNamespaceAccess::Internal,
+ FnNamespaceAccess::Internal => (),
+ FnNamespaceAccess::Global => {
+ return Err(syn::Error::new(
+ key.span(),
+ "namespace is already set to 'global'",
+ ))
+ }
+ },
+
+ ("get", Some(s)) => {
+ special = match special {
+ FnSpecialAccess::None => FnSpecialAccess::Property(Property::Get(
+ syn::Ident::new(&s.value(), s.span()),
+ )),
+ _ => return Err(syn::Error::new(item_span.span(), "conflicting getter")),
+ }
+ }
+ ("set", Some(s)) => {
+ special = match special {
+ FnSpecialAccess::None => FnSpecialAccess::Property(Property::Set(
+ syn::Ident::new(&s.value(), s.span()),
+ )),
+ _ => return Err(syn::Error::new(item_span.span(), "conflicting setter")),
+ }
+ }
+ ("index_get", None) => {
+ special = match special {
+ FnSpecialAccess::None => FnSpecialAccess::Index(Index::Get),
+ _ => {
+ return Err(syn::Error::new(item_span.span(), "conflicting index_get"))
+ }
+ }
+ }
+ ("index_set", None) => {
+ special = match special {
+ FnSpecialAccess::None => FnSpecialAccess::Index(Index::Set),
+ _ => {
+ return Err(syn::Error::new(item_span.span(), "conflicting index_set"))
+ }
+ }
+ }
+
+ (attr, ..) => {
+ return Err(syn::Error::new(
+ key.span(),
+ format!("unknown attribute '{attr}'"),
+ ))
+ }
+ }
+ }
+
+ Ok(ExportedFnParams {
+ name,
+ return_raw,
+ pure,
+ skip,
+ special,
+ namespace,
+ span: Some(span),
+ })
+ }
+}
+
+#[derive(Debug)]
+pub struct ExportedFn {
+ entire_span: Span,
+ signature: syn::Signature,
+ visibility: syn::Visibility,
+ pass_context: bool,
+ mut_receiver: bool,
+ params: ExportedFnParams,
+ cfg_attrs: Vec<syn::Attribute>,
+ #[cfg(feature = "metadata")]
+ comments: Vec<String>,
+}
+
+impl Parse for ExportedFn {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let fn_all: syn::ItemFn = input.parse()?;
+ let entire_span = fn_all.span();
+ let str_type_path = syn::parse2::<syn::Path>(quote! { str }).unwrap();
+
+ let context_type_path1 = syn::parse2::<syn::Path>(quote! { NativeCallContext }).unwrap();
+ let context_type_path2 =
+ syn::parse2::<syn::Path>(quote! { rhai::NativeCallContext }).unwrap();
+ let mut pass_context = false;
+
+ let cfg_attrs = crate::attrs::collect_cfg_attr(&fn_all.attrs);
+
+ let visibility = fn_all.vis;
+
+ // Determine if the function requires a call context
+ if let Some(syn::FnArg::Typed(syn::PatType { ref ty, .. })) = fn_all.sig.inputs.first() {
+ match flatten_type_groups(ty.as_ref()) {
+ syn::Type::Path(p)
+ if p.path == context_type_path1 || p.path == context_type_path2 =>
+ {
+ pass_context = true;
+ }
+ _ => {}
+ }
+ }
+
+ let skip_slots = usize::from(pass_context);
+
+ // Determine whether function generates a special calling convention for a mutable receiver.
+ let mut_receiver = match fn_all.sig.inputs.iter().nth(skip_slots) {
+ Some(syn::FnArg::Receiver(syn::Receiver {
+ reference: Some(_), ..
+ })) => true,
+ Some(syn::FnArg::Typed(syn::PatType { ref ty, .. })) => {
+ match flatten_type_groups(ty.as_ref()) {
+ syn::Type::Reference(syn::TypeReference {
+ mutability: Some(_),
+ ..
+ }) => true,
+ syn::Type::Reference(syn::TypeReference {
+ mutability: None,
+ ref elem,
+ ..
+ }) => match flatten_type_groups(elem.as_ref()) {
+ syn::Type::Path(ref p) if p.path == str_type_path => false,
+ _ => {
+ return Err(syn::Error::new(
+ ty.span(),
+ "references from Rhai in this position must be mutable",
+ ))
+ }
+ },
+ _ => false,
+ }
+ }
+ _ => false,
+ };
+
+ // All arguments after the first must be moved except for &str.
+ for arg in fn_all.sig.inputs.iter().skip(skip_slots + 1) {
+ let ty = match arg {
+ syn::FnArg::Typed(syn::PatType { ref ty, .. }) => ty,
+ _ => panic!("internal error: receiver argument outside of first position!?"),
+ };
+ let is_ok = match flatten_type_groups(ty.as_ref()) {
+ syn::Type::Reference(syn::TypeReference {
+ mutability: Some(_),
+ ..
+ }) => false,
+ syn::Type::Reference(syn::TypeReference {
+ mutability: None,
+ ref elem,
+ ..
+ }) => {
+ matches!(flatten_type_groups(elem.as_ref()), syn::Type::Path(ref p) if p.path == str_type_path)
+ }
+ syn::Type::Verbatim(..) => false,
+ _ => true,
+ };
+ if !is_ok {
+ return Err(syn::Error::new(
+ ty.span(),
+ "function parameters other than the first one cannot be passed by reference",
+ ));
+ }
+ }
+
+ // Check return type.
+ if let syn::ReturnType::Type(.., ref ret_type) = fn_all.sig.output {
+ match flatten_type_groups(ret_type.as_ref()) {
+ syn::Type::Ptr(..) => {
+ return Err(syn::Error::new(
+ fn_all.sig.output.span(),
+ "Rhai functions cannot return pointers",
+ ))
+ }
+ syn::Type::Reference(..) => {
+ return Err(syn::Error::new(
+ fn_all.sig.output.span(),
+ "Rhai functions cannot return references",
+ ))
+ }
+ _ => {}
+ }
+ }
+ Ok(ExportedFn {
+ entire_span,
+ signature: fn_all.sig,
+ visibility,
+ pass_context,
+ mut_receiver,
+ params: Default::default(),
+ cfg_attrs,
+ #[cfg(feature = "metadata")]
+ comments: Vec::new(),
+ })
+ }
+}
+
+impl ExportedFn {
+ #![allow(unused)]
+
+ pub fn params(&self) -> &ExportedFnParams {
+ &self.params
+ }
+
+ pub fn cfg_attrs(&self) -> &[syn::Attribute] {
+ &self.cfg_attrs
+ }
+
+ pub fn update_scope(&mut self, parent_scope: &ExportScope) {
+ let keep = match (self.params.skip, parent_scope) {
+ (true, ..) => false,
+ (.., ExportScope::PubOnly) => self.is_public(),
+ (.., ExportScope::Prefix(s)) => self.name().to_string().starts_with(s),
+ (.., ExportScope::All) => true,
+ };
+ self.params.skip = !keep;
+ }
+
+ pub fn skipped(&self) -> bool {
+ self.params.skip
+ }
+
+ pub fn pass_context(&self) -> bool {
+ self.pass_context
+ }
+
+ pub fn signature(&self) -> &syn::Signature {
+ &self.signature
+ }
+
+ pub fn mutable_receiver(&self) -> bool {
+ self.mut_receiver
+ }
+
+ pub fn is_public(&self) -> bool {
+ !matches!(self.visibility, syn::Visibility::Inherited)
+ }
+
+ pub fn span(&self) -> &Span {
+ &self.entire_span
+ }
+
+ pub fn name(&self) -> &syn::Ident {
+ &self.signature.ident
+ }
+
+ pub fn exported_names(&self) -> Vec<syn::LitStr> {
+ let mut literals: Vec<_> = self
+ .params
+ .name
+ .iter()
+ .map(|s| syn::LitStr::new(s, Span::call_site()))
+ .collect();
+
+ if let Some((s, _, span)) = self.params.special.get_fn_name() {
+ literals.push(syn::LitStr::new(&s, span));
+ }
+
+ if literals.is_empty() {
+ literals.push(syn::LitStr::new(
+ &self.signature.ident.to_string(),
+ self.signature.ident.span(),
+ ));
+ }
+
+ literals
+ }
+
+ pub fn exported_name(&self) -> Cow<str> {
+ self.params
+ .name
+ .last()
+ .map_or_else(|| self.signature.ident.to_string().into(), |s| s.into())
+ }
+
+ pub fn arg_list(&self) -> impl Iterator<Item = &syn::FnArg> {
+ let skip = usize::from(self.pass_context);
+ self.signature.inputs.iter().skip(skip)
+ }
+
+ pub fn arg_count(&self) -> usize {
+ let skip = usize::from(self.pass_context);
+ self.signature.inputs.len() - skip
+ }
+
+ pub fn return_type(&self) -> Option<&syn::Type> {
+ match self.signature.output {
+ syn::ReturnType::Type(.., ref ret_type) => Some(flatten_type_groups(ret_type)),
+ _ => None,
+ }
+ }
+
+ #[cfg(feature = "metadata")]
+ pub fn comments(&self) -> &[String] {
+ &self.comments
+ }
+
+ #[cfg(feature = "metadata")]
+ pub fn set_comments(&mut self, comments: Vec<String>) {
+ self.comments = comments
+ }
+
+ pub fn set_cfg_attrs(&mut self, cfg_attrs: Vec<syn::Attribute>) {
+ self.cfg_attrs = cfg_attrs
+ }
+
+ pub fn set_params(&mut self, mut params: ExportedFnParams) -> syn::Result<()> {
+ // Several issues are checked here to avoid issues with diagnostics caused by raising them later.
+ //
+ // 1a. Do not allow non-returning raw functions.
+ //
+ if params.return_raw.is_some() && self.return_type().is_none() {
+ return Err(syn::Error::new(
+ params.return_raw.unwrap(),
+ "functions marked with 'return_raw' must return Result<T, Box<EvalAltResult>>",
+ ));
+ }
+
+ // 1b. Do not allow non-method pure functions.
+ //
+ if params.pure.is_some() && !self.mutable_receiver() {
+ return Err(syn::Error::new(
+ params.pure.unwrap(),
+ "'pure' is not necessary on functions without a &mut first parameter",
+ ));
+ }
+
+ match params.special {
+ // 2a. Property getters must take only the subject as an argument.
+ FnSpecialAccess::Property(Property::Get(..)) if self.arg_count() != 1 => {
+ return Err(syn::Error::new(
+ self.signature.inputs.span(),
+ "property getter requires exactly 1 parameter",
+ ))
+ }
+ // 2b. Property getters must return a value.
+ FnSpecialAccess::Property(Property::Get(..)) if self.return_type().is_none() => {
+ return Err(syn::Error::new(
+ self.signature.span(),
+ "property getter must return a value",
+ ))
+ }
+ // 3a. Property setters must take the subject and a new value as arguments.
+ FnSpecialAccess::Property(Property::Set(..)) if self.arg_count() != 2 => {
+ return Err(syn::Error::new(
+ self.signature.inputs.span(),
+ "property setter requires exactly 2 parameters",
+ ))
+ }
+ // 3b. Non-raw property setters must return nothing.
+ FnSpecialAccess::Property(Property::Set(..))
+ if params.return_raw.is_none() && self.return_type().is_some() =>
+ {
+ return Err(syn::Error::new(
+ self.signature.output.span(),
+ "property setter cannot return any value",
+ ))
+ }
+ // 4a. Index getters must take the subject and the accessed "index" as arguments.
+ FnSpecialAccess::Index(Index::Get) if self.arg_count() != 2 => {
+ return Err(syn::Error::new(
+ self.signature.inputs.span(),
+ "index getter requires exactly 2 parameters",
+ ))
+ }
+ // 4b. Index getters must return a value.
+ FnSpecialAccess::Index(Index::Get) if self.return_type().is_none() => {
+ return Err(syn::Error::new(
+ self.signature.span(),
+ "index getter must return a value",
+ ))
+ }
+ // 5a. Index setters must take the subject, "index", and new value as arguments.
+ FnSpecialAccess::Index(Index::Set) if self.arg_count() != 3 => {
+ return Err(syn::Error::new(
+ self.signature.inputs.span(),
+ "index setter requires exactly 3 parameters",
+ ))
+ }
+ // 5b. Non-raw index setters must return nothing.
+ FnSpecialAccess::Index(Index::Set)
+ if params.return_raw.is_none() && self.return_type().is_some() =>
+ {
+ return Err(syn::Error::new(
+ self.signature.output.span(),
+ "index setter cannot return any value",
+ ))
+ }
+ _ => {}
+ }
+
+ self.params = params;
+ Ok(())
+ }
+
+ pub fn generate(self) -> TokenStream {
+ let name: syn::Ident =
+ syn::Ident::new(&format!("rhai_fn_{}", self.name()), self.name().span());
+ let impl_block = self.generate_impl("Token");
+ let dyn_result_fn_block = self.generate_dynamic_fn();
+ let vis = self.visibility;
+ quote! {
+ #[automatically_derived]
+ #vis mod #name {
+ use super::*;
+ #[doc(hidden)]
+ pub struct Token();
+ #impl_block
+ #dyn_result_fn_block
+ }
+ }
+ }
+
+ pub fn generate_dynamic_fn(&self) -> TokenStream {
+ let name = self.name().clone();
+
+ let mut dynamic_signature = self.signature.clone();
+ dynamic_signature.ident = syn::Ident::new("dynamic_result_fn", Span::call_site());
+ dynamic_signature.output = syn::parse2::<syn::ReturnType>(quote! {
+ -> RhaiResult
+ })
+ .unwrap();
+ let arguments: Vec<_> = dynamic_signature
+ .inputs
+ .iter()
+ .filter_map(|fn_arg| match fn_arg {
+ syn::FnArg::Typed(syn::PatType { ref pat, .. }) => match pat.as_ref() {
+ syn::Pat::Ident(ref ident) => Some(ident.ident.clone()),
+ _ => None,
+ },
+ _ => None,
+ })
+ .collect();
+
+ let return_span = self
+ .return_type()
+ .map(|r| r.span())
+ .unwrap_or_else(Span::call_site)
+ .resolved_at(Span::call_site());
+ if self.params.return_raw.is_some() {
+ quote_spanned! { return_span =>
+ #[allow(unused)]
+ #[doc(hidden)]
+ #[inline(always)]
+ pub #dynamic_signature {
+ #name(#(#arguments),*).map(Dynamic::from)
+ }
+ }
+ } else {
+ quote_spanned! { return_span =>
+ #[allow(unused)]
+ #[doc(hidden)]
+ #[inline(always)]
+ pub #dynamic_signature {
+ Ok(Dynamic::from(#name(#(#arguments),*)))
+ }
+ }
+ }
+ }
+
+ pub fn generate_impl(&self, on_type_name: &str) -> TokenStream {
+ let sig_name = self.name().clone();
+ let arg_count = self.arg_count();
+ let is_method_call = self.mutable_receiver();
+ let is_pure = !self.mutable_receiver() || self.params().pure.is_some();
+ let pass_context = self.pass_context;
+
+ let mut unpack_statements = Vec::new();
+ let mut unpack_exprs = Vec::new();
+ #[cfg(feature = "metadata")]
+ let mut input_type_names = Vec::new();
+ let mut input_type_exprs = Vec::new();
+
+ let return_type = self
+ .return_type()
+ .map(print_type)
+ .unwrap_or_else(|| "()".to_string());
+
+ let skip_first_arg;
+
+ if self.pass_context {
+ unpack_exprs.push(syn::parse2::<syn::Expr>(quote! { context.unwrap() }).unwrap());
+ }
+
+ // Handle the first argument separately if the function has a "method like" receiver
+ if is_method_call {
+ skip_first_arg = true;
+ let first_arg = self.arg_list().next().unwrap();
+ let var = syn::Ident::new("arg0", Span::call_site());
+ match first_arg {
+ syn::FnArg::Typed(syn::PatType { pat, ty, .. }) => {
+ #[cfg(feature = "metadata")]
+ let arg_name = format!("{}: {}", pat.to_token_stream(), print_type(ty));
+ let arg_type = match flatten_type_groups(ty.as_ref()) {
+ syn::Type::Reference(syn::TypeReference { ref elem, .. }) => elem.as_ref(),
+ p => p,
+ };
+ let downcast_span = quote_spanned!(arg_type.span() =>
+ &mut args[0usize].write_lock::<#arg_type>().unwrap()
+ );
+ unpack_statements.push(
+ syn::parse2::<syn::Stmt>(quote! {
+ let #var = #downcast_span;
+ })
+ .unwrap(),
+ );
+ #[cfg(feature = "metadata")]
+ input_type_names.push(arg_name);
+ input_type_exprs.push(
+ syn::parse2::<syn::Expr>(quote_spanned!(arg_type.span() =>
+ TypeId::of::<#arg_type>()
+ ))
+ .unwrap(),
+ );
+ }
+ syn::FnArg::Receiver(..) => todo!("true self parameters not implemented yet"),
+ }
+ unpack_exprs.push(syn::parse2::<syn::Expr>(quote! { #var }).unwrap());
+ } else {
+ skip_first_arg = false;
+ }
+
+ // Handle the rest of the arguments, which all are passed by value.
+ //
+ // The only exception is strings, which need to be downcast to ImmutableString to enable a
+ // zero-copy conversion to &str by reference, or a cloned String.
+ let str_type_path = syn::parse2::<syn::Path>(quote! { str }).unwrap();
+ let string_type_path = syn::parse2::<syn::Path>(quote! { String }).unwrap();
+ for (i, arg) in self.arg_list().enumerate().skip(skip_first_arg as usize) {
+ let var = syn::Ident::new(&format!("arg{i}"), Span::call_site());
+ let is_string;
+ let is_ref;
+ match arg {
+ syn::FnArg::Typed(syn::PatType { pat, ty, .. }) => {
+ #[cfg(feature = "metadata")]
+ let arg_name = format!("{}: {}", pat.to_token_stream(), print_type(ty));
+ let arg_type = ty.as_ref();
+ let downcast_span = match flatten_type_groups(arg_type) {
+ syn::Type::Reference(syn::TypeReference {
+ mutability: None,
+ ref elem,
+ ..
+ }) => match flatten_type_groups(elem.as_ref()) {
+ syn::Type::Path(ref p) if p.path == str_type_path => {
+ is_string = true;
+ is_ref = true;
+ quote_spanned!(arg_type.span().resolved_at(Span::call_site()) =>
+ mem::take(args[#i]).into_immutable_string().unwrap()
+ )
+ }
+ _ => panic!("internal error: why wasn't this found earlier!?"),
+ },
+ syn::Type::Path(ref p) if p.path == string_type_path => {
+ is_string = true;
+ is_ref = false;
+ quote_spanned!(arg_type.span().resolved_at(Span::call_site()) =>
+ mem::take(args[#i]).into_string().unwrap()
+ )
+ }
+ _ => {
+ is_string = false;
+ is_ref = false;
+ quote_spanned!(arg_type.span().resolved_at(Span::call_site()) =>
+ mem::take(args[#i]).cast::<#arg_type>()
+ )
+ }
+ };
+
+ unpack_statements.push(
+ syn::parse2::<syn::Stmt>(quote! {
+ let #var = #downcast_span;
+ })
+ .unwrap(),
+ );
+ #[cfg(feature = "metadata")]
+ input_type_names.push(arg_name);
+ if !is_string {
+ input_type_exprs.push(
+ syn::parse2::<syn::Expr>(quote_spanned!(arg_type.span() =>
+ TypeId::of::<#arg_type>()
+ ))
+ .unwrap(),
+ );
+ } else {
+ input_type_exprs.push(
+ syn::parse2::<syn::Expr>(quote_spanned!(arg_type.span() =>
+ TypeId::of::<ImmutableString>()
+ ))
+ .unwrap(),
+ );
+ }
+ }
+ syn::FnArg::Receiver(..) => panic!("internal error: how did this happen!?"),
+ }
+ if !is_ref {
+ unpack_exprs.push(syn::parse2::<syn::Expr>(quote! { #var }).unwrap());
+ } else {
+ unpack_exprs.push(syn::parse2::<syn::Expr>(quote! { &#var }).unwrap());
+ }
+ }
+
+ // In method calls, the first argument will need to be mutably borrowed. Because Rust marks
+ // that as needing to borrow the entire array, all of the previous argument unpacking via
+ // clone needs to happen first.
+ if is_method_call {
+ let arg0 = unpack_statements.remove(0);
+ unpack_statements.push(arg0);
+ }
+
+ // Handle "raw returns", aka cases where the result is a dynamic or an error.
+ //
+ // This allows skipping the Dynamic::from wrap.
+ let return_span = self
+ .return_type()
+ .map(|r| r.span())
+ .unwrap_or_else(Span::call_site)
+ .resolved_at(Span::call_site());
+ let return_expr = if self.params.return_raw.is_none() {
+ quote_spanned! { return_span =>
+ Ok(Dynamic::from(#sig_name(#(#unpack_exprs),*)))
+ }
+ } else {
+ quote_spanned! { return_span =>
+ #sig_name(#(#unpack_exprs),*).map(Dynamic::from)
+ }
+ };
+
+ let type_name = syn::Ident::new(on_type_name, Span::call_site());
+
+ #[cfg(feature = "metadata")]
+ let param_names = quote! {
+ pub const PARAM_NAMES: &'static [&'static str] = &[#(#input_type_names,)* #return_type];
+ };
+ #[cfg(not(feature = "metadata"))]
+ let param_names = quote! {};
+
+ let cfg_attrs: Vec<_> = self
+ .cfg_attrs()
+ .iter()
+ .map(syn::Attribute::to_token_stream)
+ .collect();
+
+ quote! {
+ #(#cfg_attrs)*
+ #[doc(hidden)]
+ impl #type_name {
+ #param_names
+ #[inline(always)] pub fn param_types() -> [TypeId; #arg_count] { [#(#input_type_exprs),*] }
+ }
+ #(#cfg_attrs)*
+ impl PluginFunction for #type_name {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ #(#unpack_statements)*
+ #return_expr
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { #is_method_call }
+ #[inline(always)] fn is_pure(&self) -> bool { #is_pure }
+ #[inline(always)] fn has_context(&self) -> bool { #pass_context }
+ }
+ }
+ }
+}
diff --git a/rhai/codegen/src/lib.rs b/rhai/codegen/src/lib.rs
new file mode 100644
index 0000000..aa4af27
--- /dev/null
+++ b/rhai/codegen/src/lib.rs
@@ -0,0 +1,412 @@
+//! This crate contains procedural macros to make creating Rhai plugin modules much easier.
+//!
+//! # Export an Entire Rust Module to a Rhai `Module`
+//!
+//! ```
+//! use rhai::{EvalAltResult, FLOAT};
+//! use rhai::plugin::*;
+//! use rhai::module_resolvers::*;
+//!
+//! #[export_module]
+//! mod advanced_math {
+//! pub const MYSTIC_NUMBER: FLOAT = 42.0;
+//!
+//! pub fn euclidean_distance(x1: FLOAT, y1: FLOAT, x2: FLOAT, y2: FLOAT) -> FLOAT {
+//! ((y2 - y1).abs().powf(2.0) + (x2 -x1).abs().powf(2.0)).sqrt()
+//! }
+//! }
+//!
+//! # fn main() -> Result<(), Box<EvalAltResult>> {
+//! let mut engine = Engine::new();
+//! let m = exported_module!(advanced_math);
+//! let mut r = StaticModuleResolver::new();
+//! r.insert("Math::Advanced", m);
+//! engine.set_module_resolver(r);
+//!
+//! assert_eq!(engine.eval::<FLOAT>(
+//! r#"
+//! import "Math::Advanced" as math;
+//! math::euclidean_distance(0.0, 1.0, 0.0, math::MYSTIC_NUMBER)
+//! "#)?, 41.0);
+//! # Ok(())
+//! # }
+//! ```
+//!
+//! # Register a Rust Function with a Rhai `Module`
+//!
+//! ```
+//! use rhai::{EvalAltResult, FLOAT, Module};
+//! use rhai::plugin::*;
+//! use rhai::module_resolvers::*;
+//!
+//! #[export_fn]
+//! fn distance_function(x1: FLOAT, y1: FLOAT, x2: FLOAT, y2: FLOAT) -> FLOAT {
+//! ((y2 - y1).abs().powf(2.0) + (x2 -x1).abs().powf(2.0)).sqrt()
+//! }
+//!
+//! # fn main() -> Result<(), Box<EvalAltResult>> {
+//! let mut engine = Engine::new();
+//! engine.register_fn("get_mystic_number", || 42.0 as FLOAT);
+//! let mut m = Module::new();
+//! set_exported_fn!(m, "euclidean_distance", distance_function);
+//! let mut r = StaticModuleResolver::new();
+//! r.insert("Math::Advanced", m);
+//! engine.set_module_resolver(r);
+//!
+//! assert_eq!(engine.eval::<FLOAT>(
+//! r#"
+//! import "Math::Advanced" as math;
+//! math::euclidean_distance(0.0, 1.0, 0.0, get_mystic_number())
+//! "#)?, 41.0);
+//! # Ok(())
+//! # }
+//! ```
+//!
+//! # Register a Plugin Function with an `Engine`
+//!
+//! ```
+//! use rhai::{EvalAltResult, FLOAT, Module};
+//! use rhai::plugin::*;
+//! use rhai::module_resolvers::*;
+//!
+//! #[export_fn]
+//! pub fn distance_function(x1: FLOAT, y1: FLOAT, x2: FLOAT, y2: FLOAT) -> FLOAT {
+//! ((y2 - y1).abs().powf(2.0) + (x2 -x1).abs().powf(2.0)).sqrt()
+//! }
+//!
+//! # fn main() -> Result<(), Box<EvalAltResult>> {
+//! let mut engine = Engine::new();
+//! engine.register_fn("get_mystic_number", || { 42 as FLOAT });
+//! register_exported_fn!(engine, "euclidean_distance", distance_function);
+//!
+//! assert_eq!(engine.eval::<FLOAT>(
+//! "euclidean_distance(0.0, 1.0, 0.0, get_mystic_number())"
+//! )?, 41.0);
+//! # Ok(())
+//! # }
+//! ```
+//!
+
+use quote::quote;
+use syn::{parse_macro_input, spanned::Spanned};
+
+mod attrs;
+mod function;
+mod module;
+mod register;
+mod rhai_module;
+
+#[cfg(test)]
+mod test;
+
+/// Attribute, when put on a Rust function, turns it into a _plugin function_.
+///
+/// # Usage
+///
+/// ```
+/// # use rhai::{Engine, EvalAltResult};
+/// use rhai::plugin::*;
+///
+/// #[export_fn]
+/// fn my_plugin_function(x: i64) -> i64 {
+/// x * 2
+/// }
+///
+/// # fn main() -> Result<(), Box<EvalAltResult>> {
+/// let mut engine = Engine::new();
+///
+/// register_exported_fn!(engine, "func", my_plugin_function);
+///
+/// assert_eq!(engine.eval::<i64>("func(21)")?, 42);
+/// # Ok(())
+/// # }
+/// ```
+#[proc_macro_attribute]
+pub fn export_fn(
+ args: proc_macro::TokenStream,
+ input: proc_macro::TokenStream,
+) -> proc_macro::TokenStream {
+ let mut output = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ };
+ output.extend(proc_macro2::TokenStream::from(input.clone()));
+
+ let parsed_params = match crate::attrs::outer_item_attributes(args.into(), "export_fn") {
+ Ok(args) => args,
+ Err(err) => return err.to_compile_error().into(),
+ };
+ let mut function_def = parse_macro_input!(input as function::ExportedFn);
+
+ if !function_def.cfg_attrs().is_empty() {
+ return syn::Error::new(
+ function_def.cfg_attrs()[0].span(),
+ "`cfg` attributes are not allowed for `export_fn`",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ if let Err(e) = function_def.set_params(parsed_params) {
+ return e.to_compile_error().into();
+ }
+
+ output.extend(function_def.generate());
+ proc_macro::TokenStream::from(output)
+}
+
+/// Attribute, when put on a Rust module, turns it into a _plugin module_.
+///
+/// # Usage
+///
+/// ```
+/// # use rhai::{Engine, Module, EvalAltResult};
+/// use rhai::plugin::*;
+///
+/// #[export_module]
+/// mod my_plugin_module {
+/// pub fn foo(x: i64) -> i64 { x * 2 }
+/// pub fn bar() -> i64 { 21 }
+/// }
+///
+/// # fn main() -> Result<(), Box<EvalAltResult>> {
+/// let mut engine = Engine::new();
+///
+/// let module = exported_module!(my_plugin_module);
+///
+/// engine.register_global_module(module.into());
+///
+/// assert_eq!(engine.eval::<i64>("foo(bar())")?, 42);
+/// # Ok(())
+/// # }
+/// ```
+#[proc_macro_attribute]
+pub fn export_module(
+ args: proc_macro::TokenStream,
+ input: proc_macro::TokenStream,
+) -> proc_macro::TokenStream {
+ let parsed_params = match crate::attrs::outer_item_attributes(args.into(), "export_module") {
+ Ok(args) => args,
+ Err(err) => return err.to_compile_error().into(),
+ };
+ let mut module_def = parse_macro_input!(input as module::Module);
+ if let Err(e) = module_def.set_params(parsed_params) {
+ return e.to_compile_error().into();
+ }
+
+ let tokens = module_def.generate();
+ proc_macro::TokenStream::from(tokens)
+}
+
+/// Macro to generate a Rhai `Module` from a _plugin module_ defined via [`#[export_module]`][macro@export_module].
+///
+/// # Usage
+///
+/// ```
+/// # use rhai::{Engine, Module, EvalAltResult};
+/// use rhai::plugin::*;
+///
+/// #[export_module]
+/// mod my_plugin_module {
+/// pub fn foo(x: i64) -> i64 { x * 2 }
+/// pub fn bar() -> i64 { 21 }
+/// }
+///
+/// # fn main() -> Result<(), Box<EvalAltResult>> {
+/// let mut engine = Engine::new();
+///
+/// let module = exported_module!(my_plugin_module);
+///
+/// engine.register_global_module(module.into());
+///
+/// assert_eq!(engine.eval::<i64>("foo(bar())")?, 42);
+/// # Ok(())
+/// # }
+/// ```
+#[proc_macro]
+pub fn exported_module(module_path: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ let module_path = parse_macro_input!(module_path as syn::Path);
+ proc_macro::TokenStream::from(quote::quote! {
+ #module_path::rhai_module_generate()
+ })
+}
+
+/// Macro to combine a _plugin module_ into an existing module.
+///
+/// Functions and variables in the plugin module overrides any existing similarly-named
+/// functions and variables in the target module.
+///
+/// This call is intended to be used within the [`def_package!`][crate::def_package] macro to define
+/// a custom package based on a plugin module.
+///
+/// All sub-modules, if any, in the plugin module are _flattened_ and their functions/variables
+/// registered at the top level because packages require so.
+///
+/// The text string name in the second parameter can be anything and is reserved for future use;
+/// it is recommended to be an ID string that uniquely identifies the plugin module.
+///
+/// # Usage
+///
+/// ```
+/// # use rhai::{Engine, Module, EvalAltResult};
+/// use rhai::plugin::*;
+///
+/// #[export_module]
+/// mod my_plugin_module {
+/// pub fn foo(x: i64) -> i64 { x * 2 }
+/// pub fn bar() -> i64 { 21 }
+/// }
+///
+/// # fn main() -> Result<(), Box<EvalAltResult>> {
+/// let mut engine = Engine::new();
+///
+/// let mut module = Module::new();
+/// combine_with_exported_module!(&mut module, "my_plugin_module_ID", my_plugin_module);
+///
+/// engine.register_global_module(module.into());
+///
+/// assert_eq!(engine.eval::<i64>("foo(bar())")?, 42);
+/// # Ok(())
+/// # }
+/// ```
+#[proc_macro]
+pub fn combine_with_exported_module(args: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ match crate::register::parse_register_macro(args) {
+ Ok((module_expr, _export_name, module_path)) => proc_macro::TokenStream::from(quote! {
+ #module_path::rhai_generate_into_module(#module_expr, true)
+ }),
+ Err(e) => e.to_compile_error().into(),
+ }
+}
+
+/// Macro to register a _plugin function_ (defined via [`#[export_fn]`][macro@export_fn]) into an `Engine`.
+///
+/// # Usage
+///
+/// ```
+/// # use rhai::{Engine, EvalAltResult};
+/// use rhai::plugin::*;
+///
+/// #[export_fn]
+/// fn my_plugin_function(x: i64) -> i64 {
+/// x * 2
+/// }
+///
+/// # fn main() -> Result<(), Box<EvalAltResult>> {
+/// let mut engine = Engine::new();
+///
+/// register_exported_fn!(engine, "func", my_plugin_function);
+///
+/// assert_eq!(engine.eval::<i64>("func(21)")?, 42);
+/// # Ok(())
+/// # }
+/// ```
+#[proc_macro]
+pub fn register_exported_fn(args: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ match crate::register::parse_register_macro(args) {
+ Ok((engine_expr, export_name, rust_mod_path)) => {
+ let gen_mod_path = crate::register::generated_module_path(&rust_mod_path);
+ proc_macro::TokenStream::from(quote! {
+ #engine_expr.register_fn(#export_name, #gen_mod_path::dynamic_result_fn)
+ })
+ }
+ Err(e) => e.to_compile_error().into(),
+ }
+}
+
+/// Macro to register a _plugin function_ into a Rhai `Module`.
+///
+/// # Usage
+///
+/// ```
+/// # use rhai::{Engine, EvalAltResult};
+/// use rhai::plugin::*;
+///
+/// #[export_fn]
+/// fn my_plugin_function(x: i64) -> i64 {
+/// x * 2
+/// }
+///
+/// # fn main() -> Result<(), Box<EvalAltResult>> {
+/// let mut engine = Engine::new();
+///
+/// let mut module = Module::new();
+/// set_exported_fn!(module, "func", my_plugin_function);
+///
+/// engine.register_global_module(module.into());
+///
+/// assert_eq!(engine.eval::<i64>("func(21)")?, 42);
+/// # Ok(())
+/// # }
+/// ```
+#[proc_macro]
+pub fn set_exported_fn(args: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ match crate::register::parse_register_macro(args) {
+ Ok((module_expr, export_name, rust_mod_path)) => {
+ let gen_mod_path = crate::register::generated_module_path(&rust_mod_path);
+
+ #[cfg(feature = "metadata")]
+ let param_names = quote! {
+ Some(#gen_mod_path::Token::PARAM_NAMES)
+ };
+ #[cfg(not(feature = "metadata"))]
+ let param_names = quote! { None };
+
+ proc_macro::TokenStream::from(quote! {
+ #module_expr.set_fn(#export_name, FnNamespace::Internal, FnAccess::Public,
+ #param_names,
+ &#gen_mod_path::Token::param_types(),
+ #gen_mod_path::Token().into())
+ })
+ }
+ Err(e) => e.to_compile_error().into(),
+ }
+}
+
+/// Macro to register a _plugin function_ into a Rhai `Module` and expose it globally.
+///
+/// # Usage
+///
+/// ```
+/// # use rhai::{Engine, EvalAltResult};
+/// use rhai::plugin::*;
+///
+/// #[export_fn]
+/// fn my_plugin_function(x: i64) -> i64 {
+/// x * 2
+/// }
+///
+/// # fn main() -> Result<(), Box<EvalAltResult>> {
+/// let mut engine = Engine::new();
+///
+/// let mut module = Module::new();
+/// set_exported_global_fn!(module, "func", my_plugin_function);
+///
+/// engine.register_static_module("test", module.into());
+///
+/// assert_eq!(engine.eval::<i64>("func(21)")?, 42);
+/// # Ok(())
+/// # }
+/// ```
+#[proc_macro]
+pub fn set_exported_global_fn(args: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ match crate::register::parse_register_macro(args) {
+ Ok((module_expr, export_name, rust_mod_path)) => {
+ let gen_mod_path = crate::register::generated_module_path(&rust_mod_path);
+
+ #[cfg(feature = "metadata")]
+ let param_names = quote! {
+ Some(#gen_mod_path::Token::PARAM_NAMES)
+ };
+ #[cfg(not(feature = "metadata"))]
+ let param_names = quote! { None };
+
+ proc_macro::TokenStream::from(quote! {
+ #module_expr.set_fn(#export_name, FnNamespace::Global, FnAccess::Public,
+ #param_names,
+ &#gen_mod_path::Token::param_types(),
+ #gen_mod_path::Token().into())
+ })
+ }
+ Err(e) => e.to_compile_error().into(),
+ }
+}
diff --git a/rhai/codegen/src/module.rs b/rhai/codegen/src/module.rs
new file mode 100644
index 0000000..32ac8f3
--- /dev/null
+++ b/rhai/codegen/src/module.rs
@@ -0,0 +1,357 @@
+use quote::{quote, ToTokens};
+use syn::{parse::Parse, parse::ParseStream};
+
+#[cfg(no_std)]
+use core::mem;
+#[cfg(not(no_std))]
+use std::mem;
+
+use std::borrow::Cow;
+
+use crate::attrs::{AttrItem, ExportInfo, ExportScope, ExportedParams};
+use crate::function::ExportedFn;
+use crate::rhai_module::{ExportedConst, ExportedType};
+
+#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
+pub struct ExportedModParams {
+ pub name: String,
+ skip: bool,
+ pub scope: ExportScope,
+}
+
+impl Parse for ExportedModParams {
+ fn parse(args: ParseStream) -> syn::Result<Self> {
+ if args.is_empty() {
+ return Ok(ExportedModParams::default());
+ }
+
+ Self::from_info(crate::attrs::parse_attr_items(args)?)
+ }
+}
+
+impl ExportedParams for ExportedModParams {
+ fn parse_stream(args: ParseStream) -> syn::Result<Self> {
+ Self::parse(args)
+ }
+
+ fn no_attrs() -> Self {
+ Default::default()
+ }
+
+ fn from_info(info: ExportInfo) -> syn::Result<Self> {
+ let ExportInfo { items: attrs, .. } = info;
+ let mut name = String::new();
+ let mut skip = false;
+ let mut scope = None;
+ for attr in attrs {
+ let AttrItem { key, value, .. } = attr;
+ match (key.to_string().as_ref(), value) {
+ ("name", Some(s)) => {
+ let new_name = s.value();
+ if name == new_name {
+ return Err(syn::Error::new(key.span(), "conflicting name"));
+ }
+ name = new_name;
+ }
+ ("name", None) => return Err(syn::Error::new(key.span(), "requires value")),
+
+ ("skip", None) => skip = true,
+ ("skip", Some(s)) => return Err(syn::Error::new(s.span(), "extraneous value")),
+
+ ("export_prefix", Some(_)) | ("export_all", None) if scope.is_some() => {
+ return Err(syn::Error::new(key.span(), "duplicate export scope"));
+ }
+ ("export_prefix", Some(s)) => scope = Some(ExportScope::Prefix(s.value())),
+ ("export_prefix", None) => {
+ return Err(syn::Error::new(key.span(), "requires value"))
+ }
+ ("export_all", None) => scope = Some(ExportScope::All),
+ ("export_all", Some(s)) => {
+ return Err(syn::Error::new(s.span(), "extraneous value"))
+ }
+ (attr, ..) => {
+ return Err(syn::Error::new(
+ key.span(),
+ format!("unknown attribute '{attr}'"),
+ ))
+ }
+ }
+ }
+
+ Ok(ExportedModParams {
+ name,
+ skip,
+ scope: scope.unwrap_or_default(),
+ })
+ }
+}
+
+#[derive(Debug)]
+pub struct Module {
+ mod_all: syn::ItemMod,
+ consts: Vec<ExportedConst>,
+ custom_types: Vec<ExportedType>,
+ fns: Vec<ExportedFn>,
+ sub_modules: Vec<Module>,
+ params: ExportedModParams,
+}
+
+impl Module {
+ pub fn set_params(&mut self, params: ExportedModParams) -> syn::Result<()> {
+ self.params = params;
+ Ok(())
+ }
+}
+
+impl Parse for Module {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let mut mod_all: syn::ItemMod = input.parse()?;
+
+ let fns: Vec<_>;
+ let mut consts = Vec::new();
+ let mut custom_types = Vec::new();
+ let mut sub_modules = Vec::new();
+
+ if let Some((.., ref mut content)) = mod_all.content {
+ // Gather and parse functions.
+ fns = content
+ .iter_mut()
+ .filter_map(|item| match item {
+ syn::Item::Fn(f) => Some(f),
+ _ => None,
+ })
+ .try_fold(Vec::new(), |mut vec, item_fn| -> syn::Result<_> {
+ let params =
+ crate::attrs::inner_item_attributes(&mut item_fn.attrs, "rhai_fn")?;
+
+ let f =
+ syn::parse2(item_fn.to_token_stream()).and_then(|mut f: ExportedFn| {
+ f.set_params(params)?;
+ f.set_cfg_attrs(crate::attrs::collect_cfg_attr(&item_fn.attrs));
+
+ #[cfg(feature = "metadata")]
+ f.set_comments(crate::attrs::doc_attributes(&item_fn.attrs)?);
+ Ok(f)
+ })?;
+
+ vec.push(f);
+ Ok(vec)
+ })?;
+ // Gather and parse constants definitions.
+ for item in &*content {
+ if let syn::Item::Const(syn::ItemConst {
+ vis: syn::Visibility::Public(..),
+ ref expr,
+ ident,
+ attrs,
+ ty,
+ ..
+ }) = item
+ {
+ consts.push(ExportedConst {
+ name: ident.to_string(),
+ typ: ty.clone(),
+ expr: expr.as_ref().clone(),
+ cfg_attrs: crate::attrs::collect_cfg_attr(attrs),
+ })
+ }
+ }
+ // Gather and parse type definitions.
+ for item in &*content {
+ if let syn::Item::Type(syn::ItemType {
+ vis: syn::Visibility::Public(..),
+ ident,
+ attrs,
+ ty,
+ ..
+ }) = item
+ {
+ custom_types.push(ExportedType {
+ name: ident.to_string(),
+ typ: ty.clone(),
+ cfg_attrs: crate::attrs::collect_cfg_attr(attrs),
+ })
+ }
+ }
+ // Gather and parse sub-module definitions.
+ //
+ // They are actually removed from the module's body, because they will need
+ // re-generating later when generated code is added.
+ sub_modules.reserve(content.len() - fns.len() - consts.len());
+ let mut i = 0;
+ while i < content.len() {
+ match content[i] {
+ syn::Item::Mod(..) => {
+ let mut item_mod = match content.remove(i) {
+ syn::Item::Mod(m) => m,
+ _ => unreachable!(),
+ };
+ let params: ExportedModParams =
+ crate::attrs::inner_item_attributes(&mut item_mod.attrs, "rhai_mod")?;
+ let module = syn::parse2::<Module>(item_mod.to_token_stream()).and_then(
+ |mut m| {
+ m.set_params(params)?;
+ Ok(m)
+ },
+ )?;
+ sub_modules.push(module);
+ }
+ _ => i += 1,
+ }
+ }
+ } else {
+ fns = Vec::new();
+ }
+ Ok(Module {
+ mod_all,
+ fns,
+ consts,
+ custom_types,
+ sub_modules,
+ params: ExportedModParams::default(),
+ })
+ }
+}
+
+impl Module {
+ pub fn attrs(&self) -> &[syn::Attribute] {
+ &self.mod_all.attrs
+ }
+
+ pub fn module_name(&self) -> &syn::Ident {
+ &self.mod_all.ident
+ }
+
+ pub fn exported_name(&self) -> Cow<str> {
+ if !self.params.name.is_empty() {
+ (&self.params.name).into()
+ } else {
+ self.module_name().to_string().into()
+ }
+ }
+
+ pub fn update_scope(&mut self, parent_scope: &ExportScope) {
+ let keep = match (self.params.skip, parent_scope) {
+ (true, ..) => false,
+ (.., ExportScope::PubOnly) => matches!(self.mod_all.vis, syn::Visibility::Public(..)),
+ (.., ExportScope::Prefix(s)) => self.mod_all.ident.to_string().starts_with(s),
+ (.., ExportScope::All) => true,
+ };
+ self.params.skip = !keep;
+ }
+
+ pub fn skipped(&self) -> bool {
+ self.params.skip
+ }
+
+ pub fn generate(self) -> proc_macro2::TokenStream {
+ match self.generate_inner() {
+ Ok(tokens) => tokens,
+ Err(e) => e.to_compile_error(),
+ }
+ }
+
+ fn generate_inner(self) -> Result<proc_macro2::TokenStream, syn::Error> {
+ // Check for collisions if the "name" attribute was used on inner functions.
+ crate::rhai_module::check_rename_collisions(&self.fns)?;
+
+ // Extract the current structure of the module.
+ let Module {
+ mut mod_all,
+ mut fns,
+ consts,
+ custom_types,
+ mut sub_modules,
+ params,
+ ..
+ } = self;
+ let mod_vis = mod_all.vis;
+ let mod_name = mod_all.ident.clone();
+ let (.., orig_content) = mod_all.content.take().unwrap();
+ let mod_attrs = mem::take(&mut mod_all.attrs);
+
+ #[cfg(feature = "metadata")]
+ let mod_doc = crate::attrs::doc_attributes(&mod_attrs)?.join("\n");
+ #[cfg(not(feature = "metadata"))]
+ let mod_doc = String::new();
+
+ if !params.skip {
+ // Generate new module items.
+ //
+ // This is done before inner module recursive generation, because that is destructive.
+ let mod_gen = crate::rhai_module::generate_body(
+ &mod_doc,
+ &mut fns,
+ &consts,
+ &custom_types,
+ &mut sub_modules,
+ ¶ms.scope,
+ );
+
+ // NB: sub-modules must have their new items for exporting generated in depth-first order
+ // to avoid issues caused by re-parsing them
+ let inner_modules = sub_modules
+ .into_iter()
+ .try_fold::<_, _, Result<_, syn::Error>>(Vec::new(), |mut acc, m| {
+ acc.push(m.generate_inner()?);
+ Ok(acc)
+ })?;
+
+ // Regenerate the module with the new content added.
+ Ok(quote! {
+ #(#mod_attrs)*
+ #[allow(clippy::needless_pass_by_value)]
+ #mod_vis mod #mod_name {
+ #(#orig_content)*
+ #(#inner_modules)*
+ #mod_gen
+ }
+ })
+ } else {
+ // Regenerate the original module as-is.
+ Ok(quote! {
+ #(#mod_attrs)*
+ #[allow(clippy::needless_pass_by_value)]
+ #mod_vis mod #mod_name {
+ #(#orig_content)*
+ }
+ })
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn name(&self) -> &syn::Ident {
+ &self.mod_all.ident
+ }
+
+ #[allow(dead_code)]
+ pub fn consts(&self) -> &[ExportedConst] {
+ &self.consts
+ }
+
+ #[allow(dead_code)]
+ pub fn custom_types(&self) -> &[ExportedType] {
+ &self.custom_types
+ }
+
+ #[allow(dead_code)]
+ pub fn fns(&self) -> &[ExportedFn] {
+ &self.fns
+ }
+
+ #[allow(dead_code)]
+ pub fn sub_modules(&self) -> &[Module] {
+ &self.sub_modules
+ }
+
+ #[allow(dead_code)]
+ pub fn content(&self) -> Option<&[syn::Item]> {
+ match self.mod_all {
+ syn::ItemMod {
+ content: Some((.., ref vec)),
+ ..
+ } => Some(vec),
+ _ => None,
+ }
+ }
+}
diff --git a/rhai/codegen/src/register.rs b/rhai/codegen/src/register.rs
new file mode 100644
index 0000000..773711c
--- /dev/null
+++ b/rhai/codegen/src/register.rs
@@ -0,0 +1,50 @@
+use quote::{quote, quote_spanned};
+use syn::{parse::Parser, spanned::Spanned};
+
+pub fn generated_module_path(
+ fn_path: &syn::Path,
+) -> syn::punctuated::Punctuated<syn::PathSegment, syn::Token![::]> {
+ let mut g = fn_path.clone().segments;
+ g.pop();
+ let ident = syn::Ident::new(
+ &format!("rhai_fn_{}", fn_path.segments.last().unwrap().ident),
+ fn_path.span(),
+ );
+ g.push_value(syn::PathSegment {
+ ident,
+ arguments: syn::PathArguments::None,
+ });
+ g
+}
+
+type RegisterMacroInput = (syn::Expr, proc_macro2::TokenStream, syn::Path);
+
+pub fn parse_register_macro(
+ args: proc_macro::TokenStream,
+) -> Result<RegisterMacroInput, syn::Error> {
+ let args = syn::punctuated::Punctuated::<_, syn::Token![,]>::parse_separated_nonempty
+ .parse(args)
+ .unwrap();
+ let arg_span = args.span();
+ let mut items: Vec<_> = args.into_iter().collect();
+ if items.len() != 3 {
+ return Err(syn::Error::new(
+ arg_span,
+ "this macro requires three arguments",
+ ));
+ }
+ let export_name = match &items[1] {
+ syn::Expr::Lit(lit_str) => quote_spanned!(items[1].span() => #lit_str),
+ expr => quote! { #expr },
+ };
+ let rust_mod_path = if let syn::Expr::Path(ref path) = &items[2] {
+ path.path.clone()
+ } else {
+ return Err(syn::Error::new(
+ items[2].span(),
+ "third argument must be a function name",
+ ));
+ };
+ let module = items.remove(0);
+ Ok((module, export_name, rust_mod_path))
+}
diff --git a/rhai/codegen/src/rhai_module.rs b/rhai/codegen/src/rhai_module.rs
new file mode 100644
index 0000000..e0060c5
--- /dev/null
+++ b/rhai/codegen/src/rhai_module.rs
@@ -0,0 +1,371 @@
+use proc_macro2::{Span, TokenStream};
+use quote::{quote, ToTokens};
+
+use std::collections::BTreeMap;
+
+use crate::attrs::ExportScope;
+use crate::function::{
+ flatten_type_groups, print_type, ExportedFn, FnNamespaceAccess, FnSpecialAccess, FN_GET,
+ FN_IDX_GET, FN_IDX_SET, FN_SET,
+};
+use crate::module::Module;
+
+#[derive(Debug)]
+pub struct ExportedConst {
+ pub name: String,
+ pub typ: Box<syn::Type>,
+ pub expr: syn::Expr,
+ pub cfg_attrs: Vec<syn::Attribute>,
+}
+
+#[derive(Debug)]
+pub struct ExportedType {
+ pub name: String,
+ pub typ: Box<syn::Type>,
+ pub cfg_attrs: Vec<syn::Attribute>,
+}
+
+pub fn generate_body(
+ doc: &str,
+ fns: &mut [ExportedFn],
+ consts: &[ExportedConst],
+ custom_types: &[ExportedType],
+ sub_modules: &mut [Module],
+ parent_scope: &ExportScope,
+) -> TokenStream {
+ let mut set_fn_statements = Vec::new();
+ let mut set_const_statements = Vec::new();
+ let mut add_mod_blocks = Vec::new();
+ let mut set_flattened_mod_blocks = Vec::new();
+ let str_type_path = syn::parse2::<syn::Path>(quote! { str }).unwrap();
+ let string_type_path = syn::parse2::<syn::Path>(quote! { String }).unwrap();
+
+ for ExportedConst {
+ name: const_name,
+ cfg_attrs,
+ ..
+ } in consts
+ {
+ let const_literal = syn::LitStr::new(const_name, Span::call_site());
+ let const_ref = syn::Ident::new(const_name, Span::call_site());
+
+ let cfg_attrs: Vec<_> = cfg_attrs
+ .iter()
+ .map(syn::Attribute::to_token_stream)
+ .collect();
+
+ set_const_statements.push(
+ syn::parse2::<syn::Stmt>(quote! {
+ #(#cfg_attrs)*
+ m.set_var(#const_literal, #const_ref);
+ })
+ .unwrap(),
+ );
+ }
+
+ for ExportedType {
+ name,
+ typ,
+ cfg_attrs,
+ ..
+ } in custom_types
+ {
+ let const_literal = syn::LitStr::new(name, Span::call_site());
+
+ let cfg_attrs: Vec<_> = cfg_attrs
+ .iter()
+ .map(syn::Attribute::to_token_stream)
+ .collect();
+
+ set_const_statements.push(
+ syn::parse2::<syn::Stmt>(quote! {
+ #(#cfg_attrs)*
+ m.set_custom_type::<#typ>(#const_literal);
+ })
+ .unwrap(),
+ );
+ }
+
+ for item_mod in sub_modules {
+ item_mod.update_scope(parent_scope);
+ if item_mod.skipped() {
+ continue;
+ }
+ let module_name = item_mod.module_name();
+ let exported_name = syn::LitStr::new(item_mod.exported_name().as_ref(), Span::call_site());
+ let cfg_attrs = crate::attrs::collect_cfg_attr(item_mod.attrs());
+ add_mod_blocks.push(
+ syn::parse2::<syn::ExprBlock>(quote! {
+ {
+ #(#cfg_attrs)*
+ m.set_sub_module(#exported_name, self::#module_name::rhai_module_generate());
+ }
+ })
+ .unwrap(),
+ );
+ set_flattened_mod_blocks.push(
+ syn::parse2::<syn::ExprBlock>(quote! {
+ {
+ #(#cfg_attrs)*
+ self::#module_name::rhai_generate_into_module(m, flatten);
+ }
+ })
+ .unwrap(),
+ );
+ }
+
+ // NB: these are token streams, because re-parsing messes up "> >" vs ">>"
+ let mut gen_fn_tokens = Vec::new();
+
+ for function in fns {
+ function.update_scope(parent_scope);
+ if function.skipped() {
+ continue;
+ }
+ let fn_token_name = syn::Ident::new(
+ &format!("{}_token", function.name()),
+ function.name().span(),
+ );
+ let reg_names = function.exported_names();
+
+ let fn_input_types: Vec<_> = function
+ .arg_list()
+ .map(|fn_arg| match fn_arg {
+ syn::FnArg::Receiver(..) => panic!("internal error: receiver fn outside impl!?"),
+ syn::FnArg::Typed(syn::PatType { ref ty, .. }) => {
+ let arg_type = match flatten_type_groups(ty.as_ref()) {
+ syn::Type::Reference(syn::TypeReference {
+ mutability: None,
+ ref elem,
+ ..
+ }) => match flatten_type_groups(elem.as_ref()) {
+ syn::Type::Path(ref p) if p.path == str_type_path => {
+ syn::parse2::<syn::Type>(quote! {
+ ImmutableString })
+ .unwrap()
+ }
+ _ => panic!("internal error: non-string shared reference!?"),
+ },
+ syn::Type::Path(ref p) if p.path == string_type_path => {
+ syn::parse2::<syn::Type>(quote! {
+ ImmutableString })
+ .unwrap()
+ }
+ syn::Type::Reference(syn::TypeReference {
+ mutability: Some(_),
+ ref elem,
+ ..
+ }) => match flatten_type_groups(elem.as_ref()) {
+ syn::Type::Path(ref p) => syn::parse2::<syn::Type>(quote! {
+ #p })
+ .unwrap(),
+ _ => panic!("internal error: invalid mutable reference!?"),
+ },
+ t => t.clone(),
+ };
+
+ syn::parse2::<syn::Expr>(quote! {
+ TypeId::of::<#arg_type>()})
+ .unwrap()
+ }
+ })
+ .collect();
+
+ let cfg_attrs: Vec<_> = function
+ .cfg_attrs()
+ .iter()
+ .map(syn::Attribute::to_token_stream)
+ .collect();
+
+ for fn_literal in reg_names {
+ let mut namespace = FnNamespaceAccess::Internal;
+
+ match function.params().special {
+ FnSpecialAccess::None => (),
+ FnSpecialAccess::Index(..) | FnSpecialAccess::Property(..) => {
+ let reg_name = fn_literal.value();
+ if reg_name.starts_with(FN_GET)
+ || reg_name.starts_with(FN_SET)
+ || reg_name == FN_IDX_GET
+ || reg_name == FN_IDX_SET
+ {
+ namespace = FnNamespaceAccess::Global;
+ }
+ }
+ }
+
+ match function.params().namespace {
+ FnNamespaceAccess::Unset => (),
+ ns => namespace = ns,
+ }
+
+ let ns_str = syn::Ident::new(
+ match namespace {
+ FnNamespaceAccess::Unset => unreachable!("`namespace` should be set"),
+ FnNamespaceAccess::Global => "Global",
+ FnNamespaceAccess::Internal => "Internal",
+ },
+ fn_literal.span(),
+ );
+
+ #[cfg(feature = "metadata")]
+ let (param_names, comments) = (
+ quote! { Some(#fn_token_name::PARAM_NAMES) },
+ function
+ .comments()
+ .iter()
+ .map(|s| syn::LitStr::new(s, Span::call_site()))
+ .collect::<Vec<_>>(),
+ );
+ #[cfg(not(feature = "metadata"))]
+ let (param_names, comments) = (quote! { None }, Vec::<syn::LitStr>::new());
+
+ set_fn_statements.push(if comments.is_empty() {
+ syn::parse2::<syn::Stmt>(quote! {
+ #(#cfg_attrs)*
+ m.set_fn(#fn_literal, FnNamespace::#ns_str, FnAccess::Public,
+ #param_names, &[#(#fn_input_types),*], #fn_token_name().into());
+ })
+ .unwrap()
+ } else {
+ syn::parse2::<syn::Stmt>(quote! {
+ #(#cfg_attrs)*
+ m.set_fn_with_comments(#fn_literal, FnNamespace::#ns_str, FnAccess::Public,
+ #param_names, [#(#fn_input_types),*], [#(#comments),*], #fn_token_name().into());
+ })
+ .unwrap()
+ });
+ }
+
+ gen_fn_tokens.push(quote! {
+ #(#cfg_attrs)*
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct #fn_token_name();
+ });
+
+ gen_fn_tokens.push(function.generate_impl(&fn_token_name.to_string()));
+ }
+
+ let module_docs = if doc.is_empty() {
+ None
+ } else {
+ Some(
+ syn::parse2::<syn::Stmt>(quote! {
+ m.set_doc(#doc);
+ })
+ .unwrap(),
+ )
+ };
+
+ let mut generate_fn_call = syn::parse2::<syn::ItemMod>(quote! {
+ pub mod generate_info {
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ #module_docs
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ #(#set_fn_statements)*
+ #(#set_const_statements)*
+
+ if flatten {
+ #(#set_flattened_mod_blocks)*
+ } else {
+ #(#add_mod_blocks)*
+ }
+ }
+ }
+ })
+ .unwrap();
+
+ let (.., generate_call_content) = generate_fn_call.content.take().unwrap();
+
+ quote! {
+ #(#generate_call_content)*
+ #(#gen_fn_tokens)*
+ }
+}
+
+pub fn check_rename_collisions(fns: &[ExportedFn]) -> Result<(), syn::Error> {
+ fn make_key(name: impl ToString, item_fn: &ExportedFn) -> String {
+ item_fn
+ .arg_list()
+ .fold(name.to_string(), |mut arg_str, fn_arg| {
+ let type_string: String = match fn_arg {
+ syn::FnArg::Receiver(..) => unimplemented!("receiver rhai_fns not implemented"),
+ syn::FnArg::Typed(syn::PatType { ref ty, .. }) => print_type(ty),
+ };
+ arg_str.push('.');
+ arg_str.push_str(&type_string);
+ arg_str
+ })
+ }
+
+ let mut renames = BTreeMap::new();
+ let mut fn_defs = BTreeMap::new();
+
+ for item_fn in fns {
+ if !item_fn.params().name.is_empty() || item_fn.params().special != FnSpecialAccess::None {
+ let mut names: Vec<_> = item_fn
+ .params()
+ .name
+ .iter()
+ .map(|n| (n.clone(), n.clone()))
+ .collect();
+
+ if let Some((s, n, ..)) = item_fn.params().special.get_fn_name() {
+ names.push((s, n));
+ }
+
+ for (name, fn_name) in names {
+ let current_span = item_fn.params().span.unwrap();
+ let key = make_key(name, item_fn);
+ if let Some(other_span) = renames.insert(key, current_span) {
+ let mut err = syn::Error::new(
+ current_span,
+ format!("duplicate Rhai signature for '{fn_name}'"),
+ );
+ err.combine(syn::Error::new(
+ other_span,
+ format!("duplicated function renamed '{fn_name}'"),
+ ));
+ return Err(err);
+ }
+ }
+ } else {
+ let ident = item_fn.name();
+ if let Some(other_span) = fn_defs.insert(ident.to_string(), ident.span()) {
+ let mut err =
+ syn::Error::new(ident.span(), format!("duplicate function '{ident}'"));
+ err.combine(syn::Error::new(
+ other_span,
+ format!("duplicated function '{ident}'"),
+ ));
+ return Err(err);
+ }
+ let key = make_key(ident, item_fn);
+ if let Some(fn_span) = renames.get(&key) {
+ let mut err = syn::Error::new(
+ ident.span(),
+ format!("duplicate Rhai signature for '{ident}'"),
+ );
+ err.combine(syn::Error::new(
+ *fn_span,
+ format!("duplicated function '{ident}'"),
+ ));
+ return Err(err);
+ }
+ }
+ }
+
+ Ok(())
+}
diff --git a/rhai/codegen/src/test/function.rs b/rhai/codegen/src/test/function.rs
new file mode 100644
index 0000000..b45cc4b
--- /dev/null
+++ b/rhai/codegen/src/test/function.rs
@@ -0,0 +1,577 @@
+#[cfg(test)]
+mod function_tests {
+ use crate::function::ExportedFn;
+
+ use proc_macro2::TokenStream;
+ use quote::quote;
+
+ #[test]
+ fn minimal_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn do_nothing() { }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_eq!(&item_fn.name().to_string(), "do_nothing");
+ assert!(!item_fn.mutable_receiver());
+ assert!(item_fn.is_public());
+ assert!(item_fn.return_type().is_none());
+ assert_eq!(item_fn.arg_list().count(), 0);
+ }
+
+ #[test]
+ fn one_arg_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn do_something(x: usize) { }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_eq!(&item_fn.name().to_string(), "do_something");
+ assert_eq!(item_fn.arg_list().count(), 1);
+ assert!(!item_fn.mutable_receiver());
+ assert!(item_fn.is_public());
+ assert!(item_fn.return_type().is_none());
+
+ assert_eq!(
+ item_fn.arg_list().next().unwrap(),
+ &syn::parse2::<syn::FnArg>(quote! { x: usize }).unwrap()
+ );
+ }
+
+ #[test]
+ fn two_arg_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn do_something(x: usize, y: f32) { }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_eq!(&item_fn.name().to_string(), "do_something");
+ assert_eq!(item_fn.arg_list().count(), 2);
+ assert!(!item_fn.mutable_receiver());
+ assert!(item_fn.is_public());
+ assert!(item_fn.return_type().is_none());
+
+ assert_eq!(
+ item_fn.arg_list().next().unwrap(),
+ &syn::parse2::<syn::FnArg>(quote! { x: usize }).unwrap()
+ );
+ assert_eq!(
+ item_fn.arg_list().nth(1).unwrap(),
+ &syn::parse2::<syn::FnArg>(quote! { y: f32 }).unwrap()
+ );
+ }
+
+ #[test]
+ fn usize_returning_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn get_magic_number() -> usize { 42 }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_eq!(&item_fn.name().to_string(), "get_magic_number");
+ assert!(!item_fn.mutable_receiver());
+ assert!(item_fn.is_public());
+ assert_eq!(item_fn.arg_list().count(), 0);
+ assert_eq!(
+ item_fn.return_type().unwrap(),
+ &syn::Type::Path(syn::TypePath {
+ qself: None,
+ path: syn::parse2::<syn::Path>(quote! { usize }).unwrap()
+ })
+ );
+ }
+
+ #[test]
+ fn ref_returning_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn get_magic_phrase() -> &'static str { "open sesame" }
+ };
+
+ let err = syn::parse2::<ExportedFn>(input_tokens).unwrap_err();
+ assert_eq!(format!("{err}"), "Rhai functions cannot return references");
+ }
+
+ #[test]
+ fn ptr_returning_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn get_magic_phrase() -> *const str { "open sesame" }
+ };
+
+ let err = syn::parse2::<ExportedFn>(input_tokens).unwrap_err();
+ assert_eq!(format!("{err}"), "Rhai functions cannot return pointers");
+ }
+
+ #[test]
+ fn ref_arg_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn greet(who: &Person) { }
+ };
+
+ let err = syn::parse2::<ExportedFn>(input_tokens).unwrap_err();
+ assert_eq!(
+ format!("{err}"),
+ "references from Rhai in this position must be mutable"
+ );
+ }
+
+ #[test]
+ fn ref_second_arg_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn greet(count: usize, who: &Person) { }
+ };
+
+ let err = syn::parse2::<ExportedFn>(input_tokens).unwrap_err();
+ assert_eq!(
+ format!("{err}"),
+ "function parameters other than the first one cannot be passed by reference"
+ );
+ }
+
+ #[test]
+ fn mut_ref_second_arg_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn give(item_name: &str, who: &mut Person) { }
+ };
+
+ let err = syn::parse2::<ExportedFn>(input_tokens).unwrap_err();
+ assert_eq!(
+ format!("{err}"),
+ "function parameters other than the first one cannot be passed by reference"
+ );
+ }
+
+ #[test]
+ fn str_arg_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn log(message: &str) { }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_eq!(&item_fn.name().to_string(), "log");
+ assert_eq!(item_fn.arg_list().count(), 1);
+ assert!(!item_fn.mutable_receiver());
+ assert!(item_fn.is_public());
+ assert!(item_fn.return_type().is_none());
+
+ assert_eq!(
+ item_fn.arg_list().next().unwrap(),
+ &syn::parse2::<syn::FnArg>(quote! { message: &str }).unwrap()
+ );
+ }
+
+ #[test]
+ fn str_second_arg_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn log(level: usize, message: &str) { }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_eq!(&item_fn.name().to_string(), "log");
+ assert_eq!(item_fn.arg_list().count(), 2);
+ assert!(!item_fn.mutable_receiver());
+ assert!(item_fn.is_public());
+ assert!(item_fn.return_type().is_none());
+
+ assert_eq!(
+ item_fn.arg_list().next().unwrap(),
+ &syn::parse2::<syn::FnArg>(quote! { level: usize }).unwrap()
+ );
+ assert_eq!(
+ item_fn.arg_list().nth(1).unwrap(),
+ &syn::parse2::<syn::FnArg>(quote! { message: &str }).unwrap()
+ );
+ }
+
+ #[test]
+ fn private_fn() {
+ let input_tokens: TokenStream = quote! {
+ fn do_nothing() { }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_eq!(&item_fn.name().to_string(), "do_nothing");
+ assert!(!item_fn.mutable_receiver());
+ assert!(!item_fn.is_public());
+ assert!(item_fn.return_type().is_none());
+ assert_eq!(item_fn.arg_list().count(), 0);
+ }
+
+ #[test]
+ fn receiver_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn act_upon(&mut self) { }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_eq!(&item_fn.name().to_string(), "act_upon");
+ assert!(item_fn.mutable_receiver());
+ assert!(item_fn.is_public());
+ assert!(item_fn.return_type().is_none());
+ assert_eq!(item_fn.arg_list().count(), 1);
+ }
+
+ #[test]
+ fn immutable_receiver_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn act_upon(&self) { }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_eq!(&item_fn.name().to_string(), "act_upon");
+ assert!(item_fn.mutable_receiver());
+ assert!(item_fn.is_public());
+ assert!(item_fn.return_type().is_none());
+ assert_eq!(item_fn.arg_list().count(), 1);
+ }
+}
+
+#[cfg(test)]
+mod generate_tests {
+ use crate::function::ExportedFn;
+
+ use proc_macro2::TokenStream;
+ use quote::quote;
+
+ fn assert_streams_eq(actual: TokenStream, expected: TokenStream) {
+ let actual = actual.to_string();
+ let expected = expected.to_string();
+ if actual != expected {
+ let mut counter = 0;
+ let _iter = actual.chars().zip(expected.chars()).skip_while(|(a, e)| {
+ if *a == *e {
+ counter += 1;
+ true
+ } else {
+ false
+ }
+ });
+ let (_actual_diff, _expected_diff) = {
+ let mut actual_diff = String::new();
+ let mut expected_diff = String::new();
+ for (a, e) in _iter.take(50) {
+ actual_diff.push(a);
+ expected_diff.push(e);
+ }
+ (actual_diff, expected_diff)
+ };
+ eprintln!("actual != expected, diverge at char {counter}");
+ // eprintln!(" actual: {}", _actual_diff);
+ // eprintln!("expected: {}", _expected_diff);
+ // assert!(false);
+ }
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn minimal_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn do_nothing() { }
+ };
+
+ let expected_tokens = quote! {
+ #[automatically_derived]
+ pub mod rhai_fn_do_nothing {
+ use super::*;
+ #[doc(hidden)]
+ pub struct Token();
+ #[doc(hidden)]
+ impl Token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 0usize] { [] }
+ }
+ impl PluginFunction for Token {
+ #[inline(always)] fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ Ok(Dynamic::from(do_nothing()))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ #[allow(unused)]
+ #[doc(hidden)]
+ #[inline(always)] pub fn dynamic_result_fn() -> RhaiResult {
+ Ok(Dynamic::from(do_nothing()))
+ }
+ }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_streams_eq(item_fn.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_arg_usize_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn do_something(x: usize) { }
+ };
+
+ let expected_tokens = quote! {
+ #[automatically_derived]
+ pub mod rhai_fn_do_something {
+ use super::*;
+ #[doc(hidden)]
+ pub struct Token();
+ #[doc(hidden)]
+ impl Token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: usize", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<usize>()] }
+ }
+ impl PluginFunction for Token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = mem::take(args[0usize]).cast::<usize>();
+ Ok(Dynamic::from(do_something(arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ #[allow(unused)]
+ #[doc(hidden)]
+ #[inline(always)] pub fn dynamic_result_fn(x: usize) -> RhaiResult {
+ Ok(Dynamic::from(do_something(x)))
+ }
+ }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_streams_eq(item_fn.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_arg_fn_with_context() {
+ let input_tokens: TokenStream = quote! {
+ pub fn do_something(context: NativeCallContext, x: usize) {}
+ };
+
+ let expected_tokens = quote! {
+ #[automatically_derived]
+ pub mod rhai_fn_do_something {
+ use super::*;
+ #[doc(hidden)]
+ pub struct Token();
+ #[doc(hidden)]
+ impl Token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: usize", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<usize>()] }
+ }
+ impl PluginFunction for Token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = mem::take(args[0usize]).cast::<usize>();
+ Ok(Dynamic::from(do_something(context.unwrap(), arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { true }
+ }
+ #[allow(unused)]
+ #[doc(hidden)]
+ #[inline(always)] pub fn dynamic_result_fn(context: NativeCallContext, x: usize) -> RhaiResult {
+ Ok(Dynamic::from(do_something(context, x)))
+ }
+ }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert!(item_fn.pass_context());
+ assert_streams_eq(item_fn.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn return_dynamic() {
+ let input_tokens: TokenStream = quote! {
+ pub fn return_dynamic() -> (((rhai::Dynamic))) {
+ ().into()
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[automatically_derived]
+ pub mod rhai_fn_return_dynamic {
+ use super::*;
+ #[doc(hidden)]
+ pub struct Token();
+ #[doc(hidden)]
+ impl Token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["rhai::Dynamic"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 0usize] { [] }
+ }
+ impl PluginFunction for Token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ Ok(Dynamic::from(return_dynamic()))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ #[allow(unused)]
+ #[doc(hidden)]
+ #[inline(always)] pub fn dynamic_result_fn() -> RhaiResult {
+ Ok(Dynamic::from(return_dynamic()))
+ }
+ }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_streams_eq(item_fn.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_arg_usize_fn_impl() {
+ let input_tokens: TokenStream = quote! {
+ pub fn do_something(x: usize) { }
+ };
+
+ let expected_tokens = quote! {
+ #[doc(hidden)]
+ impl TestStruct {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: usize", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<usize>()] }
+ }
+ impl PluginFunction for TestStruct {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = mem::take(args[0usize]).cast::<usize>();
+ Ok(Dynamic::from(do_something(arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_streams_eq(item_fn.generate_impl("TestStruct"), expected_tokens);
+ }
+
+ #[test]
+ fn two_arg_returning_usize_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn add_together(x: usize, y: usize) -> usize { x + y }
+ };
+
+ let expected_tokens = quote! {
+ #[automatically_derived]
+ pub mod rhai_fn_add_together {
+ use super::*;
+ #[doc(hidden)]
+ pub struct Token();
+ #[doc(hidden)]
+ impl Token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: usize", "y: usize", "usize"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 2usize] { [TypeId::of::<usize>(), TypeId::of::<usize>()] }
+ }
+ impl PluginFunction for Token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = mem::take(args[0usize]).cast::<usize>();
+ let arg1 = mem::take(args[1usize]).cast::<usize>();
+ Ok(Dynamic::from(add_together(arg0, arg1)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ #[allow(unused)]
+ #[doc(hidden)]
+ #[inline(always)] pub fn dynamic_result_fn(x: usize, y: usize) -> RhaiResult {
+ Ok(Dynamic::from(add_together(x, y)))
+ }
+ }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert_streams_eq(item_fn.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn mut_arg_usize_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn increment(x: &mut usize, y: usize) { *x += y; }
+ };
+
+ let expected_tokens = quote! {
+ #[automatically_derived]
+ pub mod rhai_fn_increment {
+ use super::*;
+ #[doc(hidden)]
+ pub struct Token();
+ #[doc(hidden)]
+ impl Token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut usize", "y: usize", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 2usize] { [TypeId::of::<usize>(), TypeId::of::<usize>()] }
+ }
+ impl PluginFunction for Token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg1 = mem::take(args[1usize]).cast::<usize>();
+ let arg0 = &mut args[0usize].write_lock::<usize>().unwrap();
+ Ok(Dynamic::from(increment(arg0, arg1)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ #[allow(unused)]
+ #[doc(hidden)]
+ #[inline(always)] pub fn dynamic_result_fn(x: &mut usize, y: usize) -> RhaiResult {
+ Ok(Dynamic::from(increment(x, y)))
+ }
+ }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert!(item_fn.mutable_receiver());
+ assert_streams_eq(item_fn.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn str_arg_fn() {
+ let input_tokens: TokenStream = quote! {
+ pub fn special_print(message: &str) { eprintln!("----{}----", message); }
+ };
+
+ let expected_tokens = quote! {
+ #[automatically_derived]
+ pub mod rhai_fn_special_print {
+ use super::*;
+ #[doc(hidden)]
+ pub struct Token();
+ #[doc(hidden)]
+ impl Token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["message: &str", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<ImmutableString>()] }
+ }
+ impl PluginFunction for Token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = mem::take(args[0usize]).into_immutable_string().unwrap();
+ Ok(Dynamic::from(special_print(&arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ #[allow(unused)]
+ #[doc(hidden)]
+ #[inline(always)] pub fn dynamic_result_fn(message: &str) -> RhaiResult {
+ Ok(Dynamic::from(special_print(message)))
+ }
+ }
+ };
+
+ let item_fn = syn::parse2::<ExportedFn>(input_tokens).unwrap();
+ assert!(!item_fn.mutable_receiver());
+ assert_streams_eq(item_fn.generate(), expected_tokens);
+ }
+}
diff --git a/rhai/codegen/src/test/mod.rs b/rhai/codegen/src/test/mod.rs
new file mode 100644
index 0000000..452c9b2
--- /dev/null
+++ b/rhai/codegen/src/test/mod.rs
@@ -0,0 +1,2 @@
+mod function;
+mod module;
diff --git a/rhai/codegen/src/test/module.rs b/rhai/codegen/src/test/module.rs
new file mode 100644
index 0000000..0f96095
--- /dev/null
+++ b/rhai/codegen/src/test/module.rs
@@ -0,0 +1,2552 @@
+#[cfg(test)]
+mod module_tests {
+ use crate::module::Module;
+
+ use proc_macro2::TokenStream;
+ use quote::quote;
+
+ #[test]
+ fn empty_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod empty { }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert!(item_mod.fns().is_empty());
+ assert!(item_mod.consts().is_empty());
+ }
+
+ #[test]
+ fn one_factory_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert!(item_mod.consts().is_empty());
+ assert_eq!(item_mod.fns().len(), 1);
+ assert_eq!(item_mod.fns()[0].name().to_string(), "get_mystic_number");
+ assert_eq!(item_mod.fns()[0].arg_count(), 0);
+ assert_eq!(
+ item_mod.fns()[0].return_type().unwrap(),
+ &syn::parse2::<syn::Type>(quote! { INT }).unwrap()
+ );
+ }
+
+ #[test]
+ fn one_factory_fn_with_custom_type_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ pub type Hello = ();
+
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert!(item_mod.consts().is_empty());
+ assert_eq!(item_mod.custom_types().len(), 1);
+ assert_eq!(item_mod.custom_types()[0].name.to_string(), "Hello");
+ assert_eq!(item_mod.fns().len(), 1);
+ assert_eq!(item_mod.fns()[0].name().to_string(), "get_mystic_number");
+ assert_eq!(item_mod.fns()[0].arg_count(), 0);
+ assert_eq!(
+ item_mod.fns()[0].return_type().unwrap(),
+ &syn::parse2::<syn::Type>(quote! { INT }).unwrap()
+ );
+ }
+
+ #[test]
+ #[cfg(feature = "metadata")]
+ fn one_factory_fn_with_comments_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ /// This is a doc-comment.
+ /// Another line.
+ /** block doc-comment */
+ // Regular comment
+ /// Final line.
+ /** doc-comment
+ in multiple lines
+ */
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert!(item_mod.consts().is_empty());
+ assert_eq!(item_mod.fns().len(), 1);
+ assert_eq!(item_mod.fns()[0].name().to_string(), "get_mystic_number");
+ assert_eq!(
+ item_mod.fns()[0].comments().to_vec(),
+ vec![
+ "\
+ /// This is a doc-comment.\n\
+ /// Another line.\n\
+ /// block doc-comment \n\
+ /// Final line.",
+ "/** doc-comment\n in multiple lines\n */"
+ ]
+ );
+ assert_eq!(item_mod.fns()[0].arg_count(), 0);
+ assert_eq!(
+ item_mod.fns()[0].return_type().unwrap(),
+ &syn::parse2::<syn::Type>(quote! { INT }).unwrap()
+ );
+ }
+
+ #[test]
+ fn one_single_arg_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ pub fn add_one_to(x: INT) -> INT {
+ x + 1
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert!(item_mod.consts().is_empty());
+ assert_eq!(item_mod.fns().len(), 1);
+ assert_eq!(item_mod.fns()[0].name().to_string(), "add_one_to");
+ assert_eq!(item_mod.fns()[0].arg_count(), 1);
+ assert_eq!(
+ item_mod.fns()[0].arg_list().next().unwrap(),
+ &syn::parse2::<syn::FnArg>(quote! { x: INT }).unwrap()
+ );
+ assert_eq!(
+ item_mod.fns()[0].return_type().unwrap(),
+ &syn::parse2::<syn::Type>(quote! { INT }).unwrap()
+ );
+ }
+
+ #[test]
+ fn one_double_arg_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ pub fn add_together(x: INT, y: INT) -> INT {
+ x + y
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ let mut args = item_mod.fns()[0].arg_list();
+ assert!(item_mod.consts().is_empty());
+ assert_eq!(item_mod.fns().len(), 1);
+ assert_eq!(item_mod.fns()[0].name().to_string(), "add_together");
+ assert_eq!(item_mod.fns()[0].arg_count(), 2);
+ assert_eq!(
+ args.next().unwrap(),
+ &syn::parse2::<syn::FnArg>(quote! { x: INT }).unwrap()
+ );
+ assert_eq!(
+ args.next().unwrap(),
+ &syn::parse2::<syn::FnArg>(quote! { y: INT }).unwrap()
+ );
+ assert!(args.next().is_none());
+ assert_eq!(
+ item_mod.fns()[0].return_type().unwrap(),
+ &syn::parse2::<syn::Type>(quote! { INT }).unwrap()
+ );
+ }
+
+ #[test]
+ fn one_constant_nested_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_constant {
+ pub mod it_is {
+ pub const MYSTIC_NUMBER: INT = 42;
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert!(item_mod.fns().is_empty());
+ assert!(item_mod.consts().is_empty());
+ assert_eq!(item_mod.sub_modules().len(), 1);
+ assert_eq!(&item_mod.sub_modules()[0].consts()[0].name, "MYSTIC_NUMBER");
+ assert_eq!(
+ item_mod.sub_modules()[0].consts()[0].expr,
+ syn::parse2::<syn::Expr>(quote! { 42 }).unwrap()
+ );
+ }
+
+ #[test]
+ fn one_skipped_fn_nested_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ pub mod skip_this {
+ #[rhai_fn(skip)]
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert!(item_mod.fns().is_empty());
+ assert!(item_mod.consts().is_empty());
+ assert_eq!(item_mod.sub_modules().len(), 1);
+ assert_eq!(item_mod.sub_modules()[0].fns().len(), 1);
+ assert!(item_mod.sub_modules()[0].fns()[0].skipped());
+ assert!(item_mod.sub_modules()[0].consts().is_empty());
+ assert!(item_mod.sub_modules()[0].sub_modules().is_empty());
+ }
+
+ #[test]
+ fn one_skipped_nested_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ #[rhai_mod(skip)]
+ pub mod skip_this {
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert!(item_mod.fns().is_empty());
+ assert!(item_mod.consts().is_empty());
+ assert_eq!(item_mod.sub_modules().len(), 1);
+ assert!(item_mod.sub_modules()[0].skipped());
+ }
+
+ #[test]
+ fn one_constant_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_constant {
+ pub const MYSTIC_NUMBER: INT = 42;
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert!(item_mod.fns().is_empty());
+ assert_eq!(item_mod.consts().len(), 1);
+ assert_eq!(&item_mod.consts()[0].name, "MYSTIC_NUMBER");
+ assert_eq!(
+ item_mod.consts()[0].expr,
+ syn::parse2::<syn::Expr>(quote! { 42 }).unwrap()
+ );
+ }
+
+ #[test]
+ fn one_skipped_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ #[rhai_fn(skip)]
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_eq!(item_mod.fns().len(), 1);
+ assert!(item_mod.fns()[0].skipped());
+ assert!(item_mod.consts().is_empty());
+ }
+
+ #[test]
+ fn one_private_constant_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_constant {
+ const MYSTIC_NUMBER: INT = 42;
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert!(item_mod.fns().is_empty());
+ assert!(item_mod.consts().is_empty());
+ }
+}
+
+#[cfg(test)]
+mod generate_tests {
+ use crate::module::Module;
+
+ use proc_macro2::TokenStream;
+ use quote::quote;
+
+ fn assert_streams_eq(actual: TokenStream, expected: TokenStream) {
+ let actual = actual.to_string();
+ let expected = expected.to_string();
+ if actual != expected {
+ let mut counter = 0;
+ let _iter = actual.chars().zip(expected.chars()).skip_while(|(a, e)| {
+ if *a == *e {
+ counter += 1;
+ true
+ } else {
+ false
+ }
+ });
+ let (_actual_diff, _expected_diff) = {
+ let mut actual_diff = String::new();
+ let mut expected_diff = String::new();
+ for (a, e) in _iter.take(50) {
+ actual_diff.push(a);
+ expected_diff.push(e);
+ }
+ (actual_diff, expected_diff)
+ };
+ eprintln!("actual != expected, diverge at char {counter}");
+ // eprintln!(" actual: {}", _actual_diff);
+ // eprintln!("expected: {}", _expected_diff);
+ // assert!(false);
+ }
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn empty_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod empty { }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod empty {
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ if flatten {} else {}
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_factory_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("get_mystic_number", FnNamespace::Internal, FnAccess::Public,
+ Some(get_mystic_number_token::PARAM_NAMES), &[],
+ get_mystic_number_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct get_mystic_number_token();
+ #[doc(hidden)]
+ impl get_mystic_number_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["INT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 0usize] { [] }
+ }
+ impl PluginFunction for get_mystic_number_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ Ok(Dynamic::from(get_mystic_number()))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_factory_fn_with_comments_module() {
+ let input_tokens: TokenStream = quote! {
+ /// This is the one_fn module!
+ /** block doc-comment
+ * multi-line
+ */
+ /// Another line!
+ /// Final line!
+ pub mod one_fn {
+ /// This is a doc-comment.
+ /// Another line.
+ /** block doc-comment */
+ // Regular comment
+ /// Final line.
+ /** doc-comment
+ in multiple lines
+ */
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ /// This is the one_fn module!
+ /** block doc-comment
+ * multi-line
+ */
+ /// Another line!
+ /// Final line!
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ /// This is a doc-comment.
+ /// Another line.
+ /** block doc-comment */
+ // Regular comment
+ /// Final line.
+ /** doc-comment
+ in multiple lines
+ */
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ m.set_doc("/// This is the one_fn module!\n/** block doc-comment\n * multi-line\n */\n/// Another line!\n/// Final line!");
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn_with_comments("get_mystic_number", FnNamespace::Internal, FnAccess::Public,
+ Some(get_mystic_number_token::PARAM_NAMES), [], [
+ "/// This is a doc-comment.\n/// Another line.\n/// block doc-comment \n/// Final line.",
+ "/** doc-comment\n in multiple lines\n */"
+ ], get_mystic_number_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct get_mystic_number_token();
+ #[doc(hidden)]
+ impl get_mystic_number_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["INT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 0usize] { [] }
+ }
+ impl PluginFunction for get_mystic_number_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ Ok(Dynamic::from(get_mystic_number()))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_single_arg_global_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_global_fn {
+ #[rhai_fn(global)]
+ pub fn add_one_to(x: INT) -> INT {
+ x + 1
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_global_fn {
+ pub fn add_one_to(x: INT) -> INT {
+ x + 1
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("add_one_to", FnNamespace::Global, FnAccess::Public,
+ Some(add_one_to_token::PARAM_NAMES), &[TypeId::of::<INT>()],
+ add_one_to_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct add_one_to_token();
+ #[doc(hidden)]
+ impl add_one_to_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: INT", "INT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<INT>()] }
+ }
+ impl PluginFunction for add_one_to_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = mem::take(args[0usize]).cast::<INT>();
+ Ok(Dynamic::from(add_one_to(arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_single_arg_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ pub fn add_one_to(x: INT) -> INT {
+ x + 1
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ pub fn add_one_to(x: INT) -> INT {
+ x + 1
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("add_one_to", FnNamespace::Internal, FnAccess::Public, Some(add_one_to_token::PARAM_NAMES),
+ &[TypeId::of::<INT>()],
+ add_one_to_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct add_one_to_token();
+ #[doc(hidden)]
+ impl add_one_to_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: INT", "INT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<INT>()] }
+ }
+ impl PluginFunction for add_one_to_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = mem::take(args[0usize]).cast::<INT>();
+ Ok(Dynamic::from(add_one_to(arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn two_fn_overload_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod two_fns {
+ #[rhai_fn(name = "add_n")]
+ pub fn add_one_to(x: INT) -> INT {
+ x + 1
+ }
+
+ #[rhai_fn(name = "add_n")]
+ pub fn add_n_to(x: INT, y: INT) -> INT {
+ x + y
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod two_fns {
+ pub fn add_one_to(x: INT) -> INT {
+ x + 1
+ }
+
+ pub fn add_n_to(x: INT, y: INT) -> INT {
+ x + y
+ }
+
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("add_n", FnNamespace::Internal, FnAccess::Public, Some(add_one_to_token::PARAM_NAMES),
+ &[TypeId::of::<INT>()],
+ add_one_to_token().into());
+ m.set_fn("add_n", FnNamespace::Internal, FnAccess::Public, Some(add_n_to_token::PARAM_NAMES),
+ &[TypeId::of::<INT>(), TypeId::of::<INT>()],
+ add_n_to_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct add_one_to_token();
+ #[doc(hidden)]
+ impl add_one_to_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: INT", "INT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<INT>()] }
+ }
+ impl PluginFunction for add_one_to_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = mem::take(args[0usize]).cast::<INT>();
+ Ok(Dynamic::from(add_one_to(arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct add_n_to_token();
+ #[doc(hidden)]
+ impl add_n_to_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: INT", "y: INT", "INT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 2usize] { [TypeId::of::<INT>(), TypeId::of::<INT>()] }
+ }
+ impl PluginFunction for add_n_to_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = mem::take(args[0usize]).cast::<INT>();
+ let arg1 = mem::take(args[1usize]).cast::<INT>();
+ Ok(Dynamic::from(add_n_to(arg0, arg1)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_double_arg_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ pub fn add_together(x: INT, y: INT) -> INT {
+ x + y
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ pub fn add_together(x: INT, y: INT) -> INT {
+ x + y
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("add_together", FnNamespace::Internal, FnAccess::Public, Some(add_together_token::PARAM_NAMES),
+ &[TypeId::of::<INT>(), TypeId::of::<INT>()],
+ add_together_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct add_together_token();
+ #[doc(hidden)]
+ impl add_together_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: INT", "y: INT", "INT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 2usize] { [TypeId::of::<INT>(), TypeId::of::<INT>()] }
+ }
+ impl PluginFunction for add_together_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = mem::take(args[0usize]).cast::<INT>();
+ let arg1 = mem::take(args[1usize]).cast::<INT>();
+ Ok(Dynamic::from(add_together(arg0, arg1)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_double_rename_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ #[rhai_fn(name = "add", name = "+", name = "add_together")]
+ pub fn add_together(x: INT, y: INT) -> INT {
+ x + y
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ pub fn add_together(x: INT, y: INT) -> INT {
+ x + y
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("add", FnNamespace::Internal, FnAccess::Public, Some(add_together_token::PARAM_NAMES),
+ &[TypeId::of::<INT>(), TypeId::of::<INT>()],
+ add_together_token().into());
+ m.set_fn("+", FnNamespace::Internal, FnAccess::Public, Some(add_together_token::PARAM_NAMES),
+ &[TypeId::of::<INT>(), TypeId::of::<INT>()],
+ add_together_token().into());
+ m.set_fn("add_together", FnNamespace::Internal, FnAccess::Public, Some(add_together_token::PARAM_NAMES),
+ &[TypeId::of::<INT>(), TypeId::of::<INT>()],
+ add_together_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct add_together_token();
+ #[doc(hidden)]
+ impl add_together_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: INT", "y: INT", "INT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 2usize] { [TypeId::of::<INT>(), TypeId::of::<INT>()] }
+ }
+ impl PluginFunction for add_together_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = mem::take(args[0usize]).cast::<INT>();
+ let arg1 = mem::take(args[1usize]).cast::<INT>();
+ Ok(Dynamic::from(add_together(arg0, arg1)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_constant_type_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_constant {
+ #[derive(Debug, Clone)]
+ pub struct Foo(pub INT);
+
+ pub type Hello = Foo;
+
+ pub const MYSTIC_NUMBER: Foo = Foo(42);
+
+ pub fn get_mystic_number(x: &mut Hello) -> INT {
+ x.0
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_constant {
+ #[derive(Debug, Clone)]
+ pub struct Foo(pub INT);
+
+ pub type Hello = Foo;
+
+ pub const MYSTIC_NUMBER: Foo = Foo(42);
+
+ pub fn get_mystic_number(x: &mut Hello) -> INT {
+ x.0
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("get_mystic_number", FnNamespace::Internal, FnAccess::Public,
+ Some(get_mystic_number_token::PARAM_NAMES), &[TypeId::of::<Hello>()],
+ get_mystic_number_token().into());
+ m.set_var("MYSTIC_NUMBER", MYSTIC_NUMBER);
+ m.set_custom_type::<Foo>("Hello");
+ if flatten {} else {}
+ }
+
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct get_mystic_number_token();
+ #[doc(hidden)]
+ impl get_mystic_number_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut Hello", "INT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<Hello>()] }
+ }
+ impl PluginFunction for get_mystic_number_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = &mut args[0usize].write_lock::<Hello>().unwrap();
+ Ok(Dynamic::from(get_mystic_number(arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_constant_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_constant {
+ pub const MYSTIC_NUMBER: INT = 42;
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_constant {
+ pub const MYSTIC_NUMBER: INT = 42;
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_var("MYSTIC_NUMBER", MYSTIC_NUMBER);
+ if flatten {} else {}
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_constant_module_imports_preserved() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_constant {
+ pub use rhai::INT;
+ pub const MYSTIC_NUMBER: INT = 42;
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_constant {
+ pub use rhai::INT;
+ pub const MYSTIC_NUMBER: INT = 42;
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_var("MYSTIC_NUMBER", MYSTIC_NUMBER);
+ if flatten {} else {}
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_private_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ fn get_mystic_number() -> INT {
+ 42
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ fn get_mystic_number() -> INT {
+ 42
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ if flatten {} else {}
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_skipped_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ #[rhai_fn(skip)]
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ if flatten {} else {}
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_skipped_sub_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ #[rhai_mod(skip)]
+ pub mod inner_secrets {
+ pub const SECRET_NUMBER: INT = 86;
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ pub fn get_mystic_number() -> INT {
+ 42
+ }
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod inner_secrets {
+ pub const SECRET_NUMBER: INT = 86;
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("get_mystic_number", FnNamespace::Internal, FnAccess::Public,
+ Some(get_mystic_number_token::PARAM_NAMES), &[],
+ get_mystic_number_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct get_mystic_number_token();
+ #[doc(hidden)]
+ impl get_mystic_number_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["INT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 0usize] { [] }
+ }
+ impl PluginFunction for get_mystic_number_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ Ok(Dynamic::from(get_mystic_number()))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_private_constant_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_constant {
+ const MYSTIC_NUMBER: INT = 42;
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_constant {
+ const MYSTIC_NUMBER: INT = 42;
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ if flatten {} else {}
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_str_arg_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod str_fn {
+ pub fn print_out_to(x: &str) {
+ x + 1
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod str_fn {
+ pub fn print_out_to(x: &str) {
+ x + 1
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("print_out_to", FnNamespace::Internal, FnAccess::Public, Some(print_out_to_token::PARAM_NAMES),
+ &[TypeId::of::<ImmutableString>()],
+ print_out_to_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct print_out_to_token();
+ #[doc(hidden)]
+ impl print_out_to_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &str", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<ImmutableString>()] }
+ }
+ impl PluginFunction for print_out_to_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = mem::take(args[0usize]).into_immutable_string().unwrap();
+ Ok(Dynamic::from(print_out_to(&arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_string_arg_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod str_fn {
+ pub fn print_out_to(x: String) {
+ x + 1
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod str_fn {
+ pub fn print_out_to(x: String) {
+ x + 1
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("print_out_to", FnNamespace::Internal, FnAccess::Public, Some(print_out_to_token::PARAM_NAMES),
+ &[TypeId::of::<ImmutableString>()],
+ print_out_to_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct print_out_to_token();
+ #[doc(hidden)]
+ impl print_out_to_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: String", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<ImmutableString>()] }
+ }
+ impl PluginFunction for print_out_to_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = mem::take(args[0usize]).into_string().unwrap();
+ Ok(Dynamic::from(print_out_to(arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { false }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn mut_ref_pure_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod ref_fn {
+ #[rhai_fn(pure)]
+ pub fn foo(x: &mut FLOAT, y: INT) -> FLOAT {
+ *x + y as FLOAT
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod ref_fn {
+ pub fn foo(x: &mut FLOAT, y: INT) -> FLOAT {
+ *x + y as FLOAT
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("foo", FnNamespace::Internal, FnAccess::Public, Some(foo_token::PARAM_NAMES),
+ &[TypeId::of::<FLOAT>(), TypeId::of::<INT>()],
+ foo_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct foo_token();
+ #[doc(hidden)]
+ impl foo_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut FLOAT", "y: INT", "FLOAT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 2usize] { [TypeId::of::<FLOAT>(), TypeId::of::<INT>()] }
+ }
+ impl PluginFunction for foo_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg1 = mem::take(args[1usize]).cast::<INT>();
+ let arg0 = &mut args[0usize].write_lock::<FLOAT>().unwrap();
+ Ok(Dynamic::from(foo(arg0, arg1)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { true }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_mut_ref_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod ref_fn {
+ pub fn increment(x: &mut FLOAT) {
+ *x += 1.0 as FLOAT;
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod ref_fn {
+ pub fn increment(x: &mut FLOAT) {
+ *x += 1.0 as FLOAT;
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("increment", FnNamespace::Internal, FnAccess::Public, Some(increment_token::PARAM_NAMES),
+ &[TypeId::of::<FLOAT>()],
+ increment_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct increment_token();
+ #[doc(hidden)]
+ impl increment_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut FLOAT", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<FLOAT>()] }
+ }
+ impl PluginFunction for increment_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = &mut args[0usize].write_lock::<FLOAT>().unwrap();
+ Ok(Dynamic::from(increment(arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_fn_nested_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ pub mod it_is {
+ pub fn increment(x: &mut FLOAT) {
+ *x += 1.0 as FLOAT;
+ }
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod it_is {
+ pub fn increment(x: &mut FLOAT) {
+ *x += 1.0 as FLOAT;
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("increment", FnNamespace::Internal, FnAccess::Public, Some(increment_token::PARAM_NAMES),
+ &[TypeId::of::<FLOAT>()],
+ increment_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct increment_token();
+ #[doc(hidden)]
+ impl increment_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut FLOAT", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<FLOAT>()] }
+ }
+ impl PluginFunction for increment_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = &mut args[0usize].write_lock::<FLOAT>().unwrap();
+ Ok(Dynamic::from(increment(arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ if flatten {
+ { self::it_is::rhai_generate_into_module(m, flatten); }
+ } else {
+ { m.set_sub_module("it_is", self::it_is::rhai_module_generate()); }
+ }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_fn_with_cfg_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ #[cfg(not(feature = "no_float"))]
+ pub mod it_is {
+ pub fn increment(x: &mut FLOAT) {
+ *x += 1.0 as FLOAT;
+ }
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ #[cfg(not(feature = "no_float"))]
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod it_is {
+ pub fn increment(x: &mut FLOAT) {
+ *x += 1.0 as FLOAT;
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("increment", FnNamespace::Internal, FnAccess::Public, Some(increment_token::PARAM_NAMES),
+ &[TypeId::of::<FLOAT>()],
+ increment_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct increment_token();
+ #[doc(hidden)]
+ impl increment_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut FLOAT", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<FLOAT>()] }
+ }
+ impl PluginFunction for increment_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = &mut args[0usize].write_lock::<FLOAT>().unwrap();
+ Ok(Dynamic::from(increment(arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ if flatten {
+ {
+ #[cfg(not(feature = "no_float"))]
+ self::it_is::rhai_generate_into_module(m, flatten);
+ }
+ } else {
+ {
+ #[cfg(not(feature = "no_float"))]
+ m.set_sub_module("it_is", self::it_is::rhai_module_generate());
+ }
+ }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_getter_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ #[rhai_fn(get = "square")]
+ pub fn int_foo(x: &mut u64) -> u64 {
+ (*x) * (*x)
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ pub fn int_foo(x: &mut u64) -> u64 {
+ (*x) * (*x)
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("get$square", FnNamespace::Global, FnAccess::Public, Some(int_foo_token::PARAM_NAMES),
+ &[TypeId::of::<u64>()],
+ int_foo_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct int_foo_token();
+ #[doc(hidden)]
+ impl int_foo_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut u64", "u64"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<u64>()] }
+ }
+ impl PluginFunction for int_foo_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = &mut args[0usize].write_lock::<u64>().unwrap();
+ Ok(Dynamic::from(int_foo(arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_getter_and_rename_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ #[rhai_fn(name = "square", get = "square")]
+ pub fn int_foo(x: &mut u64) -> u64 {
+ (*x) * (*x)
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ pub fn int_foo(x: &mut u64) -> u64 {
+ (*x) * (*x)
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("square", FnNamespace::Internal, FnAccess::Public, Some(int_foo_token::PARAM_NAMES),
+ &[TypeId::of::<u64>()],
+ int_foo_token().into());
+ m.set_fn("get$square", FnNamespace::Global, FnAccess::Public, Some(int_foo_token::PARAM_NAMES),
+ &[TypeId::of::<u64>()],
+ int_foo_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct int_foo_token();
+ #[doc(hidden)]
+ impl int_foo_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut u64", "u64"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 1usize] { [TypeId::of::<u64>()] }
+ }
+ impl PluginFunction for int_foo_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg0 = &mut args[0usize].write_lock::<u64>().unwrap();
+ Ok(Dynamic::from(int_foo(arg0)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_setter_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ #[rhai_fn(set = "squared")]
+ pub fn int_foo(x: &mut u64, y: u64) {
+ *x = y * y
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ pub fn int_foo(x: &mut u64, y: u64) {
+ *x = y * y
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("set$squared", FnNamespace::Global, FnAccess::Public, Some(int_foo_token::PARAM_NAMES),
+ &[TypeId::of::<u64>(), TypeId::of::<u64>()],
+ int_foo_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct int_foo_token();
+ #[doc(hidden)]
+ impl int_foo_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut u64", "y: u64", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 2usize] { [TypeId::of::<u64>(), TypeId::of::<u64>()] }
+ }
+ impl PluginFunction for int_foo_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg1 = mem::take(args[1usize]).cast::<u64>();
+ let arg0 = &mut args[0usize].write_lock::<u64>().unwrap();
+ Ok(Dynamic::from(int_foo(arg0, arg1)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_setter_and_rename_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_fn {
+ #[rhai_fn(name = "set_sq", set = "squared")]
+ pub fn int_foo(x: &mut u64, y: u64) {
+ *x = y * y
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_fn {
+ pub fn int_foo(x: &mut u64, y: u64) {
+ *x = y * y
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("set_sq", FnNamespace::Internal, FnAccess::Public, Some(int_foo_token::PARAM_NAMES),
+ &[TypeId::of::<u64>(), TypeId::of::<u64>()],
+ int_foo_token().into());
+ m.set_fn("set$squared", FnNamespace::Global, FnAccess::Public, Some(int_foo_token::PARAM_NAMES),
+ &[TypeId::of::<u64>(), TypeId::of::<u64>()],
+ int_foo_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct int_foo_token();
+ #[doc(hidden)]
+ impl int_foo_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut u64", "y: u64", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 2usize] { [TypeId::of::<u64>(), TypeId::of::<u64>()] }
+ }
+ impl PluginFunction for int_foo_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg1 = mem::take(args[1usize]).cast::<u64>();
+ let arg0 = &mut args[0usize].write_lock::<u64>().unwrap();
+ Ok(Dynamic::from(int_foo(arg0, arg1)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_index_getter_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_index_fn {
+ #[rhai_fn(index_get)]
+ pub fn get_by_index(x: &mut MyCollection, i: u64) -> FLOAT {
+ x.get(i)
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_index_fn {
+ pub fn get_by_index(x: &mut MyCollection, i: u64) -> FLOAT {
+ x.get(i)
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("index$get$", FnNamespace::Global, FnAccess::Public, Some(get_by_index_token::PARAM_NAMES),
+ &[TypeId::of::<MyCollection>(), TypeId::of::<u64>()],
+ get_by_index_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct get_by_index_token();
+ #[doc(hidden)]
+ impl get_by_index_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut MyCollection", "i: u64", "FLOAT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 2usize] { [TypeId::of::<MyCollection>(), TypeId::of::<u64>()] }
+ }
+ impl PluginFunction for get_by_index_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg1 = mem::take(args[1usize]).cast::<u64>();
+ let arg0 = &mut args[0usize].write_lock::<MyCollection>().unwrap();
+ Ok(Dynamic::from(get_by_index(arg0, arg1)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_index_getter_fn_with_cfg_attr_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_index_fn {
+ #[cfg(hello)]
+ #[rhai_fn(index_get)]
+ #[some_other_attr]
+ pub fn get_by_index(x: &mut MyCollection, i: u64) -> FLOAT {
+ x.get(i)
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_index_fn {
+ #[cfg(hello)]
+ #[some_other_attr]
+ pub fn get_by_index(x: &mut MyCollection, i: u64) -> FLOAT {
+ x.get(i)
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ #[cfg(hello)]
+ m.set_fn("index$get$", FnNamespace::Global, FnAccess::Public, Some(get_by_index_token::PARAM_NAMES),
+ &[TypeId::of::<MyCollection>(), TypeId::of::<u64>()],
+ get_by_index_token().into());
+ if flatten {} else {}
+ }
+ #[cfg(hello)]
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct get_by_index_token();
+ #[cfg(hello)]
+ #[doc(hidden)]
+ impl get_by_index_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut MyCollection", "i: u64", "FLOAT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 2usize] { [TypeId::of::<MyCollection>(), TypeId::of::<u64>()] }
+ }
+ #[cfg(hello)]
+ impl PluginFunction for get_by_index_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg1 = mem::take(args[1usize]).cast::<u64>();
+ let arg0 = &mut args[0usize].write_lock::<MyCollection>().unwrap();
+ Ok(Dynamic::from(get_by_index(arg0, arg1)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_index_getter_and_rename_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_index_fn {
+ #[rhai_fn(name = "get", index_get)]
+ pub fn get_by_index(x: &mut MyCollection, i: u64) -> FLOAT {
+ x.get(i)
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_index_fn {
+ pub fn get_by_index(x: &mut MyCollection, i: u64) -> FLOAT {
+ x.get(i)
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("get", FnNamespace::Internal, FnAccess::Public, Some(get_by_index_token::PARAM_NAMES),
+ &[TypeId::of::<MyCollection>(), TypeId::of::<u64>()],
+ get_by_index_token().into());
+ m.set_fn("index$get$", FnNamespace::Global, FnAccess::Public, Some(get_by_index_token::PARAM_NAMES),
+ &[TypeId::of::<MyCollection>(), TypeId::of::<u64>()],
+ get_by_index_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct get_by_index_token();
+ #[doc(hidden)]
+ impl get_by_index_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut MyCollection", "i: u64", "FLOAT"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 2usize] { [TypeId::of::<MyCollection>(), TypeId::of::<u64>()] }
+ }
+ impl PluginFunction for get_by_index_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg1 = mem::take(args[1usize]).cast::<u64>();
+ let arg0 = &mut args[0usize].write_lock::<MyCollection>().unwrap();
+ Ok(Dynamic::from(get_by_index(arg0, arg1)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_index_setter_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_index_fn {
+ #[rhai_fn(index_set)]
+ pub fn set_by_index(x: &mut MyCollection, i: u64, item: FLOAT) {
+ x.entry(i).set(item)
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_index_fn {
+ pub fn set_by_index(x: &mut MyCollection, i: u64, item: FLOAT) {
+ x.entry(i).set(item)
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("index$set$", FnNamespace::Global, FnAccess::Public, Some(set_by_index_token::PARAM_NAMES),
+ &[TypeId::of::<MyCollection>(), TypeId::of::<u64>(), TypeId::of::<FLOAT>()],
+ set_by_index_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct set_by_index_token();
+ #[doc(hidden)]
+ impl set_by_index_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut MyCollection", "i: u64", "item: FLOAT", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 3usize] { [TypeId::of::<MyCollection>(), TypeId::of::<u64>(), TypeId::of::<FLOAT>()] }
+ }
+ impl PluginFunction for set_by_index_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg1 = mem::take(args[1usize]).cast::<u64>();
+ let arg2 = mem::take(args[2usize]).cast::<FLOAT>();
+ let arg0 = &mut args[0usize].write_lock::<MyCollection>().unwrap();
+ Ok(Dynamic::from(set_by_index(arg0, arg1, arg2)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_index_setter_and_rename_fn_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_index_fn {
+ #[rhai_fn(name = "set", index_set)]
+ pub fn set_by_index(x: &mut MyCollection, i: u64, item: FLOAT) {
+ x.entry(i).set(item)
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_index_fn {
+ pub fn set_by_index(x: &mut MyCollection, i: u64, item: FLOAT) {
+ x.entry(i).set(item)
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_fn("set", FnNamespace::Internal, FnAccess::Public, Some(set_by_index_token::PARAM_NAMES),
+ &[TypeId::of::<MyCollection>(), TypeId::of::<u64>(), TypeId::of::<FLOAT>()],
+ set_by_index_token().into());
+ m.set_fn("index$set$", FnNamespace::Global, FnAccess::Public, Some(set_by_index_token::PARAM_NAMES),
+ &[TypeId::of::<MyCollection>(), TypeId::of::<u64>(), TypeId::of::<FLOAT>()],
+ set_by_index_token().into());
+ if flatten {} else {}
+ }
+ #[allow(non_camel_case_types)]
+ #[doc(hidden)]
+ pub struct set_by_index_token();
+ #[doc(hidden)]
+ impl set_by_index_token {
+ pub const PARAM_NAMES: &'static [&'static str] = &["x: &mut MyCollection", "i: u64", "item: FLOAT", "()"];
+ #[inline(always)] pub fn param_types() -> [TypeId; 3usize] { [TypeId::of::<MyCollection>(), TypeId::of::<u64>(), TypeId::of::<FLOAT>()] }
+ }
+ impl PluginFunction for set_by_index_token {
+ #[inline(always)]
+ fn call(&self, context: Option<NativeCallContext>, args: &mut [&mut Dynamic]) -> RhaiResult {
+ let arg1 = mem::take(args[1usize]).cast::<u64>();
+ let arg2 = mem::take(args[2usize]).cast::<FLOAT>();
+ let arg0 = &mut args[0usize].write_lock::<MyCollection>().unwrap();
+ Ok(Dynamic::from(set_by_index(arg0, arg1, arg2)))
+ }
+
+ #[inline(always)] fn is_method_call(&self) -> bool { true }
+ #[inline(always)] fn is_pure(&self) -> bool { false }
+ #[inline(always)] fn has_context(&self) -> bool { false }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn one_constant_nested_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod one_constant {
+ pub mod it_is {
+ pub const MYSTIC_NUMBER: INT = 42;
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod one_constant {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod it_is {
+ pub const MYSTIC_NUMBER: INT = 42;
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_var("MYSTIC_NUMBER", MYSTIC_NUMBER);
+ if flatten {} else {}
+ }
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ if flatten {
+ { self::it_is::rhai_generate_into_module(m, flatten); }
+ } else {
+ { m.set_sub_module("it_is", self::it_is::rhai_module_generate()); }
+ }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn dual_constant_nested_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod two_constants {
+ pub mod first_is {
+ pub const MYSTIC_NUMBER: INT = 42;
+ }
+ pub mod second_is {
+ #[cfg(hello)]
+ pub const SPECIAL_CPU_NUMBER: INT = 68000;
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod two_constants {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod first_is {
+ pub const MYSTIC_NUMBER: INT = 42;
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_var("MYSTIC_NUMBER", MYSTIC_NUMBER);
+ if flatten {} else {}
+ }
+ }
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod second_is {
+ #[cfg(hello)]
+ pub const SPECIAL_CPU_NUMBER: INT = 68000;
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ #[cfg(hello)]
+ m.set_var("SPECIAL_CPU_NUMBER", SPECIAL_CPU_NUMBER);
+ if flatten {} else {}
+ }
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ if flatten {
+ { self::first_is::rhai_generate_into_module(m, flatten); }
+ { self::second_is::rhai_generate_into_module(m, flatten); }
+ } else {
+ { m.set_sub_module("first_is", self::first_is::rhai_module_generate()); }
+ { m.set_sub_module("second_is", self::second_is::rhai_module_generate()); }
+ }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+
+ #[test]
+ fn deep_tree_nested_module() {
+ let input_tokens: TokenStream = quote! {
+ pub mod heap_root {
+ pub const VALUE: INT = 100;
+ pub mod left {
+ pub const VALUE: INT = 19;
+ pub mod left {
+ pub const VALUE: INT = 17;
+ pub mod left {
+ pub const VALUE: INT = 2;
+ }
+ pub mod right {
+ pub const VALUE: INT = 7;
+ }
+ }
+ pub mod right {
+ pub const VALUE: INT = 3;
+ }
+ }
+ pub mod right {
+ pub const VALUE: INT = 36;
+ pub mod left {
+ pub const VALUE: INT = 25;
+ }
+ pub mod right {
+ pub const VALUE: INT = 1;
+ }
+ }
+ }
+ };
+
+ let expected_tokens = quote! {
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod heap_root {
+ pub const VALUE: INT = 100;
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod left {
+ pub const VALUE: INT = 19;
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod left {
+ pub const VALUE: INT = 17;
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod left {
+ pub const VALUE: INT = 2;
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_var("VALUE", VALUE);
+ if flatten {} else {}
+ }
+ }
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod right {
+ pub const VALUE: INT = 7;
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_var("VALUE", VALUE);
+ if flatten {} else {}
+ }
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_var("VALUE", VALUE);
+
+ if flatten {
+ { self::left::rhai_generate_into_module(m, flatten); }
+ { self::right::rhai_generate_into_module(m, flatten); }
+ } else {
+ { m.set_sub_module("left", self::left::rhai_module_generate()); }
+ { m.set_sub_module("right", self::right::rhai_module_generate()); }
+ }
+ }
+ }
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod right {
+ pub const VALUE: INT = 3;
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_var("VALUE", VALUE);
+ if flatten {} else {}
+ }
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_var("VALUE", VALUE);
+
+ if flatten {
+ { self::left::rhai_generate_into_module(m, flatten); }
+ { self::right::rhai_generate_into_module(m, flatten); }
+ } else {
+ { m.set_sub_module("left", self::left::rhai_module_generate()); }
+ { m.set_sub_module("right", self::right::rhai_module_generate()); }
+ }
+ }
+ }
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod right {
+ pub const VALUE: INT = 36;
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod left {
+ pub const VALUE: INT = 25;
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_var("VALUE", VALUE);
+ if flatten {} else {}
+ }
+ }
+ #[allow(clippy::needless_pass_by_value)]
+ pub mod right {
+ pub const VALUE: INT = 1;
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_var("VALUE", VALUE);
+ if flatten {} else {}
+ }
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_var("VALUE", VALUE);
+
+ if flatten {
+ { self::left::rhai_generate_into_module(m, flatten); }
+ { self::right::rhai_generate_into_module(m, flatten); }
+ } else {
+ { m.set_sub_module("left", self::left::rhai_module_generate()); }
+ { m.set_sub_module("right", self::right::rhai_module_generate()); }
+ }
+ }
+ }
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[doc(hidden)]
+ pub fn rhai_module_generate() -> Module {
+ let mut m = Module::new();
+ rhai_generate_into_module(&mut m, false);
+ m.build_index();
+ m
+ }
+ #[allow(unused_mut)]
+ #[doc(hidden)]
+ pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) {
+ m.set_var("VALUE", VALUE);
+
+ if flatten {
+ { self::left::rhai_generate_into_module(m, flatten); }
+ { self::right::rhai_generate_into_module(m, flatten); }
+ } else {
+ { m.set_sub_module("left", self::left::rhai_module_generate()); }
+ { m.set_sub_module("right", self::right::rhai_module_generate()); }
+ }
+ }
+ }
+ };
+
+ let item_mod = syn::parse2::<Module>(input_tokens).unwrap();
+ assert_streams_eq(item_mod.generate(), expected_tokens);
+ }
+}
diff --git a/rhai/codegen/tests/test_functions.rs b/rhai/codegen/tests/test_functions.rs
new file mode 100644
index 0000000..3fdbf76
--- /dev/null
+++ b/rhai/codegen/tests/test_functions.rs
@@ -0,0 +1,173 @@
+use rhai::plugin::*;
+use rhai::{Engine, EvalAltResult, Module, FLOAT};
+
+pub mod raw_fn {
+ use rhai::plugin::*;
+ use rhai::FLOAT;
+
+ #[export_fn]
+ pub fn distance_function(x1: FLOAT, y1: FLOAT, x2: FLOAT, y2: FLOAT) -> FLOAT {
+ ((y2 - y1).abs().powf(2.0) + (x2 - x1).abs().powf(2.0)).sqrt()
+ }
+}
+
+#[test]
+fn raw_fn_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ engine.register_fn("get_mystic_number", || 42 as FLOAT);
+ let mut m = Module::new();
+ rhai::set_exported_fn!(m, "euclidean_distance", raw_fn::distance_function);
+ engine.register_static_module("Math::Advanced", m.into());
+
+ assert_eq!(
+ engine.eval::<FLOAT>(
+ r#"let x = Math::Advanced::euclidean_distance(0.0, 1.0, 0.0, get_mystic_number()); x"#
+ )?,
+ 41.0
+ );
+ Ok(())
+}
+
+mod raw_fn_mut {
+ use rhai::plugin::*;
+ use rhai::FLOAT;
+
+ #[export_fn]
+ pub fn add_in_place(f1: &mut FLOAT, f2: FLOAT) {
+ *f1 += f2;
+ }
+}
+
+#[test]
+fn raw_fn_mut_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ engine.register_fn("get_mystic_number", || 42 as FLOAT);
+ let mut m = Module::new();
+ rhai::set_exported_fn!(m, "add_in_place", raw_fn_mut::add_in_place);
+ engine.register_static_module("Math::Advanced", m.into());
+
+ assert_eq!(
+ engine.eval::<FLOAT>(
+ r#"let x = get_mystic_number();
+ Math::Advanced::add_in_place(x, 1.0);
+ x"#
+ )?,
+ 43.0
+ );
+ Ok(())
+}
+
+mod raw_fn_str {
+ use rhai::plugin::*;
+
+ #[export_fn]
+ pub fn write_out_str(message: &str) -> bool {
+ eprintln!("{message}");
+ true
+ }
+}
+
+#[test]
+fn raw_fn_str_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ engine.register_fn("get_mystic_number", || 42 as FLOAT);
+ let mut m = Module::new();
+ rhai::set_exported_fn!(m, "write_out_str", raw_fn_str::write_out_str);
+ engine.register_static_module("Host::IO", m.into());
+
+ assert!(engine
+ .eval::<bool>(r#"let x = Host::IO::write_out_str("hello world!"); x"#)
+ .unwrap());
+ Ok(())
+}
+
+mod mut_opaque_ref {
+ use rhai::plugin::*;
+ use rhai::INT;
+
+ #[allow(dead_code)] // used inside `export_module!`
+ #[derive(Clone)]
+ pub struct StatusMessage {
+ os_code: Option<INT>,
+ message: String,
+ is_ok: bool,
+ }
+
+ #[export_fn]
+ pub fn new_message(is_ok: bool, message: &str) -> StatusMessage {
+ StatusMessage {
+ is_ok,
+ os_code: None,
+ message: message.to_string(),
+ }
+ }
+
+ #[export_fn]
+ pub fn new_os_message(is_ok: bool, os_code: INT) -> StatusMessage {
+ StatusMessage {
+ is_ok,
+ os_code: Some(os_code),
+ message: format!("OS Code {os_code}"),
+ }
+ }
+
+ #[export_fn]
+ pub fn write_out_message(message: &mut StatusMessage) -> bool {
+ eprintln!("{}", message.message);
+ true
+ }
+}
+
+#[test]
+fn mut_opaque_ref_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let mut m = Module::new();
+ rhai::set_exported_fn!(m, "new_message", mut_opaque_ref::new_message);
+ rhai::set_exported_fn!(m, "new_os_message", mut_opaque_ref::new_os_message);
+ rhai::set_exported_fn!(m, "write_out_message", mut_opaque_ref::write_out_message);
+ engine.register_static_module("Host::Msg", m.into());
+
+ assert!(engine
+ .eval::<bool>(
+ r#"
+ let message1 = Host::Msg::new_message(true, "it worked");
+ let ok1 = Host::Msg::write_out_message(message1);
+ let message2 = Host::Msg::new_os_message(true, 0);
+ let ok2 = Host::Msg::write_out_message(message2);
+ ok1 && ok2"#
+ )
+ .unwrap());
+ Ok(())
+}
+
+pub mod raw_returning_fn {
+ use rhai::plugin::*;
+ use rhai::FLOAT;
+
+ #[export_fn(return_raw)]
+ pub fn distance_function(
+ x1: FLOAT,
+ y1: FLOAT,
+ x2: FLOAT,
+ y2: FLOAT,
+ ) -> Result<rhai::FLOAT, Box<rhai::EvalAltResult>> {
+ Ok(((y2 - y1).abs().powf(2.0) + (x2 - x1).abs().powf(2.0)).sqrt())
+ }
+}
+
+#[test]
+fn raw_returning_fn_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ engine.register_fn("get_mystic_number", || 42 as FLOAT);
+ let mut m = Module::new();
+ rhai::set_exported_fn!(m, "euclidean_distance", raw_returning_fn::distance_function);
+ engine.register_static_module("Math::Advanced", m.into());
+
+ assert_eq!(
+ engine.eval::<FLOAT>(
+ r#"let x = Math::Advanced::euclidean_distance(0.0, 1.0, 0.0, get_mystic_number()); x"#
+ )?,
+ 41.0
+ );
+ Ok(())
+}
diff --git a/rhai/codegen/tests/test_modules.rs b/rhai/codegen/tests/test_modules.rs
new file mode 100644
index 0000000..cbc6156
--- /dev/null
+++ b/rhai/codegen/tests/test_modules.rs
@@ -0,0 +1,420 @@
+use rhai::{Array, Engine, EvalAltResult, FLOAT, INT};
+
+pub mod empty_module {
+ use rhai::plugin::*;
+
+ #[allow(non_snake_case)]
+ #[export_module]
+ pub mod EmptyModule {}
+}
+
+#[test]
+fn empty_module_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let m = rhai::exported_module!(crate::empty_module::EmptyModule);
+ engine.register_static_module("Module::Empty", m.into());
+
+ Ok(())
+}
+
+pub mod one_fn_module {
+ use rhai::plugin::*;
+
+ #[export_module]
+ pub mod advanced_math {
+ use rhai::FLOAT;
+ pub fn get_mystic_number() -> FLOAT {
+ 42.0 as FLOAT
+ }
+ }
+}
+
+#[test]
+fn one_fn_module_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let m = rhai::exported_module!(crate::one_fn_module::advanced_math);
+ engine.register_static_module("Math::Advanced", m.into());
+
+ assert_eq!(
+ engine.eval::<FLOAT>(r#"let m = Math::Advanced::get_mystic_number();m"#)?,
+ 42.0
+ );
+ Ok(())
+}
+
+pub mod one_fn_and_const_module {
+ use rhai::plugin::*;
+
+ #[export_module]
+ pub mod advanced_math {
+ use rhai::FLOAT;
+
+ pub const MYSTIC_NUMBER: FLOAT = 42.0 as FLOAT;
+
+ pub fn euclidean_distance(x1: FLOAT, y1: FLOAT, x2: FLOAT, y2: FLOAT) -> FLOAT {
+ ((y2 - y1).abs().powf(2.0) + (x2 - x1).abs().powf(2.0)).sqrt()
+ }
+ }
+}
+
+#[test]
+fn one_fn_and_const_module_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let m = rhai::exported_module!(crate::one_fn_and_const_module::advanced_math);
+ engine.register_static_module("Math::Advanced", m.into());
+
+ assert_eq!(
+ engine.eval::<FLOAT>(
+ "
+ let m = Math::Advanced::MYSTIC_NUMBER;
+ let x = Math::Advanced::euclidean_distance(0.0, 1.0, 0.0, m);
+ x
+ "
+ )?,
+ 41.0
+ );
+ Ok(())
+}
+
+pub mod raw_fn_str_module {
+ use rhai::plugin::*;
+
+ #[export_module]
+ pub mod host_io {
+ pub fn write_out_str(message: &str) -> bool {
+ eprintln!("{message}");
+ true
+ }
+ }
+}
+
+#[test]
+fn raw_fn_str_module_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let m = rhai::exported_module!(crate::raw_fn_str_module::host_io);
+ engine.register_static_module("Host::IO", m.into());
+
+ assert!(engine
+ .eval::<bool>(r#"let x = Host::IO::write_out_str("hello world!"); x"#)
+ .unwrap());
+ Ok(())
+}
+
+pub mod mut_opaque_ref_module {
+ use rhai::plugin::*;
+ use rhai::INT;
+
+ #[allow(dead_code)] // used inside `exported_module!`
+ #[derive(Clone)]
+ pub struct StatusMessage {
+ os_code: Option<INT>,
+ message: String,
+ is_ok: bool,
+ }
+
+ #[export_module]
+ pub mod host_msg {
+ use super::{StatusMessage, INT};
+
+ pub fn new_message(is_ok: bool, message: &str) -> StatusMessage {
+ StatusMessage {
+ is_ok,
+ os_code: None,
+ message: message.to_string(),
+ }
+ }
+
+ pub fn new_os_message(is_ok: bool, os_code: INT) -> StatusMessage {
+ StatusMessage {
+ is_ok,
+ os_code: Some(os_code),
+ message: format!("OS Code {os_code}"),
+ }
+ }
+
+ pub fn write_out_message(message: &mut StatusMessage) -> bool {
+ eprintln!("{}", message.message);
+ true
+ }
+ }
+}
+
+#[test]
+fn mut_opaque_ref_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let m = rhai::exported_module!(crate::mut_opaque_ref_module::host_msg);
+ engine.register_static_module("Host::Msg", m.into());
+
+ assert!(engine
+ .eval::<bool>(
+ r#"
+ let success = "it worked";
+ let message1 = Host::Msg::new_message(true, success);
+ let ok1 = Host::Msg::write_out_message(message1);
+ let message2 = Host::Msg::new_os_message(true, 0);
+ let ok2 = Host::Msg::write_out_message(message2);
+ ok1 && ok2
+ "#
+ )
+ .unwrap());
+ Ok(())
+}
+
+mod duplicate_fn_rename {
+ use rhai::plugin::*;
+ #[export_module]
+ pub mod my_adds {
+ use rhai::{FLOAT, INT};
+
+ #[rhai_fn(name = "add")]
+ pub fn add_float(f1: FLOAT, f2: FLOAT) -> FLOAT {
+ f1 + f2
+ }
+
+ #[rhai_fn(name = "add")]
+ pub fn add_int(i1: INT, i2: INT) -> INT {
+ i1 + i2
+ }
+ }
+}
+
+#[test]
+fn duplicate_fn_rename_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ engine.register_fn("get_mystic_number", || 42 as FLOAT);
+ let m = rhai::exported_module!(crate::duplicate_fn_rename::my_adds);
+ engine.register_static_module("Math::Advanced", m.into());
+
+ let output_array = engine.eval::<Array>(
+ "
+ let fx = get_mystic_number();
+ let fy = Math::Advanced::add(fx, 1.0);
+ let ix = 42;
+ let iy = Math::Advanced::add(ix, 1);
+ [fy, iy]
+ ",
+ )?;
+ assert_eq!(&output_array[0].as_float().unwrap(), &43.0);
+ assert_eq!(&output_array[1].as_int().unwrap(), &43);
+ Ok(())
+}
+
+mod multiple_fn_rename {
+ use rhai::plugin::*;
+ #[export_module]
+ pub mod my_adds {
+ use rhai::{FLOAT, INT};
+
+ pub fn get_mystic_number() -> FLOAT {
+ 42.0
+ }
+ #[rhai_fn(name = "add", name = "+", name = "add_together")]
+ pub fn add_float(f1: FLOAT, f2: FLOAT) -> FLOAT {
+ f1 + f2 * 2.0
+ }
+
+ #[rhai_fn(name = "add", name = "+", name = "add_together")]
+ pub fn add_int(i1: INT, i2: INT) -> INT {
+ i1 + i2 * 2
+ }
+
+ #[rhai_fn(name = "prop", get = "prop")]
+ pub fn get_prop(x: FLOAT) -> FLOAT {
+ x * 2.0
+ }
+
+ #[rhai_fn(name = "idx", index_get)]
+ pub fn index(x: FLOAT, i: INT) -> FLOAT {
+ x + (i as FLOAT)
+ }
+ }
+}
+
+#[test]
+fn multiple_fn_rename_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let m = rhai::exported_module!(crate::multiple_fn_rename::my_adds);
+ engine.register_global_module(m.into());
+ engine.set_fast_operators(false);
+
+ let output_array = engine.eval::<Array>(
+ "
+ let fx = get_mystic_number();
+ let fy1 = add(fx, 1.0);
+ let fy2 = add_together(fx, 1.0);
+ let fy3 = fx + 1.0;
+ let p1 = fx.prop;
+ let p2 = prop(fx);
+ let idx1 = fx[1];
+ let idx2 = idx(fx, 1);
+ let ix = 42;
+ let iy1 = add(ix, 1);
+ let iy2 = add_together(ix, 1);
+ let iy3 = ix + 1;
+ [fy1, fy2, fy3, iy1, iy2, iy3, p1, p2, idx1, idx2]
+ ",
+ )?;
+ assert_eq!(&output_array[0].as_float().unwrap(), &44.0);
+ assert_eq!(&output_array[1].as_float().unwrap(), &44.0);
+ assert_eq!(&output_array[2].as_float().unwrap(), &44.0);
+ assert_eq!(&output_array[3].as_int().unwrap(), &44);
+ assert_eq!(&output_array[4].as_int().unwrap(), &44);
+ assert_eq!(&output_array[5].as_int().unwrap(), &44);
+ assert_eq!(&output_array[6].as_float().unwrap(), &84.0);
+ assert_eq!(&output_array[7].as_float().unwrap(), &84.0);
+ assert_eq!(&output_array[8].as_float().unwrap(), &43.0);
+ assert_eq!(&output_array[9].as_float().unwrap(), &43.0);
+ Ok(())
+}
+
+#[allow(dead_code)] // used inside `export_module!`
+mod export_by_prefix {
+ use rhai::plugin::*;
+
+ #[export_module(export_prefix = "foo_")]
+ pub mod my_adds {
+ use rhai::{FLOAT, INT};
+
+ #[rhai_fn(name = "foo_add_f")]
+ pub fn foo_add1(f1: FLOAT, f2: FLOAT) -> FLOAT {
+ f1 + f2
+ }
+
+ #[rhai_fn(name = "bar_add_i")]
+ fn foo_add_int(i1: INT, i2: INT) -> INT {
+ i1 + i2
+ }
+
+ #[rhai_fn(name = "foo_add_float2")]
+ pub fn add_float2(f1: FLOAT, f2: FLOAT) -> FLOAT {
+ f1 + f2
+ }
+
+ pub fn foo_m(f1: FLOAT, f2: FLOAT) -> FLOAT {
+ f1 + f2
+ }
+
+ fn foo_n(i1: INT, i2: INT) -> INT {
+ i1 + i2
+ }
+
+ pub fn bar_m(f1: FLOAT, f2: FLOAT) -> FLOAT {
+ f1 + f2
+ }
+ }
+}
+
+#[test]
+fn export_by_prefix_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let m = rhai::exported_module!(crate::export_by_prefix::my_adds);
+ engine.register_static_module("Math::Advanced", m.into());
+
+ let output_array = engine.eval::<Array>(
+ "
+ let ex = 41.0;
+ let fx = Math::Advanced::foo_add_f(ex, 1.0);
+ let gx = Math::Advanced::foo_m(41.0, 1.0);
+ let ei = 41;
+ let fi = Math::Advanced::bar_add_i(ei, 1);
+ let gi = Math::Advanced::foo_n(41, 1);
+ [fx, gx, fi, gi]
+ ",
+ )?;
+ assert_eq!(&output_array[0].as_float().unwrap(), &42.0);
+ assert_eq!(&output_array[1].as_float().unwrap(), &42.0);
+ assert_eq!(&output_array[2].as_int().unwrap(), &42);
+ assert_eq!(&output_array[3].as_int().unwrap(), &42);
+
+ assert!(matches!(*engine.eval::<FLOAT>(
+ "
+ let ex = 41.0;
+ let fx = Math::Advanced::foo_add_float2(ex, 1.0);
+ fx
+ ").unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(s, ..)
+ if s == "Math::Advanced::foo_add_float2 (f64, f64)"));
+
+ assert!(matches!(*engine.eval::<FLOAT>(
+ "
+ let ex = 41.0;
+ let fx = Math::Advanced::bar_m(ex, 1.0);
+ fx
+ ").unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(s, ..)
+ if s == "Math::Advanced::bar_m (f64, f64)"));
+
+ Ok(())
+}
+
+#[allow(dead_code)] // used inside `export_module!`
+mod export_all {
+ use rhai::plugin::*;
+
+ #[export_module(export_all)]
+ pub mod my_adds {
+ use rhai::{FLOAT, INT};
+
+ #[rhai_fn(name = "foo_add_f")]
+ pub fn add_float(f1: FLOAT, f2: FLOAT) -> FLOAT {
+ f1 + f2
+ }
+
+ #[rhai_fn(name = "foo_add_i")]
+ fn add_int(i1: INT, i2: INT) -> INT {
+ i1 + i2
+ }
+
+ #[rhai_fn(skip)]
+ pub fn add_float2(f1: FLOAT, f2: FLOAT) -> FLOAT {
+ f1 + f2
+ }
+
+ pub fn foo_m(f1: FLOAT, f2: FLOAT) -> FLOAT {
+ f1 + f2
+ }
+
+ fn foo_n(i1: INT, i2: INT) -> INT {
+ i1 + i2
+ }
+
+ #[rhai_fn(skip)]
+ fn foo_p(i1: INT, i2: INT) -> INT {
+ i1 * i2
+ }
+ }
+}
+
+#[test]
+fn export_all_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let m = rhai::exported_module!(crate::export_all::my_adds);
+ engine.register_static_module("Math::Advanced", m.into());
+
+ let output_array = engine.eval::<Array>(
+ "
+ let ex = 41.0;
+ let fx = Math::Advanced::foo_add_f(ex, 1.0);
+ let gx = Math::Advanced::foo_m(41.0, 1.0);
+ let ei = 41;
+ let fi = Math::Advanced::foo_add_i(ei, 1);
+ let gi = Math::Advanced::foo_n(41, 1);
+ [fx, gx, fi, gi]
+ ",
+ )?;
+ assert_eq!(&output_array[0].as_float().unwrap(), &42.0);
+ assert_eq!(&output_array[1].as_float().unwrap(), &42.0);
+ assert_eq!(&output_array[2].as_int().unwrap(), &42);
+ assert_eq!(&output_array[3].as_int().unwrap(), &42);
+
+ assert!(matches!(*engine.eval::<INT>(
+ "
+ let ex = 41;
+ let fx = Math::Advanced::foo_p(ex, 1);
+ fx
+ ").unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(s, ..)
+ if s == "Math::Advanced::foo_p (i64, i64)"));
+
+ Ok(())
+}
diff --git a/rhai/codegen/tests/test_nested.rs b/rhai/codegen/tests/test_nested.rs
new file mode 100644
index 0000000..c532368
--- /dev/null
+++ b/rhai/codegen/tests/test_nested.rs
@@ -0,0 +1,189 @@
+use rhai::{Array, Engine, EvalAltResult, FLOAT};
+
+pub mod one_fn_module_nested_attr {
+ use rhai::plugin::*;
+
+ #[export_module]
+ pub mod advanced_math {
+ use rhai::plugin::*;
+ use rhai::FLOAT;
+
+ #[rhai_fn(return_raw)]
+ pub fn get_mystic_number() -> Result<FLOAT, Box<EvalAltResult>> {
+ Ok(42.0)
+ }
+ }
+}
+
+#[test]
+fn one_fn_module_nested_attr_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let m = rhai::exported_module!(crate::one_fn_module_nested_attr::advanced_math);
+ engine.register_static_module("Math::Advanced", m.into());
+
+ assert_eq!(
+ engine.eval::<FLOAT>(r#"let m = Math::Advanced::get_mystic_number(); m"#)?,
+ 42.0
+ );
+ Ok(())
+}
+
+pub mod one_fn_sub_module_nested_attr {
+ use rhai::plugin::*;
+
+ #[export_module]
+ pub mod advanced_math {
+ #[rhai_mod(name = "constants")]
+ pub mod my_module {
+ use rhai::plugin::*;
+ use rhai::FLOAT;
+ #[rhai_fn(return_raw)]
+ pub fn get_mystic_number() -> Result<FLOAT, Box<EvalAltResult>> {
+ Ok(42.0)
+ }
+ }
+ }
+}
+
+#[test]
+fn one_fn_sub_module_nested_attr_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let m = rhai::exported_module!(crate::one_fn_sub_module_nested_attr::advanced_math);
+ engine.register_static_module("Math::Advanced", m.into());
+
+ assert_eq!(
+ engine.eval::<FLOAT>(
+ "
+ let m = Math::Advanced::constants::get_mystic_number();
+ m
+ "
+ )?,
+ 42.0
+ );
+ Ok(())
+}
+
+mod export_nested_by_prefix {
+ use rhai::plugin::*;
+
+ #[export_module(export_prefix = "foo_")]
+ pub mod my_adds {
+ pub mod foo_first_adders {
+ use rhai::{FLOAT, INT};
+
+ pub fn add_float(f1: FLOAT, f2: FLOAT) -> FLOAT {
+ f1 + f2
+ }
+
+ pub fn add_int(i1: INT, i2: INT) -> INT {
+ i1 + i2
+ }
+ }
+
+ pub mod foo_second_adders {
+ use rhai::{FLOAT, INT};
+
+ pub fn add_float(f1: FLOAT, f2: FLOAT) -> FLOAT {
+ f1 + f2
+ }
+
+ pub fn add_int(i1: INT, i2: INT) -> INT {
+ i1 + i2
+ }
+ }
+
+ #[allow(dead_code)] // used inside a `exported_module!`
+ #[rhai_mod(name = "foo_third_adders")]
+ pub mod baz_third_adders {
+ use rhai::{FLOAT, INT};
+
+ pub fn add_float(f1: FLOAT, f2: FLOAT) -> FLOAT {
+ f1 + f2
+ }
+
+ pub fn add_int(i1: INT, i2: INT) -> INT {
+ i1 + i2
+ }
+ }
+
+ #[allow(dead_code)] // used inside a `exported_module!`
+ pub mod bar_fourth_adders {
+ use rhai::{FLOAT, INT};
+
+ pub fn add_float(f1: FLOAT, f2: FLOAT) -> FLOAT {
+ f1 + f2
+ }
+
+ pub fn add_int(i1: INT, i2: INT) -> INT {
+ i1 + i2
+ }
+ }
+ }
+}
+
+#[test]
+fn export_nested_by_prefix_test() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let m = rhai::exported_module!(crate::export_nested_by_prefix::my_adds);
+ engine.register_static_module("Math::Advanced", m.into());
+
+ let output_array = engine.eval::<Array>(
+ "
+ let ex = 41.0;
+ let fx = Math::Advanced::foo_first_adders::add_float(ex, 1.0);
+
+ let ei = 41;
+ let fi = Math::Advanced::foo_first_adders::add_int(ei, 1);
+
+ let gx = 41.0;
+ let hx = Math::Advanced::foo_second_adders::add_float(gx, 1.0);
+
+ let gi = 41;
+ let hi = Math::Advanced::foo_second_adders::add_int(gi, 1);
+
+ [fx, hx, fi, hi]
+ ",
+ )?;
+ assert_eq!(&output_array[0].as_float().unwrap(), &42.0);
+ assert_eq!(&output_array[1].as_float().unwrap(), &42.0);
+ assert_eq!(&output_array[2].as_int().unwrap(), &42);
+ assert_eq!(&output_array[3].as_int().unwrap(), &42);
+
+ assert!(matches!(*engine.eval::<FLOAT>(
+ "
+ let ex = 41.0;
+ let fx = Math::Advanced::foo_third_adders::add_float(ex, 1.0);
+ fx
+ ").unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(s, ..)
+ if s == "Math::Advanced::foo_third_adders::add_float (f64, f64)"));
+
+ assert!(matches!(*engine.eval::<FLOAT>(
+ "
+ let ex = 41;
+ let fx = Math::Advanced::foo_third_adders::add_int(ex, 1);
+ fx
+ ").unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(s, ..)
+ if s == "Math::Advanced::foo_third_adders::add_int (i64, i64)"));
+
+ assert!(matches!(*engine.eval::<FLOAT>(
+ "
+ let ex = 41;
+ let fx = Math::Advanced::bar_fourth_adders::add_int(ex, 1);
+ fx
+ ").unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(s, ..)
+ if s == "Math::Advanced::bar_fourth_adders::add_int (i64, i64)"));
+
+ assert!(matches!(*engine.eval::<FLOAT>(
+ "
+ let ex = 41.0;
+ let fx = Math::Advanced::bar_fourth_adders::add_float(ex, 1.0);
+ fx
+ ").unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(s, ..)
+ if s == "Math::Advanced::bar_fourth_adders::add_float (f64, f64)"));
+
+ Ok(())
+}
diff --git a/rhai/codegen/tests/ui_tests.rs b/rhai/codegen/tests/ui_tests.rs
new file mode 100644
index 0000000..ef7bf58
--- /dev/null
+++ b/rhai/codegen/tests/ui_tests.rs
@@ -0,0 +1,8 @@
+#![cfg(test)]
+mod ui_tests {
+ #[test]
+ fn all() {
+ let t = trybuild::TestCases::new();
+ t.compile_fail("ui_tests/*.rs");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_fn_bad_attr.rs b/rhai/codegen/ui_tests/export_fn_bad_attr.rs
new file mode 100644
index 0000000..685b8e8
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_bad_attr.rs
@@ -0,0 +1,24 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_fn(unknown = "thing")]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_fn_bad_attr.stderr b/rhai/codegen/ui_tests/export_fn_bad_attr.stderr
new file mode 100644
index 0000000..dadd08e
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_bad_attr.stderr
@@ -0,0 +1,11 @@
+error: unknown attribute 'unknown'
+ --> ui_tests/export_fn_bad_attr.rs:9:13
+ |
+9 | #[export_fn(unknown = "thing")]
+ | ^^^^^^^
+
+error[E0425]: cannot find function `test_fn` in this scope
+ --> ui_tests/export_fn_bad_attr.rs:19:8
+ |
+19 | if test_fn(n) {
+ | ^^^^^^^ not found in this scope
diff --git a/rhai/codegen/ui_tests/export_fn_bad_value.rs b/rhai/codegen/ui_tests/export_fn_bad_value.rs
new file mode 100644
index 0000000..f8044f9
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_bad_value.rs
@@ -0,0 +1,24 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_fn(name = true)]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_fn_bad_value.stderr b/rhai/codegen/ui_tests/export_fn_bad_value.stderr
new file mode 100644
index 0000000..08061ff
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_bad_value.stderr
@@ -0,0 +1,11 @@
+error: expecting string literal
+ --> ui_tests/export_fn_bad_value.rs:9:20
+ |
+9 | #[export_fn(name = true)]
+ | ^^^^
+
+error[E0425]: cannot find function `test_fn` in this scope
+ --> ui_tests/export_fn_bad_value.rs:19:8
+ |
+19 | if test_fn(n) {
+ | ^^^^^^^ not found in this scope
diff --git a/rhai/codegen/ui_tests/export_fn_cfg.rs b/rhai/codegen/ui_tests/export_fn_cfg.rs
new file mode 100644
index 0000000..52d4c96
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_cfg.rs
@@ -0,0 +1,25 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[cfg(not(feature = "foo"))]
+#[export_fn]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_fn_cfg.stderr b/rhai/codegen/ui_tests/export_fn_cfg.stderr
new file mode 100644
index 0000000..4bbee42
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_cfg.stderr
@@ -0,0 +1,11 @@
+error: `cfg` attributes are not allowed for `export_fn`
+ --> ui_tests/export_fn_cfg.rs:9:1
+ |
+9 | #[cfg(not(feature = "foo"))]
+ | ^
+
+error[E0425]: cannot find function `test_fn` in this scope
+ --> ui_tests/export_fn_cfg.rs:20:8
+ |
+20 | if test_fn(n) {
+ | ^^^^^^^ not found in this scope
diff --git a/rhai/codegen/ui_tests/export_fn_extra_value.rs b/rhai/codegen/ui_tests/export_fn_extra_value.rs
new file mode 100644
index 0000000..d8cb962
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_extra_value.rs
@@ -0,0 +1,24 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_fn(return_raw = "yes")]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_fn_extra_value.stderr b/rhai/codegen/ui_tests/export_fn_extra_value.stderr
new file mode 100644
index 0000000..e526aa3
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_extra_value.stderr
@@ -0,0 +1,11 @@
+error: extraneous value
+ --> ui_tests/export_fn_extra_value.rs:9:26
+ |
+9 | #[export_fn(return_raw = "yes")]
+ | ^^^^^
+
+error[E0425]: cannot find function `test_fn` in this scope
+ --> ui_tests/export_fn_extra_value.rs:19:8
+ |
+19 | if test_fn(n) {
+ | ^^^^^^^ not found in this scope
diff --git a/rhai/codegen/ui_tests/export_fn_junk_arg.rs b/rhai/codegen/ui_tests/export_fn_junk_arg.rs
new file mode 100644
index 0000000..3abb939
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_junk_arg.rs
@@ -0,0 +1,24 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_fn("wheeeee")]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_fn_junk_arg.stderr b/rhai/codegen/ui_tests/export_fn_junk_arg.stderr
new file mode 100644
index 0000000..048d95e
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_junk_arg.stderr
@@ -0,0 +1,11 @@
+error: expecting identifier
+ --> ui_tests/export_fn_junk_arg.rs:9:13
+ |
+9 | #[export_fn("wheeeee")]
+ | ^^^^^^^^^
+
+error[E0425]: cannot find function `test_fn` in this scope
+ --> ui_tests/export_fn_junk_arg.rs:19:8
+ |
+19 | if test_fn(n) {
+ | ^^^^^^^ not found in this scope
diff --git a/rhai/codegen/ui_tests/export_fn_missing_value.rs b/rhai/codegen/ui_tests/export_fn_missing_value.rs
new file mode 100644
index 0000000..7497a51
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_missing_value.rs
@@ -0,0 +1,24 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_fn(name)]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_fn_missing_value.stderr b/rhai/codegen/ui_tests/export_fn_missing_value.stderr
new file mode 100644
index 0000000..c34a6a4
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_missing_value.stderr
@@ -0,0 +1,11 @@
+error: requires value
+ --> ui_tests/export_fn_missing_value.rs:9:13
+ |
+9 | #[export_fn(name)]
+ | ^^^^
+
+error[E0425]: cannot find function `test_fn` in this scope
+ --> ui_tests/export_fn_missing_value.rs:19:8
+ |
+19 | if test_fn(n) {
+ | ^^^^^^^ not found in this scope
diff --git a/rhai/codegen/ui_tests/export_fn_path_attr.rs b/rhai/codegen/ui_tests/export_fn_path_attr.rs
new file mode 100644
index 0000000..a9fed9e
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_path_attr.rs
@@ -0,0 +1,24 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_fn(rhai::name = "thing")]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_fn_path_attr.stderr b/rhai/codegen/ui_tests/export_fn_path_attr.stderr
new file mode 100644
index 0000000..a7862ae
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_path_attr.stderr
@@ -0,0 +1,11 @@
+error: expecting attribute name
+ --> ui_tests/export_fn_path_attr.rs:9:13
+ |
+9 | #[export_fn(rhai::name = "thing")]
+ | ^^^^
+
+error[E0425]: cannot find function `test_fn` in this scope
+ --> ui_tests/export_fn_path_attr.rs:19:8
+ |
+19 | if test_fn(n) {
+ | ^^^^^^^ not found in this scope
diff --git a/rhai/codegen/ui_tests/export_fn_raw_noreturn.rs b/rhai/codegen/ui_tests/export_fn_raw_noreturn.rs
new file mode 100644
index 0000000..7c8b42e
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_raw_noreturn.rs
@@ -0,0 +1,25 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_fn(return_raw)]
+pub fn test_fn(input: &mut Point) {
+ input.x += 1.0;
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ test_fn(&mut n);
+ if n.x >= 10.0 {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_fn_raw_noreturn.stderr b/rhai/codegen/ui_tests/export_fn_raw_noreturn.stderr
new file mode 100644
index 0000000..28744c6
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_raw_noreturn.stderr
@@ -0,0 +1,11 @@
+error: functions marked with 'return_raw' must return Result<T, Box<EvalAltResult>>
+ --> ui_tests/export_fn_raw_noreturn.rs:9:13
+ |
+9 | #[export_fn(return_raw)]
+ | ^^^^^^^^^^
+
+error[E0425]: cannot find function `test_fn` in this scope
+ --> ui_tests/export_fn_raw_noreturn.rs:19:5
+ |
+19 | test_fn(&mut n);
+ | ^^^^^^^ not found in this scope
diff --git a/rhai/codegen/ui_tests/export_fn_raw_return.rs b/rhai/codegen/ui_tests/export_fn_raw_return.rs
new file mode 100644
index 0000000..9df9954
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_raw_return.rs
@@ -0,0 +1,24 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_fn(return_raw)]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_fn_raw_return.stderr b/rhai/codegen/ui_tests/export_fn_raw_return.stderr
new file mode 100644
index 0000000..629cda6
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_fn_raw_return.stderr
@@ -0,0 +1,12 @@
+error[E0599]: `bool` is not an iterator
+ --> ui_tests/export_fn_raw_return.rs:10:33
+ |
+9 | #[export_fn(return_raw)]
+ | ------------------------ in this procedural macro expansion
+10 | pub fn test_fn(input: Point) -> bool {
+ | ^^^^ `bool` is not an iterator
+ |
+ = note: the following trait bounds were not satisfied:
+ `bool: std::iter::Iterator`
+ which is required by `&mut bool: std::iter::Iterator`
+ = note: this error originates in the attribute macro `export_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/rhai/codegen/ui_tests/export_mod_bad_attr.rs b/rhai/codegen/ui_tests/export_mod_bad_attr.rs
new file mode 100644
index 0000000..8f8f7c2
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_bad_attr.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_mod {
+#[rhai_fn(unknown = "thing")]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_mod::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_mod_bad_attr.stderr b/rhai/codegen/ui_tests/export_mod_bad_attr.stderr
new file mode 100644
index 0000000..bb5b9d1
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_bad_attr.stderr
@@ -0,0 +1,11 @@
+error: unknown attribute 'unknown'
+ --> ui_tests/export_mod_bad_attr.rs:11:11
+ |
+11 | #[rhai_fn(unknown = "thing")]
+ | ^^^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_mod`
+ --> ui_tests/export_mod_bad_attr.rs:22:8
+ |
+22 | if test_mod::test_fn(n) {
+ | ^^^^^^^^ use of undeclared crate or module `test_mod`
diff --git a/rhai/codegen/ui_tests/export_mod_bad_value.rs b/rhai/codegen/ui_tests/export_mod_bad_value.rs
new file mode 100644
index 0000000..c513dd3
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_bad_value.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_mod {
+#[rhai_fn(name = true)]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_mod::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_mod_bad_value.stderr b/rhai/codegen/ui_tests/export_mod_bad_value.stderr
new file mode 100644
index 0000000..a557731
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_bad_value.stderr
@@ -0,0 +1,11 @@
+error: expecting string literal
+ --> ui_tests/export_mod_bad_value.rs:11:18
+ |
+11 | #[rhai_fn(name = true)]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_mod`
+ --> ui_tests/export_mod_bad_value.rs:22:8
+ |
+22 | if test_mod::test_fn(n) {
+ | ^^^^^^^^ use of undeclared crate or module `test_mod`
diff --git a/rhai/codegen/ui_tests/export_mod_cfg.rs b/rhai/codegen/ui_tests/export_mod_cfg.rs
new file mode 100644
index 0000000..49838a7
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_cfg.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_mod {
+#[cfg(not(feature = "foo"))]
+#[rhai_fn]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_mod::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_mod_cfg.stderr b/rhai/codegen/ui_tests/export_mod_cfg.stderr
new file mode 100644
index 0000000..dc0145f
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_cfg.stderr
@@ -0,0 +1,11 @@
+error: expected attribute arguments in parentheses: #[rhai_fn(...)]
+ --> ui_tests/export_mod_cfg.rs:12:1
+ |
+12 | #[rhai_fn]
+ | ^^^^^^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_mod`
+ --> ui_tests/export_mod_cfg.rs:23:8
+ |
+23 | if test_mod::test_fn(n) {
+ | ^^^^^^^^ use of undeclared crate or module `test_mod`
diff --git a/rhai/codegen/ui_tests/export_mod_extra_value.rs b/rhai/codegen/ui_tests/export_mod_extra_value.rs
new file mode 100644
index 0000000..6636f10
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_extra_value.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_mod {
+#[rhai_fn(return_raw = "yes")]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_mod::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_mod_extra_value.stderr b/rhai/codegen/ui_tests/export_mod_extra_value.stderr
new file mode 100644
index 0000000..e1f2707
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_extra_value.stderr
@@ -0,0 +1,11 @@
+error: extraneous value
+ --> ui_tests/export_mod_extra_value.rs:11:24
+ |
+11 | #[rhai_fn(return_raw = "yes")]
+ | ^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_mod`
+ --> ui_tests/export_mod_extra_value.rs:22:8
+ |
+22 | if test_mod::test_fn(n) {
+ | ^^^^^^^^ use of undeclared crate or module `test_mod`
diff --git a/rhai/codegen/ui_tests/export_mod_junk_arg.rs b/rhai/codegen/ui_tests/export_mod_junk_arg.rs
new file mode 100644
index 0000000..12190cf
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_junk_arg.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_mod {
+#[rhai_fn("wheeeee")]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_mod::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_mod_junk_arg.stderr b/rhai/codegen/ui_tests/export_mod_junk_arg.stderr
new file mode 100644
index 0000000..763f3e7
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_junk_arg.stderr
@@ -0,0 +1,11 @@
+error: expecting identifier
+ --> ui_tests/export_mod_junk_arg.rs:11:11
+ |
+11 | #[rhai_fn("wheeeee")]
+ | ^^^^^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_mod`
+ --> ui_tests/export_mod_junk_arg.rs:22:8
+ |
+22 | if test_mod::test_fn(n) {
+ | ^^^^^^^^ use of undeclared crate or module `test_mod`
diff --git a/rhai/codegen/ui_tests/export_mod_missing_value.rs b/rhai/codegen/ui_tests/export_mod_missing_value.rs
new file mode 100644
index 0000000..f57c247
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_missing_value.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_mod {
+#[rhai_fn(name)]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_mod::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_mod_missing_value.stderr b/rhai/codegen/ui_tests/export_mod_missing_value.stderr
new file mode 100644
index 0000000..fc997c7
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_missing_value.stderr
@@ -0,0 +1,11 @@
+error: requires value
+ --> ui_tests/export_mod_missing_value.rs:11:11
+ |
+11 | #[rhai_fn(name)]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_mod`
+ --> ui_tests/export_mod_missing_value.rs:22:8
+ |
+22 | if test_mod::test_fn(n) {
+ | ^^^^^^^^ use of undeclared crate or module `test_mod`
diff --git a/rhai/codegen/ui_tests/export_mod_path_attr.rs b/rhai/codegen/ui_tests/export_mod_path_attr.rs
new file mode 100644
index 0000000..a489f04
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_path_attr.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_mod {
+#[rhai_fn(rhai::name = "thing")]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_mod::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_mod_path_attr.stderr b/rhai/codegen/ui_tests/export_mod_path_attr.stderr
new file mode 100644
index 0000000..8b4d307
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_path_attr.stderr
@@ -0,0 +1,11 @@
+error: expecting attribute name
+ --> ui_tests/export_mod_path_attr.rs:11:11
+ |
+11 | #[rhai_fn(rhai::name = "thing")]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_mod`
+ --> ui_tests/export_mod_path_attr.rs:22:8
+ |
+22 | if test_mod::test_fn(n) {
+ | ^^^^^^^^ use of undeclared crate or module `test_mod`
diff --git a/rhai/codegen/ui_tests/export_mod_raw_noreturn.rs b/rhai/codegen/ui_tests/export_mod_raw_noreturn.rs
new file mode 100644
index 0000000..926f628
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_raw_noreturn.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_mod {
+#[rhai_fn(return_raw)]
+pub fn test_fn(input: &mut Point) {
+ input.x += 1.0;
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ test_mod::test_fn(&mut n);
+ if n.x >= 10.0 {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_mod_raw_noreturn.stderr b/rhai/codegen/ui_tests/export_mod_raw_noreturn.stderr
new file mode 100644
index 0000000..ef57fb5
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_raw_noreturn.stderr
@@ -0,0 +1,11 @@
+error: functions marked with 'return_raw' must return Result<T, Box<EvalAltResult>>
+ --> ui_tests/export_mod_raw_noreturn.rs:11:11
+ |
+11 | #[rhai_fn(return_raw)]
+ | ^^^^^^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_mod`
+ --> ui_tests/export_mod_raw_noreturn.rs:22:5
+ |
+22 | test_mod::test_fn(&mut n);
+ | ^^^^^^^^ use of undeclared crate or module `test_mod`
diff --git a/rhai/codegen/ui_tests/export_mod_raw_return.rs b/rhai/codegen/ui_tests/export_mod_raw_return.rs
new file mode 100644
index 0000000..ae52301
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_raw_return.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_mod {
+#[rhai_fn(return_raw)]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_mod::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/export_mod_raw_return.stderr b/rhai/codegen/ui_tests/export_mod_raw_return.stderr
new file mode 100644
index 0000000..1464012
--- /dev/null
+++ b/rhai/codegen/ui_tests/export_mod_raw_return.stderr
@@ -0,0 +1,13 @@
+error[E0599]: `bool` is not an iterator
+ --> ui_tests/export_mod_raw_return.rs:12:33
+ |
+9 | #[export_module]
+ | ---------------- in this procedural macro expansion
+...
+12 | pub fn test_fn(input: Point) -> bool {
+ | ^^^^ `bool` is not an iterator
+ |
+ = note: the following trait bounds were not satisfied:
+ `bool: std::iter::Iterator`
+ which is required by `&mut bool: std::iter::Iterator`
+ = note: this error originates in the attribute macro `export_module` (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/rhai/codegen/ui_tests/first_shared_ref.rs b/rhai/codegen/ui_tests/first_shared_ref.rs
new file mode 100644
index 0000000..f26fc40
--- /dev/null
+++ b/rhai/codegen/ui_tests/first_shared_ref.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+struct NonClonable {
+ a: f32,
+ b: u32,
+ c: char,
+ d: bool,
+}
+
+#[export_fn]
+pub fn test_fn(input: &NonClonable) -> bool {
+ input.d
+}
+
+fn main() {
+ let n = NonClonable {
+ a: 0.0,
+ b: 10,
+ c: 'a',
+ d: true,
+ };
+ if test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/first_shared_ref.stderr b/rhai/codegen/ui_tests/first_shared_ref.stderr
new file mode 100644
index 0000000..440d865
--- /dev/null
+++ b/rhai/codegen/ui_tests/first_shared_ref.stderr
@@ -0,0 +1,11 @@
+error: references from Rhai in this position must be mutable
+ --> ui_tests/first_shared_ref.rs:11:23
+ |
+11 | pub fn test_fn(input: &NonClonable) -> bool {
+ | ^
+
+error[E0425]: cannot find function `test_fn` in this scope
+ --> ui_tests/first_shared_ref.rs:22:8
+ |
+22 | if test_fn(n) {
+ | ^^^^^^^ not found in this scope
diff --git a/rhai/codegen/ui_tests/non_clonable.rs b/rhai/codegen/ui_tests/non_clonable.rs
new file mode 100644
index 0000000..69aa85e
--- /dev/null
+++ b/rhai/codegen/ui_tests/non_clonable.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+struct NonClonable {
+ a: f32,
+ b: u32,
+ c: char,
+ d: bool,
+}
+
+#[export_fn]
+pub fn test_fn(input: NonClonable) -> bool {
+ input.d
+}
+
+fn main() {
+ let n = NonClonable {
+ a: 0.0,
+ b: 10,
+ c: 'a',
+ d: true,
+ };
+ if test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/non_clonable.stderr b/rhai/codegen/ui_tests/non_clonable.stderr
new file mode 100644
index 0000000..0c52657
--- /dev/null
+++ b/rhai/codegen/ui_tests/non_clonable.stderr
@@ -0,0 +1,16 @@
+error[E0277]: the trait bound `NonClonable: Clone` is not satisfied
+ --> ui_tests/non_clonable.rs:11:23
+ |
+11 | pub fn test_fn(input: NonClonable) -> bool {
+ | ^^^^^^^^^^^ the trait `Clone` is not implemented for `NonClonable`
+ |
+note: required by a bound in `rhai::Dynamic::cast`
+ --> $WORKSPACE/src/types/dynamic.rs
+ |
+ | pub fn cast<T: Any + Clone>(self) -> T {
+ | ^^^^^ required by this bound in `Dynamic::cast`
+help: consider annotating `NonClonable` with `#[derive(Clone)]`
+ |
+3 + #[derive(Clone)]
+4 | struct NonClonable {
+ |
diff --git a/rhai/codegen/ui_tests/non_clonable_second.rs b/rhai/codegen/ui_tests/non_clonable_second.rs
new file mode 100644
index 0000000..ef33d48
--- /dev/null
+++ b/rhai/codegen/ui_tests/non_clonable_second.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+struct NonClonable {
+ a: f32,
+ b: u32,
+ c: char,
+ d: bool,
+}
+
+#[export_fn]
+pub fn test_fn(a: u32, b: NonClonable) -> bool {
+ a == 0 && b.d
+}
+
+fn main() {
+ let n = NonClonable {
+ a: 0.0,
+ b: 10,
+ c: 'a',
+ d: true,
+ };
+ if test_fn(10, n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/non_clonable_second.stderr b/rhai/codegen/ui_tests/non_clonable_second.stderr
new file mode 100644
index 0000000..5948b92
--- /dev/null
+++ b/rhai/codegen/ui_tests/non_clonable_second.stderr
@@ -0,0 +1,16 @@
+error[E0277]: the trait bound `NonClonable: Clone` is not satisfied
+ --> ui_tests/non_clonable_second.rs:11:27
+ |
+11 | pub fn test_fn(a: u32, b: NonClonable) -> bool {
+ | ^^^^^^^^^^^ the trait `Clone` is not implemented for `NonClonable`
+ |
+note: required by a bound in `rhai::Dynamic::cast`
+ --> $WORKSPACE/src/types/dynamic.rs
+ |
+ | pub fn cast<T: Any + Clone>(self) -> T {
+ | ^^^^^ required by this bound in `Dynamic::cast`
+help: consider annotating `NonClonable` with `#[derive(Clone)]`
+ |
+3 + #[derive(Clone)]
+4 | struct NonClonable {
+ |
diff --git a/rhai/codegen/ui_tests/return_mut_ref.rs b/rhai/codegen/ui_tests/return_mut_ref.rs
new file mode 100644
index 0000000..26b5679
--- /dev/null
+++ b/rhai/codegen/ui_tests/return_mut_ref.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Clonable {
+ a: f32,
+ b: u32,
+ c: char,
+ d: bool,
+}
+
+#[export_fn]
+pub fn test_fn(input: &mut Clonable) -> &mut bool {
+ &mut input.d
+}
+
+fn main() {
+ let n = Clonable {
+ a: 0.0,
+ b: 10,
+ c: 'a',
+ d: true,
+ };
+ if test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/return_mut_ref.stderr b/rhai/codegen/ui_tests/return_mut_ref.stderr
new file mode 100644
index 0000000..c1cc6fa
--- /dev/null
+++ b/rhai/codegen/ui_tests/return_mut_ref.stderr
@@ -0,0 +1,11 @@
+error: Rhai functions cannot return references
+ --> ui_tests/return_mut_ref.rs:12:38
+ |
+12 | pub fn test_fn(input: &mut Clonable) -> &mut bool {
+ | ^
+
+error[E0425]: cannot find function `test_fn` in this scope
+ --> ui_tests/return_mut_ref.rs:23:8
+ |
+23 | if test_fn(n) {
+ | ^^^^^^^ not found in this scope
diff --git a/rhai/codegen/ui_tests/return_pointer.rs b/rhai/codegen/ui_tests/return_pointer.rs
new file mode 100644
index 0000000..302799b
--- /dev/null
+++ b/rhai/codegen/ui_tests/return_pointer.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Clonable {
+ a: f32,
+ b: u32,
+ c: char,
+ d: bool,
+}
+
+#[export_fn]
+pub fn test_fn(input: Clonable) -> *const str {
+ "yes"
+}
+
+fn main() {
+ let n = Clonable {
+ a: 0.0,
+ b: 10,
+ c: 'a',
+ d: true,
+ };
+ println!("{}", unsafe {
+ let ptr = test_fn(n);
+ *ptr
+ });
+}
diff --git a/rhai/codegen/ui_tests/return_pointer.stderr b/rhai/codegen/ui_tests/return_pointer.stderr
new file mode 100644
index 0000000..4405f82
--- /dev/null
+++ b/rhai/codegen/ui_tests/return_pointer.stderr
@@ -0,0 +1,11 @@
+error: Rhai functions cannot return pointers
+ --> ui_tests/return_pointer.rs:12:33
+ |
+12 | pub fn test_fn(input: Clonable) -> *const str {
+ | ^
+
+error[E0425]: cannot find function `test_fn` in this scope
+ --> ui_tests/return_pointer.rs:24:19
+ |
+24 | let ptr = test_fn(n);
+ | ^^^^^^^ not found in this scope
diff --git a/rhai/codegen/ui_tests/return_shared_ref.rs b/rhai/codegen/ui_tests/return_shared_ref.rs
new file mode 100644
index 0000000..2fde9c2
--- /dev/null
+++ b/rhai/codegen/ui_tests/return_shared_ref.rs
@@ -0,0 +1,24 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Clonable {
+ a: f32,
+ b: u32,
+ c: char,
+ d: bool,
+}
+
+#[export_fn]
+pub fn test_fn(input: Clonable) -> &'static str {
+ "yes"
+}
+
+fn main() {
+ let n = Clonable {
+ a: 0.0,
+ b: 10,
+ c: 'a',
+ d: true,
+ };
+ println!("{}", test_fn(n));
+}
diff --git a/rhai/codegen/ui_tests/return_shared_ref.stderr b/rhai/codegen/ui_tests/return_shared_ref.stderr
new file mode 100644
index 0000000..0a9be23
--- /dev/null
+++ b/rhai/codegen/ui_tests/return_shared_ref.stderr
@@ -0,0 +1,11 @@
+error: Rhai functions cannot return references
+ --> ui_tests/return_shared_ref.rs:12:33
+ |
+12 | pub fn test_fn(input: Clonable) -> &'static str {
+ | ^
+
+error[E0425]: cannot find function `test_fn` in this scope
+ --> ui_tests/return_shared_ref.rs:23:20
+ |
+23 | println!("{}", test_fn(n));
+ | ^^^^^^^ not found in this scope
diff --git a/rhai/codegen/ui_tests/rhai_fn_bad_attr.rs b/rhai/codegen/ui_tests/rhai_fn_bad_attr.rs
new file mode 100644
index 0000000..119efdb
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_bad_attr.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+#[rhai_fn(unknown = "thing")]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_bad_attr.stderr b/rhai/codegen/ui_tests/rhai_fn_bad_attr.stderr
new file mode 100644
index 0000000..f2b04ab
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_bad_attr.stderr
@@ -0,0 +1,11 @@
+error: unknown attribute 'unknown'
+ --> ui_tests/rhai_fn_bad_attr.rs:11:11
+ |
+11 | #[rhai_fn(unknown = "thing")]
+ | ^^^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_bad_attr.rs:22:8
+ |
+22 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_bad_value.rs b/rhai/codegen/ui_tests/rhai_fn_bad_value.rs
new file mode 100644
index 0000000..307e3c0
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_bad_value.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+#[rhai_fn(name = true)]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_bad_value.stderr b/rhai/codegen/ui_tests/rhai_fn_bad_value.stderr
new file mode 100644
index 0000000..b9ff43c
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_bad_value.stderr
@@ -0,0 +1,11 @@
+error: expecting string literal
+ --> ui_tests/rhai_fn_bad_value.rs:11:18
+ |
+11 | #[rhai_fn(name = true)]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_bad_value.rs:22:8
+ |
+22 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_duplicate_attr.rs b/rhai/codegen/ui_tests/rhai_fn_duplicate_attr.rs
new file mode 100644
index 0000000..b775fe4
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_duplicate_attr.rs
@@ -0,0 +1,18 @@
+use rhai::plugin::*;
+
+#[export_module]
+pub mod test_module {
+ #[rhai_fn(name = "test")]
+ #[rhai_fn(pure)]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_duplicate_attr.stderr b/rhai/codegen/ui_tests/rhai_fn_duplicate_attr.stderr
new file mode 100644
index 0000000..57ce356
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_duplicate_attr.stderr
@@ -0,0 +1,17 @@
+error: duplicated attribute 'rhai_fn'
+ --> ui_tests/rhai_fn_duplicate_attr.rs:6:5
+ |
+6 | #[rhai_fn(pure)]
+ | ^
+
+error[E0425]: cannot find value `n` in this scope
+ --> ui_tests/rhai_fn_duplicate_attr.rs:13:29
+ |
+13 | if test_module::test_fn(n) {
+ | ^ not found in this scope
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_duplicate_attr.rs:13:8
+ |
+13 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_extra_value.rs b/rhai/codegen/ui_tests/rhai_fn_extra_value.rs
new file mode 100644
index 0000000..f86b2df
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_extra_value.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+#[rhai_fn(return_raw = "yes")]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_extra_value.stderr b/rhai/codegen/ui_tests/rhai_fn_extra_value.stderr
new file mode 100644
index 0000000..0e7ed9c
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_extra_value.stderr
@@ -0,0 +1,11 @@
+error: extraneous value
+ --> ui_tests/rhai_fn_extra_value.rs:11:24
+ |
+11 | #[rhai_fn(return_raw = "yes")]
+ | ^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_extra_value.rs:22:8
+ |
+22 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_getter_conflict.rs b/rhai/codegen/ui_tests/rhai_fn_getter_conflict.rs
new file mode 100644
index 0000000..66cdb9d
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_getter_conflict.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(name = "foo", get = "foo", set = "bar")]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_getter_conflict.stderr b/rhai/codegen/ui_tests/rhai_fn_getter_conflict.stderr
new file mode 100644
index 0000000..2c2d879
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_getter_conflict.stderr
@@ -0,0 +1,11 @@
+error: conflicting setter
+ --> ui_tests/rhai_fn_getter_conflict.rs:12:42
+ |
+12 | #[rhai_fn(name = "foo", get = "foo", set = "bar")]
+ | ^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_getter_conflict.rs:23:8
+ |
+23 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_getter_multiple.rs b/rhai/codegen/ui_tests/rhai_fn_getter_multiple.rs
new file mode 100644
index 0000000..d60a11b
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_getter_multiple.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(name = "foo", get = "foo", get = "bar")]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_getter_multiple.stderr b/rhai/codegen/ui_tests/rhai_fn_getter_multiple.stderr
new file mode 100644
index 0000000..f6099f1
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_getter_multiple.stderr
@@ -0,0 +1,11 @@
+error: conflicting getter
+ --> ui_tests/rhai_fn_getter_multiple.rs:12:42
+ |
+12 | #[rhai_fn(name = "foo", get = "foo", get = "bar")]
+ | ^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_getter_multiple.rs:23:8
+ |
+23 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_getter_return.rs b/rhai/codegen/ui_tests/rhai_fn_getter_return.rs
new file mode 100644
index 0000000..da7c845
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_getter_return.rs
@@ -0,0 +1,29 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(get = "foo")]
+ pub fn test_fn(input: &mut Point) {
+ input.x *= 2.0;
+ }
+}
+
+fn main() {
+ let mut n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ test_module::test_fn(&mut n);
+ if n.x > 10.0 {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_getter_return.stderr b/rhai/codegen/ui_tests/rhai_fn_getter_return.stderr
new file mode 100644
index 0000000..07964e3
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_getter_return.stderr
@@ -0,0 +1,11 @@
+error: property getter must return a value
+ --> ui_tests/rhai_fn_getter_return.rs:13:9
+ |
+13 | pub fn test_fn(input: &mut Point) {
+ | ^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_getter_return.rs:23:5
+ |
+23 | test_module::test_fn(&mut n);
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_getter_signature.rs b/rhai/codegen/ui_tests/rhai_fn_getter_signature.rs
new file mode 100644
index 0000000..c4f1952
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_getter_signature.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(get = "foo")]
+ pub fn test_fn(input: Point, value: bool) -> bool {
+ value && input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n, true) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_getter_signature.stderr b/rhai/codegen/ui_tests/rhai_fn_getter_signature.stderr
new file mode 100644
index 0000000..8b7e3b8
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_getter_signature.stderr
@@ -0,0 +1,11 @@
+error: property getter requires exactly 1 parameter
+ --> ui_tests/rhai_fn_getter_signature.rs:13:20
+ |
+13 | pub fn test_fn(input: Point, value: bool) -> bool {
+ | ^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_getter_signature.rs:23:8
+ |
+23 | if test_module::test_fn(n, true) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_global_multiple.rs b/rhai/codegen/ui_tests/rhai_fn_global_multiple.rs
new file mode 100644
index 0000000..6b67011
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_global_multiple.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(global, internal)]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_global_multiple.stderr b/rhai/codegen/ui_tests/rhai_fn_global_multiple.stderr
new file mode 100644
index 0000000..6e4f2f0
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_global_multiple.stderr
@@ -0,0 +1,11 @@
+error: namespace is already set to 'global'
+ --> ui_tests/rhai_fn_global_multiple.rs:12:23
+ |
+12 | #[rhai_fn(global, internal)]
+ | ^^^^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_global_multiple.rs:23:8
+ |
+23 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_index_getter_multiple.rs b/rhai/codegen/ui_tests/rhai_fn_index_getter_multiple.rs
new file mode 100644
index 0000000..450c8aa
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_index_getter_multiple.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(name = "foo", index_get, index_get)]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_index_getter_multiple.stderr b/rhai/codegen/ui_tests/rhai_fn_index_getter_multiple.stderr
new file mode 100644
index 0000000..cadc8ab
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_index_getter_multiple.stderr
@@ -0,0 +1,11 @@
+error: conflicting index_get
+ --> ui_tests/rhai_fn_index_getter_multiple.rs:12:40
+ |
+12 | #[rhai_fn(name = "foo", index_get, index_get)]
+ | ^^^^^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_index_getter_multiple.rs:23:8
+ |
+23 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_index_getter_return.rs b/rhai/codegen/ui_tests/rhai_fn_index_getter_return.rs
new file mode 100644
index 0000000..32b8c3e
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_index_getter_return.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(index_get)]
+ pub fn test_fn(input: &mut Point, i: f32) {
+ input.x *= 2.0;
+ }
+}
+
+fn main() {
+ let mut n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(&mut n, 5.0) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_index_getter_return.stderr b/rhai/codegen/ui_tests/rhai_fn_index_getter_return.stderr
new file mode 100644
index 0000000..35abddd
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_index_getter_return.stderr
@@ -0,0 +1,11 @@
+error: index getter must return a value
+ --> ui_tests/rhai_fn_index_getter_return.rs:13:9
+ |
+13 | pub fn test_fn(input: &mut Point, i: f32) {
+ | ^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_index_getter_return.rs:23:8
+ |
+23 | if test_module::test_fn(&mut n, 5.0) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_index_getter_signature.rs b/rhai/codegen/ui_tests/rhai_fn_index_getter_signature.rs
new file mode 100644
index 0000000..e6fe68f
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_index_getter_signature.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(index_get)]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_index_getter_signature.stderr b/rhai/codegen/ui_tests/rhai_fn_index_getter_signature.stderr
new file mode 100644
index 0000000..13788fe
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_index_getter_signature.stderr
@@ -0,0 +1,11 @@
+error: index getter requires exactly 2 parameters
+ --> ui_tests/rhai_fn_index_getter_signature.rs:13:20
+ |
+13 | pub fn test_fn(input: Point) -> bool {
+ | ^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_index_getter_signature.rs:23:8
+ |
+23 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_index_setter_multiple.rs b/rhai/codegen/ui_tests/rhai_fn_index_setter_multiple.rs
new file mode 100644
index 0000000..77d8cf3
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_index_setter_multiple.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(name = "foo", index_set, index_set)]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_index_setter_multiple.stderr b/rhai/codegen/ui_tests/rhai_fn_index_setter_multiple.stderr
new file mode 100644
index 0000000..b56d3bc
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_index_setter_multiple.stderr
@@ -0,0 +1,11 @@
+error: conflicting index_set
+ --> ui_tests/rhai_fn_index_setter_multiple.rs:12:40
+ |
+12 | #[rhai_fn(name = "foo", index_set, index_set)]
+ | ^^^^^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_index_setter_multiple.rs:23:8
+ |
+23 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_junk_arg.rs b/rhai/codegen/ui_tests/rhai_fn_junk_arg.rs
new file mode 100644
index 0000000..b84424a
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_junk_arg.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+#[rhai_fn("wheeeee")]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_junk_arg.stderr b/rhai/codegen/ui_tests/rhai_fn_junk_arg.stderr
new file mode 100644
index 0000000..d538eb3
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_junk_arg.stderr
@@ -0,0 +1,11 @@
+error: expecting identifier
+ --> ui_tests/rhai_fn_junk_arg.rs:11:11
+ |
+11 | #[rhai_fn("wheeeee")]
+ | ^^^^^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_junk_arg.rs:22:8
+ |
+22 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_missing_value.rs b/rhai/codegen/ui_tests/rhai_fn_missing_value.rs
new file mode 100644
index 0000000..2534953
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_missing_value.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+#[rhai_fn(name)]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_missing_value.stderr b/rhai/codegen/ui_tests/rhai_fn_missing_value.stderr
new file mode 100644
index 0000000..bba6952
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_missing_value.stderr
@@ -0,0 +1,11 @@
+error: requires value
+ --> ui_tests/rhai_fn_missing_value.rs:11:11
+ |
+11 | #[rhai_fn(name)]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_missing_value.rs:22:8
+ |
+22 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_non_clonable_return.rs b/rhai/codegen/ui_tests/rhai_fn_non_clonable_return.rs
new file mode 100644
index 0000000..e2e2d78
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_non_clonable_return.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+struct NonClonable {
+ a: f32,
+ b: u32,
+ c: char,
+ d: bool,
+}
+
+#[export_fn]
+pub fn test_fn(input: f32) -> NonClonable {
+ NonClonable {
+ a: input,
+ b: 10,
+ c: 'a',
+ d: true,
+ }
+}
+
+fn main() {
+ let n = test_fn(20.0);
+ if n.c == 'a' {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_non_clonable_return.stderr b/rhai/codegen/ui_tests/rhai_fn_non_clonable_return.stderr
new file mode 100644
index 0000000..48184f5
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_non_clonable_return.stderr
@@ -0,0 +1,19 @@
+error[E0277]: the trait bound `NonClonable: Clone` is not satisfied
+ --> ui_tests/rhai_fn_non_clonable_return.rs:11:31
+ |
+10 | #[export_fn]
+ | ------------ in this procedural macro expansion
+11 | pub fn test_fn(input: f32) -> NonClonable {
+ | ^^^^^^^^^^^ the trait `Clone` is not implemented for `NonClonable`
+ |
+note: required by a bound in `rhai::Dynamic::from`
+ --> $WORKSPACE/src/types/dynamic.rs
+ |
+ | pub fn from<T: Variant + Clone>(value: T) -> Self {
+ | ^^^^^ required by this bound in `Dynamic::from`
+ = note: this error originates in the attribute macro `export_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
+help: consider annotating `NonClonable` with `#[derive(Clone)]`
+ |
+3 + #[derive(Clone)]
+4 | struct NonClonable {
+ |
diff --git a/rhai/codegen/ui_tests/rhai_fn_path_attr.rs b/rhai/codegen/ui_tests/rhai_fn_path_attr.rs
new file mode 100644
index 0000000..5c2174a
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_path_attr.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+#[rhai_fn(rhai::name = "thing")]
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_path_attr.stderr b/rhai/codegen/ui_tests/rhai_fn_path_attr.stderr
new file mode 100644
index 0000000..ef9c876
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_path_attr.stderr
@@ -0,0 +1,11 @@
+error: expecting attribute name
+ --> ui_tests/rhai_fn_path_attr.rs:11:11
+ |
+11 | #[rhai_fn(rhai::name = "thing")]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_path_attr.rs:22:8
+ |
+22 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_collision.rs b/rhai/codegen/ui_tests/rhai_fn_rename_collision.rs
new file mode 100644
index 0000000..38814f3
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_collision.rs
@@ -0,0 +1,33 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(name = "foo")]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+
+ #[rhai_fn(name = "foo")]
+ pub fn test_fn_2(input: Point) -> bool {
+ input.x < input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_collision.stderr b/rhai/codegen/ui_tests/rhai_fn_rename_collision.stderr
new file mode 100644
index 0000000..e865313
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_collision.stderr
@@ -0,0 +1,17 @@
+error: duplicate Rhai signature for 'foo'
+ --> ui_tests/rhai_fn_rename_collision.rs:17:15
+ |
+17 | #[rhai_fn(name = "foo")]
+ | ^^^^
+
+error: duplicated function renamed 'foo'
+ --> ui_tests/rhai_fn_rename_collision.rs:12:15
+ |
+12 | #[rhai_fn(name = "foo")]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_rename_collision.rs:28:8
+ |
+28 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_collision_oneattr.rs b/rhai/codegen/ui_tests/rhai_fn_rename_collision_oneattr.rs
new file mode 100644
index 0000000..373eab0
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_collision_oneattr.rs
@@ -0,0 +1,32 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(name = "foo")]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+
+ pub fn foo(input: Point) -> bool {
+ input.x < input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_collision_oneattr.stderr b/rhai/codegen/ui_tests/rhai_fn_rename_collision_oneattr.stderr
new file mode 100644
index 0000000..b98c127
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_collision_oneattr.stderr
@@ -0,0 +1,17 @@
+error: duplicate Rhai signature for 'foo'
+ --> ui_tests/rhai_fn_rename_collision_oneattr.rs:17:12
+ |
+17 | pub fn foo(input: Point) -> bool {
+ | ^^^
+
+error: duplicated function 'foo'
+ --> ui_tests/rhai_fn_rename_collision_oneattr.rs:12:15
+ |
+12 | #[rhai_fn(name = "foo")]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_rename_collision_oneattr.rs:27:8
+ |
+27 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.rs b/rhai/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.rs
new file mode 100644
index 0000000..112d9cc
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.rs
@@ -0,0 +1,30 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(name = "foo", get = "bar")]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+
+ #[rhai_fn(get = "bar")]
+ pub fn foo(input: Point) -> bool {
+ input.x < input.y
+ }
+}
+
+fn main() {
+ let n = Point { x: 0.0, y: 10.0 };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.stderr b/rhai/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.stderr
new file mode 100644
index 0000000..f19c017
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.stderr
@@ -0,0 +1,17 @@
+error: duplicate Rhai signature for 'bar'
+ --> ui_tests/rhai_fn_rename_collision_oneattr_multiple.rs:17:15
+ |
+17 | #[rhai_fn(get = "bar")]
+ | ^^^
+
+error: duplicated function renamed 'bar'
+ --> ui_tests/rhai_fn_rename_collision_oneattr_multiple.rs:12:15
+ |
+12 | #[rhai_fn(name = "foo", get = "bar")]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_rename_collision_oneattr_multiple.rs:25:8
+ |
+25 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_collision_with_itself.rs b/rhai/codegen/ui_tests/rhai_fn_rename_collision_with_itself.rs
new file mode 100644
index 0000000..0ba996e
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_collision_with_itself.rs
@@ -0,0 +1,25 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(name = "foo", name = "bar", name = "foo")]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point { x: 0.0, y: 10.0 };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_collision_with_itself.stderr b/rhai/codegen/ui_tests/rhai_fn_rename_collision_with_itself.stderr
new file mode 100644
index 0000000..f39add0
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_collision_with_itself.stderr
@@ -0,0 +1,17 @@
+error: duplicate Rhai signature for 'foo'
+ --> ui_tests/rhai_fn_rename_collision_with_itself.rs:12:15
+ |
+12 | #[rhai_fn(name = "foo", name = "bar", name = "foo")]
+ | ^^^^
+
+error: duplicated function renamed 'foo'
+ --> ui_tests/rhai_fn_rename_collision_with_itself.rs:12:15
+ |
+12 | #[rhai_fn(name = "foo", name = "bar", name = "foo")]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_rename_collision_with_itself.rs:20:8
+ |
+20 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_to_index_getter.rs b/rhai/codegen/ui_tests/rhai_fn_rename_to_index_getter.rs
new file mode 100644
index 0000000..394113b
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_to_index_getter.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(name = "index$get$")]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_to_index_getter.stderr b/rhai/codegen/ui_tests/rhai_fn_rename_to_index_getter.stderr
new file mode 100644
index 0000000..d269333
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_to_index_getter.stderr
@@ -0,0 +1,11 @@
+error: use attribute 'index_get' instead
+ --> ui_tests/rhai_fn_rename_to_index_getter.rs:12:15
+ |
+12 | #[rhai_fn(name = "index$get$")]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_rename_to_index_getter.rs:23:8
+ |
+23 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_to_index_setter.rs b/rhai/codegen/ui_tests/rhai_fn_rename_to_index_setter.rs
new file mode 100644
index 0000000..cb5d7c8
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_to_index_setter.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(name = "index$set$")]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_to_index_setter.stderr b/rhai/codegen/ui_tests/rhai_fn_rename_to_index_setter.stderr
new file mode 100644
index 0000000..4f2370a
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_to_index_setter.stderr
@@ -0,0 +1,11 @@
+error: use attribute 'index_set' instead
+ --> ui_tests/rhai_fn_rename_to_index_setter.rs:12:15
+ |
+12 | #[rhai_fn(name = "index$set$")]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_rename_to_index_setter.rs:23:8
+ |
+23 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_to_prop_getter.rs b/rhai/codegen/ui_tests/rhai_fn_rename_to_prop_getter.rs
new file mode 100644
index 0000000..d81aa5d
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_to_prop_getter.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(name = "get$foo")]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_to_prop_getter.stderr b/rhai/codegen/ui_tests/rhai_fn_rename_to_prop_getter.stderr
new file mode 100644
index 0000000..8b4ff8d
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_to_prop_getter.stderr
@@ -0,0 +1,11 @@
+error: use attribute 'getter = "foo"' instead
+ --> ui_tests/rhai_fn_rename_to_prop_getter.rs:12:15
+ |
+12 | #[rhai_fn(name = "get$foo")]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_rename_to_prop_getter.rs:23:8
+ |
+23 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_to_prop_setter.rs b/rhai/codegen/ui_tests/rhai_fn_rename_to_prop_setter.rs
new file mode 100644
index 0000000..d81aa5d
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_to_prop_setter.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(name = "get$foo")]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_rename_to_prop_setter.stderr b/rhai/codegen/ui_tests/rhai_fn_rename_to_prop_setter.stderr
new file mode 100644
index 0000000..7649837
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_rename_to_prop_setter.stderr
@@ -0,0 +1,11 @@
+error: use attribute 'getter = "foo"' instead
+ --> ui_tests/rhai_fn_rename_to_prop_setter.rs:12:15
+ |
+12 | #[rhai_fn(name = "get$foo")]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_rename_to_prop_setter.rs:23:8
+ |
+23 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_setter_index_signature.rs b/rhai/codegen/ui_tests/rhai_fn_setter_index_signature.rs
new file mode 100644
index 0000000..21a7184
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_setter_index_signature.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(index_set)]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_setter_index_signature.stderr b/rhai/codegen/ui_tests/rhai_fn_setter_index_signature.stderr
new file mode 100644
index 0000000..372070f
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_setter_index_signature.stderr
@@ -0,0 +1,11 @@
+error: index setter requires exactly 3 parameters
+ --> ui_tests/rhai_fn_setter_index_signature.rs:13:20
+ |
+13 | pub fn test_fn(input: Point) -> bool {
+ | ^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_setter_index_signature.rs:23:8
+ |
+23 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_setter_multiple.rs b/rhai/codegen/ui_tests/rhai_fn_setter_multiple.rs
new file mode 100644
index 0000000..170a373
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_setter_multiple.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(name = "foo", set = "foo", set = "bar")]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_setter_multiple.stderr b/rhai/codegen/ui_tests/rhai_fn_setter_multiple.stderr
new file mode 100644
index 0000000..3de313c
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_setter_multiple.stderr
@@ -0,0 +1,11 @@
+error: conflicting setter
+ --> ui_tests/rhai_fn_setter_multiple.rs:12:42
+ |
+12 | #[rhai_fn(name = "foo", set = "foo", set = "bar")]
+ | ^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_setter_multiple.rs:23:8
+ |
+23 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_setter_return.rs b/rhai/codegen/ui_tests/rhai_fn_setter_return.rs
new file mode 100644
index 0000000..5a2542a
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_setter_return.rs
@@ -0,0 +1,29 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(set = "foo")]
+ pub fn test_fn(input: &mut Point, value: f32) -> bool {
+ let z = if value % 2 { input.x } else { input.y };
+ *input.x = z;
+ }
+}
+
+fn main() {
+ let mut n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(&mut n, 5.0) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_setter_return.stderr b/rhai/codegen/ui_tests/rhai_fn_setter_return.stderr
new file mode 100644
index 0000000..c08f37f
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_setter_return.stderr
@@ -0,0 +1,11 @@
+error: property setter cannot return any value
+ --> ui_tests/rhai_fn_setter_return.rs:13:51
+ |
+13 | pub fn test_fn(input: &mut Point, value: f32) -> bool {
+ | ^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_setter_return.rs:24:8
+ |
+24 | if test_module::test_fn(&mut n, 5.0) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_fn_setter_signature.rs b/rhai/codegen/ui_tests/rhai_fn_setter_signature.rs
new file mode 100644
index 0000000..06e401a
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_setter_signature.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ #[rhai_fn(set = "foo")]
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_fn_setter_signature.stderr b/rhai/codegen/ui_tests/rhai_fn_setter_signature.stderr
new file mode 100644
index 0000000..6d34574
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_fn_setter_signature.stderr
@@ -0,0 +1,11 @@
+error: property setter requires exactly 2 parameters
+ --> ui_tests/rhai_fn_setter_signature.rs:13:20
+ |
+13 | pub fn test_fn(input: Point) -> bool {
+ | ^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_fn_setter_signature.rs:23:8
+ |
+23 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_mod_bad_attr.rs b/rhai/codegen/ui_tests/rhai_mod_bad_attr.rs
new file mode 100644
index 0000000..09319ce
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_bad_attr.rs
@@ -0,0 +1,29 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+#[rhai_mod(unknown = "thing")]
+pub mod test_mod {
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_mod_bad_attr.stderr b/rhai/codegen/ui_tests/rhai_mod_bad_attr.stderr
new file mode 100644
index 0000000..8bc7c03
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_bad_attr.stderr
@@ -0,0 +1,11 @@
+error: unknown attribute 'unknown'
+ --> ui_tests/rhai_mod_bad_attr.rs:11:12
+ |
+11 | #[rhai_mod(unknown = "thing")]
+ | ^^^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_mod_bad_attr.rs:24:8
+ |
+24 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_mod_bad_value.rs b/rhai/codegen/ui_tests/rhai_mod_bad_value.rs
new file mode 100644
index 0000000..3d30fdc
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_bad_value.rs
@@ -0,0 +1,29 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+#[rhai_mod(name = true)]
+pub mod test_mod {
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_mod_bad_value.stderr b/rhai/codegen/ui_tests/rhai_mod_bad_value.stderr
new file mode 100644
index 0000000..f3110cc
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_bad_value.stderr
@@ -0,0 +1,11 @@
+error: expecting string literal
+ --> ui_tests/rhai_mod_bad_value.rs:11:19
+ |
+11 | #[rhai_mod(name = true)]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_mod_bad_value.rs:24:8
+ |
+24 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_mod_inner_cfg_false.rs b/rhai/codegen/ui_tests/rhai_mod_inner_cfg_false.rs
new file mode 100644
index 0000000..cfb3e1b
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_inner_cfg_false.rs
@@ -0,0 +1,29 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ #[cfg(feature = "unset_feature")]
+ pub mod test_mod {
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_mod::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_mod_inner_cfg_false.stderr b/rhai/codegen/ui_tests/rhai_mod_inner_cfg_false.stderr
new file mode 100644
index 0000000..b0feeb4
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_inner_cfg_false.stderr
@@ -0,0 +1,5 @@
+error[E0433]: failed to resolve: could not find `test_mod` in `test_module`
+ --> ui_tests/rhai_mod_inner_cfg_false.rs:24:21
+ |
+24 | if test_module::test_mod::test_fn(n) {
+ | ^^^^^^^^ could not find `test_mod` in `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_mod_junk_arg.rs b/rhai/codegen/ui_tests/rhai_mod_junk_arg.rs
new file mode 100644
index 0000000..842c0ba
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_junk_arg.rs
@@ -0,0 +1,29 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+#[rhai_mod("wheeeee")]
+pub mod test_mod {
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_mod_junk_arg.stderr b/rhai/codegen/ui_tests/rhai_mod_junk_arg.stderr
new file mode 100644
index 0000000..819b879
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_junk_arg.stderr
@@ -0,0 +1,11 @@
+error: expecting identifier
+ --> ui_tests/rhai_mod_junk_arg.rs:11:12
+ |
+11 | #[rhai_mod("wheeeee")]
+ | ^^^^^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_mod_junk_arg.rs:24:8
+ |
+24 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_mod_missing_value.rs b/rhai/codegen/ui_tests/rhai_mod_missing_value.rs
new file mode 100644
index 0000000..f231b65
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_missing_value.rs
@@ -0,0 +1,29 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+#[rhai_mod(name)]
+pub mod test_mod {
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_mod_missing_value.stderr b/rhai/codegen/ui_tests/rhai_mod_missing_value.stderr
new file mode 100644
index 0000000..0036ae1
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_missing_value.stderr
@@ -0,0 +1,11 @@
+error: requires value
+ --> ui_tests/rhai_mod_missing_value.rs:11:12
+ |
+11 | #[rhai_mod(name)]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_mod_missing_value.rs:24:8
+ |
+24 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_mod_name_collisions.rs b/rhai/codegen/ui_tests/rhai_mod_name_collisions.rs
new file mode 100644
index 0000000..c770955
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_name_collisions.rs
@@ -0,0 +1,31 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+ }
+
+ pub fn test_fn(input: Point) -> bool {
+ input.x < input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_mod_name_collisions.stderr b/rhai/codegen/ui_tests/rhai_mod_name_collisions.stderr
new file mode 100644
index 0000000..6b3f1a4
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_name_collisions.stderr
@@ -0,0 +1,17 @@
+error: duplicate function 'test_fn'
+ --> ui_tests/rhai_mod_name_collisions.rs:16:12
+ |
+16 | pub fn test_fn(input: Point) -> bool {
+ | ^^^^^^^
+
+error: duplicated function 'test_fn'
+ --> ui_tests/rhai_mod_name_collisions.rs:12:12
+ |
+12 | pub fn test_fn(input: Point) -> bool {
+ | ^^^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_mod_name_collisions.rs:26:8
+ |
+26 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_mod_non_clonable_return.rs b/rhai/codegen/ui_tests/rhai_mod_non_clonable_return.rs
new file mode 100644
index 0000000..fe8f5ff
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_non_clonable_return.rs
@@ -0,0 +1,29 @@
+use rhai::plugin::*;
+
+struct NonClonable {
+ a: f32,
+ b: u32,
+ c: char,
+ d: bool,
+}
+
+#[export_module]
+pub mod test_mod {
+ pub fn test_fn(input: f32) -> NonClonable {
+ NonClonable {
+ a: input,
+ b: 10,
+ c: 'a',
+ d: true,
+ }
+ }
+}
+
+fn main() {
+ let n = test_mod::test_fn(20.0);
+ if n.c == 'a' {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_mod_non_clonable_return.stderr b/rhai/codegen/ui_tests/rhai_mod_non_clonable_return.stderr
new file mode 100644
index 0000000..97e0c11
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_non_clonable_return.stderr
@@ -0,0 +1,20 @@
+error[E0277]: the trait bound `NonClonable: Clone` is not satisfied
+ --> ui_tests/rhai_mod_non_clonable_return.rs:12:35
+ |
+10 | #[export_module]
+ | ---------------- in this procedural macro expansion
+11 | pub mod test_mod {
+12 | pub fn test_fn(input: f32) -> NonClonable {
+ | ^^^^^^^^^^^ the trait `Clone` is not implemented for `NonClonable`
+ |
+note: required by a bound in `rhai::Dynamic::from`
+ --> $WORKSPACE/src/types/dynamic.rs
+ |
+ | pub fn from<T: Variant + Clone>(value: T) -> Self {
+ | ^^^^^ required by this bound in `Dynamic::from`
+ = note: this error originates in the attribute macro `export_module` (in Nightly builds, run with -Z macro-backtrace for more info)
+help: consider annotating `NonClonable` with `#[derive(Clone)]`
+ |
+3 + #[derive(Clone)]
+4 | struct NonClonable {
+ |
diff --git a/rhai/codegen/ui_tests/rhai_mod_path_attr.rs b/rhai/codegen/ui_tests/rhai_mod_path_attr.rs
new file mode 100644
index 0000000..43be960
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_path_attr.rs
@@ -0,0 +1,29 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+#[rhai_mod(rhai::name = "thing")]
+pub mod test_mod {
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_mod_path_attr.stderr b/rhai/codegen/ui_tests/rhai_mod_path_attr.stderr
new file mode 100644
index 0000000..0d9ea5a
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_path_attr.stderr
@@ -0,0 +1,11 @@
+error: expecting attribute name
+ --> ui_tests/rhai_mod_path_attr.rs:11:12
+ |
+11 | #[rhai_mod(rhai::name = "thing")]
+ | ^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_mod_path_attr.rs:24:8
+ |
+24 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_mod_return_raw.rs b/rhai/codegen/ui_tests/rhai_mod_return_raw.rs
new file mode 100644
index 0000000..e5fed7e
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_return_raw.rs
@@ -0,0 +1,29 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+#[rhai_mod(return_raw = "yes")]
+pub mod test_mod {
+pub fn test_fn(input: Point) -> bool {
+ input.x > input.y
+}
+}
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_mod_return_raw.stderr b/rhai/codegen/ui_tests/rhai_mod_return_raw.stderr
new file mode 100644
index 0000000..b3b8443
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_return_raw.stderr
@@ -0,0 +1,11 @@
+error: unknown attribute 'return_raw'
+ --> ui_tests/rhai_mod_return_raw.rs:11:12
+ |
+11 | #[rhai_mod(return_raw = "yes")]
+ | ^^^^^^^^^^
+
+error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
+ --> ui_tests/rhai_mod_return_raw.rs:24:8
+ |
+24 | if test_module::test_fn(n) {
+ | ^^^^^^^^^^^ use of undeclared crate or module `test_module`
diff --git a/rhai/codegen/ui_tests/rhai_mod_unknown_type.rs b/rhai/codegen/ui_tests/rhai_mod_unknown_type.rs
new file mode 100644
index 0000000..4c067fd
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_unknown_type.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ pub fn test_fn(input: Pointer) -> bool {
+ input.x < input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_mod_unknown_type.stderr b/rhai/codegen/ui_tests/rhai_mod_unknown_type.stderr
new file mode 100644
index 0000000..f6b0ec8
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_unknown_type.stderr
@@ -0,0 +1,21 @@
+error[E0412]: cannot find type `Pointer` in this scope
+ --> ui_tests/rhai_mod_unknown_type.rs:12:27
+ |
+4 | pub struct Point {
+ | ---------------- similarly named struct `Point` defined here
+...
+12 | pub fn test_fn(input: Pointer) -> bool {
+ | ^^^^^^^
+ |
+help: a struct with a similar name exists
+ |
+12 | pub fn test_fn(input: Point) -> bool {
+ | ~~~~~
+help: consider importing one of these items
+ |
+11 + use core::fmt::Pointer;
+ |
+11 + use std::fmt::Pointer;
+ |
+11 + use syn::__private::fmt::Pointer;
+ |
diff --git a/rhai/codegen/ui_tests/rhai_mod_unknown_type_return.rs b/rhai/codegen/ui_tests/rhai_mod_unknown_type_return.rs
new file mode 100644
index 0000000..c2287ea
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_unknown_type_return.rs
@@ -0,0 +1,27 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Point {
+ x: f32,
+ y: f32,
+}
+
+#[export_module]
+pub mod test_module {
+ pub use super::Point;
+ pub fn test_fn(input: Point) -> boool {
+ input.x < input.y
+ }
+}
+
+fn main() {
+ let n = Point {
+ x: 0.0,
+ y: 10.0,
+ };
+ if test_module::test_fn(n) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/rhai_mod_unknown_type_return.stderr b/rhai/codegen/ui_tests/rhai_mod_unknown_type_return.stderr
new file mode 100644
index 0000000..4a8bf32
--- /dev/null
+++ b/rhai/codegen/ui_tests/rhai_mod_unknown_type_return.stderr
@@ -0,0 +1,5 @@
+error[E0412]: cannot find type `boool` in this scope
+ --> ui_tests/rhai_mod_unknown_type_return.rs:12:37
+ |
+12 | pub fn test_fn(input: Point) -> boool {
+ | ^^^^^ help: a builtin type with a similar name exists: `bool`
diff --git a/rhai/codegen/ui_tests/second_shared_ref.rs b/rhai/codegen/ui_tests/second_shared_ref.rs
new file mode 100644
index 0000000..4768c06
--- /dev/null
+++ b/rhai/codegen/ui_tests/second_shared_ref.rs
@@ -0,0 +1,28 @@
+use rhai::plugin::*;
+
+#[derive(Clone)]
+pub struct Clonable {
+ a: f32,
+ b: u32,
+ c: char,
+ d: bool,
+}
+
+#[export_fn]
+pub fn test_fn(input: Clonable, factor: &bool) -> bool {
+ input.d & factor
+}
+
+fn main() {
+ let n = Clonable {
+ a: 0.0,
+ b: 10,
+ c: 'a',
+ d: true,
+ };
+ if test_fn(n, &true) {
+ println!("yes");
+ } else {
+ println!("no");
+ }
+}
diff --git a/rhai/codegen/ui_tests/second_shared_ref.stderr b/rhai/codegen/ui_tests/second_shared_ref.stderr
new file mode 100644
index 0000000..814cd25
--- /dev/null
+++ b/rhai/codegen/ui_tests/second_shared_ref.stderr
@@ -0,0 +1,11 @@
+error: function parameters other than the first one cannot be passed by reference
+ --> ui_tests/second_shared_ref.rs:12:41
+ |
+12 | pub fn test_fn(input: Clonable, factor: &bool) -> bool {
+ | ^
+
+error[E0425]: cannot find function `test_fn` in this scope
+ --> ui_tests/second_shared_ref.rs:23:8
+ |
+23 | if test_fn(n, &true) {
+ | ^^^^^^^ not found in this scope
diff --git a/rhai/examples/README.md b/rhai/examples/README.md
new file mode 100644
index 0000000..1a9aa39
--- /dev/null
+++ b/rhai/examples/README.md
@@ -0,0 +1,36 @@
+# Sample Applications
+
+## Standard Examples
+
+| Example | Description |
+| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [`arrays_and_structs`](arrays_and_structs.rs) | shows how to register a Rust type and using it with arrays |
+| [`callback`](callback.rs) | shows how to store a Rhai closure and call it later within Rust |
+| [`custom_types_and_methods`](custom_types_and_methods.rs) | shows how to register a Rust type and methods/getters/setters for it |
+| [`custom_types`](custom_types.rs) | shows how to register a Rust type and methods/getters/setters using the `CustomType` trait. |
+| [`definitions`](./definitions) | shows how to generate definition files for use with the [Rhai Language Server](https://github.com/rhaiscript/lsp) (requires the `metadata` feature) |
+| [`hello`](hello.rs) | simple example that evaluates an expression and prints the result |
+| [`reuse_scope`](reuse_scope.rs) | evaluates two pieces of code in separate runs, but using a common `Scope` |
+| [`serde`](serde.rs) | example to serialize and deserialize Rust types with [`serde`](https://crates.io/crates/serde) (requires the `serde` feature) |
+| [`simple_fn`](simple_fn.rs) | shows how to register a simple Rust function |
+| [`strings`](strings.rs) | shows different ways to register Rust functions taking string arguments |
+| [`threading`](threading.rs) | shows how to communicate with an `Engine` running in a separate thread via an MPSC channel |
+
+## Scriptable Event Handler With State Examples
+
+Because of its popularity, included are sample implementations for the pattern
+[_Scriptable Event Handler With State_](https://rhai.rs/book/patterns/events.html) in different styles.
+
+| Example | Handler Script | Description |
+| ------------------------------------------ | ------------------------------------------------------------------ | :---------------------------------------------------------: |
+| [`event_handler_main`](event_handler_main) | [`event_handler_main/script.rhai`](event_handler_main/script.rhai) | [_Main Style_](https://rhai.rs/book/patterns/events-1.html) |
+| [`event_handler_js`](event_handler_js) | [`event_handler_js/script.rhai`](event_handler_js/script.rhai) | [_JS Style_](https://rhai.rs/book/patterns/events-2.html) |
+| [`event_handler_map`](event_handler_map) | [`event_handler_map/script.rhai`](event_handler_map/script.rhai) | [_Map Style_](https://rhai.rs/book/patterns/events-3.html) |
+
+## Running Examples
+
+Examples can be run with the following command:
+
+```sh
+cargo run --example {example_name}
+```
diff --git a/rhai/examples/arrays_and_structs.rs b/rhai/examples/arrays_and_structs.rs
new file mode 100644
index 0000000..b5a7889
--- /dev/null
+++ b/rhai/examples/arrays_and_structs.rs
@@ -0,0 +1,67 @@
+//! An example showing how to register a Rust type and use it with arrays.
+
+#[cfg(any(feature = "no_index", feature = "no_object"))]
+fn main() {
+ panic!("This example does not run under 'no_index' or 'no_object'.")
+}
+
+use rhai::{Engine, EvalAltResult};
+
+#[cfg(not(feature = "no_index"))]
+#[cfg(not(feature = "no_object"))]
+fn main() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, Clone)]
+ struct TestStruct {
+ x: i64,
+ }
+
+ impl TestStruct {
+ pub fn new() -> Self {
+ Self { x: 1 }
+ }
+ pub fn update(&mut self) {
+ self.x += 1000;
+ }
+ }
+
+ let mut engine = Engine::new();
+
+ engine
+ .register_type_with_name::<TestStruct>("TestStruct")
+ .register_fn("new_ts", TestStruct::new)
+ .register_fn("update", TestStruct::update);
+
+ #[cfg(feature = "metadata")]
+ {
+ println!("Functions registered:");
+
+ engine
+ .gen_fn_signatures(false)
+ .into_iter()
+ .for_each(|func| println!("{func}"));
+
+ println!();
+ }
+
+ let result = engine.eval::<TestStruct>(
+ "
+ let x = new_ts();
+ x.update();
+ x
+ ",
+ )?;
+
+ println!("{result:?}");
+
+ let result = engine.eval::<TestStruct>(
+ "
+ let x = [ new_ts() ];
+ x[0].update();
+ x[0]
+ ",
+ )?;
+
+ println!("{result:?}");
+
+ Ok(())
+}
diff --git a/rhai/examples/callback.rs b/rhai/examples/callback.rs
new file mode 100644
index 0000000..9998469
--- /dev/null
+++ b/rhai/examples/callback.rs
@@ -0,0 +1,42 @@
+//! This example stores a Rhai closure for later use as a callback.
+
+use rhai::{Engine, EvalAltResult, FnPtr};
+
+// To call a Rhai closure at a later time, you'd need three things:
+// 1) an `Engine` (with all needed functions registered),
+// 2) a compiled `AST`,
+// 3) the closure (of type `FnPtr`).
+fn main() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ // This script creates a closure which captures a variable and returns it.
+ let ast = engine.compile(
+ "
+ let x = 18;
+
+ // The following closure captures 'x'
+ return |a, b| {
+ x += 1; // x is incremented each time
+ (x + a) * b
+ };
+ ",
+ )?;
+
+ let closure = engine.eval_ast::<FnPtr>(&ast)?;
+
+ // Create a closure by encapsulating the `Engine`, `AST` and `FnPtr`.
+ // In a real application, you'd be handling errors.
+ let func = move |x: i64, y: i64| -> i64 { closure.call(&engine, &ast, (x, y)).unwrap() };
+
+ // Now we can call `func` anywhere just like a normal function!
+ let r1 = func(1, 2);
+
+ // Notice that each call to `func` returns a different value
+ // because the captured `x` is always changing!
+ let r2 = func(1, 2);
+ let r3 = func(1, 2);
+
+ println!("The Answers: {r1}, {r2}, {r3}"); // prints 40, 42, 44
+
+ Ok(())
+}
diff --git a/rhai/examples/custom_types.rs b/rhai/examples/custom_types.rs
new file mode 100644
index 0000000..c4d7804
--- /dev/null
+++ b/rhai/examples/custom_types.rs
@@ -0,0 +1,95 @@
+//! An example showing how to register a Rust type and methods/getters/setters using the `CustomType` trait.
+
+#[cfg(feature = "no_object")]
+fn main() {
+ panic!("This example does not run under 'no_object'.");
+}
+
+use rhai::{CustomType, Engine, EvalAltResult, TypeBuilder};
+
+#[cfg(not(feature = "no_object"))]
+fn main() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, Clone)]
+ struct TestStruct {
+ x: i64,
+ }
+
+ impl TestStruct {
+ pub fn new() -> Self {
+ Self { x: 1 }
+ }
+ pub fn update(&mut self) {
+ self.x += 1000;
+ }
+ pub fn calculate(&mut self, data: i64) -> i64 {
+ self.x * data
+ }
+ pub fn get_x(&mut self) -> i64 {
+ self.x
+ }
+ pub fn set_x(&mut self, value: i64) {
+ self.x = value;
+ }
+ }
+
+ impl IntoIterator for TestStruct {
+ type Item = i64;
+ type IntoIter = std::vec::IntoIter<Self::Item>;
+
+ #[inline]
+ #[must_use]
+ fn into_iter(self) -> Self::IntoIter {
+ vec![self.x - 1, self.x, self.x + 1].into_iter()
+ }
+ }
+
+ impl CustomType for TestStruct {
+ fn build(mut builder: TypeBuilder<Self>) {
+ #[allow(deprecated)] // The TypeBuilder api is volatile.
+ builder
+ .with_name("TestStruct")
+ .with_fn("new_ts", Self::new)
+ .with_fn("update", Self::update)
+ .with_fn("calc", Self::calculate)
+ .is_iterable()
+ .with_get_set("x", Self::get_x, Self::set_x);
+ }
+ }
+
+ let mut engine = Engine::new();
+
+ engine.build_type::<TestStruct>();
+
+ #[cfg(feature = "metadata")]
+ {
+ println!("Functions registered:");
+
+ engine
+ .gen_fn_signatures(false)
+ .into_iter()
+ .for_each(|func| println!("{func}"));
+
+ println!();
+ }
+
+ let result = engine.eval::<i64>(
+ "
+ let x = new_ts();
+
+ x.x = 42;
+
+ for n in x {
+ x.x += n;
+ print(`n = ${n}, total = ${x.x}`);
+ }
+
+ x.update();
+
+ x.calc(x.x)
+ ",
+ )?;
+
+ println!("result: {result}"); // prints 1085764
+
+ Ok(())
+}
diff --git a/rhai/examples/custom_types_and_methods.rs b/rhai/examples/custom_types_and_methods.rs
new file mode 100644
index 0000000..fecbccf
--- /dev/null
+++ b/rhai/examples/custom_types_and_methods.rs
@@ -0,0 +1,68 @@
+//! An example showing how to register a Rust type and methods/getters/setters for it.
+
+#[cfg(feature = "no_object")]
+fn main() {
+ panic!("This example does not run under 'no_object'.");
+}
+
+use rhai::{Engine, EvalAltResult};
+
+#[cfg(not(feature = "no_object"))]
+fn main() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, Clone)]
+ struct TestStruct {
+ x: i64,
+ }
+
+ impl TestStruct {
+ pub fn new() -> Self {
+ Self { x: 1 }
+ }
+ pub fn update(&mut self) {
+ self.x += 1000;
+ }
+ pub fn calculate(&mut self, data: i64) -> i64 {
+ self.x * data
+ }
+ pub fn get_x(&mut self) -> i64 {
+ self.x
+ }
+ pub fn set_x(&mut self, value: i64) {
+ self.x = value;
+ }
+ }
+
+ let mut engine = Engine::new();
+
+ engine
+ .register_type_with_name::<TestStruct>("TestStruct")
+ .register_fn("new_ts", TestStruct::new)
+ .register_fn("update", TestStruct::update)
+ .register_fn("calc", TestStruct::calculate)
+ .register_get_set("x", TestStruct::get_x, TestStruct::set_x);
+
+ #[cfg(feature = "metadata")]
+ {
+ println!("Functions registered:");
+
+ engine
+ .gen_fn_signatures(false)
+ .into_iter()
+ .for_each(|func| println!("{func}"));
+
+ println!();
+ }
+
+ let result = engine.eval::<i64>(
+ "
+ let x = new_ts();
+ x.x = 42;
+ x.update();
+ x.calc(x.x)
+ ",
+ )?;
+
+ println!("result: {result}"); // prints 1085764
+
+ Ok(())
+}
diff --git a/rhai/examples/definitions/.rhai/all_in_one.d.rhai b/rhai/examples/definitions/.rhai/all_in_one.d.rhai
new file mode 100644
index 0000000..4ec27f7
--- /dev/null
+++ b/rhai/examples/definitions/.rhai/all_in_one.d.rhai
@@ -0,0 +1,6378 @@
+module static;
+
+op ==(int, int) -> bool;
+op !=(int, int) -> bool;
+op >(int, int) -> bool;
+op >=(int, int) -> bool;
+op <(int, int) -> bool;
+op <=(int, int) -> bool;
+op &(int, int) -> int;
+op |(int, int) -> int;
+op ^(int, int) -> int;
+op ..(int, int) -> Range<int>;
+op ..=(int, int) -> RangeInclusive<int>;
+
+op ==(bool, bool) -> bool;
+op !=(bool, bool) -> bool;
+op >(bool, bool) -> bool;
+op >=(bool, bool) -> bool;
+op <(bool, bool) -> bool;
+op <=(bool, bool) -> bool;
+op &(bool, bool) -> bool;
+op |(bool, bool) -> bool;
+op ^(bool, bool) -> bool;
+
+op ==((), ()) -> bool;
+op !=((), ()) -> bool;
+op >((), ()) -> bool;
+op >=((), ()) -> bool;
+op <((), ()) -> bool;
+op <=((), ()) -> bool;
+
+op +(int, int) -> int;
+op -(int, int) -> int;
+op *(int, int) -> int;
+op /(int, int) -> int;
+op %(int, int) -> int;
+op **(int, int) -> int;
+op >>(int, int) -> int;
+op <<(int, int) -> int;
+
+op +(float, float) -> float;
+op -(float, float) -> float;
+op *(float, float) -> float;
+op /(float, float) -> float;
+op %(float, float) -> float;
+op **(float, float) -> float;
+op ==(float, float) -> bool;
+op !=(float, float) -> bool;
+op >(float, float) -> bool;
+op >=(float, float) -> bool;
+op <(float, float) -> bool;
+op <=(float, float) -> bool;
+
+op +(float, int) -> float;
+op -(float, int) -> float;
+op *(float, int) -> float;
+op /(float, int) -> float;
+op %(float, int) -> float;
+op **(float, int) -> float;
+op ==(float, int) -> bool;
+op !=(float, int) -> bool;
+op >(float, int) -> bool;
+op >=(float, int) -> bool;
+op <(float, int) -> bool;
+op <=(float, int) -> bool;
+
+op +(int, float) -> float;
+op -(int, float) -> float;
+op *(int, float) -> float;
+op /(int, float) -> float;
+op %(int, float) -> float;
+op **(int, float) -> float;
+op ==(int, float) -> bool;
+op !=(int, float) -> bool;
+op >(int, float) -> bool;
+op >=(int, float) -> bool;
+op <(int, float) -> bool;
+op <=(int, float) -> bool;
+
+op +(Decimal, Decimal) -> Decimal;
+op -(Decimal, Decimal) -> Decimal;
+op *(Decimal, Decimal) -> Decimal;
+op /(Decimal, Decimal) -> Decimal;
+op %(Decimal, Decimal) -> Decimal;
+op **(Decimal, Decimal) -> Decimal;
+op ==(Decimal, Decimal) -> bool;
+op !=(Decimal, Decimal) -> bool;
+op >(Decimal, Decimal) -> bool;
+op >=(Decimal, Decimal) -> bool;
+op <(Decimal, Decimal) -> bool;
+op <=(Decimal, Decimal) -> bool;
+
+op +(Decimal, int) -> Decimal;
+op -(Decimal, int) -> Decimal;
+op *(Decimal, int) -> Decimal;
+op /(Decimal, int) -> Decimal;
+op %(Decimal, int) -> Decimal;
+op **(Decimal, int) -> Decimal;
+op ==(Decimal, int) -> bool;
+op !=(Decimal, int) -> bool;
+op >(Decimal, int) -> bool;
+op >=(Decimal, int) -> bool;
+op <(Decimal, int) -> bool;
+op <=(Decimal, int) -> bool;
+
+op +(int, Decimal) -> Decimal;
+op -(int, Decimal) -> Decimal;
+op *(int, Decimal) -> Decimal;
+op /(int, Decimal) -> Decimal;
+op %(int, Decimal) -> Decimal;
+op **(int, Decimal) -> Decimal;
+op ==(int, Decimal) -> bool;
+op !=(int, Decimal) -> bool;
+op >(int, Decimal) -> bool;
+op >=(int, Decimal) -> bool;
+op <(int, Decimal) -> bool;
+op <=(int, Decimal) -> bool;
+
+op +(String, String) -> String;
+op -(String, String) -> String;
+op ==(String, String) -> bool;
+op !=(String, String) -> bool;
+op >(String, String) -> bool;
+op >=(String, String) -> bool;
+op <(String, String) -> bool;
+op <=(String, String) -> bool;
+
+op +(char, char) -> String;
+op ==(char, char) -> bool;
+op !=(char, char) -> bool;
+op >(char, char) -> bool;
+op >=(char, char) -> bool;
+op <(char, char) -> bool;
+op <=(char, char) -> bool;
+
+op +(char, String) -> String;
+op ==(char, String) -> bool;
+op !=(char, String) -> bool;
+op >(char, String) -> bool;
+op >=(char, String) -> bool;
+op <(char, String) -> bool;
+op <=(char, String) -> bool;
+
+op +(String, char) -> String;
+op -(String, char) -> String;
+op ==(String, char) -> bool;
+op !=(String, char) -> bool;
+op >(String, char) -> bool;
+op >=(String, char) -> bool;
+op <(String, char) -> bool;
+op <=(String, char) -> bool;
+
+op +((), String) -> String;
+op ==((), String) -> bool;
+op !=((), String) -> bool;
+op >((), String) -> bool;
+op >=((), String) -> bool;
+op <((), String) -> bool;
+op <=((), String) -> bool;
+
+op +(String, ()) -> String;
+op ==(String, ()) -> bool;
+op !=(String, ()) -> bool;
+op >(String, ()) -> bool;
+op >=(String, ()) -> bool;
+op <(String, ()) -> bool;
+op <=(String, ()) -> bool;
+
+op +(Blob, Blob) -> Blob;
+op +(Blob, char) -> Blob;
+op ==(Blob, Blob) -> bool;
+op !=(Blob, Blob) -> bool;
+
+
+op ==(Range<int>, RangeInclusive<int>) -> bool;
+op !=(Range<int>, RangeInclusive<int>) -> bool;
+
+op ==(RangeInclusive<int>, Range<int>) -> bool;
+op !=(RangeInclusive<int>, Range<int>) -> bool;
+
+op ==(Range<int>, Range<int>) -> bool;
+op !=(Range<int>, Range<int>) -> bool;
+
+op ==(RangeInclusive<int>, RangeInclusive<int>) -> bool;
+op !=(RangeInclusive<int>, RangeInclusive<int>) -> bool;
+
+op ==(?, ?) -> bool;
+op !=(?, ?) -> bool;
+op >(?, ?) -> bool;
+op >=(?, ?) -> bool;
+op <(?, ?) -> bool;
+op <=(?, ?) -> bool;
+
+
+op &=(bool, bool);
+op |=(bool, bool);
+
+op +=(int, int);
+op -=(int, int);
+op *=(int, int);
+op /=(int, int);
+op %=(int, int);
+op **=(int, int);
+op >>=(int, int);
+op <<=(int, int);
+op &=(int, int);
+op |=(int, int);
+op ^=(int, int);
+
+op +=(float, float);
+op -=(float, float);
+op *=(float, float);
+op /=(float, float);
+op %=(float, float);
+op **=(float, float);
+
+op +=(float, int);
+op -=(float, int);
+op *=(float, int);
+op /=(float, int);
+op %=(float, int);
+op **=(float, int);
+
+op +=(Decimal, Decimal);
+op -=(Decimal, Decimal);
+op *=(Decimal, Decimal);
+op /=(Decimal, Decimal);
+op %=(Decimal, Decimal);
+op **=(Decimal, Decimal);
+
+op +=(Decimal, int);
+op -=(Decimal, int);
+op *=(Decimal, int);
+op /=(Decimal, int);
+op %=(Decimal, int);
+op **=(Decimal, int);
+
+op +=(String, String);
+op -=(String, String);
+op +=(String, char);
+op -=(String, char);
+op +=(char, String);
+op +=(char, char);
+
+op +=(Array, Array);
+op +=(Array, ?);
+
+op +=(Blob, Blob);
+op +=(Blob, int);
+op +=(Blob, char);
+op +=(Blob, String);
+
+op in(?, Array) -> bool;
+op in(String, String) -> bool;
+op in(char, String) -> bool;
+op in(int, Range<int>) -> bool;
+op in(int, RangeInclusive<int>) -> bool;
+op in(String, Map) -> bool;
+op in(int, Blob) -> bool;
+
+/// Display any data to the standard output.
+///
+/// # Example
+///
+/// ```rhai
+/// let answer = 42;
+///
+/// print(`The Answer is ${answer}`);
+/// ```
+fn print(data: ?);
+
+/// Display any data to the standard output in debug format.
+///
+/// # Example
+///
+/// ```rhai
+/// let answer = 42;
+///
+/// debug(answer);
+/// ```
+fn debug(data: ?);
+
+/// Get the type of a value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// print(x.type_of()); // prints "string"
+/// ```
+fn type_of(data: ?) -> String;
+
+/// Create a function pointer to a named function.
+///
+/// If the specified name is not a valid function name, an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = Fn("foo"); // function pointer to 'foo'
+///
+/// f.call(42); // call: foo(42)
+/// ```
+fn Fn(fn_name: String) -> FnPtr;
+
+/// Call a function pointed to by a function pointer,
+/// passing following arguments to the function call.
+///
+/// If an appropriate function is not found, an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = Fn("foo"); // function pointer to 'foo'
+///
+/// f.call(1, 2, 3); // call: foo(1, 2, 3)
+/// ```
+fn call(fn_ptr: FnPtr, ...args: ?) -> ?;
+
+/// Call a function pointed to by a function pointer, binding the `this` pointer
+/// to the object of the method call, and passing on following arguments to the function call.
+///
+/// If an appropriate function is not found, an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// fn add(x) {
+/// this + x
+/// }
+///
+/// let f = Fn("add"); // function pointer to 'add'
+///
+/// let x = 41;
+///
+/// let r = x.call(f, 1); // call: add(1) with 'this' = 'x'
+///
+/// print(r); // prints 42
+/// ```
+fn call(obj: ?, fn_ptr: FnPtr, ...args: ?) -> ?;
+
+/// Curry a number of arguments into a function pointer and return it as a new function pointer.
+///
+/// # Example
+///
+/// ```rhai
+/// fn foo(x, y, z) {
+/// x + y + z
+/// }
+///
+/// let f = Fn("foo");
+///
+/// let g = f.curry(1, 2); // curried arguments: 1, 2
+///
+/// g.call(3); // call: foo(1, 2, 3)
+/// ```
+fn curry(fn_ptr: FnPtr, ...args: ?) -> FnPtr;
+
+/// Return `true` if a script-defined function exists with a specified name and
+/// number of parameters.
+///
+/// # Example
+///
+/// ```rhai
+/// fn foo(x) { }
+///
+/// print(is_def_fn("foo", 1)); // prints true
+/// print(is_def_fn("foo", 2)); // prints false
+/// print(is_def_fn("foo", 0)); // prints false
+/// print(is_def_fn("bar", 1)); // prints false
+/// ```
+fn is_def_fn(fn_name: String, num_params: int) -> bool;
+
+/// Return `true` if a variable matching a specified name is defined.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 42;
+///
+/// print(is_def_var("x")); // prints true
+/// print(is_def_var("foo")); // prints false
+///
+/// {
+/// let y = 1;
+/// print(is_def_var("y")); // prints true
+/// }
+///
+/// print(is_def_var("y")); // prints false
+/// ```
+fn is_def_var(var_name: String) -> bool;
+
+/// Return `true` if the variable is shared.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 42;
+///
+/// print(is_shared(x)); // prints false
+///
+/// let f = || x; // capture 'x', making it shared
+///
+/// print(is_shared(x)); // prints true
+/// ```
+fn is_shared(variable: ?) -> bool;
+
+/// Evaluate a text script within the current scope.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 42;
+///
+/// eval("let y = x; x = 123;");
+///
+/// print(x); // prints 123
+/// print(y); // prints 42
+/// ```
+fn eval(script: String) -> ?;
+
+/// Return `true` if the string contains another string.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello world!";
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if "world" in x {
+/// print("found!");
+/// }
+/// ```
+fn contains(string: String, find: String) -> bool;
+
+/// Return `true` if the string contains a character.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello world!";
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 'w' in x {
+/// print("found!");
+/// }
+/// ```
+fn contains(string: String, ch: char) -> bool;
+
+/// Return `true` if a value falls within the exclusive range.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let r = 1..100;
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 42 in r {
+/// print("found!");
+/// }
+/// ```
+fn contains(range: Range<int>, value: int) -> bool;
+
+/// Return `true` if a value falls within the inclusive range.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let r = 1..=100;
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 42 in r {
+/// print("found!");
+/// }
+/// ```
+fn contains(range: RangeInclusive<int>, value: int) -> bool;
+
+/// Return `true` if a key exists within the object map.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if "c" in m {
+/// print("found!");
+/// }
+/// ```
+fn contains(map: Map, string: String) -> bool;
+
+/// Return `true` if a value is found within the BLOB.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 3 in b {
+/// print("found!");
+/// }
+/// ```
+fn contains(blob: Blob, value: int) -> bool;
+
+op minus(int, int) -> int;
+
+op !(bool) -> bool;
+
+/// Return `true` if two arrays are not-equal (i.e. any element not equal or not in the same order).
+///
+/// The operator `==` is used to compare elements and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [1, 2, 3, 4, 5];
+/// let z = [1, 2, 3, 4];
+///
+/// print(x != y); // prints false
+///
+/// print(x != z); // prints true
+/// ```
+op !=(Array, Array) -> bool;
+
+/// Return `true` if two object maps are not equal (i.e. at least one property value is not equal).
+///
+/// The operator `==` is used to compare property values and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let m1 = #{a:1, b:2, c:3};
+/// let m2 = #{a:1, b:2, c:3};
+/// let m3 = #{a:1, c:3};
+///
+/// print(m1 != m2); // prints false
+///
+/// print(m1 != m3); // prints true
+/// ```
+op !=(Map, Map) -> bool;
+
+/// Return `true` if two timestamps are not equal.
+op !=(Instant, Instant) -> bool;
+
+op !=(int, f32) -> bool;
+
+op !=(int, float) -> bool;
+
+op !=(f32, int) -> bool;
+
+op !=(f32, f32) -> bool;
+
+op !=(float, int) -> bool;
+
+op !=(i128, i128) -> bool;
+
+op !=(i16, i16) -> bool;
+
+op !=(i32, i32) -> bool;
+
+op !=(i8, i8) -> bool;
+
+op !=(u128, u128) -> bool;
+
+op !=(u16, u16) -> bool;
+
+op !=(u32, u32) -> bool;
+
+op !=(u64, u64) -> bool;
+
+op !=(u8, u8) -> bool;
+
+op %(int, f32) -> f32;
+
+op %(f32, int) -> f32;
+
+op %(f32, f32) -> f32;
+
+op %(i128, i128) -> i128;
+
+op %(i16, i16) -> i16;
+
+op %(i32, i32) -> i32;
+
+op %(i8, i8) -> i8;
+
+op %(u128, u128) -> u128;
+
+op %(u16, u16) -> u16;
+
+op %(u32, u32) -> u32;
+
+op %(u64, u64) -> u64;
+
+op %(u8, u8) -> u8;
+
+op &(i128, i128) -> i128;
+
+op &(i16, i16) -> i16;
+
+op &(i32, i32) -> i32;
+
+op &(i8, i8) -> i8;
+
+op &(u128, u128) -> u128;
+
+op &(u16, u16) -> u16;
+
+op &(u32, u32) -> u32;
+
+op &(u64, u64) -> u64;
+
+op &(u8, u8) -> u8;
+
+op *(int, f32) -> f32;
+
+op *(f32, int) -> f32;
+
+op *(f32, f32) -> f32;
+
+op *(i128, i128) -> i128;
+
+op *(i16, i16) -> i16;
+
+op *(i32, i32) -> i32;
+
+op *(i8, i8) -> i8;
+
+op *(u128, u128) -> u128;
+
+op *(u16, u16) -> u16;
+
+op *(u32, u32) -> u32;
+
+op *(u64, u64) -> u64;
+
+op *(u8, u8) -> u8;
+
+op **(f32, int) -> f32;
+
+op **(f32, f32) -> f32;
+
+op **(i128, int) -> i128;
+
+op **(i16, int) -> i16;
+
+op **(i32, int) -> i32;
+
+op **(i8, int) -> i8;
+
+op **(u128, int) -> u128;
+
+op **(u16, int) -> u16;
+
+op **(u32, int) -> u32;
+
+op **(u64, int) -> u64;
+
+op **(u8, int) -> u8;
+
+op +(int) -> int;
+
+op +(f32) -> f32;
+
+op +(float) -> float;
+
+op +(i128) -> i128;
+
+op +(i16) -> i16;
+
+op +(i32) -> i32;
+
+op +(i8) -> i8;
+
+op +((), String) -> String;
+
+/// Combine two arrays into a new array and return it.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+/// let y = [true, 'x'];
+///
+/// print(x + y); // prints "[1, 2, 3, true, 'x']"
+///
+/// print(x); // prints "[1, 2, 3"
+/// ```
+op +(Array, Array) -> Array;
+
+op +(char, String) -> String;
+
+op +(?, String) -> String;
+
+/// Make a copy of the object map, add all property values of another object map
+/// (existing property values of the same names are replaced), then returning it.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+/// let n = #{a: 42, d:0};
+///
+/// print(m + n); // prints "#{a:42, b:2, c:3, d:0}"
+///
+/// print(m); // prints "#{a:1, b:2, c:3}"
+/// ```
+op +(Map, Map) -> Map;
+
+op +(String, String) -> String;
+
+op +(String, char) -> String;
+
+op +(String, ?) -> String;
+
+op +(String, Blob) -> String;
+
+op +(String, ()) -> String;
+
+/// Add the specified number of `seconds` to the timestamp and return it as a new timestamp.
+op +(Instant, float) -> Instant;
+
+/// Add the specified number of `seconds` to the timestamp and return it as a new timestamp.
+op +(Instant, int) -> Instant;
+
+op +(Blob, String) -> String;
+
+op +(int, f32) -> f32;
+
+op +(f32, int) -> f32;
+
+op +(f32, f32) -> f32;
+
+op +(i128, i128) -> i128;
+
+op +(i16, i16) -> i16;
+
+op +(i32, i32) -> i32;
+
+op +(i8, i8) -> i8;
+
+op +(u128, u128) -> u128;
+
+op +(u16, u16) -> u16;
+
+op +(u32, u32) -> u32;
+
+op +(u64, u64) -> u64;
+
+op +(u8, u8) -> u8;
+
+/// Add all property values of another object map into the object map.
+/// Existing property values of the same names are replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+/// let n = #{a: 42, d:0};
+///
+/// m.mixin(n);
+///
+/// print(m); // prints "#{a:42, b:2, c:3, d:0}"
+/// ```
+op +=(Map, Map) -> ();
+
+op +=(String, String) -> ();
+
+op +=(String, char) -> ();
+
+op +=(String, ()) -> ();
+
+op +=(String, ?) -> ();
+
+op +=(String, Blob) -> ();
+
+/// Add the specified number of `seconds` to the timestamp.
+op +=(Instant, float) -> ();
+
+/// Add the specified number of `seconds` to the timestamp.
+op +=(Instant, int) -> ();
+
+op -(int) -> int;
+
+op -(f32) -> f32;
+
+op -(float) -> float;
+
+op -(i128) -> i128;
+
+op -(i16) -> i16;
+
+op -(i32) -> i32;
+
+op -(i8) -> i8;
+
+/// Return the number of seconds between two timestamps.
+op -(Instant, Instant) -> RhaiResult;
+
+/// Subtract the specified number of `seconds` from the timestamp and return it as a new timestamp.
+op -(Instant, float) -> Instant;
+
+/// Subtract the specified number of `seconds` from the timestamp and return it as a new timestamp.
+op -(Instant, int) -> Instant;
+
+op -(int, f32) -> f32;
+
+op -(f32, int) -> f32;
+
+op -(f32, f32) -> f32;
+
+op -(i128, i128) -> i128;
+
+op -(i16, i16) -> i16;
+
+op -(i32, i32) -> i32;
+
+op -(i8, i8) -> i8;
+
+op -(u128, u128) -> u128;
+
+op -(u16, u16) -> u16;
+
+op -(u32, u32) -> u32;
+
+op -(u64, u64) -> u64;
+
+op -(u8, u8) -> u8;
+
+/// Subtract the specified number of `seconds` from the timestamp.
+op -=(Instant, float) -> ();
+
+/// Subtract the specified number of `seconds` from the timestamp.
+op -=(Instant, int) -> ();
+
+op /(int, f32) -> f32;
+
+op /(f32, int) -> f32;
+
+op /(f32, f32) -> f32;
+
+op /(i128, i128) -> i128;
+
+op /(i16, i16) -> i16;
+
+op /(i32, i32) -> i32;
+
+op /(i8, i8) -> i8;
+
+op /(u128, u128) -> u128;
+
+op /(u16, u16) -> u16;
+
+op /(u32, u32) -> u32;
+
+op /(u64, u64) -> u64;
+
+op /(u8, u8) -> u8;
+
+/// Return `true` if the first timestamp is earlier than the second.
+op <(Instant, Instant) -> bool;
+
+op <(int, f32) -> bool;
+
+op <(int, float) -> bool;
+
+op <(f32, int) -> bool;
+
+op <(f32, f32) -> bool;
+
+op <(float, int) -> bool;
+
+op <(i128, i128) -> bool;
+
+op <(i16, i16) -> bool;
+
+op <(i32, i32) -> bool;
+
+op <(i8, i8) -> bool;
+
+op <(u128, u128) -> bool;
+
+op <(u16, u16) -> bool;
+
+op <(u32, u32) -> bool;
+
+op <(u64, u64) -> bool;
+
+op <(u8, u8) -> bool;
+
+op <<(i128, int) -> i128;
+
+op <<(i16, int) -> i16;
+
+op <<(i32, int) -> i32;
+
+op <<(i8, int) -> i8;
+
+op <<(u128, int) -> u128;
+
+op <<(u16, int) -> u16;
+
+op <<(u32, int) -> u32;
+
+op <<(u64, int) -> u64;
+
+op <<(u8, int) -> u8;
+
+/// Return `true` if the first timestamp is earlier than or equals to the second.
+op <=(Instant, Instant) -> bool;
+
+op <=(int, f32) -> bool;
+
+op <=(int, float) -> bool;
+
+op <=(f32, int) -> bool;
+
+op <=(f32, f32) -> bool;
+
+op <=(float, int) -> bool;
+
+op <=(i128, i128) -> bool;
+
+op <=(i16, i16) -> bool;
+
+op <=(i32, i32) -> bool;
+
+op <=(i8, i8) -> bool;
+
+op <=(u128, u128) -> bool;
+
+op <=(u16, u16) -> bool;
+
+op <=(u32, u32) -> bool;
+
+op <=(u64, u64) -> bool;
+
+op <=(u8, u8) -> bool;
+
+/// Return `true` if two arrays are equal (i.e. all elements are equal and in the same order).
+///
+/// The operator `==` is used to compare elements and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [1, 2, 3, 4, 5];
+/// let z = [1, 2, 3, 4];
+///
+/// print(x == y); // prints true
+///
+/// print(x == z); // prints false
+/// ```
+op ==(Array, Array) -> bool;
+
+/// Return `true` if two object maps are equal (i.e. all property values are equal).
+///
+/// The operator `==` is used to compare property values and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let m1 = #{a:1, b:2, c:3};
+/// let m2 = #{a:1, b:2, c:3};
+/// let m3 = #{a:1, c:3};
+///
+/// print(m1 == m2); // prints true
+///
+/// print(m1 == m3); // prints false
+/// ```
+op ==(Map, Map) -> bool;
+
+/// Return `true` if two timestamps are equal.
+op ==(Instant, Instant) -> bool;
+
+op ==(int, f32) -> bool;
+
+op ==(int, float) -> bool;
+
+op ==(f32, int) -> bool;
+
+op ==(f32, f32) -> bool;
+
+op ==(float, int) -> bool;
+
+op ==(i128, i128) -> bool;
+
+op ==(i16, i16) -> bool;
+
+op ==(i32, i32) -> bool;
+
+op ==(i8, i8) -> bool;
+
+op ==(u128, u128) -> bool;
+
+op ==(u16, u16) -> bool;
+
+op ==(u32, u32) -> bool;
+
+op ==(u64, u64) -> bool;
+
+op ==(u8, u8) -> bool;
+
+/// Return `true` if the first timestamp is later than the second.
+op >(Instant, Instant) -> bool;
+
+op >(int, f32) -> bool;
+
+op >(int, float) -> bool;
+
+op >(f32, int) -> bool;
+
+op >(f32, f32) -> bool;
+
+op >(float, int) -> bool;
+
+op >(i128, i128) -> bool;
+
+op >(i16, i16) -> bool;
+
+op >(i32, i32) -> bool;
+
+op >(i8, i8) -> bool;
+
+op >(u128, u128) -> bool;
+
+op >(u16, u16) -> bool;
+
+op >(u32, u32) -> bool;
+
+op >(u64, u64) -> bool;
+
+op >(u8, u8) -> bool;
+
+/// Return `true` if the first timestamp is later than or equals to the second.
+op >=(Instant, Instant) -> bool;
+
+op >=(int, f32) -> bool;
+
+op >=(int, float) -> bool;
+
+op >=(f32, int) -> bool;
+
+op >=(f32, f32) -> bool;
+
+op >=(float, int) -> bool;
+
+op >=(i128, i128) -> bool;
+
+op >=(i16, i16) -> bool;
+
+op >=(i32, i32) -> bool;
+
+op >=(i8, i8) -> bool;
+
+op >=(u128, u128) -> bool;
+
+op >=(u16, u16) -> bool;
+
+op >=(u32, u32) -> bool;
+
+op >=(u64, u64) -> bool;
+
+op >=(u8, u8) -> bool;
+
+op >>(i128, int) -> i128;
+
+op >>(i16, int) -> i16;
+
+op >>(i32, int) -> i32;
+
+op >>(i8, int) -> i8;
+
+op >>(u128, int) -> u128;
+
+op >>(u16, int) -> u16;
+
+op >>(u32, int) -> u32;
+
+op >>(u64, int) -> u64;
+
+op >>(u8, int) -> u8;
+
+/// Return the natural number _e_.
+fn E() -> float;
+
+/// Return the number π.
+fn PI() -> float;
+
+op ^(i128, i128) -> i128;
+
+op ^(i16, i16) -> i16;
+
+op ^(i32, i32) -> i32;
+
+op ^(i8, i8) -> i8;
+
+op ^(u128, u128) -> u128;
+
+op ^(u16, u16) -> u16;
+
+op ^(u32, u32) -> u32;
+
+op ^(u64, u64) -> u64;
+
+op ^(u8, u8) -> u8;
+
+/// Return the absolute value of the number.
+fn abs(x: int) -> int;
+
+/// Return the absolute value of the floating-point number.
+fn abs(x: f32) -> f32;
+
+/// Return the absolute value of the floating-point number.
+fn abs(x: float) -> float;
+
+/// Return the absolute value of the number.
+fn abs(x: i128) -> i128;
+
+/// Return the absolute value of the number.
+fn abs(x: i16) -> i16;
+
+/// Return the absolute value of the number.
+fn abs(x: i32) -> i32;
+
+/// Return the absolute value of the number.
+fn abs(x: i8) -> i8;
+
+/// Return the arc-cosine of the floating-point number, in radians.
+fn acos(x: float) -> float;
+
+/// Return the arc-hyperbolic-cosine of the floating-point number, in radians.
+fn acosh(x: float) -> float;
+
+/// Return `true` if all elements in the array return `true` when applied a function named by `filter`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.all(|v| v > 3)); // prints false
+///
+/// print(x.all(|v| v > 1)); // prints true
+///
+/// print(x.all(|v, i| i > v)); // prints false
+/// ```
+fn all(array: Array, filter: String) -> bool;
+
+/// Return `true` if all elements in the array return `true` when applied the `filter` function.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.all(|v| v > 3)); // prints false
+///
+/// print(x.all(|v| v > 1)); // prints true
+///
+/// print(x.all(|v, i| i > v)); // prints false
+/// ```
+fn all(array: Array, filter: FnPtr) -> bool;
+
+/// Add all the elements of another array to the end of the array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+/// let y = [true, 'x'];
+///
+/// x.append(y);
+///
+/// print(x); // prints "[1, 2, 3, true, 'x']"
+/// ```
+fn append(array: Array, new_array: Array) -> ();
+
+/// Add another BLOB to the end of the BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob(5, 0x42);
+/// let b2 = blob(3, 0x11);
+///
+/// b1.push(b2);
+///
+/// print(b1); // prints "[4242424242111111]"
+/// ```
+fn append(blob1: Blob, blob2: Blob) -> ();
+
+/// Add a character (as UTF-8 encoded byte-stream) to the end of the BLOB
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// b.append('!');
+///
+/// print(b); // prints "[424242424221]"
+/// ```
+fn append(blob: Blob, character: char) -> ();
+
+/// Add a string (as UTF-8 encoded byte-stream) to the end of the BLOB
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// b.append("hello");
+///
+/// print(b); // prints "[424242424268656c 6c6f]"
+/// ```
+fn append(blob: Blob, string: String) -> ();
+
+/// Add a new byte `value` to the end of the BLOB.
+///
+/// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b.push(0x42);
+///
+/// print(b); // prints "[42]"
+/// ```
+fn append(blob: Blob, value: int) -> ();
+
+fn append(string: String, item: ?) -> ();
+
+fn append(string: String, utf8: Blob) -> ();
+
+/// Convert the BLOB into a string.
+///
+/// The byte stream must be valid UTF-8, otherwise an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// let x = b.as_string();
+///
+/// print(x); // prints "FFFFF"
+/// ```
+fn as_string(blob: Blob) -> String;
+
+/// Return the arc-sine of the floating-point number, in radians.
+fn asin(x: float) -> float;
+
+/// Return the arc-hyperbolic-sine of the floating-point number, in radians.
+fn asinh(x: float) -> float;
+
+/// Return the arc-tangent of the floating-point number, in radians.
+fn atan(x: float) -> float;
+
+/// Return the arc-tangent of the floating-point numbers `x` and `y`, in radians.
+fn atan(x: float, y: float) -> float;
+
+/// Return the arc-hyperbolic-tangent of the floating-point number, in radians.
+fn atanh(x: float) -> float;
+
+/// Return an iterator over all the bits in the number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits() {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int) -> Iterator<bool>;
+
+/// Return an iterator over the bits in the number starting from the specified `start` position.
+///
+/// If `start` < 0, position counts from the MSB (Most Significant Bit)>.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits(10) {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int, from: int) -> Iterator<bool>;
+
+/// Return an iterator over an exclusive range of bits in the number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits(10..24) {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int, range: Range<int>) -> Iterator<bool>;
+
+/// Return an iterator over an inclusive range of bits in the number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits(10..=23) {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int, range: RangeInclusive<int>) -> Iterator<bool>;
+
+/// Return an iterator over a portion of bits in the number.
+///
+/// * If `start` < 0, position counts from the MSB (Most Significant Bit)>.
+/// * If `len` ≤ 0, an empty iterator is returned.
+/// * If `start` position + `len` ≥ length of string, all bits of the number after the `start` position are iterated.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits(10, 8) {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int, from: int, len: int) -> Iterator<bool>;
+
+/// Return a new, empty BLOB.
+fn blob() -> Blob;
+
+/// Return a new BLOB of the specified length, filled with zeros.
+///
+/// If `len` ≤ 0, an empty BLOB is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(10);
+///
+/// print(b); // prints "[0000000000000000 0000]"
+/// ```
+fn blob(len: int) -> Blob;
+
+/// Return a new BLOB of the specified length, filled with copies of the initial `value`.
+///
+/// If `len` ≤ 0, an empty BLOB is returned.
+///
+/// Only the lower 8 bits of the initial `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(10, 0x42);
+///
+/// print(b); // prints "[4242424242424242 4242]"
+/// ```
+fn blob(len: int, value: int) -> Blob;
+
+/// Return the length of the string, in number of bytes used to store it in UTF-8 encoding.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// print(text.bytes); // prints 51
+/// ```
+fn bytes(string: String) -> int;
+
+/// Return the smallest whole number larger than or equals to the floating-point number.
+fn ceiling(x: float) -> float;
+
+/// Return an iterator over the characters in the string.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars() {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String) -> Iterator<char>;
+
+/// Return an iterator over the characters in the string starting from the `start` position.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars(2) {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String, from: int) -> Iterator<char>;
+
+/// Return an iterator over an exclusive range of characters in the string.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars(2..5) {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String, range: Range<int>) -> Iterator<char>;
+
+/// Return an iterator over an inclusive range of characters in the string.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars(2..=6) {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String, range: RangeInclusive<int>) -> Iterator<char>;
+
+/// Return an iterator over a portion of characters in the string.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, an empty iterator is returned.
+/// * If `len` ≤ 0, an empty iterator is returned.
+/// * If `start` position + `len` ≥ length of string, all characters of the string after the `start` position are iterated.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars(2, 4) {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String, start: int, len: int) -> Iterator<char>;
+
+/// Cut off the head of the array, leaving a tail of the specified length.
+///
+/// * If `len` ≤ 0, the array is cleared.
+/// * If `len` ≥ length of array, the array is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// x.chop(3);
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// x.chop(10);
+///
+/// print(x); // prints "[3, 4, 5]"
+/// ```
+fn chop(array: Array, len: int) -> ();
+
+/// Cut off the head of the BLOB, leaving a tail of the specified length.
+///
+/// * If `len` ≤ 0, the BLOB is cleared.
+/// * If `len` ≥ length of BLOB, the BLOB is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// b.chop(3);
+///
+/// print(b); // prints "[030405]"
+///
+/// b.chop(10);
+///
+/// print(b); // prints "[030405]"
+/// ```
+fn chop(blob: Blob, len: int) -> ();
+
+/// Clear the array.
+fn clear(array: Array) -> ();
+
+/// Clear the BLOB.
+fn clear(blob: Blob) -> ();
+
+/// Clear the object map.
+fn clear(map: Map) -> ();
+
+/// Clear the string, making it empty.
+fn clear(string: String) -> ();
+
+/// Return `true` if the array contains an element that equals `value`.
+///
+/// The operator `==` is used to compare elements with `value` and must be defined,
+/// otherwise `false` is assumed.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 4 in x {
+/// print("found!");
+/// }
+/// ```
+fn contains(array: Array, value: ?) -> bool;
+
+/// Return `true` if the BLOB contains a specified byte value.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.contains('h')); // prints true
+///
+/// print(text.contains('x')); // prints false
+/// ```
+fn contains(blob: Blob, value: int) -> bool;
+
+/// Returns `true` if the object map contains a specified property.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a: 1, b: 2, c: 3};
+///
+/// print(m.contains("b")); // prints true
+///
+/// print(m.contains("x")); // prints false
+/// ```
+fn contains(map: Map, property: String) -> bool;
+
+/// Return `true` if the range contains a specified value.
+fn contains(range: ExclusiveRange, value: int) -> bool;
+
+/// Return `true` if the range contains a specified value.
+fn contains(range: InclusiveRange, value: int) -> bool;
+
+/// Return `true` if the string contains a specified character.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.contains('h')); // prints true
+///
+/// print(text.contains('x')); // prints false
+/// ```
+fn contains(string: String, character: char) -> bool;
+
+/// Return `true` if the string contains a specified string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.contains("hello")); // prints true
+///
+/// print(text.contains("hey")); // prints false
+/// ```
+fn contains(string: String, match_string: String) -> bool;
+
+/// Return the cosine of the floating-point number in radians.
+fn cos(x: float) -> float;
+
+/// Return the hyperbolic cosine of the floating-point number in radians.
+fn cosh(x: float) -> float;
+
+/// Remove all characters from the string except those within an exclusive `range`.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.crop(2..8);
+///
+/// print(text); // prints "llo, w"
+/// ```
+fn crop(string: String, range: Range<int>) -> ();
+
+/// Remove all characters from the string except those within an inclusive `range`.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.crop(2..=8);
+///
+/// print(text); // prints "llo, wo"
+/// ```
+fn crop(string: String, range: RangeInclusive<int>) -> ();
+
+/// Remove all characters from the string except until the `start` position.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, the string is not modified.
+/// * If `start` ≥ length of string, the entire string is cleared.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.crop(5);
+///
+/// print(text); // prints ", world!"
+///
+/// text.crop(-3);
+///
+/// print(text); // prints "ld!"
+/// ```
+fn crop(string: String, start: int) -> ();
+
+/// Remove all characters from the string except those within a range.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, the entire string is cleared.
+/// * If `len` ≤ 0, the entire string is cleared.
+/// * If `start` position + `len` ≥ length of string, only the portion of the string after the `start` position is retained.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.crop(2, 8);
+///
+/// print(text); // prints "llo, wor"
+///
+/// text.crop(-5, 3);
+///
+/// print(text); // prints ", w"
+/// ```
+fn crop(string: String, start: int, len: int) -> ();
+
+/// Return the empty string.
+op debug() -> String;
+
+/// Convert the array into a string.
+op debug(Array) -> String;
+
+/// Convert the string into debug format.
+op debug(char) -> String;
+
+/// Convert the function pointer into a string in debug format.
+op debug(FnPtr) -> String;
+
+/// Convert the value of the `item` into a string in debug format.
+op debug(?) -> String;
+
+/// Convert the object map into a string.
+op debug(Map) -> String;
+
+/// Convert the value of `number` into a string.
+op debug(f32) -> String;
+
+/// Convert the value of `number` into a string.
+op debug(float) -> String;
+
+/// Convert the string into debug format.
+op debug(String) -> String;
+
+/// Convert the unit into a string in debug format.
+op debug(()) -> String;
+
+/// Convert the boolean value into a string in debug format.
+op debug(bool) -> String;
+
+/// Remove duplicated _consecutive_ elements from the array.
+///
+/// The operator `==` is used to compare elements and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 2, 2, 3, 4, 3, 3, 2, 1];
+///
+/// x.dedup();
+///
+/// print(x); // prints "[1, 2, 3, 4, 3, 2, 1]"
+/// ```
+fn dedup(array: Array) -> ();
+
+/// Remove duplicated _consecutive_ elements from the array that return `true` when applied a
+/// function named by `comparer`.
+///
+/// No element is removed if the correct `comparer` function does not exist.
+///
+/// # Function Parameters
+///
+/// * `element1`: copy of the current array element to compare
+/// * `element2`: copy of the next array element to compare
+///
+/// ## Return Value
+///
+/// `true` if `element1 == element2`, otherwise `false`.
+///
+/// # Example
+///
+/// ```rhai
+/// fn declining(a, b) { a >= b }
+///
+/// let x = [1, 2, 2, 2, 3, 1, 2, 3, 4, 3, 3, 2, 1];
+///
+/// x.dedup("declining");
+///
+/// print(x); // prints "[1, 2, 3, 4]"
+/// ```
+fn dedup(array: Array, comparer: String) -> ();
+
+/// Remove duplicated _consecutive_ elements from the array that return `true` when applied the
+/// `comparer` function.
+///
+/// No element is removed if the correct `comparer` function does not exist.
+///
+/// # Function Parameters
+///
+/// * `element1`: copy of the current array element to compare
+/// * `element2`: copy of the next array element to compare
+///
+/// ## Return Value
+///
+/// `true` if `element1 == element2`, otherwise `false`.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 2, 2, 3, 1, 2, 3, 4, 3, 3, 2, 1];
+///
+/// x.dedup(|a, b| a >= b);
+///
+/// print(x); // prints "[1, 2, 3, 4]"
+/// ```
+fn dedup(array: Array, comparer: FnPtr) -> ();
+
+/// Remove all elements in the array that returns `true` when applied a function named by `filter`
+/// and return them as a new array.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn small(x) { x < 3 }
+///
+/// fn screen(x, i) { x + i > 5 }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain("small");
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// print(y); // prints "[1, 2]"
+///
+/// let z = x.drain("screen");
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, filter: String) -> Array;
+
+/// Remove all elements in the array that returns `true` when applied the `filter` function and
+/// return them as a new array.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain(|v| v < 3);
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// print(y); // prints "[1, 2]"
+///
+/// let z = x.drain(|v, i| v + i > 5);
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, filter: FnPtr) -> Array;
+
+/// Remove all elements in the array within an exclusive `range` and return them as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain(1..3);
+///
+/// print(x); // prints "[1, 4, 5]"
+///
+/// print(y); // prints "[2, 3]"
+///
+/// let z = x.drain(2..3);
+///
+/// print(x); // prints "[1, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, range: Range<int>) -> Array;
+
+/// Remove all elements in the array within an inclusive `range` and return them as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain(1..=2);
+///
+/// print(x); // prints "[1, 4, 5]"
+///
+/// print(y); // prints "[2, 3]"
+///
+/// let z = x.drain(2..=2);
+///
+/// print(x); // prints "[1, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, range: RangeInclusive<int>) -> Array;
+
+/// Remove all bytes in the BLOB within an exclusive `range` and return them as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.drain(1..3);
+///
+/// print(b1); // prints "[010405]"
+///
+/// print(b2); // prints "[0203]"
+///
+/// let b3 = b1.drain(2..3);
+///
+/// print(b1); // prints "[0104]"
+///
+/// print(b3); // prints "[05]"
+/// ```
+fn drain(blob: Blob, range: Range<int>) -> Blob;
+
+/// Remove all bytes in the BLOB within an inclusive `range` and return them as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.drain(1..=2);
+///
+/// print(b1); // prints "[010405]"
+///
+/// print(b2); // prints "[0203]"
+///
+/// let b3 = b1.drain(2..=2);
+///
+/// print(b1); // prints "[0104]"
+///
+/// print(b3); // prints "[05]"
+/// ```
+fn drain(blob: Blob, range: RangeInclusive<int>) -> Blob;
+
+/// Remove all elements within a portion of the array and return them as a new array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, no element is removed and an empty array is returned.
+/// * If `len` ≤ 0, no element is removed and an empty array is returned.
+/// * If `start` position + `len` ≥ length of array, entire portion of the array after the `start` position is removed and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain(1, 2);
+///
+/// print(x); // prints "[1, 4, 5]"
+///
+/// print(y); // prints "[2, 3]"
+///
+/// let z = x.drain(-1, 1);
+///
+/// print(x); // prints "[1, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, start: int, len: int) -> Array;
+
+/// Remove all bytes within a portion of the BLOB and return them as a new BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, nothing is removed and an empty BLOB is returned.
+/// * If `len` ≤ 0, nothing is removed and an empty BLOB is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is removed and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.drain(1, 2);
+///
+/// print(b1); // prints "[010405]"
+///
+/// print(b2); // prints "[0203]"
+///
+/// let b3 = b1.drain(-1, 1);
+///
+/// print(b3); // prints "[0104]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(blob: Blob, start: int, len: int) -> Blob;
+
+/// Return the number of seconds between the current system time and the timestamp.
+///
+/// # Example
+///
+/// ```rhai
+/// let now = timestamp();
+///
+/// sleep(10.0); // sleep for 10 seconds
+///
+/// print(now.elapsed); // prints 10.???
+/// ```
+fn elapsed(timestamp: Instant) -> RhaiResult;
+
+/// Return the end of the exclusive range.
+fn end(range: ExclusiveRange) -> int;
+
+/// Return the end of the inclusive range.
+fn end(range: InclusiveRange) -> int;
+
+/// Return `true` if the string ends with a specified string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.ends_with("world!")); // prints true
+///
+/// print(text.ends_with("hello")); // prints false
+/// ```
+fn ends_with(string: String, match_string: String) -> bool;
+
+/// Return the exponential of the floating-point number.
+fn exp(x: float) -> float;
+
+/// Copy an exclusive range of the array and return it as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// print(x.extract(1..3)); // prints "[2, 3]"
+///
+/// print(x); // prints "[1, 2, 3, 4, 5]"
+/// ```
+fn extract(array: Array, range: Range<int>) -> Array;
+
+/// Copy an inclusive range of the array and return it as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// print(x.extract(1..=3)); // prints "[2, 3, 4]"
+///
+/// print(x); // prints "[1, 2, 3, 4, 5]"
+/// ```
+fn extract(array: Array, range: RangeInclusive<int>) -> Array;
+
+/// Copy a portion of the array beginning at the `start` position till the end and return it as
+/// a new array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, the entire array is copied and returned.
+/// * If `start` ≥ length of array, an empty array is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// print(x.extract(2)); // prints "[3, 4, 5]"
+///
+/// print(x.extract(-3)); // prints "[3, 4, 5]"
+///
+/// print(x); // prints "[1, 2, 3, 4, 5]"
+/// ```
+fn extract(array: Array, start: int) -> Array;
+
+/// Copy an exclusive `range` of the BLOB and return it as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.extract(1..3)); // prints "[0203]"
+///
+/// print(b); // prints "[0102030405]"
+/// ```
+fn extract(blob: Blob, range: Range<int>) -> Blob;
+
+/// Copy an inclusive `range` of the BLOB and return it as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.extract(1..=3)); // prints "[020304]"
+///
+/// print(b); // prints "[0102030405]"
+/// ```
+fn extract(blob: Blob, range: RangeInclusive<int>) -> Blob;
+
+/// Copy a portion of the BLOB beginning at the `start` position till the end and return it as
+/// a new BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, the entire BLOB is copied and returned.
+/// * If `start` ≥ length of BLOB, an empty BLOB is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.extract(2)); // prints "[030405]"
+///
+/// print(b.extract(-3)); // prints "[030405]"
+///
+/// print(b); // prints "[0102030405]"
+/// ```
+fn extract(blob: Blob, start: int) -> Blob;
+
+/// Copy a portion of the array and return it as a new array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, an empty array is returned.
+/// * If `len` ≤ 0, an empty array is returned.
+/// * If `start` position + `len` ≥ length of array, entire portion of the array after the `start` position is copied and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// print(x.extract(1, 3)); // prints "[2, 3, 4]"
+///
+/// print(x.extract(-3, 2)); // prints "[3, 4]"
+///
+/// print(x); // prints "[1, 2, 3, 4, 5]"
+/// ```
+fn extract(array: Array, start: int, len: int) -> Array;
+
+/// Copy a portion of the BLOB and return it as a new BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, an empty BLOB is returned.
+/// * If `len` ≤ 0, an empty BLOB is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is copied and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.extract(1, 3)); // prints "[020303]"
+///
+/// print(b.extract(-3, 2)); // prints "[0304]"
+///
+/// print(b); // prints "[0102030405]"
+/// ```
+fn extract(blob: Blob, start: int, len: int) -> Blob;
+
+/// Add all property values of another object map into the object map.
+/// Only properties that do not originally exist in the object map are added.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+/// let n = #{a: 42, d:0};
+///
+/// m.fill_with(n);
+///
+/// print(m); // prints "#{a:1, b:2, c:3, d:0}"
+/// ```
+fn fill_with(map: Map, map2: Map) -> ();
+
+/// Iterate through all the elements in the array, applying a `filter` function to each element
+/// in turn, and return a copy of all elements (in order) that return `true` as a new array.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.filter(|v| v >= 3);
+///
+/// print(y); // prints "[3, 4, 5]"
+///
+/// let y = x.filter(|v, i| v * i >= 10);
+///
+/// print(y); // prints "[12, 20]"
+/// ```
+fn filter(array: Array, filter: FnPtr) -> Array;
+
+/// Iterate through all the elements in the array, applying a function named by `filter` to each
+/// element in turn, and return a copy of all elements (in order) that return `true` as a new array.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn screen(x, i) { x * i >= 10 }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.filter("is_odd");
+///
+/// print(y); // prints "[1, 3, 5]"
+///
+/// let y = x.filter("screen");
+///
+/// print(y); // prints "[12, 20]"
+/// ```
+fn filter(array: Array, filter_func: String) -> Array;
+
+/// Return the largest whole number less than or equals to the floating-point number.
+fn floor(x: float) -> float;
+
+/// Return the fractional part of the floating-point number.
+fn fraction(x: float) -> float;
+
+/// Get a copy of the element at the `index` position in the array.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` < -length of array, `()` is returned.
+/// * If `index` ≥ length of array, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// print(x.get(0)); // prints 1
+///
+/// print(x.get(-1)); // prints 3
+///
+/// print(x.get(99)); // prints empty (for '()')
+/// ```
+fn get(array: Array, index: int) -> ?;
+
+/// Get the byte value at the `index` position in the BLOB.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last element).
+/// * If `index` < -length of BLOB, zero is returned.
+/// * If `index` ≥ length of BLOB, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.get(0)); // prints 1
+///
+/// print(b.get(-1)); // prints 5
+///
+/// print(b.get(99)); // prints 0
+/// ```
+fn get(blob: Blob, index: int) -> int;
+
+/// Get the value of the `property` in the object map and return a copy.
+///
+/// If `property` does not exist in the object map, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a: 1, b: 2, c: 3};
+///
+/// print(m.get("b")); // prints 2
+///
+/// print(m.get("x")); // prints empty (for '()')
+/// ```
+fn get(map: Map, property: String) -> ?;
+
+/// Get the character at the `index` position in the string.
+///
+/// * If `index` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `index` < -length of string, zero is returned.
+/// * If `index` ≥ length of string, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.get(0)); // prints 'h'
+///
+/// print(text.get(-1)); // prints '!'
+///
+/// print(text.get(99)); // prints empty (for '()')'
+/// ```
+fn get(string: String, index: int) -> ?;
+
+/// Return an iterator over all the bits in the number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits {
+/// print(bit);
+/// }
+/// ```
+fn get bits(value: int) -> Iterator<bool>;
+
+/// Return the length of the string, in number of bytes used to store it in UTF-8 encoding.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// print(text.bytes); // prints 51
+/// ```
+fn get bytes(string: String) -> int;
+
+/// Return the smallest whole number larger than or equals to the floating-point number.
+fn get ceiling(x: float) -> float;
+
+/// Return an iterator over all the characters in the string.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars {
+/// print(ch);
+/// }
+/// ```
+fn get chars(string: String) -> Iterator<char>;
+
+/// Return the number of seconds between the current system time and the timestamp.
+///
+/// # Example
+///
+/// ```rhai
+/// let now = timestamp();
+///
+/// sleep(10.0); // sleep for 10 seconds
+///
+/// print(now.elapsed); // prints 10.???
+/// ```
+fn get elapsed(timestamp: Instant) -> RhaiResult;
+
+/// Return the end of the exclusive range.
+fn get end(range: ExclusiveRange) -> int;
+
+/// Return the end of the inclusive range.
+fn get end(range: InclusiveRange) -> int;
+
+/// Return the largest whole number less than or equals to the floating-point number.
+fn get floor(x: float) -> float;
+
+/// Return the fractional part of the floating-point number.
+fn get fraction(x: float) -> float;
+
+/// Return the integral part of the floating-point number.
+fn get int(x: float) -> float;
+
+/// Return `true` if the function is an anonymous function.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = |x| x * 2;
+///
+/// print(f.is_anonymous); // prints true
+/// ```
+fn get is_anonymous(fn_ptr: FnPtr) -> bool;
+
+/// Return true if the array is empty.
+fn get is_empty(array: Array) -> bool;
+
+/// Return true if the BLOB is empty.
+fn get is_empty(blob: Blob) -> bool;
+
+/// Return true if the range contains no items.
+fn get is_empty(range: ExclusiveRange) -> bool;
+
+/// Return true if the range contains no items.
+fn get is_empty(range: InclusiveRange) -> bool;
+
+/// Return true if the string is empty.
+fn get is_empty(string: String) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: int) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: i128) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: i16) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: i32) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: i8) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u128) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u16) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u32) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u64) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u8) -> bool;
+
+/// Return `true` if the range is exclusive.
+fn get is_exclusive(range: ExclusiveRange) -> bool;
+
+/// Return `true` if the range is exclusive.
+fn get is_exclusive(range: InclusiveRange) -> bool;
+
+/// Return `true` if the floating-point number is finite.
+fn get is_finite(x: float) -> bool;
+
+/// Return `true` if the range is inclusive.
+fn get is_inclusive(range: ExclusiveRange) -> bool;
+
+/// Return `true` if the range is inclusive.
+fn get is_inclusive(range: InclusiveRange) -> bool;
+
+/// Return `true` if the floating-point number is infinite.
+fn get is_infinite(x: float) -> bool;
+
+/// Return `true` if the floating-point number is `NaN` (Not A Number).
+fn get is_nan(x: float) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: int) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: i128) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: i16) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: i32) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: i8) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u128) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u16) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u32) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u64) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u8) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: int) -> bool;
+
+/// Return true if the floating-point number is zero.
+fn get is_zero(x: f32) -> bool;
+
+/// Return true if the floating-point number is zero.
+fn get is_zero(x: float) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: i128) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: i16) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: i32) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: i8) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u128) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u16) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u32) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u64) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u8) -> bool;
+
+/// Number of elements in the array.
+fn get len(array: Array) -> int;
+
+/// Return the length of the BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(10, 0x42);
+///
+/// print(b); // prints "[4242424242424242 4242]"
+///
+/// print(b.len()); // prints 10
+/// ```
+fn get len(blob: Blob) -> int;
+
+/// Return the length of the string, in number of characters.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// print(text.len); // prints 17
+/// ```
+fn get len(string: String) -> int;
+
+/// Return the name of the function.
+///
+/// # Example
+///
+/// ```rhai
+/// fn double(x) { x * 2 }
+///
+/// let f = Fn("double");
+///
+/// print(f.name); // prints "double"
+/// ```
+fn get name(fn_ptr: FnPtr) -> String;
+
+/// Return the nearest whole number closest to the floating-point number.
+/// Rounds away from zero.
+fn get round(x: float) -> float;
+
+/// Return the start of the exclusive range.
+fn get start(range: ExclusiveRange) -> int;
+
+/// Return the start of the inclusive range.
+fn get start(range: InclusiveRange) -> int;
+
+/// Return the _tag_ of a `Dynamic` value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// x.tag = 42;
+///
+/// print(x.tag); // prints 42
+/// ```
+fn get tag(value: ?) -> int;
+
+/// Return `true` if the specified `bit` in the number is set.
+///
+/// If `bit` < 0, position counts from the MSB (Most Significant Bit).
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// print(x.get_bit(5)); // prints false
+///
+/// print(x.get_bit(6)); // prints true
+///
+/// print(x.get_bit(-48)); // prints true on 64-bit
+/// ```
+fn get_bit(value: int, bit: int) -> bool;
+
+/// Return an exclusive range of bits in the number as a new number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// print(x.get_bits(5..10)); // print 18
+/// ```
+fn get_bits(value: int, range: Range<int>) -> int;
+
+/// Return an inclusive range of bits in the number as a new number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// print(x.get_bits(5..=9)); // print 18
+/// ```
+fn get_bits(value: int, range: RangeInclusive<int>) -> int;
+
+/// Return a portion of bits in the number as a new number.
+///
+/// * If `start` < 0, position counts from the MSB (Most Significant Bit).
+/// * If `bits` ≤ 0, zero is returned.
+/// * If `start` position + `bits` ≥ total number of bits, the bits after the `start` position are returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// print(x.get_bits(5, 8)); // print 18
+/// ```
+fn get_bits(value: int, start: int, bits: int) -> int;
+
+fn get_fn_metadata_list() -> Array;
+
+fn get_fn_metadata_list(name: String) -> Array;
+
+fn get_fn_metadata_list(name: String, params: int) -> Array;
+
+/// Return the hypotenuse of a triangle with sides `x` and `y`.
+fn hypot(x: float, y: float) -> float;
+
+/// Iterate through all the elements in the array, applying a function named by `filter` to each
+/// element in turn, and return the index of the first element that returns `true`.
+/// If no element returns `true`, `-1` is returned.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn is_special(x) { x > 3 }
+///
+/// fn is_dumb(x) { x > 8 }
+///
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of("is_special")); // prints 3
+///
+/// print(x.index_of("is_dumb")); // prints -1
+/// ```
+fn index_of(array: Array, filter: String) -> int;
+
+/// Iterate through all the elements in the array, applying a `filter` function to each element
+/// in turn, and return the index of the first element that returns `true`.
+/// If no element returns `true`, `-1` is returned.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of(|v| v > 3)); // prints 3: 4 > 3
+///
+/// print(x.index_of(|v| v > 8)); // prints -1: nothing is > 8
+///
+/// print(x.index_of(|v, i| v * i > 20)); // prints 7: 4 * 7 > 20
+/// ```
+fn index_of(array: Array, filter: FnPtr) -> int;
+
+/// Find the first element in the array that equals a particular `value` and return its index.
+/// If no element equals `value`, `-1` is returned.
+///
+/// The operator `==` is used to compare elements with `value` and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of(4)); // prints 3 (first index)
+///
+/// print(x.index_of(9)); // prints -1
+///
+/// print(x.index_of("foo")); // prints -1: strings do not equal numbers
+/// ```
+fn index_of(array: Array, value: ?) -> int;
+
+/// Find the specified `character` in the string and return the first index where it is found.
+/// If the `character` is not found, `-1` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.index_of('l')); // prints 2 (first index)
+///
+/// print(text.index_of('x')); // prints -1
+/// ```
+fn index_of(string: String, character: char) -> int;
+
+/// Find the specified `character` in the string and return the first index where it is found.
+/// If the `character` is not found, `-1` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// print(text.index_of("ll")); // prints 2 (first index)
+///
+/// print(text.index_of("xx:)); // prints -1
+/// ```
+fn index_of(string: String, find_string: String) -> int;
+
+/// Iterate through all the elements in the array, starting from a particular `start` position,
+/// applying a function named by `filter` to each element in turn, and return the index of the
+/// first element that returns `true`. If no element returns `true`, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, `-1` is returned.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn plural(x) { x > 1 }
+///
+/// fn singular(x) { x < 2 }
+///
+/// fn screen(x, i) { x * i > 20 }
+///
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of("plural", 3)); // prints 5: 2 > 1
+///
+/// print(x.index_of("singular", 9)); // prints -1: nothing < 2 past index 9
+///
+/// print(x.index_of("plural", 15)); // prints -1: nothing found past end of array
+///
+/// print(x.index_of("plural", -5)); // prints 9: -5 = start from index 8
+///
+/// print(x.index_of("plural", -99)); // prints 1: -99 = start from beginning
+///
+/// print(x.index_of("screen", 8)); // prints 10: 3 * 10 > 20
+/// ```
+fn index_of(array: Array, filter: String, start: int) -> int;
+
+/// Iterate through all the elements in the array, starting from a particular `start` position,
+/// applying a `filter` function to each element in turn, and return the index of the first
+/// element that returns `true`. If no element returns `true`, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, `-1` is returned.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of(|v| v > 1, 3)); // prints 5: 2 > 1
+///
+/// print(x.index_of(|v| v < 2, 9)); // prints -1: nothing < 2 past index 9
+///
+/// print(x.index_of(|v| v > 1, 15)); // prints -1: nothing found past end of array
+///
+/// print(x.index_of(|v| v > 1, -5)); // prints 9: -5 = start from index 8
+///
+/// print(x.index_of(|v| v > 1, -99)); // prints 1: -99 = start from beginning
+///
+/// print(x.index_of(|v, i| v * i > 20, 8)); // prints 10: 3 * 10 > 20
+/// ```
+fn index_of(array: Array, filter: FnPtr, start: int) -> int;
+
+/// Find the first element in the array, starting from a particular `start` position, that
+/// equals a particular `value` and return its index. If no element equals `value`, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, `-1` is returned.
+///
+/// The operator `==` is used to compare elements with `value` and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of(4, 2)); // prints 3
+///
+/// print(x.index_of(4, 5)); // prints 7
+///
+/// print(x.index_of(4, 15)); // prints -1: nothing found past end of array
+///
+/// print(x.index_of(4, -5)); // prints 11: -5 = start from index 8
+///
+/// print(x.index_of(9, 1)); // prints -1: nothing equals 9
+///
+/// print(x.index_of("foo", 1)); // prints -1: strings do not equal numbers
+/// ```
+fn index_of(array: Array, value: ?, start: int) -> int;
+
+/// Find the specified `character` in the string, starting from the specified `start` position,
+/// and return the first index where it is found.
+/// If the `character` is not found, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, `-1` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.index_of('l', 5)); // prints 10 (first index after 5)
+///
+/// print(text.index_of('o', -7)); // prints 8
+///
+/// print(text.index_of('x', 0)); // prints -1
+/// ```
+fn index_of(string: String, character: char, start: int) -> int;
+
+/// Find the specified sub-string in the string, starting from the specified `start` position,
+/// and return the first index where it is found.
+/// If the sub-string is not found, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, `-1` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// print(text.index_of("ll", 5)); // prints 16 (first index after 5)
+///
+/// print(text.index_of("ll", -15)); // prints 16
+///
+/// print(text.index_of("xx", 0)); // prints -1
+/// ```
+fn index_of(string: String, find_string: String, start: int) -> int;
+
+/// Add a new element into the array at a particular `index` position.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` < -length of array, the element is added to the beginning of the array.
+/// * If `index` ≥ length of array, the element is appended to the end of the array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// x.insert(0, "hello");
+///
+/// x.insert(2, true);
+///
+/// x.insert(-2, 42);
+///
+/// print(x); // prints ["hello", 1, true, 2, 42, 3]
+/// ```
+fn insert(array: Array, index: int, item: ?) -> ();
+
+/// Add a byte `value` to the BLOB at a particular `index` position.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `index` < -length of BLOB, the byte value is added to the beginning of the BLOB.
+/// * If `index` ≥ length of BLOB, the byte value is appended to the end of the BLOB.
+///
+/// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// b.insert(2, 0x18);
+///
+/// print(b); // prints "[4242184242]"
+/// ```
+fn insert(blob: Blob, index: int, value: int) -> ();
+
+/// Return the integral part of the floating-point number.
+fn int(x: float) -> float;
+
+/// Return `true` if the function is an anonymous function.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = |x| x * 2;
+///
+/// print(f.is_anonymous); // prints true
+/// ```
+fn is_anonymous(fn_ptr: FnPtr) -> bool;
+
+/// Return true if the array is empty.
+fn is_empty(array: Array) -> bool;
+
+/// Return true if the BLOB is empty.
+fn is_empty(blob: Blob) -> bool;
+
+/// Return true if the map is empty.
+fn is_empty(map: Map) -> bool;
+
+/// Return true if the range contains no items.
+fn is_empty(range: ExclusiveRange) -> bool;
+
+/// Return true if the range contains no items.
+fn is_empty(range: InclusiveRange) -> bool;
+
+/// Return true if the string is empty.
+fn is_empty(string: String) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: int) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: i128) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: i16) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: i32) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: i8) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u128) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u16) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u32) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u64) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u8) -> bool;
+
+/// Return `true` if the range is exclusive.
+fn is_exclusive(range: ExclusiveRange) -> bool;
+
+/// Return `true` if the range is exclusive.
+fn is_exclusive(range: InclusiveRange) -> bool;
+
+/// Return `true` if the floating-point number is finite.
+fn is_finite(x: float) -> bool;
+
+/// Return `true` if the range is inclusive.
+fn is_inclusive(range: ExclusiveRange) -> bool;
+
+/// Return `true` if the range is inclusive.
+fn is_inclusive(range: InclusiveRange) -> bool;
+
+/// Return `true` if the floating-point number is infinite.
+fn is_infinite(x: float) -> bool;
+
+/// Return `true` if the floating-point number is `NaN` (Not A Number).
+fn is_nan(x: float) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: int) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: i128) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: i16) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: i32) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: i8) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u128) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u16) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u32) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u64) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u8) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: int) -> bool;
+
+/// Return true if the floating-point number is zero.
+fn is_zero(x: f32) -> bool;
+
+/// Return true if the floating-point number is zero.
+fn is_zero(x: float) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: i128) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: i16) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: i32) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: i8) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u128) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u16) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u32) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u64) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u8) -> bool;
+
+/// Return an array with all the property names in the object map.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// print(m.keys()); // prints ["a", "b", "c"]
+/// ```
+fn keys(map: Map) -> Array;
+
+/// Number of elements in the array.
+fn len(array: Array) -> int;
+
+/// Return the length of the BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(10, 0x42);
+///
+/// print(b); // prints "[4242424242424242 4242]"
+///
+/// print(b.len()); // prints 10
+/// ```
+fn len(blob: Blob) -> int;
+
+/// Return the number of properties in the object map.
+fn len(map: Map) -> int;
+
+/// Return the length of the string, in number of characters.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// print(text.len); // prints 17
+/// ```
+fn len(string: String) -> int;
+
+/// Return the natural log of the floating-point number.
+fn ln(x: float) -> float;
+
+/// Return the log of the floating-point number with base 10.
+fn log(x: float) -> float;
+
+/// Return the log of the floating-point number with `base`.
+fn log(x: float, base: float) -> float;
+
+/// Convert the character to lower-case.
+///
+/// # Example
+///
+/// ```rhai
+/// let ch = 'A';
+///
+/// ch.make_lower();
+///
+/// print(ch); // prints 'a'
+/// ```
+fn make_lower(character: char) -> ();
+
+/// Convert the string to all lower-case.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "HELLO, WORLD!"
+///
+/// text.make_lower();
+///
+/// print(text); // prints "hello, world!";
+/// ```
+fn make_lower(string: String) -> ();
+
+/// Convert the character to upper-case.
+///
+/// # Example
+///
+/// ```rhai
+/// let ch = 'a';
+///
+/// ch.make_upper();
+///
+/// print(ch); // prints 'A'
+/// ```
+fn make_upper(character: char) -> ();
+
+/// Convert the string to all upper-case.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!"
+///
+/// text.make_upper();
+///
+/// print(text); // prints "HELLO, WORLD!";
+/// ```
+fn make_upper(string: String) -> ();
+
+/// Iterate through all the elements in the array, applying a function named by `mapper` to each
+/// element in turn, and return the results as a new array.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `mapper` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn square(x) { x * x }
+///
+/// fn multiply(x, i) { x * i }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.map("square");
+///
+/// print(y); // prints "[1, 4, 9, 16, 25]"
+///
+/// let y = x.map("multiply");
+///
+/// print(y); // prints "[0, 2, 6, 12, 20]"
+/// ```
+fn map(array: Array, mapper: String) -> Array;
+
+/// Iterate through all the elements in the array, applying a `mapper` function to each element
+/// in turn, and return the results as a new array.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.map(|v| v * v);
+///
+/// print(y); // prints "[1, 4, 9, 16, 25]"
+///
+/// let y = x.map(|v, i| v * i);
+///
+/// print(y); // prints "[0, 2, 6, 12, 20]"
+/// ```
+fn map(array: Array, mapper: FnPtr) -> Array;
+
+/// Add all property values of another object map into the object map.
+/// Existing property values of the same names are replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+/// let n = #{a: 42, d:0};
+///
+/// m.mixin(n);
+///
+/// print(m); // prints "#{a:42, b:2, c:3, d:0}"
+/// ```
+fn mixin(map: Map, map2: Map) -> ();
+
+/// Return the name of the function.
+///
+/// # Example
+///
+/// ```rhai
+/// fn double(x) { x * 2 }
+///
+/// let f = Fn("double");
+///
+/// print(f.name); // prints "double"
+/// ```
+fn name(fn_ptr: FnPtr) -> String;
+
+/// Pad the array to at least the specified length with copies of a specified element.
+///
+/// If `len` ≤ length of array, no padding is done.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// x.pad(5, 42);
+///
+/// print(x); // prints "[1, 2, 3, 42, 42]"
+///
+/// x.pad(3, 123);
+///
+/// print(x); // prints "[1, 2, 3, 42, 42]"
+/// ```
+fn pad(array: Array, len: int, item: ?) -> ();
+
+/// Pad the BLOB to at least the specified length with copies of a specified byte `value`.
+///
+/// If `len` ≤ length of BLOB, no padding is done.
+///
+/// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(3, 0x42);
+///
+/// b.pad(5, 0x18)
+///
+/// print(b); // prints "[4242421818]"
+///
+/// b.pad(3, 0xab)
+///
+/// print(b); // prints "[4242421818]"
+/// ```
+fn pad(blob: Blob, len: int, value: int) -> ();
+
+/// Pad the string to at least the specified number of characters with the specified `character`.
+///
+/// If `len` ≤ length of string, no padding is done.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello";
+///
+/// text.pad(8, '!');
+///
+/// print(text); // prints "hello!!!"
+///
+/// text.pad(5, '*');
+///
+/// print(text); // prints "hello!!!"
+/// ```
+fn pad(string: String, len: int, character: char) -> ();
+
+/// Pad the string to at least the specified number of characters with the specified string.
+///
+/// If `len` ≤ length of string, no padding is done.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello";
+///
+/// text.pad(10, "(!)");
+///
+/// print(text); // prints "hello(!)(!)"
+///
+/// text.pad(8, '***');
+///
+/// print(text); // prints "hello(!)(!)"
+/// ```
+fn pad(string: String, len: int, padding: String) -> ();
+
+/// Parse the bytes within an exclusive `range` in the BLOB as a `FLOAT`
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_be_float(blob: Blob, range: Range<int>) -> float;
+
+/// Parse the bytes within an inclusive `range` in the BLOB as a `FLOAT`
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_be_float(blob: Blob, range: RangeInclusive<int>) -> float;
+
+/// Parse the bytes beginning at the `start` position in the BLOB as a `FLOAT`
+/// in big-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in range < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in range > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_be_float(blob: Blob, start: int, len: int) -> float;
+
+/// Parse the bytes within an exclusive `range` in the BLOB as an `INT`
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_be_int(1..3); // parse two bytes
+///
+/// print(x.to_hex()); // prints "02030000...00"
+/// ```
+fn parse_be_int(blob: Blob, range: Range<int>) -> int;
+
+/// Parse the bytes within an inclusive `range` in the BLOB as an `INT`
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_be_int(1..=3); // parse three bytes
+///
+/// print(x.to_hex()); // prints "0203040000...00"
+/// ```
+fn parse_be_int(blob: Blob, range: RangeInclusive<int>) -> int;
+
+/// Parse the bytes beginning at the `start` position in the BLOB as an `INT`
+/// in big-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in range < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in range > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_be_int(1, 2);
+///
+/// print(x.to_hex()); // prints "02030000...00"
+/// ```
+fn parse_be_int(blob: Blob, start: int, len: int) -> int;
+
+/// Parse a string into a floating-point number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = parse_int("123.456");
+///
+/// print(x); // prints 123.456
+/// ```
+fn parse_float(string: String) -> float;
+
+/// Parse a string into an integer number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = parse_int("123");
+///
+/// print(x); // prints 123
+/// ```
+fn parse_int(string: String) -> int;
+
+/// Parse a string into an integer number of the specified `radix`.
+///
+/// `radix` must be between 2 and 36.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = parse_int("123");
+///
+/// print(x); // prints 123
+///
+/// let y = parse_int("123abc", 16);
+///
+/// print(y); // prints 1194684 (0x123abc)
+/// ```
+fn parse_int(string: String, radix: int) -> int;
+
+/// Parse a JSON string into a value.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = parse_json(`{"a":1, "b":2, "c":3}`);
+///
+/// print(m); // prints #{"a":1, "b":2, "c":3}
+/// ```
+fn parse_json(json: String) -> ?;
+
+/// Parse the bytes within an exclusive `range` in the BLOB as a `FLOAT`
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_le_float(blob: Blob, range: Range<int>) -> float;
+
+/// Parse the bytes within an inclusive `range` in the BLOB as a `FLOAT`
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_le_float(blob: Blob, range: RangeInclusive<int>) -> float;
+
+/// Parse the bytes beginning at the `start` position in the BLOB as a `FLOAT`
+/// in little-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in range < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in range > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_le_float(blob: Blob, start: int, len: int) -> float;
+
+/// Parse the bytes within an exclusive `range` in the BLOB as an `INT`
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_le_int(1..3); // parse two bytes
+///
+/// print(x.to_hex()); // prints "0302"
+/// ```
+fn parse_le_int(blob: Blob, range: Range<int>) -> int;
+
+/// Parse the bytes within an inclusive `range` in the BLOB as an `INT`
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_le_int(1..=3); // parse three bytes
+///
+/// print(x.to_hex()); // prints "040302"
+/// ```
+fn parse_le_int(blob: Blob, range: RangeInclusive<int>) -> int;
+
+/// Parse the bytes beginning at the `start` position in the BLOB as an `INT`
+/// in little-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in range < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in range > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_le_int(1, 2);
+///
+/// print(x.to_hex()); // prints "0302"
+/// ```
+fn parse_le_int(blob: Blob, start: int, len: int) -> int;
+
+/// Remove the last element from the array and return it.
+///
+/// If the array is empty, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// print(x.pop()); // prints 3
+///
+/// print(x); // prints "[1, 2]"
+/// ```
+fn pop(array: Array) -> ?;
+
+/// Remove the last byte from the BLOB and return it.
+///
+/// If the BLOB is empty, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.pop()); // prints 5
+///
+/// print(b); // prints "[01020304]"
+/// ```
+fn pop(blob: Blob) -> int;
+
+/// Remove the last character from the string and return it.
+///
+/// If the string is empty, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.pop()); // prints '!'
+///
+/// print(text); // prints "hello, world"
+/// ```
+fn pop(string: String) -> ?;
+
+/// Remove a specified number of characters from the end of the string and return it as a
+/// new string.
+///
+/// * If `len` ≤ 0, the string is not modified and an empty string is returned.
+/// * If `len` ≥ length of string, the string is cleared and the entire string returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.pop(4)); // prints "rld!"
+///
+/// print(text); // prints "hello, wo"
+/// ```
+fn pop(string: String, len: int) -> String;
+
+/// Return the empty string.
+op print() -> String;
+
+/// Convert the array into a string.
+op print(Array) -> String;
+
+/// Return the character into a string.
+op print(char) -> String;
+
+/// Convert the value of the `item` into a string.
+op print(?) -> String;
+
+/// Convert the object map into a string.
+op print(Map) -> String;
+
+/// Convert the value of `number` into a string.
+op print(f32) -> String;
+
+/// Convert the value of `number` into a string.
+op print(float) -> String;
+
+/// Return the `string`.
+op print(String) -> String;
+
+/// Return the empty string.
+op print(()) -> String;
+
+/// Return the boolean value into a string.
+op print(bool) -> String;
+
+/// Add a new element, which is not another array, to the end of the array.
+///
+/// If `item` is `Array`, then `append` is more specific and will be called instead.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// x.push("hello");
+///
+/// print(x); // prints [1, 2, 3, "hello"]
+/// ```
+fn push(array: Array, item: ?) -> ();
+
+/// Add a new byte `value` to the end of the BLOB.
+///
+/// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b.push(0x42);
+///
+/// print(b); // prints "[42]"
+/// ```
+fn push(blob: Blob, value: int) -> ();
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i128, to: i128) -> Iterator<i128>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i16, to: i16) -> Iterator<i16>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i32, to: i32) -> Iterator<i32>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: int, to: int) -> Iterator<int>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i8, to: i8) -> Iterator<i8>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u128, to: u128) -> Iterator<u128>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u16, to: u16) -> Iterator<u16>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u32, to: u32) -> Iterator<u32>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u64, to: u64) -> Iterator<u64>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u8, to: u8) -> Iterator<u8>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<float>, step: float) -> Iterator<float>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<i128>, step: i128) -> Iterator<i128>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<i16>, step: i16) -> Iterator<i16>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<i32>, step: i32) -> Iterator<i32>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<int>, step: int) -> Iterator<int>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<i8>, step: i8) -> Iterator<i8>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u128>, step: u128) -> Iterator<u128>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u16>, step: u16) -> Iterator<u16>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u32>, step: u32) -> Iterator<u32>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u64>, step: u64) -> Iterator<u64>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u8>, step: u8) -> Iterator<u8>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: float, to: float, step: float) -> Iterator<float>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i128, to: i128, step: i128) -> Iterator<i128>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i16, to: i16, step: i16) -> Iterator<i16>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i32, to: i32, step: i32) -> Iterator<i32>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: int, to: int, step: int) -> Iterator<int>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i8, to: i8, step: i8) -> Iterator<i8>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u128, to: u128, step: u128) -> Iterator<u128>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u16, to: u16, step: u16) -> Iterator<u16>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u32, to: u32, step: u32) -> Iterator<u32>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u64, to: u64, step: u64) -> Iterator<u64>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u8, to: u8, step: u8) -> Iterator<u8>;
+
+/// Reduce an array by iterating through all elements while applying a function named by `reducer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `reducer` must exist taking these parameters:
+///
+/// * `result`: accumulated result, initially `()`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn process(r, x) {
+/// x + (r ?? 0)
+/// }
+/// fn process_extra(r, x, i) {
+/// x + i + (r ?? 0)
+/// }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce("process");
+///
+/// print(y); // prints 15
+///
+/// let y = x.reduce("process_extra");
+///
+/// print(y); // prints 25
+/// ```
+fn reduce(array: Array, reducer: String) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements while applying the `reducer` function.
+///
+/// # Function Parameters
+///
+/// * `result`: accumulated result, initially `()`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce(|r, v| v + (r ?? 0));
+///
+/// print(y); // prints 15
+///
+/// let y = x.reduce(|r, v, i| v + i + (r ?? 0));
+///
+/// print(y); // prints 25
+/// ```
+fn reduce(array: Array, reducer: FnPtr) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements while applying a function named by `reducer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `reducer` must exist taking these parameters:
+///
+/// * `result`: accumulated result, starting with the value of `initial`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn process(r, x) { x + r }
+///
+/// fn process_extra(r, x, i) { x + i + r }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce("process", 5);
+///
+/// print(y); // prints 20
+///
+/// let y = x.reduce("process_extra", 5);
+///
+/// print(y); // prints 30
+/// ```
+fn reduce(array: Array, reducer: String, initial: ?) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements while applying the `reducer` function.
+///
+/// # Function Parameters
+///
+/// * `result`: accumulated result, starting with the value of `initial`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce(|r, v| v + r, 5);
+///
+/// print(y); // prints 20
+///
+/// let y = x.reduce(|r, v, i| v + i + r, 5);
+///
+/// print(y); // prints 30
+/// ```
+fn reduce(array: Array, reducer: FnPtr, initial: ?) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements, in _reverse_ order,
+/// while applying a function named by `reducer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `reducer` must exist taking these parameters:
+///
+/// * `result`: accumulated result, initially `()`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn process(r, x) {
+/// x + (r ?? 0)
+/// }
+/// fn process_extra(r, x, i) {
+/// x + i + (r ?? 0)
+/// }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce_rev("process");
+///
+/// print(y); // prints 15
+///
+/// let y = x.reduce_rev("process_extra");
+///
+/// print(y); // prints 25
+/// ```
+fn reduce_rev(array: Array, reducer: String) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements, in _reverse_ order,
+/// while applying the `reducer` function.
+///
+/// # Function Parameters
+///
+/// * `result`: accumulated result, initially `()`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce_rev(|r, v| v + (r ?? 0));
+///
+/// print(y); // prints 15
+///
+/// let y = x.reduce_rev(|r, v, i| v + i + (r ?? 0));
+///
+/// print(y); // prints 25
+/// ```
+fn reduce_rev(array: Array, reducer: FnPtr) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements, in _reverse_ order,
+/// while applying a function named by `reducer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `reducer` must exist taking these parameters:
+///
+/// * `result`: accumulated result, starting with the value of `initial`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn process(r, x) { x + r }
+///
+/// fn process_extra(r, x, i) { x + i + r }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce_rev("process", 5);
+///
+/// print(y); // prints 20
+///
+/// let y = x.reduce_rev("process_extra", 5);
+///
+/// print(y); // prints 30
+/// ```
+fn reduce_rev(array: Array, reducer: String, initial: ?) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements, in _reverse_ order,
+/// while applying the `reducer` function.
+///
+/// # Function Parameters
+///
+/// * `result`: accumulated result, starting with the value of `initial`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce_rev(|r, v| v + r, 5);
+///
+/// print(y); // prints 20
+///
+/// let y = x.reduce_rev(|r, v, i| v + i + r, 5);
+///
+/// print(y); // prints 30
+/// ```
+fn reduce_rev(array: Array, reducer: FnPtr, initial: ?) -> RhaiResult;
+
+/// Remove the element at the specified `index` from the array and return it.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` < -length of array, `()` is returned.
+/// * If `index` ≥ length of array, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// print(x.remove(1)); // prints 2
+///
+/// print(x); // prints "[1, 3]"
+///
+/// print(x.remove(-2)); // prints 1
+///
+/// print(x); // prints "[3]"
+/// ```
+fn remove(array: Array, index: int) -> ?;
+
+/// Remove the byte at the specified `index` from the BLOB and return it.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `index` < -length of BLOB, zero is returned.
+/// * If `index` ≥ length of BLOB, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(x.remove(1)); // prints 2
+///
+/// print(x); // prints "[01030405]"
+///
+/// print(x.remove(-2)); // prints 4
+///
+/// print(x); // prints "[010305]"
+/// ```
+fn remove(blob: Blob, index: int) -> int;
+
+/// Remove any property of the specified `name` from the object map, returning its value.
+///
+/// If the property does not exist, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// let x = m.remove("b");
+///
+/// print(x); // prints 2
+///
+/// print(m); // prints "#{a:1, c:3}"
+/// ```
+fn remove(map: Map, property: String) -> ?;
+
+/// Remove all occurrences of a character from the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.remove("o");
+///
+/// print(text); // prints "hell, wrld! hell, fbar!"
+/// ```
+fn remove(string: String, character: char) -> ();
+
+/// Remove all occurrences of a sub-string from the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.remove("hello");
+///
+/// print(text); // prints ", world! , foobar!"
+/// ```
+fn remove(string: String, sub_string: String) -> ();
+
+/// Replace all occurrences of the specified character in the string with another character.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.replace("l", '*');
+///
+/// print(text); // prints "he**o, wor*d! he**o, foobar!"
+/// ```
+fn replace(string: String, find_character: char, substitute_character: char) -> ();
+
+/// Replace all occurrences of the specified character in the string with another string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.replace('l', "(^)");
+///
+/// print(text); // prints "he(^)(^)o, wor(^)d! he(^)(^)o, foobar!"
+/// ```
+fn replace(string: String, find_character: char, substitute_string: String) -> ();
+
+/// Replace all occurrences of the specified sub-string in the string with the specified character.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.replace("hello", '*');
+///
+/// print(text); // prints "*, world! *, foobar!"
+/// ```
+fn replace(string: String, find_string: String, substitute_character: char) -> ();
+
+/// Replace all occurrences of the specified sub-string in the string with another string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.replace("hello", "hey");
+///
+/// print(text); // prints "hey, world! hey, foobar!"
+/// ```
+fn replace(string: String, find_string: String, substitute_string: String) -> ();
+
+/// Remove all elements in the array that do not return `true` when applied a function named by
+/// `filter` and return them as a new array.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn large(x) { x >= 3 }
+///
+/// fn screen(x, i) { x + i <= 5 }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain("large");
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// print(y); // prints "[1, 2]"
+///
+/// let z = x.retain("screen");
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn retain(array: Array, filter: String) -> Array;
+
+/// Remove all elements in the array that do not return `true` when applied the `filter`
+/// function and return them as a new array.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain(|v| v >= 3);
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// print(y); // prints "[1, 2]"
+///
+/// let z = x.retain(|v, i| v + i <= 5);
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn retain(array: Array, filter: FnPtr) -> Array;
+
+/// Remove all elements in the array not within an exclusive `range` and return them as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain(1..4);
+///
+/// print(x); // prints "[2, 3, 4]"
+///
+/// print(y); // prints "[1, 5]"
+///
+/// let z = x.retain(1..3);
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[1]"
+/// ```
+fn retain(array: Array, range: Range<int>) -> Array;
+
+/// Remove all elements in the array not within an inclusive `range` and return them as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain(1..=3);
+///
+/// print(x); // prints "[2, 3, 4]"
+///
+/// print(y); // prints "[1, 5]"
+///
+/// let z = x.retain(1..=2);
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[1]"
+/// ```
+fn retain(array: Array, range: RangeInclusive<int>) -> Array;
+
+/// Remove all bytes in the BLOB not within an exclusive `range` and return them as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.retain(1..4);
+///
+/// print(b1); // prints "[020304]"
+///
+/// print(b2); // prints "[0105]"
+///
+/// let b3 = b1.retain(1..3);
+///
+/// print(b1); // prints "[0304]"
+///
+/// print(b2); // prints "[01]"
+/// ```
+fn retain(blob: Blob, range: Range<int>) -> Blob;
+
+/// Remove all bytes in the BLOB not within an inclusive `range` and return them as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.retain(1..=3);
+///
+/// print(b1); // prints "[020304]"
+///
+/// print(b2); // prints "[0105]"
+///
+/// let b3 = b1.retain(1..=2);
+///
+/// print(b1); // prints "[0304]"
+///
+/// print(b2); // prints "[01]"
+/// ```
+fn retain(blob: Blob, range: RangeInclusive<int>) -> Blob;
+
+/// Remove all elements not within a portion of the array and return them as a new array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, all elements are removed returned.
+/// * If `len` ≤ 0, all elements are removed and returned.
+/// * If `start` position + `len` ≥ length of array, entire portion of the array before the `start` position is removed and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain(1, 2);
+///
+/// print(x); // prints "[2, 3]"
+///
+/// print(y); // prints "[1, 4, 5]"
+///
+/// let z = x.retain(-1, 1);
+///
+/// print(x); // prints "[3]"
+///
+/// print(z); // prints "[2]"
+/// ```
+fn retain(array: Array, start: int, len: int) -> Array;
+
+/// Remove all bytes not within a portion of the BLOB and return them as a new BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, all elements are removed returned.
+/// * If `len` ≤ 0, all elements are removed and returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB before the `start` position is removed and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.retain(1, 2);
+///
+/// print(b1); // prints "[0203]"
+///
+/// print(b2); // prints "[010405]"
+///
+/// let b3 = b1.retain(-1, 1);
+///
+/// print(b1); // prints "[03]"
+///
+/// print(b3); // prints "[02]"
+/// ```
+fn retain(blob: Blob, start: int, len: int) -> Blob;
+
+/// Reverse all the elements in the array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// x.reverse();
+///
+/// print(x); // prints "[5, 4, 3, 2, 1]"
+/// ```
+fn reverse(array: Array) -> ();
+
+/// Reverse the BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b); // prints "[0102030405]"
+///
+/// b.reverse();
+///
+/// print(b); // prints "[0504030201]"
+/// ```
+fn reverse(blob: Blob) -> ();
+
+/// Return the nearest whole number closest to the floating-point number.
+/// Rounds away from zero.
+fn round(x: float) -> float;
+
+/// Set the element at the `index` position in the array to a new `value`.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` < -length of array, the array is not modified.
+/// * If `index` ≥ length of array, the array is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// x.set(0, 42);
+///
+/// print(x); // prints "[42, 2, 3]"
+///
+/// x.set(-3, 0);
+///
+/// print(x); // prints "[0, 2, 3]"
+///
+/// x.set(99, 123);
+///
+/// print(x); // prints "[0, 2, 3]"
+/// ```
+fn set(array: Array, index: int, value: ?) -> ();
+
+/// Set the particular `index` position in the BLOB to a new byte `value`.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `index` < -length of BLOB, the BLOB is not modified.
+/// * If `index` ≥ length of BLOB, the BLOB is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// b.set(0, 0x42);
+///
+/// print(b); // prints "[4202030405]"
+///
+/// b.set(-3, 0);
+///
+/// print(b); // prints "[4202000405]"
+///
+/// b.set(99, 123);
+///
+/// print(b); // prints "[4202000405]"
+/// ```
+fn set(blob: Blob, index: int, value: int) -> ();
+
+/// Set the value of the `property` in the object map to a new `value`.
+///
+/// If `property` does not exist in the object map, it is added.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a: 1, b: 2, c: 3};
+///
+/// m.set("b", 42)'
+///
+/// print(m); // prints "#{a: 1, b: 42, c: 3}"
+///
+/// x.set("x", 0);
+///
+/// print(m); // prints "#{a: 1, b: 42, c: 3, x: 0}"
+/// ```
+fn set(map: Map, property: String, value: ?) -> ();
+
+/// Set the `index` position in the string to a new `character`.
+///
+/// * If `index` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `index` < -length of string, the string is not modified.
+/// * If `index` ≥ length of string, the string is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.set(3, 'x');
+///
+/// print(text); // prints "helxo, world!"
+///
+/// text.set(-3, 'x');
+///
+/// print(text); // prints "hello, worxd!"
+///
+/// text.set(99, 'x');
+///
+/// print(text); // prints "hello, worxd!"
+/// ```
+fn set(string: String, index: int, character: char) -> ();
+
+/// Set the _tag_ of a `Dynamic` value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// x.tag = 42;
+///
+/// print(x.tag); // prints 42
+/// ```
+fn set tag(value: ?, tag: int) -> ();
+
+/// Set the specified `bit` in the number if the new value is `true`.
+/// Clear the `bit` if the new value is `false`.
+///
+/// If `bit` < 0, position counts from the MSB (Most Significant Bit).
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// x.set_bit(5, true);
+///
+/// print(x); // prints 123488
+///
+/// x.set_bit(6, false);
+///
+/// print(x); // prints 123424
+///
+/// x.set_bit(-48, false);
+///
+/// print(x); // prints 57888 on 64-bit
+/// ```
+fn set_bit(value: int, bit: int, new_value: bool) -> ();
+
+/// Replace an exclusive range of bits in the number with a new value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// x.set_bits(5..10, 42);
+///
+/// print(x); // print 123200
+/// ```
+fn set_bits(value: int, range: Range<int>, new_value: int) -> ();
+
+/// Replace an inclusive range of bits in the number with a new value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// x.set_bits(5..=9, 42);
+///
+/// print(x); // print 123200
+/// ```
+fn set_bits(value: int, range: RangeInclusive<int>, new_value: int) -> ();
+
+/// Replace a portion of bits in the number with a new value.
+///
+/// * If `start` < 0, position counts from the MSB (Most Significant Bit).
+/// * If `bits` ≤ 0, the number is not modified.
+/// * If `start` position + `bits` ≥ total number of bits, the bits after the `start` position are replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// x.set_bits(5, 8, 42);
+///
+/// print(x); // prints 124224
+///
+/// x.set_bits(-16, 10, 42);
+///
+/// print(x); // prints 11821949021971776 on 64-bit
+/// ```
+fn set_bits(value: int, bit: int, bits: int, new_value: int) -> ();
+
+/// Set the _tag_ of a `Dynamic` value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// x.tag = 42;
+///
+/// print(x.tag); // prints 42
+/// ```
+fn set_tag(value: ?, tag: int) -> ();
+
+/// Remove the first element from the array and return it.
+///
+/// If the array is empty, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// print(x.shift()); // prints 1
+///
+/// print(x); // prints "[2, 3]"
+/// ```
+fn shift(array: Array) -> ?;
+
+/// Remove the first byte from the BLOB and return it.
+///
+/// If the BLOB is empty, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.shift()); // prints 1
+///
+/// print(b); // prints "[02030405]"
+/// ```
+fn shift(blob: Blob) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: int) -> int;
+
+/// Return the sign (as an integer) of the floating-point number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: f32) -> int;
+
+/// Return the sign (as an integer) of the floating-point number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: float) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: i128) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: i16) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: i32) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: i8) -> int;
+
+/// Return the sine of the floating-point number in radians.
+fn sin(x: float) -> float;
+
+/// Return the hyperbolic sine of the floating-point number in radians.
+fn sinh(x: float) -> float;
+
+/// Block the current thread for a particular number of `seconds`.
+fn sleep(seconds: int) -> ();
+
+/// Block the current thread for a particular number of `seconds`.
+fn sleep(seconds: float) -> ();
+
+/// Return `true` if any element in the array that returns `true` when applied a function named
+/// by `filter`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn large(x) { x > 3 }
+///
+/// fn huge(x) { x > 10 }
+///
+/// fn screen(x, i) { i > x }
+///
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.some("large")); // prints true
+///
+/// print(x.some("huge")); // prints false
+///
+/// print(x.some("screen")); // prints true
+/// ```
+fn some(array: Array, filter: String) -> bool;
+
+/// Return `true` if any element in the array that returns `true` when applied the `filter` function.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.some(|v| v > 3)); // prints true
+///
+/// print(x.some(|v| v > 10)); // prints false
+///
+/// print(x.some(|v, i| i > v)); // prints true
+/// ```
+fn some(array: Array, filter: FnPtr) -> bool;
+
+/// Sort the array.
+///
+/// All elements in the array must be of the same data type.
+///
+/// # Supported Data Types
+///
+/// * integer numbers
+/// * floating-point numbers
+/// * decimal numbers
+/// * characters
+/// * strings
+/// * booleans
+/// * `()`
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10];
+///
+/// x.sort();
+///
+/// print(x); // prints "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
+/// ```
+fn sort(array: Array) -> ();
+
+/// Sort the array based on applying a function named by `comparer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `comparer` must exist taking these parameters:
+///
+/// * `element1`: copy of the current array element to compare
+/// * `element2`: copy of the next array element to compare
+///
+/// ## Return Value
+///
+/// * Any integer > 0 if `element1 > element2`
+/// * Zero if `element1 == element2`
+/// * Any integer < 0 if `element1 < element2`
+///
+/// # Example
+///
+/// ```rhai
+/// fn reverse(a, b) {
+/// if a > b {
+/// -1
+/// } else if a < b {
+/// 1
+/// } else {
+/// 0
+/// }
+/// }
+/// let x = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10];
+///
+/// x.sort("reverse");
+///
+/// print(x); // prints "[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]"
+/// ```
+fn sort(array: Array, comparer: String) -> ();
+
+/// Sort the array based on applying the `comparer` function.
+///
+/// # Function Parameters
+///
+/// * `element1`: copy of the current array element to compare
+/// * `element2`: copy of the next array element to compare
+///
+/// ## Return Value
+///
+/// * Any integer > 0 if `element1 > element2`
+/// * Zero if `element1 == element2`
+/// * Any integer < 0 if `element1 < element2`
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10];
+///
+/// // Do comparisons in reverse
+/// x.sort(|a, b| if a > b { -1 } else if a < b { 1 } else { 0 });
+///
+/// print(x); // prints "[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]"
+/// ```
+fn sort(array: Array, comparer: FnPtr) -> ();
+
+/// Replace an exclusive range of the array with another array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [7, 8, 9, 10];
+///
+/// x.splice(1..3, y);
+///
+/// print(x); // prints "[1, 7, 8, 9, 10, 4, 5]"
+/// ```
+fn splice(array: Array, range: Range<int>, replace: Array) -> ();
+
+/// Replace an inclusive range of the array with another array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [7, 8, 9, 10];
+///
+/// x.splice(1..=3, y);
+///
+/// print(x); // prints "[1, 7, 8, 9, 10, 5]"
+/// ```
+fn splice(array: Array, range: RangeInclusive<int>, replace: Array) -> ();
+
+/// Replace an exclusive `range` of the BLOB with another BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob(10, 0x42);
+/// let b2 = blob(5, 0x18);
+///
+/// b1.splice(1..4, b2);
+///
+/// print(b1); // prints "[4218181818184242 42424242]"
+/// ```
+fn splice(blob: Blob, range: Range<int>, replace: Blob) -> ();
+
+/// Replace an inclusive `range` of the BLOB with another BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob(10, 0x42);
+/// let b2 = blob(5, 0x18);
+///
+/// b1.splice(1..=4, b2);
+///
+/// print(b1); // prints "[4218181818184242 424242]"
+/// ```
+fn splice(blob: Blob, range: RangeInclusive<int>, replace: Blob) -> ();
+
+/// Replace a portion of the array with another array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, the other array is appended to the end of the array.
+/// * If `len` ≤ 0, the other array is inserted into the array at the `start` position without replacing any element.
+/// * If `start` position + `len` ≥ length of array, entire portion of the array after the `start` position is replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [7, 8, 9, 10];
+///
+/// x.splice(1, 2, y);
+///
+/// print(x); // prints "[1, 7, 8, 9, 10, 4, 5]"
+///
+/// x.splice(-5, 4, y);
+///
+/// print(x); // prints "[1, 7, 7, 8, 9, 10, 5]"
+/// ```
+fn splice(array: Array, start: int, len: int, replace: Array) -> ();
+
+/// Replace a portion of the BLOB with another BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, the other BLOB is appended to the end of the BLOB.
+/// * If `len` ≤ 0, the other BLOB is inserted into the BLOB at the `start` position without replacing anything.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob(10, 0x42);
+/// let b2 = blob(5, 0x18);
+///
+/// b1.splice(1, 3, b2);
+///
+/// print(b1); // prints "[4218181818184242 42424242]"
+///
+/// b1.splice(-5, 4, b2);
+///
+/// print(b1); // prints "[4218181818184218 1818181842]"
+/// ```
+fn splice(blob: Blob, start: int, len: int, replace: Blob) -> ();
+
+/// Split the string into segments based on whitespaces, returning an array of the segments.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split()); // prints ["hello,", "world!", "hello,", "foo!"]
+/// ```
+fn split(string: String) -> Array;
+
+/// Cut off the array at `index` and return it as a new array.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` is zero, the entire array is cut and returned.
+/// * If `index` < -length of array, the entire array is cut and returned.
+/// * If `index` ≥ length of array, nothing is cut from the array and an empty array is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.split(2);
+///
+/// print(y); // prints "[3, 4, 5]"
+///
+/// print(x); // prints "[1, 2]"
+/// ```
+fn split(array: Array, index: int) -> Array;
+
+/// Cut off the BLOB at `index` and return it as a new BLOB.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `index` is zero, the entire BLOB is cut and returned.
+/// * If `index` < -length of BLOB, the entire BLOB is cut and returned.
+/// * If `index` ≥ length of BLOB, nothing is cut from the BLOB and an empty BLOB is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.split(2);
+///
+/// print(b2); // prints "[030405]"
+///
+/// print(b1); // prints "[0102]"
+/// ```
+fn split(blob: Blob, index: int) -> Blob;
+
+/// Split the string into two at the specified `index` position and return it both strings
+/// as an array.
+///
+/// The character at the `index` position (if any) is returned in the _second_ string.
+///
+/// * If `index` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `index` < -length of string, it is equivalent to cutting at position 0.
+/// * If `index` ≥ length of string, it is equivalent to cutting at the end of the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.split(6)); // prints ["hello,", " world!"]
+///
+/// print(text.split(13)); // prints ["hello, world!", ""]
+///
+/// print(text.split(-6)); // prints ["hello, ", "world!"]
+///
+/// print(text.split(-99)); // prints ["", "hello, world!"]
+/// ```
+fn split(string: String, index: int) -> Array;
+
+/// Split the string into segments based on a `delimiter` string, returning an array of the segments.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split("ll")); // prints ["he", "o, world! he", "o, foo!"]
+/// ```
+fn split(string: String, delimiter: String) -> Array;
+
+/// Split the string into segments based on a `delimiter` character, returning an array of the segments.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split('l')); // prints ["he", "", "o, wor", "d! he", "", "o, foo!"]
+/// ```
+fn split(string: String, delimiter: char) -> Array;
+
+/// Split the string into at most the specified number of `segments` based on a `delimiter` string,
+/// returning an array of the segments.
+///
+/// If `segments` < 1, only one segment is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split("ll", 2)); // prints ["he", "o, world! hello, foo!"]
+/// ```
+fn split(string: String, delimiter: String, segments: int) -> Array;
+
+/// Split the string into at most the specified number of `segments` based on a `delimiter` character,
+/// returning an array of the segments.
+///
+/// If `segments` < 1, only one segment is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split('l', 3)); // prints ["he", "", "o, world! hello, foo!"]
+/// ```
+fn split(string: String, delimiter: char, segments: int) -> Array;
+
+/// Split the string into segments based on a `delimiter` string, returning an array of the
+/// segments in _reverse_ order.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split_rev("ll")); // prints ["o, foo!", "o, world! he", "he"]
+/// ```
+fn split_rev(string: String, delimiter: String) -> Array;
+
+/// Split the string into segments based on a `delimiter` character, returning an array of
+/// the segments in _reverse_ order.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split_rev('l')); // prints ["o, foo!", "", "d! he", "o, wor", "", "he"]
+/// ```
+fn split_rev(string: String, delimiter: char) -> Array;
+
+/// Split the string into at most a specified number of `segments` based on a `delimiter` string,
+/// returning an array of the segments in _reverse_ order.
+///
+/// If `segments` < 1, only one segment is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split_rev("ll", 2)); // prints ["o, foo!", "hello, world! he"]
+/// ```
+fn split_rev(string: String, delimiter: String, segments: int) -> Array;
+
+/// Split the string into at most the specified number of `segments` based on a `delimiter` character,
+/// returning an array of the segments.
+///
+/// If `segments` < 1, only one segment is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split('l', 3)); // prints ["o, foo!", "", "hello, world! he"
+/// ```
+fn split_rev(string: String, delimiter: char, segments: int) -> Array;
+
+/// Return the square root of the floating-point number.
+fn sqrt(x: float) -> float;
+
+/// Return the start of the exclusive range.
+fn start(range: ExclusiveRange) -> int;
+
+/// Return the start of the inclusive range.
+fn start(range: InclusiveRange) -> int;
+
+/// Return `true` if the string starts with a specified string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.starts_with("hello")); // prints true
+///
+/// print(text.starts_with("world")); // prints false
+/// ```
+fn starts_with(string: String, match_string: String) -> bool;
+
+/// Copy an exclusive range of characters from the string and return it as a new string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.sub_string(3..7)); // prints "lo, "
+/// ```
+fn sub_string(string: String, range: Range<int>) -> String;
+
+/// Copy an inclusive range of characters from the string and return it as a new string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.sub_string(3..=7)); // prints "lo, w"
+/// ```
+fn sub_string(string: String, range: RangeInclusive<int>) -> String;
+
+/// Copy a portion of the string beginning at the `start` position till the end and return it as
+/// a new string.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, the entire string is copied and returned.
+/// * If `start` ≥ length of string, an empty string is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.sub_string(5)); // prints ", world!"
+///
+/// print(text.sub_string(-5)); // prints "orld!"
+/// ```
+fn sub_string(string: String, start: int) -> String;
+
+/// Copy a portion of the string and return it as a new string.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, an empty string is returned.
+/// * If `len` ≤ 0, an empty string is returned.
+/// * If `start` position + `len` ≥ length of string, entire portion of the string after the `start` position is copied and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.sub_string(3, 4)); // prints "lo, "
+///
+/// print(text.sub_string(-8, 3)); // prints ", w"
+/// ```
+fn sub_string(string: String, start: int, len: int) -> String;
+
+/// Return the _tag_ of a `Dynamic` value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// x.tag = 42;
+///
+/// print(x.tag); // prints 42
+/// ```
+fn tag(value: ?) -> int;
+
+/// Return the tangent of the floating-point number in radians.
+fn tan(x: float) -> float;
+
+/// Return the hyperbolic tangent of the floating-point number in radians.
+fn tanh(x: float) -> float;
+
+/// Create a timestamp containing the current system time.
+///
+/// # Example
+///
+/// ```rhai
+/// let now = timestamp();
+///
+/// sleep(10.0); // sleep for 10 seconds
+///
+/// print(now.elapsed); // prints 10.???
+/// ```
+fn timestamp() -> Instant;
+
+/// Convert the BLOB into an array of integers.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// let x = b.to_array();
+///
+/// print(x); // prints "[66, 66, 66, 66, 66]"
+/// ```
+fn to_array(blob: Blob) -> Array;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: i128) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: i16) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: i32) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: int) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: i8) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u128) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u16) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u32) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u64) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u8) -> String;
+
+/// Convert the string into an UTF-8 encoded byte-stream as a BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// let bytes = text.to_blob();
+///
+/// print(bytes.len()); // prints 51
+/// ```
+fn to_blob(string: String) -> Blob;
+
+/// Return an array containing all the characters of the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello";
+///
+/// print(text.to_chars()); // prints "['h', 'e', 'l', 'l', 'o']"
+/// ```
+fn to_chars(string: String) -> Array;
+
+/// Convert the array into a string.
+fn to_debug(array: Array) -> String;
+
+/// Convert the string into debug format.
+fn to_debug(character: char) -> String;
+
+/// Convert the function pointer into a string in debug format.
+fn to_debug(f: FnPtr) -> String;
+
+/// Convert the value of the `item` into a string in debug format.
+fn to_debug(item: ?) -> String;
+
+/// Convert the object map into a string.
+fn to_debug(map: Map) -> String;
+
+/// Convert the value of `number` into a string.
+fn to_debug(number: f32) -> String;
+
+/// Convert the value of `number` into a string.
+fn to_debug(number: float) -> String;
+
+/// Convert the string into debug format.
+fn to_debug(string: String) -> String;
+
+/// Convert the unit into a string in debug format.
+fn to_debug(unit: ()) -> String;
+
+/// Convert the boolean value into a string in debug format.
+fn to_debug(value: bool) -> String;
+
+/// Convert radians to degrees.
+fn to_degrees(x: float) -> float;
+
+/// Convert the 32-bit floating-point number to 64-bit.
+fn to_float(x: f32) -> float;
+
+fn to_float(x: i128) -> float;
+
+fn to_float(x: i16) -> float;
+
+fn to_float(x: i32) -> float;
+
+fn to_float(x: int) -> float;
+
+fn to_float(x: i8) -> float;
+
+fn to_float(x: u128) -> float;
+
+fn to_float(x: u16) -> float;
+
+fn to_float(x: u32) -> float;
+
+fn to_float(x: u8) -> float;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: i128) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: i16) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: i32) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: int) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: i8) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u128) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u16) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u32) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u64) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u8) -> String;
+
+fn to_int(x: char) -> int;
+
+/// Convert the floating-point number into an integer.
+fn to_int(x: f32) -> int;
+
+/// Convert the floating-point number into an integer.
+fn to_int(x: float) -> int;
+
+fn to_int(x: i128) -> int;
+
+fn to_int(x: i16) -> int;
+
+fn to_int(x: i32) -> int;
+
+fn to_int(x: int) -> int;
+
+fn to_int(x: i8) -> int;
+
+fn to_int(x: u128) -> int;
+
+fn to_int(x: u16) -> int;
+
+fn to_int(x: u32) -> int;
+
+fn to_int(x: u64) -> int;
+
+fn to_int(x: u8) -> int;
+
+/// Return the JSON representation of the object map.
+///
+/// # Data types
+///
+/// Only the following data types should be kept inside the object map:
+/// `INT`, `FLOAT`, `ImmutableString`, `char`, `bool`, `()`, `Array`, `Map`.
+///
+/// # Errors
+///
+/// Data types not supported by JSON serialize into formats that may
+/// invalidate the result.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// print(m.to_json()); // prints {"a":1, "b":2, "c":3}
+/// ```
+fn to_json(map: Map) -> String;
+
+/// Convert the character to lower-case and return it as a new character.
+///
+/// # Example
+///
+/// ```rhai
+/// let ch = 'A';
+///
+/// print(ch.to_lower()); // prints 'a'
+///
+/// print(ch); // prints 'A'
+/// ```
+fn to_lower(character: char) -> char;
+
+/// Convert the string to all lower-case and return it as a new string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "HELLO, WORLD!"
+///
+/// print(text.to_lower()); // prints "hello, world!"
+///
+/// print(text); // prints "HELLO, WORLD!"
+/// ```
+fn to_lower(string: String) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: i128) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: i16) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: i32) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: int) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: i8) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u128) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u16) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u32) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u64) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u8) -> String;
+
+/// Convert degrees to radians.
+fn to_radians(x: float) -> float;
+
+/// Convert the array into a string.
+fn to_string(array: Array) -> String;
+
+/// Return the character into a string.
+fn to_string(character: char) -> String;
+
+/// Convert the value of the `item` into a string.
+fn to_string(item: ?) -> String;
+
+/// Convert the object map into a string.
+fn to_string(map: Map) -> String;
+
+/// Convert the value of `number` into a string.
+fn to_string(number: f32) -> String;
+
+/// Convert the value of `number` into a string.
+fn to_string(number: float) -> String;
+
+/// Return the `string`.
+fn to_string(string: String) -> String;
+
+/// Return the empty string.
+fn to_string(unit: ()) -> String;
+
+/// Return the boolean value into a string.
+fn to_string(value: bool) -> String;
+
+/// Convert the character to upper-case and return it as a new character.
+///
+/// # Example
+///
+/// ```rhai
+/// let ch = 'a';
+///
+/// print(ch.to_upper()); // prints 'A'
+///
+/// print(ch); // prints 'a'
+/// ```
+fn to_upper(character: char) -> char;
+
+/// Convert the string to all upper-case and return it as a new string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!"
+///
+/// print(text.to_upper()); // prints "HELLO, WORLD!"
+///
+/// print(text); // prints "hello, world!"
+/// ```
+fn to_upper(string: String) -> String;
+
+/// Remove whitespace characters from both ends of the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = " hello ";
+///
+/// text.trim();
+///
+/// print(text); // prints "hello"
+/// ```
+fn trim(string: String) -> ();
+
+/// Cut off the array at the specified length.
+///
+/// * If `len` ≤ 0, the array is cleared.
+/// * If `len` ≥ length of array, the array is not truncated.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// x.truncate(3);
+///
+/// print(x); // prints "[1, 2, 3]"
+///
+/// x.truncate(10);
+///
+/// print(x); // prints "[1, 2, 3]"
+/// ```
+fn truncate(array: Array, len: int) -> ();
+
+/// Cut off the BLOB at the specified length.
+///
+/// * If `len` ≤ 0, the BLOB is cleared.
+/// * If `len` ≥ length of BLOB, the BLOB is not truncated.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// b.truncate(3);
+///
+/// print(b); // prints "[010203]"
+///
+/// b.truncate(10);
+///
+/// print(b); // prints "[010203]"
+/// ```
+fn truncate(blob: Blob, len: int) -> ();
+
+/// Cut off the string at the specified number of characters.
+///
+/// * If `len` ≤ 0, the string is cleared.
+/// * If `len` ≥ length of string, the string is not truncated.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.truncate(13);
+///
+/// print(text); // prints "hello, world!"
+///
+/// x.truncate(10);
+///
+/// print(text); // prints "hello, world!"
+/// ```
+fn truncate(string: String, len: int) -> ();
+
+/// Return an array with all the property values in the object map.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// print(m.values()); // prints "[1, 2, 3]""
+/// ```
+fn values(map: Map) -> Array;
+
+/// Write an ASCII string to the bytes within an exclusive `range` in the BLOB.
+///
+/// Each ASCII character encodes to one single byte in the BLOB.
+/// Non-ASCII characters are ignored.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_ascii(1..5, "hello, world!");
+///
+/// print(b); // prints "[0068656c6c000000]"
+/// ```
+fn write_ascii(blob: Blob, range: Range<int>, string: String) -> ();
+
+/// Write an ASCII string to the bytes within an inclusive `range` in the BLOB.
+///
+/// Each ASCII character encodes to one single byte in the BLOB.
+/// Non-ASCII characters are ignored.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_ascii(1..=5, "hello, world!");
+///
+/// print(b); // prints "[0068656c6c6f0000]"
+/// ```
+fn write_ascii(blob: Blob, range: RangeInclusive<int>, string: String) -> ();
+
+/// Write an ASCII string to the bytes within an exclusive `range` in the BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, the BLOB is not modified.
+/// * If `len` ≤ 0, the BLOB is not modified.
+/// * If `start` position + `len` ≥ length of BLOB, only the portion of the BLOB after the `start` position is modified.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_ascii(1, 5, "hello, world!");
+///
+/// print(b); // prints "[0068656c6c6f0000]"
+/// ```
+fn write_ascii(blob: Blob, start: int, len: int, string: String) -> ();
+
+/// Write a `FLOAT` value to the bytes within an exclusive `range` in the BLOB
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_be(blob: Blob, range: Range<int>, value: float) -> ();
+
+/// Write an `INT` value to the bytes within an exclusive `range` in the BLOB
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8, 0x42);
+///
+/// b.write_be_int(1..3, 0x99);
+///
+/// print(b); // prints "[4200004242424242]"
+/// ```
+fn write_be(blob: Blob, range: Range<int>, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes within an inclusive `range` in the BLOB
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_be(blob: Blob, range: RangeInclusive<int>, value: float) -> ();
+
+/// Write an `INT` value to the bytes within an inclusive `range` in the BLOB
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8, 0x42);
+///
+/// b.write_be_int(1..=3, 0x99);
+///
+/// print(b); // prints "[4200000042424242]"
+/// ```
+fn write_be(blob: Blob, range: RangeInclusive<int>, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes beginning at the `start` position in the BLOB
+/// in big-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_be(blob: Blob, start: int, len: int, value: float) -> ();
+
+/// Write an `INT` value to the bytes beginning at the `start` position in the BLOB
+/// in big-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8, 0x42);
+///
+/// b.write_be_int(1, 3, 0x99);
+///
+/// print(b); // prints "[4200000042424242]"
+/// ```
+fn write_be(blob: Blob, start: int, len: int, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes within an exclusive `range` in the BLOB
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_le(blob: Blob, range: Range<int>, value: float) -> ();
+
+/// Write an `INT` value to the bytes within an exclusive `range` in the BLOB
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_le_int(1..3, 0x12345678);
+///
+/// print(b); // prints "[0078560000000000]"
+/// ```
+fn write_le(blob: Blob, range: Range<int>, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes within an inclusive `range` in the BLOB
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_le(blob: Blob, range: RangeInclusive<int>, value: float) -> ();
+
+/// Write an `INT` value to the bytes within an inclusive `range` in the BLOB
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_le_int(1..=3, 0x12345678);
+///
+/// print(b); // prints "[0078563400000000]"
+/// ```
+fn write_le(blob: Blob, range: RangeInclusive<int>, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes beginning at the `start` position in the BLOB
+/// in little-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_le(blob: Blob, start: int, len: int, value: float) -> ();
+
+/// Write an `INT` value to the bytes beginning at the `start` position in the BLOB
+/// in little-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_le_int(1, 3, 0x12345678);
+///
+/// print(b); // prints "[0078563400000000]"
+/// ```
+fn write_le(blob: Blob, start: int, len: int, value: int) -> ();
+
+/// Write a string to the bytes within an exclusive `range` in the BLOB in UTF-8 encoding.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_utf8(1..5, "朝には紅顔ありて夕べには白骨となる");
+///
+/// print(b); // prints "[00e69c9de3000000]"
+/// ```
+fn write_utf8(blob: Blob, range: Range<int>, string: String) -> ();
+
+/// Write a string to the bytes within an inclusive `range` in the BLOB in UTF-8 encoding.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_utf8(1..=5, "朝には紅顔ありて夕べには白骨となる");
+///
+/// print(b); // prints "[00e69c9de3810000]"
+/// ```
+fn write_utf8(blob: Blob, range: RangeInclusive<int>, string: String) -> ();
+
+/// Write a string to the bytes within an inclusive `range` in the BLOB in UTF-8 encoding.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, the BLOB is not modified.
+/// * If `len` ≤ 0, the BLOB is not modified.
+/// * If `start` position + `len` ≥ length of BLOB, only the portion of the BLOB after the `start` position is modified.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_utf8(1, 5, "朝には紅顔ありて夕べには白骨となる");
+///
+/// print(b); // prints "[00e69c9de3810000]"
+/// ```
+fn write_utf8(blob: Blob, start: int, len: int, string: String) -> ();
+
+op |(i128, i128) -> i128;
+
+op |(i16, i16) -> i16;
+
+op |(i32, i32) -> i32;
+
+op |(i8, i8) -> i8;
+
+op |(u128, u128) -> u128;
+
+op |(u16, u16) -> u16;
+
+op |(u32, u32) -> u32;
+
+op |(u64, u64) -> u64;
+
+op |(u8, u8) -> u8;
+
+module general_kenobi {
+const CONSTANT: int;
+
+/// Returns a string where "hello there" is repeated `n` times.
+fn hello_there(n: int) -> String;
+}
+
+let hello_there: string;
+
+const HELLO: string;
diff --git a/rhai/examples/definitions/.rhai/all_in_one_without_standard.d.rhai b/rhai/examples/definitions/.rhai/all_in_one_without_standard.d.rhai
new file mode 100644
index 0000000..729b64f
--- /dev/null
+++ b/rhai/examples/definitions/.rhai/all_in_one_without_standard.d.rhai
@@ -0,0 +1,14 @@
+module static;
+
+op minus(int, int) -> int;
+
+module general_kenobi {
+const CONSTANT: int;
+
+/// Returns a string where "hello there" is repeated `n` times.
+fn hello_there(n: int) -> String;
+}
+
+let hello_there: string;
+
+const HELLO: string;
diff --git a/rhai/examples/definitions/.rhai/definitions/__builtin-operators__.d.rhai b/rhai/examples/definitions/.rhai/definitions/__builtin-operators__.d.rhai
new file mode 100644
index 0000000..012a95a
--- /dev/null
+++ b/rhai/examples/definitions/.rhai/definitions/__builtin-operators__.d.rhai
@@ -0,0 +1,259 @@
+module static;
+
+op ==(int, int) -> bool;
+op !=(int, int) -> bool;
+op >(int, int) -> bool;
+op >=(int, int) -> bool;
+op <(int, int) -> bool;
+op <=(int, int) -> bool;
+op &(int, int) -> int;
+op |(int, int) -> int;
+op ^(int, int) -> int;
+op ..(int, int) -> Range<int>;
+op ..=(int, int) -> RangeInclusive<int>;
+
+op ==(bool, bool) -> bool;
+op !=(bool, bool) -> bool;
+op >(bool, bool) -> bool;
+op >=(bool, bool) -> bool;
+op <(bool, bool) -> bool;
+op <=(bool, bool) -> bool;
+op &(bool, bool) -> bool;
+op |(bool, bool) -> bool;
+op ^(bool, bool) -> bool;
+
+op ==((), ()) -> bool;
+op !=((), ()) -> bool;
+op >((), ()) -> bool;
+op >=((), ()) -> bool;
+op <((), ()) -> bool;
+op <=((), ()) -> bool;
+
+op +(int, int) -> int;
+op -(int, int) -> int;
+op *(int, int) -> int;
+op /(int, int) -> int;
+op %(int, int) -> int;
+op **(int, int) -> int;
+op >>(int, int) -> int;
+op <<(int, int) -> int;
+
+op +(float, float) -> float;
+op -(float, float) -> float;
+op *(float, float) -> float;
+op /(float, float) -> float;
+op %(float, float) -> float;
+op **(float, float) -> float;
+op ==(float, float) -> bool;
+op !=(float, float) -> bool;
+op >(float, float) -> bool;
+op >=(float, float) -> bool;
+op <(float, float) -> bool;
+op <=(float, float) -> bool;
+
+op +(float, int) -> float;
+op -(float, int) -> float;
+op *(float, int) -> float;
+op /(float, int) -> float;
+op %(float, int) -> float;
+op **(float, int) -> float;
+op ==(float, int) -> bool;
+op !=(float, int) -> bool;
+op >(float, int) -> bool;
+op >=(float, int) -> bool;
+op <(float, int) -> bool;
+op <=(float, int) -> bool;
+
+op +(int, float) -> float;
+op -(int, float) -> float;
+op *(int, float) -> float;
+op /(int, float) -> float;
+op %(int, float) -> float;
+op **(int, float) -> float;
+op ==(int, float) -> bool;
+op !=(int, float) -> bool;
+op >(int, float) -> bool;
+op >=(int, float) -> bool;
+op <(int, float) -> bool;
+op <=(int, float) -> bool;
+
+op +(Decimal, Decimal) -> Decimal;
+op -(Decimal, Decimal) -> Decimal;
+op *(Decimal, Decimal) -> Decimal;
+op /(Decimal, Decimal) -> Decimal;
+op %(Decimal, Decimal) -> Decimal;
+op **(Decimal, Decimal) -> Decimal;
+op ==(Decimal, Decimal) -> bool;
+op !=(Decimal, Decimal) -> bool;
+op >(Decimal, Decimal) -> bool;
+op >=(Decimal, Decimal) -> bool;
+op <(Decimal, Decimal) -> bool;
+op <=(Decimal, Decimal) -> bool;
+
+op +(Decimal, int) -> Decimal;
+op -(Decimal, int) -> Decimal;
+op *(Decimal, int) -> Decimal;
+op /(Decimal, int) -> Decimal;
+op %(Decimal, int) -> Decimal;
+op **(Decimal, int) -> Decimal;
+op ==(Decimal, int) -> bool;
+op !=(Decimal, int) -> bool;
+op >(Decimal, int) -> bool;
+op >=(Decimal, int) -> bool;
+op <(Decimal, int) -> bool;
+op <=(Decimal, int) -> bool;
+
+op +(int, Decimal) -> Decimal;
+op -(int, Decimal) -> Decimal;
+op *(int, Decimal) -> Decimal;
+op /(int, Decimal) -> Decimal;
+op %(int, Decimal) -> Decimal;
+op **(int, Decimal) -> Decimal;
+op ==(int, Decimal) -> bool;
+op !=(int, Decimal) -> bool;
+op >(int, Decimal) -> bool;
+op >=(int, Decimal) -> bool;
+op <(int, Decimal) -> bool;
+op <=(int, Decimal) -> bool;
+
+op +(String, String) -> String;
+op -(String, String) -> String;
+op ==(String, String) -> bool;
+op !=(String, String) -> bool;
+op >(String, String) -> bool;
+op >=(String, String) -> bool;
+op <(String, String) -> bool;
+op <=(String, String) -> bool;
+
+op +(char, char) -> String;
+op ==(char, char) -> bool;
+op !=(char, char) -> bool;
+op >(char, char) -> bool;
+op >=(char, char) -> bool;
+op <(char, char) -> bool;
+op <=(char, char) -> bool;
+
+op +(char, String) -> String;
+op ==(char, String) -> bool;
+op !=(char, String) -> bool;
+op >(char, String) -> bool;
+op >=(char, String) -> bool;
+op <(char, String) -> bool;
+op <=(char, String) -> bool;
+
+op +(String, char) -> String;
+op -(String, char) -> String;
+op ==(String, char) -> bool;
+op !=(String, char) -> bool;
+op >(String, char) -> bool;
+op >=(String, char) -> bool;
+op <(String, char) -> bool;
+op <=(String, char) -> bool;
+
+op +((), String) -> String;
+op ==((), String) -> bool;
+op !=((), String) -> bool;
+op >((), String) -> bool;
+op >=((), String) -> bool;
+op <((), String) -> bool;
+op <=((), String) -> bool;
+
+op +(String, ()) -> String;
+op ==(String, ()) -> bool;
+op !=(String, ()) -> bool;
+op >(String, ()) -> bool;
+op >=(String, ()) -> bool;
+op <(String, ()) -> bool;
+op <=(String, ()) -> bool;
+
+op +(Blob, Blob) -> Blob;
+op +(Blob, char) -> Blob;
+op ==(Blob, Blob) -> bool;
+op !=(Blob, Blob) -> bool;
+
+
+op ==(Range<int>, RangeInclusive<int>) -> bool;
+op !=(Range<int>, RangeInclusive<int>) -> bool;
+
+op ==(RangeInclusive<int>, Range<int>) -> bool;
+op !=(RangeInclusive<int>, Range<int>) -> bool;
+
+op ==(Range<int>, Range<int>) -> bool;
+op !=(Range<int>, Range<int>) -> bool;
+
+op ==(RangeInclusive<int>, RangeInclusive<int>) -> bool;
+op !=(RangeInclusive<int>, RangeInclusive<int>) -> bool;
+
+op ==(?, ?) -> bool;
+op !=(?, ?) -> bool;
+op >(?, ?) -> bool;
+op >=(?, ?) -> bool;
+op <(?, ?) -> bool;
+op <=(?, ?) -> bool;
+
+
+op &=(bool, bool);
+op |=(bool, bool);
+
+op +=(int, int);
+op -=(int, int);
+op *=(int, int);
+op /=(int, int);
+op %=(int, int);
+op **=(int, int);
+op >>=(int, int);
+op <<=(int, int);
+op &=(int, int);
+op |=(int, int);
+op ^=(int, int);
+
+op +=(float, float);
+op -=(float, float);
+op *=(float, float);
+op /=(float, float);
+op %=(float, float);
+op **=(float, float);
+
+op +=(float, int);
+op -=(float, int);
+op *=(float, int);
+op /=(float, int);
+op %=(float, int);
+op **=(float, int);
+
+op +=(Decimal, Decimal);
+op -=(Decimal, Decimal);
+op *=(Decimal, Decimal);
+op /=(Decimal, Decimal);
+op %=(Decimal, Decimal);
+op **=(Decimal, Decimal);
+
+op +=(Decimal, int);
+op -=(Decimal, int);
+op *=(Decimal, int);
+op /=(Decimal, int);
+op %=(Decimal, int);
+op **=(Decimal, int);
+
+op +=(String, String);
+op -=(String, String);
+op +=(String, char);
+op -=(String, char);
+op +=(char, String);
+op +=(char, char);
+
+op +=(Array, Array);
+op +=(Array, ?);
+
+op +=(Blob, Blob);
+op +=(Blob, int);
+op +=(Blob, char);
+op +=(Blob, String);
+
+op in(?, Array) -> bool;
+op in(String, String) -> bool;
+op in(char, String) -> bool;
+op in(int, Range<int>) -> bool;
+op in(int, RangeInclusive<int>) -> bool;
+op in(String, Map) -> bool;
+op in(int, Blob) -> bool;
diff --git a/rhai/examples/definitions/.rhai/definitions/__builtin__.d.rhai b/rhai/examples/definitions/.rhai/definitions/__builtin__.d.rhai
new file mode 100644
index 0000000..881c158
--- /dev/null
+++ b/rhai/examples/definitions/.rhai/definitions/__builtin__.d.rhai
@@ -0,0 +1,261 @@
+module static;
+
+/// Display any data to the standard output.
+///
+/// # Example
+///
+/// ```rhai
+/// let answer = 42;
+///
+/// print(`The Answer is ${answer}`);
+/// ```
+fn print(data: ?);
+
+/// Display any data to the standard output in debug format.
+///
+/// # Example
+///
+/// ```rhai
+/// let answer = 42;
+///
+/// debug(answer);
+/// ```
+fn debug(data: ?);
+
+/// Get the type of a value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// print(x.type_of()); // prints "string"
+/// ```
+fn type_of(data: ?) -> String;
+
+/// Create a function pointer to a named function.
+///
+/// If the specified name is not a valid function name, an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = Fn("foo"); // function pointer to 'foo'
+///
+/// f.call(42); // call: foo(42)
+/// ```
+fn Fn(fn_name: String) -> FnPtr;
+
+/// Call a function pointed to by a function pointer,
+/// passing following arguments to the function call.
+///
+/// If an appropriate function is not found, an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = Fn("foo"); // function pointer to 'foo'
+///
+/// f.call(1, 2, 3); // call: foo(1, 2, 3)
+/// ```
+fn call(fn_ptr: FnPtr, ...args: ?) -> ?;
+
+/// Call a function pointed to by a function pointer, binding the `this` pointer
+/// to the object of the method call, and passing on following arguments to the function call.
+///
+/// If an appropriate function is not found, an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// fn add(x) {
+/// this + x
+/// }
+///
+/// let f = Fn("add"); // function pointer to 'add'
+///
+/// let x = 41;
+///
+/// let r = x.call(f, 1); // call: add(1) with 'this' = 'x'
+///
+/// print(r); // prints 42
+/// ```
+fn call(obj: ?, fn_ptr: FnPtr, ...args: ?) -> ?;
+
+/// Curry a number of arguments into a function pointer and return it as a new function pointer.
+///
+/// # Example
+///
+/// ```rhai
+/// fn foo(x, y, z) {
+/// x + y + z
+/// }
+///
+/// let f = Fn("foo");
+///
+/// let g = f.curry(1, 2); // curried arguments: 1, 2
+///
+/// g.call(3); // call: foo(1, 2, 3)
+/// ```
+fn curry(fn_ptr: FnPtr, ...args: ?) -> FnPtr;
+
+/// Return `true` if a script-defined function exists with a specified name and
+/// number of parameters.
+///
+/// # Example
+///
+/// ```rhai
+/// fn foo(x) { }
+///
+/// print(is_def_fn("foo", 1)); // prints true
+/// print(is_def_fn("foo", 2)); // prints false
+/// print(is_def_fn("foo", 0)); // prints false
+/// print(is_def_fn("bar", 1)); // prints false
+/// ```
+fn is_def_fn(fn_name: String, num_params: int) -> bool;
+
+/// Return `true` if a variable matching a specified name is defined.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 42;
+///
+/// print(is_def_var("x")); // prints true
+/// print(is_def_var("foo")); // prints false
+///
+/// {
+/// let y = 1;
+/// print(is_def_var("y")); // prints true
+/// }
+///
+/// print(is_def_var("y")); // prints false
+/// ```
+fn is_def_var(var_name: String) -> bool;
+
+/// Return `true` if the variable is shared.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 42;
+///
+/// print(is_shared(x)); // prints false
+///
+/// let f = || x; // capture 'x', making it shared
+///
+/// print(is_shared(x)); // prints true
+/// ```
+fn is_shared(variable: ?) -> bool;
+
+/// Evaluate a text script within the current scope.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 42;
+///
+/// eval("let y = x; x = 123;");
+///
+/// print(x); // prints 123
+/// print(y); // prints 42
+/// ```
+fn eval(script: String) -> ?;
+
+/// Return `true` if the string contains another string.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello world!";
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if "world" in x {
+/// print("found!");
+/// }
+/// ```
+fn contains(string: String, find: String) -> bool;
+
+/// Return `true` if the string contains a character.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello world!";
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 'w' in x {
+/// print("found!");
+/// }
+/// ```
+fn contains(string: String, ch: char) -> bool;
+
+/// Return `true` if a value falls within the exclusive range.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let r = 1..100;
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 42 in r {
+/// print("found!");
+/// }
+/// ```
+fn contains(range: Range<int>, value: int) -> bool;
+
+/// Return `true` if a value falls within the inclusive range.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let r = 1..=100;
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 42 in r {
+/// print("found!");
+/// }
+/// ```
+fn contains(range: RangeInclusive<int>, value: int) -> bool;
+
+/// Return `true` if a key exists within the object map.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if "c" in m {
+/// print("found!");
+/// }
+/// ```
+fn contains(map: Map, string: String) -> bool;
+
+/// Return `true` if a value is found within the BLOB.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 3 in b {
+/// print("found!");
+/// }
+/// ```
+fn contains(blob: Blob, value: int) -> bool;
diff --git a/rhai/examples/definitions/.rhai/definitions/__scope__.d.rhai b/rhai/examples/definitions/.rhai/definitions/__scope__.d.rhai
new file mode 100644
index 0000000..96d874f
--- /dev/null
+++ b/rhai/examples/definitions/.rhai/definitions/__scope__.d.rhai
@@ -0,0 +1,5 @@
+module static;
+
+let hello_there: string;
+
+const HELLO: string;
\ No newline at end of file
diff --git a/rhai/examples/definitions/.rhai/definitions/__static__.d.rhai b/rhai/examples/definitions/.rhai/definitions/__static__.d.rhai
new file mode 100644
index 0000000..9a1e2f7
--- /dev/null
+++ b/rhai/examples/definitions/.rhai/definitions/__static__.d.rhai
@@ -0,0 +1,5849 @@
+module static;
+
+op minus(int, int) -> int;
+
+op !(bool) -> bool;
+
+/// Return `true` if two arrays are not-equal (i.e. any element not equal or not in the same order).
+///
+/// The operator `==` is used to compare elements and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [1, 2, 3, 4, 5];
+/// let z = [1, 2, 3, 4];
+///
+/// print(x != y); // prints false
+///
+/// print(x != z); // prints true
+/// ```
+op !=(Array, Array) -> bool;
+
+/// Return `true` if two object maps are not equal (i.e. at least one property value is not equal).
+///
+/// The operator `==` is used to compare property values and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let m1 = #{a:1, b:2, c:3};
+/// let m2 = #{a:1, b:2, c:3};
+/// let m3 = #{a:1, c:3};
+///
+/// print(m1 != m2); // prints false
+///
+/// print(m1 != m3); // prints true
+/// ```
+op !=(Map, Map) -> bool;
+
+/// Return `true` if two timestamps are not equal.
+op !=(Instant, Instant) -> bool;
+
+op !=(int, f32) -> bool;
+
+op !=(int, float) -> bool;
+
+op !=(f32, int) -> bool;
+
+op !=(f32, f32) -> bool;
+
+op !=(float, int) -> bool;
+
+op !=(i128, i128) -> bool;
+
+op !=(i16, i16) -> bool;
+
+op !=(i32, i32) -> bool;
+
+op !=(i8, i8) -> bool;
+
+op !=(u128, u128) -> bool;
+
+op !=(u16, u16) -> bool;
+
+op !=(u32, u32) -> bool;
+
+op !=(u64, u64) -> bool;
+
+op !=(u8, u8) -> bool;
+
+op %(int, f32) -> f32;
+
+op %(f32, int) -> f32;
+
+op %(f32, f32) -> f32;
+
+op %(i128, i128) -> i128;
+
+op %(i16, i16) -> i16;
+
+op %(i32, i32) -> i32;
+
+op %(i8, i8) -> i8;
+
+op %(u128, u128) -> u128;
+
+op %(u16, u16) -> u16;
+
+op %(u32, u32) -> u32;
+
+op %(u64, u64) -> u64;
+
+op %(u8, u8) -> u8;
+
+op &(i128, i128) -> i128;
+
+op &(i16, i16) -> i16;
+
+op &(i32, i32) -> i32;
+
+op &(i8, i8) -> i8;
+
+op &(u128, u128) -> u128;
+
+op &(u16, u16) -> u16;
+
+op &(u32, u32) -> u32;
+
+op &(u64, u64) -> u64;
+
+op &(u8, u8) -> u8;
+
+op *(int, f32) -> f32;
+
+op *(f32, int) -> f32;
+
+op *(f32, f32) -> f32;
+
+op *(i128, i128) -> i128;
+
+op *(i16, i16) -> i16;
+
+op *(i32, i32) -> i32;
+
+op *(i8, i8) -> i8;
+
+op *(u128, u128) -> u128;
+
+op *(u16, u16) -> u16;
+
+op *(u32, u32) -> u32;
+
+op *(u64, u64) -> u64;
+
+op *(u8, u8) -> u8;
+
+op **(f32, int) -> f32;
+
+op **(f32, f32) -> f32;
+
+op **(i128, int) -> i128;
+
+op **(i16, int) -> i16;
+
+op **(i32, int) -> i32;
+
+op **(i8, int) -> i8;
+
+op **(u128, int) -> u128;
+
+op **(u16, int) -> u16;
+
+op **(u32, int) -> u32;
+
+op **(u64, int) -> u64;
+
+op **(u8, int) -> u8;
+
+op +(int) -> int;
+
+op +(f32) -> f32;
+
+op +(float) -> float;
+
+op +(i128) -> i128;
+
+op +(i16) -> i16;
+
+op +(i32) -> i32;
+
+op +(i8) -> i8;
+
+op +((), String) -> String;
+
+/// Combine two arrays into a new array and return it.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+/// let y = [true, 'x'];
+///
+/// print(x + y); // prints "[1, 2, 3, true, 'x']"
+///
+/// print(x); // prints "[1, 2, 3"
+/// ```
+op +(Array, Array) -> Array;
+
+op +(char, String) -> String;
+
+op +(?, String) -> String;
+
+/// Make a copy of the object map, add all property values of another object map
+/// (existing property values of the same names are replaced), then returning it.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+/// let n = #{a: 42, d:0};
+///
+/// print(m + n); // prints "#{a:42, b:2, c:3, d:0}"
+///
+/// print(m); // prints "#{a:1, b:2, c:3}"
+/// ```
+op +(Map, Map) -> Map;
+
+op +(String, String) -> String;
+
+op +(String, char) -> String;
+
+op +(String, ?) -> String;
+
+op +(String, Blob) -> String;
+
+op +(String, ()) -> String;
+
+/// Add the specified number of `seconds` to the timestamp and return it as a new timestamp.
+op +(Instant, float) -> Instant;
+
+/// Add the specified number of `seconds` to the timestamp and return it as a new timestamp.
+op +(Instant, int) -> Instant;
+
+op +(Blob, String) -> String;
+
+op +(int, f32) -> f32;
+
+op +(f32, int) -> f32;
+
+op +(f32, f32) -> f32;
+
+op +(i128, i128) -> i128;
+
+op +(i16, i16) -> i16;
+
+op +(i32, i32) -> i32;
+
+op +(i8, i8) -> i8;
+
+op +(u128, u128) -> u128;
+
+op +(u16, u16) -> u16;
+
+op +(u32, u32) -> u32;
+
+op +(u64, u64) -> u64;
+
+op +(u8, u8) -> u8;
+
+/// Add all property values of another object map into the object map.
+/// Existing property values of the same names are replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+/// let n = #{a: 42, d:0};
+///
+/// m.mixin(n);
+///
+/// print(m); // prints "#{a:42, b:2, c:3, d:0}"
+/// ```
+op +=(Map, Map) -> ();
+
+op +=(String, String) -> ();
+
+op +=(String, char) -> ();
+
+op +=(String, ()) -> ();
+
+op +=(String, ?) -> ();
+
+op +=(String, Blob) -> ();
+
+/// Add the specified number of `seconds` to the timestamp.
+op +=(Instant, float) -> ();
+
+/// Add the specified number of `seconds` to the timestamp.
+op +=(Instant, int) -> ();
+
+op -(int) -> int;
+
+op -(f32) -> f32;
+
+op -(float) -> float;
+
+op -(i128) -> i128;
+
+op -(i16) -> i16;
+
+op -(i32) -> i32;
+
+op -(i8) -> i8;
+
+/// Return the number of seconds between two timestamps.
+op -(Instant, Instant) -> RhaiResult;
+
+/// Subtract the specified number of `seconds` from the timestamp and return it as a new timestamp.
+op -(Instant, float) -> Instant;
+
+/// Subtract the specified number of `seconds` from the timestamp and return it as a new timestamp.
+op -(Instant, int) -> Instant;
+
+op -(int, f32) -> f32;
+
+op -(f32, int) -> f32;
+
+op -(f32, f32) -> f32;
+
+op -(i128, i128) -> i128;
+
+op -(i16, i16) -> i16;
+
+op -(i32, i32) -> i32;
+
+op -(i8, i8) -> i8;
+
+op -(u128, u128) -> u128;
+
+op -(u16, u16) -> u16;
+
+op -(u32, u32) -> u32;
+
+op -(u64, u64) -> u64;
+
+op -(u8, u8) -> u8;
+
+/// Subtract the specified number of `seconds` from the timestamp.
+op -=(Instant, float) -> ();
+
+/// Subtract the specified number of `seconds` from the timestamp.
+op -=(Instant, int) -> ();
+
+op /(int, f32) -> f32;
+
+op /(f32, int) -> f32;
+
+op /(f32, f32) -> f32;
+
+op /(i128, i128) -> i128;
+
+op /(i16, i16) -> i16;
+
+op /(i32, i32) -> i32;
+
+op /(i8, i8) -> i8;
+
+op /(u128, u128) -> u128;
+
+op /(u16, u16) -> u16;
+
+op /(u32, u32) -> u32;
+
+op /(u64, u64) -> u64;
+
+op /(u8, u8) -> u8;
+
+/// Return `true` if the first timestamp is earlier than the second.
+op <(Instant, Instant) -> bool;
+
+op <(int, f32) -> bool;
+
+op <(int, float) -> bool;
+
+op <(f32, int) -> bool;
+
+op <(f32, f32) -> bool;
+
+op <(float, int) -> bool;
+
+op <(i128, i128) -> bool;
+
+op <(i16, i16) -> bool;
+
+op <(i32, i32) -> bool;
+
+op <(i8, i8) -> bool;
+
+op <(u128, u128) -> bool;
+
+op <(u16, u16) -> bool;
+
+op <(u32, u32) -> bool;
+
+op <(u64, u64) -> bool;
+
+op <(u8, u8) -> bool;
+
+op <<(i128, int) -> i128;
+
+op <<(i16, int) -> i16;
+
+op <<(i32, int) -> i32;
+
+op <<(i8, int) -> i8;
+
+op <<(u128, int) -> u128;
+
+op <<(u16, int) -> u16;
+
+op <<(u32, int) -> u32;
+
+op <<(u64, int) -> u64;
+
+op <<(u8, int) -> u8;
+
+/// Return `true` if the first timestamp is earlier than or equals to the second.
+op <=(Instant, Instant) -> bool;
+
+op <=(int, f32) -> bool;
+
+op <=(int, float) -> bool;
+
+op <=(f32, int) -> bool;
+
+op <=(f32, f32) -> bool;
+
+op <=(float, int) -> bool;
+
+op <=(i128, i128) -> bool;
+
+op <=(i16, i16) -> bool;
+
+op <=(i32, i32) -> bool;
+
+op <=(i8, i8) -> bool;
+
+op <=(u128, u128) -> bool;
+
+op <=(u16, u16) -> bool;
+
+op <=(u32, u32) -> bool;
+
+op <=(u64, u64) -> bool;
+
+op <=(u8, u8) -> bool;
+
+/// Return `true` if two arrays are equal (i.e. all elements are equal and in the same order).
+///
+/// The operator `==` is used to compare elements and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [1, 2, 3, 4, 5];
+/// let z = [1, 2, 3, 4];
+///
+/// print(x == y); // prints true
+///
+/// print(x == z); // prints false
+/// ```
+op ==(Array, Array) -> bool;
+
+/// Return `true` if two object maps are equal (i.e. all property values are equal).
+///
+/// The operator `==` is used to compare property values and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let m1 = #{a:1, b:2, c:3};
+/// let m2 = #{a:1, b:2, c:3};
+/// let m3 = #{a:1, c:3};
+///
+/// print(m1 == m2); // prints true
+///
+/// print(m1 == m3); // prints false
+/// ```
+op ==(Map, Map) -> bool;
+
+/// Return `true` if two timestamps are equal.
+op ==(Instant, Instant) -> bool;
+
+op ==(int, f32) -> bool;
+
+op ==(int, float) -> bool;
+
+op ==(f32, int) -> bool;
+
+op ==(f32, f32) -> bool;
+
+op ==(float, int) -> bool;
+
+op ==(i128, i128) -> bool;
+
+op ==(i16, i16) -> bool;
+
+op ==(i32, i32) -> bool;
+
+op ==(i8, i8) -> bool;
+
+op ==(u128, u128) -> bool;
+
+op ==(u16, u16) -> bool;
+
+op ==(u32, u32) -> bool;
+
+op ==(u64, u64) -> bool;
+
+op ==(u8, u8) -> bool;
+
+/// Return `true` if the first timestamp is later than the second.
+op >(Instant, Instant) -> bool;
+
+op >(int, f32) -> bool;
+
+op >(int, float) -> bool;
+
+op >(f32, int) -> bool;
+
+op >(f32, f32) -> bool;
+
+op >(float, int) -> bool;
+
+op >(i128, i128) -> bool;
+
+op >(i16, i16) -> bool;
+
+op >(i32, i32) -> bool;
+
+op >(i8, i8) -> bool;
+
+op >(u128, u128) -> bool;
+
+op >(u16, u16) -> bool;
+
+op >(u32, u32) -> bool;
+
+op >(u64, u64) -> bool;
+
+op >(u8, u8) -> bool;
+
+/// Return `true` if the first timestamp is later than or equals to the second.
+op >=(Instant, Instant) -> bool;
+
+op >=(int, f32) -> bool;
+
+op >=(int, float) -> bool;
+
+op >=(f32, int) -> bool;
+
+op >=(f32, f32) -> bool;
+
+op >=(float, int) -> bool;
+
+op >=(i128, i128) -> bool;
+
+op >=(i16, i16) -> bool;
+
+op >=(i32, i32) -> bool;
+
+op >=(i8, i8) -> bool;
+
+op >=(u128, u128) -> bool;
+
+op >=(u16, u16) -> bool;
+
+op >=(u32, u32) -> bool;
+
+op >=(u64, u64) -> bool;
+
+op >=(u8, u8) -> bool;
+
+op >>(i128, int) -> i128;
+
+op >>(i16, int) -> i16;
+
+op >>(i32, int) -> i32;
+
+op >>(i8, int) -> i8;
+
+op >>(u128, int) -> u128;
+
+op >>(u16, int) -> u16;
+
+op >>(u32, int) -> u32;
+
+op >>(u64, int) -> u64;
+
+op >>(u8, int) -> u8;
+
+/// Return the natural number _e_.
+fn E() -> float;
+
+/// Return the number π.
+fn PI() -> float;
+
+op ^(i128, i128) -> i128;
+
+op ^(i16, i16) -> i16;
+
+op ^(i32, i32) -> i32;
+
+op ^(i8, i8) -> i8;
+
+op ^(u128, u128) -> u128;
+
+op ^(u16, u16) -> u16;
+
+op ^(u32, u32) -> u32;
+
+op ^(u64, u64) -> u64;
+
+op ^(u8, u8) -> u8;
+
+/// Return the absolute value of the number.
+fn abs(x: int) -> int;
+
+/// Return the absolute value of the floating-point number.
+fn abs(x: f32) -> f32;
+
+/// Return the absolute value of the floating-point number.
+fn abs(x: float) -> float;
+
+/// Return the absolute value of the number.
+fn abs(x: i128) -> i128;
+
+/// Return the absolute value of the number.
+fn abs(x: i16) -> i16;
+
+/// Return the absolute value of the number.
+fn abs(x: i32) -> i32;
+
+/// Return the absolute value of the number.
+fn abs(x: i8) -> i8;
+
+/// Return the arc-cosine of the floating-point number, in radians.
+fn acos(x: float) -> float;
+
+/// Return the arc-hyperbolic-cosine of the floating-point number, in radians.
+fn acosh(x: float) -> float;
+
+/// Return `true` if all elements in the array return `true` when applied a function named by `filter`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.all(|v| v > 3)); // prints false
+///
+/// print(x.all(|v| v > 1)); // prints true
+///
+/// print(x.all(|v, i| i > v)); // prints false
+/// ```
+fn all(array: Array, filter: String) -> bool;
+
+/// Return `true` if all elements in the array return `true` when applied the `filter` function.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.all(|v| v > 3)); // prints false
+///
+/// print(x.all(|v| v > 1)); // prints true
+///
+/// print(x.all(|v, i| i > v)); // prints false
+/// ```
+fn all(array: Array, filter: FnPtr) -> bool;
+
+/// Add all the elements of another array to the end of the array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+/// let y = [true, 'x'];
+///
+/// x.append(y);
+///
+/// print(x); // prints "[1, 2, 3, true, 'x']"
+/// ```
+fn append(array: Array, new_array: Array) -> ();
+
+/// Add another BLOB to the end of the BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob(5, 0x42);
+/// let b2 = blob(3, 0x11);
+///
+/// b1.push(b2);
+///
+/// print(b1); // prints "[4242424242111111]"
+/// ```
+fn append(blob1: Blob, blob2: Blob) -> ();
+
+/// Add a character (as UTF-8 encoded byte-stream) to the end of the BLOB
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// b.append('!');
+///
+/// print(b); // prints "[424242424221]"
+/// ```
+fn append(blob: Blob, character: char) -> ();
+
+/// Add a string (as UTF-8 encoded byte-stream) to the end of the BLOB
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// b.append("hello");
+///
+/// print(b); // prints "[424242424268656c 6c6f]"
+/// ```
+fn append(blob: Blob, string: String) -> ();
+
+/// Add a new byte `value` to the end of the BLOB.
+///
+/// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b.push(0x42);
+///
+/// print(b); // prints "[42]"
+/// ```
+fn append(blob: Blob, value: int) -> ();
+
+fn append(string: String, item: ?) -> ();
+
+fn append(string: String, utf8: Blob) -> ();
+
+/// Convert the BLOB into a string.
+///
+/// The byte stream must be valid UTF-8, otherwise an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// let x = b.as_string();
+///
+/// print(x); // prints "FFFFF"
+/// ```
+fn as_string(blob: Blob) -> String;
+
+/// Return the arc-sine of the floating-point number, in radians.
+fn asin(x: float) -> float;
+
+/// Return the arc-hyperbolic-sine of the floating-point number, in radians.
+fn asinh(x: float) -> float;
+
+/// Return the arc-tangent of the floating-point number, in radians.
+fn atan(x: float) -> float;
+
+/// Return the arc-tangent of the floating-point numbers `x` and `y`, in radians.
+fn atan(x: float, y: float) -> float;
+
+/// Return the arc-hyperbolic-tangent of the floating-point number, in radians.
+fn atanh(x: float) -> float;
+
+/// Return an iterator over all the bits in the number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits() {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int) -> Iterator<bool>;
+
+/// Return an iterator over the bits in the number starting from the specified `start` position.
+///
+/// If `start` < 0, position counts from the MSB (Most Significant Bit)>.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits(10) {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int, from: int) -> Iterator<bool>;
+
+/// Return an iterator over an exclusive range of bits in the number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits(10..24) {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int, range: Range<int>) -> Iterator<bool>;
+
+/// Return an iterator over an inclusive range of bits in the number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits(10..=23) {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int, range: RangeInclusive<int>) -> Iterator<bool>;
+
+/// Return an iterator over a portion of bits in the number.
+///
+/// * If `start` < 0, position counts from the MSB (Most Significant Bit)>.
+/// * If `len` ≤ 0, an empty iterator is returned.
+/// * If `start` position + `len` ≥ length of string, all bits of the number after the `start` position are iterated.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits(10, 8) {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int, from: int, len: int) -> Iterator<bool>;
+
+/// Return a new, empty BLOB.
+fn blob() -> Blob;
+
+/// Return a new BLOB of the specified length, filled with zeros.
+///
+/// If `len` ≤ 0, an empty BLOB is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(10);
+///
+/// print(b); // prints "[0000000000000000 0000]"
+/// ```
+fn blob(len: int) -> Blob;
+
+/// Return a new BLOB of the specified length, filled with copies of the initial `value`.
+///
+/// If `len` ≤ 0, an empty BLOB is returned.
+///
+/// Only the lower 8 bits of the initial `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(10, 0x42);
+///
+/// print(b); // prints "[4242424242424242 4242]"
+/// ```
+fn blob(len: int, value: int) -> Blob;
+
+/// Return the length of the string, in number of bytes used to store it in UTF-8 encoding.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// print(text.bytes); // prints 51
+/// ```
+fn bytes(string: String) -> int;
+
+/// Return the smallest whole number larger than or equals to the floating-point number.
+fn ceiling(x: float) -> float;
+
+/// Return an iterator over the characters in the string.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars() {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String) -> Iterator<char>;
+
+/// Return an iterator over the characters in the string starting from the `start` position.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars(2) {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String, from: int) -> Iterator<char>;
+
+/// Return an iterator over an exclusive range of characters in the string.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars(2..5) {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String, range: Range<int>) -> Iterator<char>;
+
+/// Return an iterator over an inclusive range of characters in the string.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars(2..=6) {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String, range: RangeInclusive<int>) -> Iterator<char>;
+
+/// Return an iterator over a portion of characters in the string.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, an empty iterator is returned.
+/// * If `len` ≤ 0, an empty iterator is returned.
+/// * If `start` position + `len` ≥ length of string, all characters of the string after the `start` position are iterated.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars(2, 4) {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String, start: int, len: int) -> Iterator<char>;
+
+/// Cut off the head of the array, leaving a tail of the specified length.
+///
+/// * If `len` ≤ 0, the array is cleared.
+/// * If `len` ≥ length of array, the array is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// x.chop(3);
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// x.chop(10);
+///
+/// print(x); // prints "[3, 4, 5]"
+/// ```
+fn chop(array: Array, len: int) -> ();
+
+/// Cut off the head of the BLOB, leaving a tail of the specified length.
+///
+/// * If `len` ≤ 0, the BLOB is cleared.
+/// * If `len` ≥ length of BLOB, the BLOB is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// b.chop(3);
+///
+/// print(b); // prints "[030405]"
+///
+/// b.chop(10);
+///
+/// print(b); // prints "[030405]"
+/// ```
+fn chop(blob: Blob, len: int) -> ();
+
+/// Clear the array.
+fn clear(array: Array) -> ();
+
+/// Clear the BLOB.
+fn clear(blob: Blob) -> ();
+
+/// Clear the object map.
+fn clear(map: Map) -> ();
+
+/// Clear the string, making it empty.
+fn clear(string: String) -> ();
+
+/// Return `true` if the array contains an element that equals `value`.
+///
+/// The operator `==` is used to compare elements with `value` and must be defined,
+/// otherwise `false` is assumed.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 4 in x {
+/// print("found!");
+/// }
+/// ```
+fn contains(array: Array, value: ?) -> bool;
+
+/// Return `true` if the BLOB contains a specified byte value.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.contains('h')); // prints true
+///
+/// print(text.contains('x')); // prints false
+/// ```
+fn contains(blob: Blob, value: int) -> bool;
+
+/// Returns `true` if the object map contains a specified property.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a: 1, b: 2, c: 3};
+///
+/// print(m.contains("b")); // prints true
+///
+/// print(m.contains("x")); // prints false
+/// ```
+fn contains(map: Map, property: String) -> bool;
+
+/// Return `true` if the range contains a specified value.
+fn contains(range: ExclusiveRange, value: int) -> bool;
+
+/// Return `true` if the range contains a specified value.
+fn contains(range: InclusiveRange, value: int) -> bool;
+
+/// Return `true` if the string contains a specified character.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.contains('h')); // prints true
+///
+/// print(text.contains('x')); // prints false
+/// ```
+fn contains(string: String, character: char) -> bool;
+
+/// Return `true` if the string contains a specified string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.contains("hello")); // prints true
+///
+/// print(text.contains("hey")); // prints false
+/// ```
+fn contains(string: String, match_string: String) -> bool;
+
+/// Return the cosine of the floating-point number in radians.
+fn cos(x: float) -> float;
+
+/// Return the hyperbolic cosine of the floating-point number in radians.
+fn cosh(x: float) -> float;
+
+/// Remove all characters from the string except those within an exclusive `range`.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.crop(2..8);
+///
+/// print(text); // prints "llo, w"
+/// ```
+fn crop(string: String, range: Range<int>) -> ();
+
+/// Remove all characters from the string except those within an inclusive `range`.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.crop(2..=8);
+///
+/// print(text); // prints "llo, wo"
+/// ```
+fn crop(string: String, range: RangeInclusive<int>) -> ();
+
+/// Remove all characters from the string except until the `start` position.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, the string is not modified.
+/// * If `start` ≥ length of string, the entire string is cleared.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.crop(5);
+///
+/// print(text); // prints ", world!"
+///
+/// text.crop(-3);
+///
+/// print(text); // prints "ld!"
+/// ```
+fn crop(string: String, start: int) -> ();
+
+/// Remove all characters from the string except those within a range.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, the entire string is cleared.
+/// * If `len` ≤ 0, the entire string is cleared.
+/// * If `start` position + `len` ≥ length of string, only the portion of the string after the `start` position is retained.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.crop(2, 8);
+///
+/// print(text); // prints "llo, wor"
+///
+/// text.crop(-5, 3);
+///
+/// print(text); // prints ", w"
+/// ```
+fn crop(string: String, start: int, len: int) -> ();
+
+/// Return the empty string.
+op debug() -> String;
+
+/// Convert the array into a string.
+op debug(Array) -> String;
+
+/// Convert the string into debug format.
+op debug(char) -> String;
+
+/// Convert the function pointer into a string in debug format.
+op debug(FnPtr) -> String;
+
+/// Convert the value of the `item` into a string in debug format.
+op debug(?) -> String;
+
+/// Convert the object map into a string.
+op debug(Map) -> String;
+
+/// Convert the value of `number` into a string.
+op debug(f32) -> String;
+
+/// Convert the value of `number` into a string.
+op debug(float) -> String;
+
+/// Convert the string into debug format.
+op debug(String) -> String;
+
+/// Convert the unit into a string in debug format.
+op debug(()) -> String;
+
+/// Convert the boolean value into a string in debug format.
+op debug(bool) -> String;
+
+/// Remove duplicated _consecutive_ elements from the array.
+///
+/// The operator `==` is used to compare elements and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 2, 2, 3, 4, 3, 3, 2, 1];
+///
+/// x.dedup();
+///
+/// print(x); // prints "[1, 2, 3, 4, 3, 2, 1]"
+/// ```
+fn dedup(array: Array) -> ();
+
+/// Remove duplicated _consecutive_ elements from the array that return `true` when applied a
+/// function named by `comparer`.
+///
+/// No element is removed if the correct `comparer` function does not exist.
+///
+/// # Function Parameters
+///
+/// * `element1`: copy of the current array element to compare
+/// * `element2`: copy of the next array element to compare
+///
+/// ## Return Value
+///
+/// `true` if `element1 == element2`, otherwise `false`.
+///
+/// # Example
+///
+/// ```rhai
+/// fn declining(a, b) { a >= b }
+///
+/// let x = [1, 2, 2, 2, 3, 1, 2, 3, 4, 3, 3, 2, 1];
+///
+/// x.dedup("declining");
+///
+/// print(x); // prints "[1, 2, 3, 4]"
+/// ```
+fn dedup(array: Array, comparer: String) -> ();
+
+/// Remove duplicated _consecutive_ elements from the array that return `true` when applied the
+/// `comparer` function.
+///
+/// No element is removed if the correct `comparer` function does not exist.
+///
+/// # Function Parameters
+///
+/// * `element1`: copy of the current array element to compare
+/// * `element2`: copy of the next array element to compare
+///
+/// ## Return Value
+///
+/// `true` if `element1 == element2`, otherwise `false`.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 2, 2, 3, 1, 2, 3, 4, 3, 3, 2, 1];
+///
+/// x.dedup(|a, b| a >= b);
+///
+/// print(x); // prints "[1, 2, 3, 4]"
+/// ```
+fn dedup(array: Array, comparer: FnPtr) -> ();
+
+/// Remove all elements in the array that returns `true` when applied a function named by `filter`
+/// and return them as a new array.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn small(x) { x < 3 }
+///
+/// fn screen(x, i) { x + i > 5 }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain("small");
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// print(y); // prints "[1, 2]"
+///
+/// let z = x.drain("screen");
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, filter: String) -> Array;
+
+/// Remove all elements in the array that returns `true` when applied the `filter` function and
+/// return them as a new array.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain(|v| v < 3);
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// print(y); // prints "[1, 2]"
+///
+/// let z = x.drain(|v, i| v + i > 5);
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, filter: FnPtr) -> Array;
+
+/// Remove all elements in the array within an exclusive `range` and return them as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain(1..3);
+///
+/// print(x); // prints "[1, 4, 5]"
+///
+/// print(y); // prints "[2, 3]"
+///
+/// let z = x.drain(2..3);
+///
+/// print(x); // prints "[1, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, range: Range<int>) -> Array;
+
+/// Remove all elements in the array within an inclusive `range` and return them as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain(1..=2);
+///
+/// print(x); // prints "[1, 4, 5]"
+///
+/// print(y); // prints "[2, 3]"
+///
+/// let z = x.drain(2..=2);
+///
+/// print(x); // prints "[1, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, range: RangeInclusive<int>) -> Array;
+
+/// Remove all bytes in the BLOB within an exclusive `range` and return them as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.drain(1..3);
+///
+/// print(b1); // prints "[010405]"
+///
+/// print(b2); // prints "[0203]"
+///
+/// let b3 = b1.drain(2..3);
+///
+/// print(b1); // prints "[0104]"
+///
+/// print(b3); // prints "[05]"
+/// ```
+fn drain(blob: Blob, range: Range<int>) -> Blob;
+
+/// Remove all bytes in the BLOB within an inclusive `range` and return them as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.drain(1..=2);
+///
+/// print(b1); // prints "[010405]"
+///
+/// print(b2); // prints "[0203]"
+///
+/// let b3 = b1.drain(2..=2);
+///
+/// print(b1); // prints "[0104]"
+///
+/// print(b3); // prints "[05]"
+/// ```
+fn drain(blob: Blob, range: RangeInclusive<int>) -> Blob;
+
+/// Remove all elements within a portion of the array and return them as a new array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, no element is removed and an empty array is returned.
+/// * If `len` ≤ 0, no element is removed and an empty array is returned.
+/// * If `start` position + `len` ≥ length of array, entire portion of the array after the `start` position is removed and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain(1, 2);
+///
+/// print(x); // prints "[1, 4, 5]"
+///
+/// print(y); // prints "[2, 3]"
+///
+/// let z = x.drain(-1, 1);
+///
+/// print(x); // prints "[1, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, start: int, len: int) -> Array;
+
+/// Remove all bytes within a portion of the BLOB and return them as a new BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, nothing is removed and an empty BLOB is returned.
+/// * If `len` ≤ 0, nothing is removed and an empty BLOB is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is removed and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.drain(1, 2);
+///
+/// print(b1); // prints "[010405]"
+///
+/// print(b2); // prints "[0203]"
+///
+/// let b3 = b1.drain(-1, 1);
+///
+/// print(b3); // prints "[0104]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(blob: Blob, start: int, len: int) -> Blob;
+
+/// Return the number of seconds between the current system time and the timestamp.
+///
+/// # Example
+///
+/// ```rhai
+/// let now = timestamp();
+///
+/// sleep(10.0); // sleep for 10 seconds
+///
+/// print(now.elapsed); // prints 10.???
+/// ```
+fn elapsed(timestamp: Instant) -> RhaiResult;
+
+/// Return the end of the exclusive range.
+fn end(range: ExclusiveRange) -> int;
+
+/// Return the end of the inclusive range.
+fn end(range: InclusiveRange) -> int;
+
+/// Return `true` if the string ends with a specified string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.ends_with("world!")); // prints true
+///
+/// print(text.ends_with("hello")); // prints false
+/// ```
+fn ends_with(string: String, match_string: String) -> bool;
+
+/// Return the exponential of the floating-point number.
+fn exp(x: float) -> float;
+
+/// Copy an exclusive range of the array and return it as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// print(x.extract(1..3)); // prints "[2, 3]"
+///
+/// print(x); // prints "[1, 2, 3, 4, 5]"
+/// ```
+fn extract(array: Array, range: Range<int>) -> Array;
+
+/// Copy an inclusive range of the array and return it as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// print(x.extract(1..=3)); // prints "[2, 3, 4]"
+///
+/// print(x); // prints "[1, 2, 3, 4, 5]"
+/// ```
+fn extract(array: Array, range: RangeInclusive<int>) -> Array;
+
+/// Copy a portion of the array beginning at the `start` position till the end and return it as
+/// a new array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, the entire array is copied and returned.
+/// * If `start` ≥ length of array, an empty array is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// print(x.extract(2)); // prints "[3, 4, 5]"
+///
+/// print(x.extract(-3)); // prints "[3, 4, 5]"
+///
+/// print(x); // prints "[1, 2, 3, 4, 5]"
+/// ```
+fn extract(array: Array, start: int) -> Array;
+
+/// Copy an exclusive `range` of the BLOB and return it as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.extract(1..3)); // prints "[0203]"
+///
+/// print(b); // prints "[0102030405]"
+/// ```
+fn extract(blob: Blob, range: Range<int>) -> Blob;
+
+/// Copy an inclusive `range` of the BLOB and return it as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.extract(1..=3)); // prints "[020304]"
+///
+/// print(b); // prints "[0102030405]"
+/// ```
+fn extract(blob: Blob, range: RangeInclusive<int>) -> Blob;
+
+/// Copy a portion of the BLOB beginning at the `start` position till the end and return it as
+/// a new BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, the entire BLOB is copied and returned.
+/// * If `start` ≥ length of BLOB, an empty BLOB is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.extract(2)); // prints "[030405]"
+///
+/// print(b.extract(-3)); // prints "[030405]"
+///
+/// print(b); // prints "[0102030405]"
+/// ```
+fn extract(blob: Blob, start: int) -> Blob;
+
+/// Copy a portion of the array and return it as a new array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, an empty array is returned.
+/// * If `len` ≤ 0, an empty array is returned.
+/// * If `start` position + `len` ≥ length of array, entire portion of the array after the `start` position is copied and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// print(x.extract(1, 3)); // prints "[2, 3, 4]"
+///
+/// print(x.extract(-3, 2)); // prints "[3, 4]"
+///
+/// print(x); // prints "[1, 2, 3, 4, 5]"
+/// ```
+fn extract(array: Array, start: int, len: int) -> Array;
+
+/// Copy a portion of the BLOB and return it as a new BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, an empty BLOB is returned.
+/// * If `len` ≤ 0, an empty BLOB is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is copied and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.extract(1, 3)); // prints "[020303]"
+///
+/// print(b.extract(-3, 2)); // prints "[0304]"
+///
+/// print(b); // prints "[0102030405]"
+/// ```
+fn extract(blob: Blob, start: int, len: int) -> Blob;
+
+/// Add all property values of another object map into the object map.
+/// Only properties that do not originally exist in the object map are added.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+/// let n = #{a: 42, d:0};
+///
+/// m.fill_with(n);
+///
+/// print(m); // prints "#{a:1, b:2, c:3, d:0}"
+/// ```
+fn fill_with(map: Map, map2: Map) -> ();
+
+/// Iterate through all the elements in the array, applying a `filter` function to each element
+/// in turn, and return a copy of all elements (in order) that return `true` as a new array.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.filter(|v| v >= 3);
+///
+/// print(y); // prints "[3, 4, 5]"
+///
+/// let y = x.filter(|v, i| v * i >= 10);
+///
+/// print(y); // prints "[12, 20]"
+/// ```
+fn filter(array: Array, filter: FnPtr) -> Array;
+
+/// Iterate through all the elements in the array, applying a function named by `filter` to each
+/// element in turn, and return a copy of all elements (in order) that return `true` as a new array.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn screen(x, i) { x * i >= 10 }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.filter("is_odd");
+///
+/// print(y); // prints "[1, 3, 5]"
+///
+/// let y = x.filter("screen");
+///
+/// print(y); // prints "[12, 20]"
+/// ```
+fn filter(array: Array, filter_func: String) -> Array;
+
+/// Return the largest whole number less than or equals to the floating-point number.
+fn floor(x: float) -> float;
+
+/// Return the fractional part of the floating-point number.
+fn fraction(x: float) -> float;
+
+/// Get a copy of the element at the `index` position in the array.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` < -length of array, `()` is returned.
+/// * If `index` ≥ length of array, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// print(x.get(0)); // prints 1
+///
+/// print(x.get(-1)); // prints 3
+///
+/// print(x.get(99)); // prints empty (for '()')
+/// ```
+fn get(array: Array, index: int) -> ?;
+
+/// Get the byte value at the `index` position in the BLOB.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last element).
+/// * If `index` < -length of BLOB, zero is returned.
+/// * If `index` ≥ length of BLOB, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.get(0)); // prints 1
+///
+/// print(b.get(-1)); // prints 5
+///
+/// print(b.get(99)); // prints 0
+/// ```
+fn get(blob: Blob, index: int) -> int;
+
+/// Get the value of the `property` in the object map and return a copy.
+///
+/// If `property` does not exist in the object map, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a: 1, b: 2, c: 3};
+///
+/// print(m.get("b")); // prints 2
+///
+/// print(m.get("x")); // prints empty (for '()')
+/// ```
+fn get(map: Map, property: String) -> ?;
+
+/// Get the character at the `index` position in the string.
+///
+/// * If `index` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `index` < -length of string, zero is returned.
+/// * If `index` ≥ length of string, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.get(0)); // prints 'h'
+///
+/// print(text.get(-1)); // prints '!'
+///
+/// print(text.get(99)); // prints empty (for '()')'
+/// ```
+fn get(string: String, index: int) -> ?;
+
+/// Return an iterator over all the bits in the number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits {
+/// print(bit);
+/// }
+/// ```
+fn get bits(value: int) -> Iterator<bool>;
+
+/// Return the length of the string, in number of bytes used to store it in UTF-8 encoding.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// print(text.bytes); // prints 51
+/// ```
+fn get bytes(string: String) -> int;
+
+/// Return the smallest whole number larger than or equals to the floating-point number.
+fn get ceiling(x: float) -> float;
+
+/// Return an iterator over all the characters in the string.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars {
+/// print(ch);
+/// }
+/// ```
+fn get chars(string: String) -> Iterator<char>;
+
+/// Return the number of seconds between the current system time and the timestamp.
+///
+/// # Example
+///
+/// ```rhai
+/// let now = timestamp();
+///
+/// sleep(10.0); // sleep for 10 seconds
+///
+/// print(now.elapsed); // prints 10.???
+/// ```
+fn get elapsed(timestamp: Instant) -> RhaiResult;
+
+/// Return the end of the exclusive range.
+fn get end(range: ExclusiveRange) -> int;
+
+/// Return the end of the inclusive range.
+fn get end(range: InclusiveRange) -> int;
+
+/// Return the largest whole number less than or equals to the floating-point number.
+fn get floor(x: float) -> float;
+
+/// Return the fractional part of the floating-point number.
+fn get fraction(x: float) -> float;
+
+/// Return the integral part of the floating-point number.
+fn get int(x: float) -> float;
+
+/// Return `true` if the function is an anonymous function.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = |x| x * 2;
+///
+/// print(f.is_anonymous); // prints true
+/// ```
+fn get is_anonymous(fn_ptr: FnPtr) -> bool;
+
+/// Return true if the array is empty.
+fn get is_empty(array: Array) -> bool;
+
+/// Return true if the BLOB is empty.
+fn get is_empty(blob: Blob) -> bool;
+
+/// Return true if the range contains no items.
+fn get is_empty(range: ExclusiveRange) -> bool;
+
+/// Return true if the range contains no items.
+fn get is_empty(range: InclusiveRange) -> bool;
+
+/// Return true if the string is empty.
+fn get is_empty(string: String) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: int) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: i128) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: i16) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: i32) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: i8) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u128) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u16) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u32) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u64) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u8) -> bool;
+
+/// Return `true` if the range is exclusive.
+fn get is_exclusive(range: ExclusiveRange) -> bool;
+
+/// Return `true` if the range is exclusive.
+fn get is_exclusive(range: InclusiveRange) -> bool;
+
+/// Return `true` if the floating-point number is finite.
+fn get is_finite(x: float) -> bool;
+
+/// Return `true` if the range is inclusive.
+fn get is_inclusive(range: ExclusiveRange) -> bool;
+
+/// Return `true` if the range is inclusive.
+fn get is_inclusive(range: InclusiveRange) -> bool;
+
+/// Return `true` if the floating-point number is infinite.
+fn get is_infinite(x: float) -> bool;
+
+/// Return `true` if the floating-point number is `NaN` (Not A Number).
+fn get is_nan(x: float) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: int) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: i128) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: i16) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: i32) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: i8) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u128) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u16) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u32) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u64) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u8) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: int) -> bool;
+
+/// Return true if the floating-point number is zero.
+fn get is_zero(x: f32) -> bool;
+
+/// Return true if the floating-point number is zero.
+fn get is_zero(x: float) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: i128) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: i16) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: i32) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: i8) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u128) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u16) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u32) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u64) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u8) -> bool;
+
+/// Number of elements in the array.
+fn get len(array: Array) -> int;
+
+/// Return the length of the BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(10, 0x42);
+///
+/// print(b); // prints "[4242424242424242 4242]"
+///
+/// print(b.len()); // prints 10
+/// ```
+fn get len(blob: Blob) -> int;
+
+/// Return the length of the string, in number of characters.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// print(text.len); // prints 17
+/// ```
+fn get len(string: String) -> int;
+
+/// Return the name of the function.
+///
+/// # Example
+///
+/// ```rhai
+/// fn double(x) { x * 2 }
+///
+/// let f = Fn("double");
+///
+/// print(f.name); // prints "double"
+/// ```
+fn get name(fn_ptr: FnPtr) -> String;
+
+/// Return the nearest whole number closest to the floating-point number.
+/// Rounds away from zero.
+fn get round(x: float) -> float;
+
+/// Return the start of the exclusive range.
+fn get start(range: ExclusiveRange) -> int;
+
+/// Return the start of the inclusive range.
+fn get start(range: InclusiveRange) -> int;
+
+/// Return the _tag_ of a `Dynamic` value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// x.tag = 42;
+///
+/// print(x.tag); // prints 42
+/// ```
+fn get tag(value: ?) -> int;
+
+/// Return `true` if the specified `bit` in the number is set.
+///
+/// If `bit` < 0, position counts from the MSB (Most Significant Bit).
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// print(x.get_bit(5)); // prints false
+///
+/// print(x.get_bit(6)); // prints true
+///
+/// print(x.get_bit(-48)); // prints true on 64-bit
+/// ```
+fn get_bit(value: int, bit: int) -> bool;
+
+/// Return an exclusive range of bits in the number as a new number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// print(x.get_bits(5..10)); // print 18
+/// ```
+fn get_bits(value: int, range: Range<int>) -> int;
+
+/// Return an inclusive range of bits in the number as a new number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// print(x.get_bits(5..=9)); // print 18
+/// ```
+fn get_bits(value: int, range: RangeInclusive<int>) -> int;
+
+/// Return a portion of bits in the number as a new number.
+///
+/// * If `start` < 0, position counts from the MSB (Most Significant Bit).
+/// * If `bits` ≤ 0, zero is returned.
+/// * If `start` position + `bits` ≥ total number of bits, the bits after the `start` position are returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// print(x.get_bits(5, 8)); // print 18
+/// ```
+fn get_bits(value: int, start: int, bits: int) -> int;
+
+fn get_fn_metadata_list() -> Array;
+
+fn get_fn_metadata_list(name: String) -> Array;
+
+fn get_fn_metadata_list(name: String, params: int) -> Array;
+
+/// Return the hypotenuse of a triangle with sides `x` and `y`.
+fn hypot(x: float, y: float) -> float;
+
+/// Iterate through all the elements in the array, applying a function named by `filter` to each
+/// element in turn, and return the index of the first element that returns `true`.
+/// If no element returns `true`, `-1` is returned.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn is_special(x) { x > 3 }
+///
+/// fn is_dumb(x) { x > 8 }
+///
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of("is_special")); // prints 3
+///
+/// print(x.index_of("is_dumb")); // prints -1
+/// ```
+fn index_of(array: Array, filter: String) -> int;
+
+/// Iterate through all the elements in the array, applying a `filter` function to each element
+/// in turn, and return the index of the first element that returns `true`.
+/// If no element returns `true`, `-1` is returned.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of(|v| v > 3)); // prints 3: 4 > 3
+///
+/// print(x.index_of(|v| v > 8)); // prints -1: nothing is > 8
+///
+/// print(x.index_of(|v, i| v * i > 20)); // prints 7: 4 * 7 > 20
+/// ```
+fn index_of(array: Array, filter: FnPtr) -> int;
+
+/// Find the first element in the array that equals a particular `value` and return its index.
+/// If no element equals `value`, `-1` is returned.
+///
+/// The operator `==` is used to compare elements with `value` and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of(4)); // prints 3 (first index)
+///
+/// print(x.index_of(9)); // prints -1
+///
+/// print(x.index_of("foo")); // prints -1: strings do not equal numbers
+/// ```
+fn index_of(array: Array, value: ?) -> int;
+
+/// Find the specified `character` in the string and return the first index where it is found.
+/// If the `character` is not found, `-1` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.index_of('l')); // prints 2 (first index)
+///
+/// print(text.index_of('x')); // prints -1
+/// ```
+fn index_of(string: String, character: char) -> int;
+
+/// Find the specified `character` in the string and return the first index where it is found.
+/// If the `character` is not found, `-1` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// print(text.index_of("ll")); // prints 2 (first index)
+///
+/// print(text.index_of("xx:)); // prints -1
+/// ```
+fn index_of(string: String, find_string: String) -> int;
+
+/// Iterate through all the elements in the array, starting from a particular `start` position,
+/// applying a function named by `filter` to each element in turn, and return the index of the
+/// first element that returns `true`. If no element returns `true`, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, `-1` is returned.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn plural(x) { x > 1 }
+///
+/// fn singular(x) { x < 2 }
+///
+/// fn screen(x, i) { x * i > 20 }
+///
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of("plural", 3)); // prints 5: 2 > 1
+///
+/// print(x.index_of("singular", 9)); // prints -1: nothing < 2 past index 9
+///
+/// print(x.index_of("plural", 15)); // prints -1: nothing found past end of array
+///
+/// print(x.index_of("plural", -5)); // prints 9: -5 = start from index 8
+///
+/// print(x.index_of("plural", -99)); // prints 1: -99 = start from beginning
+///
+/// print(x.index_of("screen", 8)); // prints 10: 3 * 10 > 20
+/// ```
+fn index_of(array: Array, filter: String, start: int) -> int;
+
+/// Iterate through all the elements in the array, starting from a particular `start` position,
+/// applying a `filter` function to each element in turn, and return the index of the first
+/// element that returns `true`. If no element returns `true`, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, `-1` is returned.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of(|v| v > 1, 3)); // prints 5: 2 > 1
+///
+/// print(x.index_of(|v| v < 2, 9)); // prints -1: nothing < 2 past index 9
+///
+/// print(x.index_of(|v| v > 1, 15)); // prints -1: nothing found past end of array
+///
+/// print(x.index_of(|v| v > 1, -5)); // prints 9: -5 = start from index 8
+///
+/// print(x.index_of(|v| v > 1, -99)); // prints 1: -99 = start from beginning
+///
+/// print(x.index_of(|v, i| v * i > 20, 8)); // prints 10: 3 * 10 > 20
+/// ```
+fn index_of(array: Array, filter: FnPtr, start: int) -> int;
+
+/// Find the first element in the array, starting from a particular `start` position, that
+/// equals a particular `value` and return its index. If no element equals `value`, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, `-1` is returned.
+///
+/// The operator `==` is used to compare elements with `value` and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of(4, 2)); // prints 3
+///
+/// print(x.index_of(4, 5)); // prints 7
+///
+/// print(x.index_of(4, 15)); // prints -1: nothing found past end of array
+///
+/// print(x.index_of(4, -5)); // prints 11: -5 = start from index 8
+///
+/// print(x.index_of(9, 1)); // prints -1: nothing equals 9
+///
+/// print(x.index_of("foo", 1)); // prints -1: strings do not equal numbers
+/// ```
+fn index_of(array: Array, value: ?, start: int) -> int;
+
+/// Find the specified `character` in the string, starting from the specified `start` position,
+/// and return the first index where it is found.
+/// If the `character` is not found, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, `-1` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.index_of('l', 5)); // prints 10 (first index after 5)
+///
+/// print(text.index_of('o', -7)); // prints 8
+///
+/// print(text.index_of('x', 0)); // prints -1
+/// ```
+fn index_of(string: String, character: char, start: int) -> int;
+
+/// Find the specified sub-string in the string, starting from the specified `start` position,
+/// and return the first index where it is found.
+/// If the sub-string is not found, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, `-1` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// print(text.index_of("ll", 5)); // prints 16 (first index after 5)
+///
+/// print(text.index_of("ll", -15)); // prints 16
+///
+/// print(text.index_of("xx", 0)); // prints -1
+/// ```
+fn index_of(string: String, find_string: String, start: int) -> int;
+
+/// Add a new element into the array at a particular `index` position.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` < -length of array, the element is added to the beginning of the array.
+/// * If `index` ≥ length of array, the element is appended to the end of the array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// x.insert(0, "hello");
+///
+/// x.insert(2, true);
+///
+/// x.insert(-2, 42);
+///
+/// print(x); // prints ["hello", 1, true, 2, 42, 3]
+/// ```
+fn insert(array: Array, index: int, item: ?) -> ();
+
+/// Add a byte `value` to the BLOB at a particular `index` position.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `index` < -length of BLOB, the byte value is added to the beginning of the BLOB.
+/// * If `index` ≥ length of BLOB, the byte value is appended to the end of the BLOB.
+///
+/// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// b.insert(2, 0x18);
+///
+/// print(b); // prints "[4242184242]"
+/// ```
+fn insert(blob: Blob, index: int, value: int) -> ();
+
+/// Return the integral part of the floating-point number.
+fn int(x: float) -> float;
+
+/// Return `true` if the function is an anonymous function.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = |x| x * 2;
+///
+/// print(f.is_anonymous); // prints true
+/// ```
+fn is_anonymous(fn_ptr: FnPtr) -> bool;
+
+/// Return true if the array is empty.
+fn is_empty(array: Array) -> bool;
+
+/// Return true if the BLOB is empty.
+fn is_empty(blob: Blob) -> bool;
+
+/// Return true if the map is empty.
+fn is_empty(map: Map) -> bool;
+
+/// Return true if the range contains no items.
+fn is_empty(range: ExclusiveRange) -> bool;
+
+/// Return true if the range contains no items.
+fn is_empty(range: InclusiveRange) -> bool;
+
+/// Return true if the string is empty.
+fn is_empty(string: String) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: int) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: i128) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: i16) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: i32) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: i8) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u128) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u16) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u32) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u64) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u8) -> bool;
+
+/// Return `true` if the range is exclusive.
+fn is_exclusive(range: ExclusiveRange) -> bool;
+
+/// Return `true` if the range is exclusive.
+fn is_exclusive(range: InclusiveRange) -> bool;
+
+/// Return `true` if the floating-point number is finite.
+fn is_finite(x: float) -> bool;
+
+/// Return `true` if the range is inclusive.
+fn is_inclusive(range: ExclusiveRange) -> bool;
+
+/// Return `true` if the range is inclusive.
+fn is_inclusive(range: InclusiveRange) -> bool;
+
+/// Return `true` if the floating-point number is infinite.
+fn is_infinite(x: float) -> bool;
+
+/// Return `true` if the floating-point number is `NaN` (Not A Number).
+fn is_nan(x: float) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: int) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: i128) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: i16) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: i32) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: i8) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u128) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u16) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u32) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u64) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u8) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: int) -> bool;
+
+/// Return true if the floating-point number is zero.
+fn is_zero(x: f32) -> bool;
+
+/// Return true if the floating-point number is zero.
+fn is_zero(x: float) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: i128) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: i16) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: i32) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: i8) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u128) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u16) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u32) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u64) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u8) -> bool;
+
+/// Return an array with all the property names in the object map.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// print(m.keys()); // prints ["a", "b", "c"]
+/// ```
+fn keys(map: Map) -> Array;
+
+/// Number of elements in the array.
+fn len(array: Array) -> int;
+
+/// Return the length of the BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(10, 0x42);
+///
+/// print(b); // prints "[4242424242424242 4242]"
+///
+/// print(b.len()); // prints 10
+/// ```
+fn len(blob: Blob) -> int;
+
+/// Return the number of properties in the object map.
+fn len(map: Map) -> int;
+
+/// Return the length of the string, in number of characters.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// print(text.len); // prints 17
+/// ```
+fn len(string: String) -> int;
+
+/// Return the natural log of the floating-point number.
+fn ln(x: float) -> float;
+
+/// Return the log of the floating-point number with base 10.
+fn log(x: float) -> float;
+
+/// Return the log of the floating-point number with `base`.
+fn log(x: float, base: float) -> float;
+
+/// Convert the character to lower-case.
+///
+/// # Example
+///
+/// ```rhai
+/// let ch = 'A';
+///
+/// ch.make_lower();
+///
+/// print(ch); // prints 'a'
+/// ```
+fn make_lower(character: char) -> ();
+
+/// Convert the string to all lower-case.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "HELLO, WORLD!"
+///
+/// text.make_lower();
+///
+/// print(text); // prints "hello, world!";
+/// ```
+fn make_lower(string: String) -> ();
+
+/// Convert the character to upper-case.
+///
+/// # Example
+///
+/// ```rhai
+/// let ch = 'a';
+///
+/// ch.make_upper();
+///
+/// print(ch); // prints 'A'
+/// ```
+fn make_upper(character: char) -> ();
+
+/// Convert the string to all upper-case.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!"
+///
+/// text.make_upper();
+///
+/// print(text); // prints "HELLO, WORLD!";
+/// ```
+fn make_upper(string: String) -> ();
+
+/// Iterate through all the elements in the array, applying a function named by `mapper` to each
+/// element in turn, and return the results as a new array.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `mapper` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn square(x) { x * x }
+///
+/// fn multiply(x, i) { x * i }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.map("square");
+///
+/// print(y); // prints "[1, 4, 9, 16, 25]"
+///
+/// let y = x.map("multiply");
+///
+/// print(y); // prints "[0, 2, 6, 12, 20]"
+/// ```
+fn map(array: Array, mapper: String) -> Array;
+
+/// Iterate through all the elements in the array, applying a `mapper` function to each element
+/// in turn, and return the results as a new array.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.map(|v| v * v);
+///
+/// print(y); // prints "[1, 4, 9, 16, 25]"
+///
+/// let y = x.map(|v, i| v * i);
+///
+/// print(y); // prints "[0, 2, 6, 12, 20]"
+/// ```
+fn map(array: Array, mapper: FnPtr) -> Array;
+
+/// Add all property values of another object map into the object map.
+/// Existing property values of the same names are replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+/// let n = #{a: 42, d:0};
+///
+/// m.mixin(n);
+///
+/// print(m); // prints "#{a:42, b:2, c:3, d:0}"
+/// ```
+fn mixin(map: Map, map2: Map) -> ();
+
+/// Return the name of the function.
+///
+/// # Example
+///
+/// ```rhai
+/// fn double(x) { x * 2 }
+///
+/// let f = Fn("double");
+///
+/// print(f.name); // prints "double"
+/// ```
+fn name(fn_ptr: FnPtr) -> String;
+
+/// Pad the array to at least the specified length with copies of a specified element.
+///
+/// If `len` ≤ length of array, no padding is done.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// x.pad(5, 42);
+///
+/// print(x); // prints "[1, 2, 3, 42, 42]"
+///
+/// x.pad(3, 123);
+///
+/// print(x); // prints "[1, 2, 3, 42, 42]"
+/// ```
+fn pad(array: Array, len: int, item: ?) -> ();
+
+/// Pad the BLOB to at least the specified length with copies of a specified byte `value`.
+///
+/// If `len` ≤ length of BLOB, no padding is done.
+///
+/// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(3, 0x42);
+///
+/// b.pad(5, 0x18)
+///
+/// print(b); // prints "[4242421818]"
+///
+/// b.pad(3, 0xab)
+///
+/// print(b); // prints "[4242421818]"
+/// ```
+fn pad(blob: Blob, len: int, value: int) -> ();
+
+/// Pad the string to at least the specified number of characters with the specified `character`.
+///
+/// If `len` ≤ length of string, no padding is done.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello";
+///
+/// text.pad(8, '!');
+///
+/// print(text); // prints "hello!!!"
+///
+/// text.pad(5, '*');
+///
+/// print(text); // prints "hello!!!"
+/// ```
+fn pad(string: String, len: int, character: char) -> ();
+
+/// Pad the string to at least the specified number of characters with the specified string.
+///
+/// If `len` ≤ length of string, no padding is done.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello";
+///
+/// text.pad(10, "(!)");
+///
+/// print(text); // prints "hello(!)(!)"
+///
+/// text.pad(8, '***');
+///
+/// print(text); // prints "hello(!)(!)"
+/// ```
+fn pad(string: String, len: int, padding: String) -> ();
+
+/// Parse the bytes within an exclusive `range` in the BLOB as a `FLOAT`
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_be_float(blob: Blob, range: Range<int>) -> float;
+
+/// Parse the bytes within an inclusive `range` in the BLOB as a `FLOAT`
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_be_float(blob: Blob, range: RangeInclusive<int>) -> float;
+
+/// Parse the bytes beginning at the `start` position in the BLOB as a `FLOAT`
+/// in big-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in range < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in range > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_be_float(blob: Blob, start: int, len: int) -> float;
+
+/// Parse the bytes within an exclusive `range` in the BLOB as an `INT`
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_be_int(1..3); // parse two bytes
+///
+/// print(x.to_hex()); // prints "02030000...00"
+/// ```
+fn parse_be_int(blob: Blob, range: Range<int>) -> int;
+
+/// Parse the bytes within an inclusive `range` in the BLOB as an `INT`
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_be_int(1..=3); // parse three bytes
+///
+/// print(x.to_hex()); // prints "0203040000...00"
+/// ```
+fn parse_be_int(blob: Blob, range: RangeInclusive<int>) -> int;
+
+/// Parse the bytes beginning at the `start` position in the BLOB as an `INT`
+/// in big-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in range < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in range > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_be_int(1, 2);
+///
+/// print(x.to_hex()); // prints "02030000...00"
+/// ```
+fn parse_be_int(blob: Blob, start: int, len: int) -> int;
+
+/// Parse a string into a floating-point number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = parse_int("123.456");
+///
+/// print(x); // prints 123.456
+/// ```
+fn parse_float(string: String) -> float;
+
+/// Parse a string into an integer number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = parse_int("123");
+///
+/// print(x); // prints 123
+/// ```
+fn parse_int(string: String) -> int;
+
+/// Parse a string into an integer number of the specified `radix`.
+///
+/// `radix` must be between 2 and 36.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = parse_int("123");
+///
+/// print(x); // prints 123
+///
+/// let y = parse_int("123abc", 16);
+///
+/// print(y); // prints 1194684 (0x123abc)
+/// ```
+fn parse_int(string: String, radix: int) -> int;
+
+/// Parse a JSON string into a value.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = parse_json(`{"a":1, "b":2, "c":3}`);
+///
+/// print(m); // prints #{"a":1, "b":2, "c":3}
+/// ```
+fn parse_json(json: String) -> ?;
+
+/// Parse the bytes within an exclusive `range` in the BLOB as a `FLOAT`
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_le_float(blob: Blob, range: Range<int>) -> float;
+
+/// Parse the bytes within an inclusive `range` in the BLOB as a `FLOAT`
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_le_float(blob: Blob, range: RangeInclusive<int>) -> float;
+
+/// Parse the bytes beginning at the `start` position in the BLOB as a `FLOAT`
+/// in little-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in range < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in range > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_le_float(blob: Blob, start: int, len: int) -> float;
+
+/// Parse the bytes within an exclusive `range` in the BLOB as an `INT`
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_le_int(1..3); // parse two bytes
+///
+/// print(x.to_hex()); // prints "0302"
+/// ```
+fn parse_le_int(blob: Blob, range: Range<int>) -> int;
+
+/// Parse the bytes within an inclusive `range` in the BLOB as an `INT`
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_le_int(1..=3); // parse three bytes
+///
+/// print(x.to_hex()); // prints "040302"
+/// ```
+fn parse_le_int(blob: Blob, range: RangeInclusive<int>) -> int;
+
+/// Parse the bytes beginning at the `start` position in the BLOB as an `INT`
+/// in little-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in range < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in range > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_le_int(1, 2);
+///
+/// print(x.to_hex()); // prints "0302"
+/// ```
+fn parse_le_int(blob: Blob, start: int, len: int) -> int;
+
+/// Remove the last element from the array and return it.
+///
+/// If the array is empty, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// print(x.pop()); // prints 3
+///
+/// print(x); // prints "[1, 2]"
+/// ```
+fn pop(array: Array) -> ?;
+
+/// Remove the last byte from the BLOB and return it.
+///
+/// If the BLOB is empty, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.pop()); // prints 5
+///
+/// print(b); // prints "[01020304]"
+/// ```
+fn pop(blob: Blob) -> int;
+
+/// Remove the last character from the string and return it.
+///
+/// If the string is empty, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.pop()); // prints '!'
+///
+/// print(text); // prints "hello, world"
+/// ```
+fn pop(string: String) -> ?;
+
+/// Remove a specified number of characters from the end of the string and return it as a
+/// new string.
+///
+/// * If `len` ≤ 0, the string is not modified and an empty string is returned.
+/// * If `len` ≥ length of string, the string is cleared and the entire string returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.pop(4)); // prints "rld!"
+///
+/// print(text); // prints "hello, wo"
+/// ```
+fn pop(string: String, len: int) -> String;
+
+/// Return the empty string.
+op print() -> String;
+
+/// Convert the array into a string.
+op print(Array) -> String;
+
+/// Return the character into a string.
+op print(char) -> String;
+
+/// Convert the value of the `item` into a string.
+op print(?) -> String;
+
+/// Convert the object map into a string.
+op print(Map) -> String;
+
+/// Convert the value of `number` into a string.
+op print(f32) -> String;
+
+/// Convert the value of `number` into a string.
+op print(float) -> String;
+
+/// Return the `string`.
+op print(String) -> String;
+
+/// Return the empty string.
+op print(()) -> String;
+
+/// Return the boolean value into a string.
+op print(bool) -> String;
+
+/// Add a new element, which is not another array, to the end of the array.
+///
+/// If `item` is `Array`, then `append` is more specific and will be called instead.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// x.push("hello");
+///
+/// print(x); // prints [1, 2, 3, "hello"]
+/// ```
+fn push(array: Array, item: ?) -> ();
+
+/// Add a new byte `value` to the end of the BLOB.
+///
+/// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b.push(0x42);
+///
+/// print(b); // prints "[42]"
+/// ```
+fn push(blob: Blob, value: int) -> ();
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i128, to: i128) -> Iterator<i128>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i16, to: i16) -> Iterator<i16>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i32, to: i32) -> Iterator<i32>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: int, to: int) -> Iterator<int>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i8, to: i8) -> Iterator<i8>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u128, to: u128) -> Iterator<u128>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u16, to: u16) -> Iterator<u16>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u32, to: u32) -> Iterator<u32>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u64, to: u64) -> Iterator<u64>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u8, to: u8) -> Iterator<u8>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<float>, step: float) -> Iterator<float>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<i128>, step: i128) -> Iterator<i128>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<i16>, step: i16) -> Iterator<i16>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<i32>, step: i32) -> Iterator<i32>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<int>, step: int) -> Iterator<int>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<i8>, step: i8) -> Iterator<i8>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u128>, step: u128) -> Iterator<u128>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u16>, step: u16) -> Iterator<u16>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u32>, step: u32) -> Iterator<u32>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u64>, step: u64) -> Iterator<u64>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u8>, step: u8) -> Iterator<u8>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: float, to: float, step: float) -> Iterator<float>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i128, to: i128, step: i128) -> Iterator<i128>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i16, to: i16, step: i16) -> Iterator<i16>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i32, to: i32, step: i32) -> Iterator<i32>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: int, to: int, step: int) -> Iterator<int>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i8, to: i8, step: i8) -> Iterator<i8>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u128, to: u128, step: u128) -> Iterator<u128>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u16, to: u16, step: u16) -> Iterator<u16>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u32, to: u32, step: u32) -> Iterator<u32>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u64, to: u64, step: u64) -> Iterator<u64>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u8, to: u8, step: u8) -> Iterator<u8>;
+
+/// Reduce an array by iterating through all elements while applying a function named by `reducer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `reducer` must exist taking these parameters:
+///
+/// * `result`: accumulated result, initially `()`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn process(r, x) {
+/// x + (r ?? 0)
+/// }
+/// fn process_extra(r, x, i) {
+/// x + i + (r ?? 0)
+/// }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce("process");
+///
+/// print(y); // prints 15
+///
+/// let y = x.reduce("process_extra");
+///
+/// print(y); // prints 25
+/// ```
+fn reduce(array: Array, reducer: String) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements while applying the `reducer` function.
+///
+/// # Function Parameters
+///
+/// * `result`: accumulated result, initially `()`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce(|r, v| v + (r ?? 0));
+///
+/// print(y); // prints 15
+///
+/// let y = x.reduce(|r, v, i| v + i + (r ?? 0));
+///
+/// print(y); // prints 25
+/// ```
+fn reduce(array: Array, reducer: FnPtr) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements while applying a function named by `reducer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `reducer` must exist taking these parameters:
+///
+/// * `result`: accumulated result, starting with the value of `initial`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn process(r, x) { x + r }
+///
+/// fn process_extra(r, x, i) { x + i + r }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce("process", 5);
+///
+/// print(y); // prints 20
+///
+/// let y = x.reduce("process_extra", 5);
+///
+/// print(y); // prints 30
+/// ```
+fn reduce(array: Array, reducer: String, initial: ?) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements while applying the `reducer` function.
+///
+/// # Function Parameters
+///
+/// * `result`: accumulated result, starting with the value of `initial`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce(|r, v| v + r, 5);
+///
+/// print(y); // prints 20
+///
+/// let y = x.reduce(|r, v, i| v + i + r, 5);
+///
+/// print(y); // prints 30
+/// ```
+fn reduce(array: Array, reducer: FnPtr, initial: ?) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements, in _reverse_ order,
+/// while applying a function named by `reducer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `reducer` must exist taking these parameters:
+///
+/// * `result`: accumulated result, initially `()`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn process(r, x) {
+/// x + (r ?? 0)
+/// }
+/// fn process_extra(r, x, i) {
+/// x + i + (r ?? 0)
+/// }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce_rev("process");
+///
+/// print(y); // prints 15
+///
+/// let y = x.reduce_rev("process_extra");
+///
+/// print(y); // prints 25
+/// ```
+fn reduce_rev(array: Array, reducer: String) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements, in _reverse_ order,
+/// while applying the `reducer` function.
+///
+/// # Function Parameters
+///
+/// * `result`: accumulated result, initially `()`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce_rev(|r, v| v + (r ?? 0));
+///
+/// print(y); // prints 15
+///
+/// let y = x.reduce_rev(|r, v, i| v + i + (r ?? 0));
+///
+/// print(y); // prints 25
+/// ```
+fn reduce_rev(array: Array, reducer: FnPtr) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements, in _reverse_ order,
+/// while applying a function named by `reducer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `reducer` must exist taking these parameters:
+///
+/// * `result`: accumulated result, starting with the value of `initial`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn process(r, x) { x + r }
+///
+/// fn process_extra(r, x, i) { x + i + r }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce_rev("process", 5);
+///
+/// print(y); // prints 20
+///
+/// let y = x.reduce_rev("process_extra", 5);
+///
+/// print(y); // prints 30
+/// ```
+fn reduce_rev(array: Array, reducer: String, initial: ?) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements, in _reverse_ order,
+/// while applying the `reducer` function.
+///
+/// # Function Parameters
+///
+/// * `result`: accumulated result, starting with the value of `initial`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce_rev(|r, v| v + r, 5);
+///
+/// print(y); // prints 20
+///
+/// let y = x.reduce_rev(|r, v, i| v + i + r, 5);
+///
+/// print(y); // prints 30
+/// ```
+fn reduce_rev(array: Array, reducer: FnPtr, initial: ?) -> RhaiResult;
+
+/// Remove the element at the specified `index` from the array and return it.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` < -length of array, `()` is returned.
+/// * If `index` ≥ length of array, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// print(x.remove(1)); // prints 2
+///
+/// print(x); // prints "[1, 3]"
+///
+/// print(x.remove(-2)); // prints 1
+///
+/// print(x); // prints "[3]"
+/// ```
+fn remove(array: Array, index: int) -> ?;
+
+/// Remove the byte at the specified `index` from the BLOB and return it.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `index` < -length of BLOB, zero is returned.
+/// * If `index` ≥ length of BLOB, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(x.remove(1)); // prints 2
+///
+/// print(x); // prints "[01030405]"
+///
+/// print(x.remove(-2)); // prints 4
+///
+/// print(x); // prints "[010305]"
+/// ```
+fn remove(blob: Blob, index: int) -> int;
+
+/// Remove any property of the specified `name` from the object map, returning its value.
+///
+/// If the property does not exist, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// let x = m.remove("b");
+///
+/// print(x); // prints 2
+///
+/// print(m); // prints "#{a:1, c:3}"
+/// ```
+fn remove(map: Map, property: String) -> ?;
+
+/// Remove all occurrences of a character from the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.remove("o");
+///
+/// print(text); // prints "hell, wrld! hell, fbar!"
+/// ```
+fn remove(string: String, character: char) -> ();
+
+/// Remove all occurrences of a sub-string from the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.remove("hello");
+///
+/// print(text); // prints ", world! , foobar!"
+/// ```
+fn remove(string: String, sub_string: String) -> ();
+
+/// Replace all occurrences of the specified character in the string with another character.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.replace("l", '*');
+///
+/// print(text); // prints "he**o, wor*d! he**o, foobar!"
+/// ```
+fn replace(string: String, find_character: char, substitute_character: char) -> ();
+
+/// Replace all occurrences of the specified character in the string with another string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.replace('l', "(^)");
+///
+/// print(text); // prints "he(^)(^)o, wor(^)d! he(^)(^)o, foobar!"
+/// ```
+fn replace(string: String, find_character: char, substitute_string: String) -> ();
+
+/// Replace all occurrences of the specified sub-string in the string with the specified character.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.replace("hello", '*');
+///
+/// print(text); // prints "*, world! *, foobar!"
+/// ```
+fn replace(string: String, find_string: String, substitute_character: char) -> ();
+
+/// Replace all occurrences of the specified sub-string in the string with another string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.replace("hello", "hey");
+///
+/// print(text); // prints "hey, world! hey, foobar!"
+/// ```
+fn replace(string: String, find_string: String, substitute_string: String) -> ();
+
+/// Remove all elements in the array that do not return `true` when applied a function named by
+/// `filter` and return them as a new array.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn large(x) { x >= 3 }
+///
+/// fn screen(x, i) { x + i <= 5 }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain("large");
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// print(y); // prints "[1, 2]"
+///
+/// let z = x.retain("screen");
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn retain(array: Array, filter: String) -> Array;
+
+/// Remove all elements in the array that do not return `true` when applied the `filter`
+/// function and return them as a new array.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain(|v| v >= 3);
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// print(y); // prints "[1, 2]"
+///
+/// let z = x.retain(|v, i| v + i <= 5);
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn retain(array: Array, filter: FnPtr) -> Array;
+
+/// Remove all elements in the array not within an exclusive `range` and return them as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain(1..4);
+///
+/// print(x); // prints "[2, 3, 4]"
+///
+/// print(y); // prints "[1, 5]"
+///
+/// let z = x.retain(1..3);
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[1]"
+/// ```
+fn retain(array: Array, range: Range<int>) -> Array;
+
+/// Remove all elements in the array not within an inclusive `range` and return them as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain(1..=3);
+///
+/// print(x); // prints "[2, 3, 4]"
+///
+/// print(y); // prints "[1, 5]"
+///
+/// let z = x.retain(1..=2);
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[1]"
+/// ```
+fn retain(array: Array, range: RangeInclusive<int>) -> Array;
+
+/// Remove all bytes in the BLOB not within an exclusive `range` and return them as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.retain(1..4);
+///
+/// print(b1); // prints "[020304]"
+///
+/// print(b2); // prints "[0105]"
+///
+/// let b3 = b1.retain(1..3);
+///
+/// print(b1); // prints "[0304]"
+///
+/// print(b2); // prints "[01]"
+/// ```
+fn retain(blob: Blob, range: Range<int>) -> Blob;
+
+/// Remove all bytes in the BLOB not within an inclusive `range` and return them as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.retain(1..=3);
+///
+/// print(b1); // prints "[020304]"
+///
+/// print(b2); // prints "[0105]"
+///
+/// let b3 = b1.retain(1..=2);
+///
+/// print(b1); // prints "[0304]"
+///
+/// print(b2); // prints "[01]"
+/// ```
+fn retain(blob: Blob, range: RangeInclusive<int>) -> Blob;
+
+/// Remove all elements not within a portion of the array and return them as a new array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, all elements are removed returned.
+/// * If `len` ≤ 0, all elements are removed and returned.
+/// * If `start` position + `len` ≥ length of array, entire portion of the array before the `start` position is removed and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain(1, 2);
+///
+/// print(x); // prints "[2, 3]"
+///
+/// print(y); // prints "[1, 4, 5]"
+///
+/// let z = x.retain(-1, 1);
+///
+/// print(x); // prints "[3]"
+///
+/// print(z); // prints "[2]"
+/// ```
+fn retain(array: Array, start: int, len: int) -> Array;
+
+/// Remove all bytes not within a portion of the BLOB and return them as a new BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, all elements are removed returned.
+/// * If `len` ≤ 0, all elements are removed and returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB before the `start` position is removed and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.retain(1, 2);
+///
+/// print(b1); // prints "[0203]"
+///
+/// print(b2); // prints "[010405]"
+///
+/// let b3 = b1.retain(-1, 1);
+///
+/// print(b1); // prints "[03]"
+///
+/// print(b3); // prints "[02]"
+/// ```
+fn retain(blob: Blob, start: int, len: int) -> Blob;
+
+/// Reverse all the elements in the array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// x.reverse();
+///
+/// print(x); // prints "[5, 4, 3, 2, 1]"
+/// ```
+fn reverse(array: Array) -> ();
+
+/// Reverse the BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b); // prints "[0102030405]"
+///
+/// b.reverse();
+///
+/// print(b); // prints "[0504030201]"
+/// ```
+fn reverse(blob: Blob) -> ();
+
+/// Return the nearest whole number closest to the floating-point number.
+/// Rounds away from zero.
+fn round(x: float) -> float;
+
+/// Set the element at the `index` position in the array to a new `value`.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` < -length of array, the array is not modified.
+/// * If `index` ≥ length of array, the array is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// x.set(0, 42);
+///
+/// print(x); // prints "[42, 2, 3]"
+///
+/// x.set(-3, 0);
+///
+/// print(x); // prints "[0, 2, 3]"
+///
+/// x.set(99, 123);
+///
+/// print(x); // prints "[0, 2, 3]"
+/// ```
+fn set(array: Array, index: int, value: ?) -> ();
+
+/// Set the particular `index` position in the BLOB to a new byte `value`.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `index` < -length of BLOB, the BLOB is not modified.
+/// * If `index` ≥ length of BLOB, the BLOB is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// b.set(0, 0x42);
+///
+/// print(b); // prints "[4202030405]"
+///
+/// b.set(-3, 0);
+///
+/// print(b); // prints "[4202000405]"
+///
+/// b.set(99, 123);
+///
+/// print(b); // prints "[4202000405]"
+/// ```
+fn set(blob: Blob, index: int, value: int) -> ();
+
+/// Set the value of the `property` in the object map to a new `value`.
+///
+/// If `property` does not exist in the object map, it is added.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a: 1, b: 2, c: 3};
+///
+/// m.set("b", 42)'
+///
+/// print(m); // prints "#{a: 1, b: 42, c: 3}"
+///
+/// x.set("x", 0);
+///
+/// print(m); // prints "#{a: 1, b: 42, c: 3, x: 0}"
+/// ```
+fn set(map: Map, property: String, value: ?) -> ();
+
+/// Set the `index` position in the string to a new `character`.
+///
+/// * If `index` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `index` < -length of string, the string is not modified.
+/// * If `index` ≥ length of string, the string is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.set(3, 'x');
+///
+/// print(text); // prints "helxo, world!"
+///
+/// text.set(-3, 'x');
+///
+/// print(text); // prints "hello, worxd!"
+///
+/// text.set(99, 'x');
+///
+/// print(text); // prints "hello, worxd!"
+/// ```
+fn set(string: String, index: int, character: char) -> ();
+
+/// Set the _tag_ of a `Dynamic` value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// x.tag = 42;
+///
+/// print(x.tag); // prints 42
+/// ```
+fn set tag(value: ?, tag: int) -> ();
+
+/// Set the specified `bit` in the number if the new value is `true`.
+/// Clear the `bit` if the new value is `false`.
+///
+/// If `bit` < 0, position counts from the MSB (Most Significant Bit).
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// x.set_bit(5, true);
+///
+/// print(x); // prints 123488
+///
+/// x.set_bit(6, false);
+///
+/// print(x); // prints 123424
+///
+/// x.set_bit(-48, false);
+///
+/// print(x); // prints 57888 on 64-bit
+/// ```
+fn set_bit(value: int, bit: int, new_value: bool) -> ();
+
+/// Replace an exclusive range of bits in the number with a new value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// x.set_bits(5..10, 42);
+///
+/// print(x); // print 123200
+/// ```
+fn set_bits(value: int, range: Range<int>, new_value: int) -> ();
+
+/// Replace an inclusive range of bits in the number with a new value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// x.set_bits(5..=9, 42);
+///
+/// print(x); // print 123200
+/// ```
+fn set_bits(value: int, range: RangeInclusive<int>, new_value: int) -> ();
+
+/// Replace a portion of bits in the number with a new value.
+///
+/// * If `start` < 0, position counts from the MSB (Most Significant Bit).
+/// * If `bits` ≤ 0, the number is not modified.
+/// * If `start` position + `bits` ≥ total number of bits, the bits after the `start` position are replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// x.set_bits(5, 8, 42);
+///
+/// print(x); // prints 124224
+///
+/// x.set_bits(-16, 10, 42);
+///
+/// print(x); // prints 11821949021971776 on 64-bit
+/// ```
+fn set_bits(value: int, bit: int, bits: int, new_value: int) -> ();
+
+/// Set the _tag_ of a `Dynamic` value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// x.tag = 42;
+///
+/// print(x.tag); // prints 42
+/// ```
+fn set_tag(value: ?, tag: int) -> ();
+
+/// Remove the first element from the array and return it.
+///
+/// If the array is empty, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// print(x.shift()); // prints 1
+///
+/// print(x); // prints "[2, 3]"
+/// ```
+fn shift(array: Array) -> ?;
+
+/// Remove the first byte from the BLOB and return it.
+///
+/// If the BLOB is empty, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.shift()); // prints 1
+///
+/// print(b); // prints "[02030405]"
+/// ```
+fn shift(blob: Blob) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: int) -> int;
+
+/// Return the sign (as an integer) of the floating-point number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: f32) -> int;
+
+/// Return the sign (as an integer) of the floating-point number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: float) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: i128) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: i16) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: i32) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: i8) -> int;
+
+/// Return the sine of the floating-point number in radians.
+fn sin(x: float) -> float;
+
+/// Return the hyperbolic sine of the floating-point number in radians.
+fn sinh(x: float) -> float;
+
+/// Block the current thread for a particular number of `seconds`.
+fn sleep(seconds: int) -> ();
+
+/// Block the current thread for a particular number of `seconds`.
+fn sleep(seconds: float) -> ();
+
+/// Return `true` if any element in the array that returns `true` when applied a function named
+/// by `filter`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn large(x) { x > 3 }
+///
+/// fn huge(x) { x > 10 }
+///
+/// fn screen(x, i) { i > x }
+///
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.some("large")); // prints true
+///
+/// print(x.some("huge")); // prints false
+///
+/// print(x.some("screen")); // prints true
+/// ```
+fn some(array: Array, filter: String) -> bool;
+
+/// Return `true` if any element in the array that returns `true` when applied the `filter` function.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.some(|v| v > 3)); // prints true
+///
+/// print(x.some(|v| v > 10)); // prints false
+///
+/// print(x.some(|v, i| i > v)); // prints true
+/// ```
+fn some(array: Array, filter: FnPtr) -> bool;
+
+/// Sort the array.
+///
+/// All elements in the array must be of the same data type.
+///
+/// # Supported Data Types
+///
+/// * integer numbers
+/// * floating-point numbers
+/// * decimal numbers
+/// * characters
+/// * strings
+/// * booleans
+/// * `()`
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10];
+///
+/// x.sort();
+///
+/// print(x); // prints "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
+/// ```
+fn sort(array: Array) -> ();
+
+/// Sort the array based on applying a function named by `comparer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `comparer` must exist taking these parameters:
+///
+/// * `element1`: copy of the current array element to compare
+/// * `element2`: copy of the next array element to compare
+///
+/// ## Return Value
+///
+/// * Any integer > 0 if `element1 > element2`
+/// * Zero if `element1 == element2`
+/// * Any integer < 0 if `element1 < element2`
+///
+/// # Example
+///
+/// ```rhai
+/// fn reverse(a, b) {
+/// if a > b {
+/// -1
+/// } else if a < b {
+/// 1
+/// } else {
+/// 0
+/// }
+/// }
+/// let x = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10];
+///
+/// x.sort("reverse");
+///
+/// print(x); // prints "[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]"
+/// ```
+fn sort(array: Array, comparer: String) -> ();
+
+/// Sort the array based on applying the `comparer` function.
+///
+/// # Function Parameters
+///
+/// * `element1`: copy of the current array element to compare
+/// * `element2`: copy of the next array element to compare
+///
+/// ## Return Value
+///
+/// * Any integer > 0 if `element1 > element2`
+/// * Zero if `element1 == element2`
+/// * Any integer < 0 if `element1 < element2`
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10];
+///
+/// // Do comparisons in reverse
+/// x.sort(|a, b| if a > b { -1 } else if a < b { 1 } else { 0 });
+///
+/// print(x); // prints "[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]"
+/// ```
+fn sort(array: Array, comparer: FnPtr) -> ();
+
+/// Replace an exclusive range of the array with another array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [7, 8, 9, 10];
+///
+/// x.splice(1..3, y);
+///
+/// print(x); // prints "[1, 7, 8, 9, 10, 4, 5]"
+/// ```
+fn splice(array: Array, range: Range<int>, replace: Array) -> ();
+
+/// Replace an inclusive range of the array with another array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [7, 8, 9, 10];
+///
+/// x.splice(1..=3, y);
+///
+/// print(x); // prints "[1, 7, 8, 9, 10, 5]"
+/// ```
+fn splice(array: Array, range: RangeInclusive<int>, replace: Array) -> ();
+
+/// Replace an exclusive `range` of the BLOB with another BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob(10, 0x42);
+/// let b2 = blob(5, 0x18);
+///
+/// b1.splice(1..4, b2);
+///
+/// print(b1); // prints "[4218181818184242 42424242]"
+/// ```
+fn splice(blob: Blob, range: Range<int>, replace: Blob) -> ();
+
+/// Replace an inclusive `range` of the BLOB with another BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob(10, 0x42);
+/// let b2 = blob(5, 0x18);
+///
+/// b1.splice(1..=4, b2);
+///
+/// print(b1); // prints "[4218181818184242 424242]"
+/// ```
+fn splice(blob: Blob, range: RangeInclusive<int>, replace: Blob) -> ();
+
+/// Replace a portion of the array with another array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, the other array is appended to the end of the array.
+/// * If `len` ≤ 0, the other array is inserted into the array at the `start` position without replacing any element.
+/// * If `start` position + `len` ≥ length of array, entire portion of the array after the `start` position is replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [7, 8, 9, 10];
+///
+/// x.splice(1, 2, y);
+///
+/// print(x); // prints "[1, 7, 8, 9, 10, 4, 5]"
+///
+/// x.splice(-5, 4, y);
+///
+/// print(x); // prints "[1, 7, 7, 8, 9, 10, 5]"
+/// ```
+fn splice(array: Array, start: int, len: int, replace: Array) -> ();
+
+/// Replace a portion of the BLOB with another BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, the other BLOB is appended to the end of the BLOB.
+/// * If `len` ≤ 0, the other BLOB is inserted into the BLOB at the `start` position without replacing anything.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob(10, 0x42);
+/// let b2 = blob(5, 0x18);
+///
+/// b1.splice(1, 3, b2);
+///
+/// print(b1); // prints "[4218181818184242 42424242]"
+///
+/// b1.splice(-5, 4, b2);
+///
+/// print(b1); // prints "[4218181818184218 1818181842]"
+/// ```
+fn splice(blob: Blob, start: int, len: int, replace: Blob) -> ();
+
+/// Split the string into segments based on whitespaces, returning an array of the segments.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split()); // prints ["hello,", "world!", "hello,", "foo!"]
+/// ```
+fn split(string: String) -> Array;
+
+/// Cut off the array at `index` and return it as a new array.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` is zero, the entire array is cut and returned.
+/// * If `index` < -length of array, the entire array is cut and returned.
+/// * If `index` ≥ length of array, nothing is cut from the array and an empty array is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.split(2);
+///
+/// print(y); // prints "[3, 4, 5]"
+///
+/// print(x); // prints "[1, 2]"
+/// ```
+fn split(array: Array, index: int) -> Array;
+
+/// Cut off the BLOB at `index` and return it as a new BLOB.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `index` is zero, the entire BLOB is cut and returned.
+/// * If `index` < -length of BLOB, the entire BLOB is cut and returned.
+/// * If `index` ≥ length of BLOB, nothing is cut from the BLOB and an empty BLOB is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.split(2);
+///
+/// print(b2); // prints "[030405]"
+///
+/// print(b1); // prints "[0102]"
+/// ```
+fn split(blob: Blob, index: int) -> Blob;
+
+/// Split the string into two at the specified `index` position and return it both strings
+/// as an array.
+///
+/// The character at the `index` position (if any) is returned in the _second_ string.
+///
+/// * If `index` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `index` < -length of string, it is equivalent to cutting at position 0.
+/// * If `index` ≥ length of string, it is equivalent to cutting at the end of the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.split(6)); // prints ["hello,", " world!"]
+///
+/// print(text.split(13)); // prints ["hello, world!", ""]
+///
+/// print(text.split(-6)); // prints ["hello, ", "world!"]
+///
+/// print(text.split(-99)); // prints ["", "hello, world!"]
+/// ```
+fn split(string: String, index: int) -> Array;
+
+/// Split the string into segments based on a `delimiter` string, returning an array of the segments.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split("ll")); // prints ["he", "o, world! he", "o, foo!"]
+/// ```
+fn split(string: String, delimiter: String) -> Array;
+
+/// Split the string into segments based on a `delimiter` character, returning an array of the segments.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split('l')); // prints ["he", "", "o, wor", "d! he", "", "o, foo!"]
+/// ```
+fn split(string: String, delimiter: char) -> Array;
+
+/// Split the string into at most the specified number of `segments` based on a `delimiter` string,
+/// returning an array of the segments.
+///
+/// If `segments` < 1, only one segment is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split("ll", 2)); // prints ["he", "o, world! hello, foo!"]
+/// ```
+fn split(string: String, delimiter: String, segments: int) -> Array;
+
+/// Split the string into at most the specified number of `segments` based on a `delimiter` character,
+/// returning an array of the segments.
+///
+/// If `segments` < 1, only one segment is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split('l', 3)); // prints ["he", "", "o, world! hello, foo!"]
+/// ```
+fn split(string: String, delimiter: char, segments: int) -> Array;
+
+/// Split the string into segments based on a `delimiter` string, returning an array of the
+/// segments in _reverse_ order.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split_rev("ll")); // prints ["o, foo!", "o, world! he", "he"]
+/// ```
+fn split_rev(string: String, delimiter: String) -> Array;
+
+/// Split the string into segments based on a `delimiter` character, returning an array of
+/// the segments in _reverse_ order.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split_rev('l')); // prints ["o, foo!", "", "d! he", "o, wor", "", "he"]
+/// ```
+fn split_rev(string: String, delimiter: char) -> Array;
+
+/// Split the string into at most a specified number of `segments` based on a `delimiter` string,
+/// returning an array of the segments in _reverse_ order.
+///
+/// If `segments` < 1, only one segment is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split_rev("ll", 2)); // prints ["o, foo!", "hello, world! he"]
+/// ```
+fn split_rev(string: String, delimiter: String, segments: int) -> Array;
+
+/// Split the string into at most the specified number of `segments` based on a `delimiter` character,
+/// returning an array of the segments.
+///
+/// If `segments` < 1, only one segment is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split('l', 3)); // prints ["o, foo!", "", "hello, world! he"
+/// ```
+fn split_rev(string: String, delimiter: char, segments: int) -> Array;
+
+/// Return the square root of the floating-point number.
+fn sqrt(x: float) -> float;
+
+/// Return the start of the exclusive range.
+fn start(range: ExclusiveRange) -> int;
+
+/// Return the start of the inclusive range.
+fn start(range: InclusiveRange) -> int;
+
+/// Return `true` if the string starts with a specified string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.starts_with("hello")); // prints true
+///
+/// print(text.starts_with("world")); // prints false
+/// ```
+fn starts_with(string: String, match_string: String) -> bool;
+
+/// Copy an exclusive range of characters from the string and return it as a new string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.sub_string(3..7)); // prints "lo, "
+/// ```
+fn sub_string(string: String, range: Range<int>) -> String;
+
+/// Copy an inclusive range of characters from the string and return it as a new string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.sub_string(3..=7)); // prints "lo, w"
+/// ```
+fn sub_string(string: String, range: RangeInclusive<int>) -> String;
+
+/// Copy a portion of the string beginning at the `start` position till the end and return it as
+/// a new string.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, the entire string is copied and returned.
+/// * If `start` ≥ length of string, an empty string is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.sub_string(5)); // prints ", world!"
+///
+/// print(text.sub_string(-5)); // prints "orld!"
+/// ```
+fn sub_string(string: String, start: int) -> String;
+
+/// Copy a portion of the string and return it as a new string.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, an empty string is returned.
+/// * If `len` ≤ 0, an empty string is returned.
+/// * If `start` position + `len` ≥ length of string, entire portion of the string after the `start` position is copied and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.sub_string(3, 4)); // prints "lo, "
+///
+/// print(text.sub_string(-8, 3)); // prints ", w"
+/// ```
+fn sub_string(string: String, start: int, len: int) -> String;
+
+/// Return the _tag_ of a `Dynamic` value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// x.tag = 42;
+///
+/// print(x.tag); // prints 42
+/// ```
+fn tag(value: ?) -> int;
+
+/// Return the tangent of the floating-point number in radians.
+fn tan(x: float) -> float;
+
+/// Return the hyperbolic tangent of the floating-point number in radians.
+fn tanh(x: float) -> float;
+
+/// Create a timestamp containing the current system time.
+///
+/// # Example
+///
+/// ```rhai
+/// let now = timestamp();
+///
+/// sleep(10.0); // sleep for 10 seconds
+///
+/// print(now.elapsed); // prints 10.???
+/// ```
+fn timestamp() -> Instant;
+
+/// Convert the BLOB into an array of integers.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// let x = b.to_array();
+///
+/// print(x); // prints "[66, 66, 66, 66, 66]"
+/// ```
+fn to_array(blob: Blob) -> Array;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: i128) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: i16) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: i32) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: int) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: i8) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u128) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u16) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u32) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u64) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u8) -> String;
+
+/// Convert the string into an UTF-8 encoded byte-stream as a BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// let bytes = text.to_blob();
+///
+/// print(bytes.len()); // prints 51
+/// ```
+fn to_blob(string: String) -> Blob;
+
+/// Return an array containing all the characters of the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello";
+///
+/// print(text.to_chars()); // prints "['h', 'e', 'l', 'l', 'o']"
+/// ```
+fn to_chars(string: String) -> Array;
+
+/// Convert the array into a string.
+fn to_debug(array: Array) -> String;
+
+/// Convert the string into debug format.
+fn to_debug(character: char) -> String;
+
+/// Convert the function pointer into a string in debug format.
+fn to_debug(f: FnPtr) -> String;
+
+/// Convert the value of the `item` into a string in debug format.
+fn to_debug(item: ?) -> String;
+
+/// Convert the object map into a string.
+fn to_debug(map: Map) -> String;
+
+/// Convert the value of `number` into a string.
+fn to_debug(number: f32) -> String;
+
+/// Convert the value of `number` into a string.
+fn to_debug(number: float) -> String;
+
+/// Convert the string into debug format.
+fn to_debug(string: String) -> String;
+
+/// Convert the unit into a string in debug format.
+fn to_debug(unit: ()) -> String;
+
+/// Convert the boolean value into a string in debug format.
+fn to_debug(value: bool) -> String;
+
+/// Convert radians to degrees.
+fn to_degrees(x: float) -> float;
+
+/// Convert the 32-bit floating-point number to 64-bit.
+fn to_float(x: f32) -> float;
+
+fn to_float(x: i128) -> float;
+
+fn to_float(x: i16) -> float;
+
+fn to_float(x: i32) -> float;
+
+fn to_float(x: int) -> float;
+
+fn to_float(x: i8) -> float;
+
+fn to_float(x: u128) -> float;
+
+fn to_float(x: u16) -> float;
+
+fn to_float(x: u32) -> float;
+
+fn to_float(x: u8) -> float;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: i128) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: i16) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: i32) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: int) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: i8) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u128) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u16) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u32) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u64) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u8) -> String;
+
+fn to_int(x: char) -> int;
+
+/// Convert the floating-point number into an integer.
+fn to_int(x: f32) -> int;
+
+/// Convert the floating-point number into an integer.
+fn to_int(x: float) -> int;
+
+fn to_int(x: i128) -> int;
+
+fn to_int(x: i16) -> int;
+
+fn to_int(x: i32) -> int;
+
+fn to_int(x: int) -> int;
+
+fn to_int(x: i8) -> int;
+
+fn to_int(x: u128) -> int;
+
+fn to_int(x: u16) -> int;
+
+fn to_int(x: u32) -> int;
+
+fn to_int(x: u64) -> int;
+
+fn to_int(x: u8) -> int;
+
+/// Return the JSON representation of the object map.
+///
+/// # Data types
+///
+/// Only the following data types should be kept inside the object map:
+/// `INT`, `FLOAT`, `ImmutableString`, `char`, `bool`, `()`, `Array`, `Map`.
+///
+/// # Errors
+///
+/// Data types not supported by JSON serialize into formats that may
+/// invalidate the result.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// print(m.to_json()); // prints {"a":1, "b":2, "c":3}
+/// ```
+fn to_json(map: Map) -> String;
+
+/// Convert the character to lower-case and return it as a new character.
+///
+/// # Example
+///
+/// ```rhai
+/// let ch = 'A';
+///
+/// print(ch.to_lower()); // prints 'a'
+///
+/// print(ch); // prints 'A'
+/// ```
+fn to_lower(character: char) -> char;
+
+/// Convert the string to all lower-case and return it as a new string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "HELLO, WORLD!"
+///
+/// print(text.to_lower()); // prints "hello, world!"
+///
+/// print(text); // prints "HELLO, WORLD!"
+/// ```
+fn to_lower(string: String) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: i128) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: i16) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: i32) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: int) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: i8) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u128) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u16) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u32) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u64) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u8) -> String;
+
+/// Convert degrees to radians.
+fn to_radians(x: float) -> float;
+
+/// Convert the array into a string.
+fn to_string(array: Array) -> String;
+
+/// Return the character into a string.
+fn to_string(character: char) -> String;
+
+/// Convert the value of the `item` into a string.
+fn to_string(item: ?) -> String;
+
+/// Convert the object map into a string.
+fn to_string(map: Map) -> String;
+
+/// Convert the value of `number` into a string.
+fn to_string(number: f32) -> String;
+
+/// Convert the value of `number` into a string.
+fn to_string(number: float) -> String;
+
+/// Return the `string`.
+fn to_string(string: String) -> String;
+
+/// Return the empty string.
+fn to_string(unit: ()) -> String;
+
+/// Return the boolean value into a string.
+fn to_string(value: bool) -> String;
+
+/// Convert the character to upper-case and return it as a new character.
+///
+/// # Example
+///
+/// ```rhai
+/// let ch = 'a';
+///
+/// print(ch.to_upper()); // prints 'A'
+///
+/// print(ch); // prints 'a'
+/// ```
+fn to_upper(character: char) -> char;
+
+/// Convert the string to all upper-case and return it as a new string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!"
+///
+/// print(text.to_upper()); // prints "HELLO, WORLD!"
+///
+/// print(text); // prints "hello, world!"
+/// ```
+fn to_upper(string: String) -> String;
+
+/// Remove whitespace characters from both ends of the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = " hello ";
+///
+/// text.trim();
+///
+/// print(text); // prints "hello"
+/// ```
+fn trim(string: String) -> ();
+
+/// Cut off the array at the specified length.
+///
+/// * If `len` ≤ 0, the array is cleared.
+/// * If `len` ≥ length of array, the array is not truncated.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// x.truncate(3);
+///
+/// print(x); // prints "[1, 2, 3]"
+///
+/// x.truncate(10);
+///
+/// print(x); // prints "[1, 2, 3]"
+/// ```
+fn truncate(array: Array, len: int) -> ();
+
+/// Cut off the BLOB at the specified length.
+///
+/// * If `len` ≤ 0, the BLOB is cleared.
+/// * If `len` ≥ length of BLOB, the BLOB is not truncated.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// b.truncate(3);
+///
+/// print(b); // prints "[010203]"
+///
+/// b.truncate(10);
+///
+/// print(b); // prints "[010203]"
+/// ```
+fn truncate(blob: Blob, len: int) -> ();
+
+/// Cut off the string at the specified number of characters.
+///
+/// * If `len` ≤ 0, the string is cleared.
+/// * If `len` ≥ length of string, the string is not truncated.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.truncate(13);
+///
+/// print(text); // prints "hello, world!"
+///
+/// x.truncate(10);
+///
+/// print(text); // prints "hello, world!"
+/// ```
+fn truncate(string: String, len: int) -> ();
+
+/// Return an array with all the property values in the object map.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// print(m.values()); // prints "[1, 2, 3]""
+/// ```
+fn values(map: Map) -> Array;
+
+/// Write an ASCII string to the bytes within an exclusive `range` in the BLOB.
+///
+/// Each ASCII character encodes to one single byte in the BLOB.
+/// Non-ASCII characters are ignored.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_ascii(1..5, "hello, world!");
+///
+/// print(b); // prints "[0068656c6c000000]"
+/// ```
+fn write_ascii(blob: Blob, range: Range<int>, string: String) -> ();
+
+/// Write an ASCII string to the bytes within an inclusive `range` in the BLOB.
+///
+/// Each ASCII character encodes to one single byte in the BLOB.
+/// Non-ASCII characters are ignored.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_ascii(1..=5, "hello, world!");
+///
+/// print(b); // prints "[0068656c6c6f0000]"
+/// ```
+fn write_ascii(blob: Blob, range: RangeInclusive<int>, string: String) -> ();
+
+/// Write an ASCII string to the bytes within an exclusive `range` in the BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, the BLOB is not modified.
+/// * If `len` ≤ 0, the BLOB is not modified.
+/// * If `start` position + `len` ≥ length of BLOB, only the portion of the BLOB after the `start` position is modified.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_ascii(1, 5, "hello, world!");
+///
+/// print(b); // prints "[0068656c6c6f0000]"
+/// ```
+fn write_ascii(blob: Blob, start: int, len: int, string: String) -> ();
+
+/// Write a `FLOAT` value to the bytes within an exclusive `range` in the BLOB
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_be(blob: Blob, range: Range<int>, value: float) -> ();
+
+/// Write an `INT` value to the bytes within an exclusive `range` in the BLOB
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8, 0x42);
+///
+/// b.write_be_int(1..3, 0x99);
+///
+/// print(b); // prints "[4200004242424242]"
+/// ```
+fn write_be(blob: Blob, range: Range<int>, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes within an inclusive `range` in the BLOB
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_be(blob: Blob, range: RangeInclusive<int>, value: float) -> ();
+
+/// Write an `INT` value to the bytes within an inclusive `range` in the BLOB
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8, 0x42);
+///
+/// b.write_be_int(1..=3, 0x99);
+///
+/// print(b); // prints "[4200000042424242]"
+/// ```
+fn write_be(blob: Blob, range: RangeInclusive<int>, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes beginning at the `start` position in the BLOB
+/// in big-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_be(blob: Blob, start: int, len: int, value: float) -> ();
+
+/// Write an `INT` value to the bytes beginning at the `start` position in the BLOB
+/// in big-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8, 0x42);
+///
+/// b.write_be_int(1, 3, 0x99);
+///
+/// print(b); // prints "[4200000042424242]"
+/// ```
+fn write_be(blob: Blob, start: int, len: int, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes within an exclusive `range` in the BLOB
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_le(blob: Blob, range: Range<int>, value: float) -> ();
+
+/// Write an `INT` value to the bytes within an exclusive `range` in the BLOB
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_le_int(1..3, 0x12345678);
+///
+/// print(b); // prints "[0078560000000000]"
+/// ```
+fn write_le(blob: Blob, range: Range<int>, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes within an inclusive `range` in the BLOB
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_le(blob: Blob, range: RangeInclusive<int>, value: float) -> ();
+
+/// Write an `INT` value to the bytes within an inclusive `range` in the BLOB
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_le_int(1..=3, 0x12345678);
+///
+/// print(b); // prints "[0078563400000000]"
+/// ```
+fn write_le(blob: Blob, range: RangeInclusive<int>, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes beginning at the `start` position in the BLOB
+/// in little-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_le(blob: Blob, start: int, len: int, value: float) -> ();
+
+/// Write an `INT` value to the bytes beginning at the `start` position in the BLOB
+/// in little-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_le_int(1, 3, 0x12345678);
+///
+/// print(b); // prints "[0078563400000000]"
+/// ```
+fn write_le(blob: Blob, start: int, len: int, value: int) -> ();
+
+/// Write a string to the bytes within an exclusive `range` in the BLOB in UTF-8 encoding.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_utf8(1..5, "朝には紅顔ありて夕べには白骨となる");
+///
+/// print(b); // prints "[00e69c9de3000000]"
+/// ```
+fn write_utf8(blob: Blob, range: Range<int>, string: String) -> ();
+
+/// Write a string to the bytes within an inclusive `range` in the BLOB in UTF-8 encoding.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_utf8(1..=5, "朝には紅顔ありて夕べには白骨となる");
+///
+/// print(b); // prints "[00e69c9de3810000]"
+/// ```
+fn write_utf8(blob: Blob, range: RangeInclusive<int>, string: String) -> ();
+
+/// Write a string to the bytes within an inclusive `range` in the BLOB in UTF-8 encoding.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, the BLOB is not modified.
+/// * If `len` ≤ 0, the BLOB is not modified.
+/// * If `start` position + `len` ≥ length of BLOB, only the portion of the BLOB after the `start` position is modified.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_utf8(1, 5, "朝には紅顔ありて夕べには白骨となる");
+///
+/// print(b); // prints "[00e69c9de3810000]"
+/// ```
+fn write_utf8(blob: Blob, start: int, len: int, string: String) -> ();
+
+op |(i128, i128) -> i128;
+
+op |(i16, i16) -> i16;
+
+op |(i32, i32) -> i32;
+
+op |(i8, i8) -> i8;
+
+op |(u128, u128) -> u128;
+
+op |(u16, u16) -> u16;
+
+op |(u32, u32) -> u32;
+
+op |(u64, u64) -> u64;
+
+op |(u8, u8) -> u8;
\ No newline at end of file
diff --git a/rhai/examples/definitions/.rhai/definitions/general_kenobi.d.rhai b/rhai/examples/definitions/.rhai/definitions/general_kenobi.d.rhai
new file mode 100644
index 0000000..6257c2a
--- /dev/null
+++ b/rhai/examples/definitions/.rhai/definitions/general_kenobi.d.rhai
@@ -0,0 +1,6 @@
+module general_kenobi;
+
+const CONSTANT: int;
+
+/// Returns a string where "hello there" is repeated `n` times.
+fn hello_there(n: int) -> String;
\ No newline at end of file
diff --git a/rhai/examples/definitions/.rhai/defs.json b/rhai/examples/definitions/.rhai/defs.json
new file mode 100644
index 0000000..467a550
--- /dev/null
+++ b/rhai/examples/definitions/.rhai/defs.json
@@ -0,0 +1,49 @@
+{
+ "modules": {
+ "general_kenobi": {
+ "functions": [
+ {
+ "baseHash": 3873007749982070651,
+ "fullHash": 5865213555928423624,
+ "namespace": "internal",
+ "access": "public",
+ "name": "hello_there",
+ "type": "native",
+ "numParams": 1,
+ "params": [
+ {
+ "name": "n",
+ "type": "i64"
+ }
+ ],
+ "returnType": "String",
+ "signature": "hello_there(n: i64) -> String",
+ "docComments": [
+ "/// Returns a string where \"hello there\" is repeated `n` times."
+ ]
+ }
+ ]
+ }
+ },
+ "functions": [
+ {
+ "baseHash": 12461724250411739075,
+ "fullHash": 14530626537296006176,
+ "namespace": "global",
+ "access": "public",
+ "name": "minus",
+ "type": "native",
+ "numParams": 2,
+ "params": [
+ {
+ "type": "i64"
+ },
+ {
+ "type": "i64"
+ }
+ ],
+ "returnType": "i64",
+ "signature": "minus(_: i64, _: i64) -> i64"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/rhai/examples/definitions/main.rs b/rhai/examples/definitions/main.rs
new file mode 100644
index 0000000..7648ec8
--- /dev/null
+++ b/rhai/examples/definitions/main.rs
@@ -0,0 +1,70 @@
+use rhai::plugin::*;
+use rhai::{Engine, EvalAltResult, Scope};
+
+#[export_module]
+pub mod general_kenobi {
+ /// General Kenobi's Constant.
+ pub const CONSTANT: i64 = 42;
+
+ /// Returns a string where "hello there" is repeated `n` times.
+ pub fn hello_there(n: i64) -> String {
+ use std::convert::TryInto;
+ "hello there ".repeat(n.try_into().unwrap())
+ }
+}
+
+fn main() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let mut scope = Scope::new();
+
+ // This variable will also show up in the definitions, since it will be part of the scope.
+ scope.push("hello_there", "hello there");
+
+ // This constant will also show up in the definitions, since it will be part of the scope.
+ scope.push_constant("HELLO", "hello there");
+
+ #[cfg(not(feature = "no_module"))]
+ engine.register_static_module("general_kenobi", exported_module!(general_kenobi).into());
+
+ // Custom operators also show up in definitions.
+ #[cfg(not(feature = "no_custom_syntax"))]
+ {
+ engine.register_custom_operator("minus", 100).unwrap();
+ engine.register_fn("minus", |a: i64, b: i64| a - b);
+ }
+
+ engine.run_with_scope(
+ &mut scope,
+ "hello_there = general_kenobi::hello_there(4 minus 2);",
+ )?;
+
+ // Generate definitions for the contents of the engine and the scope.
+ engine
+ .definitions_with_scope(&scope)
+ .write_to_dir("examples/definitions/.rhai/definitions")
+ .unwrap();
+
+ // Alternatively we can write all of the above to a single file.
+ engine
+ .definitions_with_scope(&scope)
+ .write_to_file("examples/definitions/.rhai/all_in_one.d.rhai")
+ .unwrap();
+
+ // Skip standard packages if not needed (e.g. they are provided elsewhere).
+ engine
+ .definitions_with_scope(&scope)
+ .include_standard_packages(false)
+ .write_to_file("examples/definitions/.rhai/all_in_one_without_standard.d.rhai")
+ .unwrap();
+
+ // Write function definitions as JSON.
+ let json = engine
+ .definitions()
+ .include_standard_packages(false)
+ .json()
+ .unwrap();
+
+ std::fs::write("examples/definitions/.rhai/defs.json", json).unwrap();
+
+ Ok(())
+}
diff --git a/rhai/examples/definitions/script.rhai b/rhai/examples/definitions/script.rhai
new file mode 100644
index 0000000..1b8da1a
--- /dev/null
+++ b/rhai/examples/definitions/script.rhai
@@ -0,0 +1,3 @@
+// The following will be valid based on the definitions.
+hello_there = general_kenobi::hello_there(123);
+print(hello_there);
diff --git a/rhai/examples/event_handler_js/main.rs b/rhai/examples/event_handler_js/main.rs
new file mode 100644
index 0000000..efed010
--- /dev/null
+++ b/rhai/examples/event_handler_js/main.rs
@@ -0,0 +1,168 @@
+//! Implementation of the Event Handler With State Pattern - JS Style
+
+#[cfg(any(feature = "no_function", feature = "no_object"))]
+pub fn main() {
+ panic!("This example does not run under 'no_function' or 'no_object'.")
+}
+
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_object"))]
+pub fn main() {
+ use rhai::{CallFnOptions, Dynamic, Engine, Map, Scope, AST};
+ use std::io::{stdin, stdout, Write};
+
+ const SCRIPT_FILE: &str = "event_handler_js/script.rhai";
+
+ #[derive(Debug)]
+ struct Handler {
+ pub engine: Engine,
+ pub scope: Scope<'static>,
+ pub states: Dynamic,
+ pub ast: AST,
+ }
+
+ fn print_scope(scope: &Scope) {
+ for (i, (name, constant, value)) in scope.iter_raw().enumerate() {
+ #[cfg(not(feature = "no_closure"))]
+ let value_is_shared = if value.is_shared() { " (shared)" } else { "" };
+ #[cfg(feature = "no_closure")]
+ let value_is_shared = "";
+
+ println!(
+ "[{}] {}{}{} = {:?}",
+ i + 1,
+ if constant { "const " } else { "" },
+ name,
+ value_is_shared,
+ *value.read_lock::<Dynamic>().unwrap(),
+ )
+ }
+ println!();
+ }
+
+ println!("Events Handler Example - JS Style");
+ println!("==================================");
+
+ let mut input = String::new();
+
+ // Read script file
+ print!("Script file [{}]: ", SCRIPT_FILE);
+ stdout().flush().expect("flush stdout");
+
+ input.clear();
+
+ stdin().read_line(&mut input).expect("read input");
+
+ let path = match input.trim() {
+ "" => SCRIPT_FILE,
+ path => path,
+ };
+
+ // Create Engine
+ let engine = Engine::new();
+
+ // Use an object map to hold state
+ let mut states = Map::new();
+
+ // Default states can be added
+ states.insert("bool_state".into(), Dynamic::FALSE);
+
+ // Convert the object map into 'Dynamic'
+ let mut states: Dynamic = states.into();
+
+ // Create a custom 'Scope' to hold state
+ let mut scope = Scope::new();
+
+ // Add any system-provided state into the custom 'Scope'.
+ // Constants can be used to optimize the script.
+ scope.push_constant("MY_CONSTANT", 42_i64);
+
+ // Compile the handler script.
+ println!("> Loading script file: {path}");
+
+ let ast = match engine.compile_file_with_scope(&scope, path.into()) {
+ Ok(ast) => ast,
+ Err(err) => {
+ eprintln!("! Error: {err}");
+ println!("Cannot continue. Bye!");
+ return;
+ }
+ };
+
+ println!("> Script file loaded.");
+ println!();
+ println!("quit = exit program");
+ println!("scope = print scope");
+ println!("states = print states");
+ println!("event arg = run function with argument");
+ println!();
+
+ // Run the 'init' function to initialize the state, retaining variables.
+
+ let options = CallFnOptions::new()
+ .eval_ast(false)
+ .bind_this_ptr(&mut states);
+
+ let result = engine.call_fn_with_options::<()>(options, &mut scope, &ast, "init", ());
+
+ if let Err(err) = result {
+ eprintln!("! {err}")
+ }
+
+ // Create handler instance
+ let mut handler = Handler {
+ engine,
+ scope,
+ states,
+ ast,
+ };
+
+ // Events loop
+ loop {
+ print!("event> ");
+ stdout().flush().expect("flush stdout");
+
+ // Read event
+ input.clear();
+ stdin().read_line(&mut input).expect("read input");
+
+ let mut fields = input.trim().splitn(2, ' ');
+
+ let event = fields.next().expect("event").trim();
+ let arg = fields.next().unwrap_or("").to_string();
+
+ // Process event
+ match event {
+ "quit" => break,
+
+ "scope" => {
+ print_scope(&handler.scope);
+ continue;
+ }
+
+ "states" => {
+ println!("{:?}", handler.states);
+ println!();
+ continue;
+ }
+
+ // Map all other events to function calls
+ _ => {
+ let engine = &handler.engine;
+ let scope = &mut handler.scope;
+ let ast = &handler.ast;
+ let options = CallFnOptions::new()
+ .eval_ast(false)
+ .bind_this_ptr(&mut handler.states);
+
+ let result = engine.call_fn_with_options::<()>(options, scope, ast, event, (arg,));
+
+ if let Err(err) = result {
+ eprintln!("! {err}")
+ }
+ }
+ }
+ }
+
+ println!("Bye!");
+}
diff --git a/rhai/examples/event_handler_js/script.rhai b/rhai/examples/event_handler_js/script.rhai
new file mode 100644
index 0000000..08ab396
--- /dev/null
+++ b/rhai/examples/event_handler_js/script.rhai
@@ -0,0 +1,50 @@
+//! Implementation of the Event Handler With State Pattern - JS Style
+
+/// Initialize user-provided state.
+fn init() {
+ // Can detect system-provided default states!
+ // Add 'bool_state' as new state variable if one does not exist
+ if "bool_state" !in this {
+ this.bool_state = false;
+ }
+ // Add 'value' as new state variable (overwrites any existing)
+ this.value = 0;
+
+ // Can also add OOP-style functions!
+ this.log = |x| print(`State = ${this.value}, data = ${x}`);
+}
+
+/// 'start' event handler
+fn start(data) {
+ if this.bool_state {
+ throw "Already started!";
+ }
+ if this.value <= 0 {
+ throw "Conditions not yet ready to start!";
+ }
+
+ // Constant 'MY_CONSTANT' in custom scope is also visible!
+ print(`MY_CONSTANT = ${MY_CONSTANT}`);
+
+ this.value += parse_int(data);
+ this.bool_state = true;
+}
+
+/// 'end' event handler
+fn end(data) {
+ if !this.bool_state {
+ throw "Not yet started!";
+ }
+ if this.value > 0 {
+ throw "Conditions not yet ready to end!";
+ }
+ this.value = parse_int(data);
+ this.bool_state = false;
+}
+
+/// 'update' event handler
+fn update(data) {
+ let data = parse_int(data);
+ this.value += data;
+ this.log(data);
+}
diff --git a/rhai/examples/event_handler_main/main.rs b/rhai/examples/event_handler_main/main.rs
new file mode 100644
index 0000000..2a1d8f7
--- /dev/null
+++ b/rhai/examples/event_handler_main/main.rs
@@ -0,0 +1,139 @@
+//! Implementation of the Event Handler With State Pattern - Main Style
+
+#[cfg(feature = "no_function")]
+pub fn main() {
+ panic!("This example does not run under 'no_function'.")
+}
+
+#[cfg(not(feature = "no_function"))]
+pub fn main() {
+ use rhai::{CallFnOptions, Dynamic, Engine, Scope, AST};
+ use std::io::{stdin, stdout, Write};
+
+ const SCRIPT_FILE: &str = "event_handler_main/script.rhai";
+
+ #[derive(Debug)]
+ struct Handler {
+ pub engine: Engine,
+ pub scope: Scope<'static>,
+ pub ast: AST,
+ }
+
+ fn print_scope(scope: &Scope) {
+ for (i, (name, constant, value)) in scope.iter_raw().enumerate() {
+ #[cfg(not(feature = "no_closure"))]
+ let value_is_shared = if value.is_shared() { " (shared)" } else { "" };
+ #[cfg(feature = "no_closure")]
+ let value_is_shared = "";
+
+ println!(
+ "[{}] {}{}{} = {:?}",
+ i + 1,
+ if constant { "const " } else { "" },
+ name,
+ value_is_shared,
+ *value.read_lock::<Dynamic>().unwrap(),
+ )
+ }
+ println!();
+ }
+
+ println!("Events Handler Example - Main Style");
+ println!("===================================");
+
+ let mut input = String::new();
+
+ // Read script file
+ print!("Script file [{SCRIPT_FILE}]: ");
+ stdout().flush().expect("flush stdout");
+
+ input.clear();
+
+ stdin().read_line(&mut input).expect("read input");
+
+ let path = match input.trim() {
+ "" => SCRIPT_FILE,
+ path => path,
+ };
+
+ // Create Engine
+ let engine = Engine::new();
+
+ // Create a custom 'Scope' to hold state
+ let mut scope = Scope::new();
+
+ // Add any system-provided state into the custom 'Scope'.
+ // Constants can be used to optimize the script.
+ scope.push_constant("MY_CONSTANT", 42_i64);
+
+ // Compile the handler script.
+ println!("> Loading script file: {path}");
+
+ let ast = match engine.compile_file_with_scope(&scope, path.into()) {
+ Ok(ast) => ast,
+ Err(err) => {
+ eprintln!("! Error: {}", err);
+ println!("Cannot continue. Bye!");
+ return;
+ }
+ };
+
+ println!("> Script file loaded.");
+ println!();
+ println!("quit = exit program");
+ println!("scope = print scope");
+ println!("event arg = run function with argument");
+ println!();
+
+ // Run the 'init' function to initialize the state, retaining variables.
+ let options = CallFnOptions::new().eval_ast(false).rewind_scope(false);
+
+ let result = engine.call_fn_with_options::<()>(options, &mut scope, &ast, "init", ());
+
+ if let Err(err) = result {
+ eprintln!("! {err}")
+ }
+
+ // Create handler instance
+ let mut handler = Handler { engine, scope, ast };
+
+ // Events loop
+ loop {
+ print!("event> ");
+ stdout().flush().expect("flush stdout");
+
+ // Read event
+ input.clear();
+ stdin().read_line(&mut input).expect("read input");
+
+ let mut fields = input.trim().splitn(2, ' ');
+
+ let event = fields.next().expect("event").trim();
+ let arg = fields.next().unwrap_or("").to_string();
+
+ // Process event
+ match event {
+ "quit" => break,
+
+ "scope" => {
+ print_scope(&handler.scope);
+ continue;
+ }
+
+ // Map all other events to function calls
+ _ => {
+ let engine = &handler.engine;
+ let scope = &mut handler.scope;
+ let ast = &handler.ast;
+
+ let result = engine.call_fn::<()>(scope, ast, event, (arg,));
+
+ if let Err(err) = result {
+ eprintln!("! {err}")
+ }
+ }
+ }
+ }
+
+ println!("Bye!");
+}
diff --git a/rhai/examples/event_handler_main/script.rhai b/rhai/examples/event_handler_main/script.rhai
new file mode 100644
index 0000000..fdfbae5
--- /dev/null
+++ b/rhai/examples/event_handler_main/script.rhai
@@ -0,0 +1,56 @@
+//! Implementation of the Event Handler With State Pattern - Main Style
+
+/// Initialize user-provided state (shadows system-provided state, if any).
+fn init() {
+ // Add 'bool_state' and 'value' as new state variables
+ let bool_state = false;
+ let value = 0;
+
+ // Constants can also be added!
+ const EXTRA_CONSTANT = "hello, world!";
+}
+
+/// Without 'OOP' support, the can only be a function.
+fn log(value, data) {
+ print(`State = ${value}, data = ${data}`);
+}
+
+/// 'start' event handler
+fn start(data) {
+ if bool_state {
+ throw "Already started!";
+ }
+ if value <= 0 {
+ throw "Conditions not yet ready to start!";
+ }
+
+ // Constants 'MY_CONSTANT' and 'EXTRA_CONSTANT'
+ // in custom scope are also visible!
+ print(`MY_CONSTANT = ${MY_CONSTANT}`);
+ print(`EXTRA_CONSTANT = ${EXTRA_CONSTANT}`);
+
+ value += parse_int(data);
+ bool_state = true;
+}
+
+/// 'end' event handler
+fn end(data) {
+ if !bool_state {
+ throw "Not yet started!";
+ }
+ if value > 0 {
+ throw "Conditions not yet ready to end!";
+ }
+ value = parse_int(data);
+ bool_state = false;
+}
+
+/// 'update' event handler
+fn update(data) {
+ let data = parse_int(data);
+
+ value += data;
+
+ // Without OOP support, can only call function
+ log(value, data);
+}
diff --git a/rhai/examples/event_handler_map/main.rs b/rhai/examples/event_handler_map/main.rs
new file mode 100644
index 0000000..9bef50b
--- /dev/null
+++ b/rhai/examples/event_handler_map/main.rs
@@ -0,0 +1,151 @@
+//! Implementation of the Event Handler With State Pattern - Map Style
+
+#[cfg(any(feature = "no_function", feature = "no_object"))]
+pub fn main() {
+ panic!("This example does not run under 'no_function' or 'no_object'.")
+}
+
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_object"))]
+pub fn main() {
+ use rhai::{Dynamic, Engine, Map, Scope, AST};
+ use std::io::{stdin, stdout, Write};
+
+ const SCRIPT_FILE: &str = "event_handler_map/script.rhai";
+
+ #[derive(Debug)]
+ struct Handler {
+ pub engine: Engine,
+ pub scope: Scope<'static>,
+ pub ast: AST,
+ }
+
+ fn print_scope(scope: &Scope) {
+ for (i, (name, constant, value)) in scope.iter_raw().enumerate() {
+ #[cfg(not(feature = "no_closure"))]
+ let value_is_shared = if value.is_shared() { " (shared)" } else { "" };
+ #[cfg(feature = "no_closure")]
+ let value_is_shared = "";
+
+ println!(
+ "[{}] {}{}{} = {:?}",
+ i + 1,
+ if constant { "const " } else { "" },
+ name,
+ value_is_shared,
+ *value.read_lock::<Dynamic>().unwrap(),
+ )
+ }
+ println!();
+ }
+
+ println!("Events Handler Example - Map Style");
+ println!("==================================");
+
+ let mut input = String::new();
+
+ // Read script file
+ print!("Script file [{}]: ", SCRIPT_FILE);
+ stdout().flush().expect("flush stdout");
+
+ input.clear();
+
+ stdin().read_line(&mut input).expect("read input");
+
+ let path = match input.trim() {
+ "" => SCRIPT_FILE,
+ path => path,
+ };
+
+ // Create Engine
+ let mut engine = Engine::new();
+
+ // Prevent shadowing of `state`
+ #[allow(deprecated)]
+ engine.on_def_var(|_, info, _| Ok(info.name != "state"));
+
+ // Create a custom 'Scope' to hold state
+ let mut scope = Scope::new();
+
+ // Add any system-provided state into the custom 'Scope'.
+ // Constants can be used to optimize the script.
+ scope.push_constant("MY_CONSTANT", 42_i64);
+
+ // Use an object map to hold state
+ let mut states = Map::new();
+
+ // Default states can be added
+ states.insert("bool_state".into(), Dynamic::FALSE);
+
+ // Add the main states-holding object map and call it 'state'
+ scope.push("state", states);
+
+ // Compile the handler script.
+ println!("> Loading script file: {path}");
+
+ let ast = match engine.compile_file_with_scope(&scope, path.into()) {
+ Ok(ast) => ast,
+ Err(err) => {
+ eprintln!("! Error: {err}");
+ println!("Cannot continue. Bye!");
+ return;
+ }
+ };
+
+ println!("> Script file loaded.");
+ println!();
+ println!("quit = exit program");
+ println!("scope = print scope");
+ println!("event arg = run function with argument");
+ println!();
+
+ // Run the 'init' function to initialize the state, retaining variables.
+ let result = engine.call_fn::<()>(&mut scope, &ast, "init", ());
+
+ if let Err(err) = result {
+ eprintln!("! {err}")
+ }
+
+ // Create handler instance
+ let mut handler = Handler { engine, scope, ast };
+
+ // Events loop
+ loop {
+ print!("event> ");
+ stdout().flush().expect("flush stdout");
+
+ // Read event
+ input.clear();
+ stdin().read_line(&mut input).expect("read input");
+
+ let mut fields = input.trim().splitn(2, ' ');
+
+ let event = fields.next().expect("event").trim();
+ let arg = fields.next().unwrap_or("").to_string();
+
+ // Process event
+ match event {
+ "quit" => break,
+
+ "scope" => {
+ print_scope(&handler.scope);
+ continue;
+ }
+
+ // Map all other events to function calls
+ _ => {
+ let engine = &handler.engine;
+ let scope = &mut handler.scope;
+ let ast = &handler.ast;
+
+ let result = engine.call_fn::<()>(scope, ast, event, (arg,));
+
+ if let Err(err) = result {
+ eprintln!("! {err}")
+ }
+ }
+ }
+ }
+
+ println!("Bye!");
+}
diff --git a/rhai/examples/event_handler_map/script.rhai b/rhai/examples/event_handler_map/script.rhai
new file mode 100644
index 0000000..fd604f6
--- /dev/null
+++ b/rhai/examples/event_handler_map/script.rhai
@@ -0,0 +1,58 @@
+//! Implementation of the Event Handler With State Pattern - Map Style
+
+/// Initialize user-provided state.
+/// State is stored inside an object map bound to 'state'.
+fn init() {
+ // Add 'bool_state' as new state variable if one does not exist
+ if "bool_state" !in state {
+ state.bool_state = false;
+ }
+ // Add 'obj_state' as new state variable (overwrites any existing)
+ state.value = 0;
+
+ // Can also add OOP-style functions!
+ state.log = |x| print(`State = ${this.value}, data = ${x}`);
+}
+
+/// 'start' event handler
+fn start(data) {
+ // Can detect system-provided default states!
+ // Access state variables in 'state'
+ if state.bool_state {
+ throw "Already started!";
+ }
+
+ // New values can be added to the state
+ state.start_mode = data;
+
+ if state.value <= 0 {
+ throw "Conditions not yet ready to start!";
+ }
+
+ // Constant 'MY_CONSTANT' in custom scope is also visible!
+ print(`MY_CONSTANT = ${MY_CONSTANT}`);
+
+ state.value = parse_int(data);
+ state.bool_state = true;
+}
+
+/// 'end' event handler
+fn end(data) {
+ if !state.bool_state || "start_mode" !in state {
+ throw "Not yet started!";
+ }
+ if state.value > 0 {
+ throw "Conditions not yet ready to end!";
+ }
+ state.value = parse_int(data);
+ state.bool_state = false;
+}
+
+/// 'update' event handler
+fn update(data) {
+ let data = parse_int(data);
+ state.value += data;
+
+ // Call user-defined function OOP-style!
+ state.log(data);
+}
diff --git a/rhai/examples/hello.rs b/rhai/examples/hello.rs
new file mode 100644
index 0000000..dba66e5
--- /dev/null
+++ b/rhai/examples/hello.rs
@@ -0,0 +1,15 @@
+//! A simple example that evaluates an expression and prints the result.
+
+use rhai::{Engine, EvalAltResult};
+
+fn main() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ engine.run(r#"print("hello, world!")"#)?;
+
+ let result = engine.eval::<i64>("40 + 2")?;
+
+ println!("The Answer: {result}"); // prints 42
+
+ Ok(())
+}
diff --git a/rhai/examples/reuse_scope.rs b/rhai/examples/reuse_scope.rs
new file mode 100644
index 0000000..486382a
--- /dev/null
+++ b/rhai/examples/reuse_scope.rs
@@ -0,0 +1,22 @@
+//! An example that evaluates two pieces of code in separate runs, but using a common `Scope`.
+
+use rhai::{Engine, EvalAltResult, Scope};
+
+fn main() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+
+ engine.run_with_scope(&mut scope, "let x = 4 + 5")?;
+
+ println!("x = {}", scope.get_value::<i64>("x").unwrap());
+
+ for _ in 0..10 {
+ let result = engine.eval_with_scope::<i64>(&mut scope, "x += 1; x")?;
+
+ println!("result: {result}");
+ }
+
+ println!("x = {}", scope.get_value::<i64>("x").unwrap());
+
+ Ok(())
+}
diff --git a/rhai/examples/serde.rs b/rhai/examples/serde.rs
new file mode 100644
index 0000000..cbdab2b
--- /dev/null
+++ b/rhai/examples/serde.rs
@@ -0,0 +1,86 @@
+//! An example to serialize and deserialize Rust types.
+
+#[cfg(feature = "no_object")]
+fn main() {
+ panic!("This example does not run under 'no_object'.")
+}
+
+#[cfg(not(feature = "no_object"))]
+fn main() {
+ use rhai::serde::{from_dynamic, to_dynamic};
+ use rhai::{Dynamic, Engine, Map};
+ use serde::{Deserialize, Serialize};
+
+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+ struct Point {
+ x: f64,
+ y: f64,
+ }
+
+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+ struct MyStruct {
+ a: i64,
+ b: Vec<String>,
+ c: bool,
+ d: Point,
+ }
+
+ pub fn ser() {
+ let x = MyStruct {
+ a: 42,
+ b: vec!["hello".into(), "world".into()],
+ c: true,
+ d: Point {
+ x: 123.456,
+ y: 999.0,
+ },
+ };
+
+ println!("Source struct: {x:#?}");
+
+ // Convert the 'MyStruct' into a 'Dynamic'
+ let map: Dynamic = to_dynamic(x).unwrap();
+
+ assert!(map.is::<Map>());
+ println!("Serialized to Dynamic: {map:#?}");
+ }
+
+ pub fn de() {
+ let engine = Engine::new();
+ let result: Dynamic = engine
+ .eval(
+ r#"
+ #{
+ a: 42,
+ b: [ "hello", "world" ],
+ c: true,
+ d: #{ x: 123.456, y: 999.0 }
+ }
+ "#,
+ )
+ .unwrap();
+
+ println!("Source Dynamic: {result:#?}");
+
+ // Convert the 'Dynamic' object map into 'MyStruct'
+ let x: MyStruct = from_dynamic(&result).unwrap();
+
+ assert_eq!(
+ x,
+ MyStruct {
+ a: 42,
+ b: vec!["hello".into(), "world".into()],
+ c: true,
+ d: Point {
+ x: 123.456,
+ y: 999.0,
+ },
+ }
+ );
+ println!("Deserialized to struct: {x:#?}");
+ }
+
+ ser();
+ println!();
+ de();
+}
diff --git a/rhai/examples/simple_fn.rs b/rhai/examples/simple_fn.rs
new file mode 100644
index 0000000..887cad3
--- /dev/null
+++ b/rhai/examples/simple_fn.rs
@@ -0,0 +1,19 @@
+//! An example showing how to register a simple Rust function.
+
+use rhai::{Engine, EvalAltResult};
+
+fn add(x: i64, y: i64) -> i64 {
+ x + y
+}
+
+fn main() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_fn("add", add);
+
+ let result = engine.eval::<i64>("add(40, 2)")?;
+
+ println!("Answer: {result}"); // prints 42
+
+ Ok(())
+}
diff --git a/rhai/examples/strings.rs b/rhai/examples/strings.rs
new file mode 100644
index 0000000..b9c7285
--- /dev/null
+++ b/rhai/examples/strings.rs
@@ -0,0 +1,79 @@
+//! An example that registers a variety of functions that operate on strings.
+//! Remember to use `ImmutableString` or `&str` instead of `String` as parameters.
+
+use rhai::{Engine, EvalAltResult, ImmutableString, Scope};
+use std::io::{stdin, stdout, Write};
+
+/// Trim whitespace from a string. The original string argument is changed.
+///
+/// This version uses `&mut ImmutableString`
+fn trim_string(s: &mut ImmutableString) {
+ *s = s.trim().into();
+}
+
+/// Notice this is different from the built-in Rhai 'len' function for strings
+/// which counts the actual number of Unicode _characters_ in a string.
+///
+/// This version simply counts the number of _bytes_ in the UTF-8 representation.
+///
+/// This version uses `&str`.
+fn count_string_bytes(s: &str) -> i64 {
+ s.len() as i64
+}
+
+/// This version uses `ImmutableString` and `&str`.
+fn find_substring(s: ImmutableString, sub: &str) -> i64 {
+ s.find(sub).map(|x| x as i64).unwrap_or(-1)
+}
+
+fn main() -> Result<(), Box<EvalAltResult>> {
+ // Create a `raw` Engine with no built-in string functions.
+ let mut engine = Engine::new_raw();
+
+ engine
+ // Register string functions
+ .register_fn("trim", trim_string)
+ .register_fn("len", count_string_bytes)
+ .register_fn("index_of", find_substring)
+ // Register string functions using closures
+ .register_fn("display", |label: &str, value: i64| {
+ println!("{label}: {value}")
+ })
+ .register_fn("display", |label: ImmutableString, value: &str| {
+ println!(r#"{label}: "{value}""#) // Quote the input string
+ });
+
+ let mut scope = Scope::new();
+ let mut input = String::new();
+
+ loop {
+ scope.clear();
+
+ println!("Type something. Press Ctrl-C to exit.");
+ print!("strings> ");
+ stdout().flush().expect("couldn't flush stdout");
+
+ input.clear();
+
+ if let Err(err) = stdin().read_line(&mut input) {
+ panic!("input error: {}", err);
+ }
+
+ scope.push("x", input.clone());
+
+ println!("Line: {}", input.replace('\r', "\\r").replace('\n', "\\n"));
+
+ engine.run_with_scope(
+ &mut scope,
+ r#"
+ display("Length", x.len());
+ x.trim();
+ display("Trimmed", x);
+ display("Trimmed Length", x.len());
+ display("Index of \"!!!\"", x.index_of("!!!"));
+ "#,
+ )?;
+
+ println!();
+ }
+}
diff --git a/rhai/examples/threading.rs b/rhai/examples/threading.rs
new file mode 100644
index 0000000..adcf9be
--- /dev/null
+++ b/rhai/examples/threading.rs
@@ -0,0 +1,69 @@
+//! An advanced example showing how to communicate with an `Engine` running in a separate thread via
+//! an MPSC channel.
+
+use rhai::Engine;
+
+#[cfg(feature = "sync")]
+use std::sync::Mutex;
+
+fn main() {
+ // Channel: Script -> Master
+ let (tx_script, rx_master) = std::sync::mpsc::channel();
+ // Channel: Master -> Script
+ let (tx_master, rx_script) = std::sync::mpsc::channel();
+
+ #[cfg(feature = "sync")]
+ let (tx_script, rx_script) = (Mutex::new(tx_script), Mutex::new(rx_script));
+
+ // Spawn thread with Engine
+ std::thread::spawn(move || {
+ // Create Engine
+ let mut engine = Engine::new();
+
+ // Register API
+ // Notice that the API functions are blocking
+
+ #[cfg(not(feature = "sync"))]
+ engine
+ .register_fn("get", move || rx_script.recv().unwrap_or_default())
+ .register_fn("put", move |v: i64| tx_script.send(v).unwrap());
+
+ #[cfg(feature = "sync")]
+ engine
+ .register_fn("get", move || rx_script.lock().unwrap().recv().unwrap())
+ .register_fn("put", move |v: i64| {
+ tx_script.lock().unwrap().send(v).unwrap()
+ });
+
+ // Run script
+ engine
+ .run(
+ r#"
+ print("Starting script loop...");
+
+ loop {
+ let x = get();
+ print(`Script Read: ${x}`);
+ x += 1;
+ print(`Script Write: ${x}`);
+ put(x);
+ }
+ "#,
+ )
+ .unwrap();
+ });
+
+ // This is the main processing thread
+
+ println!("Starting main loop...");
+
+ let mut value: i64 = 0;
+
+ while value < 10 {
+ println!("Value: {value}");
+ // Send value to script
+ tx_master.send(value).unwrap();
+ // Receive value from script
+ value = rx_master.recv().unwrap();
+ }
+}
diff --git a/rhai/no_std/no_std_test/Cargo.toml b/rhai/no_std/no_std_test/Cargo.toml
new file mode 100644
index 0000000..e500527
--- /dev/null
+++ b/rhai/no_std/no_std_test/Cargo.toml
@@ -0,0 +1,36 @@
+[workspace]
+
+[package]
+name = "no_std_test"
+version = "0.1.0"
+edition = "2018"
+authors = ["Stephen Chung"]
+description = "no-std test application"
+homepage = "https://github.com/rhaiscript/rhai/tree/no_std/no_std_test"
+repository = "https://github.com/rhaiscript/rhai"
+
+[dependencies]
+rhai = { path = "../../", features = [ "no_std" ], default_features = false }
+wee_alloc = { version = "0.4.5", default_features = false }
+
+[profile.dev]
+panic = "abort"
+
+[profile.release]
+opt-level = "z" # optimize for size
+debug = false
+rpath = false
+debug-assertions = false
+codegen-units = 1
+panic = "abort"
+
+[profile.unix]
+inherits = "release"
+lto = true
+
+[profile.windows]
+inherits = "release"
+
+[profile.macos]
+inherits = "release"
+lto = "fat"
diff --git a/rhai/no_std/no_std_test/README.md b/rhai/no_std/no_std_test/README.md
new file mode 100644
index 0000000..3559a4a
--- /dev/null
+++ b/rhai/no_std/no_std_test/README.md
@@ -0,0 +1,26 @@
+`no-std` Test Sample
+====================
+
+This sample application is a bare-bones `no-std` build for testing.
+
+[`wee_alloc`](https://crates.io/crates/wee_alloc) is used as the allocator.
+
+
+To Build
+--------
+
+The nightly compiler is required:
+
+```bash
+cargo +nightly build --release
+```
+
+A specific profile can also be used:
+
+```bash
+cargo +nightly build --profile unix
+```
+
+Three profiles are defined: `unix`, `windows` and `macos`.
+
+The release build is optimized for size. It can be changed to optimize on speed instead.
diff --git a/rhai/no_std/no_std_test/src/main.rs b/rhai/no_std/no_std_test/src/main.rs
new file mode 100644
index 0000000..621029e
--- /dev/null
+++ b/rhai/no_std/no_std_test/src/main.rs
@@ -0,0 +1,55 @@
+//! This is a bare-bones `no-std` application that evaluates
+//! a simple expression and uses the result as the return value.
+
+#![no_std]
+#![feature(alloc_error_handler, start, core_intrinsics, lang_items, link_cfg)]
+
+extern crate alloc;
+extern crate wee_alloc;
+
+#[global_allocator]
+static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
+
+// NB: Rust needs a CRT runtime on Windows MSVC.
+#[cfg(all(windows, target_env = "msvc"))]
+#[link(name = "msvcrt")]
+#[link(name = "libcmt")]
+extern "C" {}
+
+use rhai::{Engine, INT};
+
+#[start]
+fn main(_argc: isize, _argv: *const *const u8) -> isize {
+ // Notice that this is a _raw_ engine.
+ // To do anything useful, load a few packages from `rhai::packages`.
+ let engine = Engine::new_raw();
+
+ // Evaluate a simple expression: 40 + 2
+ engine.eval_expression::<INT>("40 + 2").unwrap() as isize
+}
+
+#[alloc_error_handler]
+fn foo(_: core::alloc::Layout) -> ! {
+ core::intrinsics::abort();
+}
+
+#[panic_handler]
+#[lang = "panic_impl"]
+extern "C" fn rust_begin_panic(_: &core::panic::PanicInfo) -> ! {
+ core::intrinsics::abort();
+}
+
+#[no_mangle]
+extern "C" fn _rust_eh_personality() {}
+
+#[no_mangle]
+extern "C" fn rust_eh_personality() {}
+
+#[no_mangle]
+extern "C" fn rust_eh_register_frames() {}
+
+#[no_mangle]
+extern "C" fn rust_eh_unregister_frames() {}
+
+#[no_mangle]
+extern "C" fn _Unwind_Resume() {}
diff --git a/rhai/scripts/README.md b/rhai/scripts/README.md
new file mode 100644
index 0000000..45f53ac
--- /dev/null
+++ b/rhai/scripts/README.md
@@ -0,0 +1,30 @@
+Testing Scripts
+===============
+
+Testing scripts written in Rhai.
+
+
+Install `rhai-run` Tool
+-----------------------
+
+Use the following command to install the `rhai-run` tool:
+
+```sh
+cargo install --path . --bin rhai-run
+```
+
+
+How to Run
+----------
+
+Run scripts using the `rhai-run` tool:
+
+```sh
+rhai-run ./scripts/test_script_to_run.rhai
+```
+
+or
+
+```sh
+cargo run --bin rhai-run ./scripts/test_script_to_run.rhai
+```
diff --git a/rhai/scripts/all_in_one.d.rhai b/rhai/scripts/all_in_one.d.rhai
new file mode 100644
index 0000000..c7fdc94
--- /dev/null
+++ b/rhai/scripts/all_in_one.d.rhai
@@ -0,0 +1,6480 @@
+module static;
+
+op ==(int, int) -> bool;
+op !=(int, int) -> bool;
+op >(int, int) -> bool;
+op >=(int, int) -> bool;
+op <(int, int) -> bool;
+op <=(int, int) -> bool;
+op &(int, int) -> int;
+op |(int, int) -> int;
+op ^(int, int) -> int;
+op ..(int, int) -> Range<int>;
+op ..=(int, int) -> RangeInclusive<int>;
+
+op ==(bool, bool) -> bool;
+op !=(bool, bool) -> bool;
+op >(bool, bool) -> bool;
+op >=(bool, bool) -> bool;
+op <(bool, bool) -> bool;
+op <=(bool, bool) -> bool;
+op &(bool, bool) -> bool;
+op |(bool, bool) -> bool;
+op ^(bool, bool) -> bool;
+
+op ==((), ()) -> bool;
+op !=((), ()) -> bool;
+op >((), ()) -> bool;
+op >=((), ()) -> bool;
+op <((), ()) -> bool;
+op <=((), ()) -> bool;
+
+op +(int, int) -> int;
+op -(int, int) -> int;
+op *(int, int) -> int;
+op /(int, int) -> int;
+op %(int, int) -> int;
+op **(int, int) -> int;
+op >>(int, int) -> int;
+op <<(int, int) -> int;
+
+op +(float, float) -> float;
+op -(float, float) -> float;
+op *(float, float) -> float;
+op /(float, float) -> float;
+op %(float, float) -> float;
+op **(float, float) -> float;
+op ==(float, float) -> bool;
+op !=(float, float) -> bool;
+op >(float, float) -> bool;
+op >=(float, float) -> bool;
+op <(float, float) -> bool;
+op <=(float, float) -> bool;
+
+op +(float, int) -> float;
+op -(float, int) -> float;
+op *(float, int) -> float;
+op /(float, int) -> float;
+op %(float, int) -> float;
+op **(float, int) -> float;
+op ==(float, int) -> bool;
+op !=(float, int) -> bool;
+op >(float, int) -> bool;
+op >=(float, int) -> bool;
+op <(float, int) -> bool;
+op <=(float, int) -> bool;
+
+op +(int, float) -> float;
+op -(int, float) -> float;
+op *(int, float) -> float;
+op /(int, float) -> float;
+op %(int, float) -> float;
+op **(int, float) -> float;
+op ==(int, float) -> bool;
+op !=(int, float) -> bool;
+op >(int, float) -> bool;
+op >=(int, float) -> bool;
+op <(int, float) -> bool;
+op <=(int, float) -> bool;
+
+op +(Decimal, Decimal) -> Decimal;
+op -(Decimal, Decimal) -> Decimal;
+op *(Decimal, Decimal) -> Decimal;
+op /(Decimal, Decimal) -> Decimal;
+op %(Decimal, Decimal) -> Decimal;
+op **(Decimal, Decimal) -> Decimal;
+op ==(Decimal, Decimal) -> bool;
+op !=(Decimal, Decimal) -> bool;
+op >(Decimal, Decimal) -> bool;
+op >=(Decimal, Decimal) -> bool;
+op <(Decimal, Decimal) -> bool;
+op <=(Decimal, Decimal) -> bool;
+
+op +(Decimal, int) -> Decimal;
+op -(Decimal, int) -> Decimal;
+op *(Decimal, int) -> Decimal;
+op /(Decimal, int) -> Decimal;
+op %(Decimal, int) -> Decimal;
+op **(Decimal, int) -> Decimal;
+op ==(Decimal, int) -> bool;
+op !=(Decimal, int) -> bool;
+op >(Decimal, int) -> bool;
+op >=(Decimal, int) -> bool;
+op <(Decimal, int) -> bool;
+op <=(Decimal, int) -> bool;
+
+op +(int, Decimal) -> Decimal;
+op -(int, Decimal) -> Decimal;
+op *(int, Decimal) -> Decimal;
+op /(int, Decimal) -> Decimal;
+op %(int, Decimal) -> Decimal;
+op **(int, Decimal) -> Decimal;
+op ==(int, Decimal) -> bool;
+op !=(int, Decimal) -> bool;
+op >(int, Decimal) -> bool;
+op >=(int, Decimal) -> bool;
+op <(int, Decimal) -> bool;
+op <=(int, Decimal) -> bool;
+
+op +(String, String) -> String;
+op -(String, String) -> String;
+op ==(String, String) -> bool;
+op !=(String, String) -> bool;
+op >(String, String) -> bool;
+op >=(String, String) -> bool;
+op <(String, String) -> bool;
+op <=(String, String) -> bool;
+
+op +(char, char) -> String;
+op ==(char, char) -> bool;
+op !=(char, char) -> bool;
+op >(char, char) -> bool;
+op >=(char, char) -> bool;
+op <(char, char) -> bool;
+op <=(char, char) -> bool;
+
+op +(char, String) -> String;
+op ==(char, String) -> bool;
+op !=(char, String) -> bool;
+op >(char, String) -> bool;
+op >=(char, String) -> bool;
+op <(char, String) -> bool;
+op <=(char, String) -> bool;
+
+op +(String, char) -> String;
+op -(String, char) -> String;
+op ==(String, char) -> bool;
+op !=(String, char) -> bool;
+op >(String, char) -> bool;
+op >=(String, char) -> bool;
+op <(String, char) -> bool;
+op <=(String, char) -> bool;
+
+op +((), String) -> String;
+op ==((), String) -> bool;
+op !=((), String) -> bool;
+op >((), String) -> bool;
+op >=((), String) -> bool;
+op <((), String) -> bool;
+op <=((), String) -> bool;
+
+op +(String, ()) -> String;
+op ==(String, ()) -> bool;
+op !=(String, ()) -> bool;
+op >(String, ()) -> bool;
+op >=(String, ()) -> bool;
+op <(String, ()) -> bool;
+op <=(String, ()) -> bool;
+
+op +(Blob, Blob) -> Blob;
+op +(Blob, char) -> Blob;
+op ==(Blob, Blob) -> bool;
+op !=(Blob, Blob) -> bool;
+
+
+op ==(Range<int>, RangeInclusive<int>) -> bool;
+op !=(Range<int>, RangeInclusive<int>) -> bool;
+
+op ==(RangeInclusive<int>, Range<int>) -> bool;
+op !=(RangeInclusive<int>, Range<int>) -> bool;
+
+op ==(Range<int>, Range<int>) -> bool;
+op !=(Range<int>, Range<int>) -> bool;
+
+op ==(RangeInclusive<int>, RangeInclusive<int>) -> bool;
+op !=(RangeInclusive<int>, RangeInclusive<int>) -> bool;
+
+op ==(?, ?) -> bool;
+op !=(?, ?) -> bool;
+op >(?, ?) -> bool;
+op >=(?, ?) -> bool;
+op <(?, ?) -> bool;
+op <=(?, ?) -> bool;
+
+
+op &=(bool, bool);
+op |=(bool, bool);
+
+op +=(int, int);
+op -=(int, int);
+op *=(int, int);
+op /=(int, int);
+op %=(int, int);
+op **=(int, int);
+op >>=(int, int);
+op <<=(int, int);
+op &=(int, int);
+op |=(int, int);
+op ^=(int, int);
+
+op +=(float, float);
+op -=(float, float);
+op *=(float, float);
+op /=(float, float);
+op %=(float, float);
+op **=(float, float);
+
+op +=(float, int);
+op -=(float, int);
+op *=(float, int);
+op /=(float, int);
+op %=(float, int);
+op **=(float, int);
+
+op +=(Decimal, Decimal);
+op -=(Decimal, Decimal);
+op *=(Decimal, Decimal);
+op /=(Decimal, Decimal);
+op %=(Decimal, Decimal);
+op **=(Decimal, Decimal);
+
+op +=(Decimal, int);
+op -=(Decimal, int);
+op *=(Decimal, int);
+op /=(Decimal, int);
+op %=(Decimal, int);
+op **=(Decimal, int);
+
+op +=(String, String);
+op -=(String, String);
+op +=(String, char);
+op -=(String, char);
+op +=(char, String);
+op +=(char, char);
+
+op +=(Array, Array);
+op +=(Array, ?);
+
+op +=(Blob, Blob);
+op +=(Blob, int);
+op +=(Blob, char);
+op +=(Blob, String);
+
+op in(?, Array) -> bool;
+op in(String, String) -> bool;
+op in(char, String) -> bool;
+op in(int, Range<int>) -> bool;
+op in(int, RangeInclusive<int>) -> bool;
+op in(String, Map) -> bool;
+op in(int, Blob) -> bool;
+
+/// Display any data to the standard output.
+///
+/// # Example
+///
+/// ```rhai
+/// let answer = 42;
+///
+/// print(`The Answer is ${answer}`);
+/// ```
+fn print(data: ?);
+
+/// Display any data to the standard output in debug format.
+///
+/// # Example
+///
+/// ```rhai
+/// let answer = 42;
+///
+/// debug(answer);
+/// ```
+fn debug(data: ?);
+
+/// Get the type of a value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// print(x.type_of()); // prints "string"
+/// ```
+fn type_of(data: ?) -> String;
+
+/// Create a function pointer to a named function.
+///
+/// If the specified name is not a valid function name, an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = Fn("foo"); // function pointer to 'foo'
+///
+/// f.call(42); // call: foo(42)
+/// ```
+fn Fn(fn_name: String) -> FnPtr;
+
+/// Call a function pointed to by a function pointer,
+/// passing following arguments to the function call.
+///
+/// If an appropriate function is not found, an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = Fn("foo"); // function pointer to 'foo'
+///
+/// f.call(1, 2, 3); // call: foo(1, 2, 3)
+/// ```
+fn call(fn_ptr: FnPtr, ...args: ?) -> ?;
+
+/// Call a function pointed to by a function pointer, binding the `this` pointer
+/// to the object of the method call, and passing on following arguments to the function call.
+///
+/// If an appropriate function is not found, an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// fn add(x) {
+/// this + x
+/// }
+///
+/// let f = Fn("add"); // function pointer to 'add'
+///
+/// let x = 41;
+///
+/// let r = x.call(f, 1); // call: add(1) with 'this' = 'x'
+///
+/// print(r); // prints 42
+/// ```
+fn call(obj: ?, fn_ptr: FnPtr, ...args: ?) -> ?;
+
+/// Curry a number of arguments into a function pointer and return it as a new function pointer.
+///
+/// # Example
+///
+/// ```rhai
+/// fn foo(x, y, z) {
+/// x + y + z
+/// }
+///
+/// let f = Fn("foo");
+///
+/// let g = f.curry(1, 2); // curried arguments: 1, 2
+///
+/// g.call(3); // call: foo(1, 2, 3)
+/// ```
+fn curry(fn_ptr: FnPtr, ...args: ?) -> FnPtr;
+
+/// Return `true` if a script-defined function exists with a specified name and
+/// number of parameters.
+///
+/// # Example
+///
+/// ```rhai
+/// fn foo(x) { }
+///
+/// print(is_def_fn("foo", 1)); // prints true
+/// print(is_def_fn("foo", 2)); // prints false
+/// print(is_def_fn("foo", 0)); // prints false
+/// print(is_def_fn("bar", 1)); // prints false
+/// ```
+fn is_def_fn(fn_name: String, num_params: int) -> bool;
+
+/// Return `true` if a variable matching a specified name is defined.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 42;
+///
+/// print(is_def_var("x")); // prints true
+/// print(is_def_var("foo")); // prints false
+///
+/// {
+/// let y = 1;
+/// print(is_def_var("y")); // prints true
+/// }
+///
+/// print(is_def_var("y")); // prints false
+/// ```
+fn is_def_var(var_name: String) -> bool;
+
+/// Return `true` if the variable is shared.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 42;
+///
+/// print(is_shared(x)); // prints false
+///
+/// let f = || x; // capture 'x', making it shared
+///
+/// print(is_shared(x)); // prints true
+/// ```
+fn is_shared(variable: ?) -> bool;
+
+/// Evaluate a text script within the current scope.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 42;
+///
+/// eval("let y = x; x = 123;");
+///
+/// print(x); // prints 123
+/// print(y); // prints 42
+/// ```
+fn eval(script: String) -> ?;
+
+/// Return `true` if the string contains another string.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello world!";
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if "world" in x {
+/// print("found!");
+/// }
+/// ```
+fn contains(string: String, find: String) -> bool;
+
+/// Return `true` if the string contains a character.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello world!";
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 'w' in x {
+/// print("found!");
+/// }
+/// ```
+fn contains(string: String, ch: char) -> bool;
+
+/// Return `true` if a value falls within the exclusive range.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let r = 1..100;
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 42 in r {
+/// print("found!");
+/// }
+/// ```
+fn contains(range: Range<int>, value: int) -> bool;
+
+/// Return `true` if a value falls within the inclusive range.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let r = 1..=100;
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 42 in r {
+/// print("found!");
+/// }
+/// ```
+fn contains(range: RangeInclusive<int>, value: int) -> bool;
+
+/// Return `true` if a key exists within the object map.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if "c" in m {
+/// print("found!");
+/// }
+/// ```
+fn contains(map: Map, string: String) -> bool;
+
+/// Return `true` if a value is found within the BLOB.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 3 in b {
+/// print("found!");
+/// }
+/// ```
+fn contains(blob: Blob, value: int) -> bool;
+
+op minus(int, int) -> int;
+
+op !(bool) -> bool;
+
+/// Return `true` if two arrays are not-equal (i.e. any element not equal or not in the same order).
+///
+/// The operator `==` is used to compare elements and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [1, 2, 3, 4, 5];
+/// let z = [1, 2, 3, 4];
+///
+/// print(x != y); // prints false
+///
+/// print(x != z); // prints true
+/// ```
+op !=(Array, Array) -> bool;
+
+/// Return `true` if two object maps are not equal (i.e. at least one property value is not equal).
+///
+/// The operator `==` is used to compare property values and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let m1 = #{a:1, b:2, c:3};
+/// let m2 = #{a:1, b:2, c:3};
+/// let m3 = #{a:1, c:3};
+///
+/// print(m1 != m2); // prints false
+///
+/// print(m1 != m3); // prints true
+/// ```
+op !=(Map, Map) -> bool;
+
+/// Return `true` if two timestamps are not equal.
+op !=(Instant, Instant) -> bool;
+
+op !=(int, f32) -> bool;
+
+op !=(int, float) -> bool;
+
+op !=(f32, int) -> bool;
+
+op !=(f32, f32) -> bool;
+
+op !=(float, int) -> bool;
+
+op !=(i128, i128) -> bool;
+
+op !=(i16, i16) -> bool;
+
+op !=(i32, i32) -> bool;
+
+op !=(i8, i8) -> bool;
+
+op !=(u128, u128) -> bool;
+
+op !=(u16, u16) -> bool;
+
+op !=(u32, u32) -> bool;
+
+op !=(u64, u64) -> bool;
+
+op !=(u8, u8) -> bool;
+
+op %(int, f32) -> f32;
+
+op %(f32, int) -> f32;
+
+op %(f32, f32) -> f32;
+
+op %(i128, i128) -> i128;
+
+op %(i16, i16) -> i16;
+
+op %(i32, i32) -> i32;
+
+op %(i8, i8) -> i8;
+
+op %(u128, u128) -> u128;
+
+op %(u16, u16) -> u16;
+
+op %(u32, u32) -> u32;
+
+op %(u64, u64) -> u64;
+
+op %(u8, u8) -> u8;
+
+op &(i128, i128) -> i128;
+
+op &(i16, i16) -> i16;
+
+op &(i32, i32) -> i32;
+
+op &(i8, i8) -> i8;
+
+op &(u128, u128) -> u128;
+
+op &(u16, u16) -> u16;
+
+op &(u32, u32) -> u32;
+
+op &(u64, u64) -> u64;
+
+op &(u8, u8) -> u8;
+
+op *(int, f32) -> f32;
+
+op *(f32, int) -> f32;
+
+op *(f32, f32) -> f32;
+
+op *(i128, i128) -> i128;
+
+op *(i16, i16) -> i16;
+
+op *(i32, i32) -> i32;
+
+op *(i8, i8) -> i8;
+
+op *(u128, u128) -> u128;
+
+op *(u16, u16) -> u16;
+
+op *(u32, u32) -> u32;
+
+op *(u64, u64) -> u64;
+
+op *(u8, u8) -> u8;
+
+op **(f32, int) -> f32;
+
+op **(f32, f32) -> f32;
+
+op **(i128, int) -> i128;
+
+op **(i16, int) -> i16;
+
+op **(i32, int) -> i32;
+
+op **(i8, int) -> i8;
+
+op **(u128, int) -> u128;
+
+op **(u16, int) -> u16;
+
+op **(u32, int) -> u32;
+
+op **(u64, int) -> u64;
+
+op **(u8, int) -> u8;
+
+op +(Decimal) -> Decimal;
+
+op +(int) -> int;
+
+op +(f32) -> f32;
+
+op +(float) -> float;
+
+op +(i128) -> i128;
+
+op +(i16) -> i16;
+
+op +(i32) -> i32;
+
+op +(i8) -> i8;
+
+op +((), String) -> String;
+
+/// Combine two arrays into a new array and return it.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+/// let y = [true, 'x'];
+///
+/// print(x + y); // prints "[1, 2, 3, true, 'x']"
+///
+/// print(x); // prints "[1, 2, 3"
+/// ```
+op +(Array, Array) -> Array;
+
+op +(char, String) -> String;
+
+op +(?, String) -> String;
+
+/// Make a copy of the object map, add all property values of another object map
+/// (existing property values of the same names are replaced), then returning it.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+/// let n = #{a: 42, d:0};
+///
+/// print(m + n); // prints "#{a:42, b:2, c:3, d:0}"
+///
+/// print(m); // prints "#{a:1, b:2, c:3}"
+/// ```
+op +(Map, Map) -> Map;
+
+op +(String, String) -> String;
+
+op +(String, char) -> String;
+
+op +(String, ?) -> String;
+
+op +(String, Blob) -> String;
+
+op +(String, ()) -> String;
+
+/// Add the specified number of `seconds` to the timestamp and return it as a new timestamp.
+op +(Instant, float) -> Instant;
+
+/// Add the specified number of `seconds` to the timestamp and return it as a new timestamp.
+op +(Instant, int) -> Instant;
+
+op +(Blob, String) -> String;
+
+op +(int, f32) -> f32;
+
+op +(f32, int) -> f32;
+
+op +(f32, f32) -> f32;
+
+op +(i128, i128) -> i128;
+
+op +(i16, i16) -> i16;
+
+op +(i32, i32) -> i32;
+
+op +(i8, i8) -> i8;
+
+op +(u128, u128) -> u128;
+
+op +(u16, u16) -> u16;
+
+op +(u32, u32) -> u32;
+
+op +(u64, u64) -> u64;
+
+op +(u8, u8) -> u8;
+
+/// Add all property values of another object map into the object map.
+/// Existing property values of the same names are replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+/// let n = #{a: 42, d:0};
+///
+/// m.mixin(n);
+///
+/// print(m); // prints "#{a:42, b:2, c:3, d:0}"
+/// ```
+op +=(Map, Map) -> ();
+
+op +=(String, String) -> ();
+
+op +=(String, char) -> ();
+
+op +=(String, ()) -> ();
+
+op +=(String, ?) -> ();
+
+op +=(String, Blob) -> ();
+
+/// Add the specified number of `seconds` to the timestamp.
+op +=(Instant, float) -> ();
+
+/// Add the specified number of `seconds` to the timestamp.
+op +=(Instant, int) -> ();
+
+op -(Decimal) -> Decimal;
+
+op -(int) -> int;
+
+op -(f32) -> f32;
+
+op -(float) -> float;
+
+op -(i128) -> i128;
+
+op -(i16) -> i16;
+
+op -(i32) -> i32;
+
+op -(i8) -> i8;
+
+/// Return the number of seconds between two timestamps.
+op -(Instant, Instant) -> RhaiResult;
+
+/// Subtract the specified number of `seconds` from the timestamp and return it as a new timestamp.
+op -(Instant, float) -> Instant;
+
+/// Subtract the specified number of `seconds` from the timestamp and return it as a new timestamp.
+op -(Instant, int) -> Instant;
+
+op -(int, f32) -> f32;
+
+op -(f32, int) -> f32;
+
+op -(f32, f32) -> f32;
+
+op -(i128, i128) -> i128;
+
+op -(i16, i16) -> i16;
+
+op -(i32, i32) -> i32;
+
+op -(i8, i8) -> i8;
+
+op -(u128, u128) -> u128;
+
+op -(u16, u16) -> u16;
+
+op -(u32, u32) -> u32;
+
+op -(u64, u64) -> u64;
+
+op -(u8, u8) -> u8;
+
+/// Subtract the specified number of `seconds` from the timestamp.
+op -=(Instant, float) -> ();
+
+/// Subtract the specified number of `seconds` from the timestamp.
+op -=(Instant, int) -> ();
+
+op /(int, f32) -> f32;
+
+op /(f32, int) -> f32;
+
+op /(f32, f32) -> f32;
+
+op /(i128, i128) -> i128;
+
+op /(i16, i16) -> i16;
+
+op /(i32, i32) -> i32;
+
+op /(i8, i8) -> i8;
+
+op /(u128, u128) -> u128;
+
+op /(u16, u16) -> u16;
+
+op /(u32, u32) -> u32;
+
+op /(u64, u64) -> u64;
+
+op /(u8, u8) -> u8;
+
+/// Return `true` if the first timestamp is earlier than the second.
+op <(Instant, Instant) -> bool;
+
+op <(int, f32) -> bool;
+
+op <(int, float) -> bool;
+
+op <(f32, int) -> bool;
+
+op <(f32, f32) -> bool;
+
+op <(float, int) -> bool;
+
+op <(i128, i128) -> bool;
+
+op <(i16, i16) -> bool;
+
+op <(i32, i32) -> bool;
+
+op <(i8, i8) -> bool;
+
+op <(u128, u128) -> bool;
+
+op <(u16, u16) -> bool;
+
+op <(u32, u32) -> bool;
+
+op <(u64, u64) -> bool;
+
+op <(u8, u8) -> bool;
+
+op <<(i128, int) -> i128;
+
+op <<(i16, int) -> i16;
+
+op <<(i32, int) -> i32;
+
+op <<(i8, int) -> i8;
+
+op <<(u128, int) -> u128;
+
+op <<(u16, int) -> u16;
+
+op <<(u32, int) -> u32;
+
+op <<(u64, int) -> u64;
+
+op <<(u8, int) -> u8;
+
+/// Return `true` if the first timestamp is earlier than or equals to the second.
+op <=(Instant, Instant) -> bool;
+
+op <=(int, f32) -> bool;
+
+op <=(int, float) -> bool;
+
+op <=(f32, int) -> bool;
+
+op <=(f32, f32) -> bool;
+
+op <=(float, int) -> bool;
+
+op <=(i128, i128) -> bool;
+
+op <=(i16, i16) -> bool;
+
+op <=(i32, i32) -> bool;
+
+op <=(i8, i8) -> bool;
+
+op <=(u128, u128) -> bool;
+
+op <=(u16, u16) -> bool;
+
+op <=(u32, u32) -> bool;
+
+op <=(u64, u64) -> bool;
+
+op <=(u8, u8) -> bool;
+
+/// Return `true` if two arrays are equal (i.e. all elements are equal and in the same order).
+///
+/// The operator `==` is used to compare elements and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [1, 2, 3, 4, 5];
+/// let z = [1, 2, 3, 4];
+///
+/// print(x == y); // prints true
+///
+/// print(x == z); // prints false
+/// ```
+op ==(Array, Array) -> bool;
+
+/// Return `true` if two object maps are equal (i.e. all property values are equal).
+///
+/// The operator `==` is used to compare property values and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let m1 = #{a:1, b:2, c:3};
+/// let m2 = #{a:1, b:2, c:3};
+/// let m3 = #{a:1, c:3};
+///
+/// print(m1 == m2); // prints true
+///
+/// print(m1 == m3); // prints false
+/// ```
+op ==(Map, Map) -> bool;
+
+/// Return `true` if two timestamps are equal.
+op ==(Instant, Instant) -> bool;
+
+op ==(int, f32) -> bool;
+
+op ==(int, float) -> bool;
+
+op ==(f32, int) -> bool;
+
+op ==(f32, f32) -> bool;
+
+op ==(float, int) -> bool;
+
+op ==(i128, i128) -> bool;
+
+op ==(i16, i16) -> bool;
+
+op ==(i32, i32) -> bool;
+
+op ==(i8, i8) -> bool;
+
+op ==(u128, u128) -> bool;
+
+op ==(u16, u16) -> bool;
+
+op ==(u32, u32) -> bool;
+
+op ==(u64, u64) -> bool;
+
+op ==(u8, u8) -> bool;
+
+/// Return `true` if the first timestamp is later than the second.
+op >(Instant, Instant) -> bool;
+
+op >(int, f32) -> bool;
+
+op >(int, float) -> bool;
+
+op >(f32, int) -> bool;
+
+op >(f32, f32) -> bool;
+
+op >(float, int) -> bool;
+
+op >(i128, i128) -> bool;
+
+op >(i16, i16) -> bool;
+
+op >(i32, i32) -> bool;
+
+op >(i8, i8) -> bool;
+
+op >(u128, u128) -> bool;
+
+op >(u16, u16) -> bool;
+
+op >(u32, u32) -> bool;
+
+op >(u64, u64) -> bool;
+
+op >(u8, u8) -> bool;
+
+/// Return `true` if the first timestamp is later than or equals to the second.
+op >=(Instant, Instant) -> bool;
+
+op >=(int, f32) -> bool;
+
+op >=(int, float) -> bool;
+
+op >=(f32, int) -> bool;
+
+op >=(f32, f32) -> bool;
+
+op >=(float, int) -> bool;
+
+op >=(i128, i128) -> bool;
+
+op >=(i16, i16) -> bool;
+
+op >=(i32, i32) -> bool;
+
+op >=(i8, i8) -> bool;
+
+op >=(u128, u128) -> bool;
+
+op >=(u16, u16) -> bool;
+
+op >=(u32, u32) -> bool;
+
+op >=(u64, u64) -> bool;
+
+op >=(u8, u8) -> bool;
+
+op >>(i128, int) -> i128;
+
+op >>(i16, int) -> i16;
+
+op >>(i32, int) -> i32;
+
+op >>(i8, int) -> i8;
+
+op >>(u128, int) -> u128;
+
+op >>(u16, int) -> u16;
+
+op >>(u32, int) -> u32;
+
+op >>(u64, int) -> u64;
+
+op >>(u8, int) -> u8;
+
+/// Return the natural number _e_.
+fn E() -> float;
+
+/// Return the number π.
+fn PI() -> float;
+
+op ^(i128, i128) -> i128;
+
+op ^(i16, i16) -> i16;
+
+op ^(i32, i32) -> i32;
+
+op ^(i8, i8) -> i8;
+
+op ^(u128, u128) -> u128;
+
+op ^(u16, u16) -> u16;
+
+op ^(u32, u32) -> u32;
+
+op ^(u64, u64) -> u64;
+
+op ^(u8, u8) -> u8;
+
+/// Return the absolute value of the decimal number.
+fn abs(x: Decimal) -> Decimal;
+
+/// Return the absolute value of the number.
+fn abs(x: int) -> int;
+
+/// Return the absolute value of the floating-point number.
+fn abs(x: f32) -> f32;
+
+/// Return the absolute value of the floating-point number.
+fn abs(x: float) -> float;
+
+/// Return the absolute value of the number.
+fn abs(x: i128) -> i128;
+
+/// Return the absolute value of the number.
+fn abs(x: i16) -> i16;
+
+/// Return the absolute value of the number.
+fn abs(x: i32) -> i32;
+
+/// Return the absolute value of the number.
+fn abs(x: i8) -> i8;
+
+/// Return the arc-cosine of the floating-point number, in radians.
+fn acos(x: float) -> float;
+
+/// Return the arc-hyperbolic-cosine of the floating-point number, in radians.
+fn acosh(x: float) -> float;
+
+/// Return `true` if all elements in the array return `true` when applied a function named by `filter`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.all(|v| v > 3)); // prints false
+///
+/// print(x.all(|v| v > 1)); // prints true
+///
+/// print(x.all(|v, i| i > v)); // prints false
+/// ```
+fn all(array: Array, filter: String) -> bool;
+
+/// Return `true` if all elements in the array return `true` when applied the `filter` function.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.all(|v| v > 3)); // prints false
+///
+/// print(x.all(|v| v > 1)); // prints true
+///
+/// print(x.all(|v, i| i > v)); // prints false
+/// ```
+fn all(array: Array, filter: FnPtr) -> bool;
+
+/// Add all the elements of another array to the end of the array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+/// let y = [true, 'x'];
+///
+/// x.push(y);
+///
+/// print(x); // prints "[1, 2, 3, true, 'x']"
+/// ```
+fn append(array: Array, new_array: Array) -> ();
+
+/// Add another BLOB to the end of the BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob(5, 0x42);
+/// let b2 = blob(3, 0x11);
+///
+/// b1.push(b2);
+///
+/// print(b1); // prints "[4242424242111111]"
+/// ```
+fn append(blob1: Blob, blob2: Blob) -> ();
+
+/// Add a character (as UTF-8 encoded byte-stream) to the end of the BLOB
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// b.append('!');
+///
+/// print(b); // prints "[424242424221]"
+/// ```
+fn append(blob: Blob, character: char) -> ();
+
+/// Add a string (as UTF-8 encoded byte-stream) to the end of the BLOB
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// b.append("hello");
+///
+/// print(b); // prints "[424242424268656c 6c6f]"
+/// ```
+fn append(blob: Blob, string: String) -> ();
+
+/// Add a new byte `value` to the end of the BLOB.
+///
+/// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b.push(0x42);
+///
+/// print(b); // prints "[42]"
+/// ```
+fn append(blob: Blob, value: int) -> ();
+
+fn append(string: String, item: ?) -> ();
+
+fn append(string: String, utf8: Blob) -> ();
+
+/// Convert the BLOB into a string.
+///
+/// The byte stream must be valid UTF-8, otherwise an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// let x = b.as_string();
+///
+/// print(x); // prints "FFFFF"
+/// ```
+fn as_string(blob: Blob) -> String;
+
+/// Return the arc-sine of the floating-point number, in radians.
+fn asin(x: float) -> float;
+
+/// Return the arc-hyperbolic-sine of the floating-point number, in radians.
+fn asinh(x: float) -> float;
+
+/// Return the arc-tangent of the floating-point number, in radians.
+fn atan(x: float) -> float;
+
+/// Return the arc-tangent of the floating-point numbers `x` and `y`, in radians.
+fn atan(x: float, y: float) -> float;
+
+/// Return the arc-hyperbolic-tangent of the floating-point number, in radians.
+fn atanh(x: float) -> float;
+
+/// Get an array of object maps containing the function calls stack.
+///
+/// If there is no debugging interface registered, an empty array is returned.
+///
+/// An array of strings is returned under `no_object`.
+fn back_trace() -> Array;
+
+/// Return an iterator over all the bits in the number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits() {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int) -> Iterator<bool>;
+
+/// Return an iterator over the bits in the number starting from the specified `start` position.
+///
+/// If `start` < 0, position counts from the MSB (Most Significant Bit)>.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits(10) {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int, from: int) -> Iterator<bool>;
+
+/// Return an iterator over an exclusive range of bits in the number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits(10..24) {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int, range: Range<int>) -> Iterator<bool>;
+
+/// Return an iterator over an inclusive range of bits in the number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits(10..=23) {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int, range: RangeInclusive<int>) -> Iterator<bool>;
+
+/// Return an iterator over a portion of bits in the number.
+///
+/// * If `start` < 0, position counts from the MSB (Most Significant Bit)>.
+/// * If `len` ≤ 0, an empty iterator is returned.
+/// * If `start` position + `len` ≥ length of string, all bits of the number after the `start` position are iterated.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits(10, 8) {
+/// print(bit);
+/// }
+/// ```
+fn bits(value: int, from: int, len: int) -> Iterator<bool>;
+
+/// Return a new, empty BLOB.
+fn blob() -> Blob;
+
+/// Return a new BLOB of the specified length, filled with zeros.
+///
+/// If `len` ≤ 0, an empty BLOB is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(10);
+///
+/// print(b); // prints "[0000000000000000 0000]"
+/// ```
+fn blob(len: int) -> Blob;
+
+/// Return a new BLOB of the specified length, filled with copies of the initial `value`.
+///
+/// If `len` ≤ 0, an empty BLOB is returned.
+///
+/// Only the lower 8 bits of the initial `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(10, 0x42);
+///
+/// print(b); // prints "[4242424242424242 4242]"
+/// ```
+fn blob(len: int, value: int) -> Blob;
+
+/// Return the length of the string, in number of bytes used to store it in UTF-8 encoding.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// print(text.bytes); // prints 51
+/// ```
+fn bytes(string: String) -> int;
+
+/// Return the smallest whole number larger than or equals to the decimal number.
+fn ceiling(x: Decimal) -> Decimal;
+
+/// Return the smallest whole number larger than or equals to the floating-point number.
+fn ceiling(x: float) -> float;
+
+/// Return an iterator over the characters in the string.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars() {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String) -> Iterator<char>;
+
+/// Return an iterator over the characters in the string starting from the `start` position.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars(2) {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String, from: int) -> Iterator<char>;
+
+/// Return an iterator over an exclusive range of characters in the string.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars(2..5) {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String, range: Range<int>) -> Iterator<char>;
+
+/// Return an iterator over an inclusive range of characters in the string.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars(2..=6) {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String, range: RangeInclusive<int>) -> Iterator<char>;
+
+/// Return an iterator over a portion of characters in the string.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, an empty iterator is returned.
+/// * If `len` ≤ 0, an empty iterator is returned.
+/// * If `start` position + `len` ≥ length of string, all characters of the string after the `start` position are iterated.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars(2, 4) {
+/// print(ch);
+/// }
+/// ```
+fn chars(string: String, start: int, len: int) -> Iterator<char>;
+
+/// Cut off the head of the array, leaving a tail of the specified length.
+///
+/// * If `len` ≤ 0, the array is cleared.
+/// * If `len` ≥ length of array, the array is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// x.chop(3);
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// x.chop(10);
+///
+/// print(x); // prints "[3, 4, 5]"
+/// ```
+fn chop(array: Array, len: int) -> ();
+
+/// Cut off the head of the BLOB, leaving a tail of the specified length.
+///
+/// * If `len` ≤ 0, the BLOB is cleared.
+/// * If `len` ≥ length of BLOB, the BLOB is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// b.chop(3);
+///
+/// print(b); // prints "[030405]"
+///
+/// b.chop(10);
+///
+/// print(b); // prints "[030405]"
+/// ```
+fn chop(blob: Blob, len: int) -> ();
+
+/// Clear the array.
+fn clear(array: Array) -> ();
+
+/// Clear the BLOB.
+fn clear(blob: Blob) -> ();
+
+/// Clear the object map.
+fn clear(map: Map) -> ();
+
+/// Clear the string, making it empty.
+fn clear(string: String) -> ();
+
+/// Return `true` if the array contains an element that equals `value`.
+///
+/// The operator `==` is used to compare elements with `value` and must be defined,
+/// otherwise `false` is assumed.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 4 in x {
+/// print("found!");
+/// }
+/// ```
+fn contains(array: Array, value: ?) -> bool;
+
+/// Return the cosine of the decimal number in radians.
+fn cos(x: Decimal) -> Decimal;
+
+/// Return the cosine of the floating-point number in radians.
+fn cos(x: float) -> float;
+
+/// Return the hyperbolic cosine of the floating-point number in radians.
+fn cosh(x: float) -> float;
+
+/// Remove all characters from the string except those within an exclusive `range`.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.crop(2..8);
+///
+/// print(text); // prints "llo, w"
+/// ```
+fn crop(string: String, range: Range<int>) -> ();
+
+/// Remove all characters from the string except those within an inclusive `range`.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.crop(2..=8);
+///
+/// print(text); // prints "llo, wo"
+/// ```
+fn crop(string: String, range: RangeInclusive<int>) -> ();
+
+/// Remove all characters from the string except until the `start` position.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, the string is not modified.
+/// * If `start` ≥ length of string, the entire string is cleared.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.crop(5);
+///
+/// print(text); // prints ", world!"
+///
+/// text.crop(-3);
+///
+/// print(text); // prints "ld!"
+/// ```
+fn crop(string: String, start: int) -> ();
+
+/// Remove all characters from the string except those within a range.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, the entire string is cleared.
+/// * If `len` ≤ 0, the entire string is cleared.
+/// * If `start` position + `len` ≥ length of string, only the portion of the string after the `start` position is retained.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.crop(2, 8);
+///
+/// print(text); // prints "llo, wor"
+///
+/// text.crop(-5, 3);
+///
+/// print(text); // prints ", w"
+/// ```
+fn crop(string: String, start: int, len: int) -> ();
+
+/// Return the empty string.
+fn debug() -> String;
+
+/// Convert the array into a string.
+fn debug(array: Array) -> String;
+
+/// Convert the string into debug format.
+fn debug(character: char) -> String;
+
+/// Convert the function pointer into a string in debug format.
+fn debug(f: FnPtr) -> String;
+
+/// Convert the value of the `item` into a string in debug format.
+fn debug(item: ?) -> String;
+
+/// Convert the object map into a string.
+fn debug(map: Map) -> String;
+
+/// Convert the value of `number` into a string.
+fn debug(number: f32) -> String;
+
+/// Convert the value of `number` into a string.
+fn debug(number: float) -> String;
+
+/// Convert the string into debug format.
+fn debug(string: String) -> String;
+
+/// Convert the unit into a string in debug format.
+fn debug(unit: ()) -> String;
+
+/// Convert the boolean value into a string in debug format.
+fn debug(value: bool) -> String;
+
+/// Remove duplicated _consecutive_ elements from the array.
+///
+/// The operator `==` is used to compare elements and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 2, 2, 3, 4, 3, 3, 2, 1];
+///
+/// x.dedup();
+///
+/// print(x); // prints "[1, 2, 3, 4, 3, 2, 1]"
+/// ```
+fn dedup(array: Array) -> ();
+
+/// Remove duplicated _consecutive_ elements from the array that return `true` when applied a
+/// function named by `comparer`.
+///
+/// No element is removed if the correct `comparer` function does not exist.
+///
+/// # Function Parameters
+///
+/// * `element1`: copy of the current array element to compare
+/// * `element2`: copy of the next array element to compare
+///
+/// ## Return Value
+///
+/// `true` if `element1 == element2`, otherwise `false`.
+///
+/// # Example
+///
+/// ```rhai
+/// fn declining(a, b) { a >= b }
+///
+/// let x = [1, 2, 2, 2, 3, 1, 2, 3, 4, 3, 3, 2, 1];
+///
+/// x.dedup("declining");
+///
+/// print(x); // prints "[1, 2, 3, 4]"
+/// ```
+fn dedup(array: Array, comparer: String) -> ();
+
+/// Remove duplicated _consecutive_ elements from the array that return `true` when applied the
+/// `comparer` function.
+///
+/// No element is removed if the correct `comparer` function does not exist.
+///
+/// # Function Parameters
+///
+/// * `element1`: copy of the current array element to compare
+/// * `element2`: copy of the next array element to compare
+///
+/// ## Return Value
+///
+/// `true` if `element1 == element2`, otherwise `false`.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 2, 2, 3, 1, 2, 3, 4, 3, 3, 2, 1];
+///
+/// x.dedup(|a, b| a >= b);
+///
+/// print(x); // prints "[1, 2, 3, 4]"
+/// ```
+fn dedup(array: Array, comparer: FnPtr) -> ();
+
+/// Remove all elements in the array that returns `true` when applied a function named by `filter`
+/// and return them as a new array.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn small(x) { x < 3 }
+///
+/// fn screen(x, i) { x + i > 5 }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain("small");
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// print(y); // prints "[1, 2]"
+///
+/// let z = x.drain("screen");
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, filter: String) -> Array;
+
+/// Remove all elements in the array that returns `true` when applied the `filter` function and
+/// return them as a new array.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain(|v| v < 3);
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// print(y); // prints "[1, 2]"
+///
+/// let z = x.drain(|v, i| v + i > 5);
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, filter: FnPtr) -> Array;
+
+/// Remove all elements in the array within an exclusive `range` and return them as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain(1..3);
+///
+/// print(x); // prints "[1, 4, 5]"
+///
+/// print(y); // prints "[2, 3]"
+///
+/// let z = x.drain(2..3);
+///
+/// print(x); // prints "[1, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, range: Range<int>) -> Array;
+
+/// Remove all elements in the array within an inclusive `range` and return them as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain(1..=2);
+///
+/// print(x); // prints "[1, 4, 5]"
+///
+/// print(y); // prints "[2, 3]"
+///
+/// let z = x.drain(2..=2);
+///
+/// print(x); // prints "[1, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, range: RangeInclusive<int>) -> Array;
+
+/// Remove all bytes in the BLOB within an exclusive `range` and return them as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.drain(1..3);
+///
+/// print(b1); // prints "[010405]"
+///
+/// print(b2); // prints "[0203]"
+///
+/// let b3 = b1.drain(2..3);
+///
+/// print(b1); // prints "[0104]"
+///
+/// print(b3); // prints "[05]"
+/// ```
+fn drain(blob: Blob, range: Range<int>) -> Blob;
+
+/// Remove all bytes in the BLOB within an inclusive `range` and return them as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.drain(1..=2);
+///
+/// print(b1); // prints "[010405]"
+///
+/// print(b2); // prints "[0203]"
+///
+/// let b3 = b1.drain(2..=2);
+///
+/// print(b1); // prints "[0104]"
+///
+/// print(b3); // prints "[05]"
+/// ```
+fn drain(blob: Blob, range: RangeInclusive<int>) -> Blob;
+
+/// Remove all elements within a portion of the array and return them as a new array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, no element is removed and an empty array is returned.
+/// * If `len` ≤ 0, no element is removed and an empty array is returned.
+/// * If `start` position + `len` ≥ length of array, entire portion of the array after the `start` position is removed and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.drain(1, 2);
+///
+/// print(x); // prints "[1, 4, 5]"
+///
+/// print(y); // prints "[2, 3]"
+///
+/// let z = x.drain(-1, 1);
+///
+/// print(x); // prints "[1, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(array: Array, start: int, len: int) -> Array;
+
+/// Remove all bytes within a portion of the BLOB and return them as a new BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, nothing is removed and an empty BLOB is returned.
+/// * If `len` ≤ 0, nothing is removed and an empty BLOB is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is removed and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.drain(1, 2);
+///
+/// print(b1); // prints "[010405]"
+///
+/// print(b2); // prints "[0203]"
+///
+/// let b3 = b1.drain(-1, 1);
+///
+/// print(b3); // prints "[0104]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn drain(blob: Blob, start: int, len: int) -> Blob;
+
+/// Return the number of seconds between the current system time and the timestamp.
+///
+/// # Example
+///
+/// ```rhai
+/// let now = timestamp();
+///
+/// sleep(10.0); // sleep for 10 seconds
+///
+/// print(now.elapsed); // prints 10.???
+/// ```
+fn elapsed(timestamp: Instant) -> RhaiResult;
+
+/// Return the end of the exclusive range.
+fn end(range: ExclusiveRange) -> int;
+
+/// Return the end of the inclusive range.
+fn end(range: InclusiveRange) -> int;
+
+/// Return `true` if the string ends with a specified string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.ends_with("world!")); // prints true
+///
+/// print(text.ends_with("hello")); // prints false
+/// ```
+fn ends_with(string: String, match_string: String) -> bool;
+
+/// Return the exponential of the decimal number.
+fn exp(x: Decimal) -> Decimal;
+
+/// Return the exponential of the floating-point number.
+fn exp(x: float) -> float;
+
+/// Copy an exclusive range of the array and return it as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// print(x.extract(1..3)); // prints "[2, 3]"
+///
+/// print(x); // prints "[1, 2, 3, 4, 5]"
+/// ```
+fn extract(array: Array, range: Range<int>) -> Array;
+
+/// Copy an inclusive range of the array and return it as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// print(x.extract(1..=3)); // prints "[2, 3, 4]"
+///
+/// print(x); // prints "[1, 2, 3, 4, 5]"
+/// ```
+fn extract(array: Array, range: RangeInclusive<int>) -> Array;
+
+/// Copy a portion of the array beginning at the `start` position till the end and return it as
+/// a new array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, the entire array is copied and returned.
+/// * If `start` ≥ length of array, an empty array is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// print(x.extract(2)); // prints "[3, 4, 5]"
+///
+/// print(x.extract(-3)); // prints "[3, 4, 5]"
+///
+/// print(x); // prints "[1, 2, 3, 4, 5]"
+/// ```
+fn extract(array: Array, start: int) -> Array;
+
+/// Copy an exclusive `range` of the BLOB and return it as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.extract(1..3)); // prints "[0203]"
+///
+/// print(b); // prints "[0102030405]"
+/// ```
+fn extract(blob: Blob, range: Range<int>) -> Blob;
+
+/// Copy an inclusive `range` of the BLOB and return it as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.extract(1..=3)); // prints "[020304]"
+///
+/// print(b); // prints "[0102030405]"
+/// ```
+fn extract(blob: Blob, range: RangeInclusive<int>) -> Blob;
+
+/// Copy a portion of the BLOB beginning at the `start` position till the end and return it as
+/// a new BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, the entire BLOB is copied and returned.
+/// * If `start` ≥ length of BLOB, an empty BLOB is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.extract(2)); // prints "[030405]"
+///
+/// print(b.extract(-3)); // prints "[030405]"
+///
+/// print(b); // prints "[0102030405]"
+/// ```
+fn extract(blob: Blob, start: int) -> Blob;
+
+/// Copy a portion of the array and return it as a new array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, an empty array is returned.
+/// * If `len` ≤ 0, an empty array is returned.
+/// * If `start` position + `len` ≥ length of array, entire portion of the array after the `start` position is copied and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// print(x.extract(1, 3)); // prints "[2, 3, 4]"
+///
+/// print(x.extract(-3, 2)); // prints "[3, 4]"
+///
+/// print(x); // prints "[1, 2, 3, 4, 5]"
+/// ```
+fn extract(array: Array, start: int, len: int) -> Array;
+
+/// Copy a portion of the BLOB and return it as a new BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, an empty BLOB is returned.
+/// * If `len` ≤ 0, an empty BLOB is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is copied and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.extract(1, 3)); // prints "[020303]"
+///
+/// print(b.extract(-3, 2)); // prints "[0304]"
+///
+/// print(b); // prints "[0102030405]"
+/// ```
+fn extract(blob: Blob, start: int, len: int) -> Blob;
+
+/// Add all property values of another object map into the object map.
+/// Only properties that do not originally exist in the object map are added.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+/// let n = #{a: 42, d:0};
+///
+/// m.fill_with(n);
+///
+/// print(m); // prints "#{a:1, b:2, c:3, d:0}"
+/// ```
+fn fill_with(map: Map, map2: Map) -> ();
+
+/// Iterate through all the elements in the array, applying a `filter` function to each element
+/// in turn, and return a copy of all elements (in order) that return `true` as a new array.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.filter(|v| v >= 3);
+///
+/// print(y); // prints "[3, 4, 5]"
+///
+/// let y = x.filter(|v, i| v * i >= 10);
+///
+/// print(y); // prints "[12, 20]"
+/// ```
+fn filter(array: Array, filter: FnPtr) -> Array;
+
+/// Iterate through all the elements in the array, applying a function named by `filter` to each
+/// element in turn, and return a copy of all elements (in order) that return `true` as a new array.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn screen(x, i) { x * i >= 10 }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.filter("is_odd");
+///
+/// print(y); // prints "[1, 3, 5]"
+///
+/// let y = x.filter("screen");
+///
+/// print(y); // prints "[12, 20]"
+/// ```
+fn filter(array: Array, filter_func: String) -> Array;
+
+/// Return the largest whole number less than or equals to the decimal number.
+fn floor(x: Decimal) -> Decimal;
+
+/// Return the largest whole number less than or equals to the floating-point number.
+fn floor(x: float) -> float;
+
+/// Return the fractional part of the decimal number.
+fn fraction(x: Decimal) -> Decimal;
+
+/// Return the fractional part of the floating-point number.
+fn fraction(x: float) -> float;
+
+/// Get a copy of the element at the `index` position in the array.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` < -length of array, `()` is returned.
+/// * If `index` ≥ length of array, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// print(x.get(0)); // prints 1
+///
+/// print(x.get(-1)); // prints 3
+///
+/// print(x.get(99)); // prints empty (for '()')
+/// ```
+fn get(array: Array, index: int) -> ?;
+
+/// Get the byte value at the `index` position in the BLOB.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last element).
+/// * If `index` < -length of BLOB, zero is returned.
+/// * If `index` ≥ length of BLOB, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.get(0)); // prints 1
+///
+/// print(b.get(-1)); // prints 5
+///
+/// print(b.get(99)); // prints 0
+/// ```
+fn get(blob: Blob, index: int) -> int;
+
+/// Get the value of the `property` in the object map and return a copy.
+///
+/// If `property` does not exist in the object map, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a: 1, b: 2, c: 3};
+///
+/// print(m.get("b")); // prints 2
+///
+/// print(m.get("x")); // prints empty (for '()')
+/// ```
+fn get(map: Map, property: String) -> ?;
+
+/// Get the character at the `index` position in the string.
+///
+/// * If `index` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `index` < -length of string, zero is returned.
+/// * If `index` ≥ length of string, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.get(0)); // prints 'h'
+///
+/// print(text.get(-1)); // prints '!'
+///
+/// print(text.get(99)); // prints empty (for '()')'
+/// ```
+fn get(string: String, index: int) -> ?;
+
+/// Return an iterator over all the bits in the number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// for bit in x.bits {
+/// print(bit);
+/// }
+/// ```
+fn get bits(value: int) -> Iterator<bool>;
+
+/// Return the length of the string, in number of bytes used to store it in UTF-8 encoding.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// print(text.bytes); // prints 51
+/// ```
+fn get bytes(string: String) -> int;
+
+/// Return the smallest whole number larger than or equals to the decimal number.
+fn get ceiling(x: Decimal) -> Decimal;
+
+/// Return the smallest whole number larger than or equals to the floating-point number.
+fn get ceiling(x: float) -> float;
+
+/// Return an iterator over all the characters in the string.
+///
+/// # Example
+///
+/// ```rhai
+/// for ch in "hello, world!".chars {
+/// print(ch);
+/// }
+/// ```
+fn get chars(string: String) -> Iterator<char>;
+
+/// Return the number of seconds between the current system time and the timestamp.
+///
+/// # Example
+///
+/// ```rhai
+/// let now = timestamp();
+///
+/// sleep(10.0); // sleep for 10 seconds
+///
+/// print(now.elapsed); // prints 10.???
+/// ```
+fn get elapsed(timestamp: Instant) -> RhaiResult;
+
+/// Return the end of the exclusive range.
+fn get end(range: ExclusiveRange) -> int;
+
+/// Return the end of the inclusive range.
+fn get end(range: InclusiveRange) -> int;
+
+/// Return the largest whole number less than or equals to the decimal number.
+fn get floor(x: Decimal) -> Decimal;
+
+/// Return the largest whole number less than or equals to the floating-point number.
+fn get floor(x: float) -> float;
+
+/// Return the fractional part of the decimal number.
+fn get fraction(x: Decimal) -> Decimal;
+
+/// Return the fractional part of the floating-point number.
+fn get fraction(x: float) -> float;
+
+/// Return the integral part of the decimal number.
+fn get int(x: Decimal) -> Decimal;
+
+/// Return the integral part of the floating-point number.
+fn get int(x: float) -> float;
+
+/// Return `true` if the function is an anonymous function.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = |x| x * 2;
+///
+/// print(f.is_anonymous); // prints true
+/// ```
+fn get is_anonymous(fn_ptr: FnPtr) -> bool;
+
+/// Return true if the array is empty.
+fn get is_empty(array: Array) -> bool;
+
+/// Return true if the BLOB is empty.
+fn get is_empty(blob: Blob) -> bool;
+
+/// Return true if the range contains no items.
+fn get is_empty(range: ExclusiveRange) -> bool;
+
+/// Return true if the range contains no items.
+fn get is_empty(range: InclusiveRange) -> bool;
+
+/// Return true if the string is empty.
+fn get is_empty(string: String) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: int) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: i128) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: i16) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: i32) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: i8) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u128) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u16) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u32) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u64) -> bool;
+
+/// Return true if the number is even.
+fn get is_even(x: u8) -> bool;
+
+/// Return `true` if the range is exclusive.
+fn get is_exclusive(range: ExclusiveRange) -> bool;
+
+/// Return `true` if the range is exclusive.
+fn get is_exclusive(range: InclusiveRange) -> bool;
+
+/// Return `true` if the floating-point number is finite.
+fn get is_finite(x: float) -> bool;
+
+/// Return `true` if the range is inclusive.
+fn get is_inclusive(range: ExclusiveRange) -> bool;
+
+/// Return `true` if the range is inclusive.
+fn get is_inclusive(range: InclusiveRange) -> bool;
+
+/// Return `true` if the floating-point number is infinite.
+fn get is_infinite(x: float) -> bool;
+
+/// Return `true` if the floating-point number is `NaN` (Not A Number).
+fn get is_nan(x: float) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: int) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: i128) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: i16) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: i32) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: i8) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u128) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u16) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u32) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u64) -> bool;
+
+/// Return true if the number is odd.
+fn get is_odd(x: u8) -> bool;
+
+/// Return true if the decimal number is zero.
+fn get is_zero(x: Decimal) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: int) -> bool;
+
+/// Return true if the floating-point number is zero.
+fn get is_zero(x: f32) -> bool;
+
+/// Return true if the floating-point number is zero.
+fn get is_zero(x: float) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: i128) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: i16) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: i32) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: i8) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u128) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u16) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u32) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u64) -> bool;
+
+/// Return true if the number is zero.
+fn get is_zero(x: u8) -> bool;
+
+/// Number of elements in the array.
+fn get len(array: Array) -> int;
+
+/// Return the length of the BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(10, 0x42);
+///
+/// print(b); // prints "[4242424242424242 4242]"
+///
+/// print(b.len()); // prints 10
+/// ```
+fn get len(blob: Blob) -> int;
+
+/// Return the length of the string, in number of characters.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// print(text.len); // prints 17
+/// ```
+fn get len(string: String) -> int;
+
+/// Return the name of the function.
+///
+/// # Example
+///
+/// ```rhai
+/// fn double(x) { x * 2 }
+///
+/// let f = Fn("double");
+///
+/// print(f.name); // prints "double"
+/// ```
+fn get name(fn_ptr: FnPtr) -> String;
+
+/// Return the nearest whole number closest to the decimal number.
+/// Always round mid-point towards the closest even number.
+fn get round(x: Decimal) -> Decimal;
+
+/// Return the nearest whole number closest to the floating-point number.
+/// Rounds away from zero.
+fn get round(x: float) -> float;
+
+/// Return the start of the exclusive range.
+fn get start(range: ExclusiveRange) -> int;
+
+/// Return the start of the inclusive range.
+fn get start(range: InclusiveRange) -> int;
+
+/// Return the _tag_ of a `Dynamic` value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// x.tag = 42;
+///
+/// print(x.tag); // prints 42
+/// ```
+fn get tag(value: ?) -> int;
+
+/// Return `true` if the specified `bit` in the number is set.
+///
+/// If `bit` < 0, position counts from the MSB (Most Significant Bit).
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// print(x.get_bit(5)); // prints false
+///
+/// print(x.get_bit(6)); // prints true
+///
+/// print(x.get_bit(-48)); // prints true on 64-bit
+/// ```
+fn get_bit(value: int, bit: int) -> bool;
+
+/// Return an exclusive range of bits in the number as a new number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// print(x.get_bits(5..10)); // print 18
+/// ```
+fn get_bits(value: int, range: Range<int>) -> int;
+
+/// Return an inclusive range of bits in the number as a new number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// print(x.get_bits(5..=9)); // print 18
+/// ```
+fn get_bits(value: int, range: RangeInclusive<int>) -> int;
+
+/// Return a portion of bits in the number as a new number.
+///
+/// * If `start` < 0, position counts from the MSB (Most Significant Bit).
+/// * If `bits` ≤ 0, zero is returned.
+/// * If `start` position + `bits` ≥ total number of bits, the bits after the `start` position are returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// print(x.get_bits(5, 8)); // print 18
+/// ```
+fn get_bits(value: int, start: int, bits: int) -> int;
+
+fn get_fn_metadata_list() -> Array;
+
+fn get_fn_metadata_list(name: String) -> Array;
+
+fn get_fn_metadata_list(name: String, params: int) -> Array;
+
+/// Return the hypotenuse of a triangle with sides `x` and `y`.
+fn hypot(x: float, y: float) -> float;
+
+/// Iterate through all the elements in the array, applying a function named by `filter` to each
+/// element in turn, and return the index of the first element that returns `true`.
+/// If no element returns `true`, `-1` is returned.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn is_special(x) { x > 3 }
+///
+/// fn is_dumb(x) { x > 8 }
+///
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of("is_special")); // prints 3
+///
+/// print(x.index_of("is_dumb")); // prints -1
+/// ```
+fn index_of(array: Array, filter: String) -> int;
+
+/// Iterate through all the elements in the array, applying a `filter` function to each element
+/// in turn, and return the index of the first element that returns `true`.
+/// If no element returns `true`, `-1` is returned.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of(|v| v > 3)); // prints 3: 4 > 3
+///
+/// print(x.index_of(|v| v > 8)); // prints -1: nothing is > 8
+///
+/// print(x.index_of(|v, i| v * i > 20)); // prints 7: 4 * 7 > 20
+/// ```
+fn index_of(array: Array, filter: FnPtr) -> int;
+
+/// Find the first element in the array that equals a particular `value` and return its index.
+/// If no element equals `value`, `-1` is returned.
+///
+/// The operator `==` is used to compare elements with `value` and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of(4)); // prints 3 (first index)
+///
+/// print(x.index_of(9)); // prints -1
+///
+/// print(x.index_of("foo")); // prints -1: strings do not equal numbers
+/// ```
+fn index_of(array: Array, value: ?) -> int;
+
+/// Find the specified `character` in the string and return the first index where it is found.
+/// If the `character` is not found, `-1` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.index_of('l')); // prints 2 (first index)
+///
+/// print(text.index_of('x')); // prints -1
+/// ```
+fn index_of(string: String, character: char) -> int;
+
+/// Find the specified `character` in the string and return the first index where it is found.
+/// If the `character` is not found, `-1` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// print(text.index_of("ll")); // prints 2 (first index)
+///
+/// print(text.index_of("xx:)); // prints -1
+/// ```
+fn index_of(string: String, find_string: String) -> int;
+
+/// Iterate through all the elements in the array, starting from a particular `start` position,
+/// applying a function named by `filter` to each element in turn, and return the index of the
+/// first element that returns `true`. If no element returns `true`, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, `-1` is returned.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn plural(x) { x > 1 }
+///
+/// fn singular(x) { x < 2 }
+///
+/// fn screen(x, i) { x * i > 20 }
+///
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of("plural", 3)); // prints 5: 2 > 1
+///
+/// print(x.index_of("singular", 9)); // prints -1: nothing < 2 past index 9
+///
+/// print(x.index_of("plural", 15)); // prints -1: nothing found past end of array
+///
+/// print(x.index_of("plural", -5)); // prints 9: -5 = start from index 8
+///
+/// print(x.index_of("plural", -99)); // prints 1: -99 = start from beginning
+///
+/// print(x.index_of("screen", 8)); // prints 10: 3 * 10 > 20
+/// ```
+fn index_of(array: Array, filter: String, start: int) -> int;
+
+/// Iterate through all the elements in the array, starting from a particular `start` position,
+/// applying a `filter` function to each element in turn, and return the index of the first
+/// element that returns `true`. If no element returns `true`, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, `-1` is returned.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of(|v| v > 1, 3)); // prints 5: 2 > 1
+///
+/// print(x.index_of(|v| v < 2, 9)); // prints -1: nothing < 2 past index 9
+///
+/// print(x.index_of(|v| v > 1, 15)); // prints -1: nothing found past end of array
+///
+/// print(x.index_of(|v| v > 1, -5)); // prints 9: -5 = start from index 8
+///
+/// print(x.index_of(|v| v > 1, -99)); // prints 1: -99 = start from beginning
+///
+/// print(x.index_of(|v, i| v * i > 20, 8)); // prints 10: 3 * 10 > 20
+/// ```
+fn index_of(array: Array, filter: FnPtr, start: int) -> int;
+
+/// Find the first element in the array, starting from a particular `start` position, that
+/// equals a particular `value` and return its index. If no element equals `value`, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, `-1` is returned.
+///
+/// The operator `==` is used to compare elements with `value` and must be defined,
+/// otherwise `false` is assumed.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.index_of(4, 2)); // prints 3
+///
+/// print(x.index_of(4, 5)); // prints 7
+///
+/// print(x.index_of(4, 15)); // prints -1: nothing found past end of array
+///
+/// print(x.index_of(4, -5)); // prints 11: -5 = start from index 8
+///
+/// print(x.index_of(9, 1)); // prints -1: nothing equals 9
+///
+/// print(x.index_of("foo", 1)); // prints -1: strings do not equal numbers
+/// ```
+fn index_of(array: Array, value: ?, start: int) -> int;
+
+/// Find the specified `character` in the string, starting from the specified `start` position,
+/// and return the first index where it is found.
+/// If the `character` is not found, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, `-1` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.index_of('l', 5)); // prints 10 (first index after 5)
+///
+/// print(text.index_of('o', -7)); // prints 8
+///
+/// print(text.index_of('x', 0)); // prints -1
+/// ```
+fn index_of(string: String, character: char, start: int) -> int;
+
+/// Find the specified sub-string in the string, starting from the specified `start` position,
+/// and return the first index where it is found.
+/// If the sub-string is not found, `-1` is returned.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, `-1` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// print(text.index_of("ll", 5)); // prints 16 (first index after 5)
+///
+/// print(text.index_of("ll", -15)); // prints 16
+///
+/// print(text.index_of("xx", 0)); // prints -1
+/// ```
+fn index_of(string: String, find_string: String, start: int) -> int;
+
+/// Add a new element into the array at a particular `index` position.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` < -length of array, the element is added to the beginning of the array.
+/// * If `index` ≥ length of array, the element is appended to the end of the array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// x.insert(0, "hello");
+///
+/// x.insert(2, true);
+///
+/// x.insert(-2, 42);
+///
+/// print(x); // prints ["hello", 1, true, 2, 42, 3]
+/// ```
+fn insert(array: Array, index: int, item: ?) -> ();
+
+/// Add a byte `value` to the BLOB at a particular `index` position.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `index` < -length of BLOB, the byte value is added to the beginning of the BLOB.
+/// * If `index` ≥ length of BLOB, the byte value is appended to the end of the BLOB.
+///
+/// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// b.insert(2, 0x18);
+///
+/// print(b); // prints "[4242184242]"
+/// ```
+fn insert(blob: Blob, index: int, value: int) -> ();
+
+/// Return the integral part of the decimal number.
+fn int(x: Decimal) -> Decimal;
+
+/// Return the integral part of the floating-point number.
+fn int(x: float) -> float;
+
+/// Return `true` if the function is an anonymous function.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = |x| x * 2;
+///
+/// print(f.is_anonymous); // prints true
+/// ```
+fn is_anonymous(fn_ptr: FnPtr) -> bool;
+
+/// Return true if the array is empty.
+fn is_empty(array: Array) -> bool;
+
+/// Return true if the BLOB is empty.
+fn is_empty(blob: Blob) -> bool;
+
+/// Return true if the map is empty.
+fn is_empty(map: Map) -> bool;
+
+/// Return true if the range contains no items.
+fn is_empty(range: ExclusiveRange) -> bool;
+
+/// Return true if the range contains no items.
+fn is_empty(range: InclusiveRange) -> bool;
+
+/// Return true if the string is empty.
+fn is_empty(string: String) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: int) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: i128) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: i16) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: i32) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: i8) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u128) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u16) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u32) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u64) -> bool;
+
+/// Return true if the number is even.
+fn is_even(x: u8) -> bool;
+
+/// Return `true` if the range is exclusive.
+fn is_exclusive(range: ExclusiveRange) -> bool;
+
+/// Return `true` if the range is exclusive.
+fn is_exclusive(range: InclusiveRange) -> bool;
+
+/// Return `true` if the floating-point number is finite.
+fn is_finite(x: float) -> bool;
+
+/// Return `true` if the range is inclusive.
+fn is_inclusive(range: ExclusiveRange) -> bool;
+
+/// Return `true` if the range is inclusive.
+fn is_inclusive(range: InclusiveRange) -> bool;
+
+/// Return `true` if the floating-point number is infinite.
+fn is_infinite(x: float) -> bool;
+
+/// Return `true` if the floating-point number is `NaN` (Not A Number).
+fn is_nan(x: float) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: int) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: i128) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: i16) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: i32) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: i8) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u128) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u16) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u32) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u64) -> bool;
+
+/// Return true if the number is odd.
+fn is_odd(x: u8) -> bool;
+
+/// Return true if the decimal number is zero.
+fn is_zero(x: Decimal) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: int) -> bool;
+
+/// Return true if the floating-point number is zero.
+fn is_zero(x: f32) -> bool;
+
+/// Return true if the floating-point number is zero.
+fn is_zero(x: float) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: i128) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: i16) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: i32) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: i8) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u128) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u16) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u32) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u64) -> bool;
+
+/// Return true if the number is zero.
+fn is_zero(x: u8) -> bool;
+
+/// Return an array with all the property names in the object map.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// print(m.keys()); // prints ["a", "b", "c"]
+/// ```
+fn keys(map: Map) -> Array;
+
+/// Number of elements in the array.
+fn len(array: Array) -> int;
+
+/// Return the length of the BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(10, 0x42);
+///
+/// print(b); // prints "[4242424242424242 4242]"
+///
+/// print(b.len()); // prints 10
+/// ```
+fn len(blob: Blob) -> int;
+
+/// Return the number of properties in the object map.
+fn len(map: Map) -> int;
+
+/// Return the length of the string, in number of characters.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// print(text.len); // prints 17
+/// ```
+fn len(string: String) -> int;
+
+/// Return the natural log of the decimal number.
+fn ln(x: Decimal) -> Decimal;
+
+/// Return the natural log of the floating-point number.
+fn ln(x: float) -> float;
+
+/// Return the log of the decimal number with base 10.
+fn log(x: Decimal) -> Decimal;
+
+/// Return the log of the floating-point number with base 10.
+fn log(x: float) -> float;
+
+/// Return the log of the floating-point number with `base`.
+fn log(x: float, base: float) -> float;
+
+/// Convert the character to lower-case.
+///
+/// # Example
+///
+/// ```rhai
+/// let ch = 'A';
+///
+/// ch.make_lower();
+///
+/// print(ch); // prints 'a'
+/// ```
+fn make_lower(character: char) -> ();
+
+/// Convert the string to all lower-case.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "HELLO, WORLD!"
+///
+/// text.make_lower();
+///
+/// print(text); // prints "hello, world!";
+/// ```
+fn make_lower(string: String) -> ();
+
+/// Convert the character to upper-case.
+///
+/// # Example
+///
+/// ```rhai
+/// let ch = 'a';
+///
+/// ch.make_upper();
+///
+/// print(ch); // prints 'A'
+/// ```
+fn make_upper(character: char) -> ();
+
+/// Convert the string to all upper-case.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!"
+///
+/// text.make_upper();
+///
+/// print(text); // prints "HELLO, WORLD!";
+/// ```
+fn make_upper(string: String) -> ();
+
+/// Iterate through all the elements in the array, applying a function named by `mapper` to each
+/// element in turn, and return the results as a new array.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `mapper` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn square(x) { x * x }
+///
+/// fn multiply(x, i) { x * i }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.map("square");
+///
+/// print(y); // prints "[1, 4, 9, 16, 25]"
+///
+/// let y = x.map("multiply");
+///
+/// print(y); // prints "[0, 2, 6, 12, 20]"
+/// ```
+fn map(array: Array, mapper: String) -> Array;
+
+/// Iterate through all the elements in the array, applying a `mapper` function to each element
+/// in turn, and return the results as a new array.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.map(|v| v * v);
+///
+/// print(y); // prints "[1, 4, 9, 16, 25]"
+///
+/// let y = x.map(|v, i| v * i);
+///
+/// print(y); // prints "[0, 2, 6, 12, 20]"
+/// ```
+fn map(array: Array, mapper: FnPtr) -> Array;
+
+/// Add all property values of another object map into the object map.
+/// Existing property values of the same names are replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+/// let n = #{a: 42, d:0};
+///
+/// m.mixin(n);
+///
+/// print(m); // prints "#{a:42, b:2, c:3, d:0}"
+/// ```
+fn mixin(map: Map, map2: Map) -> ();
+
+/// Return the name of the function.
+///
+/// # Example
+///
+/// ```rhai
+/// fn double(x) { x * 2 }
+///
+/// let f = Fn("double");
+///
+/// print(f.name); // prints "double"
+/// ```
+fn name(fn_ptr: FnPtr) -> String;
+
+/// Pad the array to at least the specified length with copies of a specified element.
+///
+/// If `len` ≤ length of array, no padding is done.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// x.pad(5, 42);
+///
+/// print(x); // prints "[1, 2, 3, 42, 42]"
+///
+/// x.pad(3, 123);
+///
+/// print(x); // prints "[1, 2, 3, 42, 42]"
+/// ```
+fn pad(array: Array, len: int, item: ?) -> ();
+
+/// Pad the BLOB to at least the specified length with copies of a specified byte `value`.
+///
+/// If `len` ≤ length of BLOB, no padding is done.
+///
+/// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(3, 0x42);
+///
+/// b.pad(5, 0x18)
+///
+/// print(b); // prints "[4242421818]"
+///
+/// b.pad(3, 0xab)
+///
+/// print(b); // prints "[4242421818]"
+/// ```
+fn pad(blob: Blob, len: int, value: int) -> ();
+
+/// Pad the string to at least the specified number of characters with the specified `character`.
+///
+/// If `len` ≤ length of string, no padding is done.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello";
+///
+/// text.pad(8, '!');
+///
+/// print(text); // prints "hello!!!"
+///
+/// text.pad(5, '*');
+///
+/// print(text); // prints "hello!!!"
+/// ```
+fn pad(string: String, len: int, character: char) -> ();
+
+/// Pad the string to at least the specified number of characters with the specified string.
+///
+/// If `len` ≤ length of string, no padding is done.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello";
+///
+/// text.pad(10, "(!)");
+///
+/// print(text); // prints "hello(!)(!)"
+///
+/// text.pad(8, '***');
+///
+/// print(text); // prints "hello(!)(!)"
+/// ```
+fn pad(string: String, len: int, padding: String) -> ();
+
+/// Parse the bytes within an exclusive `range` in the BLOB as a `FLOAT`
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_be_float(blob: Blob, range: Range<int>) -> float;
+
+/// Parse the bytes within an inclusive `range` in the BLOB as a `FLOAT`
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_be_float(blob: Blob, range: RangeInclusive<int>) -> float;
+
+/// Parse the bytes beginning at the `start` position in the BLOB as a `FLOAT`
+/// in big-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in range < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in range > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_be_float(blob: Blob, start: int, len: int) -> float;
+
+/// Parse the bytes within an exclusive `range` in the BLOB as an `INT`
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_be_int(1..3); // parse two bytes
+///
+/// print(x.to_hex()); // prints "02030000...00"
+/// ```
+fn parse_be_int(blob: Blob, range: Range<int>) -> int;
+
+/// Parse the bytes within an inclusive `range` in the BLOB as an `INT`
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_be_int(1..=3); // parse three bytes
+///
+/// print(x.to_hex()); // prints "0203040000...00"
+/// ```
+fn parse_be_int(blob: Blob, range: RangeInclusive<int>) -> int;
+
+/// Parse the bytes beginning at the `start` position in the BLOB as an `INT`
+/// in big-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in range < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in range > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_be_int(1, 2);
+///
+/// print(x.to_hex()); // prints "02030000...00"
+/// ```
+fn parse_be_int(blob: Blob, start: int, len: int) -> int;
+
+/// Parse a string into a decimal number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = parse_decimal("123.456");
+///
+/// print(x); // prints 123.456
+/// ```
+fn parse_decimal(string: String) -> Decimal;
+
+/// Parse a string into a floating-point number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = parse_int("123.456");
+///
+/// print(x); // prints 123.456
+/// ```
+fn parse_float(string: String) -> float;
+
+/// Parse a string into an integer number.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = parse_int("123");
+///
+/// print(x); // prints 123
+/// ```
+fn parse_int(string: String) -> int;
+
+/// Parse a string into an integer number of the specified `radix`.
+///
+/// `radix` must be between 2 and 36.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = parse_int("123");
+///
+/// print(x); // prints 123
+///
+/// let y = parse_int("123abc", 16);
+///
+/// print(y); // prints 1194684 (0x123abc)
+/// ```
+fn parse_int(string: String, radix: int) -> int;
+
+/// Parse the bytes within an exclusive `range` in the BLOB as a `FLOAT`
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_le_float(blob: Blob, range: Range<int>) -> float;
+
+/// Parse the bytes within an inclusive `range` in the BLOB as a `FLOAT`
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_le_float(blob: Blob, range: RangeInclusive<int>) -> float;
+
+/// Parse the bytes beginning at the `start` position in the BLOB as a `FLOAT`
+/// in little-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in range < number of bytes for `FLOAT`, zeros are padded.
+/// * If number of bytes in range > number of bytes for `FLOAT`, extra bytes are ignored.
+fn parse_le_float(blob: Blob, start: int, len: int) -> float;
+
+/// Parse the bytes within an exclusive `range` in the BLOB as an `INT`
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_le_int(1..3); // parse two bytes
+///
+/// print(x.to_hex()); // prints "0302"
+/// ```
+fn parse_le_int(blob: Blob, range: Range<int>) -> int;
+
+/// Parse the bytes within an inclusive `range` in the BLOB as an `INT`
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_le_int(1..=3); // parse three bytes
+///
+/// print(x.to_hex()); // prints "040302"
+/// ```
+fn parse_le_int(blob: Blob, range: RangeInclusive<int>) -> int;
+
+/// Parse the bytes beginning at the `start` position in the BLOB as an `INT`
+/// in little-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in range < number of bytes for `INT`, zeros are padded.
+/// * If number of bytes in range > number of bytes for `INT`, extra bytes are ignored.
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// let x = b.parse_le_int(1, 2);
+///
+/// print(x.to_hex()); // prints "0302"
+/// ```
+fn parse_le_int(blob: Blob, start: int, len: int) -> int;
+
+/// Remove the last element from the array and return it.
+///
+/// If the array is empty, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// print(x.pop()); // prints 3
+///
+/// print(x); // prints "[1, 2]"
+/// ```
+fn pop(array: Array) -> ?;
+
+/// Remove the last byte from the BLOB and return it.
+///
+/// If the BLOB is empty, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.pop()); // prints 5
+///
+/// print(b); // prints "[01020304]"
+/// ```
+fn pop(blob: Blob) -> int;
+
+/// Remove the last character from the string and return it.
+///
+/// If the string is empty, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.pop()); // prints '!'
+///
+/// print(text); // prints "hello, world"
+/// ```
+fn pop(string: String) -> ?;
+
+/// Remove a specified number of characters from the end of the string and return it as a
+/// new string.
+///
+/// * If `len` ≤ 0, the string is not modified and an empty string is returned.
+/// * If `len` ≥ length of string, the string is cleared and the entire string returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.pop(4)); // prints "rld!"
+///
+/// print(text); // prints "hello, wo"
+/// ```
+fn pop(string: String, len: int) -> String;
+
+/// Return the empty string.
+fn print() -> String;
+
+/// Convert the array into a string.
+fn print(array: Array) -> String;
+
+/// Return the character into a string.
+fn print(character: char) -> String;
+
+/// Convert the value of the `item` into a string.
+fn print(item: ?) -> String;
+
+/// Convert the object map into a string.
+fn print(map: Map) -> String;
+
+/// Convert the value of `number` into a string.
+fn print(number: f32) -> String;
+
+/// Convert the value of `number` into a string.
+fn print(number: float) -> String;
+
+/// Return the `string`.
+fn print(string: String) -> String;
+
+/// Return the empty string.
+fn print(unit: ()) -> String;
+
+/// Return the boolean value into a string.
+fn print(value: bool) -> String;
+
+/// Add a new element, which is not another array, to the end of the array.
+///
+/// If `item` is `Array`, then `append` is more specific and will be called instead.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// x.push("hello");
+///
+/// print(x); // prints [1, 2, 3, "hello"]
+/// ```
+fn push(array: Array, item: ?) -> ();
+
+/// Add a new byte `value` to the end of the BLOB.
+///
+/// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b.push(0x42);
+///
+/// print(b); // prints "[42]"
+/// ```
+fn push(blob: Blob, value: int) -> ();
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i128, to: i128) -> Iterator<i128>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i16, to: i16) -> Iterator<i16>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i32, to: i32) -> Iterator<i32>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: int, to: int) -> Iterator<int>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i8, to: i8) -> Iterator<i8>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u128, to: u128) -> Iterator<u128>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u16, to: u16) -> Iterator<u16>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u32, to: u32) -> Iterator<u32>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u64, to: u64) -> Iterator<u64>;
+
+/// Return an iterator over the exclusive range of `from..to`.
+/// The value `to` is never included.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17
+/// for n in range(8, 18) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u8, to: u8) -> Iterator<u8>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<Decimal>, step: Decimal) -> Iterator<Decimal>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<float>, step: float) -> Iterator<float>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<i128>, step: i128) -> Iterator<i128>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<i16>, step: i16) -> Iterator<i16>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<i32>, step: i32) -> Iterator<i32>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<int>, step: int) -> Iterator<int>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<i8>, step: i8) -> Iterator<i8>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u128>, step: u128) -> Iterator<u128>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u16>, step: u16) -> Iterator<u16>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u32>, step: u32) -> Iterator<u32>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u64>, step: u64) -> Iterator<u64>;
+
+/// Return an iterator over an exclusive range, each iteration increasing by `step`.
+///
+/// If `range` is reversed and `step` < 0, iteration goes backwards.
+///
+/// Otherwise, if `range` is empty, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8..18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18..8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(range: Range<u8>, step: u8) -> Iterator<u8>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: Decimal, to: Decimal, step: Decimal) -> Iterator<Decimal>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: float, to: float, step: float) -> Iterator<float>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i128, to: i128, step: i128) -> Iterator<i128>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i16, to: i16, step: i16) -> Iterator<i16>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i32, to: i32, step: i32) -> Iterator<i32>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: int, to: int, step: int) -> Iterator<int>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: i8, to: i8, step: i8) -> Iterator<i8>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u128, to: u128, step: u128) -> Iterator<u128>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u16, to: u16, step: u16) -> Iterator<u16>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u32, to: u32, step: u32) -> Iterator<u32>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u64, to: u64, step: u64) -> Iterator<u64>;
+
+/// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.
+/// The value `to` is never included.
+///
+/// If `from` > `to` and `step` < 0, iteration goes backwards.
+///
+/// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// // prints all values from 8 to 17 in steps of 3
+/// for n in range(8, 18, 3) {
+/// print(n);
+/// }
+///
+/// // prints all values down from 18 to 9 in steps of -3
+/// for n in range(18, 8, -3) {
+/// print(n);
+/// }
+/// ```
+fn range(from: u8, to: u8, step: u8) -> Iterator<u8>;
+
+/// Reduce an array by iterating through all elements while applying a function named by `reducer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `reducer` must exist taking these parameters:
+///
+/// * `result`: accumulated result, initially `()`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn process(r, x) {
+/// x + (r ?? 0)
+/// }
+/// fn process_extra(r, x, i) {
+/// x + i + (r ?? 0)
+/// }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce("process");
+///
+/// print(y); // prints 15
+///
+/// let y = x.reduce("process_extra");
+///
+/// print(y); // prints 25
+/// ```
+fn reduce(array: Array, reducer: String) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements while applying the `reducer` function.
+///
+/// # Function Parameters
+///
+/// * `result`: accumulated result, initially `()`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce(|r, v| v + (r ?? 0));
+///
+/// print(y); // prints 15
+///
+/// let y = x.reduce(|r, v, i| v + i + (r ?? 0));
+///
+/// print(y); // prints 25
+/// ```
+fn reduce(array: Array, reducer: FnPtr) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements while applying a function named by `reducer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `reducer` must exist taking these parameters:
+///
+/// * `result`: accumulated result, starting with the value of `initial`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn process(r, x) { x + r }
+///
+/// fn process_extra(r, x, i) { x + i + r }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce("process", 5);
+///
+/// print(y); // prints 20
+///
+/// let y = x.reduce("process_extra", 5);
+///
+/// print(y); // prints 30
+/// ```
+fn reduce(array: Array, reducer: String, initial: ?) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements while applying the `reducer` function.
+///
+/// # Function Parameters
+///
+/// * `result`: accumulated result, starting with the value of `initial`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce(|r, v| v + r, 5);
+///
+/// print(y); // prints 20
+///
+/// let y = x.reduce(|r, v, i| v + i + r, 5);
+///
+/// print(y); // prints 30
+/// ```
+fn reduce(array: Array, reducer: FnPtr, initial: ?) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements, in _reverse_ order,
+/// while applying a function named by `reducer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `reducer` must exist taking these parameters:
+///
+/// * `result`: accumulated result, initially `()`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn process(r, x) {
+/// x + (r ?? 0)
+/// }
+/// fn process_extra(r, x, i) {
+/// x + i + (r ?? 0)
+/// }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce_rev("process");
+///
+/// print(y); // prints 15
+///
+/// let y = x.reduce_rev("process_extra");
+///
+/// print(y); // prints 25
+/// ```
+fn reduce_rev(array: Array, reducer: String) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements, in _reverse_ order,
+/// while applying the `reducer` function.
+///
+/// # Function Parameters
+///
+/// * `result`: accumulated result, initially `()`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce_rev(|r, v| v + (r ?? 0));
+///
+/// print(y); // prints 15
+///
+/// let y = x.reduce_rev(|r, v, i| v + i + (r ?? 0));
+///
+/// print(y); // prints 25
+/// ```
+fn reduce_rev(array: Array, reducer: FnPtr) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements, in _reverse_ order,
+/// while applying a function named by `reducer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `reducer` must exist taking these parameters:
+///
+/// * `result`: accumulated result, starting with the value of `initial`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn process(r, x) { x + r }
+///
+/// fn process_extra(r, x, i) { x + i + r }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce_rev("process", 5);
+///
+/// print(y); // prints 20
+///
+/// let y = x.reduce_rev("process_extra", 5);
+///
+/// print(y); // prints 30
+/// ```
+fn reduce_rev(array: Array, reducer: String, initial: ?) -> RhaiResult;
+
+/// Reduce an array by iterating through all elements, in _reverse_ order,
+/// while applying the `reducer` function.
+///
+/// # Function Parameters
+///
+/// * `result`: accumulated result, starting with the value of `initial`
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.reduce_rev(|r, v| v + r, 5);
+///
+/// print(y); // prints 20
+///
+/// let y = x.reduce_rev(|r, v, i| v + i + r, 5);
+///
+/// print(y); // prints 30
+/// ```
+fn reduce_rev(array: Array, reducer: FnPtr, initial: ?) -> RhaiResult;
+
+/// Remove the element at the specified `index` from the array and return it.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` < -length of array, `()` is returned.
+/// * If `index` ≥ length of array, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// print(x.remove(1)); // prints 2
+///
+/// print(x); // prints "[1, 3]"
+///
+/// print(x.remove(-2)); // prints 1
+///
+/// print(x); // prints "[3]"
+/// ```
+fn remove(array: Array, index: int) -> ?;
+
+/// Remove the byte at the specified `index` from the BLOB and return it.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `index` < -length of BLOB, zero is returned.
+/// * If `index` ≥ length of BLOB, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(x.remove(1)); // prints 2
+///
+/// print(x); // prints "[01030405]"
+///
+/// print(x.remove(-2)); // prints 4
+///
+/// print(x); // prints "[010305]"
+/// ```
+fn remove(blob: Blob, index: int) -> int;
+
+/// Remove any property of the specified `name` from the object map, returning its value.
+///
+/// If the property does not exist, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// let x = m.remove("b");
+///
+/// print(x); // prints 2
+///
+/// print(m); // prints "#{a:1, c:3}"
+/// ```
+fn remove(map: Map, property: String) -> ?;
+
+/// Remove all occurrences of a character from the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.remove("o");
+///
+/// print(text); // prints "hell, wrld! hell, fbar!"
+/// ```
+fn remove(string: String, character: char) -> ();
+
+/// Remove all occurrences of a sub-string from the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.remove("hello");
+///
+/// print(text); // prints ", world! , foobar!"
+/// ```
+fn remove(string: String, sub_string: String) -> ();
+
+/// Replace all occurrences of the specified character in the string with another character.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.replace("l", '*');
+///
+/// print(text); // prints "he**o, wor*d! he**o, foobar!"
+/// ```
+fn replace(string: String, find_character: char, substitute_character: char) -> ();
+
+/// Replace all occurrences of the specified character in the string with another string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.replace('l', "(^)");
+///
+/// print(text); // prints "he(^)(^)o, wor(^)d! he(^)(^)o, foobar!"
+/// ```
+fn replace(string: String, find_character: char, substitute_string: String) -> ();
+
+/// Replace all occurrences of the specified sub-string in the string with the specified character.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.replace("hello", '*');
+///
+/// print(text); // prints "*, world! *, foobar!"
+/// ```
+fn replace(string: String, find_string: String, substitute_character: char) -> ();
+
+/// Replace all occurrences of the specified sub-string in the string with another string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.replace("hello", "hey");
+///
+/// print(text); // prints "hey, world! hey, foobar!"
+/// ```
+fn replace(string: String, find_string: String, substitute_string: String) -> ();
+
+/// Remove all elements in the array that do not return `true` when applied a function named by
+/// `filter` and return them as a new array.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn large(x) { x >= 3 }
+///
+/// fn screen(x, i) { x + i <= 5 }
+///
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain("large");
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// print(y); // prints "[1, 2]"
+///
+/// let z = x.retain("screen");
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn retain(array: Array, filter: String) -> Array;
+
+/// Remove all elements in the array that do not return `true` when applied the `filter`
+/// function and return them as a new array.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain(|v| v >= 3);
+///
+/// print(x); // prints "[3, 4, 5]"
+///
+/// print(y); // prints "[1, 2]"
+///
+/// let z = x.retain(|v, i| v + i <= 5);
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[5]"
+/// ```
+fn retain(array: Array, filter: FnPtr) -> Array;
+
+/// Remove all elements in the array not within an exclusive `range` and return them as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain(1..4);
+///
+/// print(x); // prints "[2, 3, 4]"
+///
+/// print(y); // prints "[1, 5]"
+///
+/// let z = x.retain(1..3);
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[1]"
+/// ```
+fn retain(array: Array, range: Range<int>) -> Array;
+
+/// Remove all elements in the array not within an inclusive `range` and return them as a new array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain(1..=3);
+///
+/// print(x); // prints "[2, 3, 4]"
+///
+/// print(y); // prints "[1, 5]"
+///
+/// let z = x.retain(1..=2);
+///
+/// print(x); // prints "[3, 4]"
+///
+/// print(z); // prints "[1]"
+/// ```
+fn retain(array: Array, range: RangeInclusive<int>) -> Array;
+
+/// Remove all bytes in the BLOB not within an exclusive `range` and return them as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.retain(1..4);
+///
+/// print(b1); // prints "[020304]"
+///
+/// print(b2); // prints "[0105]"
+///
+/// let b3 = b1.retain(1..3);
+///
+/// print(b1); // prints "[0304]"
+///
+/// print(b2); // prints "[01]"
+/// ```
+fn retain(blob: Blob, range: Range<int>) -> Blob;
+
+/// Remove all bytes in the BLOB not within an inclusive `range` and return them as a new BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.retain(1..=3);
+///
+/// print(b1); // prints "[020304]"
+///
+/// print(b2); // prints "[0105]"
+///
+/// let b3 = b1.retain(1..=2);
+///
+/// print(b1); // prints "[0304]"
+///
+/// print(b2); // prints "[01]"
+/// ```
+fn retain(blob: Blob, range: RangeInclusive<int>) -> Blob;
+
+/// Remove all elements not within a portion of the array and return them as a new array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, all elements are removed returned.
+/// * If `len` ≤ 0, all elements are removed and returned.
+/// * If `start` position + `len` ≥ length of array, entire portion of the array before the `start` position is removed and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.retain(1, 2);
+///
+/// print(x); // prints "[2, 3]"
+///
+/// print(y); // prints "[1, 4, 5]"
+///
+/// let z = x.retain(-1, 1);
+///
+/// print(x); // prints "[3]"
+///
+/// print(z); // prints "[2]"
+/// ```
+fn retain(array: Array, start: int, len: int) -> Array;
+
+/// Remove all bytes not within a portion of the BLOB and return them as a new BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, all elements are removed returned.
+/// * If `len` ≤ 0, all elements are removed and returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB before the `start` position is removed and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.retain(1, 2);
+///
+/// print(b1); // prints "[0203]"
+///
+/// print(b2); // prints "[010405]"
+///
+/// let b3 = b1.retain(-1, 1);
+///
+/// print(b1); // prints "[03]"
+///
+/// print(b3); // prints "[02]"
+/// ```
+fn retain(blob: Blob, start: int, len: int) -> Blob;
+
+/// Reverse all the elements in the array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// x.reverse();
+///
+/// print(x); // prints "[5, 4, 3, 2, 1]"
+/// ```
+fn reverse(array: Array) -> ();
+
+/// Reverse the BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b); // prints "[0102030405]"
+///
+/// b.reverse();
+///
+/// print(b); // prints "[0504030201]"
+/// ```
+fn reverse(blob: Blob) -> ();
+
+/// Return the nearest whole number closest to the decimal number.
+/// Always round mid-point towards the closest even number.
+fn round(x: Decimal) -> Decimal;
+
+/// Return the nearest whole number closest to the floating-point number.
+/// Rounds away from zero.
+fn round(x: float) -> float;
+
+/// Round the decimal number to the specified number of `digits` after the decimal point and return it.
+/// Always round mid-point towards the closest even number.
+fn round(x: Decimal, digits: int) -> Decimal;
+
+/// Round the decimal number to the specified number of `digits` after the decimal point and return it.
+/// Always round towards zero.
+fn round_down(x: Decimal, digits: int) -> Decimal;
+
+/// Round the decimal number to the specified number of `digits` after the decimal point and return it.
+/// Always round mid-points towards zero.
+fn round_half_down(x: Decimal, digits: int) -> Decimal;
+
+/// Round the decimal number to the specified number of `digits` after the decimal point and return it.
+/// Always round mid-points away from zero.
+fn round_half_up(x: Decimal, digits: int) -> Decimal;
+
+/// Round the decimal number to the specified number of `digits` after the decimal point and return it.
+/// Always round away from zero.
+fn round_up(x: Decimal, digits: int) -> Decimal;
+
+/// Set the element at the `index` position in the array to a new `value`.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` < -length of array, the array is not modified.
+/// * If `index` ≥ length of array, the array is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// x.set(0, 42);
+///
+/// print(x); // prints "[42, 2, 3]"
+///
+/// x.set(-3, 0);
+///
+/// print(x); // prints "[0, 2, 3]"
+///
+/// x.set(99, 123);
+///
+/// print(x); // prints "[0, 2, 3]"
+/// ```
+fn set(array: Array, index: int, value: ?) -> ();
+
+/// Set the particular `index` position in the BLOB to a new byte `value`.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `index` < -length of BLOB, the BLOB is not modified.
+/// * If `index` ≥ length of BLOB, the BLOB is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// b.set(0, 0x42);
+///
+/// print(b); // prints "[4202030405]"
+///
+/// b.set(-3, 0);
+///
+/// print(b); // prints "[4202000405]"
+///
+/// b.set(99, 123);
+///
+/// print(b); // prints "[4202000405]"
+/// ```
+fn set(blob: Blob, index: int, value: int) -> ();
+
+/// Set the value of the `property` in the object map to a new `value`.
+///
+/// If `property` does not exist in the object map, it is added.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a: 1, b: 2, c: 3};
+///
+/// m.set("b", 42)'
+///
+/// print(m); // prints "#{a: 1, b: 42, c: 3}"
+///
+/// x.set("x", 0);
+///
+/// print(m); // prints "#{a: 1, b: 42, c: 3, x: 0}"
+/// ```
+fn set(map: Map, property: String, value: ?) -> ();
+
+/// Set the `index` position in the string to a new `character`.
+///
+/// * If `index` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `index` < -length of string, the string is not modified.
+/// * If `index` ≥ length of string, the string is not modified.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// text.set(3, 'x');
+///
+/// print(text); // prints "helxo, world!"
+///
+/// text.set(-3, 'x');
+///
+/// print(text); // prints "hello, worxd!"
+///
+/// text.set(99, 'x');
+///
+/// print(text); // prints "hello, worxd!"
+/// ```
+fn set(string: String, index: int, character: char) -> ();
+
+/// Set the _tag_ of a `Dynamic` value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// x.tag = 42;
+///
+/// print(x.tag); // prints 42
+/// ```
+fn set tag(value: ?, tag: int) -> ();
+
+/// Set the specified `bit` in the number if the new value is `true`.
+/// Clear the `bit` if the new value is `false`.
+///
+/// If `bit` < 0, position counts from the MSB (Most Significant Bit).
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// x.set_bit(5, true);
+///
+/// print(x); // prints 123488
+///
+/// x.set_bit(6, false);
+///
+/// print(x); // prints 123424
+///
+/// x.set_bit(-48, false);
+///
+/// print(x); // prints 57888 on 64-bit
+/// ```
+fn set_bit(value: int, bit: int, new_value: bool) -> ();
+
+/// Replace an exclusive range of bits in the number with a new value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// x.set_bits(5..10, 42);
+///
+/// print(x); // print 123200
+/// ```
+fn set_bits(value: int, range: Range<int>, new_value: int) -> ();
+
+/// Replace an inclusive range of bits in the number with a new value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// x.set_bits(5..=9, 42);
+///
+/// print(x); // print 123200
+/// ```
+fn set_bits(value: int, range: RangeInclusive<int>, new_value: int) -> ();
+
+/// Replace a portion of bits in the number with a new value.
+///
+/// * If `start` < 0, position counts from the MSB (Most Significant Bit).
+/// * If `bits` ≤ 0, the number is not modified.
+/// * If `start` position + `bits` ≥ total number of bits, the bits after the `start` position are replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 123456;
+///
+/// x.set_bits(5, 8, 42);
+///
+/// print(x); // prints 124224
+///
+/// x.set_bits(-16, 10, 42);
+///
+/// print(x); // prints 11821949021971776 on 64-bit
+/// ```
+fn set_bits(value: int, bit: int, bits: int, new_value: int) -> ();
+
+/// Set the _tag_ of a `Dynamic` value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// x.tag = 42;
+///
+/// print(x.tag); // prints 42
+/// ```
+fn set_tag(value: ?, tag: int) -> ();
+
+/// Remove the first element from the array and return it.
+///
+/// If the array is empty, `()` is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3];
+///
+/// print(x.shift()); // prints 1
+///
+/// print(x); // prints "[2, 3]"
+/// ```
+fn shift(array: Array) -> ?;
+
+/// Remove the first byte from the BLOB and return it.
+///
+/// If the BLOB is empty, zero is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// print(b.shift()); // prints 1
+///
+/// print(b); // prints "[02030405]"
+/// ```
+fn shift(blob: Blob) -> int;
+
+/// Return the sign (as an integer) of the decimal number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: Decimal) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: int) -> int;
+
+/// Return the sign (as an integer) of the floating-point number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: f32) -> int;
+
+/// Return the sign (as an integer) of the floating-point number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: float) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: i128) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: i16) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: i32) -> int;
+
+/// Return the sign (as an integer) of the number according to the following:
+///
+/// * `0` if the number is zero
+/// * `1` if the number is positive
+/// * `-1` if the number is negative
+fn sign(x: i8) -> int;
+
+/// Return the sine of the decimal number in radians.
+fn sin(x: Decimal) -> Decimal;
+
+/// Return the sine of the floating-point number in radians.
+fn sin(x: float) -> float;
+
+/// Return the hyperbolic sine of the floating-point number in radians.
+fn sinh(x: float) -> float;
+
+/// Block the current thread for a particular number of `seconds`.
+fn sleep(seconds: int) -> ();
+
+/// Block the current thread for a particular number of `seconds`.
+fn sleep(seconds: float) -> ();
+
+/// Return `true` if any element in the array that returns `true` when applied a function named
+/// by `filter`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `filter` must exist taking these parameters:
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// fn large(x) { x > 3 }
+///
+/// fn huge(x) { x > 10 }
+///
+/// fn screen(x, i) { i > x }
+///
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.some("large")); // prints true
+///
+/// print(x.some("huge")); // prints false
+///
+/// print(x.some("screen")); // prints true
+/// ```
+fn some(array: Array, filter: String) -> bool;
+
+/// Return `true` if any element in the array that returns `true` when applied the `filter` function.
+///
+/// # Function Parameters
+///
+/// * `element`: copy of array element
+/// * `index` _(optional)_: current index in the array
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+///
+/// print(x.some(|v| v > 3)); // prints true
+///
+/// print(x.some(|v| v > 10)); // prints false
+///
+/// print(x.some(|v, i| i > v)); // prints true
+/// ```
+fn some(array: Array, filter: FnPtr) -> bool;
+
+/// Sort the array.
+///
+/// All elements in the array must be of the same data type.
+///
+/// # Supported Data Types
+///
+/// * integer numbers
+/// * floating-point numbers
+/// * decimal numbers
+/// * characters
+/// * strings
+/// * booleans
+/// * `()`
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10];
+///
+/// x.sort();
+///
+/// print(x); // prints "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
+/// ```
+fn sort(array: Array) -> ();
+
+/// Sort the array based on applying a function named by `comparer`.
+///
+/// # Function Parameters
+///
+/// A function with the same name as the value of `comparer` must exist taking these parameters:
+///
+/// * `element1`: copy of the current array element to compare
+/// * `element2`: copy of the next array element to compare
+///
+/// ## Return Value
+///
+/// * Any integer > 0 if `element1 > element2`
+/// * Zero if `element1 == element2`
+/// * Any integer < 0 if `element1 < element2`
+///
+/// # Example
+///
+/// ```rhai
+/// fn reverse(a, b) {
+/// if a > b {
+/// -1
+/// } else if a < b {
+/// 1
+/// } else {
+/// 0
+/// }
+/// }
+/// let x = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10];
+///
+/// x.sort("reverse");
+///
+/// print(x); // prints "[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]"
+/// ```
+fn sort(array: Array, comparer: String) -> ();
+
+/// Sort the array based on applying the `comparer` function.
+///
+/// # Function Parameters
+///
+/// * `element1`: copy of the current array element to compare
+/// * `element2`: copy of the next array element to compare
+///
+/// ## Return Value
+///
+/// * Any integer > 0 if `element1 > element2`
+/// * Zero if `element1 == element2`
+/// * Any integer < 0 if `element1 < element2`
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10];
+///
+/// // Do comparisons in reverse
+/// x.sort(|a, b| if a > b { -1 } else if a < b { 1 } else { 0 });
+///
+/// print(x); // prints "[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]"
+/// ```
+fn sort(array: Array, comparer: FnPtr) -> ();
+
+/// Replace an exclusive range of the array with another array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [7, 8, 9, 10];
+///
+/// x.splice(1..3, y);
+///
+/// print(x); // prints "[1, 7, 8, 9, 10, 4, 5]"
+/// ```
+fn splice(array: Array, range: Range<int>, replace: Array) -> ();
+
+/// Replace an inclusive range of the array with another array.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [7, 8, 9, 10];
+///
+/// x.splice(1..=3, y);
+///
+/// print(x); // prints "[1, 7, 8, 9, 10, 5]"
+/// ```
+fn splice(array: Array, range: RangeInclusive<int>, replace: Array) -> ();
+
+/// Replace an exclusive `range` of the BLOB with another BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob(10, 0x42);
+/// let b2 = blob(5, 0x18);
+///
+/// b1.splice(1..4, b2);
+///
+/// print(b1); // prints "[4218181818184242 42424242]"
+/// ```
+fn splice(blob: Blob, range: Range<int>, replace: Blob) -> ();
+
+/// Replace an inclusive `range` of the BLOB with another BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob(10, 0x42);
+/// let b2 = blob(5, 0x18);
+///
+/// b1.splice(1..=4, b2);
+///
+/// print(b1); // prints "[4218181818184242 424242]"
+/// ```
+fn splice(blob: Blob, range: RangeInclusive<int>, replace: Blob) -> ();
+
+/// Replace a portion of the array with another array.
+///
+/// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `start` < -length of array, position counts from the beginning of the array.
+/// * If `start` ≥ length of array, the other array is appended to the end of the array.
+/// * If `len` ≤ 0, the other array is inserted into the array at the `start` position without replacing any element.
+/// * If `start` position + `len` ≥ length of array, entire portion of the array after the `start` position is replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+/// let y = [7, 8, 9, 10];
+///
+/// x.splice(1, 2, y);
+///
+/// print(x); // prints "[1, 7, 8, 9, 10, 4, 5]"
+///
+/// x.splice(-5, 4, y);
+///
+/// print(x); // prints "[1, 7, 7, 8, 9, 10, 5]"
+/// ```
+fn splice(array: Array, start: int, len: int, replace: Array) -> ();
+
+/// Replace a portion of the BLOB with another BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, the other BLOB is appended to the end of the BLOB.
+/// * If `len` ≤ 0, the other BLOB is inserted into the BLOB at the `start` position without replacing anything.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is replaced.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob(10, 0x42);
+/// let b2 = blob(5, 0x18);
+///
+/// b1.splice(1, 3, b2);
+///
+/// print(b1); // prints "[4218181818184242 42424242]"
+///
+/// b1.splice(-5, 4, b2);
+///
+/// print(b1); // prints "[4218181818184218 1818181842]"
+/// ```
+fn splice(blob: Blob, start: int, len: int, replace: Blob) -> ();
+
+/// Split the string into segments based on whitespaces, returning an array of the segments.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split()); // prints ["hello,", "world!", "hello,", "foo!"]
+/// ```
+fn split(string: String) -> Array;
+
+/// Cut off the array at `index` and return it as a new array.
+///
+/// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+/// * If `index` is zero, the entire array is cut and returned.
+/// * If `index` < -length of array, the entire array is cut and returned.
+/// * If `index` ≥ length of array, nothing is cut from the array and an empty array is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// let y = x.split(2);
+///
+/// print(y); // prints "[3, 4, 5]"
+///
+/// print(x); // prints "[1, 2]"
+/// ```
+fn split(array: Array, index: int) -> Array;
+
+/// Cut off the BLOB at `index` and return it as a new BLOB.
+///
+/// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `index` is zero, the entire BLOB is cut and returned.
+/// * If `index` < -length of BLOB, the entire BLOB is cut and returned.
+/// * If `index` ≥ length of BLOB, nothing is cut from the BLOB and an empty BLOB is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let b1 = blob();
+///
+/// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+///
+/// let b2 = b1.split(2);
+///
+/// print(b2); // prints "[030405]"
+///
+/// print(b1); // prints "[0102]"
+/// ```
+fn split(blob: Blob, index: int) -> Blob;
+
+/// Split the string into two at the specified `index` position and return it both strings
+/// as an array.
+///
+/// The character at the `index` position (if any) is returned in the _second_ string.
+///
+/// * If `index` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `index` < -length of string, it is equivalent to cutting at position 0.
+/// * If `index` ≥ length of string, it is equivalent to cutting at the end of the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.split(6)); // prints ["hello,", " world!"]
+///
+/// print(text.split(13)); // prints ["hello, world!", ""]
+///
+/// print(text.split(-6)); // prints ["hello, ", "world!"]
+///
+/// print(text.split(-99)); // prints ["", "hello, world!"]
+/// ```
+fn split(string: String, index: int) -> Array;
+
+/// Split the string into segments based on a `delimiter` string, returning an array of the segments.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split("ll")); // prints ["he", "o, world! he", "o, foo!"]
+/// ```
+fn split(string: String, delimiter: String) -> Array;
+
+/// Split the string into segments based on a `delimiter` character, returning an array of the segments.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split('l')); // prints ["he", "", "o, wor", "d! he", "", "o, foo!"]
+/// ```
+fn split(string: String, delimiter: char) -> Array;
+
+/// Split the string into at most the specified number of `segments` based on a `delimiter` string,
+/// returning an array of the segments.
+///
+/// If `segments` < 1, only one segment is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split("ll", 2)); // prints ["he", "o, world! hello, foo!"]
+/// ```
+fn split(string: String, delimiter: String, segments: int) -> Array;
+
+/// Split the string into at most the specified number of `segments` based on a `delimiter` character,
+/// returning an array of the segments.
+///
+/// If `segments` < 1, only one segment is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split('l', 3)); // prints ["he", "", "o, world! hello, foo!"]
+/// ```
+fn split(string: String, delimiter: char, segments: int) -> Array;
+
+/// Split the string into segments based on a `delimiter` string, returning an array of the
+/// segments in _reverse_ order.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split_rev("ll")); // prints ["o, foo!", "o, world! he", "he"]
+/// ```
+fn split_rev(string: String, delimiter: String) -> Array;
+
+/// Split the string into segments based on a `delimiter` character, returning an array of
+/// the segments in _reverse_ order.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split_rev('l')); // prints ["o, foo!", "", "d! he", "o, wor", "", "he"]
+/// ```
+fn split_rev(string: String, delimiter: char) -> Array;
+
+/// Split the string into at most a specified number of `segments` based on a `delimiter` string,
+/// returning an array of the segments in _reverse_ order.
+///
+/// If `segments` < 1, only one segment is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split_rev("ll", 2)); // prints ["o, foo!", "hello, world! he"]
+/// ```
+fn split_rev(string: String, delimiter: String, segments: int) -> Array;
+
+/// Split the string into at most the specified number of `segments` based on a `delimiter` character,
+/// returning an array of the segments.
+///
+/// If `segments` < 1, only one segment is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foo!";
+///
+/// print(text.split('l', 3)); // prints ["o, foo!", "", "hello, world! he"
+/// ```
+fn split_rev(string: String, delimiter: char, segments: int) -> Array;
+
+/// Return the square root of the decimal number.
+fn sqrt(x: Decimal) -> Decimal;
+
+/// Return the square root of the floating-point number.
+fn sqrt(x: float) -> float;
+
+/// Return the start of the exclusive range.
+fn start(range: ExclusiveRange) -> int;
+
+/// Return the start of the inclusive range.
+fn start(range: InclusiveRange) -> int;
+
+/// Return `true` if the string starts with a specified string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.starts_with("hello")); // prints true
+///
+/// print(text.starts_with("world")); // prints false
+/// ```
+fn starts_with(string: String, match_string: String) -> bool;
+
+/// Copy an exclusive range of characters from the string and return it as a new string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.sub_string(3..7)); // prints "lo, "
+/// ```
+fn sub_string(string: String, range: Range<int>) -> String;
+
+/// Copy an inclusive range of characters from the string and return it as a new string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.sub_string(3..=7)); // prints "lo, w"
+/// ```
+fn sub_string(string: String, range: RangeInclusive<int>) -> String;
+
+/// Copy a portion of the string beginning at the `start` position till the end and return it as
+/// a new string.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, the entire string is copied and returned.
+/// * If `start` ≥ length of string, an empty string is returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.sub_string(5)); // prints ", world!"
+///
+/// print(text.sub_string(-5)); // prints "orld!"
+/// ```
+fn sub_string(string: String, start: int) -> String;
+
+/// Copy a portion of the string and return it as a new string.
+///
+/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+/// * If `start` < -length of string, position counts from the beginning of the string.
+/// * If `start` ≥ length of string, an empty string is returned.
+/// * If `len` ≤ 0, an empty string is returned.
+/// * If `start` position + `len` ≥ length of string, entire portion of the string after the `start` position is copied and returned.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!";
+///
+/// print(text.sub_string(3, 4)); // prints "lo, "
+///
+/// print(text.sub_string(-8, 3)); // prints ", w"
+/// ```
+fn sub_string(string: String, start: int, len: int) -> String;
+
+/// Return the _tag_ of a `Dynamic` value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// x.tag = 42;
+///
+/// print(x.tag); // prints 42
+/// ```
+fn tag(value: ?) -> int;
+
+/// Return the tangent of the decimal number in radians.
+fn tan(x: Decimal) -> Decimal;
+
+/// Return the tangent of the floating-point number in radians.
+fn tan(x: float) -> float;
+
+/// Return the hyperbolic tangent of the floating-point number in radians.
+fn tanh(x: float) -> float;
+
+/// Create a timestamp containing the current system time.
+///
+/// # Example
+///
+/// ```rhai
+/// let now = timestamp();
+///
+/// sleep(10.0); // sleep for 10 seconds
+///
+/// print(now.elapsed); // prints 10.???
+/// ```
+fn timestamp() -> Instant;
+
+/// Convert the BLOB into an array of integers.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob(5, 0x42);
+///
+/// let x = b.to_array();
+///
+/// print(x); // prints "[66, 66, 66, 66, 66]"
+/// ```
+fn to_array(blob: Blob) -> Array;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: i128) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: i16) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: i32) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: int) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: i8) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u128) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u16) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u32) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u64) -> String;
+
+/// Convert the `value` into a string in binary format.
+fn to_binary(value: u8) -> String;
+
+/// Convert the string into an UTF-8 encoded byte-stream as a BLOB.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "朝には紅顔ありて夕べには白骨となる";
+///
+/// let bytes = text.to_blob();
+///
+/// print(bytes.len()); // prints 51
+/// ```
+fn to_blob(string: String) -> Blob;
+
+/// Return an array containing all the characters of the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello";
+///
+/// print(text.to_chars()); // prints "['h', 'e', 'l', 'l', 'o']"
+/// ```
+fn to_chars(string: String) -> Array;
+
+/// Convert the array into a string.
+fn to_debug(array: Array) -> String;
+
+/// Convert the string into debug format.
+fn to_debug(character: char) -> String;
+
+/// Convert the function pointer into a string in debug format.
+fn to_debug(f: FnPtr) -> String;
+
+/// Convert the value of the `item` into a string in debug format.
+fn to_debug(item: ?) -> String;
+
+/// Convert the object map into a string.
+fn to_debug(map: Map) -> String;
+
+/// Convert the value of `number` into a string.
+fn to_debug(number: f32) -> String;
+
+/// Convert the value of `number` into a string.
+fn to_debug(number: float) -> String;
+
+/// Convert the string into debug format.
+fn to_debug(string: String) -> String;
+
+/// Convert the unit into a string in debug format.
+fn to_debug(unit: ()) -> String;
+
+/// Convert the boolean value into a string in debug format.
+fn to_debug(value: bool) -> String;
+
+/// Convert the floating-point number to decimal.
+fn to_decimal(x: f32) -> Decimal;
+
+/// Convert the floating-point number to decimal.
+fn to_decimal(x: float) -> Decimal;
+
+fn to_decimal(x: i16) -> Decimal;
+
+fn to_decimal(x: i32) -> Decimal;
+
+fn to_decimal(x: int) -> Decimal;
+
+fn to_decimal(x: i8) -> Decimal;
+
+fn to_decimal(x: u16) -> Decimal;
+
+fn to_decimal(x: u32) -> Decimal;
+
+fn to_decimal(x: u64) -> Decimal;
+
+fn to_decimal(x: u8) -> Decimal;
+
+/// Convert radians to degrees.
+fn to_degrees(x: float) -> float;
+
+/// Convert the decimal number to floating-point.
+fn to_float(x: Decimal) -> float;
+
+/// Convert the 32-bit floating-point number to 64-bit.
+fn to_float(x: f32) -> float;
+
+fn to_float(x: i128) -> float;
+
+fn to_float(x: i16) -> float;
+
+fn to_float(x: i32) -> float;
+
+fn to_float(x: int) -> float;
+
+fn to_float(x: i8) -> float;
+
+fn to_float(x: u128) -> float;
+
+fn to_float(x: u16) -> float;
+
+fn to_float(x: u32) -> float;
+
+fn to_float(x: u8) -> float;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: i128) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: i16) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: i32) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: int) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: i8) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u128) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u16) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u32) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u64) -> String;
+
+/// Convert the `value` into a string in hex format.
+fn to_hex(value: u8) -> String;
+
+/// Convert the decimal number into an integer.
+fn to_int(x: Decimal) -> int;
+
+fn to_int(x: char) -> int;
+
+/// Convert the floating-point number into an integer.
+fn to_int(x: f32) -> int;
+
+/// Convert the floating-point number into an integer.
+fn to_int(x: float) -> int;
+
+fn to_int(x: i128) -> int;
+
+fn to_int(x: i16) -> int;
+
+fn to_int(x: i32) -> int;
+
+fn to_int(x: int) -> int;
+
+fn to_int(x: i8) -> int;
+
+fn to_int(x: u128) -> int;
+
+fn to_int(x: u16) -> int;
+
+fn to_int(x: u32) -> int;
+
+fn to_int(x: u64) -> int;
+
+fn to_int(x: u8) -> int;
+
+/// Return the JSON representation of the object map.
+///
+/// # Data types
+///
+/// Only the following data types should be kept inside the object map:
+/// `INT`, `FLOAT`, `ImmutableString`, `char`, `bool`, `()`, `Array`, `Map`.
+///
+/// # Errors
+///
+/// Data types not supported by JSON serialize into formats that may
+/// invalidate the result.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// print(m.to_json()); // prints {"a":1, "b":2, "c":3}
+/// ```
+fn to_json(map: Map) -> String;
+
+/// Convert the character to lower-case and return it as a new character.
+///
+/// # Example
+///
+/// ```rhai
+/// let ch = 'A';
+///
+/// print(ch.to_lower()); // prints 'a'
+///
+/// print(ch); // prints 'A'
+/// ```
+fn to_lower(character: char) -> char;
+
+/// Convert the string to all lower-case and return it as a new string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "HELLO, WORLD!"
+///
+/// print(text.to_lower()); // prints "hello, world!"
+///
+/// print(text); // prints "HELLO, WORLD!"
+/// ```
+fn to_lower(string: String) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: i128) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: i16) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: i32) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: int) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: i8) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u128) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u16) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u32) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u64) -> String;
+
+/// Convert the `value` into a string in octal format.
+fn to_octal(value: u8) -> String;
+
+/// Convert degrees to radians.
+fn to_radians(x: float) -> float;
+
+/// Convert the array into a string.
+fn to_string(array: Array) -> String;
+
+/// Return the character into a string.
+fn to_string(character: char) -> String;
+
+/// Convert the value of the `item` into a string.
+fn to_string(item: ?) -> String;
+
+/// Convert the object map into a string.
+fn to_string(map: Map) -> String;
+
+/// Convert the value of `number` into a string.
+fn to_string(number: f32) -> String;
+
+/// Convert the value of `number` into a string.
+fn to_string(number: float) -> String;
+
+/// Return the `string`.
+fn to_string(string: String) -> String;
+
+/// Return the empty string.
+fn to_string(unit: ()) -> String;
+
+/// Return the boolean value into a string.
+fn to_string(value: bool) -> String;
+
+/// Convert the character to upper-case and return it as a new character.
+///
+/// # Example
+///
+/// ```rhai
+/// let ch = 'a';
+///
+/// print(ch.to_upper()); // prints 'A'
+///
+/// print(ch); // prints 'a'
+/// ```
+fn to_upper(character: char) -> char;
+
+/// Convert the string to all upper-case and return it as a new string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world!"
+///
+/// print(text.to_upper()); // prints "HELLO, WORLD!"
+///
+/// print(text); // prints "hello, world!"
+/// ```
+fn to_upper(string: String) -> String;
+
+/// Remove whitespace characters from both ends of the string.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = " hello ";
+///
+/// text.trim();
+///
+/// print(text); // prints "hello"
+/// ```
+fn trim(string: String) -> ();
+
+/// Cut off the array at the specified length.
+///
+/// * If `len` ≤ 0, the array is cleared.
+/// * If `len` ≥ length of array, the array is not truncated.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = [1, 2, 3, 4, 5];
+///
+/// x.truncate(3);
+///
+/// print(x); // prints "[1, 2, 3]"
+///
+/// x.truncate(10);
+///
+/// print(x); // prints "[1, 2, 3]"
+/// ```
+fn truncate(array: Array, len: int) -> ();
+
+/// Cut off the BLOB at the specified length.
+///
+/// * If `len` ≤ 0, the BLOB is cleared.
+/// * If `len` ≥ length of BLOB, the BLOB is not truncated.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// b.truncate(3);
+///
+/// print(b); // prints "[010203]"
+///
+/// b.truncate(10);
+///
+/// print(b); // prints "[010203]"
+/// ```
+fn truncate(blob: Blob, len: int) -> ();
+
+/// Cut off the string at the specified number of characters.
+///
+/// * If `len` ≤ 0, the string is cleared.
+/// * If `len` ≥ length of string, the string is not truncated.
+///
+/// # Example
+///
+/// ```rhai
+/// let text = "hello, world! hello, foobar!";
+///
+/// text.truncate(13);
+///
+/// print(text); // prints "hello, world!"
+///
+/// x.truncate(10);
+///
+/// print(text); // prints "hello, world!"
+/// ```
+fn truncate(string: String, len: int) -> ();
+
+/// Return an array with all the property values in the object map.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// print(m.values()); // prints "[1, 2, 3]""
+/// ```
+fn values(map: Map) -> Array;
+
+/// Write an ASCII string to the bytes within an exclusive `range` in the BLOB.
+///
+/// Each ASCII character encodes to one single byte in the BLOB.
+/// Non-ASCII characters are ignored.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_ascii(1..5, "hello, world!");
+///
+/// print(b); // prints "[0068656c6c000000]"
+/// ```
+fn write_ascii(blob: Blob, range: Range<int>, string: String) -> ();
+
+/// Write an ASCII string to the bytes within an inclusive `range` in the BLOB.
+///
+/// Each ASCII character encodes to one single byte in the BLOB.
+/// Non-ASCII characters are ignored.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_ascii(1..=5, "hello, world!");
+///
+/// print(b); // prints "[0068656c6c6f0000]"
+/// ```
+fn write_ascii(blob: Blob, range: RangeInclusive<int>, string: String) -> ();
+
+/// Write an ASCII string to the bytes within an exclusive `range` in the BLOB.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, the BLOB is not modified.
+/// * If `len` ≤ 0, the BLOB is not modified.
+/// * If `start` position + `len` ≥ length of BLOB, only the portion of the BLOB after the `start` position is modified.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_ascii(1, 5, "hello, world!");
+///
+/// print(b); // prints "[0068656c6c6f0000]"
+/// ```
+fn write_ascii(blob: Blob, start: int, len: int, string: String) -> ();
+
+/// Write a `FLOAT` value to the bytes within an exclusive `range` in the BLOB
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_be(blob: Blob, range: Range<int>, value: float) -> ();
+
+/// Write an `INT` value to the bytes within an exclusive `range` in the BLOB
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8, 0x42);
+///
+/// b.write_be_int(1..3, 0x99);
+///
+/// print(b); // prints "[4200004242424242]"
+/// ```
+fn write_be(blob: Blob, range: Range<int>, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes within an inclusive `range` in the BLOB
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_be(blob: Blob, range: RangeInclusive<int>, value: float) -> ();
+
+/// Write an `INT` value to the bytes within an inclusive `range` in the BLOB
+/// in big-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8, 0x42);
+///
+/// b.write_be_int(1..=3, 0x99);
+///
+/// print(b); // prints "[4200000042424242]"
+/// ```
+fn write_be(blob: Blob, range: RangeInclusive<int>, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes beginning at the `start` position in the BLOB
+/// in big-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_be(blob: Blob, start: int, len: int, value: float) -> ();
+
+/// Write an `INT` value to the bytes beginning at the `start` position in the BLOB
+/// in big-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8, 0x42);
+///
+/// b.write_be_int(1, 3, 0x99);
+///
+/// print(b); // prints "[4200000042424242]"
+/// ```
+fn write_be(blob: Blob, start: int, len: int, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes within an exclusive `range` in the BLOB
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_le(blob: Blob, range: Range<int>, value: float) -> ();
+
+/// Write an `INT` value to the bytes within an exclusive `range` in the BLOB
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_le_int(1..3, 0x12345678);
+///
+/// print(b); // prints "[0078560000000000]"
+/// ```
+fn write_le(blob: Blob, range: Range<int>, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes within an inclusive `range` in the BLOB
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_le(blob: Blob, range: RangeInclusive<int>, value: float) -> ();
+
+/// Write an `INT` value to the bytes within an inclusive `range` in the BLOB
+/// in little-endian byte order.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_le_int(1..=3, 0x12345678);
+///
+/// print(b); // prints "[0078563400000000]"
+/// ```
+fn write_le(blob: Blob, range: RangeInclusive<int>, value: int) -> ();
+
+/// Write a `FLOAT` value to the bytes beginning at the `start` position in the BLOB
+/// in little-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+/// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+fn write_le(blob: Blob, start: int, len: int, value: float) -> ();
+
+/// Write an `INT` value to the bytes beginning at the `start` position in the BLOB
+/// in little-endian byte order.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, zero is returned.
+/// * If `len` ≤ 0, zero is returned.
+/// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+///
+/// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+/// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_le_int(1, 3, 0x12345678);
+///
+/// print(b); // prints "[0078563400000000]"
+/// ```
+fn write_le(blob: Blob, start: int, len: int, value: int) -> ();
+
+/// Write a string to the bytes within an exclusive `range` in the BLOB in UTF-8 encoding.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_utf8(1..5, "朝には紅顔ありて夕べには白骨となる");
+///
+/// print(b); // prints "[00e69c9de3000000]"
+/// ```
+fn write_utf8(blob: Blob, range: Range<int>, string: String) -> ();
+
+/// Write a string to the bytes within an inclusive `range` in the BLOB in UTF-8 encoding.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_utf8(1..=5, "朝には紅顔ありて夕べには白骨となる");
+///
+/// print(b); // prints "[00e69c9de3810000]"
+/// ```
+fn write_utf8(blob: Blob, range: RangeInclusive<int>, string: String) -> ();
+
+/// Write a string to the bytes within an inclusive `range` in the BLOB in UTF-8 encoding.
+///
+/// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+/// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+/// * If `start` ≥ length of BLOB, the BLOB is not modified.
+/// * If `len` ≤ 0, the BLOB is not modified.
+/// * If `start` position + `len` ≥ length of BLOB, only the portion of the BLOB after the `start` position is modified.
+///
+/// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+/// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+///
+/// ```rhai
+/// let b = blob(8);
+///
+/// b.write_utf8(1, 5, "朝には紅顔ありて夕べには白骨となる");
+///
+/// print(b); // prints "[00e69c9de3810000]"
+/// ```
+fn write_utf8(blob: Blob, start: int, len: int, string: String) -> ();
+
+op |(i128, i128) -> i128;
+
+op |(i16, i16) -> i16;
+
+op |(i32, i32) -> i32;
+
+op |(i8, i8) -> i8;
+
+op |(u128, u128) -> u128;
+
+op |(u16, u16) -> u16;
+
+op |(u32, u32) -> u32;
+
+op |(u64, u64) -> u64;
+
+op |(u8, u8) -> u8;
diff --git a/rhai/scripts/array.rhai b/rhai/scripts/array.rhai
new file mode 100644
index 0000000..1c449fd
--- /dev/null
+++ b/rhai/scripts/array.rhai
@@ -0,0 +1,8 @@
+let x = [1, 2, 3];
+
+print("x[1] should be 2:");
+print(x[1]);
+
+x[1] = 5;
+
+print(`x[1] should be 5: ${x[1]}`);
diff --git a/rhai/scripts/assignment.rhai b/rhai/scripts/assignment.rhai
new file mode 100644
index 0000000..68802a7
--- /dev/null
+++ b/rhai/scripts/assignment.rhai
@@ -0,0 +1,5 @@
+//! This script contains a single assignment statement.
+
+let x = 78;
+
+print(`x should be 78: ${x}`);
diff --git a/rhai/scripts/comments.rhai b/rhai/scripts/comments.rhai
new file mode 100644
index 0000000..63b9f04
--- /dev/null
+++ b/rhai/scripts/comments.rhai
@@ -0,0 +1,11 @@
+// I am a single line comment!
+
+let /* I am a spy in a variable declaration! */ x = 5;
+
+/* I am a simple
+ multi-line
+ comment */
+
+/* look /* at /* that, /* multi-line */ comments */ can be */ nested */
+
+/* surrounded by */ let this_is_not_a_comment = true // comments
diff --git a/rhai/scripts/doc-comments.rhai b/rhai/scripts/doc-comments.rhai
new file mode 100644
index 0000000..f2a56ff
--- /dev/null
+++ b/rhai/scripts/doc-comments.rhai
@@ -0,0 +1,29 @@
+//! This script illustrates how to put doc-comments on functions.
+
+/// The function `foo`, which prints `hello, world!` and a magic number,
+/// accepts three parameters.
+///
+/// # Parameters
+///
+/// * `x` - `i64`
+/// * `y` - `string`
+/// * `z` - `bool`
+///
+/// # Notes
+///
+/// This is a doc-comment. It can be obtained with the `metadata` feature.
+///
+/// An example is the `rhai-doc` app.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = foo(42, "hello", true);
+///
+/// print(x); // prints 47
+/// ```
+fn foo(x, y, z) {
+ print(`hello, world! ${if z { x + y.len() } else { x } }`);
+}
+
+foo(39, "bar", true);
diff --git a/rhai/scripts/fibonacci.rhai b/rhai/scripts/fibonacci.rhai
new file mode 100644
index 0000000..9201571
--- /dev/null
+++ b/rhai/scripts/fibonacci.rhai
@@ -0,0 +1,32 @@
+//! This script calculates the n-th Fibonacci number using a really dumb algorithm
+//! to test the speed of the scripting engine.
+
+const TARGET = 28;
+const REPEAT = 5;
+const ANSWER = 317_811;
+
+fn fib(n) {
+ if n < 2 {
+ n
+ } else {
+ fib(n-1) + fib(n-2)
+ }
+}
+
+print(`Running Fibonacci(${TARGET}) x ${REPEAT} times...`);
+print("Ready... Go!");
+
+let result;
+let now = timestamp();
+
+for n in 0..REPEAT {
+ result = fib(TARGET);
+}
+
+print(`Finished. Run time = ${now.elapsed} seconds.`);
+
+print(`Fibonacci number #${TARGET} = ${result}`);
+
+if result != ANSWER {
+ print(`The answer is WRONG! Should be ${ANSWER}!`);
+}
diff --git a/rhai/scripts/for1.rhai b/rhai/scripts/for1.rhai
new file mode 100644
index 0000000..6d4062e
--- /dev/null
+++ b/rhai/scripts/for1.rhai
@@ -0,0 +1,20 @@
+//! This script runs for-loops.
+
+let arr = [1, true, 123.456, "hello", 3, 42];
+
+// Loop over array with counter
+for (a, i) in arr {
+ for (b, j) in ['x', 42, (), 123, 99, 0.5] {
+ if b > 100 { continue; }
+
+ print(`(${i}, ${j}) = (${a}, ${b})`);
+ }
+
+ if a == 3 { break; }
+}
+//print(a); // <- if you uncomment this line, the script will fail to compile
+ // because 'a' is not defined here
+
+for i in range(5, 0, -1) { // runs from 5 down to 1
+ print(i);
+}
diff --git a/rhai/scripts/for2.rhai b/rhai/scripts/for2.rhai
new file mode 100644
index 0000000..010a3a0
--- /dev/null
+++ b/rhai/scripts/for2.rhai
@@ -0,0 +1,28 @@
+//! This script runs for-loops
+
+const MAX = 1_000_000;
+
+print(`Iterating an array with ${MAX} items...`);
+
+print("Ready... Go!");
+
+let now = timestamp();
+
+let list = [];
+
+// Loop over range
+for i in 0..MAX {
+ list.push(i);
+}
+
+print(`Time = ${now.elapsed} seconds...`);
+
+let sum = 0;
+
+// Loop over array
+for i in list {
+ sum += i;
+}
+
+print(`Sum = ${sum}`);
+print(`Finished. Total run time = ${now.elapsed} seconds.`);
diff --git a/rhai/scripts/for3.rhai b/rhai/scripts/for3.rhai
new file mode 100644
index 0000000..eac4919
--- /dev/null
+++ b/rhai/scripts/for3.rhai
@@ -0,0 +1,30 @@
+//! This script runs for-loops with closures.
+
+const MAX = 100;
+const CHECK = ((MAX - 1) ** 2) * MAX;
+
+print("Ready... Go!");
+
+let now = timestamp();
+
+print(`Creating ${MAX} closures...`);
+
+let list = [];
+
+// Loop over range
+for i in 0..MAX {
+ list.push(|| i ** 2);
+}
+
+print(`Time = ${now.elapsed} seconds...`);
+print(`Summing ${MAX} closures...`);
+
+let sum = 0;
+
+// Loop over array
+for f in list {
+ sum += f.call();
+}
+
+print(`Sum = ${sum} (should be ${CHECK})`);
+print(`Finished. Total run time = ${now.elapsed} seconds.`);
diff --git a/rhai/scripts/function_decl1.rhai b/rhai/scripts/function_decl1.rhai
new file mode 100644
index 0000000..d358b4b
--- /dev/null
+++ b/rhai/scripts/function_decl1.rhai
@@ -0,0 +1,9 @@
+//! This script defines a function and calls it.
+
+fn call_me() {
+ return 3;
+}
+
+let result = call_me();
+
+print(`call_me() should be 3: ${result}`);
diff --git a/rhai/scripts/function_decl2.rhai b/rhai/scripts/function_decl2.rhai
new file mode 100644
index 0000000..7ce09e3
--- /dev/null
+++ b/rhai/scripts/function_decl2.rhai
@@ -0,0 +1,14 @@
+//! This script defines a function with two parameters and local variables.
+
+let a = 3;
+
+fn add(a, b) {
+ a = 42; // notice that 'a' is passed by value
+ a + b; // notice that the last value is returned even if terminated by a semicolon
+}
+
+let result = add(a, 4);
+
+print(`add(a, 4) should be 46: ${result}`);
+
+print(`a should still be 3: ${a}`); // prints 3: 'a' is never changed
diff --git a/rhai/scripts/function_decl3.rhai b/rhai/scripts/function_decl3.rhai
new file mode 100644
index 0000000..9ce0b50
--- /dev/null
+++ b/rhai/scripts/function_decl3.rhai
@@ -0,0 +1,12 @@
+//! This script defines a function with many parameters.
+
+const KEY = 38;
+
+fn f(a, b, c, d, e, f) {
+ let x = global::KEY; // <- access global module
+ a - b * c - d * e - f + x
+}
+
+let result = f(100, 5, 2, 9, 6, 32);
+
+print(`result should be 42: ${result}`);
diff --git a/rhai/scripts/function_decl4.rhai b/rhai/scripts/function_decl4.rhai
new file mode 100644
index 0000000..07ae2be
--- /dev/null
+++ b/rhai/scripts/function_decl4.rhai
@@ -0,0 +1,12 @@
+//! This script defines a function that acts as a method.
+
+// Use 'this' to refer to the object of a method call
+fn action(x, y) {
+ this = this.abs() + x * y; // 'this' can be modified
+}
+
+let obj = -40;
+
+obj.action(1, 2); // call 'action' as method
+
+print(`obj should now be 42: ${obj}`);
diff --git a/rhai/scripts/function_decl5.rhai b/rhai/scripts/function_decl5.rhai
new file mode 100644
index 0000000..65a4c11
--- /dev/null
+++ b/rhai/scripts/function_decl5.rhai
@@ -0,0 +1,35 @@
+//! This script defines multiple versions of the same function
+//! for use as method with different data types.
+
+// For strings
+fn string.calc(x) {
+ this.len + x
+}
+// For integers
+fn int.calc(x) {
+ this * x
+}
+// For booleans
+fn bool.calc(x) {
+ if this { x } else { 0}
+}
+// For arrays
+fn array.calc(x) {
+ this.len + x
+}
+// For object maps
+fn map.calc(x) {
+ this[x]
+}
+// Catch-all
+fn calc(x) {
+ `${this}: ${x}`
+}
+
+print("hello".calc(42)); // 47
+print(42.calc(42)); // 1764
+print(true.calc(42)); // 42
+print(false.calc(42)); // 0
+print([1,2,3].calc(42)); // 45
+print(#{"a": 1, "b": 2}.calc("b")); // 2
+print('x'.calc(42)); // x: 42
diff --git a/rhai/scripts/if1.rhai b/rhai/scripts/if1.rhai
new file mode 100644
index 0000000..92830b3
--- /dev/null
+++ b/rhai/scripts/if1.rhai
@@ -0,0 +1,16 @@
+//! This script runs if statements.
+
+let a = 42;
+let b = 123;
+let x = 999;
+
+if a > b {
+ print("Oops! a > b");
+} else if a < b {
+ print("a < b, x should be 0");
+
+ let x = 0; // <- this 'x' shadows the global 'x'
+ print(x); // should print 0
+} else {
+ print("Oops! a == b");
+}
diff --git a/rhai/scripts/if2.rhai b/rhai/scripts/if2.rhai
new file mode 100644
index 0000000..15e855b
--- /dev/null
+++ b/rhai/scripts/if2.rhai
@@ -0,0 +1,12 @@
+//! This script runs an if expression.
+
+let a = 42;
+let b = 123;
+
+let x = if a <= b { // <- if-expression
+ b - a
+} else {
+ a - b
+} * 10;
+
+print(`x should be 810: ${x}`);
diff --git a/rhai/scripts/loop.rhai b/rhai/scripts/loop.rhai
new file mode 100644
index 0000000..8f843c2
--- /dev/null
+++ b/rhai/scripts/loop.rhai
@@ -0,0 +1,14 @@
+//! This script runs an infinite loop, ending it with a break statement.
+
+let x = 10;
+
+// simulate do..while using loop
+loop {
+ print(x);
+
+ x -= 1;
+
+ if x <= 0 { break; }
+}
+
+export x as foo;
diff --git a/rhai/scripts/mat_mul.rhai b/rhai/scripts/mat_mul.rhai
new file mode 100644
index 0000000..9830a66
--- /dev/null
+++ b/rhai/scripts/mat_mul.rhai
@@ -0,0 +1,65 @@
+//! This script simulates multi-dimensional matrix calculations.
+
+const SIZE = 50;
+
+fn new_mat(x, y) {
+ let row = [];
+ row.pad(y, 0.0);
+
+ let matrix = [];
+ matrix.pad(x, row);
+
+ matrix
+}
+
+fn mat_gen() {
+ const n = global::SIZE;
+ const tmp = 1.0 / n / n;
+ let m = new_mat(n, n);
+
+ for i in 0..n {
+ for j in 0..n {
+ m[i][j] = tmp * (i - j) * (i + j);
+ }
+ }
+
+ m
+}
+
+fn mat_mul(a, b) {
+ let b2 = new_mat(a[0].len, b[0].len);
+
+ for i in 0..a[0].len {
+ for j in 0..b[0].len {
+ b2[j][i] = b[i][j];
+ }
+ }
+
+ let c = new_mat(a.len, b[0].len);
+
+ for i in 0..c.len {
+ for j in 0..c[i].len {
+ c[i][j] = 0.0;
+
+ for z in 0..a[i].len {
+ c[i][j] += a[i][z] * b2[j][z];
+ }
+ }
+ }
+
+ c
+}
+
+const now = timestamp();
+
+const a = mat_gen();
+const b = mat_gen();
+const c = mat_mul(a, b);
+
+/*
+for i in 0..SIZE) {
+ print(c[i]);
+}
+*/
+
+print(`Finished. Run time = ${now.elapsed} seconds.`);
diff --git a/rhai/scripts/module.rhai b/rhai/scripts/module.rhai
new file mode 100644
index 0000000..fc92ec1
--- /dev/null
+++ b/rhai/scripts/module.rhai
@@ -0,0 +1,5 @@
+//! This script imports an external script as a module.
+
+import "loop" as x;
+
+print(`Module test! foo = ${x::foo}`);
diff --git a/rhai/scripts/oop.rhai b/rhai/scripts/oop.rhai
new file mode 100644
index 0000000..2c2ecd3
--- /dev/null
+++ b/rhai/scripts/oop.rhai
@@ -0,0 +1,41 @@
+//! This script simulates object-oriented programming (OOP) techniques using closures.
+
+// External variable that will be captured.
+let last_value = ();
+
+// Define object
+let obj1 = #{
+ _data: 42, // data field
+ get_data: || this._data, // property getter
+ action: || print(`Data=${this._data}`), // method
+ update: |x| { // property setter
+ this._data = x;
+ last_value = this._data; // capture 'last_value'
+ this.action();
+ }
+};
+
+if obj1.get_data() > 0 { // property access
+ obj1.update(123); // call method
+} else {
+ print("we have a problem here");
+}
+
+// Define another object based on the first object
+let obj2 = #{
+ _data: 0, // data field - new value
+ update: |x| { // property setter - another function
+ this._data = x * 2;
+ last_value = this._data; // capture 'last_value'
+ this.action();
+ }
+};
+obj2.fill_with(obj1); // add all other fields from obj1
+
+if obj2.get_data() > 0 { // property access
+ print("we have another problem here");
+} else {
+ obj2.update(42); // call method
+}
+
+print(`Should be 84: ${last_value}`);
diff --git a/rhai/scripts/op1.rhai b/rhai/scripts/op1.rhai
new file mode 100644
index 0000000..daf13f4
--- /dev/null
+++ b/rhai/scripts/op1.rhai
@@ -0,0 +1,5 @@
+//! This script runs a single expression.
+
+print("The result should be 46:");
+
+print(34 + 12);
diff --git a/rhai/scripts/op2.rhai b/rhai/scripts/op2.rhai
new file mode 100644
index 0000000..94163a8
--- /dev/null
+++ b/rhai/scripts/op2.rhai
@@ -0,0 +1,7 @@
+//! This script runs a complex expression.
+
+print("The result should be 182:");
+
+let x = 12 + 34 * 5;
+
+print(x);
diff --git a/rhai/scripts/op3.rhai b/rhai/scripts/op3.rhai
new file mode 100644
index 0000000..dc1bd99
--- /dev/null
+++ b/rhai/scripts/op3.rhai
@@ -0,0 +1,7 @@
+//! This script runs a complex expression.
+
+print("The result should be 230:");
+
+let x = (12 + 34) * 5;
+
+print(x);
diff --git a/rhai/scripts/primes.rhai b/rhai/scripts/primes.rhai
new file mode 100644
index 0000000..068c534
--- /dev/null
+++ b/rhai/scripts/primes.rhai
@@ -0,0 +1,33 @@
+//! This script uses the Sieve of Eratosthenes to calculate prime numbers.
+
+let now = timestamp();
+
+const ANSWER = 78_498;
+const MAX_NUMBER_TO_CHECK = 1_000_000;
+
+let prime_mask = [];
+prime_mask.pad(MAX_NUMBER_TO_CHECK + 1, true);
+
+prime_mask[0] = false;
+prime_mask[1] = false;
+
+let total_primes_found = 0;
+
+for p in 2..=MAX_NUMBER_TO_CHECK {
+ if !prime_mask[p] { continue; }
+
+ //print(p);
+
+ total_primes_found += 1;
+
+ for i in range(2 * p, MAX_NUMBER_TO_CHECK + 1, p) {
+ prime_mask[i] = false;
+ }
+}
+
+print(`Total ${total_primes_found} primes <= ${MAX_NUMBER_TO_CHECK}`);
+print(`Run time = ${now.elapsed} seconds.`);
+
+if total_primes_found != ANSWER {
+ print(`The answer is WRONG! Should be ${ANSWER}!`);
+}
diff --git a/rhai/scripts/speed_test.rhai b/rhai/scripts/speed_test.rhai
new file mode 100644
index 0000000..a5207dc
--- /dev/null
+++ b/rhai/scripts/speed_test.rhai
@@ -0,0 +1,12 @@
+//! This script runs 1 million iterations to test the speed of the scripting engine.
+
+let now = timestamp();
+let x = 1_000_000;
+
+print("Ready... Go!");
+
+while x > 0 {
+ x -= 1;
+}
+
+print(`Finished. Run time = ${now.elapsed} seconds.`);
diff --git a/rhai/scripts/string.rhai b/rhai/scripts/string.rhai
new file mode 100644
index 0000000..66f5183
--- /dev/null
+++ b/rhai/scripts/string.rhai
@@ -0,0 +1,46 @@
+//! This script tests string operations.
+
+print("hello");
+print("this\nis \\ nice"); // escape sequences
+print("0x40 hex is \x40"); // hex escape sequence
+print("Unicode fun: \u2764"); // Unicode escape sequence
+print("more fun: \U0001F603"); // Unicode escape sequence
+print("foo" + " " + "bar"); // string building using strings
+print("foo" < "bar"); // string comparison
+print("foo" >= "bar"); // string comparison
+print("the answer is " + 42); // string building using non-string types
+
+let s = "\u2764 hello, world! \U0001F603"; // string variable
+print(`length=${s.len}`); // should be 17
+
+s[s.len-3] = '?'; // change the string
+print(`Question: ${s}`); // should print 'Question: hello, world?'
+
+// Line continuation:
+let s = "This is a long \
+ string constructed using \
+ line continuation";
+
+// String interpolation
+print(`One string: ${s}`);
+
+// Multi-line literal string:
+let s = `
+ \U0001F603 This is a multi-line
+ "string" with \t\x20\r\n
+made using multi-line literal
+ string syntax.
+`;
+
+print(s);
+
+// Interpolation
+let s = `This is interpolation ${
+ let x = `within ${let y = "yet another level \
+ of interpolation!"; y} interpolation`;
+ x
+} within literal string.`;
+
+print(s);
+
+print(">>> END <<<");
diff --git a/rhai/scripts/strings_map.rhai b/rhai/scripts/strings_map.rhai
new file mode 100644
index 0000000..88ae45d
--- /dev/null
+++ b/rhai/scripts/strings_map.rhai
@@ -0,0 +1,105 @@
+//! This script tests object maps and strings.
+
+print("Ready... Go!");
+
+let now = timestamp();
+
+let adverbs = [ "moderately", "really", "slightly", "very" ];
+
+let adjectives = [
+ "abandoned", "able", "absolute", "academic", "acceptable", "acclaimed",
+ "accomplished", "accurate", "aching", "acidic", "acrobatic", "active",
+ "actual", "adept", "admirable", "admired", "adolescent", "adorable", "adored",
+ "advanced", "adventurous", "affectionate", "afraid", "aged", "aggravating",
+ "aggressive", "agile", "agitated", "agonizing", "agreeable", "ajar",
+ "alarmed", "alarming", "alert", "alienated", "alive", "all", "altruistic",
+ "amazing", "ambitious", "ample", "amused", "amusing", "anchored", "ancient",
+ "angelic", "angry", "anguished", "animated", "annual", "another", "antique",
+ "anxious", "any", "apprehensive", "appropriate", "apt", "arctic", "arid",
+ "aromatic", "artistic", "ashamed", "assured", "astonishing", "athletic",
+ "attached", "attentive", "attractive", "austere", "authentic", "authorized",
+ "automatic", "avaricious", "average", "aware", "awesome", "awful", "awkward",
+ "babyish", "back", "bad", "baggy", "bare", "barren", "basic", "beautiful",
+ "belated", "beloved", "beneficial", "best", "better", "bewitched", "big",
+ "big-hearted", "biodegradable", "bite-sized", "bitter", "black",
+ "black-and-white", "bland", "blank", "blaring", "bleak", "blind", "blissful",
+ "blond", "blue", "blushing", "bogus", "boiling", "bold", "bony", "boring",
+ "bossy", "both", "bouncy", "bountiful", "bowed", "brave", "breakable",
+ "brief", "bright", "brilliant", "brisk", "broken", "bronze", "brown",
+ "bruised", "bubbly", "bulky", "bumpy", "buoyant", "burdensome", "burly",
+ "bustling", "busy", "buttery", "buzzing", "calculating", "calm", "candid",
+ "canine", "capital", "carefree", "careful", "careless", "caring", "cautious",
+ "cavernous", "celebrated", "charming", "cheap", "cheerful", "cheery", "chief",
+ "chilly", "chubby", "circular", "classic", "clean", "clear", "clear-cut",
+ "clever", "close", "closed", "cloudy", "clueless", "clumsy", "cluttered",
+ "coarse", "cold", "colorful", "colorless", "colossal", "comfortable",
+ "common", "compassionate", "competent", "complete", "complex", "complicated",
+ "composed", "concerned", "concrete", "confused", "conscious", "considerate",
+ "constant", "content", "conventional", "cooked", "cool", "cooperative",
+ "coordinated", "corny", "corrupt", "costly", "courageous", "courteous",
+ "crafty"
+];
+
+let animals = [
+ "aardvark", "african buffalo", "albatross", "alligator", "alpaca", "ant",
+ "anteater", "antelope", "ape", "armadillo", "baboon", "badger", "barracuda",
+ "bat", "bear", "beaver", "bee", "bison", "black panther", "blue jay", "boar",
+ "butterfly", "camel", "capybara", "carduelis", "caribou", "cassowary", "cat",
+ "caterpillar", "cattle", "chamois", "cheetah", "chicken", "chimpanzee",
+ "chinchilla", "chough", "clam", "cobra", "cockroach", "cod", "cormorant",
+ "coyote", "crab", "crane", "crocodile", "crow", "curlew", "deer", "dinosaur",
+ "dog", "dolphin", "domestic pig", "donkey", "dotterel", "dove", "dragonfly",
+ "duck", "dugong", "dunlin", "eagle", "echidna", "eel", "elephant seal",
+ "elephant", "elk", "emu", "falcon", "ferret", "finch", "fish", "flamingo",
+ "fly", "fox", "frog", "gaur", "gazelle", "gerbil", "giant panda", "giraffe",
+ "gnat", "goat", "goldfish", "goose", "gorilla", "goshawk", "grasshopper",
+ "grouse", "guanaco", "guinea fowl", "guinea pig", "gull", "hamster", "hare",
+ "hawk", "hedgehog", "heron", "herring", "hippopotamus", "hornet", "horse",
+ "human", "hummingbird", "hyena", "ibex", "ibis", "jackal", "jaguar", "jay",
+ "jellyfish", "kangaroo", "kingfisher", "koala", "komodo dragon", "kookabura",
+ "kouprey", "kudu", "lapwing", "lark", "lemur", "leopard", "lion", "llama",
+ "lobster", "locust", "loris", "louse", "lyrebird", "magpie", "mallard",
+ "manatee", "mandrill", "mantis", "marten", "meerkat", "mink", "mole",
+ "mongoose", "monkey", "moose", "mosquito", "mouse", "mule", "narwhal", "newt",
+ "nightingale", "octopus", "okapi", "opossum", "oryx", "ostrich", "otter",
+ "owl", "oyster", "parrot", "partridge", "peafowl", "pelican", "penguin",
+ "pheasant", "pigeon", "pinniped", "polar bear", "pony", "porcupine",
+ "porpoise", "prairie dog", "quail", "quelea", "quetzal", "rabbit", "raccoon",
+ "ram", "rat", "raven", "red deer", "red panda", "reindeer", "rhinoceros",
+ "rook", "salamander", "salmon", "sand dollar", "sandpiper", "sardine",
+ "scorpion", "sea lion", "sea urchin", "seahorse", "shark", "sheep", "shrew",
+ "skunk", "snail", "snake", "sparrow", "spider", "spoonbill", "squid",
+ "wallaby", "wildebeest"
+];
+
+let keys = [];
+
+for animal in animals {
+ for adjective in adjectives {
+ for adverb in adverbs {
+ keys.push(`${adverb} ${adjective} ${animal}`)
+ }
+ }
+}
+
+let map = #{};
+
+let i = 0;
+
+for key in keys {
+ map[key] = i;
+ i += 1;
+}
+
+let sum = 0;
+
+for key in keys {
+ sum += map[key];
+}
+
+for key in keys {
+ map.remove(key);
+}
+
+print(`Sum = ${sum}`);
+print(`Finished. Run time = ${now.elapsed} seconds.`);
diff --git a/rhai/scripts/switch.rhai b/rhai/scripts/switch.rhai
new file mode 100644
index 0000000..8e2bb6e
--- /dev/null
+++ b/rhai/scripts/switch.rhai
@@ -0,0 +1,22 @@
+//! This script runs a switch statement in a for-loop.
+
+let arr = [42, 123.456, "hello", true, "hey", 'x', 999, 1, 2, 3, 4];
+
+for item in arr {
+ switch item {
+ // Match single integer
+ 42 => print("The Answer!"),
+ // Match single floating-point number
+ 123.456 => print(`Floating point... ${item}`),
+ // Match single string
+ "hello" => print(`${item} world!`),
+ // Match another integer
+ 999 => print(`Got 999: ${item}`),
+ // Match range with condition
+ 0..100 if item % 2 == 0 => print(`A small even number: ${item}`),
+ // Match another range
+ 0..100 => print(`A small odd number: ${item}`),
+ // Default case
+ _ => print(`Something else: <${item}> is ${type_of(item)}`)
+ }
+}
diff --git a/rhai/scripts/while.rhai b/rhai/scripts/while.rhai
new file mode 100644
index 0000000..21c0681
--- /dev/null
+++ b/rhai/scripts/while.rhai
@@ -0,0 +1,8 @@
+//! This script runs a while loop.
+
+let x = 10;
+
+while x > 0 {
+ print(x);
+ x -= 1;
+}
diff --git a/rhai/src/README.md b/rhai/src/README.md
new file mode 100644
index 0000000..e02fd0b
--- /dev/null
+++ b/rhai/src/README.md
@@ -0,0 +1,33 @@
+Source Structure
+================
+
+Root Sources
+------------
+
+| Source file | Description |
+| -------------- | ------------------------------------------------------------------------------- |
+| `lib.rs` | Crate root |
+| `engine.rs` | The scripting engine, defines the `Engine` type |
+| `tokenizer.rs` | Script tokenizer/lexer |
+| `parser.rs` | Script parser |
+| `optimizer.rs` | Script optimizer |
+| `defer.rs` | Utilities for deferred clean-up of resources |
+| `reify.rs` | Utilities for making generic types concrete |
+| `tests.rs` | Unit tests (not integration tests, which are in the main `tests` sub-directory) |
+
+
+Sub-Directories
+---------------
+
+| Sub-directory | Description |
+| ------------- | ------------------------------------------------------------------ |
+| `config` | Configuration |
+| `types` | Common data types (e.g. `Dynamic`, errors) |
+| `api` | Public API for the scripting engine |
+| `ast` | AST definition |
+| `module` | Support for modules |
+| `packages` | Pre-defined packages |
+| `func` | Registering and calling functions (native Rust and script-defined) |
+| `eval` | AST evaluation |
+| `serde` | Support for [`serde`](https://crates.io/crates/serde) and metadata |
+| `bin` | Pre-built CLI binaries |
diff --git a/rhai/src/api/build_type.rs b/rhai/src/api/build_type.rs
new file mode 100644
index 0000000..1edce75
--- /dev/null
+++ b/rhai/src/api/build_type.rs
@@ -0,0 +1,271 @@
+//! Trait to build a custom type for use with [`Engine`].
+use crate::{types::dynamic::Variant, Engine, Identifier, RegisterNativeFunction};
+use std::marker::PhantomData;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+use crate::func::register::Mut;
+
+/// Trait to build the API of a custom type for use with an [`Engine`]
+/// (i.e. register the type and its getters, setters, methods, etc.).
+///
+/// # Example
+///
+/// ```
+/// use rhai::{CustomType, TypeBuilder, Engine};
+///
+/// #[derive(Debug, Clone, Eq, PartialEq)]
+/// struct TestStruct {
+/// field: i64
+/// }
+///
+/// impl TestStruct {
+/// fn new() -> Self {
+/// Self { field: 1 }
+/// }
+/// fn update(&mut self, offset: i64) {
+/// self.field += offset;
+/// }
+/// fn get_value(&mut self) -> i64 {
+/// self.field
+/// }
+/// fn set_value(&mut self, value: i64) {
+/// self.field = value;
+/// }
+/// }
+///
+/// impl CustomType for TestStruct {
+/// fn build(mut builder: TypeBuilder<Self>) {
+/// builder
+/// .with_name("TestStruct")
+/// .with_fn("new_ts", Self::new)
+/// .with_fn("update", Self::update);
+/// }
+/// }
+///
+/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+///
+/// let mut engine = Engine::new();
+///
+/// // Register API for the custom type.
+/// engine.build_type::<TestStruct>();
+///
+///
+/// # #[cfg(not(feature = "no_object"))]
+/// assert_eq!(
+/// engine.eval::<TestStruct>("let x = new_ts(); x.update(41); x")?,
+/// TestStruct { field: 42 }
+/// );
+/// # Ok(())
+/// # }
+/// ```
+pub trait CustomType: Variant + Clone {
+ /// Builds the custom type for use with the [`Engine`].
+ ///
+ /// Methods, property getters/setters, indexers etc. should be registered in this function.
+ fn build(builder: TypeBuilder<Self>);
+}
+
+impl Engine {
+ /// Build the API of a custom type for use with the [`Engine`].
+ ///
+ /// The custom type must implement [`CustomType`].
+ #[inline]
+ pub fn build_type<T: CustomType>(&mut self) -> &mut Self {
+ T::build(TypeBuilder::new(self));
+ self
+ }
+}
+
+/// Builder to build the API of a custom type for use with an [`Engine`].
+///
+/// The type is automatically registered when this builder is dropped.
+///
+/// ## Pretty-Print Name
+///
+/// By default the type is registered with [`Engine::register_type`] (i.e. without a pretty-print name).
+///
+/// To define a pretty-print name, call [`with_name`][`TypeBuilder::with_name`],
+/// to use [`Engine::register_type_with_name`] instead.
+#[must_use]
+pub struct TypeBuilder<'a, T: Variant + Clone> {
+ engine: &'a mut Engine,
+ name: Option<&'static str>,
+ _marker: PhantomData<T>,
+}
+
+impl<'a, T: Variant + Clone> TypeBuilder<'a, T> {
+ /// Create a [`TypeBuilder`] linked to a particular [`Engine`] instance.
+ #[inline(always)]
+ fn new(engine: &'a mut Engine) -> Self {
+ Self {
+ engine,
+ name: None,
+ _marker: PhantomData,
+ }
+ }
+}
+
+impl<'a, T: Variant + Clone> TypeBuilder<'a, T> {
+ /// Set a pretty-print name for the `type_of` function.
+ #[inline(always)]
+ pub fn with_name(&mut self, name: &'static str) -> &mut Self {
+ self.name = Some(name);
+ self
+ }
+
+ /// Register a custom function.
+ #[inline(always)]
+ pub fn with_fn<A: 'static, const N: usize, const C: bool, R: Variant + Clone, const L: bool>(
+ &mut self,
+ name: impl AsRef<str> + Into<Identifier>,
+ method: impl RegisterNativeFunction<A, N, C, R, L>,
+ ) -> &mut Self {
+ self.engine.register_fn(name, method);
+ self
+ }
+}
+
+impl<'a, T> TypeBuilder<'a, T>
+where
+ T: Variant + Clone + IntoIterator,
+ <T as IntoIterator>::Item: Variant + Clone,
+{
+ /// Register a type iterator.
+ /// This is an advanced API.
+ #[inline(always)]
+ pub fn is_iterable(&mut self) -> &mut Self {
+ self.engine.register_iterator::<T>();
+ self
+ }
+}
+
+#[cfg(not(feature = "no_object"))]
+impl<'a, T: Variant + Clone> TypeBuilder<'a, T> {
+ /// Register a getter function.
+ ///
+ /// The function signature must start with `&mut self` and not `&self`.
+ ///
+ /// Not available under `no_object`.
+ #[inline(always)]
+ pub fn with_get<const C: bool, V: Variant + Clone, const L: bool>(
+ &mut self,
+ name: impl AsRef<str>,
+ get_fn: impl RegisterNativeFunction<(Mut<T>,), 1, C, V, L> + crate::func::SendSync + 'static,
+ ) -> &mut Self {
+ self.engine.register_get(name, get_fn);
+ self
+ }
+
+ /// Register a setter function.
+ ///
+ /// Not available under `no_object`.
+ #[inline(always)]
+ pub fn with_set<const C: bool, V: Variant + Clone, const L: bool>(
+ &mut self,
+ name: impl AsRef<str>,
+ set_fn: impl RegisterNativeFunction<(Mut<T>, V), 2, C, (), L> + crate::func::SendSync + 'static,
+ ) -> &mut Self {
+ self.engine.register_set(name, set_fn);
+ self
+ }
+
+ /// Short-hand for registering both getter and setter functions.
+ ///
+ /// All function signatures must start with `&mut self` and not `&self`.
+ ///
+ /// Not available under `no_object`.
+ #[inline(always)]
+ pub fn with_get_set<
+ const C1: bool,
+ const C2: bool,
+ V: Variant + Clone,
+ const L1: bool,
+ const L2: bool,
+ >(
+ &mut self,
+ name: impl AsRef<str>,
+ get_fn: impl RegisterNativeFunction<(Mut<T>,), 1, C1, V, L1> + crate::func::SendSync + 'static,
+ set_fn: impl RegisterNativeFunction<(Mut<T>, V), 2, C2, (), L2>
+ + crate::func::SendSync
+ + 'static,
+ ) -> &mut Self {
+ self.engine.register_get_set(name, get_fn, set_fn);
+ self
+ }
+}
+
+#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+impl<'a, T: Variant + Clone> TypeBuilder<'a, T> {
+ /// Register an index getter.
+ ///
+ /// The function signature must start with `&mut self` and not `&self`.
+ ///
+ /// Not available under both `no_index` and `no_object`.
+ #[inline(always)]
+ pub fn with_indexer_get<
+ X: Variant + Clone,
+ const C: bool,
+ V: Variant + Clone,
+ const L: bool,
+ >(
+ &mut self,
+ get_fn: impl RegisterNativeFunction<(Mut<T>, X), 2, C, V, L> + crate::func::SendSync + 'static,
+ ) -> &mut Self {
+ self.engine.register_indexer_get(get_fn);
+ self
+ }
+
+ /// Register an index setter.
+ ///
+ /// Not available under both `no_index` and `no_object`.
+ #[inline(always)]
+ pub fn with_indexer_set<
+ X: Variant + Clone,
+ const C: bool,
+ V: Variant + Clone,
+ const L: bool,
+ >(
+ &mut self,
+ set_fn: impl RegisterNativeFunction<(Mut<T>, X, V), 3, C, (), L>
+ + crate::func::SendSync
+ + 'static,
+ ) -> &mut Self {
+ self.engine.register_indexer_set(set_fn);
+ self
+ }
+
+ /// Short-hand for registering both index getter and setter functions.
+ ///
+ /// Not available under both `no_index` and `no_object`.
+ #[inline(always)]
+ pub fn with_indexer_get_set<
+ X: Variant + Clone,
+ const C1: bool,
+ const C2: bool,
+ V: Variant + Clone,
+ const L1: bool,
+ const L2: bool,
+ >(
+ &mut self,
+ get_fn: impl RegisterNativeFunction<(Mut<T>, X), 2, C1, V, L1> + crate::func::SendSync + 'static,
+ set_fn: impl RegisterNativeFunction<(Mut<T>, X, V), 3, C2, (), L2>
+ + crate::func::SendSync
+ + 'static,
+ ) -> &mut Self {
+ self.engine.register_indexer_get_set(get_fn, set_fn);
+ self
+ }
+}
+
+impl<'a, T: Variant + Clone> Drop for TypeBuilder<'a, T> {
+ #[inline]
+ fn drop(&mut self) {
+ if let Some(name) = self.name {
+ self.engine.register_type_with_name::<T>(name);
+ } else {
+ self.engine.register_type::<T>();
+ }
+ }
+}
diff --git a/rhai/src/api/call_fn.rs b/rhai/src/api/call_fn.rs
new file mode 100644
index 0000000..09e3e24
--- /dev/null
+++ b/rhai/src/api/call_fn.rs
@@ -0,0 +1,286 @@
+//! Module that defines the `call_fn` API of [`Engine`].
+#![cfg(not(feature = "no_function"))]
+
+use crate::eval::{Caches, GlobalRuntimeState};
+use crate::types::dynamic::Variant;
+use crate::{
+ Dynamic, Engine, FuncArgs, Position, RhaiResult, RhaiResultOf, Scope, StaticVec, AST, ERR,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{any::type_name, mem};
+
+/// Options for calling a script-defined function via [`Engine::call_fn_with_options`].
+#[derive(Debug, Hash)]
+#[non_exhaustive]
+#[must_use]
+pub struct CallFnOptions<'t> {
+ /// A value for binding to the `this` pointer (if any). Default [`None`].
+ pub this_ptr: Option<&'t mut Dynamic>,
+ /// The custom state of this evaluation run (if any), overrides [`Engine::default_tag`]. Default [`None`].
+ pub tag: Option<Dynamic>,
+ /// Evaluate the [`AST`] to load necessary modules before calling the function? Default `true`.
+ pub eval_ast: bool,
+ /// Rewind the [`Scope`] after the function call? Default `true`.
+ pub rewind_scope: bool,
+}
+
+impl Default for CallFnOptions<'_> {
+ #[inline(always)]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<'a> CallFnOptions<'a> {
+ /// Create a default [`CallFnOptions`].
+ #[inline(always)]
+ pub fn new() -> Self {
+ Self {
+ this_ptr: None,
+ tag: None,
+ eval_ast: true,
+ rewind_scope: true,
+ }
+ }
+ /// Bind to the `this` pointer.
+ #[inline(always)]
+ pub fn bind_this_ptr(mut self, value: &'a mut Dynamic) -> Self {
+ self.this_ptr = Some(value);
+ self
+ }
+ /// Set the custom state of this evaluation run (if any).
+ #[inline(always)]
+ pub fn with_tag(mut self, value: impl Variant + Clone) -> Self {
+ self.tag = Some(Dynamic::from(value));
+ self
+ }
+ /// Set whether to evaluate the [`AST`] to load necessary modules before calling the function.
+ #[inline(always)]
+ pub const fn eval_ast(mut self, value: bool) -> Self {
+ self.eval_ast = value;
+ self
+ }
+ /// Set whether to rewind the [`Scope`] after the function call.
+ #[inline(always)]
+ pub const fn rewind_scope(mut self, value: bool) -> Self {
+ self.rewind_scope = value;
+ self
+ }
+}
+
+impl Engine {
+ /// Call a script function defined in an [`AST`] with multiple arguments.
+ ///
+ /// Not available under `no_function`.
+ ///
+ /// The [`AST`] is evaluated before calling the function.
+ /// This allows a script to load the necessary modules.
+ /// This is usually desired. If not, use [`call_fn_with_options`][Engine::call_fn_with_options] instead.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Scope};
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// let ast = engine.compile("
+ /// fn add(x, y) { len(x) + y + foo }
+ /// fn add1(x) { len(x) + 1 + foo }
+ /// fn bar() { foo/2 }
+ /// ")?;
+ ///
+ /// let mut scope = Scope::new();
+ /// scope.push("foo", 42_i64);
+ ///
+ /// // Call the script-defined function
+ /// let result = engine.call_fn::<i64>(&mut scope, &ast, "add", ( "abc", 123_i64 ) )?;
+ /// assert_eq!(result, 168);
+ ///
+ /// let result = engine.call_fn::<i64>(&mut scope, &ast, "add1", ( "abc", ) )?;
+ /// // ^^^^^^^^^^ tuple of one
+ /// assert_eq!(result, 46);
+ ///
+ /// let result = engine.call_fn::<i64>(&mut scope, &ast, "bar", () )?;
+ /// assert_eq!(result, 21);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn call_fn<T: Variant + Clone>(
+ &self,
+ scope: &mut Scope,
+ ast: &AST,
+ name: impl AsRef<str>,
+ args: impl FuncArgs,
+ ) -> RhaiResultOf<T> {
+ self.call_fn_with_options(CallFnOptions::default(), scope, ast, name, args)
+ }
+ /// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments.
+ ///
+ /// Options are provided via the [`CallFnOptions`] type.
+ /// This is an advanced API.
+ ///
+ /// Not available under `no_function`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Scope, Dynamic, CallFnOptions};
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// let ast = engine.compile("
+ /// fn action(x) { this += x; } // function using 'this' pointer
+ /// fn decl(x) { let hello = x; } // declaring variables
+ /// ")?;
+ ///
+ /// let mut scope = Scope::new();
+ /// scope.push("foo", 42_i64);
+ ///
+ /// // Binding the 'this' pointer
+ /// let mut value = 1_i64.into();
+ /// let options = CallFnOptions::new().bind_this_ptr(&mut value);
+ ///
+ /// engine.call_fn_with_options(options, &mut scope, &ast, "action", ( 41_i64, ))?;
+ /// assert_eq!(value.as_int().unwrap(), 42);
+ ///
+ /// // Do not rewind scope
+ /// let options = CallFnOptions::default().rewind_scope(false);
+ ///
+ /// engine.call_fn_with_options(options, &mut scope, &ast, "decl", ( 42_i64, ))?;
+ /// assert_eq!(scope.get_value::<i64>("hello").unwrap(), 42);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn call_fn_with_options<T: Variant + Clone>(
+ &self,
+ options: CallFnOptions,
+ scope: &mut Scope,
+ ast: &AST,
+ name: impl AsRef<str>,
+ args: impl FuncArgs,
+ ) -> RhaiResultOf<T> {
+ let mut arg_values = StaticVec::new_const();
+ args.parse(&mut arg_values);
+
+ self._call_fn(
+ scope,
+ &mut GlobalRuntimeState::new(self),
+ &mut Caches::new(),
+ ast,
+ name.as_ref(),
+ arg_values.as_mut(),
+ options,
+ )
+ .and_then(|result| {
+ result.try_cast_raw().map_err(|r| {
+ let result_type = self.map_type_name(r.type_name());
+ let cast_type = match type_name::<T>() {
+ typ @ _ if typ.contains("::") => self.map_type_name(typ),
+ typ @ _ => typ,
+ };
+ ERR::ErrorMismatchOutputType(cast_type.into(), result_type.into(), Position::NONE)
+ .into()
+ })
+ })
+ }
+ /// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments.
+ ///
+ /// # Arguments
+ ///
+ /// All the arguments are _consumed_, meaning that they're replaced by `()`. This is to avoid
+ /// unnecessarily cloning the arguments.
+ ///
+ /// Do not use the arguments after this call. If they are needed afterwards, clone them _before_
+ /// calling this function.
+ #[inline(always)]
+ pub(crate) fn _call_fn(
+ &self,
+ scope: &mut Scope,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ ast: &AST,
+ name: &str,
+ arg_values: &mut [Dynamic],
+ options: CallFnOptions,
+ ) -> RhaiResult {
+ let statements = ast.statements();
+
+ let orig_lib_len = global.lib.len();
+
+ let orig_tag = options.tag.map(|v| mem::replace(&mut global.tag, v));
+ let mut this_ptr = options.this_ptr;
+
+ global.lib.push(ast.shared_lib().clone());
+
+ #[cfg(not(feature = "no_module"))]
+ let orig_embedded_module_resolver = std::mem::replace(
+ &mut global.embedded_module_resolver,
+ ast.resolver().cloned(),
+ );
+
+ let rewind_scope = options.rewind_scope;
+
+ let result = if options.eval_ast && !statements.is_empty() {
+ defer! {
+ scope if rewind_scope => rewind;
+ let orig_scope_len = scope.len();
+ }
+
+ self.eval_global_statements(global, caches, scope, statements)
+ } else {
+ Ok(Dynamic::UNIT)
+ }
+ .and_then(|_| {
+ let args = &mut arg_values.iter_mut().collect::<StaticVec<_>>();
+
+ // Check for data race.
+ #[cfg(not(feature = "no_closure"))]
+ crate::func::ensure_no_data_race(name, args, false)?;
+
+ ast.shared_lib()
+ .get_script_fn(name, args.len())
+ .map_or_else(
+ || Err(ERR::ErrorFunctionNotFound(name.into(), Position::NONE).into()),
+ |fn_def| {
+ self.call_script_fn(
+ global,
+ caches,
+ scope,
+ this_ptr.as_deref_mut(),
+ None,
+ fn_def,
+ args,
+ rewind_scope,
+ Position::NONE,
+ )
+ },
+ )
+ });
+
+ #[cfg(feature = "debugging")]
+ if self.is_debugger_registered() {
+ global.debugger_mut().status = crate::eval::DebuggerStatus::Terminate;
+ let node = &crate::ast::Stmt::Noop(Position::NONE);
+ self.run_debugger(global, caches, scope, this_ptr, node)?;
+ }
+
+ #[cfg(not(feature = "no_module"))]
+ {
+ global.embedded_module_resolver = orig_embedded_module_resolver;
+ }
+
+ if let Some(value) = orig_tag {
+ global.tag = value;
+ }
+
+ global.lib.truncate(orig_lib_len);
+
+ result
+ }
+}
diff --git a/rhai/src/api/compile.rs b/rhai/src/api/compile.rs
new file mode 100644
index 0000000..6d506b4
--- /dev/null
+++ b/rhai/src/api/compile.rs
@@ -0,0 +1,322 @@
+//! Module that defines the public compilation API of [`Engine`].
+
+use crate::func::native::locked_write;
+use crate::parser::{ParseResult, ParseState};
+use crate::types::StringsInterner;
+use crate::{Engine, OptimizationLevel, Scope, AST};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+impl Engine {
+ /// Compile a string into an [`AST`], which can be used later for evaluation.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Compile a script to an AST and store it for later evaluation
+ /// let ast = engine.compile("40 + 2")?;
+ ///
+ /// for _ in 0..42 {
+ /// assert_eq!(engine.eval_ast::<i64>(&ast)?, 42);
+ /// }
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn compile(&self, script: impl AsRef<str>) -> ParseResult<AST> {
+ self.compile_with_scope(&Scope::new(), script)
+ }
+ /// Compile a string into an [`AST`] using own scope, which can be used later for evaluation.
+ ///
+ /// ## Constants Propagation
+ ///
+ /// If not [`OptimizationLevel::None`][crate::OptimizationLevel::None], constants defined within
+ /// the scope are propagated throughout the script _including_ functions. This allows functions
+ /// to be optimized based on dynamic global constants.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// # #[cfg(not(feature = "no_optimize"))]
+ /// # {
+ /// use rhai::{Engine, Scope, OptimizationLevel};
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Create initialized scope
+ /// let mut scope = Scope::new();
+ /// scope.push_constant("x", 42_i64); // 'x' is a constant
+ ///
+ /// // Compile a script to an AST and store it for later evaluation.
+ /// // Notice that `Full` optimization is on, so constants are folded
+ /// // into function calls and operators.
+ /// let ast = engine.compile_with_scope(&mut scope,
+ /// "if x > 40 { x } else { 0 }" // all 'x' are replaced with 42
+ /// )?;
+ ///
+ /// // Normally this would have failed because no scope is passed into the 'eval_ast'
+ /// // call and so the variable 'x' does not exist. Here, it passes because the script
+ /// // has been optimized and all references to 'x' are already gone.
+ /// assert_eq!(engine.eval_ast::<i64>(&ast)?, 42);
+ /// # }
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn compile_with_scope(&self, scope: &Scope, script: impl AsRef<str>) -> ParseResult<AST> {
+ self.compile_scripts_with_scope(scope, &[script])
+ }
+ /// Compile a string into an [`AST`] using own scope, which can be used later for evaluation,
+ /// embedding all imported modules.
+ ///
+ /// Not available under `no_module`.
+ ///
+ /// Modules referred by `import` statements containing literal string paths are eagerly resolved
+ /// via the current [module resolver][crate::ModuleResolver] and embedded into the resultant
+ /// [`AST`]. When it is evaluated later, `import` statement directly recall pre-resolved
+ /// [modules][crate::Module] and the resolution process is not performed again.
+ #[cfg(not(feature = "no_module"))]
+ pub fn compile_into_self_contained(
+ &self,
+ scope: &Scope,
+ script: impl AsRef<str>,
+ ) -> crate::RhaiResultOf<AST> {
+ use crate::{
+ ast::{ASTNode, Expr, Stmt},
+ func::native::shared_take_or_clone,
+ module::resolvers::StaticModuleResolver,
+ };
+ use std::collections::BTreeSet;
+
+ fn collect_imports(
+ ast: &AST,
+ resolver: &StaticModuleResolver,
+ imports: &mut BTreeSet<crate::Identifier>,
+ ) {
+ ast.walk(&mut |path| match path.last().unwrap() {
+ // Collect all `import` statements with a string constant path
+ ASTNode::Stmt(Stmt::Import(x, ..)) => match x.0 {
+ Expr::StringConstant(ref s, ..)
+ if !resolver.contains_path(s)
+ && (imports.is_empty() || !imports.contains(s.as_str())) =>
+ {
+ imports.insert(s.clone().into());
+ true
+ }
+ _ => true,
+ },
+ _ => true,
+ });
+ }
+
+ let mut ast = self.compile_with_scope(scope, script)?;
+
+ let mut resolver = StaticModuleResolver::new();
+ let mut imports = BTreeSet::new();
+
+ collect_imports(&ast, &resolver, &mut imports);
+
+ if !imports.is_empty() {
+ while let Some(path) = imports.iter().next() {
+ let path = path.clone();
+
+ match self
+ .module_resolver()
+ .resolve_ast(self, None, &path, crate::Position::NONE)
+ {
+ Some(Ok(module_ast)) => collect_imports(&module_ast, &resolver, &mut imports),
+ Some(err) => return err,
+ None => (),
+ }
+
+ let module =
+ self.module_resolver()
+ .resolve(self, None, &path, crate::Position::NONE)?;
+
+ let module = shared_take_or_clone(module);
+
+ imports.remove(&path);
+ resolver.insert(path, module);
+ }
+ ast.set_resolver(resolver);
+ }
+
+ Ok(ast)
+ }
+ /// When passed a list of strings, first join the strings into one large script, and then
+ /// compile them into an [`AST`] using own scope, which can be used later for evaluation.
+ ///
+ /// The scope is useful for passing constants into the script for optimization when using
+ /// [`OptimizationLevel::Full`][crate::OptimizationLevel::Full].
+ ///
+ /// ## Note
+ ///
+ /// All strings are simply parsed one after another with nothing inserted in between, not even a
+ /// newline or space.
+ ///
+ /// ## Constants Propagation
+ ///
+ /// If not [`OptimizationLevel::None`][crate::OptimizationLevel::None], constants defined within
+ /// the scope are propagated throughout the script _including_ functions. This allows functions
+ /// to be optimized based on dynamic global constants.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// # #[cfg(not(feature = "no_optimize"))]
+ /// # {
+ /// use rhai::{Engine, Scope, OptimizationLevel};
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Create initialized scope
+ /// let mut scope = Scope::new();
+ /// scope.push_constant("x", 42_i64); // 'x' is a constant
+ ///
+ /// // Compile a script made up of script segments to an AST and store it for later evaluation.
+ /// // Notice that `Full` optimization is on, so constants are folded
+ /// // into function calls and operators.
+ /// let ast = engine.compile_scripts_with_scope(&mut scope, &[
+ /// "if x > 40", // all 'x' are replaced with 42
+ /// "{ x } el",
+ /// "se { 0 }" // segments do not need to be valid scripts!
+ /// ])?;
+ ///
+ /// // Normally this would have failed because no scope is passed into the 'eval_ast'
+ /// // call and so the variable 'x' does not exist. Here, it passes because the script
+ /// // has been optimized and all references to 'x' are already gone.
+ /// assert_eq!(engine.eval_ast::<i64>(&ast)?, 42);
+ /// # }
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn compile_scripts_with_scope<S: AsRef<str>>(
+ &self,
+ scope: &Scope,
+ scripts: impl AsRef<[S]>,
+ ) -> ParseResult<AST> {
+ self.compile_with_scope_and_optimization_level(
+ Some(scope),
+ scripts,
+ self.optimization_level,
+ )
+ }
+ /// Join a list of strings and compile into an [`AST`] using own scope at a specific optimization level.
+ ///
+ /// ## Constants Propagation
+ ///
+ /// If not [`OptimizationLevel::None`], constants defined within the scope are propagated
+ /// throughout the script _including_ functions. This allows functions to be optimized based on
+ /// dynamic global constants.
+ #[inline]
+ pub(crate) fn compile_with_scope_and_optimization_level<S: AsRef<str>>(
+ &self,
+ scope: Option<&Scope>,
+ scripts: impl AsRef<[S]>,
+ optimization_level: OptimizationLevel,
+ ) -> ParseResult<AST> {
+ let (stream, tc) = self.lex_raw(scripts.as_ref(), self.token_mapper.as_deref());
+
+ let mut interner;
+ let mut guard;
+ let interned_strings = if let Some(ref interner) = self.interned_strings {
+ guard = locked_write(interner);
+ &mut *guard
+ } else {
+ interner = StringsInterner::new();
+ &mut interner
+ };
+
+ let state = &mut ParseState::new(scope, interned_strings, tc);
+ let mut _ast = self.parse(stream.peekable(), state, optimization_level)?;
+ #[cfg(feature = "metadata")]
+ _ast.set_doc(&state.tokenizer_control.borrow().global_comments);
+ Ok(_ast)
+ }
+ /// Compile a string containing an expression into an [`AST`],
+ /// which can be used later for evaluation.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Compile a script to an AST and store it for later evaluation
+ /// let ast = engine.compile_expression("40 + 2")?;
+ ///
+ /// for _ in 0..42 {
+ /// assert_eq!(engine.eval_ast::<i64>(&ast)?, 42);
+ /// }
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn compile_expression(&self, script: impl AsRef<str>) -> ParseResult<AST> {
+ self.compile_expression_with_scope(&Scope::new(), script)
+ }
+ /// Compile a string containing an expression into an [`AST`] using own scope,
+ /// which can be used later for evaluation.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// # #[cfg(not(feature = "no_optimize"))]
+ /// # {
+ /// use rhai::{Engine, Scope, OptimizationLevel};
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Create initialized scope
+ /// let mut scope = Scope::new();
+ /// scope.push_constant("x", 10_i64); // 'x' is a constant
+ ///
+ /// // Compile a script to an AST and store it for later evaluation.
+ /// // Notice that `Full` optimization is on, so constants are folded
+ /// // into function calls and operators.
+ /// let ast = engine.compile_expression_with_scope(&mut scope,
+ /// "2 + (x + x) * 2" // all 'x' are replaced with 10
+ /// )?;
+ ///
+ /// // Normally this would have failed because no scope is passed into the 'eval_ast'
+ /// // call and so the variable 'x' does not exist. Here, it passes because the script
+ /// // has been optimized and all references to 'x' are already gone.
+ /// assert_eq!(engine.eval_ast::<i64>(&ast)?, 42);
+ /// # }
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn compile_expression_with_scope(
+ &self,
+ scope: &Scope,
+ script: impl AsRef<str>,
+ ) -> ParseResult<AST> {
+ let scripts = [script];
+ let (stream, t) = self.lex_raw(&scripts, self.token_mapper.as_deref());
+
+ let mut interner;
+ let mut guard;
+ let interned_strings = if let Some(ref interner) = self.interned_strings {
+ guard = locked_write(interner);
+ &mut *guard
+ } else {
+ interner = StringsInterner::new();
+ &mut interner
+ };
+
+ let state = &mut ParseState::new(Some(scope), interned_strings, t);
+ self.parse_global_expr(stream.peekable(), state, |_| {}, self.optimization_level)
+ }
+}
diff --git a/rhai/src/api/custom_syntax.rs b/rhai/src/api/custom_syntax.rs
new file mode 100644
index 0000000..4cdb308
--- /dev/null
+++ b/rhai/src/api/custom_syntax.rs
@@ -0,0 +1,395 @@
+//! Module implementing custom syntax for [`Engine`].
+#![cfg(not(feature = "no_custom_syntax"))]
+
+use crate::ast::Expr;
+use crate::func::SendSync;
+use crate::parser::ParseResult;
+use crate::tokenizer::{is_reserved_keyword_or_symbol, is_valid_identifier, Token};
+use crate::types::dynamic::Variant;
+use crate::{
+ Dynamic, Engine, EvalContext, Identifier, ImmutableString, LexError, Position, RhaiResult,
+ StaticVec,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{borrow::Borrow, ops::Deref};
+
+/// Collection of special markers for custom syntax definition.
+pub mod markers {
+ /// Special marker for matching an expression.
+ pub const CUSTOM_SYNTAX_MARKER_EXPR: &str = "$expr$";
+ /// Special marker for matching a statements block.
+ pub const CUSTOM_SYNTAX_MARKER_BLOCK: &str = "$block$";
+ /// Special marker for matching an identifier.
+ pub const CUSTOM_SYNTAX_MARKER_IDENT: &str = "$ident$";
+ /// Special marker for matching a single symbol.
+ pub const CUSTOM_SYNTAX_MARKER_SYMBOL: &str = "$symbol$";
+ /// Special marker for matching a string literal.
+ pub const CUSTOM_SYNTAX_MARKER_STRING: &str = "$string$";
+ /// Special marker for matching an integer number.
+ pub const CUSTOM_SYNTAX_MARKER_INT: &str = "$int$";
+ /// Special marker for matching a floating-point number.
+ #[cfg(not(feature = "no_float"))]
+ pub const CUSTOM_SYNTAX_MARKER_FLOAT: &str = "$float$";
+ /// Special marker for matching a boolean value.
+ pub const CUSTOM_SYNTAX_MARKER_BOOL: &str = "$bool$";
+ /// Special marker for identifying the custom syntax variant.
+ pub const CUSTOM_SYNTAX_MARKER_SYNTAX_VARIANT: &str = "$$";
+}
+
+/// A general expression evaluation trait object.
+#[cfg(not(feature = "sync"))]
+pub type FnCustomSyntaxEval = dyn Fn(&mut EvalContext, &[Expression], &Dynamic) -> RhaiResult;
+/// A general expression evaluation trait object.
+#[cfg(feature = "sync")]
+pub type FnCustomSyntaxEval =
+ dyn Fn(&mut EvalContext, &[Expression], &Dynamic) -> RhaiResult + Send + Sync;
+
+/// A general expression parsing trait object.
+#[cfg(not(feature = "sync"))]
+pub type FnCustomSyntaxParse =
+ dyn Fn(&[ImmutableString], &str, &mut Dynamic) -> ParseResult<Option<ImmutableString>>;
+/// A general expression parsing trait object.
+#[cfg(feature = "sync")]
+pub type FnCustomSyntaxParse = dyn Fn(&[ImmutableString], &str, &mut Dynamic) -> ParseResult<Option<ImmutableString>>
+ + Send
+ + Sync;
+
+/// An expression sub-tree in an [`AST`][crate::AST].
+#[derive(Debug, Clone)]
+pub struct Expression<'a>(&'a Expr);
+
+impl<'a> From<&'a Expr> for Expression<'a> {
+ #[inline(always)]
+ fn from(expr: &'a Expr) -> Self {
+ Self(expr)
+ }
+}
+
+impl Expression<'_> {
+ /// Evaluate this [expression tree][Expression] within an [evaluation context][`EvalContext`].
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is very low level. It evaluates an expression from an [`AST`][crate::AST].
+ #[inline(always)]
+ pub fn eval_with_context(&self, context: &mut EvalContext) -> RhaiResult {
+ context.eval_expression_tree(self)
+ }
+ /// Evaluate this [expression tree][Expression] within an [evaluation context][`EvalContext`].
+ ///
+ /// The following option is available:
+ ///
+ /// * whether to rewind the [`Scope`][crate::Scope] after evaluation if the expression is a [`StmtBlock`][crate::ast::StmtBlock]
+ ///
+ /// # WARNING - Unstable API
+ ///
+ /// This API is volatile and may change in the future.
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is _extremely_ low level. It evaluates an expression from an [`AST`][crate::AST].
+ #[deprecated = "This API is NOT deprecated, but it is considered volatile and may change in the future."]
+ #[inline(always)]
+ pub fn eval_with_context_raw(
+ &self,
+ context: &mut EvalContext,
+ rewind_scope: bool,
+ ) -> RhaiResult {
+ #[allow(deprecated)]
+ context.eval_expression_tree_raw(self, rewind_scope)
+ }
+ /// Get the value of this expression if it is a variable name or a string constant.
+ ///
+ /// Returns [`None`] also if the constant is not of the specified type.
+ #[inline(always)]
+ #[must_use]
+ pub fn get_string_value(&self) -> Option<&str> {
+ match self.0 {
+ #[cfg(not(feature = "no_module"))]
+ Expr::Variable(x, ..) if !x.1.is_empty() => None,
+ Expr::Variable(x, ..) => Some(x.3.as_str()),
+ #[cfg(not(feature = "no_function"))]
+ Expr::ThisPtr(..) => Some(crate::engine::KEYWORD_THIS),
+ Expr::StringConstant(x, ..) => Some(x.as_str()),
+ _ => None,
+ }
+ }
+ /// Get the position of this expression.
+ #[inline(always)]
+ #[must_use]
+ pub const fn position(&self) -> Position {
+ self.0.position()
+ }
+ /// Get the value of this expression if it is a literal constant.
+ ///
+ /// Supports [`INT`][crate::INT], [`FLOAT`][crate::FLOAT], `()`, `char`, `bool` and
+ /// [`ImmutableString`][crate::ImmutableString].
+ ///
+ /// Returns [`None`] also if the constant is not of the specified type.
+ #[inline]
+ #[must_use]
+ pub fn get_literal_value<T: Variant>(&self) -> Option<T> {
+ // Coded this way in order to maximally leverage potentials for dead-code removal.
+ match self.0 {
+ Expr::IntegerConstant(x, ..) => reify! { *x => Option<T> },
+
+ #[cfg(not(feature = "no_float"))]
+ Expr::FloatConstant(x, ..) => reify! { *x => Option<T> },
+
+ Expr::CharConstant(x, ..) => reify! { *x => Option<T> },
+ Expr::StringConstant(x, ..) => reify! { x.clone() => Option<T> },
+ Expr::Variable(x, ..) => reify! { x.3.clone() => Option<T> },
+ Expr::BoolConstant(x, ..) => reify! { *x => Option<T> },
+ Expr::Unit(..) => reify! { () => Option<T> },
+
+ _ => None,
+ }
+ }
+}
+
+impl Borrow<Expr> for Expression<'_> {
+ #[inline(always)]
+ #[must_use]
+ fn borrow(&self) -> &Expr {
+ self.0
+ }
+}
+
+impl AsRef<Expr> for Expression<'_> {
+ #[inline(always)]
+ #[must_use]
+ fn as_ref(&self) -> &Expr {
+ self.0
+ }
+}
+
+impl Deref for Expression<'_> {
+ type Target = Expr;
+
+ #[inline(always)]
+ #[must_use]
+ fn deref(&self) -> &Self::Target {
+ self.0
+ }
+}
+
+/// Definition of a custom syntax definition.
+pub struct CustomSyntax {
+ /// A parsing function to return the next token in a custom syntax based on the
+ /// symbols parsed so far.
+ pub parse: Box<FnCustomSyntaxParse>,
+ /// Custom syntax implementation function.
+ pub func: Box<FnCustomSyntaxEval>,
+ /// Any variables added/removed in the scope?
+ pub scope_may_be_changed: bool,
+}
+
+impl Engine {
+ /// Register a custom syntax with the [`Engine`].
+ ///
+ /// Not available under `no_custom_syntax`.
+ ///
+ /// * `symbols` holds a slice of strings that define the custom syntax.
+ /// * `scope_may_be_changed` specifies variables _may_ be added/removed by this custom syntax.
+ /// * `func` is the implementation function.
+ ///
+ /// ## Note on `symbols`
+ ///
+ /// * Whitespaces around symbols are stripped.
+ /// * Symbols that are all-whitespace or empty are ignored.
+ /// * If `symbols` does not contain at least one valid token, then the custom syntax registration
+ /// is simply ignored.
+ ///
+ /// ## Note on `scope_may_be_changed`
+ ///
+ /// If `scope_may_be_changed` is `true`, then _size_ of the current [`Scope`][crate::Scope]
+ /// _may_ be modified by this custom syntax.
+ ///
+ /// Adding new variables and/or removing variables count.
+ ///
+ /// Simply modifying the values of existing variables does NOT count, as the _size_ of the
+ /// current [`Scope`][crate::Scope] is unchanged, so `false` should be passed.
+ ///
+ /// Replacing one variable with another (i.e. adding a new variable and removing one variable at
+ /// the same time so that the total _size_ of the [`Scope`][crate::Scope] is unchanged) also
+ /// does NOT count, so `false` should be passed.
+ pub fn register_custom_syntax<S: AsRef<str> + Into<Identifier>>(
+ &mut self,
+ symbols: impl AsRef<[S]>,
+ scope_may_be_changed: bool,
+ func: impl Fn(&mut EvalContext, &[Expression]) -> RhaiResult + SendSync + 'static,
+ ) -> ParseResult<&mut Self> {
+ #[allow(clippy::wildcard_imports)]
+ use markers::*;
+
+ let mut segments = StaticVec::<ImmutableString>::new();
+
+ for s in symbols.as_ref() {
+ let s = s.as_ref().trim();
+
+ // Skip empty symbols
+ if s.is_empty() {
+ continue;
+ }
+
+ let token = Token::lookup_symbol_from_syntax(s).or_else(|| {
+ is_reserved_keyword_or_symbol(s)
+ .0
+ .then(|| Token::Reserved(Box::new(s.into())))
+ });
+
+ let seg = match s {
+ // Markers not in first position
+ CUSTOM_SYNTAX_MARKER_IDENT
+ | CUSTOM_SYNTAX_MARKER_SYMBOL
+ | CUSTOM_SYNTAX_MARKER_EXPR
+ | CUSTOM_SYNTAX_MARKER_BLOCK
+ | CUSTOM_SYNTAX_MARKER_BOOL
+ | CUSTOM_SYNTAX_MARKER_INT
+ | CUSTOM_SYNTAX_MARKER_STRING
+ if !segments.is_empty() =>
+ {
+ s.into()
+ }
+ // Markers not in first position
+ #[cfg(not(feature = "no_float"))]
+ CUSTOM_SYNTAX_MARKER_FLOAT if !segments.is_empty() => s.into(),
+
+ // Identifier not in first position
+ _ if !segments.is_empty() && is_valid_identifier(s) => s.into(),
+
+ // Keyword/symbol not in first position
+ _ if !segments.is_empty() && token.is_some() => {
+ // Make it a custom keyword/symbol if it is disabled or reserved
+ if (self.is_symbol_disabled(s)
+ || token.as_ref().map_or(false, Token::is_reserved))
+ && !self.is_custom_keyword(s)
+ {
+ self.custom_keywords
+ .get_or_insert_with(Default::default)
+ .insert(s.into(), None);
+ }
+ s.into()
+ }
+
+ // Standard keyword in first position but not disabled
+ _ if segments.is_empty()
+ && token.as_ref().map_or(false, Token::is_standard_keyword)
+ && !self.is_symbol_disabled(s) =>
+ {
+ return Err(LexError::ImproperSymbol(
+ s.to_string(),
+ format!("Improper symbol for custom syntax at position #0: '{s}'"),
+ )
+ .into_err(Position::NONE));
+ }
+
+ // Identifier or symbol in first position
+ _ if segments.is_empty()
+ && (is_valid_identifier(s) || is_reserved_keyword_or_symbol(s).0) =>
+ {
+ // Make it a custom keyword/symbol if it is disabled or reserved
+ if self.is_symbol_disabled(s)
+ || (token.as_ref().map_or(false, Token::is_reserved)
+ && !self.is_custom_keyword(s))
+ {
+ self.custom_keywords
+ .get_or_insert_with(Default::default)
+ .insert(s.into(), None);
+ }
+ s.into()
+ }
+
+ // Anything else is an error
+ _ => {
+ return Err(LexError::ImproperSymbol(
+ s.to_string(),
+ format!(
+ "Improper symbol for custom syntax at position #{}: '{s}'",
+ segments.len() + 1,
+ ),
+ )
+ .into_err(Position::NONE));
+ }
+ };
+
+ segments.push(seg);
+ }
+
+ // If the syntax has nothing, just ignore the registration
+ if segments.is_empty() {
+ return Ok(self);
+ }
+
+ // The first keyword/symbol is the discriminator
+ let key = segments[0].clone();
+
+ self.register_custom_syntax_with_state_raw(
+ key,
+ // Construct the parsing function
+ move |stream, _, _| match stream.len() {
+ len if len >= segments.len() => Ok(None),
+ len => Ok(Some(segments[len].clone())),
+ },
+ scope_may_be_changed,
+ move |context, expressions, _| func(context, expressions),
+ );
+
+ Ok(self)
+ }
+ /// Register a custom syntax with the [`Engine`] with custom user-defined state.
+ ///
+ /// Not available under `no_custom_syntax`.
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is very low level.
+ ///
+ /// * `scope_may_be_changed` specifies variables have been added/removed by this custom syntax.
+ /// * `parse` is the parsing function.
+ /// * `func` is the implementation function.
+ ///
+ /// All custom keywords used as symbols must be manually registered via [`Engine::register_custom_operator`].
+ /// Otherwise, they won't be recognized.
+ ///
+ /// # Parsing Function Signature
+ ///
+ /// The parsing function has the following signature:
+ ///
+ /// `Fn(symbols: &[ImmutableString], look_ahead: &str, state: &mut Dynamic) -> Result<Option<ImmutableString>, ParseError>`
+ ///
+ /// where:
+ /// * `symbols`: a slice of symbols that have been parsed so far, possibly containing `$expr$` and/or `$block$`;
+ /// `$ident$` and other literal markers are replaced by the actual text
+ /// * `look_ahead`: a string slice containing the next symbol that is about to be read
+ /// * `state`: a [`Dynamic`] value that contains a user-defined state
+ ///
+ /// ## Return value
+ ///
+ /// * `Ok(None)`: parsing complete and there are no more symbols to match.
+ /// * `Ok(Some(symbol))`: the next symbol to match, which can also be `$expr$`, `$ident$` or `$block$`.
+ /// * `Err(ParseError)`: error that is reflected back to the [`Engine`], normally `ParseError(ParseErrorType::BadInput(LexError::ImproperSymbol(message)), Position::NONE)` to indicate a syntax error, but it can be any [`ParseError`][crate::ParseError].
+ pub fn register_custom_syntax_with_state_raw(
+ &mut self,
+ key: impl Into<Identifier>,
+ parse: impl Fn(&[ImmutableString], &str, &mut Dynamic) -> ParseResult<Option<ImmutableString>>
+ + SendSync
+ + 'static,
+ scope_may_be_changed: bool,
+ func: impl Fn(&mut EvalContext, &[Expression], &Dynamic) -> RhaiResult + SendSync + 'static,
+ ) -> &mut Self {
+ self.custom_syntax
+ .get_or_insert_with(Default::default)
+ .insert(
+ key.into(),
+ CustomSyntax {
+ parse: Box::new(parse),
+ func: Box::new(func),
+ scope_may_be_changed,
+ }
+ .into(),
+ );
+ self
+ }
+}
diff --git a/rhai/src/api/definitions/builtin-functions.d.rhai b/rhai/src/api/definitions/builtin-functions.d.rhai
new file mode 100644
index 0000000..0ccab0d
--- /dev/null
+++ b/rhai/src/api/definitions/builtin-functions.d.rhai
@@ -0,0 +1,274 @@
+/// Display any data to the standard output.
+///
+/// # Example
+///
+/// ```rhai
+/// let answer = 42;
+///
+/// print(`The Answer is ${answer}`);
+/// ```
+fn print(data: ?);
+
+/// Display any data to the standard output in debug format.
+///
+/// # Example
+///
+/// ```rhai
+/// let answer = 42;
+///
+/// debug(answer);
+/// ```
+fn debug(data: ?);
+
+/// Get the type of a value.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello, world!";
+///
+/// print(x.type_of()); // prints "string"
+/// ```
+fn type_of(data: ?) -> String;
+
+/// Create a function pointer to a named function.
+///
+/// If the specified name is not a valid function name, an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = Fn("foo"); // function pointer to 'foo'
+///
+/// f.call(42); // call: foo(42)
+/// ```
+fn Fn(fn_name: String) -> FnPtr;
+
+/// Call a function pointed to by a function pointer,
+/// passing following arguments to the function call.
+///
+/// If an appropriate function is not found, an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// let f = Fn("foo"); // function pointer to 'foo'
+///
+/// f.call(1, 2, 3); // call: foo(1, 2, 3)
+/// ```
+fn call(fn_ptr: FnPtr, ...args: ?) -> ?;
+
+/// Call a function pointed to by a function pointer, binding the `this` pointer
+/// to the object of the method call, and passing on following arguments to the function call.
+///
+/// If an appropriate function is not found, an error is raised.
+///
+/// # Example
+///
+/// ```rhai
+/// fn add(x) {
+/// this + x
+/// }
+///
+/// let f = Fn("add"); // function pointer to 'add'
+///
+/// let x = 41;
+///
+/// let r = x.call(f, 1); // call: add(1) with 'this' = 'x'
+///
+/// print(r); // prints 42
+/// ```
+fn call(obj: ?, fn_ptr: FnPtr, ...args: ?) -> ?;
+
+/// Curry a number of arguments into a function pointer and return it as a new function pointer.
+///
+/// # Example
+///
+/// ```rhai
+/// fn foo(x, y, z) {
+/// x + y + z
+/// }
+///
+/// let f = Fn("foo");
+///
+/// let g = f.curry(1, 2); // curried arguments: 1, 2
+///
+/// g.call(3); // call: foo(1, 2, 3)
+/// ```
+fn curry(fn_ptr: FnPtr, ...args: ?) -> FnPtr;
+
+/// Return `true` if a script-defined function exists with a specified name and
+/// number of parameters.
+///
+/// # Example
+///
+/// ```rhai
+/// fn foo(x) { }
+///
+/// print(is_def_fn("foo", 1)); // prints true
+/// print(is_def_fn("foo", 2)); // prints false
+/// print(is_def_fn("foo", 0)); // prints false
+/// print(is_def_fn("bar", 1)); // prints false
+/// ```
+fn is_def_fn(fn_name: String, num_params: int) -> bool;
+
+/// Return `true` if a script-defined function exists with a specified name and
+/// number of parameters, bound to a specified type for `this`.
+///
+/// # Example
+///
+/// ```rhai
+/// // A method that requires `this` to be `MyType`
+/// fn MyType.foo(x) { }
+///
+/// print(is_def_fn("MyType", "foo", 1)); // prints true
+/// print(is_def_fn("foo", 1)); // prints false
+/// print(is_def_fn("MyType", "foo", 2)); // prints false
+/// ```
+fn is_def_fn(this_type: String, fn_name: String, num_params: int) -> bool;
+
+/// Return `true` if a variable matching a specified name is defined.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 42;
+///
+/// print(is_def_var("x")); // prints true
+/// print(is_def_var("foo")); // prints false
+///
+/// {
+/// let y = 1;
+/// print(is_def_var("y")); // prints true
+/// }
+///
+/// print(is_def_var("y")); // prints false
+/// ```
+fn is_def_var(var_name: String) -> bool;
+
+/// Return `true` if the variable is shared.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 42;
+///
+/// print(is_shared(x)); // prints false
+///
+/// let f = || x; // capture 'x', making it shared
+///
+/// print(is_shared(x)); // prints true
+/// ```
+fn is_shared(variable: ?) -> bool;
+
+/// Evaluate a text script within the current scope.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = 42;
+///
+/// eval("let y = x; x = 123;");
+///
+/// print(x); // prints 123
+/// print(y); // prints 42
+/// ```
+fn eval(script: String) -> ?;
+
+/// Return `true` if the string contains another string.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello world!";
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if "world" in x {
+/// print("found!");
+/// }
+/// ```
+fn contains(string: String, find: String) -> bool;
+
+/// Return `true` if the string contains a character.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let x = "hello world!";
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 'w' in x {
+/// print("found!");
+/// }
+/// ```
+fn contains(string: String, ch: char) -> bool;
+
+/// Return `true` if a value falls within the exclusive range.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let r = 1..100;
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 42 in r {
+/// print("found!");
+/// }
+/// ```
+fn contains(range: Range<int>, value: int) -> bool;
+
+/// Return `true` if a value falls within the inclusive range.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let r = 1..=100;
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 42 in r {
+/// print("found!");
+/// }
+/// ```
+fn contains(range: RangeInclusive<int>, value: int) -> bool;
+
+/// Return `true` if a key exists within the object map.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let m = #{a:1, b:2, c:3};
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if "c" in m {
+/// print("found!");
+/// }
+/// ```
+fn contains(map: Map, string: String) -> bool;
+
+/// Return `true` if a value is found within the BLOB.
+///
+/// This function also drives the `in` operator.
+///
+/// # Example
+///
+/// ```rhai
+/// let b = blob();
+///
+/// b += 1; b += 2; b += 3; b += 4; b += 5;
+///
+/// // The 'in' operator calls 'contains' in the background
+/// if 3 in b {
+/// print("found!");
+/// }
+/// ```
+fn contains(blob: Blob, value: int) -> bool;
diff --git a/rhai/src/api/definitions/builtin-operators.d.rhai b/rhai/src/api/definitions/builtin-operators.d.rhai
new file mode 100644
index 0000000..70897e6
--- /dev/null
+++ b/rhai/src/api/definitions/builtin-operators.d.rhai
@@ -0,0 +1,257 @@
+op ==(int, int) -> bool;
+op !=(int, int) -> bool;
+op >(int, int) -> bool;
+op >=(int, int) -> bool;
+op <(int, int) -> bool;
+op <=(int, int) -> bool;
+op &(int, int) -> int;
+op |(int, int) -> int;
+op ^(int, int) -> int;
+op ..(int, int) -> Range<int>;
+op ..=(int, int) -> RangeInclusive<int>;
+
+op ==(bool, bool) -> bool;
+op !=(bool, bool) -> bool;
+op >(bool, bool) -> bool;
+op >=(bool, bool) -> bool;
+op <(bool, bool) -> bool;
+op <=(bool, bool) -> bool;
+op &(bool, bool) -> bool;
+op |(bool, bool) -> bool;
+op ^(bool, bool) -> bool;
+
+op ==((), ()) -> bool;
+op !=((), ()) -> bool;
+op >((), ()) -> bool;
+op >=((), ()) -> bool;
+op <((), ()) -> bool;
+op <=((), ()) -> bool;
+
+op +(int, int) -> int;
+op -(int, int) -> int;
+op *(int, int) -> int;
+op /(int, int) -> int;
+op %(int, int) -> int;
+op **(int, int) -> int;
+op >>(int, int) -> int;
+op <<(int, int) -> int;
+
+op +(float, float) -> float;
+op -(float, float) -> float;
+op *(float, float) -> float;
+op /(float, float) -> float;
+op %(float, float) -> float;
+op **(float, float) -> float;
+op ==(float, float) -> bool;
+op !=(float, float) -> bool;
+op >(float, float) -> bool;
+op >=(float, float) -> bool;
+op <(float, float) -> bool;
+op <=(float, float) -> bool;
+
+op +(float, int) -> float;
+op -(float, int) -> float;
+op *(float, int) -> float;
+op /(float, int) -> float;
+op %(float, int) -> float;
+op **(float, int) -> float;
+op ==(float, int) -> bool;
+op !=(float, int) -> bool;
+op >(float, int) -> bool;
+op >=(float, int) -> bool;
+op <(float, int) -> bool;
+op <=(float, int) -> bool;
+
+op +(int, float) -> float;
+op -(int, float) -> float;
+op *(int, float) -> float;
+op /(int, float) -> float;
+op %(int, float) -> float;
+op **(int, float) -> float;
+op ==(int, float) -> bool;
+op !=(int, float) -> bool;
+op >(int, float) -> bool;
+op >=(int, float) -> bool;
+op <(int, float) -> bool;
+op <=(int, float) -> bool;
+
+op +(Decimal, Decimal) -> Decimal;
+op -(Decimal, Decimal) -> Decimal;
+op *(Decimal, Decimal) -> Decimal;
+op /(Decimal, Decimal) -> Decimal;
+op %(Decimal, Decimal) -> Decimal;
+op **(Decimal, Decimal) -> Decimal;
+op ==(Decimal, Decimal) -> bool;
+op !=(Decimal, Decimal) -> bool;
+op >(Decimal, Decimal) -> bool;
+op >=(Decimal, Decimal) -> bool;
+op <(Decimal, Decimal) -> bool;
+op <=(Decimal, Decimal) -> bool;
+
+op +(Decimal, int) -> Decimal;
+op -(Decimal, int) -> Decimal;
+op *(Decimal, int) -> Decimal;
+op /(Decimal, int) -> Decimal;
+op %(Decimal, int) -> Decimal;
+op **(Decimal, int) -> Decimal;
+op ==(Decimal, int) -> bool;
+op !=(Decimal, int) -> bool;
+op >(Decimal, int) -> bool;
+op >=(Decimal, int) -> bool;
+op <(Decimal, int) -> bool;
+op <=(Decimal, int) -> bool;
+
+op +(int, Decimal) -> Decimal;
+op -(int, Decimal) -> Decimal;
+op *(int, Decimal) -> Decimal;
+op /(int, Decimal) -> Decimal;
+op %(int, Decimal) -> Decimal;
+op **(int, Decimal) -> Decimal;
+op ==(int, Decimal) -> bool;
+op !=(int, Decimal) -> bool;
+op >(int, Decimal) -> bool;
+op >=(int, Decimal) -> bool;
+op <(int, Decimal) -> bool;
+op <=(int, Decimal) -> bool;
+
+op +(String, String) -> String;
+op -(String, String) -> String;
+op ==(String, String) -> bool;
+op !=(String, String) -> bool;
+op >(String, String) -> bool;
+op >=(String, String) -> bool;
+op <(String, String) -> bool;
+op <=(String, String) -> bool;
+
+op +(char, char) -> String;
+op ==(char, char) -> bool;
+op !=(char, char) -> bool;
+op >(char, char) -> bool;
+op >=(char, char) -> bool;
+op <(char, char) -> bool;
+op <=(char, char) -> bool;
+
+op +(char, String) -> String;
+op ==(char, String) -> bool;
+op !=(char, String) -> bool;
+op >(char, String) -> bool;
+op >=(char, String) -> bool;
+op <(char, String) -> bool;
+op <=(char, String) -> bool;
+
+op +(String, char) -> String;
+op -(String, char) -> String;
+op ==(String, char) -> bool;
+op !=(String, char) -> bool;
+op >(String, char) -> bool;
+op >=(String, char) -> bool;
+op <(String, char) -> bool;
+op <=(String, char) -> bool;
+
+op +((), String) -> String;
+op ==((), String) -> bool;
+op !=((), String) -> bool;
+op >((), String) -> bool;
+op >=((), String) -> bool;
+op <((), String) -> bool;
+op <=((), String) -> bool;
+
+op +(String, ()) -> String;
+op ==(String, ()) -> bool;
+op !=(String, ()) -> bool;
+op >(String, ()) -> bool;
+op >=(String, ()) -> bool;
+op <(String, ()) -> bool;
+op <=(String, ()) -> bool;
+
+op +(Blob, Blob) -> Blob;
+op +(Blob, char) -> Blob;
+op ==(Blob, Blob) -> bool;
+op !=(Blob, Blob) -> bool;
+
+
+op ==(Range<int>, RangeInclusive<int>) -> bool;
+op !=(Range<int>, RangeInclusive<int>) -> bool;
+
+op ==(RangeInclusive<int>, Range<int>) -> bool;
+op !=(RangeInclusive<int>, Range<int>) -> bool;
+
+op ==(Range<int>, Range<int>) -> bool;
+op !=(Range<int>, Range<int>) -> bool;
+
+op ==(RangeInclusive<int>, RangeInclusive<int>) -> bool;
+op !=(RangeInclusive<int>, RangeInclusive<int>) -> bool;
+
+op ==(?, ?) -> bool;
+op !=(?, ?) -> bool;
+op >(?, ?) -> bool;
+op >=(?, ?) -> bool;
+op <(?, ?) -> bool;
+op <=(?, ?) -> bool;
+
+
+op &=(bool, bool);
+op |=(bool, bool);
+
+op +=(int, int);
+op -=(int, int);
+op *=(int, int);
+op /=(int, int);
+op %=(int, int);
+op **=(int, int);
+op >>=(int, int);
+op <<=(int, int);
+op &=(int, int);
+op |=(int, int);
+op ^=(int, int);
+
+op +=(float, float);
+op -=(float, float);
+op *=(float, float);
+op /=(float, float);
+op %=(float, float);
+op **=(float, float);
+
+op +=(float, int);
+op -=(float, int);
+op *=(float, int);
+op /=(float, int);
+op %=(float, int);
+op **=(float, int);
+
+op +=(Decimal, Decimal);
+op -=(Decimal, Decimal);
+op *=(Decimal, Decimal);
+op /=(Decimal, Decimal);
+op %=(Decimal, Decimal);
+op **=(Decimal, Decimal);
+
+op +=(Decimal, int);
+op -=(Decimal, int);
+op *=(Decimal, int);
+op /=(Decimal, int);
+op %=(Decimal, int);
+op **=(Decimal, int);
+
+op +=(String, String);
+op -=(String, String);
+op +=(String, char);
+op -=(String, char);
+op +=(char, String);
+op +=(char, char);
+
+op +=(Array, Array);
+op +=(Array, ?);
+
+op +=(Blob, Blob);
+op +=(Blob, int);
+op +=(Blob, char);
+op +=(Blob, String);
+
+op in(?, Array) -> bool;
+op in(String, String) -> bool;
+op in(char, String) -> bool;
+op in(int, Range<int>) -> bool;
+op in(int, RangeInclusive<int>) -> bool;
+op in(String, Map) -> bool;
+op in(int, Blob) -> bool;
diff --git a/rhai/src/api/definitions/mod.rs b/rhai/src/api/definitions/mod.rs
new file mode 100644
index 0000000..f4fae1d
--- /dev/null
+++ b/rhai/src/api/definitions/mod.rs
@@ -0,0 +1,598 @@
+//! Module that defines functions to output definition files for [`Engine`].
+#![cfg(feature = "internals")]
+#![cfg(feature = "metadata")]
+
+use crate::module::{FuncInfo, ModuleFlags};
+use crate::tokenizer::{is_valid_function_name, Token};
+use crate::{Engine, FnAccess, FnPtr, Module, Scope, INT};
+
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{any::type_name, borrow::Cow, cmp::Ordering, fmt};
+
+impl Engine {
+ /// _(metadata, internals)_ Return [`Definitions`] that can be used to generate definition files
+ /// for the [`Engine`].
+ /// Exported under the `internals` and `metadata` feature only.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # use rhai::Engine;
+ /// # fn main() -> std::io::Result<()> {
+ /// let engine = Engine::new();
+ ///
+ /// engine
+ /// .definitions()
+ /// .write_to_dir(".rhai/definitions")?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn definitions(&self) -> Definitions {
+ Definitions {
+ engine: self,
+ scope: None,
+ config: DefinitionsConfig::default(),
+ }
+ }
+
+ /// _(metadata, internals)_ Return [`Definitions`] that can be used to generate definition files
+ /// for the [`Engine`] and the given [`Scope`].
+ /// Exported under the `internals` and `metadata` feature only.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # use rhai::{Engine, Scope};
+ /// # fn main() -> std::io::Result<()> {
+ /// let engine = Engine::new();
+ /// let scope = Scope::new();
+ /// engine
+ /// .definitions_with_scope(&scope)
+ /// .write_to_dir(".rhai/definitions")?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn definitions_with_scope<'e>(&'e self, scope: &'e Scope<'e>) -> Definitions<'e> {
+ Definitions {
+ engine: self,
+ scope: Some(scope),
+ config: DefinitionsConfig::default(),
+ }
+ }
+}
+
+/// Internal configuration for module generation.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
+#[non_exhaustive]
+pub struct DefinitionsConfig {
+ /// Write `module ...` headers in definition files (default `false`).
+ pub write_headers: bool,
+ /// Include standard packages (default `true`).
+ pub include_standard_packages: bool,
+}
+
+impl Default for DefinitionsConfig {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self {
+ write_headers: false,
+ include_standard_packages: true,
+ }
+ }
+}
+
+/// _(metadata, internals)_ Definitions helper type to generate definition files based on the
+/// contents of an [`Engine`].
+/// Exported under the `internals` and `metadata` feature only.
+#[derive(Debug, Clone)]
+pub struct Definitions<'e> {
+ /// The [`Engine`].
+ engine: &'e Engine,
+ /// Optional [`Scope`] to include.
+ scope: Option<&'e Scope<'e>>,
+ config: DefinitionsConfig,
+}
+
+impl Definitions<'_> {
+ /// Write `module ...` headers in separate definitions, default `false`.
+ ///
+ /// Headers are always present in content that is expected to be written to a file
+ /// (i.e. `write_to*` and `*_file` methods).
+ #[inline(always)]
+ #[must_use]
+ pub const fn with_headers(mut self, headers: bool) -> Self {
+ self.config.write_headers = headers;
+ self
+ }
+ /// Include standard packages when writing definition files.
+ #[inline(always)]
+ #[must_use]
+ pub const fn include_standard_packages(mut self, include_standard_packages: bool) -> Self {
+ self.config.include_standard_packages = include_standard_packages;
+ self
+ }
+ /// Get the [`Engine`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn engine(&self) -> &Engine {
+ self.engine
+ }
+ /// Get the [`Scope`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn scope(&self) -> Option<&Scope> {
+ self.scope
+ }
+ /// Get the configuration.
+ #[inline(always)]
+ #[must_use]
+ pub(crate) const fn config(&self) -> &DefinitionsConfig {
+ &self.config
+ }
+}
+
+impl Definitions<'_> {
+ /// Output all definition files returned from [`iter_files`][Definitions::iter_files] to a
+ /// specified directory.
+ ///
+ /// This function creates the directories and overrides any existing files if needed.
+ #[cfg(all(not(feature = "no_std"), not(target_family = "wasm")))]
+ #[inline]
+ pub fn write_to_dir(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
+ use std::fs;
+
+ let path = path.as_ref();
+
+ fs::create_dir_all(path)?;
+
+ for (file_name, content) in self.iter_files() {
+ fs::write(path.join(file_name), content)?;
+ }
+
+ Ok(())
+ }
+
+ /// Output all definitions merged into a single file.
+ ///
+ /// The parent directory must exist but the file will be created or overwritten as needed.
+ #[cfg(all(not(feature = "no_std"), not(target_family = "wasm")))]
+ #[inline(always)]
+ pub fn write_to_file(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
+ std::fs::write(path, self.single_file())
+ }
+
+ /// Return all definitions merged into a single file.
+ #[inline]
+ #[must_use]
+ pub fn single_file(&self) -> String {
+ let config = DefinitionsConfig {
+ write_headers: false,
+ ..self.config
+ };
+
+ let mut def_file = String::from("module static;\n\n");
+
+ if config.include_standard_packages {
+ def_file += &Self::builtin_functions_operators_impl(config);
+ def_file += "\n";
+ def_file += &Self::builtin_functions_impl(config);
+ def_file += "\n";
+ }
+ def_file += &self.static_module_impl(config);
+ def_file += "\n";
+
+ #[cfg(not(feature = "no_module"))]
+ {
+ use std::fmt::Write;
+
+ for (module_name, module_def) in self.modules_impl(config) {
+ write!(
+ &mut def_file,
+ "\nmodule {module_name} {{\n{module_def}\n}}\n"
+ )
+ .unwrap();
+ }
+ def_file += "\n";
+ }
+
+ def_file += &self.scope_items_impl(config);
+
+ def_file += "\n";
+
+ def_file
+ }
+
+ /// Iterate over generated definition files.
+ ///
+ /// The returned iterator yields all definition files as (filename, content) pairs.
+ #[inline]
+ pub fn iter_files(&self) -> impl Iterator<Item = (String, String)> + '_ {
+ let config = DefinitionsConfig {
+ write_headers: true,
+ ..self.config
+ };
+
+ if config.include_standard_packages {
+ vec![
+ (
+ "__builtin__.d.rhai".to_string(),
+ Self::builtin_functions_impl(config),
+ ),
+ (
+ "__builtin-operators__.d.rhai".to_string(),
+ Self::builtin_functions_operators_impl(config),
+ ),
+ ]
+ } else {
+ vec![]
+ }
+ .into_iter()
+ .chain(std::iter::once((
+ "__static__.d.rhai".to_string(),
+ self.static_module_impl(config),
+ )))
+ .chain(self.scope.iter().map(move |_| {
+ (
+ "__scope__.d.rhai".to_string(),
+ self.scope_items_impl(config),
+ )
+ }))
+ .chain(
+ #[cfg(not(feature = "no_module"))]
+ {
+ self.modules_impl(config)
+ .map(|(name, def)| (format!("{name}.d.rhai"), def))
+ },
+ #[cfg(feature = "no_module")]
+ {
+ std::iter::empty()
+ },
+ )
+ }
+
+ /// Return definitions for all builtin functions.
+ #[inline(always)]
+ #[must_use]
+ pub fn builtin_functions(&self) -> String {
+ Self::builtin_functions_impl(self.config)
+ }
+
+ /// Return definitions for all builtin functions.
+ #[must_use]
+ fn builtin_functions_impl(config: DefinitionsConfig) -> String {
+ let def = include_str!("builtin-functions.d.rhai");
+
+ if config.write_headers {
+ format!("module static;\n\n{def}")
+ } else {
+ def.to_string()
+ }
+ }
+
+ /// Return definitions for all builtin operators.
+ #[inline(always)]
+ #[must_use]
+ pub fn builtin_functions_operators(&self) -> String {
+ Self::builtin_functions_operators_impl(self.config)
+ }
+
+ /// Return definitions for all builtin operators.
+ #[must_use]
+ fn builtin_functions_operators_impl(config: DefinitionsConfig) -> String {
+ let def = include_str!("builtin-operators.d.rhai");
+
+ if config.write_headers {
+ format!("module static;\n\n{def}")
+ } else {
+ def.to_string()
+ }
+ }
+
+ /// Return definitions for all globally available functions and constants.
+ #[inline(always)]
+ #[must_use]
+ pub fn static_module(&self) -> String {
+ self.static_module_impl(self.config)
+ }
+
+ /// Return definitions for all globally available functions and constants.
+ #[must_use]
+ fn static_module_impl(&self, config: DefinitionsConfig) -> String {
+ let mut s = if config.write_headers {
+ String::from("module static;\n\n")
+ } else {
+ String::new()
+ };
+
+ let exclude_flags = if self.config.include_standard_packages {
+ ModuleFlags::empty()
+ } else {
+ ModuleFlags::STANDARD_LIB
+ };
+
+ self.engine
+ .global_modules
+ .iter()
+ .filter(|m| !m.flags.contains(exclude_flags))
+ .enumerate()
+ .for_each(|(i, m)| {
+ if i > 0 {
+ s += "\n\n";
+ }
+ m.write_definition(&mut s, self).unwrap();
+ });
+
+ s
+ }
+
+ /// Return definitions for all items inside the [`Scope`], if any.
+ #[inline(always)]
+ #[must_use]
+ pub fn scope_items(&self) -> String {
+ self.scope_items_impl(self.config)
+ }
+
+ /// Return definitions for all items inside the [`Scope`], if any.
+ #[must_use]
+ fn scope_items_impl(&self, config: DefinitionsConfig) -> String {
+ let mut s = if config.write_headers {
+ String::from("module static;\n\n")
+ } else {
+ String::new()
+ };
+
+ if let Some(scope) = self.scope {
+ scope.write_definition(&mut s, self).unwrap();
+ }
+
+ s
+ }
+
+ /// Return a (module name, definitions) pair for each registered static [module][Module].
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline(always)]
+ pub fn modules(&self) -> impl Iterator<Item = (String, String)> + '_ {
+ self.modules_impl(self.config)
+ }
+
+ /// Return a (module name, definitions) pair for each registered static [module][Module].
+ #[cfg(not(feature = "no_module"))]
+ fn modules_impl(
+ &self,
+ config: DefinitionsConfig,
+ ) -> impl Iterator<Item = (String, String)> + '_ {
+ let mut m = self
+ .engine
+ .global_sub_modules
+ .as_ref()
+ .into_iter()
+ .flatten()
+ .map(move |(name, module)| {
+ (
+ name.to_string(),
+ if config.write_headers {
+ format!("module {name};\n\n{}", module.definition(self))
+ } else {
+ module.definition(self)
+ },
+ )
+ })
+ .collect::<Vec<_>>();
+
+ m.sort_by(|(name1, _), (name2, _)| name1.cmp(name2));
+
+ m.into_iter()
+ }
+}
+
+impl Module {
+ /// Return definitions for all items inside the [`Module`].
+ #[cfg(not(feature = "no_module"))]
+ #[must_use]
+ fn definition(&self, def: &Definitions) -> String {
+ let mut s = String::new();
+ self.write_definition(&mut s, def).unwrap();
+ s
+ }
+
+ /// Output definitions for all items inside the [`Module`].
+ fn write_definition(&self, writer: &mut dyn fmt::Write, def: &Definitions) -> fmt::Result {
+ let mut first = true;
+
+ let mut submodules = self.iter_sub_modules().collect::<Vec<_>>();
+ submodules.sort_by(|(a, _), (b, _)| a.cmp(b));
+
+ for (submodule_name, submodule) in submodules {
+ if !first {
+ writer.write_str("\n\n")?;
+ }
+ first = false;
+
+ writeln!(writer, "module {submodule_name} {{")?;
+ submodule.write_definition(writer, def)?;
+ writer.write_str("}")?;
+ }
+
+ let mut vars = self.iter_var().collect::<Vec<_>>();
+ vars.sort_by(|(a, _), (b, _)| a.cmp(b));
+
+ for (name, value) in vars {
+ if !first {
+ writer.write_str("\n\n")?;
+ }
+ first = false;
+
+ let ty = def_type_name(value.type_name(), def.engine);
+
+ write!(writer, "const {name}: {ty};")?;
+ }
+
+ let mut func_infos = self.iter_fn().collect::<Vec<_>>();
+ func_infos.sort_by(|a, b| match a.metadata.name.cmp(&b.metadata.name) {
+ Ordering::Equal => match a.metadata.num_params.cmp(&b.metadata.num_params) {
+ Ordering::Equal => (a.metadata.params_info.join("")
+ + a.metadata.return_type.as_str())
+ .cmp(&(b.metadata.params_info.join("") + b.metadata.return_type.as_str())),
+ o => o,
+ },
+ o => o,
+ });
+
+ for f in func_infos {
+ if !first {
+ writer.write_str("\n\n")?;
+ }
+ first = false;
+
+ if f.metadata.access != FnAccess::Private {
+ let operator =
+ !f.metadata.name.contains('$') && !is_valid_function_name(&f.metadata.name);
+
+ #[cfg(not(feature = "no_custom_syntax"))]
+ let operator = operator || def.engine.is_custom_keyword(f.metadata.name.as_str());
+
+ f.write_definition(writer, def, operator)?;
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl FuncInfo {
+ /// Output definitions for a function.
+ fn write_definition(
+ &self,
+ writer: &mut dyn fmt::Write,
+ def: &Definitions,
+ operator: bool,
+ ) -> fmt::Result {
+ for comment in &*self.metadata.comments {
+ writeln!(writer, "{comment}")?;
+ }
+
+ if operator {
+ writer.write_str("op ")?;
+ } else {
+ writer.write_str("fn ")?;
+ }
+
+ if let Some(name) = self.metadata.name.strip_prefix("get$") {
+ write!(writer, "get {name}(")?;
+ } else if let Some(name) = self.metadata.name.strip_prefix("set$") {
+ write!(writer, "set {name}(")?;
+ } else {
+ write!(writer, "{}(", self.metadata.name)?;
+ }
+
+ let mut first = true;
+ for i in 0..self.metadata.num_params {
+ if !first {
+ writer.write_str(", ")?;
+ }
+ first = false;
+
+ let (param_name, param_type) =
+ self.metadata
+ .params_info
+ .get(i)
+ .map_or(("_", "?".into()), |s| {
+ let mut s = s.splitn(2, ':');
+ (
+ s.next().unwrap_or("_").split(' ').last().unwrap(),
+ s.next()
+ .map_or(Cow::Borrowed("?"), |ty| def_type_name(ty, def.engine)),
+ )
+ });
+
+ if operator {
+ write!(writer, "{param_type}")?;
+ } else {
+ write!(writer, "{param_name}: {param_type}")?;
+ }
+ }
+
+ write!(
+ writer,
+ ") -> {};",
+ def_type_name(&self.metadata.return_type, def.engine)
+ )?;
+
+ Ok(())
+ }
+}
+
+/// We have to transform some of the types.
+///
+/// This is highly inefficient and is currently based on trial and error with the core packages.
+///
+/// It tries to flatten types, removing `&` and `&mut`, and paths, while keeping generics.
+///
+/// Associated generic types are also rewritten into regular generic type parameters.
+#[must_use]
+fn def_type_name<'a>(ty: &'a str, engine: &'a Engine) -> Cow<'a, str> {
+ let ty = engine.format_type_name(ty).replace("crate::", "");
+ let ty = ty.strip_prefix("&mut").unwrap_or(&*ty).trim();
+ let ty = ty.split("::").last().unwrap();
+
+ let ty = ty
+ .strip_prefix("RhaiResultOf<")
+ .and_then(|s| s.strip_suffix('>'))
+ .map_or(ty, str::trim);
+
+ let ty = ty
+ .replace("Iterator<Item=", "Iterator<")
+ .replace("Dynamic", "?")
+ .replace("INT", "int")
+ .replace(type_name::<INT>(), "int")
+ .replace("FLOAT", "float")
+ .replace("&str", "String")
+ .replace("ImmutableString", "String");
+
+ #[cfg(not(feature = "no_float"))]
+ let ty = ty.replace(type_name::<crate::FLOAT>(), "float");
+
+ #[cfg(not(feature = "no_index"))]
+ let ty = ty.replace(type_name::<crate::Array>(), "Array");
+
+ #[cfg(not(feature = "no_index"))]
+ let ty = ty.replace(type_name::<crate::Blob>(), "Blob");
+
+ #[cfg(not(feature = "no_object"))]
+ let ty = ty.replace(type_name::<crate::Map>(), "Map");
+
+ #[cfg(not(feature = "no_time"))]
+ let ty = ty.replace(type_name::<crate::Instant>(), "Instant");
+
+ let ty = ty.replace(type_name::<FnPtr>(), "FnPtr");
+
+ ty.into()
+}
+
+impl Scope<'_> {
+ /// _(metadata, internals)_ Return definitions for all items inside the [`Scope`].
+ fn write_definition(&self, writer: &mut dyn fmt::Write, def: &Definitions) -> fmt::Result {
+ let mut first = true;
+ for (name, constant, value) in self.iter_raw() {
+ if !first {
+ writer.write_str("\n\n")?;
+ }
+ first = false;
+
+ let kw = if constant { Token::Const } else { Token::Let };
+ let ty = def_type_name(value.type_name(), def.engine);
+
+ write!(writer, "{kw} {name}: {ty};")?;
+ }
+
+ Ok(())
+ }
+}
diff --git a/rhai/src/api/deprecated.rs b/rhai/src/api/deprecated.rs
new file mode 100644
index 0000000..41d5891
--- /dev/null
+++ b/rhai/src/api/deprecated.rs
@@ -0,0 +1,1229 @@
+//! Module containing all deprecated API that will be removed in the next major version.
+
+use crate::func::RegisterNativeFunction;
+use crate::types::dynamic::Variant;
+use crate::{
+ Dynamic, Engine, EvalAltResult, FnPtr, Identifier, ImmutableString, Module, NativeCallContext,
+ Position, RhaiResult, RhaiResultOf, Scope, SharedModule, TypeBuilder, AST,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+use crate::func::register::Mut;
+
+#[cfg(not(target_vendor = "teaclave"))]
+#[cfg(not(feature = "no_std"))]
+#[cfg(not(target_family = "wasm"))]
+use std::path::PathBuf;
+
+impl Engine {
+ /// Evaluate a file, but throw away the result and only return error (if any).
+ /// Useful for when you don't need the result, but still need to keep track of possible errors.
+ ///
+ /// Not available under `no_std` or `WASM`.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`run_file`][Engine::run_file] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[cfg(not(target_vendor = "teaclave"))]
+ #[deprecated(since = "1.1.0", note = "use `run_file` instead")]
+ #[cfg(not(feature = "no_std"))]
+ #[cfg(not(target_family = "wasm"))]
+ #[inline(always)]
+ pub fn consume_file(&self, path: PathBuf) -> RhaiResultOf<()> {
+ self.run_file(path)
+ }
+
+ /// Evaluate a file with own scope, but throw away the result and only return error (if any).
+ /// Useful for when you don't need the result, but still need to keep track of possible errors.
+ ///
+ /// Not available under `no_std` or `WASM`.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`run_file_with_scope`][Engine::run_file_with_scope] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[cfg(not(target_vendor = "teaclave"))]
+ #[deprecated(since = "1.1.0", note = "use `run_file_with_scope` instead")]
+ #[cfg(not(feature = "no_std"))]
+ #[cfg(not(target_family = "wasm"))]
+ #[inline(always)]
+ pub fn consume_file_with_scope(&self, scope: &mut Scope, path: PathBuf) -> RhaiResultOf<()> {
+ self.run_file_with_scope(scope, path)
+ }
+
+ /// Evaluate a string, but throw away the result and only return error (if any).
+ /// Useful for when you don't need the result, but still need to keep track of possible errors.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`run`][Engine::run] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.1.0", note = "use `run` instead")]
+ #[inline(always)]
+ pub fn consume(&self, script: &str) -> RhaiResultOf<()> {
+ self.run(script)
+ }
+
+ /// Evaluate a string with own scope, but throw away the result and only return error (if any).
+ /// Useful for when you don't need the result, but still need to keep track of possible errors.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`run_with_scope`][Engine::run_with_scope] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.1.0", note = "use `run_with_scope` instead")]
+ #[inline(always)]
+ pub fn consume_with_scope(&self, scope: &mut Scope, script: &str) -> RhaiResultOf<()> {
+ self.run_with_scope(scope, script)
+ }
+
+ /// Evaluate an [`AST`], but throw away the result and only return error (if any).
+ /// Useful for when you don't need the result, but still need to keep track of possible errors.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`run_ast`][Engine::run_ast] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.1.0", note = "use `run_ast` instead")]
+ #[inline(always)]
+ pub fn consume_ast(&self, ast: &AST) -> RhaiResultOf<()> {
+ self.run_ast(ast)
+ }
+
+ /// Evaluate an [`AST`] with own scope, but throw away the result and only return error (if any).
+ /// Useful for when you don't need the result, but still need to keep track of possible errors.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`run_ast_with_scope`][Engine::run_ast_with_scope] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.1.0", note = "use `run_ast_with_scope` instead")]
+ #[inline(always)]
+ pub fn consume_ast_with_scope(&self, scope: &mut Scope, ast: &AST) -> RhaiResultOf<()> {
+ self.run_ast_with_scope(scope, ast)
+ }
+ /// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments
+ /// and optionally a value for binding to the `this` pointer.
+ ///
+ /// Not available under `no_function`.
+ ///
+ /// There is an option to evaluate the [`AST`] to load necessary modules before calling the function.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`call_fn_with_options`][Engine::call_fn_with_options] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.1.0", note = "use `call_fn_with_options` instead")]
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ pub fn call_fn_dynamic(
+ &self,
+ scope: &mut Scope,
+ ast: &AST,
+ eval_ast: bool,
+ name: impl AsRef<str>,
+ this_ptr: Option<&mut Dynamic>,
+ arg_values: impl AsMut<[Dynamic]>,
+ ) -> RhaiResult {
+ #[allow(deprecated)]
+ self.call_fn_raw(scope, ast, eval_ast, true, name, this_ptr, arg_values)
+ }
+ /// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments.
+ ///
+ /// Not available under `no_function`.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`call_fn_with_options`][Engine::call_fn_with_options] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.12.0", note = "use `call_fn_with_options` instead")]
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ pub fn call_fn_raw(
+ &self,
+ scope: &mut Scope,
+ ast: &AST,
+ eval_ast: bool,
+ rewind_scope: bool,
+ name: impl AsRef<str>,
+ this_ptr: Option<&mut Dynamic>,
+ arg_values: impl AsMut<[Dynamic]>,
+ ) -> RhaiResult {
+ let mut arg_values = arg_values;
+
+ let options = crate::CallFnOptions {
+ this_ptr,
+ eval_ast,
+ rewind_scope,
+ ..Default::default()
+ };
+
+ self._call_fn(
+ scope,
+ &mut crate::eval::GlobalRuntimeState::new(self),
+ &mut crate::eval::Caches::new(),
+ ast,
+ name.as_ref(),
+ arg_values.as_mut(),
+ options,
+ )
+ }
+ /// Register a custom fallible function with the [`Engine`].
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`register_fn`][Engine::register_fn] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.9.1", note = "use `register_fn` instead")]
+ #[inline(always)]
+ pub fn register_result_fn<A: 'static, const N: usize, const C: bool, R: Variant + Clone>(
+ &mut self,
+ name: impl AsRef<str> + Into<Identifier>,
+ func: impl RegisterNativeFunction<A, N, C, R, true>,
+ ) -> &mut Self {
+ self.register_fn(name, func)
+ }
+ /// Register a getter function for a member of a registered type with the [`Engine`].
+ ///
+ /// The function signature must start with `&mut self` and not `&self`.
+ ///
+ /// Not available under `no_object`.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`register_get`][Engine::register_get] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.9.1", note = "use `register_get` instead")]
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ pub fn register_get_result<T: Variant + Clone, const C: bool, V: Variant + Clone>(
+ &mut self,
+ name: impl AsRef<str>,
+ get_fn: impl RegisterNativeFunction<(Mut<T>,), 1, C, V, true> + crate::func::SendSync + 'static,
+ ) -> &mut Self {
+ self.register_get(name, get_fn)
+ }
+ /// Register a setter function for a member of a registered type with the [`Engine`].
+ ///
+ /// Not available under `no_object`.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`register_set`][Engine::register_set] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.9.1", note = "use `register_set` instead")]
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ pub fn register_set_result<T: Variant + Clone, V: Variant + Clone, const C: bool, S>(
+ &mut self,
+ name: impl AsRef<str>,
+ set_fn: impl RegisterNativeFunction<(Mut<T>, V), 2, C, (), true>
+ + crate::func::SendSync
+ + 'static,
+ ) -> &mut Self {
+ self.register_set(name, set_fn)
+ }
+ /// Register an index getter for a custom type with the [`Engine`].
+ ///
+ /// The function signature must start with `&mut self` and not `&self`.
+ ///
+ /// Not available under both `no_index` and `no_object`.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`register_indexer_get`][Engine::register_indexer_get] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.9.1", note = "use `register_indexer_get` instead")]
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ #[inline(always)]
+ pub fn register_indexer_get_result<
+ T: Variant + Clone,
+ X: Variant + Clone,
+ V: Variant + Clone,
+ const C: bool,
+ >(
+ &mut self,
+ get_fn: impl RegisterNativeFunction<(Mut<T>, X), 2, C, V, true>
+ + crate::func::SendSync
+ + 'static,
+ ) -> &mut Self {
+ self.register_indexer_get(get_fn)
+ }
+ /// Register an index setter for a custom type with the [`Engine`].
+ ///
+ /// Not available under both `no_index` and `no_object`.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`register_indexer_set`][Engine::register_indexer_set] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.9.1", note = "use `register_indexer_set` instead")]
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ #[inline(always)]
+ pub fn register_indexer_set_result<
+ T: Variant + Clone,
+ X: Variant + Clone,
+ V: Variant + Clone,
+ const C: bool,
+ >(
+ &mut self,
+ set_fn: impl RegisterNativeFunction<(Mut<T>, X, V), 3, C, (), true>
+ + crate::func::SendSync
+ + 'static,
+ ) -> &mut Self {
+ self.register_indexer_set(set_fn)
+ }
+ /// Register a custom syntax with the [`Engine`].
+ ///
+ /// Not available under `no_custom_syntax`.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated.
+ /// Use [`register_custom_syntax_with_state_raw`][Engine::register_custom_syntax_with_state_raw] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(
+ since = "1.11.0",
+ note = "use `register_custom_syntax_with_state_raw` instead"
+ )]
+ #[inline(always)]
+ #[cfg(not(feature = "no_custom_syntax"))]
+ pub fn register_custom_syntax_raw(
+ &mut self,
+ key: impl Into<Identifier>,
+ parse: impl Fn(&[ImmutableString], &str) -> crate::parser::ParseResult<Option<ImmutableString>>
+ + crate::func::SendSync
+ + 'static,
+ scope_may_be_changed: bool,
+ func: impl Fn(&mut crate::EvalContext, &[crate::Expression]) -> RhaiResult
+ + crate::func::SendSync
+ + 'static,
+ ) -> &mut Self {
+ self.register_custom_syntax_with_state_raw(
+ key,
+ move |keywords, look_ahead, _| parse(keywords, look_ahead),
+ scope_may_be_changed,
+ move |context, expressions, _| func(context, expressions),
+ )
+ }
+ /// _(internals)_ Evaluate a list of statements with no `this` pointer.
+ /// Exported under the `internals` feature only.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. It will be removed in the next major version.
+ #[cfg(feature = "internals")]
+ #[inline(always)]
+ #[deprecated(since = "1.12.0")]
+ pub fn eval_statements_raw(
+ &self,
+ global: &mut crate::eval::GlobalRuntimeState,
+ caches: &mut crate::eval::Caches,
+ scope: &mut Scope,
+ statements: &[crate::ast::Stmt],
+ ) -> RhaiResult {
+ self.eval_global_statements(global, caches, scope, statements)
+ }
+}
+
+impl Dynamic {
+ /// Convert the [`Dynamic`] into a [`String`] and return it.
+ /// If there are other references to the same string, a cloned copy is returned.
+ /// Returns the name of the actual type if the cast fails.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`into_string`][Dynamic::into_string] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.1.0", note = "use `into_string` instead")]
+ #[inline(always)]
+ pub fn as_string(self) -> Result<String, &'static str> {
+ self.into_string()
+ }
+
+ /// Convert the [`Dynamic`] into an [`ImmutableString`] and return it.
+ /// Returns the name of the actual type if the cast fails.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`into_immutable_string`][Dynamic::into_immutable_string] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.1.0", note = "use `into_immutable_string` instead")]
+ #[inline(always)]
+ pub fn as_immutable_string(self) -> Result<ImmutableString, &'static str> {
+ self.into_immutable_string()
+ }
+}
+
+impl NativeCallContext<'_> {
+ /// Create a new [`NativeCallContext`].
+ ///
+ /// # Unimplemented
+ ///
+ /// This method is deprecated. It is no longer implemented and always panics.
+ ///
+ /// Use [`FnPtr::call`] to call a function pointer directly.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(
+ since = "1.3.0",
+ note = "use `FnPtr::call` to call a function pointer directly."
+ )]
+ #[inline(always)]
+ #[must_use]
+ #[allow(unused_variables)]
+ pub fn new(engine: &Engine, fn_name: &str, lib: &[SharedModule]) -> Self {
+ unimplemented!("`NativeCallContext::new` is deprecated");
+ }
+
+ /// Call a function inside the call context.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`call_fn_raw`][NativeCallContext::call_fn_raw] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.2.0", note = "use `call_fn_raw` instead")]
+ #[inline(always)]
+ pub fn call_fn_dynamic_raw(
+ &self,
+ fn_name: impl AsRef<str>,
+ is_method_call: bool,
+ args: &mut [&mut Dynamic],
+ ) -> RhaiResult {
+ self.call_fn_raw(fn_name.as_ref(), is_method_call, is_method_call, args)
+ }
+}
+
+#[allow(useless_deprecated)]
+#[deprecated(since = "1.2.0", note = "explicitly wrap `EvalAltResult` in `Err`")]
+impl<T> From<EvalAltResult> for RhaiResultOf<T> {
+ #[inline(always)]
+ fn from(err: EvalAltResult) -> Self {
+ Err(err.into())
+ }
+}
+
+impl FnPtr {
+ /// Get the number of curried arguments.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`curry().len()`][`FnPtr::curry`] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.8.0", note = "use `curry().len()` instead")]
+ #[inline(always)]
+ #[must_use]
+ pub fn num_curried(&self) -> usize {
+ self.curry().len()
+ }
+ /// Call the function pointer with curried arguments (if any).
+ /// The function may be script-defined (not available under `no_function`) or native Rust.
+ ///
+ /// This method is intended for calling a function pointer that is passed into a native Rust
+ /// function as an argument. Therefore, the [`AST`] is _NOT_ evaluated before calling the
+ /// function.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`call_within_context`][FnPtr::call_within_context] or
+ /// [`call_raw`][FnPtr::call_raw] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(
+ since = "1.3.0",
+ note = "use `call_within_context` or `call_raw` instead"
+ )]
+ #[inline(always)]
+ pub fn call_dynamic(
+ &self,
+ context: &NativeCallContext,
+ this_ptr: Option<&mut Dynamic>,
+ arg_values: impl AsMut<[Dynamic]>,
+ ) -> RhaiResult {
+ self.call_raw(context, this_ptr, arg_values)
+ }
+}
+
+#[cfg(not(feature = "no_custom_syntax"))]
+impl crate::Expression<'_> {
+ /// If this expression is a variable name, return it. Otherwise [`None`].
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`get_string_value`][crate::Expression::get_string_value] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.4.0", note = "use `get_string_value` instead")]
+ #[inline(always)]
+ #[must_use]
+ pub fn get_variable_name(&self) -> Option<&str> {
+ self.get_string_value()
+ }
+}
+
+impl Position {
+ /// Create a new [`Position`].
+ ///
+ /// If `line` is zero, then [`None`] is returned.
+ ///
+ /// If `position` is zero, then it is at the beginning of a line.
+ ///
+ /// # Deprecated
+ ///
+ /// This function is deprecated. Use [`new`][Position::new] (which panics when `line` is zero) instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.6.0", note = "use `new` instead")]
+ #[inline(always)]
+ #[must_use]
+ pub const fn new_const(line: u16, position: u16) -> Option<Self> {
+ if line == 0 {
+ None
+ } else {
+ Some(Self::new(line, position))
+ }
+ }
+}
+
+#[allow(deprecated)]
+impl<'a, T: Variant + Clone> TypeBuilder<'a, T> {
+ /// Register a custom fallible function.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use `with_fn` instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.9.1", note = "use `with_fn` instead")]
+ #[inline(always)]
+ pub fn with_result_fn<S, A: 'static, const N: usize, const C: bool, R, F>(
+ &mut self,
+ name: S,
+ method: F,
+ ) -> &mut Self
+ where
+ S: AsRef<str> + Into<Identifier>,
+ R: Variant + Clone,
+ F: RegisterNativeFunction<A, N, C, R, true>,
+ {
+ self.with_fn(name, method)
+ }
+
+ /// Register a fallible getter function.
+ ///
+ /// The function signature must start with `&mut self` and not `&self`.
+ ///
+ /// Not available under `no_object`.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use `with_get` instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.9.1", note = "use `with_get` instead")]
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ pub fn with_get_result<V: Variant + Clone, const C: bool>(
+ &mut self,
+ name: impl AsRef<str>,
+ get_fn: impl RegisterNativeFunction<(Mut<T>,), 1, C, V, true> + crate::func::SendSync + 'static,
+ ) -> &mut Self {
+ self.with_get(name, get_fn)
+ }
+
+ /// Register a fallible setter function.
+ ///
+ /// Not available under `no_object`.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use `with_set` instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.9.1", note = "use `with_set` instead")]
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ pub fn with_set_result<V: Variant + Clone, const C: bool>(
+ &mut self,
+ name: impl AsRef<str>,
+ set_fn: impl RegisterNativeFunction<(Mut<T>, V), 2, C, (), true>
+ + crate::func::SendSync
+ + 'static,
+ ) -> &mut Self {
+ self.with_set(name, set_fn)
+ }
+
+ /// Register an fallible index getter.
+ ///
+ /// The function signature must start with `&mut self` and not `&self`.
+ ///
+ /// Not available under both `no_index` and `no_object`.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use `with_indexer_get` instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.9.1", note = "use `with_indexer_get` instead")]
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ #[inline(always)]
+ pub fn with_indexer_get_result<X: Variant + Clone, V: Variant + Clone, const C: bool>(
+ &mut self,
+ get_fn: impl RegisterNativeFunction<(Mut<T>, X), 2, C, V, true>
+ + crate::func::SendSync
+ + 'static,
+ ) -> &mut Self {
+ self.with_indexer_get(get_fn)
+ }
+
+ /// Register an fallible index setter.
+ ///
+ /// Not available under both `no_index` and `no_object`.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use `with_indexer_set` instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.9.1", note = "use `with_indexer_set` instead")]
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ #[inline(always)]
+ pub fn with_indexer_set_result<X: Variant + Clone, V: Variant + Clone, const C: bool>(
+ &mut self,
+ set_fn: impl RegisterNativeFunction<(Mut<T>, X, V), 3, C, (), true>
+ + crate::func::SendSync
+ + 'static,
+ ) -> &mut Self {
+ self.with_indexer_set(set_fn)
+ }
+}
+
+impl Module {
+ /// Create a new [`Module`] with a pre-sized capacity for functions.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use `new` instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[inline(always)]
+ #[must_use]
+ #[deprecated(since = "1.12.0", note = "use `new` instead")]
+ pub fn with_capacity(_capacity: usize) -> Self {
+ Self::new()
+ }
+}
+
+#[cfg(not(feature = "no_index"))]
+use crate::plugin::*;
+
+#[cfg(not(feature = "no_index"))]
+#[export_module]
+pub mod deprecated_array_functions {
+ use crate::packages::array_basic::array_functions::*;
+ use crate::{Array, INT};
+
+ /// Iterate through all the elements in the array, applying a function named by `mapper` to each
+ /// element in turn, and return the results as a new array.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.map(Fn("fn_name"))` instead.
+ ///
+ /// # Function Parameters
+ ///
+ /// A function with the same name as the value of `mapper` must exist taking these parameters:
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn square(x) { x * x }
+ ///
+ /// fn multiply(x, i) { x * i }
+ ///
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.map("square");
+ ///
+ /// print(y); // prints "[1, 4, 9, 16, 25]"
+ ///
+ /// let y = x.map("multiply");
+ ///
+ /// print(y); // prints "[0, 2, 6, 12, 20]"
+ /// ```
+ #[rhai_fn(name = "map", return_raw)]
+ pub fn map_by_fn_name(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ mapper: &str,
+ ) -> RhaiResultOf<Array> {
+ map(ctx, array, FnPtr::new(mapper)?)
+ }
+ /// Iterate through all the elements in the array, applying a function named by `filter` to each
+ /// element in turn, and return a copy of all elements (in order) that return `true` as a new array.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.filter(Fn("fn_name"))` instead.
+ ///
+ /// # Function Parameters
+ ///
+ /// A function with the same name as the value of `filter` must exist taking these parameters:
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn screen(x, i) { x * i >= 10 }
+ ///
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.filter("is_odd");
+ ///
+ /// print(y); // prints "[1, 3, 5]"
+ ///
+ /// let y = x.filter("screen");
+ ///
+ /// print(y); // prints "[12, 20]"
+ /// ```
+ #[rhai_fn(name = "filter", return_raw)]
+ pub fn filter_by_fn_name(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ filter_func: &str,
+ ) -> RhaiResultOf<Array> {
+ filter(ctx, array, FnPtr::new(filter_func)?)
+ }
+ /// Iterate through all the elements in the array, applying a function named by `filter` to each
+ /// element in turn, and return the index of the first element that returns `true`.
+ /// If no element returns `true`, `-1` is returned.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.index_of(Fn("fn_name"))` instead.
+ ///
+ /// # Function Parameters
+ ///
+ /// A function with the same name as the value of `filter` must exist taking these parameters:
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn is_special(x) { x > 3 }
+ ///
+ /// fn is_dumb(x) { x > 8 }
+ ///
+ /// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+ ///
+ /// print(x.index_of("is_special")); // prints 3
+ ///
+ /// print(x.index_of("is_dumb")); // prints -1
+ /// ```
+ #[rhai_fn(name = "index_of", return_raw, pure)]
+ pub fn index_of_by_fn_name(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ filter: &str,
+ ) -> RhaiResultOf<INT> {
+ index_of_filter(ctx, array, FnPtr::new(filter)?)
+ }
+ /// Iterate through all the elements in the array, starting from a particular `start` position,
+ /// applying a function named by `filter` to each element in turn, and return the index of the
+ /// first element that returns `true`. If no element returns `true`, `-1` is returned.
+ ///
+ /// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `start` < -length of array, position counts from the beginning of the array.
+ /// * If `start` ≥ length of array, `-1` is returned.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.index_of(Fn("fn_name"), start)` instead.
+ ///
+ /// # Function Parameters
+ ///
+ /// A function with the same name as the value of `filter` must exist taking these parameters:
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn plural(x) { x > 1 }
+ ///
+ /// fn singular(x) { x < 2 }
+ ///
+ /// fn screen(x, i) { x * i > 20 }
+ ///
+ /// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+ ///
+ /// print(x.index_of("plural", 3)); // prints 5: 2 > 1
+ ///
+ /// print(x.index_of("singular", 9)); // prints -1: nothing < 2 past index 9
+ ///
+ /// print(x.index_of("plural", 15)); // prints -1: nothing found past end of array
+ ///
+ /// print(x.index_of("plural", -5)); // prints 9: -5 = start from index 8
+ ///
+ /// print(x.index_of("plural", -99)); // prints 1: -99 = start from beginning
+ ///
+ /// print(x.index_of("screen", 8)); // prints 10: 3 * 10 > 20
+ /// ```
+ #[rhai_fn(name = "index_of", return_raw, pure)]
+ pub fn index_of_by_fn_name_starting_from(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ filter: &str,
+ start: INT,
+ ) -> RhaiResultOf<INT> {
+ index_of_filter_starting_from(ctx, array, FnPtr::new(filter)?, start)
+ }
+ /// Return `true` if any element in the array that returns `true` when applied a function named
+ /// by `filter`.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.some(Fn("fn_name"))` instead.
+ ///
+ /// # Function Parameters
+ ///
+ /// A function with the same name as the value of `filter` must exist taking these parameters:
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn large(x) { x > 3 }
+ ///
+ /// fn huge(x) { x > 10 }
+ ///
+ /// fn screen(x, i) { i > x }
+ ///
+ /// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+ ///
+ /// print(x.some("large")); // prints true
+ ///
+ /// print(x.some("huge")); // prints false
+ ///
+ /// print(x.some("screen")); // prints true
+ /// ```
+ #[rhai_fn(name = "some", return_raw, pure)]
+ pub fn some_by_fn_name(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ filter: &str,
+ ) -> RhaiResultOf<bool> {
+ some(ctx, array, FnPtr::new(filter)?)
+ }
+ /// Return `true` if all elements in the array return `true` when applied a function named by `filter`.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.all(Fn("fn_name"))` instead.
+ ///
+ /// # Function Parameters
+ ///
+ /// A function with the same name as the value of `filter` must exist taking these parameters:
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+ ///
+ /// print(x.all(|v| v > 3)); // prints false
+ ///
+ /// print(x.all(|v| v > 1)); // prints true
+ ///
+ /// print(x.all(|v, i| i > v)); // prints false
+ /// ```
+ #[rhai_fn(name = "all", return_raw, pure)]
+ pub fn all_by_fn_name(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ filter: &str,
+ ) -> RhaiResultOf<bool> {
+ all(ctx, array, FnPtr::new(filter)?)
+ }
+ /// Remove duplicated _consecutive_ elements from the array that return `true` when applied a
+ /// function named by `comparer`.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.dedup(Fn("fn_name"))` instead.
+ ///
+ /// No element is removed if the correct `comparer` function does not exist.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element1`: copy of the current array element to compare
+ /// * `element2`: copy of the next array element to compare
+ ///
+ /// ## Return Value
+ ///
+ /// `true` if `element1 == element2`, otherwise `false`.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn declining(a, b) { a >= b }
+ ///
+ /// let x = [1, 2, 2, 2, 3, 1, 2, 3, 4, 3, 3, 2, 1];
+ ///
+ /// x.dedup("declining");
+ ///
+ /// print(x); // prints "[1, 2, 3, 4]"
+ /// ```
+ #[rhai_fn(name = "dedup", return_raw)]
+ pub fn dedup_by_fn_name(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ comparer: &str,
+ ) -> RhaiResultOf<()> {
+ Ok(dedup_by_comparer(ctx, array, FnPtr::new(comparer)?))
+ }
+ /// Reduce an array by iterating through all elements while applying a function named by `reducer`.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.reduce(Fn("fn_name"))` instead.
+ ///
+ /// # Function Parameters
+ ///
+ /// A function with the same name as the value of `reducer` must exist taking these parameters:
+ ///
+ /// * `result`: accumulated result, initially `()`
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn process(r, x) {
+ /// x + (r ?? 0)
+ /// }
+ /// fn process_extra(r, x, i) {
+ /// x + i + (r ?? 0)
+ /// }
+ ///
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.reduce("process");
+ ///
+ /// print(y); // prints 15
+ ///
+ /// let y = x.reduce("process_extra");
+ ///
+ /// print(y); // prints 25
+ /// ```
+ #[rhai_fn(name = "reduce", return_raw, pure)]
+ pub fn reduce_by_fn_name(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ reducer: &str,
+ ) -> RhaiResult {
+ reduce(ctx, array, FnPtr::new(reducer)?)
+ }
+ /// Reduce an array by iterating through all elements while applying a function named by `reducer`.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.reduce(Fn("fn_name"), initial)` instead.
+ ///
+ /// # Function Parameters
+ ///
+ /// A function with the same name as the value of `reducer` must exist taking these parameters:
+ ///
+ /// * `result`: accumulated result, starting with the value of `initial`
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn process(r, x) { x + r }
+ ///
+ /// fn process_extra(r, x, i) { x + i + r }
+ ///
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.reduce("process", 5);
+ ///
+ /// print(y); // prints 20
+ ///
+ /// let y = x.reduce("process_extra", 5);
+ ///
+ /// print(y); // prints 30
+ /// ```
+ #[rhai_fn(name = "reduce", return_raw, pure)]
+ pub fn reduce_by_fn_name_with_initial(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ reducer: &str,
+ initial: Dynamic,
+ ) -> RhaiResult {
+ reduce_with_initial(ctx, array, FnPtr::new(reducer)?, initial)
+ }
+ /// Reduce an array by iterating through all elements, in _reverse_ order,
+ /// while applying a function named by `reducer`.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.reduce_rev(Fn("fn_name"))` instead.
+ ///
+ /// # Function Parameters
+ ///
+ /// A function with the same name as the value of `reducer` must exist taking these parameters:
+ ///
+ /// * `result`: accumulated result, initially `()`
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn process(r, x) {
+ /// x + (r ?? 0)
+ /// }
+ /// fn process_extra(r, x, i) {
+ /// x + i + (r ?? 0)
+ /// }
+ ///
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.reduce_rev("process");
+ ///
+ /// print(y); // prints 15
+ ///
+ /// let y = x.reduce_rev("process_extra");
+ ///
+ /// print(y); // prints 25
+ /// ```
+ #[rhai_fn(name = "reduce_rev", return_raw, pure)]
+ pub fn reduce_rev_by_fn_name(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ reducer: &str,
+ ) -> RhaiResult {
+ reduce_rev(ctx, array, FnPtr::new(reducer)?)
+ }
+ /// Reduce an array by iterating through all elements, in _reverse_ order,
+ /// while applying a function named by `reducer`.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.reduce_rev(Fn("fn_name"), initial)` instead.
+ ///
+ /// # Function Parameters
+ ///
+ /// A function with the same name as the value of `reducer` must exist taking these parameters:
+ ///
+ /// * `result`: accumulated result, starting with the value of `initial`
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn process(r, x) { x + r }
+ ///
+ /// fn process_extra(r, x, i) { x + i + r }
+ ///
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.reduce_rev("process", 5);
+ ///
+ /// print(y); // prints 20
+ ///
+ /// let y = x.reduce_rev("process_extra", 5);
+ ///
+ /// print(y); // prints 30
+ /// ```
+ #[rhai_fn(name = "reduce_rev", return_raw, pure)]
+ pub fn reduce_rev_by_fn_name_with_initial(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ reducer: &str,
+ initial: Dynamic,
+ ) -> RhaiResult {
+ reduce_rev_with_initial(ctx, array, FnPtr::new(reducer)?, initial)
+ }
+ /// Sort the array based on applying a function named by `comparer`.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.sort(Fn("fn_name"))` instead.
+ ///
+ /// # Function Parameters
+ ///
+ /// A function with the same name as the value of `comparer` must exist taking these parameters:
+ ///
+ /// * `element1`: copy of the current array element to compare
+ /// * `element2`: copy of the next array element to compare
+ ///
+ /// ## Return Value
+ ///
+ /// * Any integer > 0 if `element1 > element2`
+ /// * Zero if `element1 == element2`
+ /// * Any integer < 0 if `element1 < element2`
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn reverse(a, b) {
+ /// if a > b {
+ /// -1
+ /// } else if a < b {
+ /// 1
+ /// } else {
+ /// 0
+ /// }
+ /// }
+ /// let x = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10];
+ ///
+ /// x.sort("reverse");
+ ///
+ /// print(x); // prints "[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]"
+ /// ```
+ #[rhai_fn(name = "sort", return_raw)]
+ pub fn sort_by_fn_name(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ comparer: &str,
+ ) -> RhaiResultOf<()> {
+ Ok(sort(ctx, array, FnPtr::new(comparer)?))
+ }
+ /// Remove all elements in the array that returns `true` when applied a function named by `filter`
+ /// and return them as a new array.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.drain(Fn("fn_name"))` instead.
+ ///
+ /// # Function Parameters
+ ///
+ /// A function with the same name as the value of `filter` must exist taking these parameters:
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn small(x) { x < 3 }
+ ///
+ /// fn screen(x, i) { x + i > 5 }
+ ///
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.drain("small");
+ ///
+ /// print(x); // prints "[3, 4, 5]"
+ ///
+ /// print(y); // prints "[1, 2]"
+ ///
+ /// let z = x.drain("screen");
+ ///
+ /// print(x); // prints "[3, 4]"
+ ///
+ /// print(z); // prints "[5]"
+ /// ```
+ #[rhai_fn(name = "drain", return_raw)]
+ pub fn drain_by_fn_name(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ filter: &str,
+ ) -> RhaiResultOf<Array> {
+ drain(ctx, array, FnPtr::new(filter)?)
+ }
+ /// Remove all elements in the array that do not return `true` when applied a function named by
+ /// `filter` and return them as a new array.
+ ///
+ /// # Deprecated API
+ ///
+ /// This method is deprecated and will be removed from the next major version.
+ /// Use `array.retain(Fn("fn_name"))` instead.
+ ///
+ /// # Function Parameters
+ ///
+ /// A function with the same name as the value of `filter` must exist taking these parameters:
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn large(x) { x >= 3 }
+ ///
+ /// fn screen(x, i) { x + i <= 5 }
+ ///
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.retain("large");
+ ///
+ /// print(x); // prints "[3, 4, 5]"
+ ///
+ /// print(y); // prints "[1, 2]"
+ ///
+ /// let z = x.retain("screen");
+ ///
+ /// print(x); // prints "[3, 4]"
+ ///
+ /// print(z); // prints "[5]"
+ /// ```
+ #[rhai_fn(name = "retain", return_raw)]
+ pub fn retain_by_fn_name(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ filter: &str,
+ ) -> RhaiResultOf<Array> {
+ retain(ctx, array, FnPtr::new(filter)?)
+ }
+}
diff --git a/rhai/src/api/eval.rs b/rhai/src/api/eval.rs
new file mode 100644
index 0000000..7ac42d1
--- /dev/null
+++ b/rhai/src/api/eval.rs
@@ -0,0 +1,292 @@
+//! Module that defines the public evaluation API of [`Engine`].
+
+use crate::eval::{Caches, GlobalRuntimeState};
+use crate::func::native::locked_write;
+use crate::parser::ParseState;
+use crate::types::dynamic::Variant;
+use crate::types::StringsInterner;
+use crate::{
+ Dynamic, Engine, OptimizationLevel, Position, RhaiResult, RhaiResultOf, Scope, AST, ERR,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ any::{type_name, TypeId},
+ mem,
+};
+
+impl Engine {
+ /// Evaluate a string as a script, returning the result value or an error.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// assert_eq!(engine.eval::<i64>("40 + 2")?, 42);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn eval<T: Variant + Clone>(&self, script: &str) -> RhaiResultOf<T> {
+ self.eval_with_scope(&mut Scope::new(), script)
+ }
+ /// Evaluate a string as a script with own scope, returning the result value or an error.
+ ///
+ /// ## Constants Propagation
+ ///
+ /// If not [`OptimizationLevel::None`][crate::OptimizationLevel::None], constants defined within
+ /// the scope are propagated throughout the script _including_ functions.
+ ///
+ /// This allows functions to be optimized based on dynamic global constants.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Scope};
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Create initialized scope
+ /// let mut scope = Scope::new();
+ /// scope.push("x", 40_i64);
+ ///
+ /// assert_eq!(engine.eval_with_scope::<i64>(&mut scope, "x += 2; x")?, 42);
+ /// assert_eq!(engine.eval_with_scope::<i64>(&mut scope, "x += 2; x")?, 44);
+ ///
+ /// // The variable in the scope is modified
+ /// assert_eq!(scope.get_value::<i64>("x").expect("variable x should exist"), 44);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn eval_with_scope<T: Variant + Clone>(
+ &self,
+ scope: &mut Scope,
+ script: &str,
+ ) -> RhaiResultOf<T> {
+ let ast = self.compile_with_scope_and_optimization_level(
+ Some(scope),
+ [script],
+ self.optimization_level,
+ )?;
+ self.eval_ast_with_scope(scope, &ast)
+ }
+ /// Evaluate a string containing an expression, returning the result value or an error.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// assert_eq!(engine.eval_expression::<i64>("40 + 2")?, 42);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn eval_expression<T: Variant + Clone>(&self, script: &str) -> RhaiResultOf<T> {
+ self.eval_expression_with_scope(&mut Scope::new(), script)
+ }
+ /// Evaluate a string containing an expression with own scope, returning the result value or an error.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Scope};
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Create initialized scope
+ /// let mut scope = Scope::new();
+ /// scope.push("x", 40_i64);
+ ///
+ /// assert_eq!(engine.eval_expression_with_scope::<i64>(&mut scope, "x + 2")?, 42);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn eval_expression_with_scope<T: Variant + Clone>(
+ &self,
+ scope: &mut Scope,
+ script: &str,
+ ) -> RhaiResultOf<T> {
+ let scripts = [script];
+ let ast = {
+ let mut interner;
+ let mut guard;
+ let interned_strings = if let Some(ref interner) = self.interned_strings {
+ guard = locked_write(interner);
+ &mut *guard
+ } else {
+ interner = StringsInterner::new();
+ &mut interner
+ };
+
+ let (stream, tc) = self.lex_raw(&scripts, self.token_mapper.as_deref());
+
+ let state = &mut ParseState::new(Some(scope), interned_strings, tc);
+
+ // No need to optimize a lone expression
+ self.parse_global_expr(
+ stream.peekable(),
+ state,
+ |_| {},
+ #[cfg(not(feature = "no_optimize"))]
+ OptimizationLevel::None,
+ #[cfg(feature = "no_optimize")]
+ OptimizationLevel::default(),
+ )?
+ };
+
+ self.eval_ast_with_scope(scope, &ast)
+ }
+ /// Evaluate an [`AST`], returning the result value or an error.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Compile a script to an AST and store it for later evaluation
+ /// let ast = engine.compile("40 + 2")?;
+ ///
+ /// // Evaluate it
+ /// assert_eq!(engine.eval_ast::<i64>(&ast)?, 42);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn eval_ast<T: Variant + Clone>(&self, ast: &AST) -> RhaiResultOf<T> {
+ self.eval_ast_with_scope(&mut Scope::new(), ast)
+ }
+ /// Evaluate an [`AST`] with own scope, returning the result value or an error.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Scope};
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Create initialized scope
+ /// let mut scope = Scope::new();
+ /// scope.push("x", 40_i64);
+ ///
+ /// // Compile a script to an AST and store it for later evaluation
+ /// let ast = engine.compile("x += 2; x")?;
+ ///
+ /// // Evaluate it
+ /// assert_eq!(engine.eval_ast_with_scope::<i64>(&mut scope, &ast)?, 42);
+ /// assert_eq!(engine.eval_ast_with_scope::<i64>(&mut scope, &ast)?, 44);
+ ///
+ /// // The variable in the scope is modified
+ /// assert_eq!(scope.get_value::<i64>("x").expect("variable x should exist"), 44);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn eval_ast_with_scope<T: Variant + Clone>(
+ &self,
+ scope: &mut Scope,
+ ast: &AST,
+ ) -> RhaiResultOf<T> {
+ let global = &mut GlobalRuntimeState::new(self);
+ let caches = &mut Caches::new();
+
+ let result = self.eval_ast_with_scope_raw(global, caches, scope, ast)?;
+
+ // Bail out early if the return type needs no cast
+ if TypeId::of::<T>() == TypeId::of::<Dynamic>() {
+ return Ok(reify! { result => T });
+ }
+
+ result.try_cast_raw::<T>().map_err(|v| {
+ let typename = match type_name::<T>() {
+ typ @ _ if typ.contains("::") => self.map_type_name(typ),
+ typ @ _ => typ,
+ };
+
+ ERR::ErrorMismatchOutputType(
+ typename.into(),
+ self.map_type_name(v.type_name()).into(),
+ Position::NONE,
+ )
+ .into()
+ })
+ }
+ /// Evaluate an [`AST`] with own scope, returning the result value or an error.
+ #[inline]
+ pub(crate) fn eval_ast_with_scope_raw(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ ast: &AST,
+ ) -> RhaiResult {
+ let orig_source = mem::replace(&mut global.source, ast.source_raw().cloned());
+
+ #[cfg(not(feature = "no_function"))]
+ let orig_lib_len = global.lib.len();
+
+ #[cfg(not(feature = "no_function"))]
+ global.lib.push(ast.shared_lib().clone());
+
+ #[cfg(not(feature = "no_module"))]
+ let orig_embedded_module_resolver = mem::replace(
+ &mut global.embedded_module_resolver,
+ ast.resolver().cloned(),
+ );
+
+ defer! { global => move |g| {
+ #[cfg(not(feature = "no_module"))]
+ {
+ g.embedded_module_resolver = orig_embedded_module_resolver;
+ }
+
+ #[cfg(not(feature = "no_function"))]
+ g.lib.truncate(orig_lib_len);
+
+ g.source = orig_source;
+ }}
+
+ let r = self.eval_global_statements(global, caches, scope, ast.statements())?;
+
+ #[cfg(feature = "debugging")]
+ if self.is_debugger_registered() {
+ global.debugger_mut().status = crate::eval::DebuggerStatus::Terminate;
+ let node = &crate::ast::Stmt::Noop(Position::NONE);
+ self.run_debugger(global, caches, scope, None, node)?;
+ }
+
+ Ok(r)
+ }
+}
+
+/// Evaluate a string as a script, returning the result value or an error.
+///
+/// # Example
+///
+/// ```
+/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+/// let result: i64 = rhai::eval("40 + 2")?;
+///
+/// assert_eq!(result, 42);
+/// # Ok(())
+/// # }
+/// ```
+#[inline(always)]
+pub fn eval<T: Variant + Clone>(script: &str) -> RhaiResultOf<T> {
+ Engine::new().eval(script)
+}
diff --git a/rhai/src/api/events.rs b/rhai/src/api/events.rs
new file mode 100644
index 0000000..7fd2daf
--- /dev/null
+++ b/rhai/src/api/events.rs
@@ -0,0 +1,369 @@
+//! Module that defines public event handlers for [`Engine`].
+
+use crate::func::SendSync;
+use crate::{Dynamic, Engine, EvalContext, Position, RhaiResultOf};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// Information on a variable definition.
+#[derive(Debug, Hash)]
+#[non_exhaustive]
+pub struct VarDefInfo<'a> {
+ /// Name of the variable to be defined.
+ pub name: &'a str,
+ /// `true` if the statement is `const`, otherwise it is `let`.
+ pub is_const: bool,
+ /// The current nesting level, with zero being the global level.
+ pub nesting_level: usize,
+ /// Will the variable _shadow_ an existing variable?
+ pub will_shadow: bool,
+}
+
+impl Engine {
+ /// Provide a callback that will be invoked before each variable access.
+ ///
+ /// # WARNING - Unstable API
+ ///
+ /// This API is volatile and may change in the future.
+ ///
+ /// # Callback Function Signature
+ ///
+ /// `Fn(name: &str, index: usize, context: EvalContext) -> Result<Option<Dynamic>, Box<EvalAltResult>>`
+ ///
+ /// where:
+ /// * `name`: name of the variable.
+ /// * `index`: an offset from the bottom of the current [`Scope`][crate::Scope] that the
+ /// variable is supposed to reside. Offsets start from 1, with 1 meaning the last variable in
+ /// the current [`Scope`][crate::Scope]. Essentially the correct variable is at position
+ /// `scope.len() - index`. If `index` is zero, then there is no pre-calculated offset position
+ /// and a search through the current [`Scope`][crate::Scope] must be performed.
+ /// * `context`: the current [evaluation context][`EvalContext`].
+ ///
+ /// ## Return value
+ ///
+ /// * `Ok(None)`: continue with normal variable access.
+ /// * `Ok(Some(Dynamic))`: the variable's value.
+ ///
+ /// ## Raising errors
+ ///
+ /// Return `Err(...)` if there is an error.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Register a variable resolver.
+ /// engine.on_var(|name, _, _| {
+ /// match name {
+ /// "MYSTIC_NUMBER" => Ok(Some(42_i64.into())),
+ /// _ => Ok(None)
+ /// }
+ /// });
+ ///
+ /// engine.eval::<i64>("MYSTIC_NUMBER")?;
+ ///
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[deprecated = "This API is NOT deprecated, but it is considered volatile and may change in the future."]
+ #[inline(always)]
+ pub fn on_var(
+ &mut self,
+ callback: impl Fn(&str, usize, EvalContext) -> RhaiResultOf<Option<Dynamic>>
+ + SendSync
+ + 'static,
+ ) -> &mut Self {
+ self.resolve_var = Some(Box::new(callback));
+ self
+ }
+ /// Provide a callback that will be invoked before the definition of each variable .
+ ///
+ /// # WARNING - Unstable API
+ ///
+ /// This API is volatile and may change in the future.
+ ///
+ /// # Callback Function Signature
+ ///
+ /// `Fn(is_runtime: bool, info: VarInfo, context: EvalContext) -> Result<bool, Box<EvalAltResult>>`
+ ///
+ /// where:
+ /// * `is_runtime`: `true` if the variable definition event happens during runtime, `false` if during compilation.
+ /// * `info`: information on the variable.
+ /// * `context`: the current [evaluation context][`EvalContext`].
+ ///
+ /// ## Return value
+ ///
+ /// * `Ok(true)`: continue with normal variable definition.
+ /// * `Ok(false)`: deny the variable definition with an [runtime error][crate::EvalAltResult::ErrorRuntime].
+ ///
+ /// ## Raising errors
+ ///
+ /// Return `Err(...)` if there is an error.
+ ///
+ /// # Example
+ ///
+ /// ```should_panic
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Register a variable definition filter.
+ /// engine.on_def_var(|_, info, _| {
+ /// // Disallow defining MYSTIC_NUMBER as a constant
+ /// if info.name == "MYSTIC_NUMBER" && info.is_const {
+ /// Ok(false)
+ /// } else {
+ /// Ok(true)
+ /// }
+ /// });
+ ///
+ /// // The following runs fine:
+ /// engine.eval::<i64>("let MYSTIC_NUMBER = 42;")?;
+ ///
+ /// // The following will cause an error:
+ /// engine.eval::<i64>("const MYSTIC_NUMBER = 42;")?;
+ ///
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[deprecated = "This API is NOT deprecated, but it is considered volatile and may change in the future."]
+ #[inline(always)]
+ pub fn on_def_var(
+ &mut self,
+ callback: impl Fn(bool, VarDefInfo, EvalContext) -> RhaiResultOf<bool> + SendSync + 'static,
+ ) -> &mut Self {
+ self.def_var_filter = Some(Box::new(callback));
+ self
+ }
+ /// _(internals)_ Register a callback that will be invoked during parsing to remap certain tokens.
+ /// Exported under the `internals` feature only.
+ ///
+ /// # WARNING - Unstable API
+ ///
+ /// This API is volatile and may change in the future.
+ ///
+ /// # Callback Function Signature
+ ///
+ /// `Fn(token: Token, pos: Position, state: &TokenizeState) -> Token`
+ ///
+ /// where:
+ /// * [`token`][crate::tokenizer::Token]: current token parsed
+ /// * [`pos`][`Position`]: location of the token
+ /// * [`state`][crate::tokenizer::TokenizeState]: current state of the tokenizer
+ ///
+ /// ## Raising errors
+ ///
+ /// It is possible to raise a parsing error by returning
+ /// [`Token::LexError`][crate::tokenizer::Token::LexError] as the mapped token.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Token};
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Register a token mapper.
+ /// # #[allow(deprecated)]
+ /// engine.on_parse_token(|token, _, _| {
+ /// match token {
+ /// // Convert all integer literals to strings
+ /// Token::IntegerConstant(n) => Token::StringConstant(Box::new(n.to_string().into())),
+ /// // Convert 'begin' .. 'end' to '{' .. '}'
+ /// Token::Identifier(s) if &*s == "begin" => Token::LeftBrace,
+ /// Token::Identifier(s) if &*s == "end" => Token::RightBrace,
+ /// // Pass through all other tokens unchanged
+ /// _ => token
+ /// }
+ /// });
+ ///
+ /// assert_eq!(engine.eval::<String>("42")?, "42");
+ /// assert_eq!(engine.eval::<bool>("true")?, true);
+ /// assert_eq!(engine.eval::<String>("let x = 42; begin let x = 0; end; x")?, "42");
+ ///
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[deprecated = "This API is NOT deprecated, but it is considered volatile and may change in the future."]
+ #[cfg(feature = "internals")]
+ #[inline(always)]
+ pub fn on_parse_token(
+ &mut self,
+ callback: impl Fn(
+ crate::tokenizer::Token,
+ Position,
+ &crate::tokenizer::TokenizeState,
+ ) -> crate::tokenizer::Token
+ + SendSync
+ + 'static,
+ ) -> &mut Self {
+ self.token_mapper = Some(Box::new(callback));
+ self
+ }
+ /// Register a callback for script evaluation progress.
+ ///
+ /// Not available under `unchecked`.
+ ///
+ /// # Callback Function Signature
+ ///
+ /// `Fn(counter: u64) -> Option<Dynamic>`
+ ///
+ /// ## Return value
+ ///
+ /// * `None`: continue running the script.
+ /// * `Some(Dynamic)`: terminate the script with the specified exception value.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// # use std::sync::RwLock;
+ /// # use std::sync::Arc;
+ /// use rhai::Engine;
+ ///
+ /// let result = Arc::new(RwLock::new(0_u64));
+ /// let logger = result.clone();
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// engine.on_progress(move |ops| {
+ /// if ops > 1000 {
+ /// Some("Over 1,000 operations!".into())
+ /// } else if ops % 123 == 0 {
+ /// *logger.write().unwrap() = ops;
+ /// None
+ /// } else {
+ /// None
+ /// }
+ /// });
+ ///
+ /// engine.run("for x in 0..5000 { print(x); }")
+ /// .expect_err("should error");
+ ///
+ /// assert_eq!(*result.read().unwrap(), 984);
+ ///
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[cfg(not(feature = "unchecked"))]
+ #[inline(always)]
+ pub fn on_progress(
+ &mut self,
+ callback: impl Fn(u64) -> Option<Dynamic> + SendSync + 'static,
+ ) -> &mut Self {
+ self.progress = Some(Box::new(callback));
+ self
+ }
+ /// Override default action of `print` (print to stdout using [`println!`])
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// # use std::sync::RwLock;
+ /// # use std::sync::Arc;
+ /// use rhai::Engine;
+ ///
+ /// let result = Arc::new(RwLock::new(String::new()));
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Override action of 'print' function
+ /// let logger = result.clone();
+ /// engine.on_print(move |s| logger.write().unwrap().push_str(s));
+ ///
+ /// engine.run("print(40 + 2);")?;
+ ///
+ /// assert_eq!(*result.read().unwrap(), "42");
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn on_print(&mut self, callback: impl Fn(&str) + SendSync + 'static) -> &mut Self {
+ self.print = Some(Box::new(callback));
+ self
+ }
+ /// Override default action of `debug` (print to stdout using [`println!`])
+ ///
+ /// # Callback Function Signature
+ ///
+ /// The callback function signature passed takes the following form:
+ ///
+ /// `Fn(text: &str, source: Option<&str>, pos: Position)`
+ ///
+ /// where:
+ /// * `text`: the text to display
+ /// * `source`: current source, if any
+ /// * [`pos`][`Position`]: location of the `debug` call
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// # use std::sync::RwLock;
+ /// # use std::sync::Arc;
+ /// use rhai::Engine;
+ ///
+ /// let result = Arc::new(RwLock::new(String::new()));
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Override action of 'print' function
+ /// let logger = result.clone();
+ /// engine.on_debug(move |s, src, pos| logger.write().unwrap().push_str(
+ /// &format!("{} @ {:?} > {}", src.unwrap_or("unknown"), pos, s)
+ /// ));
+ ///
+ /// let mut ast = engine.compile(r#"let x = "hello"; debug(x);"#)?;
+ /// ast.set_source("world");
+ /// engine.run_ast(&ast)?;
+ ///
+ /// #[cfg(not(feature = "no_position"))]
+ /// assert_eq!(*result.read().unwrap(), r#"world @ 1:18 > "hello""#);
+ /// #[cfg(feature = "no_position")]
+ /// assert_eq!(*result.read().unwrap(), r#"world @ none > "hello""#);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn on_debug(
+ &mut self,
+ callback: impl Fn(&str, Option<&str>, Position) + SendSync + 'static,
+ ) -> &mut Self {
+ self.debug = Some(Box::new(callback));
+ self
+ }
+ /// _(debugging)_ Register a callback for debugging.
+ /// Exported under the `debugging` feature only.
+ ///
+ /// # WARNING - Unstable API
+ ///
+ /// This API is volatile and may change in the future.
+ #[deprecated = "This API is NOT deprecated, but it is considered volatile and may change in the future."]
+ #[cfg(feature = "debugging")]
+ #[inline(always)]
+ pub fn register_debugger(
+ &mut self,
+ init: impl Fn(&Self, crate::debugger::Debugger) -> crate::debugger::Debugger
+ + SendSync
+ + 'static,
+ callback: impl Fn(
+ EvalContext,
+ crate::eval::DebuggerEvent,
+ crate::ast::ASTNode,
+ Option<&str>,
+ Position,
+ ) -> RhaiResultOf<crate::eval::DebuggerCommand>
+ + SendSync
+ + 'static,
+ ) -> &mut Self {
+ self.debugger_interface = Some((Box::new(init), Box::new(callback)));
+ self
+ }
+}
diff --git a/rhai/src/api/files.rs b/rhai/src/api/files.rs
new file mode 100644
index 0000000..8ebe85a
--- /dev/null
+++ b/rhai/src/api/files.rs
@@ -0,0 +1,260 @@
+//! Module that defines the public file-based API of [`Engine`].
+#![cfg(not(target_vendor = "teaclave"))]
+#![cfg(not(feature = "no_std"))]
+#![cfg(not(target_family = "wasm"))]
+
+use crate::types::dynamic::Variant;
+use crate::{Engine, RhaiResultOf, Scope, AST, ERR};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ fs::File,
+ io::Read,
+ path::{Path, PathBuf},
+};
+
+impl Engine {
+ /// Read the contents of a file into a string.
+ fn read_file(path: impl AsRef<Path>) -> RhaiResultOf<String> {
+ let path = path.as_ref();
+
+ let mut f = File::open(path).map_err(|err| {
+ ERR::ErrorSystem(
+ format!("Cannot open script file '{}'", path.to_string_lossy()),
+ err.into(),
+ )
+ })?;
+
+ let mut contents = String::new();
+
+ f.read_to_string(&mut contents).map_err(|err| {
+ ERR::ErrorSystem(
+ format!("Cannot read script file '{}'", path.to_string_lossy()),
+ err.into(),
+ )
+ })?;
+
+ if contents.starts_with("#!") {
+ // Remove shebang
+ if let Some(n) = contents.find('\n') {
+ contents.drain(0..n).count();
+ } else {
+ contents.clear();
+ }
+ };
+
+ Ok(contents)
+ }
+ /// Compile a script file into an [`AST`], which can be used later for evaluation.
+ ///
+ /// Not available under `no_std` or `WASM`.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Compile a script file to an AST and store it for later evaluation.
+ /// // Notice that a PathBuf is required which can easily be constructed from a string.
+ /// let ast = engine.compile_file("script.rhai".into())?;
+ ///
+ /// for _ in 0..42 {
+ /// engine.eval_ast::<i64>(&ast)?;
+ /// }
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn compile_file(&self, path: PathBuf) -> RhaiResultOf<AST> {
+ self.compile_file_with_scope(&Scope::new(), path)
+ }
+ /// Compile a script file into an [`AST`] using own scope, which can be used later for evaluation.
+ ///
+ /// Not available under `no_std` or `WASM`.
+ ///
+ /// ## Constants Propagation
+ ///
+ /// If not [`OptimizationLevel::None`][crate::OptimizationLevel::None], constants defined within
+ /// the scope are propagated throughout the script _including_ functions.
+ ///
+ /// This allows functions to be optimized based on dynamic global constants.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// # #[cfg(not(feature = "no_optimize"))]
+ /// # {
+ /// use rhai::{Engine, Scope, OptimizationLevel};
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Create initialized scope
+ /// let mut scope = Scope::new();
+ /// scope.push_constant("x", 42_i64); // 'x' is a constant
+ ///
+ /// // Compile a script to an AST and store it for later evaluation.
+ /// // Notice that a PathBuf is required which can easily be constructed from a string.
+ /// let ast = engine.compile_file_with_scope(&scope, "script.rhai".into())?;
+ ///
+ /// let result = engine.eval_ast::<i64>(&ast)?;
+ /// # }
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn compile_file_with_scope(&self, scope: &Scope, path: PathBuf) -> RhaiResultOf<AST> {
+ Self::read_file(&path).and_then(|contents| {
+ let mut ast = self.compile_with_scope(scope, contents)?;
+ ast.set_source(path.to_string_lossy().as_ref());
+ Ok(ast)
+ })
+ }
+ /// Evaluate a script file, returning the result value or an error.
+ ///
+ /// Not available under `no_std` or `WASM`.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Notice that a PathBuf is required which can easily be constructed from a string.
+ /// let result = engine.eval_file::<i64>("script.rhai".into())?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn eval_file<T: Variant + Clone>(&self, path: PathBuf) -> RhaiResultOf<T> {
+ Self::read_file(path).and_then(|contents| self.eval::<T>(&contents))
+ }
+ /// Evaluate a script file with own scope, returning the result value or an error.
+ ///
+ /// Not available under `no_std` or `WASM`.
+ ///
+ /// ## Constants Propagation
+ ///
+ /// If not [`OptimizationLevel::None`][crate::OptimizationLevel::None], constants defined within
+ /// the scope are propagated throughout the script _including_ functions.
+ ///
+ /// This allows functions to be optimized based on dynamic global constants.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Scope};
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Create initialized scope
+ /// let mut scope = Scope::new();
+ /// scope.push("x", 42_i64);
+ ///
+ /// // Notice that a PathBuf is required which can easily be constructed from a string.
+ /// let result = engine.eval_file_with_scope::<i64>(&mut scope, "script.rhai".into())?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn eval_file_with_scope<T: Variant + Clone>(
+ &self,
+ scope: &mut Scope,
+ path: PathBuf,
+ ) -> RhaiResultOf<T> {
+ Self::read_file(path).and_then(|contents| self.eval_with_scope(scope, &contents))
+ }
+ /// Evaluate a file.
+ ///
+ /// Not available under `no_std` or `WASM`.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Notice that a PathBuf is required which can easily be constructed from a string.
+ /// engine.run_file("script.rhai".into())?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn run_file(&self, path: PathBuf) -> RhaiResultOf<()> {
+ Self::read_file(path).and_then(|contents| self.run(&contents))
+ }
+ /// Evaluate a file with own scope.
+ ///
+ /// Not available under `no_std` or `WASM`.
+ ///
+ /// ## Constants Propagation
+ ///
+ /// If not [`OptimizationLevel::None`][crate::OptimizationLevel::None], constants defined within
+ /// the scope are propagated throughout the script _including_ functions.
+ ///
+ /// This allows functions to be optimized based on dynamic global constants.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Scope};
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Create initialized scope
+ /// let mut scope = Scope::new();
+ /// scope.push("x", 42_i64);
+ ///
+ /// // Notice that a PathBuf is required which can easily be constructed from a string.
+ /// engine.run_file_with_scope(&mut scope, "script.rhai".into())?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn run_file_with_scope(&self, scope: &mut Scope, path: PathBuf) -> RhaiResultOf<()> {
+ Self::read_file(path).and_then(|contents| self.run_with_scope(scope, &contents))
+ }
+}
+
+/// Evaluate a script file, returning the result value or an error.
+///
+/// Not available under `no_std` or `WASM`.
+///
+/// # Example
+///
+/// ```no_run
+/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+/// let result = rhai::eval_file::<i64>("script.rhai")?;
+/// # Ok(())
+/// # }
+/// ```
+#[inline]
+pub fn eval_file<T: Variant + Clone>(path: impl AsRef<Path>) -> RhaiResultOf<T> {
+ Engine::read_file(path).and_then(|contents| Engine::new().eval::<T>(&contents))
+}
+
+/// Evaluate a file.
+///
+/// Not available under `no_std` or `WASM`.
+///
+/// # Example
+///
+/// ```no_run
+/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+/// rhai::run_file("script.rhai")?;
+/// # Ok(())
+/// # }
+/// ```
+#[inline]
+pub fn run_file(path: impl AsRef<Path>) -> RhaiResultOf<()> {
+ Engine::read_file(path).and_then(|contents| Engine::new().run(&contents))
+}
diff --git a/rhai/src/api/formatting.rs b/rhai/src/api/formatting.rs
new file mode 100644
index 0000000..7fb7a37
--- /dev/null
+++ b/rhai/src/api/formatting.rs
@@ -0,0 +1,298 @@
+//! Module that provide formatting services to the [`Engine`].
+use crate::packages::iter_basic::{BitRange, CharsStream, StepRange};
+use crate::parser::{ParseResult, ParseState};
+use crate::types::StringsInterner;
+use crate::{
+ Engine, ExclusiveRange, FnPtr, ImmutableString, InclusiveRange, Position, RhaiError,
+ SmartString, ERR,
+};
+use std::any::type_name;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// Map the name of a standard type into a friendly form.
+#[inline]
+#[must_use]
+fn map_std_type_name(name: &str, shorthands: bool) -> &str {
+ let name = name.trim();
+
+ if name == type_name::<String>() {
+ return if shorthands { "string" } else { "String" };
+ }
+ if name == type_name::<ImmutableString>() || name == "ImmutableString" {
+ return if shorthands {
+ "string"
+ } else {
+ "ImmutableString"
+ };
+ }
+ if name == type_name::<&str>() {
+ return if shorthands { "string" } else { "&str" };
+ }
+ #[cfg(feature = "decimal")]
+ if name == type_name::<rust_decimal::Decimal>() {
+ return if shorthands { "decimal" } else { "Decimal" };
+ }
+ if name == type_name::<FnPtr>() || name == "FnPtr" {
+ return if shorthands { "Fn" } else { "FnPtr" };
+ }
+ #[cfg(not(feature = "no_index"))]
+ if name == type_name::<crate::Array>() || name == "Array" {
+ return if shorthands { "array" } else { "Array" };
+ }
+ #[cfg(not(feature = "no_index"))]
+ if name == type_name::<crate::Blob>() || name == "Blob" {
+ return if shorthands { "blob" } else { "Blob" };
+ }
+ #[cfg(not(feature = "no_object"))]
+ if name == type_name::<crate::Map>() || name == "Map" {
+ return if shorthands { "map" } else { "Map" };
+ }
+ #[cfg(not(feature = "no_time"))]
+ if name == type_name::<crate::Instant>() || name == "Instant" {
+ return if shorthands { "timestamp" } else { "Instant" };
+ }
+ if name == type_name::<ExclusiveRange>() || name == "ExclusiveRange" {
+ return if shorthands {
+ "range"
+ } else if cfg!(feature = "only_i32") {
+ "Range<i32>"
+ } else {
+ "Range<i64>"
+ };
+ }
+ if name == type_name::<InclusiveRange>() || name == "InclusiveRange" {
+ return if shorthands {
+ "range="
+ } else if cfg!(feature = "only_i32") {
+ "RangeInclusive<i32>"
+ } else {
+ "RangeInclusive<i64>"
+ };
+ }
+ if name == type_name::<BitRange>() {
+ return if shorthands { "range" } else { "BitRange" };
+ }
+ if name == type_name::<CharsStream>() {
+ return if shorthands { "range" } else { "CharStream" };
+ }
+
+ let step_range_name = type_name::<StepRange<u8>>();
+ let step_range_name = &step_range_name[..step_range_name.len() - 3];
+
+ if name.starts_with(step_range_name) && name.ends_with('>') {
+ return if shorthands {
+ "range"
+ } else {
+ let step_range_name = step_range_name.split("::").last().unwrap();
+ &step_range_name[..step_range_name.len() - 1]
+ };
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ if name == type_name::<crate::packages::iter_basic::StepRange<crate::FLOAT>>() {
+ return if shorthands {
+ "range"
+ } else {
+ "StepFloatRange"
+ };
+ }
+ #[cfg(feature = "decimal")]
+ if name == type_name::<crate::packages::iter_basic::StepRange<rust_decimal::Decimal>>() {
+ return if shorthands {
+ "range"
+ } else {
+ "StepDecimalRange"
+ };
+ }
+
+ name.strip_prefix("rhai::")
+ .map_or(name, |s| map_std_type_name(s, shorthands))
+}
+
+/// Format a Rust type to be display-friendly.
+///
+/// * `rhai::` prefix is cleared.
+/// * `()` is cleared.
+/// * `&mut` is cleared.
+/// * `INT` and `FLOAT` are expanded.
+/// * [`RhaiResult`][crate::RhaiResult] and [`RhaiResultOf<T>`][crate::RhaiResultOf] are expanded.
+#[cfg(feature = "metadata")]
+pub fn format_type(typ: &str, is_return_type: bool) -> std::borrow::Cow<str> {
+ const RESULT_TYPE: &str = "Result<";
+ const ERROR_TYPE: &str = ",Box<EvalAltResult>>";
+ const RHAI_RESULT_TYPE: &str = "RhaiResult";
+ const RHAI_RESULT_TYPE_EXPAND: &str = "Result<Dynamic, Box<EvalAltResult>>";
+ const RHAI_RESULT_OF_TYPE: &str = "RhaiResultOf<";
+ const RHAI_RESULT_OF_TYPE_EXPAND: &str = "Result<{}, Box<EvalAltResult>>";
+ const RHAI_RANGE: &str = "ExclusiveRange";
+ const RHAI_RANGE_TYPE: &str = "Range<";
+ const RHAI_RANGE_EXPAND: &str = "Range<{}>";
+ const RHAI_INCLUSIVE_RANGE: &str = "InclusiveRange";
+ const RHAI_INCLUSIVE_RANGE_TYPE: &str = "RangeInclusive<";
+ const RHAI_INCLUSIVE_RANGE_EXPAND: &str = "RangeInclusive<{}>";
+
+ let typ = typ.trim();
+
+ if let Some(x) = typ.strip_prefix("rhai::") {
+ return format_type(x, is_return_type);
+ } else if let Some(x) = typ.strip_prefix("&mut ") {
+ let r = format_type(x, false);
+ return if r == x {
+ typ.into()
+ } else {
+ format!("&mut {r}").into()
+ };
+ } else if typ.contains(' ') {
+ let typ = typ.replace(' ', "");
+ let r = format_type(&typ, is_return_type);
+ return r.into_owned().into();
+ }
+
+ match typ {
+ "" | "()" if is_return_type => "".into(),
+ "INT" => std::any::type_name::<crate::INT>().into(),
+ #[cfg(not(feature = "no_float"))]
+ "FLOAT" => std::any::type_name::<crate::FLOAT>().into(),
+ RHAI_RANGE => RHAI_RANGE_EXPAND
+ .replace("{}", std::any::type_name::<crate::INT>())
+ .into(),
+ RHAI_INCLUSIVE_RANGE => RHAI_INCLUSIVE_RANGE_EXPAND
+ .replace("{}", std::any::type_name::<crate::INT>())
+ .into(),
+ RHAI_RESULT_TYPE => RHAI_RESULT_TYPE_EXPAND.into(),
+ ty if ty.starts_with(RHAI_RANGE_TYPE) && ty.ends_with('>') => {
+ let inner = &ty[RHAI_RANGE_TYPE.len()..ty.len() - 1];
+ RHAI_RANGE_EXPAND
+ .replace("{}", format_type(inner, false).trim())
+ .into()
+ }
+ ty if ty.starts_with(RHAI_INCLUSIVE_RANGE_TYPE) && ty.ends_with('>') => {
+ let inner = &ty[RHAI_INCLUSIVE_RANGE_TYPE.len()..ty.len() - 1];
+ RHAI_INCLUSIVE_RANGE_EXPAND
+ .replace("{}", format_type(inner, false).trim())
+ .into()
+ }
+ ty if ty.starts_with(RHAI_RESULT_OF_TYPE) && ty.ends_with('>') => {
+ let inner = &ty[RHAI_RESULT_OF_TYPE.len()..ty.len() - 1];
+ RHAI_RESULT_OF_TYPE_EXPAND
+ .replace("{}", format_type(inner, false).trim())
+ .into()
+ }
+ ty if ty.starts_with(RESULT_TYPE) && ty.ends_with(ERROR_TYPE) => {
+ let inner = &ty[RESULT_TYPE.len()..ty.len() - ERROR_TYPE.len()];
+ RHAI_RESULT_OF_TYPE_EXPAND
+ .replace("{}", format_type(inner, false).trim())
+ .into()
+ }
+ ty => ty.into(),
+ }
+}
+
+impl Engine {
+ /// Pretty-print a type name.
+ ///
+ /// If a type is registered via [`register_type_with_name`][Engine::register_type_with_name],
+ /// the type name provided for the registration will be used.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the type name is `&mut`.
+ #[inline]
+ #[must_use]
+ pub fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str {
+ self.global_modules
+ .iter()
+ .find_map(|m| m.get_custom_type(name))
+ .or_else(|| {
+ #[cfg(not(feature = "no_module"))]
+ return self
+ .global_sub_modules
+ .as_ref()
+ .into_iter()
+ .flatten()
+ .find_map(|(_, m)| m.get_custom_type(name));
+ #[cfg(feature = "no_module")]
+ return None;
+ })
+ .unwrap_or_else(|| map_std_type_name(name, true))
+ }
+
+ /// Format a type name.
+ ///
+ /// If a type is registered via [`register_type_with_name`][Engine::register_type_with_name],
+ /// the type name provided for the registration will be used.
+ #[cfg(feature = "metadata")]
+ #[inline]
+ #[must_use]
+ pub(crate) fn format_type_name<'a>(&'a self, name: &'a str) -> std::borrow::Cow<'a, str> {
+ if let Some(x) = name.strip_prefix("&mut ") {
+ let r = self.format_type_name(x);
+
+ return if x == r {
+ name.into()
+ } else {
+ format!("&mut {r}").into()
+ };
+ }
+
+ self.global_modules
+ .iter()
+ .find_map(|m| m.get_custom_type(name))
+ .or_else(|| {
+ #[cfg(not(feature = "no_module"))]
+ return self
+ .global_sub_modules
+ .as_ref()
+ .into_iter()
+ .flatten()
+ .find_map(|(_, m)| m.get_custom_type(name));
+ #[cfg(feature = "no_module")]
+ return None;
+ })
+ .unwrap_or_else(|| match name {
+ "INT" => type_name::<crate::INT>(),
+ #[cfg(not(feature = "no_float"))]
+ "FLOAT" => type_name::<crate::FLOAT>(),
+ _ => map_std_type_name(name, false),
+ })
+ .into()
+ }
+
+ /// Make a `Box<`[`EvalAltResult<ErrorMismatchDataType>`][ERR::ErrorMismatchDataType]`>`.
+ #[cold]
+ #[inline(never)]
+ #[must_use]
+ pub(crate) fn make_type_mismatch_err<T>(&self, typ: &str, pos: Position) -> RhaiError {
+ let t = self.map_type_name(type_name::<T>()).into();
+ ERR::ErrorMismatchDataType(t, typ.into(), pos).into()
+ }
+
+ /// Compact a script to eliminate insignificant whitespaces and comments.
+ ///
+ /// This is useful to prepare a script for further compressing.
+ ///
+ /// The output script is semantically identical to the input script, except smaller in size.
+ ///
+ /// Unlike other uglifiers and minifiers, this method does not rename variables nor perform any
+ /// optimization on the input script.
+ #[inline]
+ pub fn compact_script(&self, script: impl AsRef<str>) -> ParseResult<String> {
+ let scripts = [script];
+ let (mut stream, tc) = self.lex_raw(&scripts, self.token_mapper.as_deref());
+ tc.borrow_mut().compressed = Some(String::new());
+ stream.state.last_token = Some(SmartString::new_const());
+ let mut interner = StringsInterner::new();
+ let mut state = ParseState::new(None, &mut interner, tc);
+ let mut _ast = self.parse(
+ stream.peekable(),
+ &mut state,
+ #[cfg(not(feature = "no_optimize"))]
+ crate::OptimizationLevel::None,
+ #[cfg(feature = "no_optimize")]
+ (),
+ )?;
+ let tc = state.tokenizer_control.borrow();
+ Ok(tc.compressed.as_ref().unwrap().into())
+ }
+}
diff --git a/rhai/src/api/json.rs b/rhai/src/api/json.rs
new file mode 100644
index 0000000..ee266d2
--- /dev/null
+++ b/rhai/src/api/json.rs
@@ -0,0 +1,189 @@
+//! Module that defines JSON manipulation functions for [`Engine`].
+#![cfg(not(feature = "no_object"))]
+
+use crate::func::native::locked_write;
+use crate::parser::{ParseSettingFlags, ParseState};
+use crate::tokenizer::Token;
+use crate::types::StringsInterner;
+use crate::{Engine, LexError, Map, OptimizationLevel, RhaiResultOf};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+impl Engine {
+ /// Parse a JSON string into an [object map][Map].
+ ///
+ /// This is a light-weight alternative to using, say, [`serde_json`](https://crates.io/crates/serde_json)
+ /// to deserialize the JSON.
+ ///
+ /// Not available under `no_object`.
+ ///
+ /// The JSON string must be an object hash. It cannot be a simple primitive value.
+ ///
+ /// Set `has_null` to `true` in order to map `null` values to `()`.
+ /// Setting it to `false` causes a syntax error for any `null` value.
+ ///
+ /// JSON sub-objects are handled transparently.
+ ///
+ /// This function can be used together with [`format_map_as_json`] to work with JSON texts
+ /// without using the [`serde_json`](https://crates.io/crates/serde_json) crate (which is heavy).
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Map};
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// let map = engine.parse_json(r#"
+ /// {
+ /// "a": 123,
+ /// "b": 42,
+ /// "c": {
+ /// "x": false,
+ /// "y": true,
+ /// "z": '$'
+ /// },
+ /// "d": null
+ /// }"#, true)?;
+ ///
+ /// assert_eq!(map.len(), 4);
+ /// assert_eq!(map["a"].as_int().expect("a should exist"), 123);
+ /// assert_eq!(map["b"].as_int().expect("b should exist"), 42);
+ /// assert_eq!(map["d"].as_unit().expect("d should exist"), ());
+ ///
+ /// let c = map["c"].read_lock::<Map>().expect("c should exist");
+ /// assert_eq!(c["x"].as_bool().expect("x should be bool"), false);
+ /// assert_eq!(c["y"].as_bool().expect("y should be bool"), true);
+ /// assert_eq!(c["z"].as_char().expect("z should be char"), '$');
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn parse_json(&self, json: impl AsRef<str>, has_null: bool) -> RhaiResultOf<Map> {
+ let scripts = [json.as_ref()];
+
+ let (stream, tokenizer_control) = self.lex_raw(
+ &scripts,
+ Some(if has_null {
+ &|token, _, _| {
+ match token {
+ // `null` => `()`
+ Token::Reserved(s) if &*s == "null" => Token::Unit,
+ // `{` => `#{`
+ Token::LeftBrace => Token::MapStart,
+ // Disallowed syntax
+ t @ (Token::Unit | Token::MapStart) => Token::LexError(
+ LexError::ImproperSymbol(t.literal_syntax().to_string(), String::new())
+ .into(),
+ ),
+ Token::InterpolatedString(..) => Token::LexError(
+ LexError::ImproperSymbol(
+ "interpolated string".to_string(),
+ String::new(),
+ )
+ .into(),
+ ),
+ // All others
+ _ => token,
+ }
+ }
+ } else {
+ &|token, _, _| {
+ match token {
+ Token::Reserved(s) if &*s == "null" => Token::LexError(
+ LexError::ImproperSymbol("null".to_string(), String::new()).into(),
+ ),
+ // `{` => `#{`
+ Token::LeftBrace => Token::MapStart,
+ // Disallowed syntax
+ t @ (Token::Unit | Token::MapStart) => Token::LexError(
+ LexError::ImproperSymbol(t.literal_syntax().to_string(), String::new())
+ .into(),
+ ),
+ Token::InterpolatedString(..) => Token::LexError(
+ LexError::ImproperSymbol(
+ "interpolated string".to_string(),
+ String::new(),
+ )
+ .into(),
+ ),
+ // All others
+ _ => token,
+ }
+ }
+ }),
+ );
+
+ let ast = {
+ let mut interner;
+ let mut guard;
+ let interned_strings = if let Some(ref interner) = self.interned_strings {
+ guard = locked_write(interner);
+ &mut *guard
+ } else {
+ interner = StringsInterner::new();
+ &mut interner
+ };
+
+ let state = &mut ParseState::new(None, interned_strings, tokenizer_control);
+
+ self.parse_global_expr(
+ stream.peekable(),
+ state,
+ |s| s.flags |= ParseSettingFlags::DISALLOW_UNQUOTED_MAP_PROPERTIES,
+ #[cfg(not(feature = "no_optimize"))]
+ OptimizationLevel::None,
+ #[cfg(feature = "no_optimize")]
+ OptimizationLevel::default(),
+ )?
+ };
+
+ self.eval_ast(&ast)
+ }
+}
+
+/// Return the JSON representation of an [object map][Map].
+///
+/// Not available under `no_std`.
+///
+/// This function can be used together with [`Engine::parse_json`] to work with JSON texts
+/// without using the [`serde_json`](https://crates.io/crates/serde_json) crate (which is heavy).
+///
+/// # Data types
+///
+/// Only the following data types should be kept inside the object map: [`INT`][crate::INT],
+/// [`FLOAT`][crate::FLOAT], [`ImmutableString`][crate::ImmutableString], `char`, `bool`, `()`,
+/// [`Array`][crate::Array], [`Map`].
+///
+/// # Errors
+///
+/// Data types not supported by JSON serialize into formats that may invalidate the result.
+#[inline]
+#[must_use]
+pub fn format_map_as_json(map: &Map) -> String {
+ let mut result = String::from('{');
+
+ for (key, value) in map {
+ use std::fmt::Write;
+
+ if result.len() > 1 {
+ result.push(',');
+ }
+
+ write!(result, "{key:?}").unwrap();
+ result.push(':');
+
+ if let Some(val) = value.read_lock::<Map>() {
+ result.push_str(&format_map_as_json(&val));
+ } else if value.is_unit() {
+ result.push_str("null");
+ } else {
+ write!(result, "{value:?}").unwrap();
+ }
+ }
+
+ result.push('}');
+
+ result
+}
diff --git a/rhai/src/api/limits.rs b/rhai/src/api/limits.rs
new file mode 100644
index 0000000..6754edb
--- /dev/null
+++ b/rhai/src/api/limits.rs
@@ -0,0 +1,285 @@
+//! Settings for [`Engine`]'s limitations.
+#![cfg(not(feature = "unchecked"))]
+
+use crate::Engine;
+use std::num::{NonZeroU64, NonZeroUsize};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(debug_assertions)]
+pub mod default_limits {
+ #[cfg(not(feature = "no_function"))]
+ pub const MAX_CALL_STACK_DEPTH: usize = 8;
+ pub const MAX_EXPR_DEPTH: usize = 32;
+ #[cfg(not(feature = "no_function"))]
+ pub const MAX_FUNCTION_EXPR_DEPTH: usize = 16;
+}
+#[cfg(not(debug_assertions))]
+pub mod default_limits {
+ #[cfg(not(feature = "no_function"))]
+ pub const MAX_CALL_STACK_DEPTH: usize = 64;
+ pub const MAX_EXPR_DEPTH: usize = 64;
+ #[cfg(not(feature = "no_function"))]
+ pub const MAX_FUNCTION_EXPR_DEPTH: usize = 32;
+}
+
+/// A type containing all the limits imposed by the [`Engine`].
+///
+/// Not available under `unchecked`.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct Limits {
+ /// Maximum levels of call-stack to prevent infinite recursion.
+ ///
+ /// Set to zero to effectively disable function calls.
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ pub max_call_stack_depth: usize,
+ /// Maximum depth of statements/expressions at global level.
+ pub max_expr_depth: Option<NonZeroUsize>,
+ /// Maximum depth of statements/expressions in functions.
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ pub max_function_expr_depth: Option<NonZeroUsize>,
+ /// Maximum number of operations allowed to run.
+ pub max_operations: Option<NonZeroU64>,
+ /// Maximum number of [modules][crate::Module] allowed to load.
+ ///
+ /// Set to zero to effectively disable loading any [module][crate::Module].
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ pub max_modules: usize,
+ /// Maximum length of a [string][crate::ImmutableString].
+ pub max_string_len: Option<NonZeroUsize>,
+ /// Maximum length of an [array][crate::Array].
+ ///
+ /// Not available under `no_index`.
+ #[cfg(not(feature = "no_index"))]
+ pub max_array_size: Option<NonZeroUsize>,
+ /// Maximum number of properties in an [object map][crate::Map].
+ ///
+ /// Not available under `no_object`.
+ #[cfg(not(feature = "no_object"))]
+ pub max_map_size: Option<NonZeroUsize>,
+}
+
+impl Limits {
+ /// Create a new [`Limits`] with default values.
+ ///
+ /// Not available under `unchecked`.
+ #[inline]
+ pub const fn new() -> Self {
+ Self {
+ #[cfg(not(feature = "no_function"))]
+ max_call_stack_depth: default_limits::MAX_CALL_STACK_DEPTH,
+ max_expr_depth: NonZeroUsize::new(default_limits::MAX_EXPR_DEPTH),
+ #[cfg(not(feature = "no_function"))]
+ max_function_expr_depth: NonZeroUsize::new(default_limits::MAX_FUNCTION_EXPR_DEPTH),
+ max_operations: None,
+ #[cfg(not(feature = "no_module"))]
+ max_modules: usize::MAX,
+ max_string_len: None,
+ #[cfg(not(feature = "no_index"))]
+ max_array_size: None,
+ #[cfg(not(feature = "no_object"))]
+ max_map_size: None,
+ }
+ }
+}
+
+impl Default for Limits {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Engine {
+ /// Is there a data size limit set?
+ #[inline(always)]
+ pub(crate) const fn has_data_size_limit(&self) -> bool {
+ self.limits.max_string_len.is_some()
+ || {
+ #[cfg(not(feature = "no_index"))]
+ {
+ self.limits.max_array_size.is_some()
+ }
+ #[cfg(feature = "no_index")]
+ false
+ }
+ || {
+ #[cfg(not(feature = "no_object"))]
+ {
+ self.limits.max_map_size.is_some()
+ }
+ #[cfg(feature = "no_object")]
+ false
+ }
+ }
+ /// Set the maximum levels of function calls allowed for a script in order to avoid
+ /// infinite recursion and stack overflows.
+ ///
+ /// Not available under `unchecked` or `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ pub fn set_max_call_levels(&mut self, levels: usize) -> &mut Self {
+ self.limits.max_call_stack_depth = levels;
+ self
+ }
+ /// The maximum levels of function calls allowed for a script.
+ ///
+ /// Zero under `no_function`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn max_call_levels(&self) -> usize {
+ #[cfg(not(feature = "no_function"))]
+ return self.limits.max_call_stack_depth;
+ #[cfg(feature = "no_function")]
+ return 0;
+ }
+ /// Set the maximum number of operations allowed for a script to run to avoid
+ /// consuming too much resources (0 for unlimited).
+ ///
+ /// Not available under `unchecked`.
+ #[inline(always)]
+ pub fn set_max_operations(&mut self, operations: u64) -> &mut Self {
+ self.limits.max_operations = NonZeroU64::new(operations);
+ self
+ }
+ /// The maximum number of operations allowed for a script to run (0 for unlimited).
+ ///
+ /// Not available under `unchecked`.
+ #[inline]
+ #[must_use]
+ pub const fn max_operations(&self) -> u64 {
+ match self.limits.max_operations {
+ Some(n) => n.get(),
+ None => 0,
+ }
+ }
+ /// Set the maximum number of imported [modules][crate::Module] allowed for a script.
+ ///
+ /// Not available under `unchecked` or `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline(always)]
+ pub fn set_max_modules(&mut self, modules: usize) -> &mut Self {
+ self.limits.max_modules = modules;
+ self
+ }
+ /// The maximum number of imported [modules][crate::Module] allowed for a script.
+ ///
+ /// Zero under `no_module`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn max_modules(&self) -> usize {
+ #[cfg(not(feature = "no_module"))]
+ return self.limits.max_modules;
+ #[cfg(feature = "no_module")]
+ return 0;
+ }
+ /// Set the depth limits for expressions (0 for unlimited).
+ ///
+ /// Not available under `unchecked`.
+ #[inline(always)]
+ pub fn set_max_expr_depths(
+ &mut self,
+ max_expr_depth: usize,
+ #[cfg(not(feature = "no_function"))] max_function_expr_depth: usize,
+ ) -> &mut Self {
+ self.limits.max_expr_depth = NonZeroUsize::new(max_expr_depth);
+ #[cfg(not(feature = "no_function"))]
+ {
+ self.limits.max_function_expr_depth = NonZeroUsize::new(max_function_expr_depth);
+ }
+ self
+ }
+ /// The depth limit for expressions (0 for unlimited).
+ #[inline]
+ #[must_use]
+ pub const fn max_expr_depth(&self) -> usize {
+ match self.limits.max_expr_depth {
+ Some(n) => n.get(),
+ None => 0,
+ }
+ }
+ /// The depth limit for expressions in functions (0 for unlimited).
+ ///
+ /// Zero under `no_function`.
+ #[inline]
+ #[must_use]
+ pub const fn max_function_expr_depth(&self) -> usize {
+ #[cfg(not(feature = "no_function"))]
+ return match self.limits.max_function_expr_depth {
+ Some(n) => n.get(),
+ None => 0,
+ };
+ #[cfg(feature = "no_function")]
+ return 0;
+ }
+ /// Set the maximum length, in bytes, of [strings][crate::ImmutableString] (0 for unlimited).
+ ///
+ /// Not available under `unchecked`.
+ #[inline(always)]
+ pub fn set_max_string_size(&mut self, max_len: usize) -> &mut Self {
+ self.limits.max_string_len = NonZeroUsize::new(max_len);
+ self
+ }
+ /// The maximum length, in bytes, of [strings][crate::ImmutableString] (0 for unlimited).
+ #[inline]
+ #[must_use]
+ pub const fn max_string_size(&self) -> usize {
+ match self.limits.max_string_len {
+ Some(n) => n.get(),
+ None => 0,
+ }
+ }
+ /// Set the maximum length of [arrays][crate::Array] (0 for unlimited).
+ ///
+ /// Not available under `unchecked` or `no_index`.
+ #[cfg(not(feature = "no_index"))]
+ #[inline(always)]
+ pub fn set_max_array_size(&mut self, max_size: usize) -> &mut Self {
+ self.limits.max_array_size = NonZeroUsize::new(max_size);
+ self
+ }
+ /// The maximum length of [arrays][crate::Array] (0 for unlimited).
+ ///
+ /// Zero under `no_index`.
+ #[inline]
+ #[must_use]
+ pub const fn max_array_size(&self) -> usize {
+ #[cfg(not(feature = "no_index"))]
+ return match self.limits.max_array_size {
+ Some(n) => n.get(),
+ None => 0,
+ };
+ #[cfg(feature = "no_index")]
+ return 0;
+ }
+ /// Set the maximum size of [object maps][crate::Map] (0 for unlimited).
+ ///
+ /// Not available under `unchecked` or `no_object`.
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ pub fn set_max_map_size(&mut self, max_size: usize) -> &mut Self {
+ self.limits.max_map_size = NonZeroUsize::new(max_size);
+ self
+ }
+ /// The maximum size of [object maps][crate::Map] (0 for unlimited).
+ ///
+ /// Zero under `no_object`.
+ #[inline]
+ #[must_use]
+ pub const fn max_map_size(&self) -> usize {
+ #[cfg(not(feature = "no_object"))]
+ return match self.limits.max_map_size {
+ Some(n) => n.get(),
+ None => 0,
+ };
+ #[cfg(feature = "no_object")]
+ return 0;
+ }
+}
diff --git a/rhai/src/api/limits_unchecked.rs b/rhai/src/api/limits_unchecked.rs
new file mode 100644
index 0000000..975162c
--- /dev/null
+++ b/rhai/src/api/limits_unchecked.rs
@@ -0,0 +1,71 @@
+//! Placeholder settings for [`Engine`]'s limitations.
+#![cfg(feature = "unchecked")]
+
+use crate::Engine;
+
+impl Engine {
+ /// The maximum levels of function calls allowed for a script.
+ ///
+ /// Always returns [`usize::MAX`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn max_call_levels(&self) -> usize {
+ usize::MAX
+ }
+ /// The maximum number of operations allowed for a script to run (0 for unlimited).
+ ///
+ /// Always returns zero.
+ #[inline(always)]
+ #[must_use]
+ pub const fn max_operations(&self) -> u64 {
+ 0
+ }
+ /// The maximum number of imported [modules][crate::Module] allowed for a script.
+ ///
+ /// Always returns [`usize::MAX`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn max_modules(&self) -> usize {
+ usize::MAX
+ }
+ /// The depth limit for expressions (0 for unlimited).
+ ///
+ /// Always returns zero.
+ #[inline(always)]
+ #[must_use]
+ pub const fn max_expr_depth(&self) -> usize {
+ 0
+ }
+ /// The depth limit for expressions in functions (0 for unlimited).
+ ///
+ /// Always returns zero.
+ #[inline(always)]
+ #[must_use]
+ pub const fn max_function_expr_depth(&self) -> usize {
+ 0
+ }
+ /// The maximum length of [strings][crate::ImmutableString] (0 for unlimited).
+ ///
+ /// Always returns zero.
+ #[inline(always)]
+ #[must_use]
+ pub const fn max_string_size(&self) -> usize {
+ 0
+ }
+ /// The maximum length of [arrays][crate::Array] (0 for unlimited).
+ ///
+ /// Always returns zero.
+ #[inline(always)]
+ #[must_use]
+ pub const fn max_array_size(&self) -> usize {
+ 0
+ }
+ /// The maximum size of [object maps][crate::Map] (0 for unlimited).
+ ///
+ /// Always returns zero.
+ #[inline(always)]
+ #[must_use]
+ pub const fn max_map_size(&self) -> usize {
+ 0
+ }
+}
diff --git a/rhai/src/api/mod.rs b/rhai/src/api/mod.rs
new file mode 100644
index 0000000..bc6fa1a
--- /dev/null
+++ b/rhai/src/api/mod.rs
@@ -0,0 +1,246 @@
+//! Module defining the public API of the Rhai engine.
+
+pub mod eval;
+
+pub mod run;
+
+pub mod compile;
+
+pub mod json;
+
+pub mod files;
+
+pub mod register;
+
+pub mod call_fn;
+
+pub mod options;
+
+pub mod optimize;
+
+pub mod limits;
+pub mod limits_unchecked;
+
+pub mod events;
+
+pub mod formatting;
+
+pub mod custom_syntax;
+
+pub mod build_type;
+
+#[cfg(feature = "metadata")]
+pub mod definitions;
+
+pub mod deprecated;
+
+use crate::{Dynamic, Engine, Identifier};
+
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+pub mod default_limits {
+ #[cfg(not(feature = "unchecked"))]
+ pub use super::limits::default_limits::*;
+
+ pub const MAX_DYNAMIC_PARAMETERS: usize = 16;
+}
+
+impl Engine {
+ /// The module resolution service used by the [`Engine`].
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline(always)]
+ #[must_use]
+ pub fn module_resolver(&self) -> &dyn crate::ModuleResolver {
+ static DUMMY_RESOLVER: crate::module::resolvers::DummyModuleResolver =
+ crate::module::resolvers::DummyModuleResolver;
+
+ self.module_resolver.as_deref().unwrap_or(&DUMMY_RESOLVER)
+ }
+
+ /// Set the module resolution service used by the [`Engine`].
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline(always)]
+ pub fn set_module_resolver(
+ &mut self,
+ resolver: impl crate::ModuleResolver + 'static,
+ ) -> &mut Self {
+ self.module_resolver = Some(Box::new(resolver));
+ self
+ }
+
+ /// Disable a particular keyword or operator in the language.
+ ///
+ /// # Examples
+ ///
+ /// The following will raise an error during parsing because the `if` keyword is disabled and is
+ /// recognized as a reserved symbol!
+ ///
+ /// ```rust,should_panic
+ /// # fn main() -> Result<(), rhai::ParseError> {
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// engine.disable_symbol("if"); // disable the 'if' keyword
+ ///
+ /// engine.compile("let x = if true { 42 } else { 0 };")?;
+ /// // ^ 'if' is rejected as a reserved symbol
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// The following will raise an error during parsing because the `+=` operator is disabled.
+ ///
+ /// ```rust,should_panic
+ /// # fn main() -> Result<(), rhai::ParseError> {
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// engine.disable_symbol("+="); // disable the '+=' operator
+ ///
+ /// engine.compile("let x = 42; x += 1;")?;
+ /// // ^ unknown operator
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn disable_symbol(&mut self, symbol: impl Into<Identifier>) -> &mut Self {
+ self.disabled_symbols
+ .get_or_insert_with(Default::default)
+ .insert(symbol.into());
+ self
+ }
+
+ /// Is a particular keyword or operator disabled?
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// engine.disable_symbol("if"); // disable the 'if' keyword
+ ///
+ /// assert!(engine.is_symbol_disabled("if"));
+ /// ```
+ #[inline]
+ pub fn is_symbol_disabled(&self, symbol: &str) -> bool {
+ self.disabled_symbols
+ .as_ref()
+ .map_or(false, |m| m.contains(symbol))
+ }
+
+ /// Register a custom operator with a precedence into the language.
+ ///
+ /// Not available under `no_custom_syntax`.
+ ///
+ /// The operator can be a valid identifier, a reserved symbol, a disabled operator or a disabled keyword.
+ ///
+ /// The precedence cannot be zero.
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Register a custom operator called '#' and give it
+ /// // a precedence of 160 (i.e. between +|- and *|/).
+ /// engine.register_custom_operator("#", 160).expect("should succeed");
+ ///
+ /// // Register a binary function named '#'
+ /// engine.register_fn("#", |x: i64, y: i64| (x * y) - (x + y));
+ ///
+ /// assert_eq!(
+ /// engine.eval_expression::<i64>("1 + 2 * 3 # 4 - 5 / 6")?,
+ /// 15
+ /// );
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[cfg(not(feature = "no_custom_syntax"))]
+ pub fn register_custom_operator(
+ &mut self,
+ keyword: impl AsRef<str>,
+ precedence: u8,
+ ) -> Result<&mut Self, String> {
+ use crate::tokenizer::Token;
+
+ let precedence = crate::engine::Precedence::new(precedence)
+ .ok_or_else(|| "precedence cannot be zero".to_string())?;
+
+ let keyword = keyword.as_ref();
+
+ match Token::lookup_symbol_from_syntax(keyword) {
+ // Standard identifiers and reserved keywords are OK
+ None | Some(Token::Reserved(..)) => (),
+ // custom keywords are OK
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Some(Token::Custom(..)) => (),
+ // Active standard keywords cannot be made custom
+ // Disabled keywords are OK
+ Some(token)
+ if token.is_standard_keyword()
+ && !self.is_symbol_disabled(token.literal_syntax()) =>
+ {
+ return Err(format!("'{keyword}' is a reserved keyword"))
+ }
+ // Active standard symbols cannot be made custom
+ Some(token)
+ if token.is_standard_symbol()
+ && !self.is_symbol_disabled(token.literal_syntax()) =>
+ {
+ return Err(format!("'{keyword}' is a reserved operator"))
+ }
+ // Active standard symbols cannot be made custom
+ Some(token) if !self.is_symbol_disabled(token.literal_syntax()) => {
+ return Err(format!("'{keyword}' is a reserved symbol"))
+ }
+ // Disabled symbols are OK
+ Some(_) => (),
+ }
+
+ // Add to custom keywords
+ self.custom_keywords
+ .get_or_insert_with(Default::default)
+ .insert(keyword.into(), Some(precedence));
+
+ Ok(self)
+ }
+ /// Is a keyword registered as a custom keyword?
+ ///
+ /// Not available under `no_custom_syntax`.
+ #[cfg(not(feature = "no_custom_syntax"))]
+ #[inline]
+ pub(crate) fn is_custom_keyword(&self, keyword: &str) -> bool {
+ self.custom_keywords
+ .as_ref()
+ .map_or(false, |m| m.contains_key(keyword))
+ }
+
+ /// Get the default value of the custom state for each evaluation run.
+ #[inline(always)]
+ pub const fn default_tag(&self) -> &Dynamic {
+ &self.def_tag
+ }
+ /// Get a mutable reference to the default value of the custom state for each evaluation run.
+ #[inline(always)]
+ pub fn default_tag_mut(&mut self) -> &mut Dynamic {
+ &mut self.def_tag
+ }
+ /// Set the default value of the custom state for each evaluation run.
+ #[inline(always)]
+ pub fn set_default_tag(&mut self, value: impl Into<Dynamic>) -> &mut Self {
+ self.def_tag = value.into();
+ self
+ }
+}
diff --git a/rhai/src/api/optimize.rs b/rhai/src/api/optimize.rs
new file mode 100644
index 0000000..32927c0
--- /dev/null
+++ b/rhai/src/api/optimize.rs
@@ -0,0 +1,74 @@
+//! Module that defines the script optimization API of [`Engine`].
+#![cfg(not(feature = "no_optimize"))]
+
+use crate::{Engine, OptimizationLevel, Scope, AST};
+
+impl Engine {
+ /// Control whether and how the [`Engine`] will optimize an [`AST`] after compilation.
+ ///
+ /// Not available under `no_optimize`.
+ #[inline(always)]
+ pub fn set_optimization_level(&mut self, optimization_level: OptimizationLevel) -> &mut Self {
+ self.optimization_level = optimization_level;
+ self
+ }
+
+ /// The current optimization level.
+ /// It controls whether and how the [`Engine`] will optimize an [`AST`] after compilation.
+ ///
+ /// Not available under `no_optimize`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn optimization_level(&self) -> OptimizationLevel {
+ self.optimization_level
+ }
+
+ /// Optimize the [`AST`] with constants defined in an external Scope.
+ /// An optimized copy of the [`AST`] is returned while the original [`AST`] is consumed.
+ ///
+ /// Not available under `no_optimize`.
+ ///
+ /// Although optimization is performed by default during compilation, sometimes it is necessary
+ /// to _re_-optimize an [`AST`].
+ ///
+ /// For example, when working with constants that are passed in via an external scope,
+ /// it will be more efficient to optimize the [`AST`] once again to take advantage of the new constants.
+ ///
+ /// With this method, it is no longer necessary to recompile a large script.
+ /// The script [`AST`] can be compiled just once.
+ ///
+ /// Before evaluation, constants are passed into the [`Engine`] via an external scope
+ /// (i.e. with [`Scope::push_constant`][Scope::push_constant]).
+ ///
+ /// Then, the [`AST`] is cloned and the copy re-optimized before running.
+ #[inline]
+ #[must_use]
+ pub fn optimize_ast(
+ &self,
+ scope: &Scope,
+ ast: AST,
+ optimization_level: OptimizationLevel,
+ ) -> AST {
+ let mut ast = ast;
+
+ let mut _new_ast = self.optimize_into_ast(
+ Some(scope),
+ ast.take_statements(),
+ #[cfg(not(feature = "no_function"))]
+ ast.shared_lib()
+ .iter_fn()
+ .map(|f| f.func.get_script_fn_def().cloned().expect("`ScriptFnDef"))
+ .collect(),
+ optimization_level,
+ );
+
+ #[cfg(feature = "metadata")]
+ if let Some(doc) = ast.doc_mut() {
+ _new_ast.set_doc(std::mem::take(doc));
+ } else {
+ _new_ast.clear_doc();
+ }
+
+ _new_ast
+ }
+}
diff --git a/rhai/src/api/options.rs b/rhai/src/api/options.rs
new file mode 100644
index 0000000..4c23f4c
--- /dev/null
+++ b/rhai/src/api/options.rs
@@ -0,0 +1,209 @@
+//! Settings for [`Engine`]'s language options.
+
+use crate::Engine;
+use bitflags::bitflags;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+bitflags! {
+ /// Bit-flags containing all language options for the [`Engine`].
+ pub struct LangOptions: u16 {
+ /// Is `if`-expression allowed?
+ const IF_EXPR = 0b_0000_0000_0001;
+ /// Is `switch` expression allowed?
+ const SWITCH_EXPR = 0b_0000_0000_0010;
+ /// Are loop expressions allowed?
+ const LOOP_EXPR = 0b_0000_0000_0100;
+ /// Is statement-expression allowed?
+ const STMT_EXPR = 0b_0000_0000_1000;
+ /// Is anonymous function allowed?
+ #[cfg(not(feature = "no_function"))]
+ const ANON_FN = 0b_0000_0001_0000;
+ /// Is looping allowed?
+ const LOOPING = 0b_0000_0010_0000;
+ /// Is variables shadowing allowed?
+ const SHADOWING = 0b_0000_0100_0000;
+ /// Strict variables mode?
+ const STRICT_VAR = 0b_0000_1000_0000;
+ /// Raise error if an object map property does not exist?
+ /// Returns `()` if `false`.
+ #[cfg(not(feature = "no_object"))]
+ const FAIL_ON_INVALID_MAP_PROPERTY = 0b_0001_0000_0000;
+ /// Fast operators mode?
+ const FAST_OPS = 0b_0010_0000_0000;
+ }
+}
+
+impl LangOptions {
+ /// Create a new [`LangOptions`] with default values.
+ #[inline(always)]
+ #[must_use]
+ pub const fn new() -> Self {
+ Self::from_bits_truncate(
+ Self::IF_EXPR.bits()
+ | Self::SWITCH_EXPR.bits()
+ | Self::LOOP_EXPR.bits()
+ | Self::STMT_EXPR.bits()
+ | Self::LOOPING.bits()
+ | Self::SHADOWING.bits()
+ | Self::FAST_OPS.bits()
+ | {
+ #[cfg(not(feature = "no_function"))]
+ {
+ Self::ANON_FN.bits()
+ }
+ #[cfg(feature = "no_function")]
+ {
+ Self::empty().bits()
+ }
+ },
+ )
+ }
+}
+
+impl Engine {
+ /// Is `if`-expression allowed?
+ /// Default is `true`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn allow_if_expression(&self) -> bool {
+ self.options.contains(LangOptions::IF_EXPR)
+ }
+ /// Set whether `if`-expression is allowed.
+ #[inline(always)]
+ pub fn set_allow_if_expression(&mut self, enable: bool) -> &mut Self {
+ self.options.set(LangOptions::IF_EXPR, enable);
+ self
+ }
+ /// Is `switch` expression allowed?
+ /// Default is `true`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn allow_switch_expression(&self) -> bool {
+ self.options.contains(LangOptions::SWITCH_EXPR)
+ }
+ /// Set whether `switch` expression is allowed.
+ #[inline(always)]
+ pub fn set_allow_switch_expression(&mut self, enable: bool) -> &mut Self {
+ self.options.set(LangOptions::SWITCH_EXPR, enable);
+ self
+ }
+ /// Are loop expressions allowed?
+ /// Default is `true`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn allow_loop_expressions(&self) -> bool {
+ self.options.contains(LangOptions::LOOP_EXPR)
+ }
+ /// Set whether loop expressions are allowed.
+ #[inline(always)]
+ pub fn set_allow_loop_expressions(&mut self, enable: bool) -> &mut Self {
+ self.options.set(LangOptions::LOOP_EXPR, enable);
+ self
+ }
+ /// Is statement-expression allowed?
+ /// Default is `true`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn allow_statement_expression(&self) -> bool {
+ self.options.contains(LangOptions::STMT_EXPR)
+ }
+ /// Set whether statement-expression is allowed.
+ #[inline(always)]
+ pub fn set_allow_statement_expression(&mut self, enable: bool) -> &mut Self {
+ self.options.set(LangOptions::STMT_EXPR, enable);
+ self
+ }
+ /// Is anonymous function allowed?
+ /// Default is `true`.
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ #[must_use]
+ pub const fn allow_anonymous_fn(&self) -> bool {
+ self.options.contains(LangOptions::ANON_FN)
+ }
+ /// Set whether anonymous function is allowed.
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ pub fn set_allow_anonymous_fn(&mut self, enable: bool) -> &mut Self {
+ self.options.set(LangOptions::ANON_FN, enable);
+ self
+ }
+ /// Is looping allowed?
+ /// Default is `true`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn allow_looping(&self) -> bool {
+ self.options.contains(LangOptions::LOOPING)
+ }
+ /// Set whether looping is allowed.
+ #[inline(always)]
+ pub fn set_allow_looping(&mut self, enable: bool) -> &mut Self {
+ self.options.set(LangOptions::LOOPING, enable);
+ self
+ }
+ /// Is variables shadowing allowed?
+ /// Default is `true`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn allow_shadowing(&self) -> bool {
+ self.options.contains(LangOptions::SHADOWING)
+ }
+ /// Set whether variables shadowing is allowed.
+ #[inline(always)]
+ pub fn set_allow_shadowing(&mut self, enable: bool) -> &mut Self {
+ self.options.set(LangOptions::SHADOWING, enable);
+ self
+ }
+ /// Is strict variables mode enabled?
+ /// Default is `false`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn strict_variables(&self) -> bool {
+ self.options.contains(LangOptions::STRICT_VAR)
+ }
+ /// Set whether strict variables mode is enabled.
+ #[inline(always)]
+ pub fn set_strict_variables(&mut self, enable: bool) -> &mut Self {
+ self.options.set(LangOptions::STRICT_VAR, enable);
+ self
+ }
+ /// Raise error if an object map property does not exist?
+ /// Default is `false`.
+ ///
+ /// Not available under `no_object`.
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ #[must_use]
+ pub const fn fail_on_invalid_map_property(&self) -> bool {
+ self.options
+ .contains(LangOptions::FAIL_ON_INVALID_MAP_PROPERTY)
+ }
+ /// Set whether to raise error if an object map property does not exist.
+ ///
+ /// Not available under `no_object`.
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ pub fn set_fail_on_invalid_map_property(&mut self, enable: bool) -> &mut Self {
+ self.options
+ .set(LangOptions::FAIL_ON_INVALID_MAP_PROPERTY, enable);
+ self
+ }
+ /// Is fast operators mode enabled?
+ /// Default is `false`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn fast_operators(&self) -> bool {
+ self.options.contains(LangOptions::FAST_OPS)
+ }
+ /// Set whether fast operators mode is enabled.
+ #[inline(always)]
+ pub fn set_fast_operators(&mut self, enable: bool) -> &mut Self {
+ self.options.set(LangOptions::FAST_OPS, enable);
+ self
+ }
+}
diff --git a/rhai/src/api/register.rs b/rhai/src/api/register.rs
new file mode 100644
index 0000000..ab64ef3
--- /dev/null
+++ b/rhai/src/api/register.rs
@@ -0,0 +1,809 @@
+//! Module that defines the public function/module registration API of [`Engine`].
+
+use crate::func::{FnCallArgs, RegisterNativeFunction, SendSync};
+use crate::module::ModuleFlags;
+use crate::types::dynamic::Variant;
+use crate::{
+ Engine, FnAccess, FnNamespace, Identifier, Module, NativeCallContext, RhaiResultOf, Shared,
+ SharedModule,
+};
+use std::any::{type_name, TypeId};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+use crate::func::register::Mut;
+
+impl Engine {
+ /// Get a mutable reference to the global namespace module
+ /// (which is the first module in `global_modules`).
+ #[inline(always)]
+ #[must_use]
+ fn global_namespace_mut(&mut self) -> &mut Module {
+ if self.global_modules.is_empty() {
+ let mut global_namespace = Module::new();
+ global_namespace.flags |= ModuleFlags::INTERNAL;
+ self.global_modules.push(global_namespace.into());
+ }
+
+ Shared::get_mut(self.global_modules.first_mut().unwrap()).expect("not shared")
+ }
+ /// Register a custom function with the [`Engine`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// // Normal function
+ /// fn add(x: i64, y: i64) -> i64 {
+ /// x + y
+ /// }
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// engine.register_fn("add", add);
+ ///
+ /// assert_eq!(engine.eval::<i64>("add(40, 2)")?, 42);
+ ///
+ /// // You can also register a closure.
+ /// engine.register_fn("sub", |x: i64, y: i64| x - y );
+ ///
+ /// assert_eq!(engine.eval::<i64>("sub(44, 2)")?, 42);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn register_fn<
+ A: 'static,
+ const N: usize,
+ const C: bool,
+ R: Variant + Clone,
+ const L: bool,
+ F: RegisterNativeFunction<A, N, C, R, L>,
+ >(
+ &mut self,
+ name: impl AsRef<str> + Into<Identifier>,
+ func: F,
+ ) -> &mut Self {
+ let param_types = F::param_types();
+
+ #[cfg(feature = "metadata")]
+ let mut param_type_names: crate::StaticVec<_> = F::param_names()
+ .iter()
+ .map(|ty| format!("_: {}", self.format_type_name(ty)))
+ .collect();
+
+ #[cfg(feature = "metadata")]
+ if F::return_type() != TypeId::of::<()>() {
+ param_type_names.push(self.format_type_name(F::return_type_name()).into());
+ }
+
+ #[cfg(feature = "metadata")]
+ let param_type_names: crate::StaticVec<_> =
+ param_type_names.iter().map(String::as_str).collect();
+ #[cfg(feature = "metadata")]
+ let param_type_names = Some(param_type_names.as_ref());
+
+ #[cfg(not(feature = "metadata"))]
+ let param_type_names: Option<&[&str]> = None;
+
+ let fn_name = name.as_ref();
+ let is_pure = true;
+
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ let is_pure = is_pure && (F::num_params() != 3 || fn_name != crate::engine::FN_IDX_SET);
+ #[cfg(not(feature = "no_object"))]
+ let is_pure =
+ is_pure && (F::num_params() != 2 || !fn_name.starts_with(crate::engine::FN_SET));
+
+ let func = func.into_callable_function(fn_name.into(), is_pure);
+
+ self.global_namespace_mut().set_fn(
+ name,
+ FnNamespace::Global,
+ FnAccess::Public,
+ param_type_names,
+ param_types,
+ func,
+ );
+ self
+ }
+ /// Register a function of the [`Engine`].
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is very low level. It takes a list of [`TypeId`][std::any::TypeId]'s
+ /// indicating the actual types of the parameters.
+ ///
+ /// # Arguments
+ ///
+ /// Arguments are simply passed in as a mutable array of [`&mut Dynamic`][crate::Dynamic].
+ /// The arguments are guaranteed to be of the correct types matching the [`TypeId`][std::any::TypeId]'s.
+ ///
+ /// To access a primary argument value (i.e. cloning is cheap), use: `args[n].as_xxx().unwrap()`
+ ///
+ /// To access an argument value and avoid cloning, use `args[n].take().cast::<T>()`.
+ /// Notice that this will _consume_ the argument, replacing it with `()`.
+ ///
+ /// To access the first mutable parameter, use `args.get_mut(0).unwrap()`
+ #[inline(always)]
+ pub fn register_raw_fn<T: Variant + Clone>(
+ &mut self,
+ name: impl AsRef<str> + Into<Identifier>,
+ arg_types: impl AsRef<[TypeId]>,
+ func: impl Fn(NativeCallContext, &mut FnCallArgs) -> RhaiResultOf<T> + SendSync + 'static,
+ ) -> &mut Self {
+ self.global_namespace_mut().set_raw_fn(
+ name,
+ FnNamespace::Global,
+ FnAccess::Public,
+ arg_types,
+ func,
+ );
+ self
+ }
+ /// Register a custom type for use with the [`Engine`].
+ /// The type must implement [`Clone`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// #[derive(Debug, Clone, Eq, PartialEq)]
+ /// struct TestStruct {
+ /// field: i64
+ /// }
+ ///
+ /// impl TestStruct {
+ /// fn new() -> Self {
+ /// Self { field: 1 }
+ /// }
+ /// fn update(&mut self, offset: i64) {
+ /// self.field += offset;
+ /// }
+ /// }
+ ///
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Register API for the custom type.
+ /// engine
+ /// .register_type::<TestStruct>()
+ /// .register_fn("new_ts", TestStruct::new)
+ /// // Use `register_fn` to register methods on the type.
+ /// .register_fn("update", TestStruct::update);
+ ///
+ /// # #[cfg(not(feature = "no_object"))]
+ /// assert_eq!(
+ /// engine.eval::<TestStruct>("let x = new_ts(); x.update(41); x")?,
+ /// TestStruct { field: 42 }
+ /// );
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn register_type<T: Variant + Clone>(&mut self) -> &mut Self {
+ self.register_type_with_name::<T>(type_name::<T>())
+ }
+ /// Register a custom type for use with the [`Engine`], with a pretty-print name
+ /// for the `type_of` function. The type must implement [`Clone`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// #[derive(Clone)]
+ /// struct TestStruct {
+ /// field: i64
+ /// }
+ ///
+ /// impl TestStruct {
+ /// fn new() -> Self {
+ /// Self { field: 1 }
+ /// }
+ /// }
+ ///
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Register API for the custom type.
+ /// engine
+ /// .register_type::<TestStruct>()
+ /// .register_fn("new_ts", TestStruct::new);
+ ///
+ /// assert_eq!(
+ /// engine.eval::<String>("let x = new_ts(); type_of(x)")?,
+ /// "rust_out::TestStruct"
+ /// );
+ ///
+ /// // Re-register the custom type with a name.
+ /// engine.register_type_with_name::<TestStruct>("Hello");
+ ///
+ /// assert_eq!(
+ /// engine.eval::<String>("let x = new_ts(); type_of(x)")?,
+ /// "Hello"
+ /// );
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn register_type_with_name<T: Variant + Clone>(&mut self, name: &str) -> &mut Self {
+ self.global_namespace_mut().set_custom_type::<T>(name);
+ self
+ }
+ /// Register a custom type for use with the [`Engine`], with a pretty-print name
+ /// for the `type_of` function. The type must implement [`Clone`].
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is low level.
+ #[inline(always)]
+ pub fn register_type_with_name_raw(
+ &mut self,
+ type_path: impl Into<Identifier>,
+ name: impl Into<Identifier>,
+ ) -> &mut Self {
+ // Add the pretty-print type name into the map
+ self.global_namespace_mut()
+ .set_custom_type_raw(type_path, name);
+ self
+ }
+ /// Register a type iterator for an iterable type with the [`Engine`].
+ /// This is an advanced API.
+ #[inline(always)]
+ pub fn register_iterator<T>(&mut self) -> &mut Self
+ where
+ T: Variant + Clone + IntoIterator,
+ <T as IntoIterator>::Item: Variant + Clone,
+ {
+ self.global_namespace_mut().set_iterable::<T>();
+ self
+ }
+ /// Register a fallible type iterator for an iterable type with the [`Engine`].
+ /// This is an advanced API.
+ #[inline(always)]
+ pub fn register_iterator_result<T, X>(&mut self) -> &mut Self
+ where
+ T: Variant + Clone + IntoIterator<Item = RhaiResultOf<X>>,
+ X: Variant + Clone,
+ {
+ self.global_namespace_mut().set_iterable_result::<T, X>();
+ self
+ }
+ /// Register a getter function for a member of a registered type with the [`Engine`].
+ ///
+ /// The function signature must start with `&mut self` and not `&self`.
+ ///
+ /// Not available under `no_object`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// #[derive(Clone)]
+ /// struct TestStruct {
+ /// field: i64
+ /// }
+ ///
+ /// impl TestStruct {
+ /// fn new() -> Self {
+ /// Self { field: 1 }
+ /// }
+ /// // Even a getter must start with `&mut self` and not `&self`.
+ /// fn get_field(&mut self) -> i64 {
+ /// self.field
+ /// }
+ /// }
+ ///
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Register API for the custom type.
+ /// engine
+ /// .register_type::<TestStruct>()
+ /// .register_fn("new_ts", TestStruct::new)
+ /// // Register a getter on a property (notice it doesn't have to be the same name).
+ /// .register_get("xyz", TestStruct::get_field);
+ ///
+ /// assert_eq!(engine.eval::<i64>("let a = new_ts(); a.xyz")?, 1);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ pub fn register_get<T: Variant + Clone, const C: bool, V: Variant + Clone, const L: bool>(
+ &mut self,
+ name: impl AsRef<str>,
+ get_fn: impl RegisterNativeFunction<(Mut<T>,), 1, C, V, L> + SendSync + 'static,
+ ) -> &mut Self {
+ self.register_fn(crate::engine::make_getter(name.as_ref()).as_str(), get_fn)
+ }
+ /// Register a setter function for a member of a registered type with the [`Engine`].
+ ///
+ /// Not available under `no_object`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// #[derive(Debug, Clone, Eq, PartialEq)]
+ /// struct TestStruct {
+ /// field: i64
+ /// }
+ ///
+ /// impl TestStruct {
+ /// fn new() -> Self {
+ /// Self { field: 1 }
+ /// }
+ /// fn set_field(&mut self, new_val: i64) {
+ /// self.field = new_val;
+ /// }
+ /// }
+ ///
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Register API for the custom type.
+ /// engine
+ /// .register_type::<TestStruct>()
+ /// .register_fn("new_ts", TestStruct::new)
+ /// // Register a setter on a property (notice it doesn't have to be the same name)
+ /// .register_set("xyz", TestStruct::set_field);
+ ///
+ /// // Notice that, with a getter, there is no way to get the property value
+ /// assert_eq!(
+ /// engine.eval::<TestStruct>("let a = new_ts(); a.xyz = 42; a")?,
+ /// TestStruct { field: 42 }
+ /// );
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ pub fn register_set<T: Variant + Clone, const C: bool, V: Variant + Clone, const L: bool>(
+ &mut self,
+ name: impl AsRef<str>,
+ set_fn: impl RegisterNativeFunction<(Mut<T>, V), 2, C, (), L> + SendSync + 'static,
+ ) -> &mut Self {
+ self.register_fn(crate::engine::make_setter(name.as_ref()).as_str(), set_fn)
+ }
+ /// Short-hand for registering both getter and setter functions
+ /// of a registered type with the [`Engine`].
+ ///
+ /// All function signatures must start with `&mut self` and not `&self`.
+ ///
+ /// Not available under `no_object`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// #[derive(Clone)]
+ /// struct TestStruct {
+ /// field: i64
+ /// }
+ ///
+ /// impl TestStruct {
+ /// fn new() -> Self {
+ /// Self { field: 1 }
+ /// }
+ /// // Even a getter must start with `&mut self` and not `&self`.
+ /// fn get_field(&mut self) -> i64 {
+ /// self.field
+ /// }
+ /// fn set_field(&mut self, new_val: i64) {
+ /// self.field = new_val;
+ /// }
+ /// }
+ ///
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Register API for the custom type.
+ /// engine
+ /// .register_type::<TestStruct>()
+ /// .register_fn("new_ts", TestStruct::new)
+ /// // Register both a getter and a setter on a property
+ /// // (notice it doesn't have to be the same name)
+ /// .register_get_set("xyz", TestStruct::get_field, TestStruct::set_field);
+ ///
+ /// assert_eq!(engine.eval::<i64>("let a = new_ts(); a.xyz = 42; a.xyz")?, 42);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ pub fn register_get_set<
+ T: Variant + Clone,
+ const C1: bool,
+ const C2: bool,
+ V: Variant + Clone,
+ const L1: bool,
+ const L2: bool,
+ >(
+ &mut self,
+ name: impl AsRef<str>,
+ get_fn: impl RegisterNativeFunction<(Mut<T>,), 1, C1, V, L1> + SendSync + 'static,
+ set_fn: impl RegisterNativeFunction<(Mut<T>, V), 2, C2, (), L2> + SendSync + 'static,
+ ) -> &mut Self {
+ self.register_get(&name, get_fn).register_set(&name, set_fn)
+ }
+ /// Register an index getter for a custom type with the [`Engine`].
+ ///
+ /// The function signature must start with `&mut self` and not `&self`.
+ ///
+ /// Not available under both `no_index` and `no_object`.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the type is [`Array`][crate::Array], [`Map`][crate::Map], [`String`],
+ /// [`ImmutableString`][crate::ImmutableString], `&str` or [`INT`][crate::INT].
+ /// Indexers for arrays, object maps, strings and integers cannot be registered.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// #[derive(Clone)]
+ /// struct TestStruct {
+ /// fields: Vec<i64>
+ /// }
+ ///
+ /// impl TestStruct {
+ /// fn new() -> Self {
+ /// Self { fields: vec![1, 2, 3, 4, 5] }
+ /// }
+ /// // Even a getter must start with `&mut self` and not `&self`.
+ /// fn get_field(&mut self, index: i64) -> i64 {
+ /// self.fields[index as usize]
+ /// }
+ /// }
+ ///
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Register API for the custom type.
+ /// # #[cfg(not(feature = "no_object"))]
+ /// engine.register_type::<TestStruct>();
+ ///
+ /// engine
+ /// .register_fn("new_ts", TestStruct::new)
+ /// // Register an indexer.
+ /// .register_indexer_get(TestStruct::get_field);
+ ///
+ /// # #[cfg(not(feature = "no_index"))]
+ /// assert_eq!(engine.eval::<i64>("let a = new_ts(); a[2]")?, 3);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ #[inline]
+ pub fn register_indexer_get<
+ T: Variant + Clone,
+ X: Variant + Clone,
+ const C: bool,
+ V: Variant + Clone,
+ const L: bool,
+ >(
+ &mut self,
+ get_fn: impl RegisterNativeFunction<(Mut<T>, X), 2, C, V, L> + SendSync + 'static,
+ ) -> &mut Self {
+ #[cfg(not(feature = "no_index"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Array>() {
+ panic!("Cannot register indexer for arrays.");
+ }
+ #[cfg(not(feature = "no_object"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Map>() {
+ panic!("Cannot register indexer for object maps.");
+ }
+ if TypeId::of::<T>() == TypeId::of::<String>()
+ || TypeId::of::<T>() == TypeId::of::<&str>()
+ || TypeId::of::<T>() == TypeId::of::<crate::ImmutableString>()
+ {
+ panic!("Cannot register indexer for strings.");
+ }
+ if TypeId::of::<T>() == TypeId::of::<crate::INT>() {
+ panic!("Cannot register indexer for integers.");
+ }
+
+ self.register_fn(crate::engine::FN_IDX_GET, get_fn)
+ }
+ /// Register an index setter for a custom type with the [`Engine`].
+ ///
+ /// Not available under both `no_index` and `no_object`.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the type is [`Array`][crate::Array], [`Map`][crate::Map], [`String`],
+ /// [`ImmutableString`][crate::ImmutableString], `&str` or [`INT`][crate::INT].
+ /// Indexers for arrays, object maps, strings and integers cannot be registered.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// #[derive(Clone)]
+ /// struct TestStruct {
+ /// fields: Vec<i64>
+ /// }
+ ///
+ /// impl TestStruct {
+ /// fn new() -> Self {
+ /// Self { fields: vec![1, 2, 3, 4, 5] }
+ /// }
+ /// fn set_field(&mut self, index: i64, value: i64) {
+ /// self.fields[index as usize] = value;
+ /// }
+ /// }
+ ///
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Register API for the custom type.
+ /// # #[cfg(not(feature = "no_object"))]
+ /// engine.register_type::<TestStruct>();
+ ///
+ /// engine
+ /// .register_fn("new_ts", TestStruct::new)
+ /// // Register an indexer.
+ /// .register_indexer_set(TestStruct::set_field);
+ ///
+ /// # #[cfg(not(feature = "no_index"))]
+ /// let result = engine.eval::<TestStruct>("let a = new_ts(); a[2] = 42; a")?;
+ ///
+ /// # #[cfg(not(feature = "no_index"))]
+ /// assert_eq!(result.fields[2], 42);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ #[inline]
+ pub fn register_indexer_set<
+ T: Variant + Clone,
+ X: Variant + Clone,
+ const C: bool,
+ V: Variant + Clone,
+ const L: bool,
+ >(
+ &mut self,
+ set_fn: impl RegisterNativeFunction<(Mut<T>, X, V), 3, C, (), L> + SendSync + 'static,
+ ) -> &mut Self {
+ #[cfg(not(feature = "no_index"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Array>() {
+ panic!("Cannot register indexer for arrays.");
+ }
+ #[cfg(not(feature = "no_object"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Map>() {
+ panic!("Cannot register indexer for object maps.");
+ }
+ if TypeId::of::<T>() == TypeId::of::<String>()
+ || TypeId::of::<T>() == TypeId::of::<&str>()
+ || TypeId::of::<T>() == TypeId::of::<crate::ImmutableString>()
+ {
+ panic!("Cannot register indexer for strings.");
+ }
+ if TypeId::of::<T>() == TypeId::of::<crate::INT>() {
+ panic!("Cannot register indexer for integers.");
+ }
+
+ self.register_fn(crate::engine::FN_IDX_SET, set_fn)
+ }
+ /// Short-hand for registering both index getter and setter functions for a custom type with the [`Engine`].
+ ///
+ /// Not available under both `no_index` and `no_object`.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the type is [`Array`][crate::Array], [`Map`][crate::Map], [`String`],
+ /// [`ImmutableString`][crate::ImmutableString], `&str` or [`INT`][crate::INT].
+ /// Indexers for arrays, object maps, strings and integers cannot be registered.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// #[derive(Clone)]
+ /// struct TestStruct {
+ /// fields: Vec<i64>
+ /// }
+ ///
+ /// impl TestStruct {
+ /// fn new() -> Self {
+ /// Self { fields: vec![1, 2, 3, 4, 5] }
+ /// }
+ /// // Even a getter must start with `&mut self` and not `&self`.
+ /// fn get_field(&mut self, index: i64) -> i64 {
+ /// self.fields[index as usize]
+ /// }
+ /// fn set_field(&mut self, index: i64, value: i64) {
+ /// self.fields[index as usize] = value;
+ /// }
+ /// }
+ ///
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Register API for the custom type.
+ /// # #[cfg(not(feature = "no_object"))]
+ /// engine.register_type::<TestStruct>();
+ ///
+ /// engine
+ /// .register_fn("new_ts", TestStruct::new)
+ /// // Register an indexer.
+ /// .register_indexer_get_set(TestStruct::get_field, TestStruct::set_field);
+ ///
+ /// # #[cfg(not(feature = "no_index"))]
+ /// assert_eq!(engine.eval::<i64>("let a = new_ts(); a[2] = 42; a[2]")?, 42);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ #[inline(always)]
+ pub fn register_indexer_get_set<
+ T: Variant + Clone,
+ X: Variant + Clone,
+ const C1: bool,
+ const C2: bool,
+ V: Variant + Clone,
+ const L1: bool,
+ const L2: bool,
+ >(
+ &mut self,
+ get_fn: impl RegisterNativeFunction<(Mut<T>, X), 2, C1, V, L1> + SendSync + 'static,
+ set_fn: impl RegisterNativeFunction<(Mut<T>, X, V), 3, C2, (), L2> + SendSync + 'static,
+ ) -> &mut Self {
+ self.register_indexer_get(get_fn)
+ .register_indexer_set(set_fn)
+ }
+ /// Register a shared [`Module`] into the global namespace of [`Engine`].
+ ///
+ /// All functions and type iterators are automatically available to scripts without namespace
+ /// qualifications.
+ ///
+ /// Sub-modules and variables are **ignored**.
+ ///
+ /// When searching for functions, modules loaded later are preferred. In other words, loaded
+ /// modules are searched in reverse order.
+ #[inline(always)]
+ pub fn register_global_module(&mut self, module: SharedModule) -> &mut Self {
+ // Make sure the global namespace is created.
+ let _ = self.global_namespace_mut();
+
+ // Insert the module into the front.
+ // The first module is always the global namespace.
+ self.global_modules.insert(1, module);
+ self
+ }
+ /// Register a shared [`Module`] as a static module namespace with the [`Engine`].
+ ///
+ /// Functions marked [`FnNamespace::Global`] and type iterators are exposed to scripts without
+ /// namespace qualifications.
+ ///
+ /// Not available under `no_module`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Shared, Module};
+ ///
+ /// let mut engine = Engine::new();
+ ///
+ /// // Create the module
+ /// let mut module = Module::new();
+ /// module.set_native_fn("calc", |x: i64| Ok(x + 1));
+ ///
+ /// let module: Shared<Module> = module.into();
+ ///
+ /// engine
+ /// // Register the module as a fixed sub-module
+ /// .register_static_module("foo::bar::baz", module.clone())
+ /// // Multiple registrations to the same partial path is also OK!
+ /// .register_static_module("foo::bar::hello", module.clone())
+ /// .register_static_module("CalcService", module);
+ ///
+ /// assert_eq!(engine.eval::<i64>("foo::bar::baz::calc(41)")?, 42);
+ /// assert_eq!(engine.eval::<i64>("foo::bar::hello::calc(41)")?, 42);
+ /// assert_eq!(engine.eval::<i64>("CalcService::calc(41)")?, 42);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[cfg(not(feature = "no_module"))]
+ pub fn register_static_module(
+ &mut self,
+ name: impl AsRef<str>,
+ module: SharedModule,
+ ) -> &mut Self {
+ use std::collections::BTreeMap;
+
+ fn register_static_module_raw(
+ root: &mut BTreeMap<Identifier, SharedModule>,
+ name: &str,
+ module: SharedModule,
+ ) {
+ let separator = crate::engine::NAMESPACE_SEPARATOR;
+
+ if name.contains(separator) {
+ let mut iter = name.splitn(2, separator);
+ let sub_module = iter.next().expect("contains separator").trim();
+ let remainder = iter.next().expect("contains separator").trim();
+
+ if root.is_empty() || !root.contains_key(sub_module) {
+ let mut m = Module::new();
+ register_static_module_raw(m.get_sub_modules_mut(), remainder, module);
+ m.build_index();
+ root.insert(sub_module.into(), m.into());
+ } else {
+ let m = root.remove(sub_module).expect("contains sub-module");
+ let mut m = crate::func::shared_take_or_clone(m);
+ register_static_module_raw(m.get_sub_modules_mut(), remainder, module);
+ m.build_index();
+ root.insert(sub_module.into(), m.into());
+ }
+ } else if module.is_indexed() {
+ root.insert(name.into(), module);
+ } else {
+ // Index the module (making a clone copy if necessary) if it is not indexed
+ let mut module = crate::func::shared_take_or_clone(module);
+ module.build_index();
+ root.insert(name.into(), module.into());
+ }
+ }
+
+ register_static_module_raw(
+ self.global_sub_modules.get_or_insert_with(Default::default),
+ name.as_ref(),
+ module,
+ );
+ self
+ }
+ /// _(metadata)_ Generate a list of all registered functions.
+ /// Exported under the `metadata` feature only.
+ ///
+ /// Functions from the following sources are included, in order:
+ /// 1) Functions registered into the global namespace
+ /// 2) Functions in registered sub-modules
+ /// 3) Functions in registered packages
+ /// 4) Functions in standard packages (optional)
+ #[cfg(feature = "metadata")]
+ #[inline]
+ #[must_use]
+ pub fn gen_fn_signatures(&self, include_packages: bool) -> Vec<String> {
+ let mut signatures = Vec::with_capacity(64);
+
+ if let Some(global_namespace) = self.global_modules.first() {
+ signatures.extend(global_namespace.gen_fn_signatures());
+ }
+
+ #[cfg(not(feature = "no_module"))]
+ for (name, m) in self.global_sub_modules.as_ref().into_iter().flatten() {
+ signatures.extend(m.gen_fn_signatures().map(|f| format!("{name}::{f}")));
+ }
+
+ let exclude_flags = if include_packages {
+ crate::module::ModuleFlags::INTERNAL
+ } else {
+ crate::module::ModuleFlags::INTERNAL | crate::module::ModuleFlags::STANDARD_LIB
+ };
+
+ signatures.extend(
+ self.global_modules
+ .iter()
+ .skip(1)
+ .filter(|m| !m.flags.contains(exclude_flags))
+ .flat_map(|m| m.gen_fn_signatures()),
+ );
+
+ signatures
+ }
+}
diff --git a/rhai/src/api/run.rs b/rhai/src/api/run.rs
new file mode 100644
index 0000000..1fe1e47
--- /dev/null
+++ b/rhai/src/api/run.rs
@@ -0,0 +1,166 @@
+//! Module that defines the public evaluation API of [`Engine`].
+
+use crate::eval::{Caches, GlobalRuntimeState};
+use crate::func::native::locked_write;
+use crate::parser::ParseState;
+use crate::types::StringsInterner;
+use crate::{Engine, RhaiResultOf, Scope, AST};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+impl Engine {
+ /// Evaluate a string as a script.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// engine.run("print(40 + 2);")?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn run(&self, script: &str) -> RhaiResultOf<()> {
+ self.run_with_scope(&mut Scope::new(), script)
+ }
+ /// Evaluate a string as a script with own scope.
+ ///
+ /// ## Constants Propagation
+ ///
+ /// If not [`OptimizationLevel::None`][crate::OptimizationLevel::None], constants defined within
+ /// the scope are propagated throughout the script _including_ functions.
+ ///
+ /// This allows functions to be optimized based on dynamic global constants.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Scope};
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Create initialized scope
+ /// let mut scope = Scope::new();
+ /// scope.push("x", 40_i64);
+ ///
+ /// engine.run_with_scope(&mut scope, "x += 2; print(x);")?;
+ ///
+ /// // The variable in the scope is modified
+ /// assert_eq!(scope.get_value::<i64>("x").expect("variable x should exist"), 42);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn run_with_scope(&self, scope: &mut Scope, script: &str) -> RhaiResultOf<()> {
+ let scripts = [script];
+ let ast = {
+ let (stream, tc) = self.lex_raw(&scripts, self.token_mapper.as_deref());
+
+ let mut interner;
+ let mut guard;
+ let interned_strings = if let Some(ref interner) = self.interned_strings {
+ guard = locked_write(interner);
+ &mut *guard
+ } else {
+ interner = StringsInterner::new();
+ &mut interner
+ };
+
+ let state = &mut ParseState::new(Some(scope), interned_strings, tc);
+ self.parse(stream.peekable(), state, self.optimization_level)?
+ };
+ self.run_ast_with_scope(scope, &ast)
+ }
+ /// Evaluate an [`AST`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Compile a script to an AST and store it for later evaluation
+ /// let ast = engine.compile("print(40 + 2);")?;
+ ///
+ /// // Evaluate it
+ /// engine.run_ast(&ast)?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn run_ast(&self, ast: &AST) -> RhaiResultOf<()> {
+ self.run_ast_with_scope(&mut Scope::new(), ast)
+ }
+ /// Evaluate an [`AST`] with own scope.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Scope};
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// // Create initialized scope
+ /// let mut scope = Scope::new();
+ /// scope.push("x", 40_i64);
+ ///
+ /// // Compile a script to an AST and store it for later evaluation
+ /// let ast = engine.compile("x += 2; x")?;
+ ///
+ /// // Evaluate it
+ /// engine.run_ast_with_scope(&mut scope, &ast)?;
+ ///
+ /// // The variable in the scope is modified
+ /// assert_eq!(scope.get_value::<i64>("x").expect("variable x should exist"), 42);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn run_ast_with_scope(&self, scope: &mut Scope, ast: &AST) -> RhaiResultOf<()> {
+ let caches = &mut Caches::new();
+ let global = &mut GlobalRuntimeState::new(self);
+ global.source = ast.source_raw().cloned();
+
+ #[cfg(not(feature = "no_function"))]
+ global.lib.push(ast.shared_lib().clone());
+
+ #[cfg(not(feature = "no_module"))]
+ {
+ global.embedded_module_resolver = ast.resolver().cloned();
+ }
+
+ let _ = self.eval_global_statements(global, caches, scope, ast.statements())?;
+
+ #[cfg(feature = "debugging")]
+ if self.is_debugger_registered() {
+ global.debugger_mut().status = crate::eval::DebuggerStatus::Terminate;
+ let node = &crate::ast::Stmt::Noop(crate::Position::NONE);
+ self.run_debugger(global, caches, scope, None, node)?;
+ }
+
+ Ok(())
+ }
+}
+
+/// Evaluate a string as a script.
+///
+/// # Example
+///
+/// ```
+/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+/// rhai::run("print(40 + 2);")?;
+/// # Ok(())
+/// # }
+/// ```
+#[inline(always)]
+pub fn run(script: &str) -> RhaiResultOf<()> {
+ Engine::new().run(script)
+}
diff --git a/rhai/src/ast/ast.rs b/rhai/src/ast/ast.rs
new file mode 100644
index 0000000..6734a44
--- /dev/null
+++ b/rhai/src/ast/ast.rs
@@ -0,0 +1,1081 @@
+//! Module defining the AST (abstract syntax tree).
+
+use super::{ASTFlags, Expr, FnAccess, Stmt, StmtBlock, StmtBlockContainer};
+use crate::{Dynamic, FnNamespace, ImmutableString, Position};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ borrow::Borrow,
+ fmt,
+ hash::Hash,
+ ops::{Add, AddAssign},
+ ptr,
+};
+
+/// Compiled AST (abstract syntax tree) of a Rhai script.
+///
+/// # Thread Safety
+///
+/// Currently, [`AST`] is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`.
+#[derive(Clone)]
+pub struct AST {
+ /// Source of the [`AST`].
+ source: Option<ImmutableString>,
+ /// [`AST`] documentation.
+ #[cfg(feature = "metadata")]
+ doc: Option<Box<crate::SmartString>>,
+ /// Global statements.
+ body: Option<Box<StmtBlock>>,
+ /// Script-defined functions.
+ #[cfg(not(feature = "no_function"))]
+ lib: crate::SharedModule,
+ /// Embedded module resolver, if any.
+ #[cfg(not(feature = "no_module"))]
+ resolver: Option<crate::Shared<crate::module::resolvers::StaticModuleResolver>>,
+}
+
+impl Default for AST {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::empty()
+ }
+}
+
+impl fmt::Debug for AST {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut fp = f.debug_struct("AST");
+
+ fp.field("source", &self.source);
+ #[cfg(feature = "metadata")]
+ fp.field("doc", &self.doc);
+ #[cfg(not(feature = "no_module"))]
+ fp.field("resolver", &self.resolver);
+
+ fp.field(
+ "body",
+ &self
+ .body
+ .as_deref()
+ .map(|b| b.as_slice())
+ .unwrap_or_default(),
+ );
+
+ #[cfg(not(feature = "no_function"))]
+ for (.., fn_def) in self.lib.iter_script_fn() {
+ let sig = fn_def.to_string();
+ fp.field(&sig, &fn_def.body.as_slice());
+ }
+
+ fp.finish()
+ }
+}
+
+impl AST {
+ /// Create a new [`AST`].
+ #[cfg(not(feature = "internals"))]
+ #[inline]
+ #[must_use]
+ pub(crate) fn new(
+ statements: impl IntoIterator<Item = Stmt>,
+ #[cfg(not(feature = "no_function"))] functions: impl Into<crate::SharedModule>,
+ ) -> Self {
+ let stmt = StmtBlock::new(statements, Position::NONE, Position::NONE);
+
+ Self {
+ source: None,
+ #[cfg(feature = "metadata")]
+ doc: None,
+ body: (!stmt.is_empty()).then(|| stmt.into()),
+ #[cfg(not(feature = "no_function"))]
+ lib: functions.into(),
+ #[cfg(not(feature = "no_module"))]
+ resolver: None,
+ }
+ }
+ /// _(internals)_ Create a new [`AST`].
+ /// Exported under the `internals` feature only.
+ #[cfg(feature = "internals")]
+ #[inline]
+ #[must_use]
+ pub fn new(
+ statements: impl IntoIterator<Item = Stmt>,
+ #[cfg(not(feature = "no_function"))] functions: impl Into<crate::SharedModule>,
+ ) -> Self {
+ let stmt = StmtBlock::new(statements, Position::NONE, Position::NONE);
+
+ Self {
+ source: None,
+ #[cfg(feature = "metadata")]
+ doc: None,
+ body: (!stmt.is_empty()).then(|| stmt.into()),
+ #[cfg(not(feature = "no_function"))]
+ lib: functions.into(),
+ #[cfg(not(feature = "no_module"))]
+ resolver: None,
+ }
+ }
+ /// Create a new [`AST`] with a source name.
+ #[cfg(not(feature = "internals"))]
+ #[inline]
+ #[must_use]
+ pub(crate) fn new_with_source(
+ statements: impl IntoIterator<Item = Stmt>,
+ #[cfg(not(feature = "no_function"))] functions: impl Into<crate::SharedModule>,
+ source: impl Into<ImmutableString>,
+ ) -> Self {
+ let mut ast = Self::new(
+ statements,
+ #[cfg(not(feature = "no_function"))]
+ functions,
+ );
+ ast.set_source(source);
+ ast
+ }
+ /// _(internals)_ Create a new [`AST`] with a source name.
+ /// Exported under the `internals` feature only.
+ #[cfg(feature = "internals")]
+ #[inline]
+ #[must_use]
+ pub fn new_with_source(
+ statements: impl IntoIterator<Item = Stmt>,
+ #[cfg(not(feature = "no_function"))] functions: impl Into<crate::SharedModule>,
+ source: impl Into<ImmutableString>,
+ ) -> Self {
+ let mut ast = Self::new(
+ statements,
+ #[cfg(not(feature = "no_function"))]
+ functions,
+ );
+ ast.set_source(source);
+ ast
+ }
+ /// Create an empty [`AST`].
+ #[inline]
+ #[must_use]
+ pub fn empty() -> Self {
+ Self {
+ source: None,
+ #[cfg(feature = "metadata")]
+ doc: None,
+ body: None,
+ #[cfg(not(feature = "no_function"))]
+ lib: crate::Module::new().into(),
+ #[cfg(not(feature = "no_module"))]
+ resolver: None,
+ }
+ }
+ /// Get the source, if any.
+ #[inline(always)]
+ #[must_use]
+ pub fn source(&self) -> Option<&str> {
+ self.source.as_ref().map(|s| s.as_str())
+ }
+ /// Get a reference to the source.
+ #[inline(always)]
+ #[must_use]
+ pub(crate) const fn source_raw(&self) -> Option<&ImmutableString> {
+ self.source.as_ref()
+ }
+ /// Set the source.
+ #[inline]
+ pub fn set_source(&mut self, source: impl Into<ImmutableString>) -> &mut Self {
+ let source = source.into();
+
+ #[cfg(not(feature = "no_function"))]
+ crate::Shared::get_mut(&mut self.lib)
+ .as_mut()
+ .map(|m| m.set_id(source.clone()));
+
+ self.source = (!source.is_empty()).then(|| source);
+
+ self
+ }
+ /// Clear the source.
+ #[inline(always)]
+ pub fn clear_source(&mut self) -> &mut Self {
+ self.source = None;
+ self
+ }
+ /// Get the documentation (if any).
+ /// Exported under the `metadata` feature only.
+ ///
+ /// Documentation is a collection of all comment lines beginning with `//!`.
+ ///
+ /// Leading white-spaces are stripped, and each line always starts with `//!`.
+ #[cfg(feature = "metadata")]
+ #[inline(always)]
+ #[must_use]
+ pub fn doc(&self) -> &str {
+ self.doc.as_ref().map(|s| s.as_str()).unwrap_or_default()
+ }
+ /// Clear the documentation.
+ /// Exported under the `metadata` feature only.
+ #[cfg(feature = "metadata")]
+ #[inline(always)]
+ pub fn clear_doc(&mut self) -> &mut Self {
+ self.doc = None;
+ self
+ }
+ /// Get a mutable reference to the documentation.
+ ///
+ /// Only available under `metadata`.
+ #[cfg(feature = "metadata")]
+ #[inline(always)]
+ #[must_use]
+ #[allow(dead_code)]
+ pub(crate) fn doc_mut(&mut self) -> Option<&mut crate::SmartString> {
+ self.doc.as_deref_mut()
+ }
+ /// Set the documentation.
+ ///
+ /// Only available under `metadata`.
+ #[cfg(feature = "metadata")]
+ #[inline(always)]
+ pub(crate) fn set_doc(&mut self, doc: impl Into<crate::SmartString>) {
+ let doc = doc.into();
+ self.doc = (!doc.is_empty()).then(|| doc.into());
+ }
+ /// Get the statements.
+ #[cfg(not(feature = "internals"))]
+ #[inline(always)]
+ #[must_use]
+ pub(crate) fn statements(&self) -> &[Stmt] {
+ self.body
+ .as_deref()
+ .map(StmtBlock::statements)
+ .unwrap_or_default()
+ }
+ /// _(internals)_ Get the statements.
+ /// Exported under the `internals` feature only.
+ #[cfg(feature = "internals")]
+ #[inline(always)]
+ #[must_use]
+ pub fn statements(&self) -> &[Stmt] {
+ self.body
+ .as_deref()
+ .map(StmtBlock::statements)
+ .unwrap_or_default()
+ }
+ /// Extract the statements.
+ #[allow(dead_code)]
+ #[inline(always)]
+ #[must_use]
+ pub(crate) fn take_statements(&mut self) -> StmtBlockContainer {
+ self.body
+ .as_deref_mut()
+ .map(StmtBlock::take_statements)
+ .unwrap_or_default()
+ }
+ /// Does this [`AST`] contain script-defined functions?
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ #[must_use]
+ pub fn has_functions(&self) -> bool {
+ !self.lib.is_empty()
+ }
+ /// Get the internal shared [`Module`][crate::Module] containing all script-defined functions.
+ #[cfg(not(feature = "internals"))]
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ #[must_use]
+ pub(crate) const fn shared_lib(&self) -> &crate::SharedModule {
+ &self.lib
+ }
+ /// _(internals)_ Get the internal shared [`Module`][crate::Module] containing all script-defined functions.
+ /// Exported under the `internals` feature only.
+ ///
+ /// Not available under `no_function`.
+ #[cfg(feature = "internals")]
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ #[must_use]
+ pub const fn shared_lib(&self) -> &crate::SharedModule {
+ &self.lib
+ }
+ /// Get the embedded [module resolver][crate::ModuleResolver].
+ #[cfg(not(feature = "internals"))]
+ #[cfg(not(feature = "no_module"))]
+ #[inline(always)]
+ #[must_use]
+ pub(crate) const fn resolver(
+ &self,
+ ) -> Option<&crate::Shared<crate::module::resolvers::StaticModuleResolver>> {
+ self.resolver.as_ref()
+ }
+ /// _(internals)_ Get the embedded [module resolver][crate::ModuleResolver].
+ /// Exported under the `internals` feature only.
+ ///
+ /// Not available under `no_module`.
+ #[cfg(feature = "internals")]
+ #[cfg(not(feature = "no_module"))]
+ #[inline(always)]
+ #[must_use]
+ pub const fn resolver(
+ &self,
+ ) -> Option<&crate::Shared<crate::module::resolvers::StaticModuleResolver>> {
+ self.resolver.as_ref()
+ }
+ /// Set the embedded [module resolver][crate::ModuleResolver].
+ #[cfg(not(feature = "no_module"))]
+ #[inline(always)]
+ pub(crate) fn set_resolver(
+ &mut self,
+ resolver: impl Into<crate::Shared<crate::module::resolvers::StaticModuleResolver>>,
+ ) -> &mut Self {
+ self.resolver = Some(resolver.into());
+ self
+ }
+ /// Clone the [`AST`]'s functions into a new [`AST`].
+ /// No statements are cloned.
+ ///
+ /// Not available under `no_function`.
+ ///
+ /// This operation is cheap because functions are shared.
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ #[must_use]
+ pub fn clone_functions_only(&self) -> Self {
+ self.clone_functions_only_filtered(|_, _, _, _, _| true)
+ }
+ /// Clone the [`AST`]'s functions into a new [`AST`] based on a filter predicate.
+ /// No statements are cloned.
+ ///
+ /// Not available under `no_function`.
+ ///
+ /// This operation is cheap because functions are shared.
+ #[cfg(not(feature = "no_function"))]
+ #[inline]
+ #[must_use]
+ pub fn clone_functions_only_filtered(
+ &self,
+ filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool,
+ ) -> Self {
+ let mut lib = crate::Module::new();
+ lib.merge_filtered(&self.lib, &filter);
+ Self {
+ source: self.source.clone(),
+ #[cfg(feature = "metadata")]
+ doc: self.doc.clone(),
+ body: None,
+ lib: lib.into(),
+ #[cfg(not(feature = "no_module"))]
+ resolver: self.resolver.clone(),
+ }
+ }
+ /// Clone the [`AST`]'s script statements into a new [`AST`].
+ /// No functions are cloned.
+ #[inline(always)]
+ #[must_use]
+ pub fn clone_statements_only(&self) -> Self {
+ Self {
+ source: self.source.clone(),
+ #[cfg(feature = "metadata")]
+ doc: self.doc.clone(),
+ body: self.body.clone(),
+ #[cfg(not(feature = "no_function"))]
+ lib: crate::Module::new().into(),
+ #[cfg(not(feature = "no_module"))]
+ resolver: self.resolver.clone(),
+ }
+ }
+ /// Merge two [`AST`] into one. Both [`AST`]'s are untouched and a new, merged,
+ /// version is returned.
+ ///
+ /// Statements in the second [`AST`] are simply appended to the end of the first _without any processing_.
+ /// Thus, the return value of the first [`AST`] (if using expression-statement syntax) is buried.
+ /// Of course, if the first [`AST`] uses a `return` statement at the end, then
+ /// the second [`AST`] will essentially be dead code.
+ ///
+ /// All script-defined functions in the second [`AST`] overwrite similarly-named functions
+ /// in the first [`AST`] with the same number of parameters.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// # #[cfg(not(feature = "no_function"))]
+ /// # {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// let ast1 = engine.compile("
+ /// fn foo(x) { 42 + x }
+ /// foo(1)
+ /// ")?;
+ ///
+ /// let ast2 = engine.compile(r#"
+ /// fn foo(n) { `hello${n}` }
+ /// foo("!")
+ /// "#)?;
+ ///
+ /// let ast = ast1.merge(&ast2); // Merge 'ast2' into 'ast1'
+ ///
+ /// // Notice that using the '+' operator also works:
+ /// // let ast = &ast1 + &ast2;
+ ///
+ /// // 'ast' is essentially:
+ /// //
+ /// // fn foo(n) { `hello${n}` } // <- definition of first 'foo' is overwritten
+ /// // foo(1) // <- notice this will be "hello1" instead of 43,
+ /// // // but it is no longer the return value
+ /// // foo("!") // returns "hello!"
+ ///
+ /// // Evaluate it
+ /// assert_eq!(engine.eval_ast::<String>(&ast)?, "hello!");
+ /// # }
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn merge(&self, other: &Self) -> Self {
+ self.merge_filtered_impl(other, |_, _, _, _, _| true)
+ }
+ /// Combine one [`AST`] with another. The second [`AST`] is consumed.
+ ///
+ /// Statements in the second [`AST`] are simply appended to the end of the first _without any processing_.
+ /// Thus, the return value of the first [`AST`] (if using expression-statement syntax) is buried.
+ /// Of course, if the first [`AST`] uses a `return` statement at the end, then
+ /// the second [`AST`] will essentially be dead code.
+ ///
+ /// All script-defined functions in the second [`AST`] overwrite similarly-named functions
+ /// in the first [`AST`] with the same number of parameters.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// # #[cfg(not(feature = "no_function"))]
+ /// # {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// let mut ast1 = engine.compile("
+ /// fn foo(x) { 42 + x }
+ /// foo(1)
+ /// ")?;
+ ///
+ /// let ast2 = engine.compile(r#"
+ /// fn foo(n) { `hello${n}` }
+ /// foo("!")
+ /// "#)?;
+ ///
+ /// ast1.combine(ast2); // Combine 'ast2' into 'ast1'
+ ///
+ /// // Notice that using the '+=' operator also works:
+ /// // ast1 += ast2;
+ ///
+ /// // 'ast1' is essentially:
+ /// //
+ /// // fn foo(n) { `hello${n}` } // <- definition of first 'foo' is overwritten
+ /// // foo(1) // <- notice this will be "hello1" instead of 43,
+ /// // // but it is no longer the return value
+ /// // foo("!") // returns "hello!"
+ ///
+ /// // Evaluate it
+ /// assert_eq!(engine.eval_ast::<String>(&ast1)?, "hello!");
+ /// # }
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline(always)]
+ pub fn combine(&mut self, other: Self) -> &mut Self {
+ self.combine_filtered_impl(other, |_, _, _, _, _| true)
+ }
+ /// Merge two [`AST`] into one. Both [`AST`]'s are untouched and a new, merged, version
+ /// is returned.
+ ///
+ /// Not available under `no_function`.
+ ///
+ /// Statements in the second [`AST`] are simply appended to the end of the first _without any processing_.
+ /// Thus, the return value of the first [`AST`] (if using expression-statement syntax) is buried.
+ /// Of course, if the first [`AST`] uses a `return` statement at the end, then
+ /// the second [`AST`] will essentially be dead code.
+ ///
+ /// All script-defined functions in the second [`AST`] are first selected based on a filter
+ /// predicate, then overwrite similarly-named functions in the first [`AST`] with the
+ /// same number of parameters.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// let ast1 = engine.compile("
+ /// fn foo(x) { 42 + x }
+ /// foo(1)
+ /// ")?;
+ ///
+ /// let ast2 = engine.compile(r#"
+ /// fn foo(n) { `hello${n}` }
+ /// fn error() { 0 }
+ /// foo("!")
+ /// "#)?;
+ ///
+ /// // Merge 'ast2', picking only 'error()' but not 'foo(..)', into 'ast1'
+ /// let ast = ast1.merge_filtered(&ast2, |_, _, script, name, params|
+ /// script && name == "error" && params == 0);
+ ///
+ /// // 'ast' is essentially:
+ /// //
+ /// // fn foo(n) { 42 + n } // <- definition of 'ast1::foo' is not overwritten
+ /// // // because 'ast2::foo' is filtered away
+ /// // foo(1) // <- notice this will be 43 instead of "hello1",
+ /// // // but it is no longer the return value
+ /// // fn error() { 0 } // <- this function passes the filter and is merged
+ /// // foo("!") // <- returns "42!"
+ ///
+ /// // Evaluate it
+ /// assert_eq!(engine.eval_ast::<String>(&ast)?, "42!");
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ #[must_use]
+ pub fn merge_filtered(
+ &self,
+ other: &Self,
+ filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool,
+ ) -> Self {
+ self.merge_filtered_impl(other, filter)
+ }
+ /// Merge two [`AST`] into one. Both [`AST`]'s are untouched and a new, merged, version
+ /// is returned.
+ #[inline]
+ #[must_use]
+ fn merge_filtered_impl(
+ &self,
+ other: &Self,
+ _filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool,
+ ) -> Self {
+ let merged = match (&self.body, &other.body) {
+ (Some(body), Some(other)) => {
+ let mut body = body.as_ref().clone();
+ body.extend(other.iter().cloned());
+ body
+ }
+ (Some(body), None) => body.as_ref().clone(),
+ (None, Some(body)) => body.as_ref().clone(),
+ (None, None) => StmtBlock::NONE,
+ };
+
+ #[cfg(not(feature = "no_function"))]
+ let lib = {
+ let mut lib = self.lib.as_ref().clone();
+ lib.merge_filtered(&other.lib, &_filter);
+ lib
+ };
+
+ let mut _ast = if let Some(ref source) = other.source {
+ Self::new_with_source(
+ merged,
+ #[cfg(not(feature = "no_function"))]
+ lib,
+ source.clone(),
+ )
+ } else {
+ Self::new(
+ merged,
+ #[cfg(not(feature = "no_function"))]
+ lib,
+ )
+ };
+
+ #[cfg(not(feature = "no_module"))]
+ match (
+ self.resolver().map_or(true, |r| r.is_empty()),
+ other.resolver().map_or(true, |r| r.is_empty()),
+ ) {
+ (true, true) => (),
+ (false, true) => {
+ _ast.set_resolver(self.resolver().unwrap().clone());
+ }
+ (true, false) => {
+ _ast.set_resolver(other.resolver().unwrap().clone());
+ }
+ (false, false) => {
+ let mut resolver = self.resolver().unwrap().as_ref().clone();
+ let other_resolver = other.resolver().unwrap().as_ref().clone();
+ for (k, v) in other_resolver {
+ resolver.insert(k, crate::func::shared_take_or_clone(v));
+ }
+ _ast.set_resolver(resolver);
+ }
+ }
+
+ #[cfg(feature = "metadata")]
+ if let Some(ref other_doc) = other.doc {
+ if let Some(ref mut ast_doc) = _ast.doc {
+ ast_doc.push('\n');
+ ast_doc.push_str(other_doc);
+ } else {
+ _ast.doc = Some(other_doc.clone());
+ }
+ }
+
+ _ast
+ }
+ /// Combine one [`AST`] with another. The second [`AST`] is consumed.
+ ///
+ /// Not available under `no_function`.
+ ///
+ /// Statements in the second [`AST`] are simply appended to the end of the first _without any processing_.
+ /// Thus, the return value of the first [`AST`] (if using expression-statement syntax) is buried.
+ /// Of course, if the first [`AST`] uses a `return` statement at the end, then
+ /// the second [`AST`] will essentially be dead code.
+ ///
+ /// All script-defined functions in the second [`AST`] are first selected based on a filter
+ /// predicate, then overwrite similarly-named functions in the first [`AST`] with the
+ /// same number of parameters.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// let mut ast1 = engine.compile("
+ /// fn foo(x) { 42 + x }
+ /// foo(1)
+ /// ")?;
+ ///
+ /// let ast2 = engine.compile(r#"
+ /// fn foo(n) { `hello${n}` }
+ /// fn error() { 0 }
+ /// foo("!")
+ /// "#)?;
+ ///
+ /// // Combine 'ast2', picking only 'error()' but not 'foo(..)', into 'ast1'
+ /// ast1.combine_filtered(ast2, |_, _, script, name, params|
+ /// script && name == "error" && params == 0);
+ ///
+ /// // 'ast1' is essentially:
+ /// //
+ /// // fn foo(n) { 42 + n } // <- definition of 'ast1::foo' is not overwritten
+ /// // // because 'ast2::foo' is filtered away
+ /// // foo(1) // <- notice this will be 43 instead of "hello1",
+ /// // // but it is no longer the return value
+ /// // fn error() { 0 } // <- this function passes the filter and is merged
+ /// // foo("!") // <- returns "42!"
+ ///
+ /// // Evaluate it
+ /// assert_eq!(engine.eval_ast::<String>(&ast1)?, "42!");
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ pub fn combine_filtered(
+ &mut self,
+ other: Self,
+ filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool,
+ ) -> &mut Self {
+ self.combine_filtered_impl(other, filter)
+ }
+ /// Combine one [`AST`] with another. The second [`AST`] is consumed.
+ fn combine_filtered_impl(
+ &mut self,
+ other: Self,
+ _filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool,
+ ) -> &mut Self {
+ #[cfg(not(feature = "no_module"))]
+ match (
+ self.resolver().map_or(true, |r| r.is_empty()),
+ other.resolver().map_or(true, |r| r.is_empty()),
+ ) {
+ (_, true) => (),
+ (true, false) => {
+ self.set_resolver(other.resolver.unwrap());
+ }
+ (false, false) => {
+ let resolver = crate::func::shared_make_mut(self.resolver.as_mut().unwrap());
+ let other_resolver = crate::func::shared_take_or_clone(other.resolver.unwrap());
+ for (k, v) in other_resolver {
+ resolver.insert(k, crate::func::shared_take_or_clone(v));
+ }
+ }
+ }
+
+ match (&mut self.body, other.body) {
+ (Some(body), Some(other)) => body.extend(other.into_iter()),
+ (Some(_), None) => (),
+ (None, body @ Some(_)) => self.body = body,
+ (None, None) => (),
+ }
+
+ #[cfg(not(feature = "no_function"))]
+ if !other.lib.is_empty() {
+ crate::func::shared_make_mut(&mut self.lib).merge_filtered(&other.lib, &_filter);
+ }
+
+ #[cfg(feature = "metadata")]
+ if let Some(other_doc) = other.doc {
+ if let Some(ref mut self_doc) = self.doc {
+ self_doc.push('\n');
+ self_doc.push_str(&other_doc);
+ } else {
+ self.doc = Some(other_doc);
+ }
+ }
+
+ self
+ }
+ /// Filter out the functions, retaining only some based on a filter predicate.
+ ///
+ /// Not available under `no_function`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// # #[cfg(not(feature = "no_function"))]
+ /// # {
+ /// use rhai::Engine;
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// let mut ast = engine.compile(r#"
+ /// fn foo(n) { n + 1 }
+ /// fn bar() { print("hello"); }
+ /// "#)?;
+ ///
+ /// // Remove all functions except 'foo(..)'
+ /// ast.retain_functions(|_, _, name, params| name == "foo" && params == 1);
+ /// # }
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[cfg(not(feature = "no_function"))]
+ #[inline]
+ pub fn retain_functions(
+ &mut self,
+ filter: impl Fn(FnNamespace, FnAccess, &str, usize) -> bool,
+ ) -> &mut Self {
+ if self.has_functions() {
+ crate::func::shared_make_mut(&mut self.lib).retain_script_functions(filter);
+ }
+ self
+ }
+ /// _(internals)_ Iterate through all function definitions.
+ /// Exported under the `internals` feature only.
+ ///
+ /// Not available under `no_function`.
+ #[cfg(feature = "internals")]
+ #[cfg(not(feature = "no_function"))]
+ #[inline]
+ pub fn iter_fn_def(&self) -> impl Iterator<Item = &crate::Shared<super::ScriptFnDef>> {
+ self.lib.iter_script_fn().map(|(.., fn_def)| fn_def)
+ }
+ /// Iterate through all function definitions.
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "internals"))]
+ #[cfg(not(feature = "no_function"))]
+ #[allow(dead_code)]
+ #[inline]
+ pub(crate) fn iter_fn_def(&self) -> impl Iterator<Item = &crate::Shared<super::ScriptFnDef>> {
+ self.lib.iter_script_fn().map(|(.., fn_def)| fn_def)
+ }
+ /// Iterate through all function definitions.
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ #[inline]
+ pub fn iter_functions(&self) -> impl Iterator<Item = super::ScriptFnMetadata> {
+ self.lib
+ .iter_script_fn()
+ .map(|(.., fn_def)| fn_def.as_ref().into())
+ }
+ /// Clear all function definitions in the [`AST`].
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ pub fn clear_functions(&mut self) -> &mut Self {
+ self.lib = crate::Module::new().into();
+ self
+ }
+ /// Clear all statements in the [`AST`], leaving only function definitions.
+ #[inline(always)]
+ pub fn clear_statements(&mut self) -> &mut Self {
+ self.body = None;
+ self
+ }
+ /// Extract all top-level literal constant and/or variable definitions.
+ /// This is useful for extracting all global constants from a script without actually running it.
+ ///
+ /// A literal constant/variable definition takes the form of:
+ /// `const VAR = `_value_`;` and `let VAR = `_value_`;`
+ /// where _value_ is a literal expression or will be optimized into a literal.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Scope};
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// let ast = engine.compile(
+ /// "
+ /// const A = 40 + 2; // constant that optimizes into a literal
+ /// let b = 123; // literal variable
+ /// const B = b * A; // non-literal constant
+ /// const C = 999; // literal constant
+ /// b = A + C; // expression
+ ///
+ /// { // <- new block scope
+ /// const Z = 0; // <- literal constant not at top-level
+ ///
+ /// print(Z); // make sure the block is not optimized away
+ /// }
+ /// ")?;
+ ///
+ /// let mut iter = ast.iter_literal_variables(true, false)
+ /// .map(|(name, is_const, value)| (name, is_const, value.as_int().unwrap()));
+ ///
+ /// # #[cfg(not(feature = "no_optimize"))]
+ /// assert_eq!(iter.next(), Some(("A", true, 42)));
+ /// assert_eq!(iter.next(), Some(("C", true, 999)));
+ /// assert_eq!(iter.next(), None);
+ ///
+ /// let mut iter = ast.iter_literal_variables(false, true)
+ /// .map(|(name, is_const, value)| (name, is_const, value.as_int().unwrap()));
+ ///
+ /// assert_eq!(iter.next(), Some(("b", false, 123)));
+ /// assert_eq!(iter.next(), None);
+ ///
+ /// let mut iter = ast.iter_literal_variables(true, true)
+ /// .map(|(name, is_const, value)| (name, is_const, value.as_int().unwrap()));
+ ///
+ /// # #[cfg(not(feature = "no_optimize"))]
+ /// assert_eq!(iter.next(), Some(("A", true, 42)));
+ /// assert_eq!(iter.next(), Some(("b", false, 123)));
+ /// assert_eq!(iter.next(), Some(("C", true, 999)));
+ /// assert_eq!(iter.next(), None);
+ ///
+ /// let scope: Scope = ast.iter_literal_variables(true, false).collect();
+ ///
+ /// # #[cfg(not(feature = "no_optimize"))]
+ /// assert_eq!(scope.len(), 2);
+ ///
+ /// Ok(())
+ /// # }
+ /// ```
+ pub fn iter_literal_variables(
+ &self,
+ include_constants: bool,
+ include_variables: bool,
+ ) -> impl Iterator<Item = (&str, bool, Dynamic)> {
+ self.statements().iter().filter_map(move |stmt| match stmt {
+ Stmt::Var(x, options, ..)
+ if options.contains(ASTFlags::CONSTANT) && include_constants
+ || !options.contains(ASTFlags::CONSTANT) && include_variables =>
+ {
+ let (name, expr, ..) = &**x;
+ expr.get_literal_value()
+ .map(|value| (name.as_str(), options.contains(ASTFlags::CONSTANT), value))
+ }
+ _ => None,
+ })
+ }
+ /// Recursively walk the [`AST`], including function bodies (if any).
+ /// Return `false` from the callback to terminate the walk.
+ #[cfg(not(feature = "internals"))]
+ #[cfg(not(feature = "no_module"))]
+ #[inline(always)]
+ pub(crate) fn walk(&self, on_node: &mut impl FnMut(&[ASTNode]) -> bool) -> bool {
+ self._walk(on_node)
+ }
+ /// _(internals)_ Recursively walk the [`AST`], including function bodies (if any).
+ /// Return `false` from the callback to terminate the walk.
+ /// Exported under the `internals` feature only.
+ #[cfg(feature = "internals")]
+ #[inline(always)]
+ pub fn walk(&self, on_node: &mut impl FnMut(&[ASTNode]) -> bool) -> bool {
+ self._walk(on_node)
+ }
+ /// Recursively walk the [`AST`], including function bodies (if any).
+ /// Return `false` from the callback to terminate the walk.
+ fn _walk(&self, on_node: &mut impl FnMut(&[ASTNode]) -> bool) -> bool {
+ let path = &mut Vec::new();
+
+ for stmt in self.statements() {
+ if !stmt.walk(path, on_node) {
+ return false;
+ }
+ }
+ #[cfg(not(feature = "no_function"))]
+ for stmt in self.iter_fn_def().flat_map(|f| f.body.iter()) {
+ if !stmt.walk(path, on_node) {
+ return false;
+ }
+ }
+
+ true
+ }
+}
+
+impl<A: AsRef<AST>> Add<A> for &AST {
+ type Output = AST;
+
+ #[inline(always)]
+ fn add(self, rhs: A) -> Self::Output {
+ self.merge(rhs.as_ref())
+ }
+}
+
+impl<A: Into<Self>> AddAssign<A> for AST {
+ #[inline(always)]
+ fn add_assign(&mut self, rhs: A) {
+ self.combine(rhs.into());
+ }
+}
+
+impl Borrow<[Stmt]> for AST {
+ #[inline(always)]
+ #[must_use]
+ fn borrow(&self) -> &[Stmt] {
+ self.statements()
+ }
+}
+
+impl AsRef<[Stmt]> for AST {
+ #[inline(always)]
+ #[must_use]
+ fn as_ref(&self) -> &[Stmt] {
+ self.statements()
+ }
+}
+
+#[cfg(not(feature = "no_function"))]
+impl Borrow<crate::Module> for AST {
+ #[inline(always)]
+ #[must_use]
+ fn borrow(&self) -> &crate::Module {
+ self.shared_lib()
+ }
+}
+
+#[cfg(not(feature = "no_function"))]
+impl AsRef<crate::Module> for AST {
+ #[inline(always)]
+ #[must_use]
+ fn as_ref(&self) -> &crate::Module {
+ self.shared_lib().as_ref()
+ }
+}
+
+#[cfg(not(feature = "no_function"))]
+impl Borrow<crate::SharedModule> for AST {
+ #[inline(always)]
+ #[must_use]
+ fn borrow(&self) -> &crate::SharedModule {
+ self.shared_lib()
+ }
+}
+
+#[cfg(not(feature = "no_function"))]
+impl AsRef<crate::SharedModule> for AST {
+ #[inline(always)]
+ #[must_use]
+ fn as_ref(&self) -> &crate::SharedModule {
+ self.shared_lib()
+ }
+}
+
+/// _(internals)_ An [`AST`] node, consisting of either an [`Expr`] or a [`Stmt`].
+/// Exported under the `internals` feature only.
+#[derive(Debug, Clone, Copy, Hash)]
+#[non_exhaustive]
+pub enum ASTNode<'a> {
+ /// A statement ([`Stmt`]).
+ Stmt(&'a Stmt),
+ /// An expression ([`Expr`]).
+ Expr(&'a Expr),
+}
+
+impl<'a> From<&'a Stmt> for ASTNode<'a> {
+ #[inline(always)]
+ fn from(stmt: &'a Stmt) -> Self {
+ Self::Stmt(stmt)
+ }
+}
+
+impl<'a> From<&'a Expr> for ASTNode<'a> {
+ #[inline(always)]
+ fn from(expr: &'a Expr) -> Self {
+ Self::Expr(expr)
+ }
+}
+
+impl PartialEq for ASTNode<'_> {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (Self::Stmt(x), Self::Stmt(y)) => ptr::eq(*x, *y),
+ (Self::Expr(x), Self::Expr(y)) => ptr::eq(*x, *y),
+ _ => false,
+ }
+ }
+}
+
+impl Eq for ASTNode<'_> {}
+
+impl ASTNode<'_> {
+ /// Is this [`ASTNode`] a [`Stmt`]?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_stmt(&self) -> bool {
+ matches!(self, Self::Stmt(..))
+ }
+ /// Is this [`ASTNode`] an [`Expr`]?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_expr(&self) -> bool {
+ matches!(self, Self::Expr(..))
+ }
+ /// Get the [`Position`] of this [`ASTNode`].
+ #[inline]
+ #[must_use]
+ pub fn position(&self) -> Position {
+ match self {
+ Self::Stmt(stmt) => stmt.position(),
+ Self::Expr(expr) => expr.position(),
+ }
+ }
+}
+
+impl AST {
+ /// _(internals)_ Get the internal [`Module`][crate::Module] containing all script-defined functions.
+ /// Exported under the `internals` feature only.
+ ///
+ /// Not available under `no_function`.
+ ///
+ /// # Deprecated
+ ///
+ /// This method is deprecated. Use [`shared_lib`][AST::shared_lib] instead.
+ ///
+ /// This method will be removed in the next major version.
+ #[deprecated(since = "1.3.0", note = "use `shared_lib` instead")]
+ #[cfg(feature = "internals")]
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ #[must_use]
+ pub fn lib(&self) -> &crate::Module {
+ &self.lib
+ }
+}
diff --git a/rhai/src/ast/expr.rs b/rhai/src/ast/expr.rs
new file mode 100644
index 0000000..98cfbd8
--- /dev/null
+++ b/rhai/src/ast/expr.rs
@@ -0,0 +1,920 @@
+//! Module defining script expressions.
+
+use super::{ASTFlags, ASTNode, Ident, Namespace, Stmt, StmtBlock};
+use crate::engine::{KEYWORD_FN_PTR, OP_EXCLUSIVE_RANGE, OP_INCLUSIVE_RANGE};
+use crate::tokenizer::Token;
+use crate::types::dynamic::Union;
+use crate::{
+ calc_fn_hash, Dynamic, FnArgsVec, FnPtr, Identifier, ImmutableString, Position, SmartString,
+ StaticVec, INT,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ collections::BTreeMap,
+ fmt,
+ fmt::Write,
+ hash::Hash,
+ iter::once,
+ mem,
+ num::{NonZeroU8, NonZeroUsize},
+};
+
+/// _(internals)_ A binary expression.
+/// Exported under the `internals` feature only.
+#[derive(Debug, Clone, Hash)]
+pub struct BinaryExpr {
+ /// LHS expression.
+ pub lhs: Expr,
+ /// RHS expression.
+ pub rhs: Expr,
+}
+
+impl From<(Expr, Expr)> for BinaryExpr {
+ #[inline(always)]
+ fn from(value: (Expr, Expr)) -> Self {
+ Self {
+ lhs: value.0,
+ rhs: value.1,
+ }
+ }
+}
+
+/// _(internals)_ A custom syntax expression.
+/// Exported under the `internals` feature only.
+///
+/// Not available under `no_custom_syntax`.
+#[cfg(not(feature = "no_custom_syntax"))]
+#[derive(Debug, Clone, Hash)]
+pub struct CustomExpr {
+ /// List of keywords.
+ pub inputs: StaticVec<Expr>,
+ /// List of tokens actually parsed.
+ pub tokens: StaticVec<ImmutableString>,
+ /// State value.
+ pub state: Dynamic,
+ /// Is the current [`Scope`][crate::Scope] possibly modified by this custom statement
+ /// (e.g. introducing a new variable)?
+ pub scope_may_be_changed: bool,
+ /// Is this custom syntax self-terminated?
+ pub self_terminated: bool,
+}
+
+#[cfg(not(feature = "no_custom_syntax"))]
+impl CustomExpr {
+ /// Is this custom syntax self-terminated (i.e. no need for a semicolon terminator)?
+ ///
+ /// A self-terminated custom syntax always ends in `$block$`, `}` or `;`
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_self_terminated(&self) -> bool {
+ self.self_terminated
+ }
+}
+
+/// _(internals)_ A set of function call hashes. Exported under the `internals` feature only.
+///
+/// Two separate hashes are pre-calculated because of the following patterns:
+///
+/// ```rhai
+/// func(a, b, c); // Native: func(a, b, c) - 3 parameters
+/// // Script: func(a, b, c) - 3 parameters
+///
+/// a.func(b, c); // Native: func(&mut a, b, c) - 3 parameters
+/// // Script: func(b, c) - 2 parameters
+/// ```
+///
+/// For normal function calls, the native hash equals the script hash.
+///
+/// For method-style calls, the script hash contains one fewer parameter.
+///
+/// Function call hashes are used in the following manner:
+///
+/// * First, the script hash (if any) is tried, which contains only the called function's name plus
+/// the number of parameters.
+///
+/// * Next, the actual types of arguments are hashed and _combined_ with the native hash, which is
+/// then used to search for a native function.
+///
+/// In other words, a complete native function call hash always contains the called function's
+/// name plus the types of the arguments. This is due to possible function overloading for
+/// different parameter types.
+#[derive(Clone, Copy, Eq, PartialEq, Hash)]
+pub struct FnCallHashes {
+ /// Pre-calculated hash for a script-defined function ([`None`] if native functions only).
+ #[cfg(not(feature = "no_function"))]
+ script: Option<u64>,
+ /// Pre-calculated hash for a native Rust function with no parameter types.
+ native: u64,
+}
+
+impl fmt::Debug for FnCallHashes {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ #[cfg(not(feature = "no_function"))]
+ if let Some(script) = self.script {
+ return if script == self.native {
+ fmt::Debug::fmt(&self.native, f)
+ } else {
+ write!(f, "({script}, {})", self.native)
+ };
+ }
+
+ write!(f, "{} (native only)", self.native)
+ }
+}
+
+impl FnCallHashes {
+ /// Create a [`FnCallHashes`] from a single hash.
+ #[inline]
+ #[must_use]
+ pub const fn from_hash(hash: u64) -> Self {
+ Self {
+ #[cfg(not(feature = "no_function"))]
+ script: Some(hash),
+ native: hash,
+ }
+ }
+ /// Create a [`FnCallHashes`] with only the native Rust hash.
+ #[inline]
+ #[must_use]
+ pub const fn from_native_only(hash: u64) -> Self {
+ Self {
+ #[cfg(not(feature = "no_function"))]
+ script: None,
+ native: hash,
+ }
+ }
+ /// Create a [`FnCallHashes`] with both script function and native Rust hashes.
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ #[inline]
+ #[must_use]
+ pub const fn from_script_and_native(script: u64, native: u64) -> Self {
+ Self {
+ script: Some(script),
+ native,
+ }
+ }
+ /// Is this [`FnCallHashes`] native-only?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_native_only(&self) -> bool {
+ #[cfg(not(feature = "no_function"))]
+ return self.script.is_none();
+ #[cfg(feature = "no_function")]
+ return true;
+ }
+ /// Get the native hash.
+ ///
+ /// The hash returned is never zero.
+ #[inline(always)]
+ #[must_use]
+ pub const fn native(&self) -> u64 {
+ self.native
+ }
+ /// Get the script hash.
+ ///
+ /// The hash returned is never zero.
+ ///
+ /// # Panics
+ ///
+ /// Panics if this [`FnCallHashes`] is native-only.
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ #[must_use]
+ pub fn script(&self) -> u64 {
+ debug_assert!(self.script.is_some());
+ self.script.unwrap()
+ }
+}
+
+/// _(internals)_ A function call.
+/// Exported under the `internals` feature only.
+#[derive(Clone, Hash)]
+pub struct FnCallExpr {
+ /// Namespace of the function, if any.
+ pub namespace: Namespace,
+ /// Function name.
+ pub name: ImmutableString,
+ /// Pre-calculated hashes.
+ pub hashes: FnCallHashes,
+ /// List of function call argument expressions.
+ pub args: FnArgsVec<Expr>,
+ /// Does this function call capture the parent scope?
+ pub capture_parent_scope: bool,
+ /// Is this function call a native operator?
+ pub op_token: Option<Token>,
+}
+
+impl fmt::Debug for FnCallExpr {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut ff = f.debug_struct("FnCallExpr");
+ if !self.namespace.is_empty() {
+ ff.field("namespace", &self.namespace);
+ }
+ ff.field("hash", &self.hashes)
+ .field("name", &self.name)
+ .field("args", &self.args);
+ if self.op_token.is_some() {
+ ff.field("op_token", &self.op_token);
+ }
+ if self.capture_parent_scope {
+ ff.field("capture_parent_scope", &self.capture_parent_scope);
+ }
+ ff.finish()
+ }
+}
+
+impl FnCallExpr {
+ /// Does this function call contain a qualified namespace?
+ ///
+ /// Always `false` under `no_module`.
+ #[inline(always)]
+ #[must_use]
+ pub fn is_qualified(&self) -> bool {
+ !self.namespace.is_empty()
+ }
+ /// Convert this into an [`Expr::FnCall`].
+ #[inline(always)]
+ #[must_use]
+ pub fn into_fn_call_expr(self, pos: Position) -> Expr {
+ Expr::FnCall(self.into(), pos)
+ }
+ /// Are all arguments constant?
+ #[inline]
+ #[must_use]
+ pub fn constant_args(&self) -> bool {
+ self.args.is_empty() || self.args.iter().all(Expr::is_constant)
+ }
+}
+
+/// _(internals)_ An expression sub-tree.
+/// Exported under the `internals` feature only.
+#[derive(Clone, Hash)]
+#[non_exhaustive]
+pub enum Expr {
+ /// Dynamic constant.
+ ///
+ /// Used to hold complex constants such as [`Array`][crate::Array] or [`Map`][crate::Map] for quick cloning.
+ /// Primitive data types should use the appropriate variants to avoid an allocation.
+ ///
+ /// The [`Dynamic`] value is boxed in order to avoid bloating the size of [`Expr`].
+ DynamicConstant(Box<Dynamic>, Position),
+ /// Boolean constant.
+ BoolConstant(bool, Position),
+ /// Integer constant.
+ IntegerConstant(INT, Position),
+ /// Floating-point constant.
+ #[cfg(not(feature = "no_float"))]
+ FloatConstant(crate::types::FloatWrapper<crate::FLOAT>, Position),
+ /// Character constant.
+ CharConstant(char, Position),
+ /// [String][ImmutableString] constant.
+ StringConstant(ImmutableString, Position),
+ /// An interpolated [string][ImmutableString].
+ InterpolatedString(Box<FnArgsVec<Expr>>, Position),
+ /// [ expr, ... ]
+ Array(Box<FnArgsVec<Expr>>, Position),
+ /// #{ name:expr, ... }
+ Map(
+ Box<(StaticVec<(Ident, Expr)>, BTreeMap<Identifier, Dynamic>)>,
+ Position,
+ ),
+ /// ()
+ Unit(Position),
+ /// Variable access - (optional long index, namespace, namespace hash, variable name), optional short index, position
+ ///
+ /// The short index is [`u8`] which is used when the index is <= 255, which should be
+ /// the vast majority of cases (unless there are more than 255 variables defined!).
+ /// This is to avoid reading a pointer redirection during each variable access.
+ Variable(
+ Box<(Option<NonZeroUsize>, Namespace, u64, ImmutableString)>,
+ Option<NonZeroU8>,
+ Position,
+ ),
+ /// `this`.
+ ThisPtr(Position),
+ /// Property access - ((getter, hash), (setter, hash), prop)
+ Property(
+ Box<(
+ (ImmutableString, u64),
+ (ImmutableString, u64),
+ ImmutableString,
+ )>,
+ Position,
+ ),
+ /// xxx `.` method `(` expr `,` ... `)`
+ MethodCall(Box<FnCallExpr>, Position),
+ /// { [statement][Stmt] ... }
+ Stmt(Box<StmtBlock>),
+ /// func `(` expr `,` ... `)`
+ FnCall(Box<FnCallExpr>, Position),
+ /// lhs `.` rhs | lhs `?.` rhs
+ ///
+ /// ### Flags
+ ///
+ /// * [`NEGATED`][ASTFlags::NEGATED] = `?.` (`.` if unset)
+ /// * [`BREAK`][ASTFlags::BREAK] = terminate the chain (recurse into the chain if unset)
+ Dot(Box<BinaryExpr>, ASTFlags, Position),
+ /// lhs `[` rhs `]`
+ ///
+ /// ### Flags
+ ///
+ /// * [`NEGATED`][ASTFlags::NEGATED] = `?[` ... `]` (`[` ... `]` if unset)
+ /// * [`BREAK`][ASTFlags::BREAK] = terminate the chain (recurse into the chain if unset)
+ Index(Box<BinaryExpr>, ASTFlags, Position),
+ /// lhs `&&` rhs
+ And(Box<BinaryExpr>, Position),
+ /// lhs `||` rhs
+ Or(Box<BinaryExpr>, Position),
+ /// lhs `??` rhs
+ Coalesce(Box<BinaryExpr>, Position),
+ /// Custom syntax
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Custom(Box<CustomExpr>, Position),
+}
+
+impl Default for Expr {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::Unit(Position::NONE)
+ }
+}
+
+impl fmt::Debug for Expr {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut display_pos = self.start_position();
+
+ match self {
+ Self::DynamicConstant(value, ..) => write!(f, "{value:?}"),
+ Self::BoolConstant(value, ..) => write!(f, "{value:?}"),
+ Self::IntegerConstant(value, ..) => write!(f, "{value:?}"),
+ #[cfg(not(feature = "no_float"))]
+ Self::FloatConstant(value, ..) => write!(f, "{value:?}"),
+ Self::CharConstant(value, ..) => write!(f, "{value:?}"),
+ Self::StringConstant(value, ..) => write!(f, "{value:?}"),
+ Self::Unit(..) => f.write_str("()"),
+
+ Self::InterpolatedString(x, ..) => {
+ f.write_str("InterpolatedString")?;
+ return f.debug_list().entries(x.iter()).finish();
+ }
+ Self::Array(x, ..) => {
+ f.write_str("Array")?;
+ f.debug_list().entries(x.iter()).finish()
+ }
+ Self::Map(x, ..) => {
+ f.write_str("Map")?;
+ f.debug_map()
+ .entries(x.0.iter().map(|(k, v)| (k, v)))
+ .finish()
+ }
+ Self::ThisPtr(..) => f.debug_struct("ThisPtr").finish(),
+ Self::Variable(x, i, ..) => {
+ f.write_str("Variable(")?;
+
+ #[cfg(not(feature = "no_module"))]
+ if !x.1.is_empty() {
+ write!(f, "{}{}", x.1, crate::engine::NAMESPACE_SEPARATOR)?;
+ let pos = x.1.position();
+ if !pos.is_none() {
+ display_pos = pos;
+ }
+ }
+ f.write_str(&x.3)?;
+ #[cfg(not(feature = "no_module"))]
+ if let Some(n) = x.1.index() {
+ write!(f, " #{n}")?;
+ }
+ if let Some(n) = i.map_or_else(|| x.0, |n| NonZeroUsize::new(n.get() as usize)) {
+ write!(f, " #{n}")?;
+ }
+ f.write_str(")")
+ }
+ Self::Property(x, ..) => write!(f, "Property({})", x.2),
+ Self::MethodCall(x, ..) => f.debug_tuple("MethodCall").field(x).finish(),
+ Self::Stmt(x) => {
+ let pos = x.span();
+ if !pos.is_none() {
+ display_pos = pos.start();
+ }
+ f.write_str("ExprStmtBlock")?;
+ f.debug_list().entries(x.iter()).finish()
+ }
+ Self::FnCall(x, ..) => fmt::Debug::fmt(x, f),
+ Self::Index(x, options, pos) => {
+ if !pos.is_none() {
+ display_pos = *pos;
+ }
+
+ let mut f = f.debug_struct("Index");
+
+ f.field("lhs", &x.lhs).field("rhs", &x.rhs);
+ if !options.is_empty() {
+ f.field("options", options);
+ }
+ f.finish()
+ }
+ Self::Dot(x, options, pos) => {
+ if !pos.is_none() {
+ display_pos = *pos;
+ }
+
+ let mut f = f.debug_struct("Dot");
+
+ f.field("lhs", &x.lhs).field("rhs", &x.rhs);
+ if !options.is_empty() {
+ f.field("options", options);
+ }
+ f.finish()
+ }
+ Self::And(x, pos) | Self::Or(x, pos) | Self::Coalesce(x, pos) => {
+ let op_name = match self {
+ Self::And(..) => "And",
+ Self::Or(..) => "Or",
+ Self::Coalesce(..) => "Coalesce",
+ expr => unreachable!("`And`, `Or` or `Coalesce` expected but gets {:?}", expr),
+ };
+
+ if !pos.is_none() {
+ display_pos = *pos;
+ }
+
+ f.debug_struct(op_name)
+ .field("lhs", &x.lhs)
+ .field("rhs", &x.rhs)
+ .finish()
+ }
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Self::Custom(x, ..) => f.debug_tuple("Custom").field(x).finish(),
+ }?;
+
+ write!(f, " @ {display_pos:?}")
+ }
+}
+
+impl Expr {
+ /// Get the [`Dynamic`] value of a literal constant expression.
+ ///
+ /// Returns [`None`] if the expression is not a literal constant.
+ #[inline]
+ #[must_use]
+ pub fn get_literal_value(&self) -> Option<Dynamic> {
+ Some(match self {
+ Self::DynamicConstant(x, ..) => x.as_ref().clone(),
+ Self::IntegerConstant(x, ..) => (*x).into(),
+ #[cfg(not(feature = "no_float"))]
+ Self::FloatConstant(x, ..) => (*x).into(),
+ Self::CharConstant(x, ..) => (*x).into(),
+ Self::StringConstant(x, ..) => x.clone().into(),
+ Self::BoolConstant(x, ..) => (*x).into(),
+ Self::Unit(..) => Dynamic::UNIT,
+
+ #[cfg(not(feature = "no_index"))]
+ Self::Array(x, ..) if self.is_constant() => {
+ let mut arr = crate::Array::with_capacity(x.len());
+ arr.extend(x.iter().map(|v| v.get_literal_value().unwrap()));
+ Dynamic::from_array(arr)
+ }
+
+ #[cfg(not(feature = "no_object"))]
+ Self::Map(x, ..) if self.is_constant() => {
+ let mut map = x.1.clone();
+
+ for (k, v) in &x.0 {
+ *map.get_mut(k.name.as_str()).unwrap() = v.get_literal_value().unwrap();
+ }
+
+ Dynamic::from_map(map)
+ }
+
+ // Interpolated string
+ Self::InterpolatedString(x, ..) if self.is_constant() => {
+ let mut s = SmartString::new_const();
+ for segment in x.iter() {
+ let v = segment.get_literal_value().unwrap();
+ write!(&mut s, "{v}").unwrap();
+ }
+ s.into()
+ }
+
+ // Fn
+ Self::FnCall(ref x, ..)
+ if !x.is_qualified() && x.args.len() == 1 && x.name == KEYWORD_FN_PTR =>
+ {
+ if let Self::StringConstant(ref s, ..) = x.args[0] {
+ FnPtr::new(s.clone()).ok()?.into()
+ } else {
+ return None;
+ }
+ }
+
+ // Binary operators
+ Self::FnCall(x, ..) if !x.is_qualified() && x.args.len() == 2 => {
+ match x.name.as_str() {
+ // x..y
+ OP_EXCLUSIVE_RANGE => {
+ if let Self::IntegerConstant(ref start, ..) = x.args[0] {
+ if let Self::IntegerConstant(ref end, ..) = x.args[1] {
+ (*start..*end).into()
+ } else {
+ return None;
+ }
+ } else {
+ return None;
+ }
+ }
+ // x..=y
+ OP_INCLUSIVE_RANGE => {
+ if let Self::IntegerConstant(ref start, ..) = x.args[0] {
+ if let Self::IntegerConstant(ref end, ..) = x.args[1] {
+ (*start..=*end).into()
+ } else {
+ return None;
+ }
+ } else {
+ return None;
+ }
+ }
+ _ => return None,
+ }
+ }
+
+ _ => return None,
+ })
+ }
+ /// Create an [`Expr`] from a [`Dynamic`] value.
+ #[inline]
+ #[must_use]
+ pub fn from_dynamic(value: Dynamic, pos: Position) -> Self {
+ match value.0 {
+ Union::Unit(..) => Self::Unit(pos),
+ Union::Bool(b, ..) => Self::BoolConstant(b, pos),
+ Union::Str(s, ..) => Self::StringConstant(s, pos),
+ Union::Char(c, ..) => Self::CharConstant(c, pos),
+ Union::Int(i, ..) => Self::IntegerConstant(i, pos),
+
+ #[cfg(feature = "decimal")]
+ Union::Decimal(value, ..) => Self::DynamicConstant(Box::new((*value).into()), pos),
+
+ #[cfg(not(feature = "no_float"))]
+ Union::Float(f, ..) => Self::FloatConstant(f, pos),
+
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(a, ..) => Self::DynamicConstant(Box::new((*a).into()), pos),
+
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(m, ..) => Self::DynamicConstant(Box::new((*m).into()), pos),
+
+ Union::FnPtr(f, ..) if !f.is_curried() => Self::FnCall(
+ FnCallExpr {
+ namespace: Namespace::NONE,
+ name: KEYWORD_FN_PTR.into(),
+ hashes: FnCallHashes::from_hash(calc_fn_hash(None, f.fn_name(), 1)),
+ args: once(Self::StringConstant(f.fn_name().into(), pos)).collect(),
+ capture_parent_scope: false,
+ op_token: None,
+ }
+ .into(),
+ pos,
+ ),
+
+ _ => Self::DynamicConstant(value.into(), pos),
+ }
+ }
+ /// Is the expression a simple variable access?
+ ///
+ /// `non_qualified` is ignored under `no_module`.
+ #[cfg(not(target_vendor = "teaclave"))]
+ #[inline]
+ #[must_use]
+ pub(crate) fn is_variable_access(&self, _non_qualified: bool) -> bool {
+ match self {
+ #[cfg(not(feature = "no_module"))]
+ Self::Variable(x, ..) if _non_qualified && !x.1.is_empty() => false,
+ Self::Variable(..) => true,
+ _ => false,
+ }
+ }
+ /// Return the variable name if the expression a simple variable access.
+ ///
+ /// `non_qualified` is ignored under `no_module`.
+ #[inline]
+ #[must_use]
+ pub(crate) fn get_variable_name(&self, _non_qualified: bool) -> Option<&str> {
+ match self {
+ #[cfg(not(feature = "no_module"))]
+ Self::Variable(x, ..) if _non_qualified && !x.1.is_empty() => None,
+ Self::Variable(x, ..) => Some(x.3.as_str()),
+ _ => None,
+ }
+ }
+ /// Get the [options][ASTFlags] of the expression.
+ #[inline]
+ #[must_use]
+ pub const fn options(&self) -> ASTFlags {
+ match self {
+ Self::Index(_, options, _) | Self::Dot(_, options, _) => *options,
+
+ #[cfg(not(feature = "no_float"))]
+ Self::FloatConstant(..) => ASTFlags::empty(),
+
+ Self::DynamicConstant(..)
+ | Self::BoolConstant(..)
+ | Self::IntegerConstant(..)
+ | Self::CharConstant(..)
+ | Self::Unit(..)
+ | Self::StringConstant(..)
+ | Self::Array(..)
+ | Self::Map(..)
+ | Self::Variable(..)
+ | Self::ThisPtr(..)
+ | Self::And(..)
+ | Self::Or(..)
+ | Self::Coalesce(..)
+ | Self::FnCall(..)
+ | Self::MethodCall(..)
+ | Self::InterpolatedString(..)
+ | Self::Property(..)
+ | Self::Stmt(..) => ASTFlags::empty(),
+
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Self::Custom(..) => ASTFlags::empty(),
+ }
+ }
+ /// Get the [position][Position] of the expression.
+ #[inline]
+ #[must_use]
+ pub const fn position(&self) -> Position {
+ match self {
+ #[cfg(not(feature = "no_float"))]
+ Self::FloatConstant(.., pos) => *pos,
+
+ Self::DynamicConstant(.., pos)
+ | Self::BoolConstant(.., pos)
+ | Self::IntegerConstant(.., pos)
+ | Self::CharConstant(.., pos)
+ | Self::Unit(pos)
+ | Self::StringConstant(.., pos)
+ | Self::Array(.., pos)
+ | Self::Map(.., pos)
+ | Self::Variable(.., pos)
+ | Self::ThisPtr(pos)
+ | Self::And(.., pos)
+ | Self::Or(.., pos)
+ | Self::Coalesce(.., pos)
+ | Self::FnCall(.., pos)
+ | Self::MethodCall(.., pos)
+ | Self::Index(.., pos)
+ | Self::Dot(.., pos)
+ | Self::InterpolatedString(.., pos)
+ | Self::Property(.., pos) => *pos,
+
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Self::Custom(.., pos) => *pos,
+
+ Self::Stmt(x) => x.position(),
+ }
+ }
+ /// Get the starting [position][Position] of the expression.
+ /// For a binary expression, this will be the left-most LHS instead of the operator.
+ #[inline]
+ #[must_use]
+ pub fn start_position(&self) -> Position {
+ match self {
+ #[cfg(not(feature = "no_module"))]
+ Self::Variable(x, ..) => {
+ if x.1.is_empty() {
+ self.position()
+ } else {
+ x.1.position()
+ }
+ }
+
+ Self::And(x, ..)
+ | Self::Or(x, ..)
+ | Self::Coalesce(x, ..)
+ | Self::Index(x, ..)
+ | Self::Dot(x, ..) => x.lhs.start_position(),
+
+ Self::FnCall(.., pos) => *pos,
+
+ _ => self.position(),
+ }
+ }
+ /// Override the [position][Position] of the expression.
+ #[inline]
+ pub fn set_position(&mut self, new_pos: Position) -> &mut Self {
+ match self {
+ #[cfg(not(feature = "no_float"))]
+ Self::FloatConstant(.., pos) => *pos = new_pos,
+
+ Self::DynamicConstant(.., pos)
+ | Self::BoolConstant(.., pos)
+ | Self::IntegerConstant(.., pos)
+ | Self::CharConstant(.., pos)
+ | Self::Unit(pos)
+ | Self::StringConstant(.., pos)
+ | Self::Array(.., pos)
+ | Self::Map(.., pos)
+ | Self::And(.., pos)
+ | Self::Or(.., pos)
+ | Self::Coalesce(.., pos)
+ | Self::Dot(.., pos)
+ | Self::Index(.., pos)
+ | Self::Variable(.., pos)
+ | Self::ThisPtr(pos)
+ | Self::FnCall(.., pos)
+ | Self::MethodCall(.., pos)
+ | Self::InterpolatedString(.., pos)
+ | Self::Property(.., pos) => *pos = new_pos,
+
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Self::Custom(.., pos) => *pos = new_pos,
+
+ Self::Stmt(x) => x.set_position(new_pos, Position::NONE),
+ }
+
+ self
+ }
+ /// Is the expression pure?
+ ///
+ /// A pure expression has no side effects.
+ #[inline]
+ #[must_use]
+ pub fn is_pure(&self) -> bool {
+ match self {
+ Self::InterpolatedString(x, ..) | Self::Array(x, ..) => x.iter().all(Self::is_pure),
+
+ Self::Map(x, ..) => x.0.iter().map(|(.., v)| v).all(Self::is_pure),
+
+ Self::And(x, ..) | Self::Or(x, ..) | Self::Coalesce(x, ..) => {
+ x.lhs.is_pure() && x.rhs.is_pure()
+ }
+
+ Self::Stmt(x) => x.iter().all(Stmt::is_pure),
+
+ Self::Variable(..) => true,
+
+ _ => self.is_constant(),
+ }
+ }
+ /// Is the expression the unit `()` literal?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_unit(&self) -> bool {
+ matches!(self, Self::Unit(..))
+ }
+ /// Is the expression a constant?
+ #[inline]
+ #[must_use]
+ pub fn is_constant(&self) -> bool {
+ match self {
+ #[cfg(not(feature = "no_float"))]
+ Self::FloatConstant(..) => true,
+
+ Self::DynamicConstant(..)
+ | Self::BoolConstant(..)
+ | Self::IntegerConstant(..)
+ | Self::CharConstant(..)
+ | Self::StringConstant(..)
+ | Self::Unit(..) => true,
+
+ Self::InterpolatedString(x, ..) | Self::Array(x, ..) => x.iter().all(Self::is_constant),
+
+ Self::Map(x, ..) => x.0.iter().map(|(.., expr)| expr).all(Self::is_constant),
+
+ _ => false,
+ }
+ }
+ /// Is a particular [token][Token] allowed as a postfix operator to this expression?
+ #[inline]
+ #[must_use]
+ pub const fn is_valid_postfix(&self, token: &Token) -> bool {
+ match token {
+ #[cfg(not(feature = "no_object"))]
+ Token::Period | Token::Elvis => return true,
+ #[cfg(not(feature = "no_index"))]
+ Token::LeftBracket | Token::QuestionBracket => return true,
+ _ => (),
+ }
+
+ match self {
+ #[cfg(not(feature = "no_float"))]
+ Self::FloatConstant(..) => false,
+
+ Self::DynamicConstant(..)
+ | Self::BoolConstant(..)
+ | Self::CharConstant(..)
+ | Self::And(..)
+ | Self::Or(..)
+ | Self::Coalesce(..)
+ | Self::Unit(..) => false,
+
+ Self::IntegerConstant(..)
+ | Self::StringConstant(..)
+ | Self::InterpolatedString(..)
+ | Self::FnCall(..)
+ | Self::ThisPtr(..)
+ | Self::MethodCall(..)
+ | Self::Stmt(..)
+ | Self::Dot(..)
+ | Self::Index(..)
+ | Self::Array(..)
+ | Self::Map(..) => false,
+
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Self::Custom(..) => false,
+
+ Self::Variable(..) => matches!(
+ token,
+ Token::LeftParen | Token::Unit | Token::Bang | Token::DoubleColon
+ ),
+
+ Self::Property(..) => matches!(token, Token::LeftParen),
+ }
+ }
+ /// Return this [`Expr`], replacing it with [`Expr::Unit`].
+ #[inline(always)]
+ pub fn take(&mut self) -> Self {
+ mem::take(self)
+ }
+ /// Recursively walk this expression.
+ /// Return `false` from the callback to terminate the walk.
+ pub fn walk<'a>(
+ &'a self,
+ path: &mut Vec<ASTNode<'a>>,
+ on_node: &mut impl FnMut(&[ASTNode]) -> bool,
+ ) -> bool {
+ // Push the current node onto the path
+ path.push(self.into());
+
+ if !on_node(path) {
+ return false;
+ }
+
+ match self {
+ Self::Stmt(x) => {
+ for s in &**x {
+ if !s.walk(path, on_node) {
+ return false;
+ }
+ }
+ }
+ Self::InterpolatedString(x, ..) | Self::Array(x, ..) => {
+ for e in &**x {
+ if !e.walk(path, on_node) {
+ return false;
+ }
+ }
+ }
+ Self::Map(x, ..) => {
+ for (.., e) in &x.0 {
+ if !e.walk(path, on_node) {
+ return false;
+ }
+ }
+ }
+ Self::Index(x, ..)
+ | Self::Dot(x, ..)
+ | Self::And(x, ..)
+ | Self::Or(x, ..)
+ | Self::Coalesce(x, ..) => {
+ if !x.lhs.walk(path, on_node) {
+ return false;
+ }
+ if !x.rhs.walk(path, on_node) {
+ return false;
+ }
+ }
+ Self::FnCall(x, ..) => {
+ for e in &x.args {
+ if !e.walk(path, on_node) {
+ return false;
+ }
+ }
+ }
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Self::Custom(x, ..) => {
+ for e in &x.inputs {
+ if !e.walk(path, on_node) {
+ return false;
+ }
+ }
+ }
+ _ => (),
+ }
+
+ path.pop().unwrap();
+
+ true
+ }
+}
diff --git a/rhai/src/ast/flags.rs b/rhai/src/ast/flags.rs
new file mode 100644
index 0000000..f58eae6
--- /dev/null
+++ b/rhai/src/ast/flags.rs
@@ -0,0 +1,53 @@
+//! Module defining script options.
+
+use bitflags::bitflags;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// A type representing the access mode of a function.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
+#[cfg_attr(feature = "metadata", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "metadata", serde(rename_all = "camelCase"))]
+#[non_exhaustive]
+pub enum FnAccess {
+ /// Private function.
+ Private,
+ /// Public function.
+ Public,
+}
+
+impl FnAccess {
+ /// Is this function private?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_private(self) -> bool {
+ match self {
+ Self::Private => true,
+ Self::Public => false,
+ }
+ }
+ /// Is this function public?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_public(self) -> bool {
+ match self {
+ Self::Private => false,
+ Self::Public => true,
+ }
+ }
+}
+
+bitflags! {
+ /// _(internals)_ Bit-flags containing [`AST`][crate::AST] node configuration options.
+ /// Exported under the `internals` feature only.
+ pub struct ASTFlags: u8 {
+ /// The [`AST`][crate::AST] node is read-only.
+ const CONSTANT = 0b_0000_0001;
+ /// The [`AST`][crate::AST] node is exposed to the outside (i.e. public).
+ const EXPORTED = 0b_0000_0010;
+ /// The [`AST`][crate::AST] node is negated (i.e. whatever information is the opposite).
+ const NEGATED = 0b_0000_0100;
+ /// The [`AST`][crate::AST] node breaks out of normal control flow.
+ const BREAK = 0b_0000_1000;
+ }
+}
diff --git a/rhai/src/ast/ident.rs b/rhai/src/ast/ident.rs
new file mode 100644
index 0000000..ffc5f96
--- /dev/null
+++ b/rhai/src/ast/ident.rs
@@ -0,0 +1,71 @@
+//! Module defining script identifiers.
+
+use crate::{ImmutableString, Position};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ borrow::Borrow,
+ fmt,
+ hash::Hash,
+ ops::{Deref, DerefMut},
+};
+
+/// _(internals)_ An identifier containing a name and a [position][Position].
+/// Exported under the `internals` feature only.
+#[derive(Clone, Eq, PartialEq, Hash)]
+pub struct Ident {
+ /// Identifier name.
+ pub name: ImmutableString,
+ /// Position.
+ pub pos: Position,
+}
+
+impl fmt::Debug for Ident {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{:?}", self.name)?;
+ self.pos.debug_print(f)
+ }
+}
+
+impl Borrow<str> for Ident {
+ #[inline(always)]
+ #[must_use]
+ fn borrow(&self) -> &str {
+ self.name.as_ref()
+ }
+}
+
+impl AsRef<str> for Ident {
+ #[inline(always)]
+ #[must_use]
+ fn as_ref(&self) -> &str {
+ self.name.as_ref()
+ }
+}
+
+impl Deref for Ident {
+ type Target = ImmutableString;
+
+ #[inline(always)]
+ fn deref(&self) -> &Self::Target {
+ &self.name
+ }
+}
+
+impl DerefMut for Ident {
+ #[inline(always)]
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.name
+ }
+}
+
+impl Ident {
+ /// Get the name of the identifier as a string slice.
+ #[inline(always)]
+ #[must_use]
+ pub fn as_str(&self) -> &str {
+ self.name.as_str()
+ }
+}
diff --git a/rhai/src/ast/mod.rs b/rhai/src/ast/mod.rs
new file mode 100644
index 0000000..cfa3a94
--- /dev/null
+++ b/rhai/src/ast/mod.rs
@@ -0,0 +1,32 @@
+//! Module defining the AST (abstract syntax tree).
+
+pub mod ast;
+pub mod expr;
+pub mod flags;
+pub mod ident;
+pub mod namespace;
+pub mod namespace_none;
+pub mod script_fn;
+pub mod stmt;
+
+pub use ast::{ASTNode, AST};
+#[cfg(not(feature = "no_custom_syntax"))]
+pub use expr::CustomExpr;
+pub use expr::{BinaryExpr, Expr, FnCallExpr, FnCallHashes};
+pub use flags::{ASTFlags, FnAccess};
+pub use ident::Ident;
+#[cfg(not(feature = "no_module"))]
+pub use namespace::Namespace;
+#[cfg(feature = "no_module")]
+pub use namespace_none::Namespace;
+#[cfg(not(feature = "no_function"))]
+pub use script_fn::{ScriptFnDef, ScriptFnMetadata};
+pub use stmt::{
+ CaseBlocksList, ConditionalExpr, FlowControl, OpAssignment, RangeCase, Stmt, StmtBlock,
+ StmtBlockContainer, SwitchCasesCollection,
+};
+
+/// _(internals)_ Placeholder for a script-defined function.
+/// Exported under the `internals` feature only.
+#[cfg(feature = "no_function")]
+pub type ScriptFnDef = ();
diff --git a/rhai/src/ast/namespace.rs b/rhai/src/ast/namespace.rs
new file mode 100644
index 0000000..c6cd91e
--- /dev/null
+++ b/rhai/src/ast/namespace.rs
@@ -0,0 +1,152 @@
+//! Namespace reference type.
+#![cfg(not(feature = "no_module"))]
+
+use crate::ast::Ident;
+use crate::{Position, StaticVec};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ fmt,
+ num::NonZeroUsize,
+ ops::{Deref, DerefMut},
+};
+
+/// _(internals)_ A chain of [module][crate::Module] names to namespace-qualify a variable or function call.
+/// Exported under the `internals` feature only.
+///
+/// Not available under `no_module`.
+///
+/// A [`u64`] offset to the current stack of imported [modules][crate::Module] in the
+/// [global runtime state][crate::GlobalRuntimeState] is cached for quick search purposes.
+///
+/// A [`StaticVec`] is used because the vast majority of namespace-qualified access contains only
+/// one level, and it is wasteful to always allocate a [`Vec`] with one element.
+#[derive(Clone, Eq, PartialEq, Default, Hash)]
+pub struct Namespace {
+ path: StaticVec<Ident>,
+ index: Option<NonZeroUsize>,
+}
+
+impl fmt::Debug for Namespace {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.is_empty() {
+ return f.write_str("NONE");
+ }
+
+ if let Some(index) = self.index {
+ write!(f, "{index} -> ")?;
+ }
+
+ f.write_str(
+ &self
+ .path
+ .iter()
+ .map(Ident::as_str)
+ .collect::<StaticVec<_>>()
+ .join(crate::engine::NAMESPACE_SEPARATOR),
+ )
+ }
+}
+
+impl fmt::Display for Namespace {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.is_empty() {
+ return Ok(());
+ }
+
+ f.write_str(
+ &self
+ .path
+ .iter()
+ .map(Ident::as_str)
+ .collect::<StaticVec<_>>()
+ .join(crate::engine::NAMESPACE_SEPARATOR),
+ )
+ }
+}
+
+impl Deref for Namespace {
+ type Target = StaticVec<Ident>;
+
+ #[inline(always)]
+ fn deref(&self) -> &Self::Target {
+ &self.path
+ }
+}
+
+impl DerefMut for Namespace {
+ #[inline(always)]
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.path
+ }
+}
+
+impl From<Vec<Ident>> for Namespace {
+ #[inline]
+ fn from(mut path: Vec<Ident>) -> Self {
+ path.shrink_to_fit();
+ Self {
+ index: None,
+ path: path.into(),
+ }
+ }
+}
+
+impl From<StaticVec<Ident>> for Namespace {
+ #[inline]
+ fn from(mut path: StaticVec<Ident>) -> Self {
+ path.shrink_to_fit();
+ Self { index: None, path }
+ }
+}
+
+impl Namespace {
+ /// Constant for no namespace.
+ pub const NONE: Self = Self {
+ index: None,
+ path: StaticVec::new_const(),
+ };
+
+ /// Create a new [`Namespace`].
+ #[inline(always)]
+ #[must_use]
+ pub fn new(root: impl Into<Ident>) -> Self {
+ let mut path = StaticVec::new_const();
+ path.push(root.into());
+
+ Self { index: None, path }
+ }
+ /// Get the [`Scope`][crate::Scope] index offset.
+ #[inline(always)]
+ #[must_use]
+ pub(crate) const fn index(&self) -> Option<NonZeroUsize> {
+ self.index
+ }
+ /// Set the [`Scope`][crate::Scope] index offset.
+ #[inline(always)]
+ pub(crate) fn set_index(&mut self, index: Option<NonZeroUsize>) {
+ self.index = index;
+ }
+ /// Get the [position][Position] of this [`Namespace`].
+ ///
+ /// # Panics
+ ///
+ /// Panics if the path is empty.
+ #[inline(always)]
+ #[must_use]
+ pub fn position(&self) -> Position {
+ self.path[0].pos
+ }
+ /// Get the first path segment of this [`Namespace`].
+ ///
+ /// # Panics
+ ///
+ /// Panics if the path is empty.
+ #[inline(always)]
+ #[must_use]
+ pub fn root(&self) -> &str {
+ &self.path[0].name
+ }
+}
diff --git a/rhai/src/ast/namespace_none.rs b/rhai/src/ast/namespace_none.rs
new file mode 100644
index 0000000..eafe1c5
--- /dev/null
+++ b/rhai/src/ast/namespace_none.rs
@@ -0,0 +1,22 @@
+//! Namespace reference type.
+#![cfg(feature = "no_module")]
+
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// _(internals)_ A chain of [module][crate::Module] names to namespace-qualify a variable or function call.
+/// Exported under the `internals` feature only.
+///
+/// Not available under `no_module`.
+#[derive(Debug, Clone, Eq, PartialEq, Default, Hash)]
+pub struct Namespace;
+
+impl Namespace {
+ /// Constant for no namespace.
+ pub const NONE: Self = Self;
+
+ #[inline(always)]
+ pub const fn is_empty(&self) -> bool {
+ true
+ }
+}
diff --git a/rhai/src/ast/script_fn.rs b/rhai/src/ast/script_fn.rs
new file mode 100644
index 0000000..7120dbb
--- /dev/null
+++ b/rhai/src/ast/script_fn.rs
@@ -0,0 +1,151 @@
+//! Module defining script-defined functions.
+#![cfg(not(feature = "no_function"))]
+
+use super::{FnAccess, StmtBlock};
+use crate::{FnArgsVec, ImmutableString};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{fmt, hash::Hash};
+
+/// _(internals)_ A type containing information on a script-defined function.
+/// Exported under the `internals` feature only.
+#[derive(Debug, Clone)]
+pub struct ScriptFnDef {
+ /// Function body.
+ pub body: StmtBlock,
+ /// Function name.
+ pub name: ImmutableString,
+ /// Function access mode.
+ pub access: FnAccess,
+ #[cfg(not(feature = "no_object"))]
+ /// Type of `this` pointer, if any.
+ /// Not available under `no_object`.
+ pub this_type: Option<ImmutableString>,
+ /// Names of function parameters.
+ pub params: FnArgsVec<ImmutableString>,
+ /// _(metadata)_ Function doc-comments (if any).
+ /// Exported under the `metadata` feature only.
+ ///
+ /// Doc-comments are comment lines beginning with `///` or comment blocks beginning with `/**`,
+ /// placed immediately before a function definition.
+ ///
+ /// Block doc-comments are kept in a single string slice with line-breaks within.
+ ///
+ /// Line doc-comments are merged, with line-breaks, into a single string slice without a termination line-break.
+ ///
+ /// Leading white-spaces are stripped, and each string slice always starts with the
+ /// corresponding doc-comment leader: `///` or `/**`.
+ ///
+ /// Each line in non-block doc-comments starts with `///`.
+ #[cfg(feature = "metadata")]
+ pub comments: Box<[crate::SmartString]>,
+}
+
+impl fmt::Display for ScriptFnDef {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ #[cfg(not(feature = "no_object"))]
+ let this_type = self
+ .this_type
+ .as_ref()
+ .map_or(String::new(), |s| format!("{:?}.", s));
+
+ #[cfg(feature = "no_object")]
+ let this_type = "";
+
+ write!(
+ f,
+ "{}{}{}({})",
+ match self.access {
+ FnAccess::Public => "",
+ FnAccess::Private => "private ",
+ },
+ this_type,
+ self.name,
+ self.params
+ .iter()
+ .map(|s| s.as_str())
+ .collect::<FnArgsVec<_>>()
+ .join(", ")
+ )
+ }
+}
+
+/// A type containing the metadata of a script-defined function.
+///
+/// Not available under `no_function`.
+///
+/// Created by [`AST::iter_functions`][super::AST::iter_functions].
+#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Hash)]
+#[non_exhaustive]
+pub struct ScriptFnMetadata<'a> {
+ /// Function name.
+ pub name: &'a str,
+ /// Function parameters (if any).
+ pub params: Vec<&'a str>,
+ /// Function access mode.
+ pub access: FnAccess,
+ #[cfg(not(feature = "no_object"))]
+ /// Type of `this` pointer, if any.
+ /// Not available under `no_object`.
+ pub this_type: Option<&'a str>,
+ /// _(metadata)_ Function doc-comments (if any).
+ /// Exported under the `metadata` feature only.
+ ///
+ /// Doc-comments are comment lines beginning with `///` or comment blocks beginning with `/**`,
+ /// placed immediately before a function definition.
+ ///
+ /// Block doc-comments are kept in a single string slice with line-breaks within.
+ ///
+ /// Line doc-comments are merged, with line-breaks, into a single string slice without a termination line-break.
+ ///
+ /// Leading white-spaces are stripped, and each string slice always starts with the
+ /// corresponding doc-comment leader: `///` or `/**`.
+ ///
+ /// Each line in non-block doc-comments starts with `///`.
+ #[cfg(feature = "metadata")]
+ pub comments: Vec<&'a str>,
+}
+
+impl fmt::Display for ScriptFnMetadata<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ #[cfg(not(feature = "no_object"))]
+ let this_type = self
+ .this_type
+ .as_ref()
+ .map_or(String::new(), |s| format!("{:?}.", s));
+
+ #[cfg(feature = "no_object")]
+ let this_type = "";
+
+ write!(
+ f,
+ "{}{}{}({})",
+ match self.access {
+ FnAccess::Public => "",
+ FnAccess::Private => "private ",
+ },
+ this_type,
+ self.name,
+ self.params
+ .iter()
+ .copied()
+ .collect::<FnArgsVec<_>>()
+ .join(", ")
+ )
+ }
+}
+
+impl<'a> From<&'a ScriptFnDef> for ScriptFnMetadata<'a> {
+ #[inline]
+ fn from(value: &'a ScriptFnDef) -> Self {
+ Self {
+ name: &value.name,
+ params: value.params.iter().map(|s| s.as_str()).collect(),
+ access: value.access,
+ #[cfg(not(feature = "no_object"))]
+ this_type: value.this_type.as_ref().map(|s| s.as_str()),
+ #[cfg(feature = "metadata")]
+ comments: value.comments.iter().map(<_>::as_ref).collect(),
+ }
+ }
+}
diff --git a/rhai/src/ast/stmt.rs b/rhai/src/ast/stmt.rs
new file mode 100644
index 0000000..75cdebf
--- /dev/null
+++ b/rhai/src/ast/stmt.rs
@@ -0,0 +1,1190 @@
+//! Module defining script statements.
+
+use super::{ASTFlags, ASTNode, BinaryExpr, Expr, FnCallExpr, Ident};
+use crate::engine::{KEYWORD_EVAL, OP_EQUALS};
+use crate::func::StraightHashMap;
+use crate::tokenizer::Token;
+use crate::types::dynamic::Union;
+use crate::types::Span;
+use crate::{calc_fn_hash, Dynamic, Position, StaticVec, INT};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ borrow::Borrow,
+ fmt,
+ hash::{Hash, Hasher},
+ mem,
+ num::NonZeroUsize,
+ ops::{Deref, DerefMut, Range, RangeInclusive},
+};
+
+/// _(internals)_ An op-assignment operator.
+/// Exported under the `internals` feature only.
+///
+/// This type may hold a straight assignment (i.e. not an op-assignment).
+#[derive(Clone, PartialEq, Hash)]
+pub struct OpAssignment {
+ /// Hash of the op-assignment call.
+ hash_op_assign: u64,
+ /// Hash of the underlying operator call (for fallback).
+ hash_op: u64,
+ /// Op-assignment operator.
+ op_assign: Token,
+ /// Syntax of op-assignment operator.
+ op_assign_syntax: &'static str,
+ /// Underlying operator.
+ op: Token,
+ /// Syntax of underlying operator.
+ op_syntax: &'static str,
+ /// [Position] of the op-assignment operator.
+ pos: Position,
+}
+
+impl OpAssignment {
+ /// Create a new [`OpAssignment`] that is only a straight assignment.
+ #[must_use]
+ #[inline(always)]
+ pub const fn new_assignment(pos: Position) -> Self {
+ Self {
+ hash_op_assign: 0,
+ hash_op: 0,
+ op_assign: Token::Equals,
+ op_assign_syntax: OP_EQUALS,
+ op: Token::Equals,
+ op_syntax: OP_EQUALS,
+ pos,
+ }
+ }
+ /// Is this an op-assignment?
+ #[must_use]
+ #[inline(always)]
+ pub fn is_op_assignment(&self) -> bool {
+ !matches!(self.op, Token::Equals)
+ }
+ /// Get information if this [`OpAssignment`] is an op-assignment.
+ ///
+ /// Returns `( hash_op_assign, hash_op, op_assign, op_assign_syntax, op, op_syntax )`:
+ ///
+ /// * `hash_op_assign`: Hash of the op-assignment call.
+ /// * `hash_op`: Hash of the underlying operator call (for fallback).
+ /// * `op_assign`: Op-assignment operator.
+ /// * `op_assign_syntax`: Syntax of op-assignment operator.
+ /// * `op`: Underlying operator.
+ /// * `op_syntax`: Syntax of underlying operator.
+ #[must_use]
+ #[inline]
+ pub fn get_op_assignment_info(
+ &self,
+ ) -> Option<(u64, u64, &Token, &'static str, &Token, &'static str)> {
+ if self.is_op_assignment() {
+ Some((
+ self.hash_op_assign,
+ self.hash_op,
+ &self.op_assign,
+ self.op_assign_syntax,
+ &self.op,
+ self.op_syntax,
+ ))
+ } else {
+ None
+ }
+ }
+ /// Get the [position][Position] of this [`OpAssignment`].
+ #[must_use]
+ #[inline(always)]
+ pub const fn position(&self) -> Position {
+ self.pos
+ }
+ /// Create a new [`OpAssignment`].
+ ///
+ /// # Panics
+ ///
+ /// Panics if the name is not an op-assignment operator.
+ #[must_use]
+ #[inline(always)]
+ pub fn new_op_assignment(name: &str, pos: Position) -> Self {
+ let op = Token::lookup_symbol_from_syntax(name).expect("operator");
+ Self::new_op_assignment_from_token(op, pos)
+ }
+ /// Create a new [`OpAssignment`] from a [`Token`].
+ ///
+ /// # Panics
+ ///
+ /// Panics if the token is not an op-assignment operator.
+ #[must_use]
+ pub fn new_op_assignment_from_token(op_assign: Token, pos: Position) -> Self {
+ let op = op_assign
+ .get_base_op_from_assignment()
+ .expect("op-assignment operator");
+
+ let op_assign_syntax = op_assign.literal_syntax();
+ let op_syntax = op.literal_syntax();
+
+ Self {
+ hash_op_assign: calc_fn_hash(None, op_assign_syntax, 2),
+ hash_op: calc_fn_hash(None, op_syntax, 2),
+ op_assign,
+ op_assign_syntax,
+ op,
+ op_syntax,
+ pos,
+ }
+ }
+ /// Create a new [`OpAssignment`] from a base operator.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the name is not an operator that can be converted into an op-operator.
+ #[must_use]
+ #[inline(always)]
+ pub fn new_op_assignment_from_base(name: &str, pos: Position) -> Self {
+ let op = Token::lookup_symbol_from_syntax(name).expect("operator");
+ Self::new_op_assignment_from_base_token(&op, pos)
+ }
+ /// Convert a [`Token`] into a new [`OpAssignment`].
+ ///
+ /// # Panics
+ ///
+ /// Panics if the token is cannot be converted into an op-assignment operator.
+ #[inline(always)]
+ #[must_use]
+ pub fn new_op_assignment_from_base_token(op: &Token, pos: Position) -> Self {
+ Self::new_op_assignment_from_token(op.convert_to_op_assignment().expect("operator"), pos)
+ }
+}
+
+impl fmt::Debug for OpAssignment {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.is_op_assignment() {
+ f.debug_struct("OpAssignment")
+ .field("hash_op_assign", &self.hash_op_assign)
+ .field("hash_op", &self.hash_op)
+ .field("op_assign", &self.op_assign)
+ .field("op_assign_syntax", &self.op_assign_syntax)
+ .field("op", &self.op)
+ .field("op_syntax", &self.op_syntax)
+ .field("pos", &self.pos)
+ .finish()
+ } else {
+ fmt::Debug::fmt(&self.pos, f)
+ }
+ }
+}
+
+/// An expression with a condition.
+///
+/// The condition may simply be [`Expr::BoolConstant`] with `true` if there is actually no condition.
+#[derive(Debug, Clone, Default, Hash)]
+pub struct ConditionalExpr {
+ /// Condition.
+ pub condition: Expr,
+ /// Expression.
+ pub expr: Expr,
+}
+
+impl<E: Into<Expr>> From<E> for ConditionalExpr {
+ #[inline(always)]
+ fn from(value: E) -> Self {
+ Self {
+ condition: Expr::BoolConstant(true, Position::NONE),
+ expr: value.into(),
+ }
+ }
+}
+
+impl<E: Into<Expr>> From<(Expr, E)> for ConditionalExpr {
+ #[inline(always)]
+ fn from(value: (Expr, E)) -> Self {
+ Self {
+ condition: value.0,
+ expr: value.1.into(),
+ }
+ }
+}
+
+impl ConditionalExpr {
+ /// Is the condition always `true`?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_always_true(&self) -> bool {
+ matches!(self.condition, Expr::BoolConstant(true, ..))
+ }
+ /// Is the condition always `false`?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_always_false(&self) -> bool {
+ matches!(self.condition, Expr::BoolConstant(false, ..))
+ }
+}
+
+/// _(internals)_ A type containing a range case for a `switch` statement.
+/// Exported under the `internals` feature only.
+#[derive(Clone, Hash)]
+pub enum RangeCase {
+ /// Exclusive range.
+ ExclusiveInt(Range<INT>, usize),
+ /// Inclusive range.
+ InclusiveInt(RangeInclusive<INT>, usize),
+}
+
+impl fmt::Debug for RangeCase {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::ExclusiveInt(r, n) => write!(f, "{}..{} => {n}", r.start, r.end),
+ Self::InclusiveInt(r, n) => write!(f, "{}..={} => {n}", *r.start(), *r.end()),
+ }
+ }
+}
+
+impl From<Range<INT>> for RangeCase {
+ #[inline(always)]
+ fn from(value: Range<INT>) -> Self {
+ Self::ExclusiveInt(value, usize::MAX)
+ }
+}
+
+impl From<RangeInclusive<INT>> for RangeCase {
+ #[inline(always)]
+ fn from(value: RangeInclusive<INT>) -> Self {
+ Self::InclusiveInt(value, usize::MAX)
+ }
+}
+
+impl IntoIterator for RangeCase {
+ type Item = INT;
+ type IntoIter = Box<dyn Iterator<Item = Self::Item>>;
+
+ #[inline]
+ #[must_use]
+ fn into_iter(self) -> Self::IntoIter {
+ match self {
+ Self::ExclusiveInt(r, ..) => Box::new(r),
+ Self::InclusiveInt(r, ..) => Box::new(r),
+ }
+ }
+}
+
+impl RangeCase {
+ /// Returns `true` if the range contains no items.
+ #[inline]
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ match self {
+ Self::ExclusiveInt(r, ..) => r.is_empty(),
+ Self::InclusiveInt(r, ..) => r.is_empty(),
+ }
+ }
+ /// Size of the range.
+ #[inline]
+ #[must_use]
+ pub fn len(&self) -> INT {
+ match self {
+ Self::ExclusiveInt(r, ..) if r.is_empty() => 0,
+ Self::ExclusiveInt(r, ..) => r.end - r.start,
+ Self::InclusiveInt(r, ..) if r.is_empty() => 0,
+ Self::InclusiveInt(r, ..) => *r.end() - *r.start() + 1,
+ }
+ }
+ /// Is the specified value within this range?
+ #[inline]
+ #[must_use]
+ pub fn contains(&self, value: &Dynamic) -> bool {
+ match value {
+ Dynamic(Union::Int(v, ..)) => self.contains_int(*v),
+ #[cfg(not(feature = "no_float"))]
+ Dynamic(Union::Float(v, ..)) => self.contains_float(**v),
+ #[cfg(feature = "decimal")]
+ Dynamic(Union::Decimal(v, ..)) => self.contains_decimal(**v),
+ _ => false,
+ }
+ }
+ /// Is the specified number within this range?
+ #[inline]
+ #[must_use]
+ pub fn contains_int(&self, n: INT) -> bool {
+ match self {
+ Self::ExclusiveInt(r, ..) => r.contains(&n),
+ Self::InclusiveInt(r, ..) => r.contains(&n),
+ }
+ }
+ /// Is the specified floating-point number within this range?
+ #[cfg(not(feature = "no_float"))]
+ #[inline]
+ #[must_use]
+ pub fn contains_float(&self, n: crate::FLOAT) -> bool {
+ use crate::FLOAT;
+
+ match self {
+ Self::ExclusiveInt(r, ..) => ((r.start as FLOAT)..(r.end as FLOAT)).contains(&n),
+ Self::InclusiveInt(r, ..) => ((*r.start() as FLOAT)..=(*r.end() as FLOAT)).contains(&n),
+ }
+ }
+ /// Is the specified decimal number within this range?
+ #[cfg(feature = "decimal")]
+ #[inline]
+ #[must_use]
+ pub fn contains_decimal(&self, n: rust_decimal::Decimal) -> bool {
+ use rust_decimal::Decimal;
+
+ match self {
+ Self::ExclusiveInt(r, ..) => {
+ (Into::<Decimal>::into(r.start)..Into::<Decimal>::into(r.end)).contains(&n)
+ }
+ Self::InclusiveInt(r, ..) => {
+ (Into::<Decimal>::into(*r.start())..=Into::<Decimal>::into(*r.end())).contains(&n)
+ }
+ }
+ }
+ /// Is the specified range inclusive?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_inclusive(&self) -> bool {
+ match self {
+ Self::ExclusiveInt(..) => false,
+ Self::InclusiveInt(..) => true,
+ }
+ }
+ /// Get the index to the [`ConditionalExpr`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn index(&self) -> usize {
+ match self {
+ Self::ExclusiveInt(.., n) | Self::InclusiveInt(.., n) => *n,
+ }
+ }
+ /// Set the index to the [`ConditionalExpr`].
+ #[inline(always)]
+ pub fn set_index(&mut self, index: usize) {
+ match self {
+ Self::ExclusiveInt(.., n) | Self::InclusiveInt(.., n) => *n = index,
+ }
+ }
+}
+
+pub type CaseBlocksList = smallvec::SmallVec<[usize; 1]>;
+
+/// _(internals)_ A type containing all cases for a `switch` statement.
+/// Exported under the `internals` feature only.
+#[derive(Debug, Clone)]
+pub struct SwitchCasesCollection {
+ /// List of [`ConditionalExpr`]'s.
+ pub expressions: StaticVec<ConditionalExpr>,
+ /// Dictionary mapping value hashes to [`ConditionalExpr`]'s.
+ pub cases: StraightHashMap<CaseBlocksList>,
+ /// List of range cases.
+ pub ranges: StaticVec<RangeCase>,
+ /// Statements block for the default case (there can be no condition for the default case).
+ pub def_case: Option<usize>,
+}
+
+impl Hash for SwitchCasesCollection {
+ #[inline(always)]
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.expressions.hash(state);
+
+ self.cases.len().hash(state);
+ self.cases.iter().for_each(|kv| kv.hash(state));
+
+ self.ranges.hash(state);
+ self.def_case.hash(state);
+ }
+}
+
+/// Number of items to keep inline for [`StmtBlockContainer`].
+#[cfg(not(feature = "no_std"))]
+const STMT_BLOCK_INLINE_SIZE: usize = 8;
+
+/// _(internals)_ The underlying container type for [`StmtBlock`].
+/// Exported under the `internals` feature only.
+///
+/// A [`SmallVec`](https://crates.io/crates/smallvec) containing up to 8 items inline is used to
+/// hold a statements block, with the assumption that most program blocks would container fewer than
+/// 8 statements, and those that do have a lot more statements.
+#[cfg(not(feature = "no_std"))]
+pub type StmtBlockContainer = smallvec::SmallVec<[Stmt; STMT_BLOCK_INLINE_SIZE]>;
+
+/// _(internals)_ The underlying container type for [`StmtBlock`].
+/// Exported under the `internals` feature only.
+#[cfg(feature = "no_std")]
+pub type StmtBlockContainer = StaticVec<Stmt>;
+
+/// _(internals)_ A scoped block of statements.
+/// Exported under the `internals` feature only.
+#[derive(Clone, Hash, Default)]
+pub struct StmtBlock {
+ /// List of [statements][Stmt].
+ block: StmtBlockContainer,
+ /// [Position] of the statements block.
+ span: Span,
+}
+
+impl StmtBlock {
+ /// A [`StmtBlock`] that does not exist.
+ pub const NONE: Self = Self::empty(Position::NONE);
+
+ /// Create a new [`StmtBlock`].
+ #[inline(always)]
+ #[must_use]
+ pub fn new(
+ statements: impl IntoIterator<Item = Stmt>,
+ start_pos: Position,
+ end_pos: Position,
+ ) -> Self {
+ Self::new_with_span(statements, Span::new(start_pos, end_pos))
+ }
+ /// Create a new [`StmtBlock`].
+ #[must_use]
+ pub fn new_with_span(statements: impl IntoIterator<Item = Stmt>, span: Span) -> Self {
+ let mut statements: smallvec::SmallVec<_> = statements.into_iter().collect();
+ statements.shrink_to_fit();
+ Self {
+ block: statements,
+ span,
+ }
+ }
+ /// Create an empty [`StmtBlock`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn empty(pos: Position) -> Self {
+ Self {
+ block: StmtBlockContainer::new_const(),
+ span: Span::new(pos, pos),
+ }
+ }
+ /// Returns `true` if this statements block contains no statements.
+ #[inline(always)]
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ self.block.is_empty()
+ }
+ /// Number of statements in this statements block.
+ #[inline(always)]
+ #[must_use]
+ pub fn len(&self) -> usize {
+ self.block.len()
+ }
+ /// Get the statements of this statements block.
+ #[inline(always)]
+ #[must_use]
+ pub fn statements(&self) -> &[Stmt] {
+ &self.block
+ }
+ /// Extract the statements.
+ #[inline(always)]
+ #[must_use]
+ pub(crate) fn take_statements(&mut self) -> StmtBlockContainer {
+ mem::take(&mut self.block)
+ }
+ /// Get an iterator over the statements of this statements block.
+ #[inline(always)]
+ pub fn iter(&self) -> impl Iterator<Item = &Stmt> {
+ self.block.iter()
+ }
+ /// Get the start position (location of the beginning `{`) of this statements block.
+ #[inline(always)]
+ #[must_use]
+ pub const fn position(&self) -> Position {
+ (self.span).start()
+ }
+ /// Get the end position (location of the ending `}`) of this statements block.
+ #[inline(always)]
+ #[must_use]
+ pub const fn end_position(&self) -> Position {
+ (self.span).end()
+ }
+ /// Get the positions (locations of the beginning `{` and ending `}`) of this statements block.
+ #[inline(always)]
+ #[must_use]
+ pub const fn span(&self) -> Span {
+ self.span
+ }
+ /// Get the positions (locations of the beginning `{` and ending `}`) of this statements block
+ /// or a default.
+ #[inline(always)]
+ #[must_use]
+ pub const fn span_or_else(&self, def_start_pos: Position, def_end_pos: Position) -> Span {
+ Span::new(
+ (self.span).start().or_else(def_start_pos),
+ (self.span).end().or_else(def_end_pos),
+ )
+ }
+ /// Set the positions of this statements block.
+ #[inline(always)]
+ pub fn set_position(&mut self, start_pos: Position, end_pos: Position) {
+ self.span = Span::new(start_pos, end_pos);
+ }
+}
+
+impl Deref for StmtBlock {
+ type Target = StmtBlockContainer;
+
+ #[inline(always)]
+ fn deref(&self) -> &Self::Target {
+ &self.block
+ }
+}
+
+impl DerefMut for StmtBlock {
+ #[inline(always)]
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.block
+ }
+}
+
+impl Borrow<[Stmt]> for StmtBlock {
+ #[inline(always)]
+ #[must_use]
+ fn borrow(&self) -> &[Stmt] {
+ &self.block
+ }
+}
+
+impl AsRef<[Stmt]> for StmtBlock {
+ #[inline(always)]
+ #[must_use]
+ fn as_ref(&self) -> &[Stmt] {
+ &self.block
+ }
+}
+
+impl AsMut<[Stmt]> for StmtBlock {
+ #[inline(always)]
+ #[must_use]
+ fn as_mut(&mut self) -> &mut [Stmt] {
+ &mut self.block
+ }
+}
+
+impl fmt::Debug for StmtBlock {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str("Block")?;
+ fmt::Debug::fmt(&self.block, f)?;
+ if !self.span.is_none() {
+ write!(f, " @ {:?}", self.span())?;
+ }
+ Ok(())
+ }
+}
+
+impl From<Stmt> for StmtBlock {
+ #[inline]
+ fn from(stmt: Stmt) -> Self {
+ match stmt {
+ Stmt::Block(block) => *block,
+ Stmt::Noop(pos) => Self {
+ block: StmtBlockContainer::new_const(),
+ span: Span::new(pos, pos),
+ },
+ _ => {
+ let pos = stmt.position();
+ Self {
+ block: vec![stmt].into(),
+ span: Span::new(pos, Position::NONE),
+ }
+ }
+ }
+ }
+}
+
+impl IntoIterator for StmtBlock {
+ type Item = Stmt;
+ #[cfg(not(feature = "no_std"))]
+ type IntoIter = smallvec::IntoIter<[Stmt; STMT_BLOCK_INLINE_SIZE]>;
+ #[cfg(feature = "no_std")]
+ type IntoIter = smallvec::IntoIter<[Stmt; crate::STATIC_VEC_INLINE_SIZE]>;
+
+ #[inline(always)]
+ fn into_iter(self) -> Self::IntoIter {
+ self.block.into_iter()
+ }
+}
+
+impl<'a> IntoIterator for &'a StmtBlock {
+ type Item = &'a Stmt;
+ type IntoIter = std::slice::Iter<'a, Stmt>;
+
+ #[inline(always)]
+ fn into_iter(self) -> Self::IntoIter {
+ self.block.iter()
+ }
+}
+
+impl Extend<Stmt> for StmtBlock {
+ #[inline(always)]
+ fn extend<T: IntoIterator<Item = Stmt>>(&mut self, iter: T) {
+ self.block.extend(iter);
+ }
+}
+
+/// _(internals)_ A flow control block containing:
+/// * an expression,
+/// * a statements body
+/// * an alternate statements body
+///
+/// Exported under the `internals` feature only.
+#[derive(Debug, Clone, Hash)]
+pub struct FlowControl {
+ /// Flow control expression.
+ pub expr: Expr,
+ /// Main body.
+ pub body: StmtBlock,
+ /// Branch body.
+ pub branch: StmtBlock,
+}
+
+/// _(internals)_ A statement.
+/// Exported under the `internals` feature only.
+#[derive(Debug, Clone, Hash)]
+#[non_exhaustive]
+pub enum Stmt {
+ /// No-op.
+ Noop(Position),
+ /// `if` expr `{` stmt `}` `else` `{` stmt `}`
+ If(Box<FlowControl>, Position),
+ /// `switch` expr `{` literal or range or _ `if` condition `=>` stmt `,` ... `}`
+ ///
+ /// ### Data Structure
+ ///
+ /// 0) Hash table for (condition, block)
+ /// 1) Default block
+ /// 2) List of ranges: (start, end, inclusive, condition, statement)
+ Switch(Box<(Expr, SwitchCasesCollection)>, Position),
+ /// `while` expr `{` stmt `}` | `loop` `{` stmt `}`
+ ///
+ /// If the guard expression is [`UNIT`][Expr::Unit], then it is a `loop` statement.
+ While(Box<FlowControl>, Position),
+ /// `do` `{` stmt `}` `while`|`until` expr
+ ///
+ /// ### Flags
+ ///
+ /// * [`NONE`][ASTFlags::NONE] = `while`
+ /// * [`NEGATED`][ASTFlags::NEGATED] = `until`
+ Do(Box<FlowControl>, ASTFlags, Position),
+ /// `for` `(` id `,` counter `)` `in` expr `{` stmt `}`
+ For(Box<(Ident, Ident, FlowControl)>, Position),
+ /// \[`export`\] `let`|`const` id `=` expr
+ ///
+ /// ### Flags
+ ///
+ /// * [`EXPORTED`][ASTFlags::EXPORTED] = `export`
+ /// * [`CONSTANT`][ASTFlags::CONSTANT] = `const`
+ Var(Box<(Ident, Expr, Option<NonZeroUsize>)>, ASTFlags, Position),
+ /// expr op`=` expr
+ Assignment(Box<(OpAssignment, BinaryExpr)>),
+ /// func `(` expr `,` ... `)`
+ ///
+ /// This is a duplicate of [`Expr::FnCall`] to cover the very common pattern of a single
+ /// function call forming one statement.
+ FnCall(Box<FnCallExpr>, Position),
+ /// `{` stmt`;` ... `}`
+ Block(Box<StmtBlock>),
+ /// `try` `{` stmt; ... `}` `catch` `(` var `)` `{` stmt; ... `}`
+ TryCatch(Box<FlowControl>, Position),
+ /// [expression][Expr]
+ Expr(Box<Expr>),
+ /// `continue`/`break` expr
+ ///
+ /// ### Flags
+ ///
+ /// * [`NONE`][ASTFlags::NONE] = `continue`
+ /// * [`BREAK`][ASTFlags::BREAK] = `break`
+ BreakLoop(Option<Box<Expr>>, ASTFlags, Position),
+ /// `return`/`throw` expr
+ ///
+ /// ### Flags
+ ///
+ /// * [`NONE`][ASTFlags::NONE] = `return`
+ /// * [`BREAK`][ASTFlags::BREAK] = `throw`
+ Return(Option<Box<Expr>>, ASTFlags, Position),
+ /// `import` expr `as` alias
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ Import(Box<(Expr, Ident)>, Position),
+ /// `export` var `as` alias
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ Export(Box<(Ident, Ident)>, Position),
+ /// Convert a list of variables to shared.
+ ///
+ /// Not available under `no_closure`.
+ ///
+ /// # Notes
+ ///
+ /// This variant does not map to any language structure. It is currently only used only to
+ /// convert normal variables into shared variables when they are _captured_ by a closure.
+ #[cfg(not(feature = "no_closure"))]
+ Share(Box<crate::FnArgsVec<(crate::ast::Ident, Option<NonZeroUsize>)>>),
+}
+
+impl Default for Stmt {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::Noop(Position::NONE)
+ }
+}
+
+impl From<StmtBlock> for Stmt {
+ #[inline(always)]
+ fn from(block: StmtBlock) -> Self {
+ Self::Block(block.into())
+ }
+}
+
+impl<T: IntoIterator<Item = Self>> From<(T, Position, Position)> for Stmt {
+ #[inline(always)]
+ fn from(value: (T, Position, Position)) -> Self {
+ StmtBlock::new(value.0, value.1, value.2).into()
+ }
+}
+
+impl<T: IntoIterator<Item = Self>> From<(T, Span)> for Stmt {
+ #[inline(always)]
+ fn from(value: (T, Span)) -> Self {
+ StmtBlock::new_with_span(value.0, value.1).into()
+ }
+}
+
+impl Stmt {
+ /// Is this statement [`Noop`][Stmt::Noop]?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_noop(&self) -> bool {
+ matches!(self, Self::Noop(..))
+ }
+ /// Get the [options][ASTFlags] of this statement.
+ #[inline]
+ #[must_use]
+ pub const fn options(&self) -> ASTFlags {
+ match self {
+ Self::Do(_, options, _)
+ | Self::Var(_, options, _)
+ | Self::BreakLoop(_, options, _)
+ | Self::Return(_, options, _) => *options,
+
+ Self::Noop(..)
+ | Self::If(..)
+ | Self::Switch(..)
+ | Self::Block(..)
+ | Self::Expr(..)
+ | Self::FnCall(..)
+ | Self::While(..)
+ | Self::For(..)
+ | Self::TryCatch(..)
+ | Self::Assignment(..) => ASTFlags::empty(),
+
+ #[cfg(not(feature = "no_module"))]
+ Self::Import(..) | Self::Export(..) => ASTFlags::empty(),
+
+ #[cfg(not(feature = "no_closure"))]
+ Self::Share(..) => ASTFlags::empty(),
+ }
+ }
+ /// Get the [position][Position] of this statement.
+ #[must_use]
+ pub fn position(&self) -> Position {
+ match self {
+ Self::Noop(pos)
+ | Self::BreakLoop(.., pos)
+ | Self::FnCall(.., pos)
+ | Self::If(.., pos)
+ | Self::Switch(.., pos)
+ | Self::While(.., pos)
+ | Self::Do(.., pos)
+ | Self::For(.., pos)
+ | Self::Return(.., pos)
+ | Self::Var(.., pos)
+ | Self::TryCatch(.., pos) => *pos,
+
+ Self::Assignment(x) => x.0.pos,
+
+ Self::Block(x) => x.position(),
+
+ Self::Expr(x) => x.start_position(),
+
+ #[cfg(not(feature = "no_module"))]
+ Self::Import(.., pos) => *pos,
+ #[cfg(not(feature = "no_module"))]
+ Self::Export(.., pos) => *pos,
+
+ #[cfg(not(feature = "no_closure"))]
+ Self::Share(x) => x[0].0.pos,
+ }
+ }
+ /// Override the [position][Position] of this statement.
+ pub fn set_position(&mut self, new_pos: Position) -> &mut Self {
+ match self {
+ Self::Noop(pos)
+ | Self::BreakLoop(.., pos)
+ | Self::FnCall(.., pos)
+ | Self::If(.., pos)
+ | Self::Switch(.., pos)
+ | Self::While(.., pos)
+ | Self::Do(.., pos)
+ | Self::For(.., pos)
+ | Self::Return(.., pos)
+ | Self::Var(.., pos)
+ | Self::TryCatch(.., pos) => *pos = new_pos,
+
+ Self::Assignment(x) => x.0.pos = new_pos,
+
+ Self::Block(x) => x.set_position(new_pos, x.end_position()),
+
+ Self::Expr(x) => {
+ x.set_position(new_pos);
+ }
+
+ #[cfg(not(feature = "no_module"))]
+ Self::Import(.., pos) => *pos = new_pos,
+ #[cfg(not(feature = "no_module"))]
+ Self::Export(.., pos) => *pos = new_pos,
+
+ #[cfg(not(feature = "no_closure"))]
+ Self::Share(x) => x.iter_mut().for_each(|(x, _)| x.pos = new_pos),
+ }
+
+ self
+ }
+ /// Does this statement return a value?
+ #[must_use]
+ pub const fn returns_value(&self) -> bool {
+ match self {
+ Self::If(..)
+ | Self::Switch(..)
+ | Self::Block(..)
+ | Self::Expr(..)
+ | Self::FnCall(..) => true,
+
+ Self::Noop(..)
+ | Self::While(..)
+ | Self::Do(..)
+ | Self::For(..)
+ | Self::TryCatch(..) => false,
+
+ Self::Var(..) | Self::Assignment(..) | Self::BreakLoop(..) | Self::Return(..) => false,
+
+ #[cfg(not(feature = "no_module"))]
+ Self::Import(..) | Self::Export(..) => false,
+
+ #[cfg(not(feature = "no_closure"))]
+ Self::Share(..) => false,
+ }
+ }
+ /// Is this statement self-terminated (i.e. no need for a semicolon terminator)?
+ #[must_use]
+ pub const fn is_self_terminated(&self) -> bool {
+ match self {
+ Self::If(..)
+ | Self::Switch(..)
+ | Self::While(..)
+ | Self::For(..)
+ | Self::Block(..)
+ | Self::TryCatch(..) => true,
+
+ // A No-op requires a semicolon in order to know it is an empty statement!
+ Self::Noop(..) => false,
+
+ Self::Expr(e) => match &**e {
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Expr::Custom(x, ..) if x.is_self_terminated() => true,
+ _ => false,
+ },
+
+ Self::Var(..)
+ | Self::Assignment(..)
+ | Self::FnCall(..)
+ | Self::Do(..)
+ | Self::BreakLoop(..)
+ | Self::Return(..) => false,
+
+ #[cfg(not(feature = "no_module"))]
+ Self::Import(..) | Self::Export(..) => false,
+
+ #[cfg(not(feature = "no_closure"))]
+ Self::Share(..) => false,
+ }
+ }
+ /// Is this statement _pure_?
+ ///
+ /// A pure statement has no side effects.
+ #[must_use]
+ pub fn is_pure(&self) -> bool {
+ match self {
+ Self::Noop(..) => true,
+ Self::Expr(expr) => expr.is_pure(),
+ Self::If(x, ..) => {
+ x.expr.is_pure()
+ && x.body.iter().all(Self::is_pure)
+ && x.branch.iter().all(Self::is_pure)
+ }
+ Self::Switch(x, ..) => {
+ let (expr, sw) = &**x;
+ expr.is_pure()
+ && sw.cases.values().flat_map(|cases| cases.iter()).all(|&c| {
+ let block = &sw.expressions[c];
+ block.condition.is_pure() && block.expr.is_pure()
+ })
+ && sw.ranges.iter().all(|r| {
+ let block = &sw.expressions[r.index()];
+ block.condition.is_pure() && block.expr.is_pure()
+ })
+ && sw.def_case.is_some()
+ && sw.expressions[sw.def_case.unwrap()].expr.is_pure()
+ }
+
+ // Loops that exit can be pure because it can never be infinite.
+ Self::While(x, ..) if matches!(x.expr, Expr::BoolConstant(false, ..)) => true,
+ Self::Do(x, options, ..) if matches!(x.expr, Expr::BoolConstant(..)) => match x.expr {
+ Expr::BoolConstant(cond, ..) if cond == options.contains(ASTFlags::NEGATED) => {
+ x.body.iter().all(Self::is_pure)
+ }
+ _ => false,
+ },
+
+ // Loops are never pure since they can be infinite - and that's a side effect.
+ Self::While(..) | Self::Do(..) => false,
+
+ // For loops can be pure because if the iterable is pure, it is finite,
+ // so infinite loops can never occur.
+ Self::For(x, ..) => x.2.expr.is_pure() && x.2.body.iter().all(Self::is_pure),
+
+ Self::Var(..) | Self::Assignment(..) | Self::FnCall(..) => false,
+ Self::Block(block, ..) => block.iter().all(Self::is_pure),
+ Self::BreakLoop(..) | Self::Return(..) => false,
+ Self::TryCatch(x, ..) => {
+ x.expr.is_pure()
+ && x.body.iter().all(Self::is_pure)
+ && x.branch.iter().all(Self::is_pure)
+ }
+
+ #[cfg(not(feature = "no_module"))]
+ Self::Import(..) => false,
+ #[cfg(not(feature = "no_module"))]
+ Self::Export(..) => false,
+
+ #[cfg(not(feature = "no_closure"))]
+ Self::Share(..) => false,
+ }
+ }
+ /// Does this statement's behavior depend on its containing block?
+ ///
+ /// A statement that depends on its containing block behaves differently when promoted to an
+ /// upper block.
+ ///
+ /// Currently only variable definitions (i.e. `let` and `const`), `import`/`export` statements,
+ /// and `eval` calls (which may in turn define variables) fall under this category.
+ #[inline]
+ #[must_use]
+ pub fn is_block_dependent(&self) -> bool {
+ match self {
+ Self::Var(..) => true,
+
+ Self::Expr(e) => match &**e {
+ Expr::Stmt(s) => s.iter().all(Self::is_block_dependent),
+ Expr::FnCall(x, ..) => !x.is_qualified() && x.name == KEYWORD_EVAL,
+ _ => false,
+ },
+
+ Self::FnCall(x, ..) => !x.is_qualified() && x.name == KEYWORD_EVAL,
+
+ #[cfg(not(feature = "no_module"))]
+ Self::Import(..) | Self::Export(..) => true,
+
+ _ => false,
+ }
+ }
+ /// Is this statement _pure_ within the containing block?
+ ///
+ /// An internally pure statement only has side effects that disappear outside the block.
+ ///
+ /// Currently only variable definitions (i.e. `let` and `const`) and `import`/`export`
+ /// statements are internally pure, other than pure expressions.
+ #[inline]
+ #[must_use]
+ pub fn is_internally_pure(&self) -> bool {
+ match self {
+ Self::Var(x, ..) => x.1.is_pure(),
+
+ Self::Expr(e) => match &**e {
+ Expr::Stmt(s) => s.iter().all(Self::is_internally_pure),
+ _ => self.is_pure(),
+ },
+
+ #[cfg(not(feature = "no_module"))]
+ Self::Import(x, ..) => x.0.is_pure(),
+ #[cfg(not(feature = "no_module"))]
+ Self::Export(..) => true,
+
+ _ => self.is_pure(),
+ }
+ }
+ /// Does this statement break the current control flow through the containing block?
+ ///
+ /// Currently this is only true for `return`, `throw`, `break` and `continue`.
+ ///
+ /// All statements following this statement will essentially be dead code.
+ #[inline]
+ #[must_use]
+ pub const fn is_control_flow_break(&self) -> bool {
+ matches!(self, Self::Return(..) | Self::BreakLoop(..))
+ }
+ /// Return this [`Stmt`], replacing it with [`Stmt::Noop`].
+ #[inline(always)]
+ pub fn take(&mut self) -> Self {
+ mem::take(self)
+ }
+ /// Recursively walk this statement.
+ /// Return `false` from the callback to terminate the walk.
+ pub fn walk<'a>(
+ &'a self,
+ path: &mut Vec<ASTNode<'a>>,
+ on_node: &mut impl FnMut(&[ASTNode]) -> bool,
+ ) -> bool {
+ // Push the current node onto the path
+ path.push(self.into());
+
+ if !on_node(path) {
+ return false;
+ }
+
+ match self {
+ Self::Var(x, ..) => {
+ if !x.1.walk(path, on_node) {
+ return false;
+ }
+ }
+ Self::If(x, ..) => {
+ if !x.expr.walk(path, on_node) {
+ return false;
+ }
+ for s in &x.body {
+ if !s.walk(path, on_node) {
+ return false;
+ }
+ }
+ for s in &x.branch {
+ if !s.walk(path, on_node) {
+ return false;
+ }
+ }
+ }
+ Self::Switch(x, ..) => {
+ let (expr, sw) = &**x;
+
+ if !expr.walk(path, on_node) {
+ return false;
+ }
+ for (.., blocks) in &sw.cases {
+ for &b in blocks {
+ let block = &sw.expressions[b];
+
+ if !block.condition.walk(path, on_node) {
+ return false;
+ }
+ if !block.expr.walk(path, on_node) {
+ return false;
+ }
+ }
+ }
+ for r in &sw.ranges {
+ let block = &sw.expressions[r.index()];
+
+ if !block.condition.walk(path, on_node) {
+ return false;
+ }
+ if !block.expr.walk(path, on_node) {
+ return false;
+ }
+ }
+ if let Some(index) = sw.def_case {
+ if !sw.expressions[index].expr.walk(path, on_node) {
+ return false;
+ }
+ }
+ }
+ Self::While(x, ..) | Self::Do(x, ..) => {
+ if !x.expr.walk(path, on_node) {
+ return false;
+ }
+ for s in x.body.statements() {
+ if !s.walk(path, on_node) {
+ return false;
+ }
+ }
+ }
+ Self::For(x, ..) => {
+ if !x.2.expr.walk(path, on_node) {
+ return false;
+ }
+ for s in &x.2.body {
+ if !s.walk(path, on_node) {
+ return false;
+ }
+ }
+ }
+ Self::Assignment(x, ..) => {
+ if !x.1.lhs.walk(path, on_node) {
+ return false;
+ }
+ if !x.1.rhs.walk(path, on_node) {
+ return false;
+ }
+ }
+ Self::FnCall(x, ..) => {
+ for s in &x.args {
+ if !s.walk(path, on_node) {
+ return false;
+ }
+ }
+ }
+ Self::Block(x, ..) => {
+ for s in x.statements() {
+ if !s.walk(path, on_node) {
+ return false;
+ }
+ }
+ }
+ Self::TryCatch(x, ..) => {
+ for s in &x.body {
+ if !s.walk(path, on_node) {
+ return false;
+ }
+ }
+ for s in &x.branch {
+ if !s.walk(path, on_node) {
+ return false;
+ }
+ }
+ }
+ Self::Expr(e) => {
+ if !e.walk(path, on_node) {
+ return false;
+ }
+ }
+ Self::Return(Some(e), ..) => {
+ if !e.walk(path, on_node) {
+ return false;
+ }
+ }
+ #[cfg(not(feature = "no_module"))]
+ Self::Import(x, ..) => {
+ if !x.0.walk(path, on_node) {
+ return false;
+ }
+ }
+ _ => (),
+ }
+
+ path.pop().unwrap();
+
+ true
+ }
+}
diff --git a/rhai/src/bin/README.md b/rhai/src/bin/README.md
new file mode 100644
index 0000000..1480589
--- /dev/null
+++ b/rhai/src/bin/README.md
@@ -0,0 +1,42 @@
+Rhai Tools
+==========
+
+Tools for working with Rhai scripts.
+
+| Tool | Required feature(s) | Description |
+| -------------------------------------------------------------------------------- | :-----------------: | ----------------------------------------------------- |
+| [`rhai-run`](https://github.com/rhaiscript/rhai/blob/main/src/bin/rhai-run.rs) | | runs each filename passed to it as a Rhai script |
+| [`rhai-repl`](https://github.com/rhaiscript/rhai/blob/main/src/bin/rhai-repl.rs) | `rustyline` | a simple REPL that interactively evaluates statements |
+| [`rhai-dbg`](https://github.com/rhaiscript/rhai/blob/main/src/bin/rhai-dbg.rs) | `debugging` | the _Rhai Debugger_ |
+
+For convenience, a feature named `bin-features` is available which is a combination of the following:
+
+* `decimal` – support for decimal numbers
+* `metadata` – access functions metadata
+* `serde` – export functions metadata to JSON
+* `debugging` – required by `rhai-dbg`
+* `rustyline` – required by `rhai-repl`
+
+
+How to Run
+----------
+
+```sh
+cargo run --features bin-features --bin sample_app_to_run
+```
+
+
+How to Install
+--------------
+
+To install these all tools (with full features), use the following command:
+
+```sh
+cargo install --path . --bins --features bin-features
+```
+
+or specifically:
+
+```sh
+cargo install --path . --bin sample_app_to_run --features bin-features
+```
diff --git a/rhai/src/bin/rhai-dbg.rs b/rhai/src/bin/rhai-dbg.rs
new file mode 100644
index 0000000..6fedabb
--- /dev/null
+++ b/rhai/src/bin/rhai-dbg.rs
@@ -0,0 +1,651 @@
+use rhai::debugger::{BreakPoint, DebuggerCommand, DebuggerEvent};
+use rhai::{Dynamic, Engine, EvalAltResult, ImmutableString, Position, Scope, INT};
+
+use std::{
+ env,
+ fs::File,
+ io::{stdin, stdout, Read, Write},
+ path::Path,
+ process::exit,
+};
+
+/// Pretty-print source line.
+fn print_source(lines: &[String], pos: Position, offset: usize, window: (usize, usize)) {
+ if pos.is_none() {
+ // No position
+ println!();
+ return;
+ }
+
+ let line = pos.line().unwrap() - 1;
+ let start = if line >= window.0 { line - window.0 } else { 0 };
+ let end = usize::min(line + window.1, lines.len() - 1);
+ let line_no_len = end.to_string().len();
+
+ // Print error position
+ if start >= end {
+ println!("{}: {}", start + 1, lines[start]);
+ if let Some(pos) = pos.position() {
+ println!("{0:>1$}", "^", pos + offset + line_no_len + 2);
+ }
+ } else {
+ for (n, s) in lines.iter().enumerate().take(end + 1).skip(start) {
+ let marker = if n == line { "> " } else { " " };
+
+ println!(
+ "{0}{1}{2:>3$}{5}│ {0}{4}{5}",
+ if n == line { "\x1b[33m" } else { "" },
+ marker,
+ n + 1,
+ line_no_len,
+ s,
+ if n == line { "\x1b[39m" } else { "" },
+ );
+
+ if n == line {
+ if let Some(pos) = pos.position() {
+ let shift = offset + line_no_len + marker.len() + 2;
+ println!("{0:>1$}{2:>3$}", "│ ", shift, "\x1b[36m^\x1b[39m", pos + 10);
+ }
+ }
+ }
+ }
+}
+
+fn print_current_source(
+ context: &mut rhai::EvalContext,
+ source: Option<&str>,
+ pos: Position,
+ lines: &[String],
+ window: (usize, usize),
+) {
+ let current_source = &mut *context
+ .global_runtime_state_mut()
+ .debugger_mut()
+ .state_mut()
+ .write_lock::<ImmutableString>()
+ .unwrap();
+ let src = source.unwrap_or("");
+ if src != current_source {
+ println!(
+ "\x1b[34m>>> Source => {}\x1b[39m",
+ source.unwrap_or("main script")
+ );
+ *current_source = src.into();
+ }
+ if !src.is_empty() {
+ // Print just a line number for imported modules
+ println!("{src} @ {pos:?}");
+ } else {
+ // Print the current source line
+ print_source(lines, pos, 0, window);
+ }
+}
+
+/// Pretty-print error.
+fn print_error(input: &str, mut err: EvalAltResult) {
+ let lines: Vec<_> = input.trim().split('\n').collect();
+ let pos = err.take_position();
+
+ let line_no = if lines.len() > 1 {
+ if pos.is_none() {
+ String::new()
+ } else {
+ format!("{}: ", pos.line().unwrap())
+ }
+ } else {
+ String::new()
+ };
+
+ // Print error position
+ if pos.is_none() {
+ // No position
+ println!("\x1b[31m{err}\x1b[39m");
+ } else {
+ // Specific position - print line text
+ println!("{line_no}{}", lines[pos.line().unwrap() - 1]);
+
+ for (i, err_line) in err.to_string().split('\n').enumerate() {
+ // Display position marker
+ println!(
+ "\x1b[31m{0:>1$}{err_line}\x1b[39m",
+ if i > 0 { "| " } else { "^ " },
+ line_no.len() + pos.position().unwrap() + 1,
+ );
+ }
+ }
+}
+
+/// Print debug help.
+fn print_debug_help() {
+ println!("help, h => print this help");
+ println!("quit, q, exit, kill => quit");
+ println!("scope => print the scope");
+ println!("operations => print the total operations performed");
+ println!("source => print the current source");
+ println!("print, p => print all variables de-duplicated");
+ println!("print/p this => print the `this` pointer");
+ println!("print/p <variable> => print the current value of a variable");
+ #[cfg(not(feature = "no_module"))]
+ println!("imports => print all imported modules");
+ println!("node => print the current AST node");
+ println!("list, l => print the current source line");
+ println!("list/l <line#> => print a source line");
+ println!("backtrace, bt => print the current call-stack");
+ println!("info break, i b => print all break-points");
+ println!("enable/en <bp#> => enable a break-point");
+ println!("disable/dis <bp#> => disable a break-point");
+ println!("delete, d => delete all break-points");
+ println!("delete/d <bp#> => delete a break-point");
+ #[cfg(not(feature = "no_position"))]
+ println!("break, b => set a new break-point at the current position");
+ #[cfg(not(feature = "no_position"))]
+ println!("break/b <line#> => set a new break-point at a line number");
+ #[cfg(not(feature = "no_object"))]
+ println!("break/b .<prop> => set a new break-point for a property access");
+ println!("break/b <func> => set a new break-point for a function call");
+ println!(
+ "break/b <func> <#args> => set a new break-point for a function call with #args arguments"
+ );
+ println!("throw => throw a runtime exception");
+ println!("throw <message...> => throw an exception with string data");
+ println!("throw <#> => throw an exception with numeric data");
+ println!("run, r => restart the script evaluation from beginning");
+ println!("step, s => go to the next expression, diving into functions");
+ println!("over, o => go to the next expression, skipping oer functions");
+ println!("next, n, <Enter> => go to the next statement, skipping over functions");
+ println!("finish, f => continue until the end of the current function call");
+ println!("continue, c => continue normal execution");
+ println!();
+}
+
+// Load script to debug.
+fn load_script(engine: &Engine) -> (rhai::AST, String) {
+ if let Some(filename) = env::args().nth(1) {
+ let mut contents = String::new();
+
+ let filename = match Path::new(&filename).canonicalize() {
+ Err(err) => {
+ eprintln!("\x1b[31mError script file path: {filename}\n{err}\x1b[39m");
+ exit(1);
+ }
+ Ok(f) => {
+ match f.strip_prefix(std::env::current_dir().unwrap().canonicalize().unwrap()) {
+ Ok(f) => f.into(),
+ _ => f,
+ }
+ }
+ };
+
+ let mut f = match File::open(&filename) {
+ Err(err) => {
+ eprintln!(
+ "\x1b[31mError reading script file: {}\n{}\x1b[39m",
+ filename.to_string_lossy(),
+ err
+ );
+ exit(1);
+ }
+ Ok(f) => f,
+ };
+
+ if let Err(err) = f.read_to_string(&mut contents) {
+ println!(
+ "Error reading script file: {}\n{}",
+ filename.to_string_lossy(),
+ err
+ );
+ exit(1);
+ }
+
+ let script = if contents.starts_with("#!") {
+ // Skip shebang
+ &contents[contents.find('\n').unwrap_or(0)..]
+ } else {
+ &contents[..]
+ };
+
+ let ast = match engine
+ .compile(script)
+ .map_err(Into::<Box<EvalAltResult>>::into)
+ {
+ Err(err) => {
+ print_error(script, *err);
+ exit(1);
+ }
+ Ok(ast) => ast,
+ };
+
+ println!("Script '{}' loaded.", filename.to_string_lossy());
+
+ (ast, contents)
+ } else {
+ eprintln!("\x1b[31mNo script file specified.\x1b[39m");
+ exit(1);
+ }
+}
+
+// Main callback for debugging.
+fn debug_callback(
+ mut context: rhai::EvalContext,
+ event: DebuggerEvent,
+ node: rhai::ASTNode,
+ source: Option<&str>,
+ pos: Position,
+ lines: &[String],
+) -> Result<DebuggerCommand, Box<EvalAltResult>> {
+ // Check event
+ match event {
+ DebuggerEvent::Start if source.is_some() => {
+ println!("\x1b[32m! Script '{}' start\x1b[39m", source.unwrap())
+ }
+ DebuggerEvent::Start => println!("\x1b[32m! Script start\x1b[39m"),
+ DebuggerEvent::End if source.is_some() => {
+ println!("\x1b[31m! Script '{}' end\x1b[39m", source.unwrap())
+ }
+ DebuggerEvent::End => println!("\x1b[31m! Script end\x1b[39m"),
+ DebuggerEvent::Step => (),
+ DebuggerEvent::BreakPoint(n) => {
+ match context.global_runtime_state().debugger().break_points()[n] {
+ #[cfg(not(feature = "no_position"))]
+ BreakPoint::AtPosition { .. } => (),
+ BreakPoint::AtFunctionName { ref name, .. }
+ | BreakPoint::AtFunctionCall { ref name, .. } => {
+ println!("! Call to function {name}.")
+ }
+ #[cfg(not(feature = "no_object"))]
+ BreakPoint::AtProperty { ref name, .. } => {
+ println!("! Property {name} accessed.")
+ }
+ _ => unreachable!(),
+ }
+ }
+ DebuggerEvent::FunctionExitWithValue(r) => {
+ println!(
+ "! Return from function call '{}' => {:?}",
+ context
+ .global_runtime_state()
+ .debugger()
+ .call_stack()
+ .last()
+ .unwrap()
+ .fn_name,
+ r
+ )
+ }
+ DebuggerEvent::FunctionExitWithError(err) => {
+ println!(
+ "! Return from function call '{}' with error: {}",
+ context
+ .global_runtime_state()
+ .debugger()
+ .call_stack()
+ .last()
+ .unwrap()
+ .fn_name,
+ err
+ )
+ }
+ _ => unreachable!(),
+ }
+
+ // Print current source line
+ print_current_source(&mut context, source, pos, lines, (0, 0));
+
+ // Read stdin for commands
+ let mut input = String::new();
+
+ loop {
+ print!("dbg> ");
+
+ stdout().flush().expect("couldn't flush stdout");
+
+ input.clear();
+
+ match stdin().read_line(&mut input) {
+ Ok(0) => break Ok(DebuggerCommand::Continue),
+ Ok(_) => match input.split_whitespace().collect::<Vec<_>>().as_slice() {
+ ["help" | "h"] => print_debug_help(),
+ ["exit" | "quit" | "q" | "kill", ..] => {
+ println!("Script terminated. Bye!");
+ exit(0);
+ }
+ ["node"] => {
+ if pos.is_none() {
+ println!("{node:?}");
+ } else {
+ match source {
+ Some(source) => println!("{node:?} {source} @ {pos:?}"),
+ None => println!("{node:?} @ {pos:?}"),
+ }
+ }
+ println!();
+ }
+ ["operations"] => {
+ println!("{}", context.global_runtime_state().num_operations)
+ }
+ ["source"] => {
+ println!("{}", context.global_runtime_state().source().unwrap_or(""))
+ }
+ ["list" | "l"] => print_current_source(&mut context, source, pos, lines, (3, 6)),
+ ["list" | "l", n] if n.parse::<usize>().is_ok() => {
+ let num = n.parse::<usize>().unwrap();
+ if num == 0 || num > lines.len() {
+ eprintln!("\x1b[31mInvalid line: {num}\x1b[39m");
+ } else {
+ let pos = Position::new(num as u16, 0);
+ print_current_source(&mut context, source, pos, lines, (3, 6));
+ }
+ }
+ ["continue" | "c"] => break Ok(DebuggerCommand::Continue),
+ ["finish" | "f"] => break Ok(DebuggerCommand::FunctionExit),
+ [] | ["step" | "s"] => break Ok(DebuggerCommand::StepInto),
+ ["over" | "o"] => break Ok(DebuggerCommand::StepOver),
+ ["next" | "n"] => break Ok(DebuggerCommand::Next),
+ ["scope"] => println!("{}", context.scope()),
+ ["print" | "p", "this"] => match context.this_ptr() {
+ Some(value) => println!("=> {value:?}"),
+ None => println!("`this` pointer is unbound."),
+ },
+ ["print" | "p", var_name] => match context.scope().get_value::<Dynamic>(var_name) {
+ Some(value) => println!("=> {value:?}"),
+ None => eprintln!("Variable not found: {var_name}"),
+ },
+ ["print" | "p"] => {
+ println!("{}", context.scope().clone_visible());
+ if let Some(value) = context.this_ptr() {
+ println!("this = {value:?}");
+ }
+ }
+ #[cfg(not(feature = "no_module"))]
+ ["imports"] => {
+ for (i, (name, module)) in context
+ .global_runtime_state()
+ .scan_imports_raw()
+ .enumerate()
+ {
+ println!(
+ "[{}] {} = {}",
+ i + 1,
+ name,
+ module.id().unwrap_or("<unknown>")
+ );
+ }
+
+ println!();
+ }
+ #[cfg(not(feature = "no_function"))]
+ ["backtrace" | "bt"] => {
+ for frame in context
+ .global_runtime_state()
+ .debugger()
+ .call_stack()
+ .iter()
+ .rev()
+ {
+ println!("{frame}")
+ }
+ }
+ ["info" | "i", "break" | "b"] => Iterator::for_each(
+ context
+ .global_runtime_state()
+ .debugger()
+ .break_points()
+ .iter()
+ .enumerate(),
+ |(i, bp)| match bp {
+ #[cfg(not(feature = "no_position"))]
+ rhai::debugger::BreakPoint::AtPosition { pos, .. } => {
+ let line_num = format!("[{}] line ", i + 1);
+ print!("{line_num}");
+ print_source(lines, *pos, line_num.len(), (0, 0));
+ }
+ _ => println!("[{}] {bp}", i + 1),
+ },
+ ),
+ ["enable" | "en", n] => {
+ if let Ok(n) = n.parse::<usize>() {
+ let range = 1..=context
+ .global_runtime_state_mut()
+ .debugger()
+ .break_points()
+ .len();
+ if range.contains(&n) {
+ context
+ .global_runtime_state_mut()
+ .debugger_mut()
+ .break_points_mut()
+ .get_mut(n - 1)
+ .unwrap()
+ .enable(true);
+ println!("Break-point #{n} enabled.")
+ } else {
+ eprintln!("\x1b[31mInvalid break-point: {n}\x1b[39m");
+ }
+ } else {
+ eprintln!("\x1b[31mInvalid break-point: '{n}'\x1b[39m");
+ }
+ }
+ ["disable" | "dis", n] => {
+ if let Ok(n) = n.parse::<usize>() {
+ let range = 1..=context
+ .global_runtime_state_mut()
+ .debugger()
+ .break_points()
+ .len();
+ if range.contains(&n) {
+ context
+ .global_runtime_state_mut()
+ .debugger_mut()
+ .break_points_mut()
+ .get_mut(n - 1)
+ .unwrap()
+ .enable(false);
+ println!("Break-point #{n} disabled.")
+ } else {
+ eprintln!("\x1b[31mInvalid break-point: {n}\x1b[39m");
+ }
+ } else {
+ eprintln!("\x1b[31mInvalid break-point: '{n}'\x1b[39m");
+ }
+ }
+ ["delete" | "d", n] => {
+ if let Ok(n) = n.parse::<usize>() {
+ let range = 1..=context
+ .global_runtime_state_mut()
+ .debugger()
+ .break_points()
+ .len();
+ if range.contains(&n) {
+ context
+ .global_runtime_state_mut()
+ .debugger_mut()
+ .break_points_mut()
+ .remove(n - 1);
+ println!("Break-point #{n} deleted.")
+ } else {
+ eprintln!("\x1b[31mInvalid break-point: {n}\x1b[39m");
+ }
+ } else {
+ eprintln!("\x1b[31mInvalid break-point: '{n}'\x1b[39m");
+ }
+ }
+ ["delete" | "d"] => {
+ context
+ .global_runtime_state_mut()
+ .debugger_mut()
+ .break_points_mut()
+ .clear();
+ println!("All break-points deleted.");
+ }
+ ["break" | "b", fn_name, args] => {
+ if let Ok(args) = args.parse::<usize>() {
+ let bp = rhai::debugger::BreakPoint::AtFunctionCall {
+ name: fn_name.trim().into(),
+ args,
+ enabled: true,
+ };
+ println!("Break-point added for {bp}");
+ context
+ .global_runtime_state_mut()
+ .debugger_mut()
+ .break_points_mut()
+ .push(bp);
+ } else {
+ eprintln!("\x1b[31mInvalid number of arguments: '{args}'\x1b[39m");
+ }
+ }
+ // Property name
+ #[cfg(not(feature = "no_object"))]
+ ["break" | "b", param] if param.starts_with('.') && param.len() > 1 => {
+ let bp = rhai::debugger::BreakPoint::AtProperty {
+ name: param[1..].into(),
+ enabled: true,
+ };
+ println!("Break-point added for {bp}");
+ context
+ .global_runtime_state_mut()
+ .debugger_mut()
+ .break_points_mut()
+ .push(bp);
+ }
+ // Numeric parameter
+ #[cfg(not(feature = "no_position"))]
+ ["break" | "b", param] if param.parse::<usize>().is_ok() => {
+ let n = param.parse::<usize>().unwrap();
+ let range = if source.is_none() {
+ 1..=lines.len()
+ } else {
+ 1..=(u16::MAX as usize)
+ };
+
+ if range.contains(&n) {
+ let bp = rhai::debugger::BreakPoint::AtPosition {
+ source: source.map(|s| s.into()),
+ pos: Position::new(n as u16, 0),
+ enabled: true,
+ };
+ println!("Break-point added {bp}");
+ context
+ .global_runtime_state_mut()
+ .debugger_mut()
+ .break_points_mut()
+ .push(bp);
+ } else {
+ eprintln!("\x1b[31mInvalid line number: '{n}'\x1b[39m");
+ }
+ }
+ // Function name parameter
+ ["break" | "b", param] => {
+ let bp = rhai::debugger::BreakPoint::AtFunctionName {
+ name: param.trim().into(),
+ enabled: true,
+ };
+ println!("Break-point added for {bp}");
+ context
+ .global_runtime_state_mut()
+ .debugger_mut()
+ .break_points_mut()
+ .push(bp);
+ }
+ #[cfg(not(feature = "no_position"))]
+ ["break" | "b"] => {
+ let bp = rhai::debugger::BreakPoint::AtPosition {
+ source: source.map(|s| s.into()),
+ pos,
+ enabled: true,
+ };
+ println!("Break-point added {bp}");
+ context
+ .global_runtime_state_mut()
+ .debugger_mut()
+ .break_points_mut()
+ .push(bp);
+ }
+ ["throw"] => break Err(EvalAltResult::ErrorRuntime(Dynamic::UNIT, pos).into()),
+ ["throw", num] if num.trim().parse::<INT>().is_ok() => {
+ let value = num.trim().parse::<INT>().unwrap().into();
+ break Err(EvalAltResult::ErrorRuntime(value, pos).into());
+ }
+ #[cfg(not(feature = "no_float"))]
+ ["throw", num] if num.trim().parse::<rhai::FLOAT>().is_ok() => {
+ let value = num.trim().parse::<rhai::FLOAT>().unwrap().into();
+ break Err(EvalAltResult::ErrorRuntime(value, pos).into());
+ }
+ ["throw", ..] => {
+ let msg = input.trim().split_once(' ').map(|(_, x)| x).unwrap_or("");
+ break Err(EvalAltResult::ErrorRuntime(msg.trim().into(), pos).into());
+ }
+ ["run" | "r"] => {
+ println!("Terminating current run...");
+ break Err(EvalAltResult::ErrorTerminated(Dynamic::UNIT, pos).into());
+ }
+ _ => eprintln!(
+ "\x1b[31mInvalid debugger command: '{}'\x1b[39m",
+ input.trim()
+ ),
+ },
+ Err(err) => panic!("input error: {}", err),
+ }
+ }
+}
+
+fn main() {
+ let title = format!("Rhai Debugger (version {})", env!("CARGO_PKG_VERSION"));
+ println!("{title}");
+ println!("{0:=<1$}", "", title.len());
+
+ // Initialize scripting engine
+ let mut engine = Engine::new();
+
+ #[cfg(not(feature = "no_optimize"))]
+ engine.set_optimization_level(rhai::OptimizationLevel::None);
+
+ let (ast, script) = load_script(&engine);
+
+ // Hook up debugger
+ let lines: Vec<_> = script.trim().split('\n').map(|s| s.to_string()).collect();
+
+ #[allow(deprecated)]
+ engine.register_debugger(
+ // Store the current source in the debugger state
+ |engine, mut debugger| {
+ debugger.set_state(engine.const_empty_string());
+ debugger
+ },
+ // Main debugging interface
+ move |context, event, node, source, pos| {
+ debug_callback(context, event, node, source, pos, &lines)
+ },
+ );
+
+ // Set a file module resolver without caching
+ #[cfg(not(feature = "no_module"))]
+ #[cfg(not(feature = "no_std"))]
+ {
+ let mut resolver = rhai::module_resolvers::FileModuleResolver::new();
+ resolver.enable_cache(false);
+ engine.set_module_resolver(resolver);
+ }
+
+ println!("Type 'help' for commands list.");
+ println!();
+
+ // Evaluate
+ while let Err(err) = engine.run_ast_with_scope(&mut Scope::new(), &ast) {
+ match *err {
+ // Loop back to restart
+ EvalAltResult::ErrorTerminated(..) => {
+ println!("Restarting script...");
+ }
+ // Break evaluation
+ _ => {
+ print_error(&script, *err);
+ println!();
+ break;
+ }
+ }
+ }
+
+ println!("Script terminated. Bye!");
+}
diff --git a/rhai/src/bin/rhai-repl.rs b/rhai/src/bin/rhai-repl.rs
new file mode 100644
index 0000000..c296f38
--- /dev/null
+++ b/rhai/src/bin/rhai-repl.rs
@@ -0,0 +1,595 @@
+use rhai::plugin::*;
+use rhai::{Dynamic, Engine, EvalAltResult, Module, Scope, AST, INT};
+use rustyline::config::Builder;
+use rustyline::error::ReadlineError;
+use rustyline::history::{History, SearchDirection};
+use rustyline::{Cmd, DefaultEditor, Event, EventHandler, KeyCode, KeyEvent, Modifiers, Movement};
+
+use std::{env, fs::File, io::Read, path::Path, process::exit};
+
+const HISTORY_FILE: &str = ".rhai-repl-history";
+
+/// Pretty-print error.
+fn print_error(input: &str, mut err: EvalAltResult) {
+ let lines: Vec<_> = input.split('\n').collect();
+ let pos = err.take_position();
+
+ let line_no = if lines.len() > 1 {
+ if pos.is_none() {
+ String::new()
+ } else {
+ format!("{}: ", pos.line().unwrap())
+ }
+ } else {
+ String::new()
+ };
+
+ // Print error position
+ if pos.is_none() {
+ // No position
+ println!("{err}");
+ } else {
+ // Specific position - print line text
+ println!("{line_no}{}", lines[pos.line().unwrap() - 1]);
+
+ for (i, err_line) in err.to_string().split('\n').enumerate() {
+ // Display position marker
+ println!(
+ "{0:>1$}{err_line}",
+ if i > 0 { "| " } else { "^ " },
+ line_no.len() + pos.position().unwrap() + 1,
+ );
+ }
+ }
+}
+
+/// Print help text.
+fn print_help() {
+ println!("help => print this help");
+ println!("quit, exit => quit");
+ println!("keys => print list of key bindings");
+ println!("history => print lines history");
+ println!("!! => repeat the last history line");
+ println!("!<#> => repeat a particular history line");
+ println!("!<text> => repeat the last history line starting with some text");
+ println!("!?<text> => repeat the last history line containing some text");
+ println!("scope => print all variables in the scope");
+ println!("strict => toggle on/off Strict Variables Mode");
+ #[cfg(not(feature = "no_optimize"))]
+ println!("optimize => toggle on/off script optimization");
+ #[cfg(feature = "metadata")]
+ println!("functions => print all functions defined");
+ #[cfg(feature = "metadata")]
+ println!("json => output all functions to `metadata.json`");
+ println!("ast => print the last AST (optimized)");
+ #[cfg(not(feature = "no_optimize"))]
+ println!("astu => print the last raw, un-optimized AST");
+ println!();
+ println!("press Ctrl-Enter or end a line with `\\`");
+ println!("to continue to the next line.");
+ println!();
+}
+
+/// Print key bindings.
+fn print_keys() {
+ println!("Home => move to beginning of line");
+ println!("Ctrl-Home => move to beginning of input");
+ println!("End => move to end of line");
+ println!("Ctrl-End => move to end of input");
+ println!("Left => move left");
+ println!("Ctrl-Left => move left by one word");
+ println!("Right => move right by one word");
+ println!("Ctrl-Right => move right");
+ println!("Up => previous line or history");
+ println!("Ctrl-Up => previous history");
+ println!("Down => next line or history");
+ println!("Ctrl-Down => next history");
+ println!("Ctrl-R => reverse search history");
+ println!(" (Ctrl-S forward, Ctrl-G cancel)");
+ println!("Ctrl-L => clear screen");
+ #[cfg(target_family = "windows")]
+ println!("Escape => clear all input");
+ println!("Ctrl-C => exit");
+ println!("Ctrl-D => EOF (when line empty)");
+ println!("Ctrl-H, Backspace => backspace");
+ println!("Ctrl-D, Del => delete character");
+ println!("Ctrl-U => delete from start");
+ println!("Ctrl-W => delete previous word");
+ println!("Ctrl-T => transpose characters");
+ println!("Ctrl-V => insert special character");
+ println!("Ctrl-Y => paste yank");
+ #[cfg(target_family = "unix")]
+ println!("Ctrl-Z => suspend");
+ #[cfg(target_family = "windows")]
+ println!("Ctrl-Z => undo");
+ println!("Ctrl-_ => undo");
+ println!("Enter => run code");
+ println!("Shift-Ctrl-Enter => continue to next line");
+ println!();
+ println!("Plus all standard Emacs key bindings");
+ println!();
+}
+
+// Load script files specified in the command line.
+#[cfg(not(feature = "no_module"))]
+#[cfg(not(feature = "no_std"))]
+fn load_script_files(engine: &mut Engine) {
+ // Load init scripts
+ let mut contents = String::new();
+ let mut has_init_scripts = false;
+
+ for filename in env::args().skip(1) {
+ let filename = match Path::new(&filename).canonicalize() {
+ Err(err) => {
+ eprintln!("Error script file path: {filename}\n{err}");
+ exit(1);
+ }
+ Ok(f) => {
+ match f.strip_prefix(std::env::current_dir().unwrap().canonicalize().unwrap()) {
+ Ok(f) => f.into(),
+ _ => f,
+ }
+ }
+ };
+
+ contents.clear();
+
+ let mut f = match File::open(&filename) {
+ Err(err) => {
+ eprintln!(
+ "Error reading script file: {}\n{}",
+ filename.to_string_lossy(),
+ err
+ );
+ exit(1);
+ }
+ Ok(f) => f,
+ };
+
+ if let Err(err) = f.read_to_string(&mut contents) {
+ println!(
+ "Error reading script file: {}\n{}",
+ filename.to_string_lossy(),
+ err
+ );
+ exit(1);
+ }
+
+ let module = match engine
+ .compile(&contents)
+ .map_err(|err| err.into())
+ .and_then(|mut ast| {
+ ast.set_source(filename.to_string_lossy().to_string());
+ Module::eval_ast_as_new(Scope::new(), &ast, engine)
+ }) {
+ Err(err) => {
+ let filename = filename.to_string_lossy();
+
+ eprintln!("{:=<1$}", "", filename.len());
+ eprintln!("{filename}");
+ eprintln!("{:=<1$}", "", filename.len());
+ eprintln!();
+
+ print_error(&contents, *err);
+ exit(1);
+ }
+ Ok(m) => m,
+ };
+
+ engine.register_global_module(module.into());
+
+ has_init_scripts = true;
+
+ println!("Script '{}' loaded.", filename.to_string_lossy());
+ }
+
+ if has_init_scripts {
+ println!();
+ }
+}
+
+// Setup the Rustyline editor.
+fn setup_editor() -> DefaultEditor {
+ //env_logger::init();
+ let config = Builder::new()
+ .tab_stop(4)
+ .indent_size(4)
+ .bracketed_paste(true)
+ .build();
+ let mut rl = DefaultEditor::with_config(config).unwrap();
+
+ // Bind more keys
+
+ // On Windows, Esc clears the input buffer
+ #[cfg(target_family = "windows")]
+ rl.bind_sequence(
+ Event::KeySeq(vec![KeyEvent(KeyCode::Esc, Modifiers::empty())]),
+ EventHandler::Simple(Cmd::Kill(Movement::WholeBuffer)),
+ );
+ // On Windows, Ctrl-Z is undo
+ #[cfg(target_family = "windows")]
+ rl.bind_sequence(
+ Event::KeySeq(vec![KeyEvent::ctrl('z')]),
+ EventHandler::Simple(Cmd::Undo(1)),
+ );
+ // Map Ctrl-Enter to insert a new line - bypass need for `\` continuation
+ rl.bind_sequence(
+ Event::KeySeq(vec![KeyEvent(KeyCode::Char('J'), Modifiers::CTRL)]),
+ EventHandler::Simple(Cmd::Newline),
+ );
+ rl.bind_sequence(
+ Event::KeySeq(vec![KeyEvent(KeyCode::Enter, Modifiers::CTRL)]),
+ EventHandler::Simple(Cmd::Newline),
+ );
+ // Map Ctrl-Home and Ctrl-End for beginning/end of input
+ rl.bind_sequence(
+ Event::KeySeq(vec![KeyEvent(KeyCode::Home, Modifiers::CTRL)]),
+ EventHandler::Simple(Cmd::Move(Movement::BeginningOfBuffer)),
+ );
+ rl.bind_sequence(
+ Event::KeySeq(vec![KeyEvent(KeyCode::End, Modifiers::CTRL)]),
+ EventHandler::Simple(Cmd::Move(Movement::EndOfBuffer)),
+ );
+ // Map Ctrl-Up and Ctrl-Down to skip up/down the history, even through multi-line histories
+ rl.bind_sequence(
+ Event::KeySeq(vec![KeyEvent(KeyCode::Down, Modifiers::CTRL)]),
+ EventHandler::Simple(Cmd::NextHistory),
+ );
+ rl.bind_sequence(
+ Event::KeySeq(vec![KeyEvent(KeyCode::Up, Modifiers::CTRL)]),
+ EventHandler::Simple(Cmd::PreviousHistory),
+ );
+
+ // Load the history file
+ if rl.load_history(HISTORY_FILE).is_err() {
+ eprintln!("! No previous lines history!");
+ }
+
+ rl
+}
+
+/// Module containing sample functions.
+#[export_module]
+mod sample_functions {
+ /// This is a sample function.
+ ///
+ /// It takes two numbers and prints them to a string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let result = test(42, 123);
+ ///
+ /// print(result); // prints "42 123"
+ /// ```
+ pub fn test(x: INT, y: INT) -> String {
+ format!("{x} {y}")
+ }
+
+ /// This is a sample method for integers.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = 42;
+ ///
+ /// x.test(123, "hello");
+ ///
+ /// print(x); // prints 170
+ /// ```
+ #[rhai_fn(name = "test")]
+ pub fn test2(x: &mut INT, y: INT, z: &str) {
+ *x += y + (z.len() as INT);
+ println!("{x} {y} {z}");
+ }
+}
+
+fn main() {
+ let title = format!("Rhai REPL tool (version {})", env!("CARGO_PKG_VERSION"));
+ println!("{title}");
+ println!("{0:=<1$}", "", title.len());
+
+ #[cfg(not(feature = "no_optimize"))]
+ let mut optimize_level = rhai::OptimizationLevel::Simple;
+
+ // Initialize scripting engine
+ let mut engine = Engine::new();
+
+ #[cfg(not(feature = "no_module"))]
+ #[cfg(not(feature = "no_std"))]
+ load_script_files(&mut engine);
+
+ // Setup Engine
+ #[cfg(not(feature = "no_optimize"))]
+ engine.set_optimization_level(rhai::OptimizationLevel::None);
+
+ // Set a file module resolver without caching
+ #[cfg(not(feature = "no_module"))]
+ #[cfg(not(feature = "no_std"))]
+ {
+ let mut resolver = rhai::module_resolvers::FileModuleResolver::new();
+ resolver.enable_cache(false);
+ engine.set_module_resolver(resolver);
+ }
+
+ // Register sample functions
+ engine.register_global_module(exported_module!(sample_functions).into());
+
+ // Create scope
+ let mut scope = Scope::new();
+
+ // REPL line editor setup
+ let mut rl = setup_editor();
+
+ // REPL loop
+ let mut input = String::new();
+ let mut replacement = None;
+ let mut replacement_index = 0;
+ let mut history_offset = 1;
+
+ let mut main_ast = AST::empty();
+ #[cfg(not(feature = "no_optimize"))]
+ let mut ast_u = AST::empty();
+ let mut ast = AST::empty();
+
+ print_help();
+
+ 'main_loop: loop {
+ if let Some(replace) = replacement.take() {
+ input = replace;
+ if rl
+ .add_history_entry(input.clone())
+ .expect("Failed to add history entry")
+ {
+ history_offset += 1;
+ }
+ if input.contains('\n') {
+ println!("[{replacement_index}] ~~~~");
+ println!("{input}");
+ println!("~~~~");
+ } else {
+ println!("[{replacement_index}] {input}");
+ }
+ replacement_index = 0;
+ } else {
+ input.clear();
+
+ loop {
+ let prompt = if input.is_empty() { "repl> " } else { " > " };
+
+ match rl.readline(prompt) {
+ // Line continuation
+ Ok(mut line) if line.ends_with('\\') => {
+ line.pop();
+ input += &line;
+ input.push('\n');
+ }
+ Ok(line) => {
+ input += &line;
+ let cmd = input.trim();
+ if !cmd.is_empty()
+ && !cmd.starts_with('!')
+ && cmd.trim() != "history"
+ && rl
+ .add_history_entry(input.clone())
+ .expect("Failed to add history entry")
+ {
+ history_offset += 1;
+ }
+ break;
+ }
+
+ Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break 'main_loop,
+
+ Err(err) => {
+ eprintln!("Error: {err:?}");
+ break 'main_loop;
+ }
+ }
+ }
+ }
+
+ let cmd = input.trim();
+
+ if cmd.is_empty() {
+ continue;
+ }
+
+ // Implement standard commands
+ match cmd {
+ "help" => {
+ print_help();
+ continue;
+ }
+ "keys" => {
+ print_keys();
+ continue;
+ }
+ "exit" | "quit" => break, // quit
+ "history" => {
+ for (i, h) in rl.history().iter().enumerate() {
+ match &h.split('\n').collect::<Vec<_>>()[..] {
+ [line] => println!("[{}] {line}", history_offset + i),
+ lines => {
+ for (x, line) in lines.iter().enumerate() {
+ let number = format!("[{}]", history_offset + i);
+ if x == 0 {
+ println!("{number} {}", line.trim_end());
+ } else {
+ println!("{0:>1$} {2}", "", number.len(), line.trim_end());
+ }
+ }
+ }
+ }
+ }
+ continue;
+ }
+ "strict" if engine.strict_variables() => {
+ engine.set_strict_variables(false);
+ println!("Strict Variables Mode turned OFF.");
+ continue;
+ }
+ "strict" => {
+ engine.set_strict_variables(true);
+ println!("Strict Variables Mode turned ON.");
+ continue;
+ }
+ #[cfg(not(feature = "no_optimize"))]
+ "optimize" if optimize_level == rhai::OptimizationLevel::Simple => {
+ optimize_level = rhai::OptimizationLevel::None;
+ println!("Script optimization turned OFF.");
+ continue;
+ }
+ #[cfg(not(feature = "no_optimize"))]
+ "optimize" => {
+ optimize_level = rhai::OptimizationLevel::Simple;
+ println!("Script optimization turned ON.");
+ continue;
+ }
+ "scope" => {
+ println!("{scope}");
+ continue;
+ }
+ #[cfg(not(feature = "no_optimize"))]
+ "astu" => {
+ // print the last un-optimized AST
+ println!("{ast_u:#?}\n");
+ continue;
+ }
+ "ast" => {
+ // print the last AST
+ println!("{ast:#?}\n");
+ continue;
+ }
+ #[cfg(feature = "metadata")]
+ "functions" => {
+ // print a list of all registered functions
+ for f in engine.gen_fn_signatures(false) {
+ println!("{f}")
+ }
+
+ #[cfg(not(feature = "no_function"))]
+ for f in main_ast.iter_functions() {
+ println!("{f}")
+ }
+
+ println!();
+ continue;
+ }
+ #[cfg(feature = "metadata")]
+ "json" => {
+ use std::io::Write;
+
+ let json = engine
+ .gen_fn_metadata_with_ast_to_json(&main_ast, false)
+ .expect("Unable to generate JSON");
+ let mut f = std::fs::File::create("metadata.json")
+ .expect("Unable to create `metadata.json`");
+ f.write_all(json.as_bytes()).expect("Unable to write data");
+ println!("Functions metadata written to `metadata.json`.");
+ continue;
+ }
+ "!!" => {
+ match rl.history().iter().last() {
+ Some(line) => {
+ replacement = Some(line.clone());
+ replacement_index = history_offset + rl.history().len() - 1;
+ }
+ None => eprintln!("No lines history!"),
+ }
+ continue;
+ }
+ _ if cmd.starts_with("!?") => {
+ let text = cmd[2..].trim();
+ let history = rl
+ .history()
+ .iter()
+ .rev()
+ .enumerate()
+ .find(|&(.., h)| h.contains(text));
+
+ match history {
+ Some((n, line)) => {
+ replacement = Some(line.clone());
+ replacement_index = history_offset + (rl.history().len() - 1 - n);
+ }
+ None => eprintln!("History line not found: {text}"),
+ }
+ continue;
+ }
+ _ if cmd.starts_with('!') => {
+ if let Ok(num) = cmd[1..].parse::<usize>() {
+ if num >= history_offset {
+ if let Some(line) = rl
+ .history()
+ .get(num - history_offset, SearchDirection::Forward)
+ .expect("Failed to get history entry")
+ {
+ replacement = Some(line.entry.into());
+ replacement_index = num;
+ continue;
+ }
+ }
+ } else {
+ let prefix = cmd[1..].trim();
+ if let Some((n, line)) = rl
+ .history()
+ .iter()
+ .rev()
+ .enumerate()
+ .find(|&(.., h)| h.trim_start().starts_with(prefix))
+ {
+ replacement = Some(line.clone());
+ replacement_index = history_offset + (rl.history().len() - 1 - n);
+ continue;
+ }
+ }
+ eprintln!("History line not found: {}", &cmd[1..]);
+ continue;
+ }
+ _ => (),
+ }
+
+ match engine
+ .compile_with_scope(&scope, &input)
+ .map_err(Into::into)
+ .and_then(|r| {
+ #[cfg(not(feature = "no_optimize"))]
+ {
+ ast_u = r.clone();
+
+ ast = engine.optimize_ast(&scope, r, optimize_level);
+ }
+
+ #[cfg(feature = "no_optimize")]
+ {
+ ast = r;
+ }
+
+ // Merge the AST into the main
+ main_ast += ast.clone();
+
+ // Evaluate
+ engine.eval_ast_with_scope::<Dynamic>(&mut scope, &main_ast)
+ }) {
+ Ok(result) if !result.is_unit() => {
+ println!("=> {result:?}");
+ println!();
+ }
+ Ok(_) => (),
+ Err(err) => {
+ println!();
+ print_error(&input, *err);
+ println!();
+ }
+ }
+
+ // Throw away all the statements, leaving only the functions
+ main_ast.clear_statements();
+ }
+
+ rl.save_history(HISTORY_FILE)
+ .expect("Failed to save history");
+
+ println!("Bye!");
+}
diff --git a/rhai/src/bin/rhai-run.rs b/rhai/src/bin/rhai-run.rs
new file mode 100644
index 0000000..4d59fc6
--- /dev/null
+++ b/rhai/src/bin/rhai-run.rs
@@ -0,0 +1,108 @@
+use rhai::{Engine, EvalAltResult, Position};
+
+use std::{env, fs::File, io::Read, path::Path, process::exit};
+
+fn eprint_error(input: &str, mut err: EvalAltResult) {
+ fn eprint_line(lines: &[&str], pos: Position, err_msg: &str) {
+ let line = pos.line().unwrap();
+ let line_no = format!("{line}: ");
+
+ eprintln!("{line_no}{}", lines[line - 1]);
+
+ for (i, err_line) in err_msg.to_string().split('\n').enumerate() {
+ // Display position marker
+ println!(
+ "{0:>1$}{err_line}",
+ if i > 0 { "| " } else { "^ " },
+ line_no.len() + pos.position().unwrap() + 1,
+ );
+ }
+ eprintln!();
+ }
+
+ let lines: Vec<_> = input.split('\n').collect();
+
+ // Print error
+ let pos = err.take_position();
+
+ if pos.is_none() {
+ // No position
+ eprintln!("{err}");
+ } else {
+ // Specific position
+ eprint_line(&lines, pos, &err.to_string())
+ }
+}
+
+fn main() {
+ let mut contents = String::new();
+
+ for filename in env::args().skip(1) {
+ let filename = match Path::new(&filename).canonicalize() {
+ Err(err) => {
+ eprintln!("Error script file path: {filename}\n{err}");
+ exit(1);
+ }
+ Ok(f) => match f.strip_prefix(std::env::current_dir().unwrap().canonicalize().unwrap())
+ {
+ Ok(f) => f.into(),
+ _ => f,
+ },
+ };
+
+ // Initialize scripting engine
+ #[allow(unused_mut)]
+ let mut engine = Engine::new();
+
+ #[cfg(not(feature = "no_optimize"))]
+ engine.set_optimization_level(rhai::OptimizationLevel::Simple);
+
+ let mut f = match File::open(&filename) {
+ Err(err) => {
+ eprintln!(
+ "Error reading script file: {}\n{}",
+ filename.to_string_lossy(),
+ err
+ );
+ exit(1);
+ }
+ Ok(f) => f,
+ };
+
+ contents.clear();
+
+ if let Err(err) = f.read_to_string(&mut contents) {
+ eprintln!(
+ "Error reading script file: {}\n{}",
+ filename.to_string_lossy(),
+ err
+ );
+ exit(1);
+ }
+
+ let contents = if contents.starts_with("#!") {
+ // Skip shebang
+ &contents[contents.find('\n').unwrap_or(0)..]
+ } else {
+ &contents[..]
+ };
+
+ if let Err(err) = engine
+ .compile(contents)
+ .map_err(|err| err.into())
+ .and_then(|mut ast| {
+ ast.set_source(filename.to_string_lossy().to_string());
+ engine.run_ast(&ast)
+ })
+ {
+ let filename = filename.to_string_lossy();
+
+ eprintln!("{:=<1$}", "", filename.len());
+ eprintln!("{filename}");
+ eprintln!("{:=<1$}", "", filename.len());
+ eprintln!();
+
+ eprint_error(contents, *err);
+ }
+ }
+}
diff --git a/rhai/src/config/hashing.rs b/rhai/src/config/hashing.rs
new file mode 100644
index 0000000..38bab24
--- /dev/null
+++ b/rhai/src/config/hashing.rs
@@ -0,0 +1,243 @@
+//! Fixed hashing seeds for stable hashing.
+//!
+//! Set to [`None`] to disable stable hashing.
+//!
+//! See [`rhai::config::hashing::set_ahash_seed`][set_ahash_seed].
+//!
+//! # Example
+//!
+//! ```rust
+//! // Set the hashing seed to [1, 2, 3, 4]
+//! rhai::config::hashing::set_ahash_seed(Some([1, 2, 3, 4])).unwrap();
+//! ```
+//! Alternatively, set this at compile time via the `RHAI_AHASH_SEED` environment variable.
+//!
+//! # Example
+//!
+//! ```sh
+//! env RHAI_AHASH_SEED ="[236,800,954,213]"
+//! ```
+// [236,800,954,213], haha funny yume nikki reference epic uboachan face numberworld nexus moment 100
+
+use crate::config::hashing_env;
+use core::panic::{RefUnwindSafe, UnwindSafe};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ cell::UnsafeCell,
+ marker::PhantomData,
+ mem,
+ mem::MaybeUninit,
+ sync::atomic::{AtomicBool, AtomicUsize, Ordering},
+};
+
+// omg its hokma from record team here to record our locks
+// what does this do?
+// so what this does is keep track of a global address in memory that acts as a global lock
+// i stole this from crossbeam so read their docs for more
+#[must_use]
+struct HokmaLock {
+ lock: AtomicUsize,
+}
+
+impl HokmaLock {
+ #[inline(always)]
+ pub const fn new() -> Self {
+ Self {
+ lock: AtomicUsize::new(0),
+ }
+ }
+
+ pub fn write(&'static self) -> WhenTheHokmaSuppression {
+ loop {
+ // We are only interested in error results
+ if let Err(previous) =
+ self.lock
+ .compare_exchange(1, 1, Ordering::SeqCst, Ordering::SeqCst)
+ {
+ // If we failed, previous cannot be 1
+ return WhenTheHokmaSuppression {
+ hokma: self,
+ state: previous,
+ };
+ }
+ }
+ }
+}
+
+struct WhenTheHokmaSuppression {
+ hokma: &'static HokmaLock,
+ state: usize,
+}
+
+impl WhenTheHokmaSuppression {
+ #[inline]
+ pub fn the_price_of_silence(self) {
+ self.hokma.lock.store(self.state, Ordering::SeqCst);
+ mem::forget(self);
+ }
+}
+
+impl Drop for WhenTheHokmaSuppression {
+ #[inline]
+ fn drop(&mut self) {
+ self.hokma
+ .lock
+ .store(self.state.wrapping_add(2), Ordering::SeqCst);
+ }
+}
+
+#[inline(always)]
+fn hokmalock(address: usize) -> &'static HokmaLock {
+ const LEN: usize = 787;
+ #[allow(clippy::declare_interior_mutable_const)]
+ const LCK: HokmaLock = HokmaLock::new();
+ static RECORDS: [HokmaLock; LEN] = [LCK; LEN];
+
+ &RECORDS[address % LEN]
+}
+
+/// # Safety
+///
+/// LOL, there is a reason its called `SusLock`
+#[must_use]
+pub struct SusLock<T: 'static> {
+ initialized: AtomicBool,
+ data: UnsafeCell<MaybeUninit<T>>,
+ _marker: PhantomData<T>,
+}
+
+impl<T: 'static> Default for SusLock<T> {
+ #[inline(always)]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<T: 'static> SusLock<T> {
+ /// Create a new [`SusLock`].
+ #[inline]
+ pub const fn new() -> Self {
+ Self {
+ initialized: AtomicBool::new(false),
+ data: UnsafeCell::new(MaybeUninit::uninit()),
+ _marker: PhantomData,
+ }
+ }
+
+ /// Is the [`SusLock`] initialized?
+ #[inline(always)]
+ #[must_use]
+ pub fn is_initialized(&self) -> bool {
+ self.initialized.load(Ordering::SeqCst)
+ }
+
+ /// Return the value of the [`SusLock`] (if initialized).
+ #[inline]
+ #[must_use]
+ pub fn get(&self) -> Option<&'static T> {
+ if self.initialized.load(Ordering::SeqCst) {
+ let hokma = hokmalock(self.data.get() as usize);
+ // we forgo the optimistic read, because we don't really care
+ let guard = hokma.write();
+ let cast: *const T = self.data.get().cast();
+ let val = unsafe { &*cast.cast::<T>() };
+ guard.the_price_of_silence();
+ Some(val)
+ } else {
+ None
+ }
+ }
+
+ /// Return the value of the [`SusLock`], initializing it if not yet done.
+ #[inline]
+ #[must_use]
+ pub fn get_or_init(&self, f: impl FnOnce() -> T) -> &'static T {
+ if !self.initialized.load(Ordering::SeqCst) {
+ self.initialized.store(true, Ordering::SeqCst);
+ let hokma = hokmalock(self.data.get() as usize);
+ hokma.write();
+ unsafe {
+ self.data.get().write(MaybeUninit::new(f()));
+ }
+ }
+
+ self.get().unwrap()
+ }
+
+ /// Initialize the value of the [`SusLock`].
+ ///
+ /// # Error
+ ///
+ /// If the [`SusLock`] has already been initialized, the current value is returned as error.
+ #[inline]
+ pub fn init(&self, value: T) -> Result<(), T> {
+ if self.initialized.load(Ordering::SeqCst) {
+ Err(value)
+ } else {
+ let _ = self.get_or_init(|| value);
+ Ok(())
+ }
+ }
+}
+
+unsafe impl<T: Sync + Send> Sync for SusLock<T> {}
+unsafe impl<T: Send> Send for SusLock<T> {}
+impl<T: RefUnwindSafe + UnwindSafe> RefUnwindSafe for SusLock<T> {}
+
+impl<T: 'static> Drop for SusLock<T> {
+ #[inline]
+ fn drop(&mut self) {
+ if self.initialized.load(Ordering::SeqCst) {
+ unsafe { (*self.data.get()).assume_init_drop() };
+ }
+ }
+}
+
+static AHASH_SEED: SusLock<Option<[u64; 4]>> = SusLock::new();
+
+/// Set the hashing seed. This is used to hash functions etc.
+///
+/// This is a static global value and affects every Rhai instance.
+/// This should not be used _unless_ you know you need it.
+///
+/// # Warning
+///
+/// * You can only call this function **ONCE** for the entire duration of program execution.
+/// * You **MUST** call this before performing **ANY** Rhai operation (e.g. creating an [`Engine`][crate::Engine]).
+///
+/// # Error
+///
+/// Returns an error containing the existing hashing seed if already set.
+///
+/// # Example
+///
+/// ```rust
+/// # use rhai::Engine;
+/// // Set the hashing seed to [1, 2, 3, 4]
+/// rhai::config::hashing::set_ahash_seed(Some([1, 2, 3, 4])).unwrap();
+///
+/// // Use Rhai AFTER setting the hashing seed
+/// let engine = Engine::new();
+/// ```
+#[inline(always)]
+pub fn set_ahash_seed(new_seed: Option<[u64; 4]>) -> Result<(), Option<[u64; 4]>> {
+ AHASH_SEED.init(new_seed)
+}
+
+/// Get the current hashing Seed.
+///
+/// If the seed is not yet defined, the `RHAI_AHASH_SEED` environment variable (if any) is used.
+///
+/// Otherwise, the hashing seed is randomized to protect against DOS attacks.
+///
+/// See [`rhai::config::hashing::set_ahash_seed`][set_ahash_seed] for more.
+#[inline]
+#[must_use]
+pub fn get_ahash_seed() -> &'static Option<[u64; 4]> {
+ if !AHASH_SEED.is_initialized() {
+ return &hashing_env::AHASH_SEED;
+ }
+
+ AHASH_SEED.get().unwrap_or(&hashing_env::AHASH_SEED)
+}
diff --git a/rhai/src/config/hashing_env.rs b/rhai/src/config/hashing_env.rs
new file mode 100644
index 0000000..9890487
--- /dev/null
+++ b/rhai/src/config/hashing_env.rs
@@ -0,0 +1,3 @@
+//! This file is automatically recreated during build time by `build.rs` from `build.template`.
+
+pub const AHASH_SEED: Option<[u64; 4]> = None;
diff --git a/rhai/src/config/mod.rs b/rhai/src/config/mod.rs
new file mode 100644
index 0000000..35aac0a
--- /dev/null
+++ b/rhai/src/config/mod.rs
@@ -0,0 +1,4 @@
+//! Configuration for Rhai.
+
+pub mod hashing;
+mod hashing_env;
diff --git a/rhai/src/defer.rs b/rhai/src/defer.rs
new file mode 100644
index 0000000..4bf0ff2
--- /dev/null
+++ b/rhai/src/defer.rs
@@ -0,0 +1,108 @@
+//! Facility to run state restoration logic at the end of scope.
+
+use std::ops::{Deref, DerefMut};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// Automatically restore state at the end of the scope.
+macro_rules! defer {
+ (let $temp:ident = $var:ident . $prop:ident; $code:stmt) => {
+ defer!(let $temp = $var.$prop; $code => move |v| v.$prop = $temp);
+ };
+ (let $temp:ident = $var:ident . $prop:ident; $code:stmt => $restore:expr) => {
+ let $temp = $var.$prop;
+ $code
+ defer!($var => $restore);
+ };
+ ($var:ident => $restore:ident; let $temp:ident = $save:expr;) => {
+ defer!($var => $restore; let $temp = $save; {});
+ };
+ ($var:ident if $guard:expr => $restore:ident; let $temp:ident = $save:expr;) => {
+ defer!($var if $guard => $restore; let $temp = $save; {});
+ };
+ ($var:ident => $restore:ident; let $temp:ident = $save:expr; $code:stmt) => {
+ let $temp = $save;
+ $code
+ defer!($var => move |v| { v.$restore($temp); });
+ };
+ ($var:ident if $guard:expr => $restore:ident; let $temp:ident = $save:expr; $code:stmt) => {
+ let $temp = $save;
+ $code
+ defer!($var if $guard => move |v| { v.$restore($temp); });
+ };
+ ($var:ident => $restore:expr) => {
+ defer!($var = $var => $restore);
+ };
+ ($var:ident = $value:expr => $restore:expr) => {
+ let $var = &mut *crate::Deferred::lock($value, $restore);
+ };
+ ($var:ident if Some($guard:ident) => $restore:expr) => {
+ defer!($var = ($var) if Some($guard) => $restore);
+ };
+ ($var:ident = ( $value:expr ) if Some($guard:ident) => $restore:expr) => {
+ let mut __rx__;
+ let $var = if let Some($guard) = $guard {
+ __rx__ = crate::Deferred::lock($value, $restore);
+ &mut *__rx__
+ } else {
+ &mut *$value
+ };
+ };
+ ($var:ident if $guard:expr => $restore:expr) => {
+ defer!($var = ($var) if $guard => $restore);
+ };
+ ($var:ident = ( $value:expr ) if $guard:expr => $restore:expr) => {
+ let mut __rx__;
+ let $var = if $guard {
+ __rx__ = crate::Deferred::lock($value, $restore);
+ &mut *__rx__
+ } else {
+ &mut *$value
+ };
+ };
+}
+
+/// Run custom restoration logic upon the end of scope.
+#[must_use]
+pub struct Deferred<'a, T: ?Sized, R: FnOnce(&mut T)> {
+ lock: &'a mut T,
+ defer: Option<R>,
+}
+
+impl<'a, T: ?Sized, R: FnOnce(&mut T)> Deferred<'a, T, R> {
+ /// Create a new [`Deferred`] that locks a mutable reference and runs restoration logic at
+ /// the end of scope.
+ ///
+ /// Beware that the end of scope means the end of its lifetime, not necessarily waiting until
+ /// the current block scope is exited.
+ #[inline(always)]
+ pub fn lock(value: &'a mut T, restore: R) -> Self {
+ Self {
+ lock: value,
+ defer: Some(restore),
+ }
+ }
+}
+
+impl<'a, T: ?Sized, R: FnOnce(&mut T)> Drop for Deferred<'a, T, R> {
+ #[inline(always)]
+ fn drop(&mut self) {
+ self.defer.take().unwrap()(self.lock);
+ }
+}
+
+impl<'a, T: ?Sized, R: FnOnce(&mut T)> Deref for Deferred<'a, T, R> {
+ type Target = T;
+
+ #[inline(always)]
+ fn deref(&self) -> &Self::Target {
+ self.lock
+ }
+}
+
+impl<'a, T: ?Sized, R: FnOnce(&mut T)> DerefMut for Deferred<'a, T, R> {
+ #[inline(always)]
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.lock
+ }
+}
diff --git a/rhai/src/engine.rs b/rhai/src/engine.rs
new file mode 100644
index 0000000..eacf74a
--- /dev/null
+++ b/rhai/src/engine.rs
@@ -0,0 +1,369 @@
+//! Main module defining the script evaluation [`Engine`].
+
+use crate::api::options::LangOptions;
+use crate::func::native::{
+ locked_write, OnDebugCallback, OnDefVarCallback, OnParseTokenCallback, OnPrintCallback,
+ OnVarCallback,
+};
+use crate::packages::{Package, StandardPackage};
+use crate::tokenizer::Token;
+use crate::types::StringsInterner;
+use crate::{
+ Dynamic, Identifier, ImmutableString, Locked, OptimizationLevel, SharedModule, StaticVec,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{collections::BTreeSet, fmt, num::NonZeroU8};
+
+pub type Precedence = NonZeroU8;
+
+pub const KEYWORD_PRINT: &str = "print";
+pub const KEYWORD_DEBUG: &str = "debug";
+pub const KEYWORD_TYPE_OF: &str = "type_of";
+pub const KEYWORD_EVAL: &str = "eval";
+pub const KEYWORD_FN_PTR: &str = "Fn";
+pub const KEYWORD_FN_PTR_CALL: &str = "call";
+pub const KEYWORD_FN_PTR_CURRY: &str = "curry";
+#[cfg(not(feature = "no_closure"))]
+pub const KEYWORD_IS_SHARED: &str = "is_shared";
+pub const KEYWORD_IS_DEF_VAR: &str = "is_def_var";
+#[cfg(not(feature = "no_function"))]
+pub const KEYWORD_IS_DEF_FN: &str = "is_def_fn";
+#[cfg(not(feature = "no_function"))]
+pub const KEYWORD_THIS: &str = "this";
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_module"))]
+pub const KEYWORD_GLOBAL: &str = "global";
+#[cfg(not(feature = "no_object"))]
+pub const FN_GET: &str = "get$";
+#[cfg(not(feature = "no_object"))]
+pub const FN_SET: &str = "set$";
+#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+pub const FN_IDX_GET: &str = "index$get$";
+#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+pub const FN_IDX_SET: &str = "index$set$";
+#[cfg(not(feature = "no_function"))]
+pub const FN_ANONYMOUS: &str = "anon$";
+
+/// Standard equality comparison operator.
+///
+/// Some standard functions (e.g. searching an [`Array`][crate::Array]) implicitly call this
+/// function to compare two [`Dynamic`] values.
+pub const OP_EQUALS: &str = Token::EqualsTo.literal_syntax();
+
+/// Standard containment testing function.
+///
+/// The `in` operator is implemented as a call to this function.
+pub const OP_CONTAINS: &str = "contains";
+
+/// Standard not operator.
+pub const OP_NOT: &str = Token::Bang.literal_syntax();
+
+/// Standard exclusive range operator.
+pub const OP_EXCLUSIVE_RANGE: &str = Token::ExclusiveRange.literal_syntax();
+
+/// Standard inclusive range operator.
+pub const OP_INCLUSIVE_RANGE: &str = Token::InclusiveRange.literal_syntax();
+
+/// Separator for namespaces.
+#[cfg(not(feature = "no_module"))]
+pub const NAMESPACE_SEPARATOR: &str = Token::DoubleColon.literal_syntax();
+
+/// Rhai main scripting engine.
+///
+/// # Thread Safety
+///
+/// [`Engine`] is re-entrant.
+///
+/// Currently, [`Engine`] is neither [`Send`] nor [`Sync`].
+/// Use the `sync` feature to make it [`Send`] `+` [`Sync`].
+///
+/// # Example
+///
+/// ```
+/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+/// use rhai::Engine;
+///
+/// let engine = Engine::new();
+///
+/// let result = engine.eval::<i64>("40 + 2")?;
+///
+/// println!("Answer: {result}"); // prints 42
+/// # Ok(())
+/// # }
+/// ```
+pub struct Engine {
+ /// A collection of all modules loaded into the global namespace of the Engine.
+ pub(crate) global_modules: StaticVec<SharedModule>,
+ /// A collection of all sub-modules directly loaded into the Engine.
+ #[cfg(not(feature = "no_module"))]
+ pub(crate) global_sub_modules: Option<std::collections::BTreeMap<Identifier, SharedModule>>,
+
+ /// A module resolution service.
+ #[cfg(not(feature = "no_module"))]
+ pub(crate) module_resolver: Option<Box<dyn crate::ModuleResolver>>,
+
+ /// Strings interner.
+ pub(crate) interned_strings: Option<Box<Locked<StringsInterner>>>,
+
+ /// A set of symbols to disable.
+ pub(crate) disabled_symbols: Option<BTreeSet<Identifier>>,
+ /// A map containing custom keywords and precedence to recognize.
+ #[cfg(not(feature = "no_custom_syntax"))]
+ pub(crate) custom_keywords: Option<std::collections::BTreeMap<Identifier, Option<Precedence>>>,
+ /// Custom syntax.
+ #[cfg(not(feature = "no_custom_syntax"))]
+ pub(crate) custom_syntax: Option<
+ std::collections::BTreeMap<Identifier, Box<crate::api::custom_syntax::CustomSyntax>>,
+ >,
+ /// Callback closure for filtering variable definition.
+ pub(crate) def_var_filter: Option<Box<OnDefVarCallback>>,
+ /// Callback closure for resolving variable access.
+ pub(crate) resolve_var: Option<Box<OnVarCallback>>,
+ /// Callback closure to remap tokens during parsing.
+ pub(crate) token_mapper: Option<Box<OnParseTokenCallback>>,
+
+ /// Callback closure for implementing the `print` command.
+ pub(crate) print: Option<Box<OnPrintCallback>>,
+ /// Callback closure for implementing the `debug` command.
+ pub(crate) debug: Option<Box<OnDebugCallback>>,
+ /// Callback closure for progress reporting.
+ #[cfg(not(feature = "unchecked"))]
+ pub(crate) progress: Option<Box<crate::func::native::OnProgressCallback>>,
+
+ /// Language options.
+ pub(crate) options: LangOptions,
+
+ /// Default value for the custom state.
+ pub(crate) def_tag: Dynamic,
+
+ /// Script optimization level.
+ pub(crate) optimization_level: OptimizationLevel,
+
+ /// Max limits.
+ #[cfg(not(feature = "unchecked"))]
+ pub(crate) limits: crate::api::limits::Limits,
+
+ /// Callback closure for debugging.
+ #[cfg(feature = "debugging")]
+ pub(crate) debugger_interface: Option<(
+ Box<crate::eval::OnDebuggingInit>,
+ Box<crate::eval::OnDebuggerCallback>,
+ )>,
+}
+
+impl fmt::Debug for Engine {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut f = f.debug_struct("Engine");
+
+ f.field("global_modules", &self.global_modules);
+
+ #[cfg(not(feature = "no_module"))]
+ f.field("global_sub_modules", &self.global_sub_modules);
+
+ f.field("disabled_symbols", &self.disabled_symbols);
+
+ #[cfg(not(feature = "no_custom_syntax"))]
+ f.field("custom_keywords", &self.custom_keywords).field(
+ "custom_syntax",
+ &self
+ .custom_syntax
+ .iter()
+ .flat_map(|m| m.keys())
+ .map(crate::SmartString::as_str)
+ .collect::<String>(),
+ );
+
+ f.field("def_var_filter", &self.def_var_filter.is_some())
+ .field("resolve_var", &self.resolve_var.is_some())
+ .field("token_mapper", &self.token_mapper.is_some());
+
+ #[cfg(not(feature = "unchecked"))]
+ f.field("progress", &self.progress.is_some());
+
+ f.field("options", &self.options);
+
+ #[cfg(not(feature = "unchecked"))]
+ f.field("limits", &self.limits);
+
+ #[cfg(feature = "debugging")]
+ f.field("debugger_interface", &self.debugger_interface.is_some());
+
+ f.finish()
+ }
+}
+
+impl Default for Engine {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// Make getter function
+#[cfg(not(feature = "no_object"))]
+#[inline(always)]
+#[must_use]
+pub fn make_getter(id: &str) -> Identifier {
+ let mut buf = Identifier::new_const();
+ buf.push_str(FN_GET);
+ buf.push_str(id);
+ buf
+}
+
+/// Make setter function
+#[cfg(not(feature = "no_object"))]
+#[inline(always)]
+#[must_use]
+pub fn make_setter(id: &str) -> Identifier {
+ let mut buf = Identifier::new_const();
+ buf.push_str(FN_SET);
+ buf.push_str(id);
+ buf
+}
+
+impl Engine {
+ /// An empty raw [`Engine`].
+ pub const RAW: Self = Self {
+ global_modules: StaticVec::new_const(),
+
+ #[cfg(not(feature = "no_module"))]
+ global_sub_modules: None,
+
+ #[cfg(not(feature = "no_module"))]
+ module_resolver: None,
+
+ interned_strings: None,
+ disabled_symbols: None,
+ #[cfg(not(feature = "no_custom_syntax"))]
+ custom_keywords: None,
+ #[cfg(not(feature = "no_custom_syntax"))]
+ custom_syntax: None,
+
+ def_var_filter: None,
+ resolve_var: None,
+ token_mapper: None,
+
+ print: None,
+ debug: None,
+
+ #[cfg(not(feature = "unchecked"))]
+ progress: None,
+
+ options: LangOptions::new(),
+
+ def_tag: Dynamic::UNIT,
+
+ #[cfg(not(feature = "no_optimize"))]
+ optimization_level: OptimizationLevel::Simple,
+ #[cfg(feature = "no_optimize")]
+ optimization_level: (),
+
+ #[cfg(not(feature = "unchecked"))]
+ limits: crate::api::limits::Limits::new(),
+
+ #[cfg(feature = "debugging")]
+ debugger_interface: None,
+ };
+
+ /// Create a new [`Engine`].
+ #[inline]
+ #[must_use]
+ pub fn new() -> Self {
+ // Create the new scripting Engine
+ let mut engine = Self::new_raw();
+
+ #[cfg(not(feature = "no_module"))]
+ #[cfg(not(feature = "no_std"))]
+ #[cfg(not(target_family = "wasm"))]
+ {
+ engine.module_resolver =
+ Some(Box::new(crate::module::resolvers::FileModuleResolver::new()));
+ }
+
+ engine.interned_strings = Some(Locked::new(StringsInterner::new()).into());
+
+ // default print/debug implementations
+ #[cfg(not(feature = "no_std"))]
+ #[cfg(not(target_family = "wasm"))]
+ {
+ engine.print = Some(Box::new(|s| println!("{s}")));
+ engine.debug = Some(Box::new(|s, source, pos| match (source, pos) {
+ (Some(source), crate::Position::NONE) => println!("{source} | {s}"),
+ #[cfg(not(feature = "no_position"))]
+ (Some(source), pos) => println!("{source} @ {pos:?} | {s}"),
+ (None, crate::Position::NONE) => println!("{s}"),
+ #[cfg(not(feature = "no_position"))]
+ (None, pos) => println!("{pos:?} | {s}"),
+ }));
+ }
+
+ engine.register_global_module(StandardPackage::new().as_shared_module());
+
+ engine
+ }
+
+ /// Create a new [`Engine`] with minimal built-in functions.
+ /// It returns a copy of [`Engine::RAW`].
+ ///
+ /// This is useful for creating a custom scripting engine with only the functions you need.
+ ///
+ /// Use [`register_global_module`][Engine::register_global_module] to add packages of functions.
+ #[inline]
+ #[must_use]
+ pub const fn new_raw() -> Self {
+ Self::RAW
+ }
+
+ /// Get an interned [string][ImmutableString].
+ #[cfg(not(feature = "internals"))]
+ #[inline(always)]
+ #[must_use]
+ pub(crate) fn get_interned_string(
+ &self,
+ string: impl AsRef<str> + Into<ImmutableString>,
+ ) -> ImmutableString {
+ if let Some(ref interner) = self.interned_strings {
+ locked_write(interner).get(string)
+ } else {
+ string.into()
+ }
+ }
+
+ /// _(internals)_ Get an interned [string][ImmutableString].
+ /// Exported under the `internals` feature only.
+ ///
+ /// [`Engine`] keeps a cache of [`ImmutableString`] instances and tries to avoid new allocations
+ /// when an existing instance is found.
+ #[cfg(feature = "internals")]
+ #[inline]
+ #[must_use]
+ pub fn get_interned_string(
+ &self,
+ string: impl AsRef<str> + Into<ImmutableString>,
+ ) -> ImmutableString {
+ if let Some(ref interner) = self.interned_strings {
+ locked_write(interner).get(string)
+ } else {
+ string.into()
+ }
+ }
+
+ /// Get an empty [`ImmutableString`] which refers to a shared instance.
+ #[inline(always)]
+ #[must_use]
+ pub fn const_empty_string(&self) -> ImmutableString {
+ self.get_interned_string("")
+ }
+
+ /// Is there a debugger interface registered with this [`Engine`]?
+ #[cfg(feature = "debugging")]
+ #[inline(always)]
+ #[must_use]
+ pub(crate) const fn is_debugger_registered(&self) -> bool {
+ self.debugger_interface.is_some()
+ }
+}
diff --git a/rhai/src/eval/cache.rs b/rhai/src/eval/cache.rs
new file mode 100644
index 0000000..3e04457
--- /dev/null
+++ b/rhai/src/eval/cache.rs
@@ -0,0 +1,82 @@
+//! System caches.
+
+use crate::func::{CallableFunction, StraightHashMap};
+use crate::types::BloomFilterU64;
+use crate::{ImmutableString, StaticVec};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// _(internals)_ An entry in a function resolution cache.
+/// Exported under the `internals` feature only.
+#[derive(Debug, Clone)]
+pub struct FnResolutionCacheEntry {
+ /// Function.
+ pub func: CallableFunction,
+ /// Optional source.
+ pub source: Option<ImmutableString>,
+}
+
+/// _(internals)_ A function resolution cache with a bloom filter.
+/// Exported under the `internals` feature only.
+///
+/// The bloom filter is used to rapidly check whether a function hash has never been encountered.
+/// It enables caching a hash only during the second encounter to avoid "one-hit wonders".
+#[derive(Debug, Clone, Default)]
+pub struct FnResolutionCache {
+ /// Hash map containing cached functions.
+ pub map: StraightHashMap<Option<FnResolutionCacheEntry>>,
+ /// Bloom filter to avoid caching "one-hit wonders".
+ pub filter: BloomFilterU64,
+}
+
+impl FnResolutionCache {
+ /// Clear the [`FnResolutionCache`].
+ #[inline(always)]
+ pub fn clear(&mut self) {
+ self.map.clear();
+ self.filter.clear();
+ }
+}
+
+/// _(internals)_ A type containing system-wide caches.
+/// Exported under the `internals` feature only.
+///
+/// The following caches are contained inside this type:
+/// * A stack of [function resolution caches][FnResolutionCache]
+#[derive(Debug, Clone)]
+pub struct Caches(StaticVec<FnResolutionCache>);
+
+impl Caches {
+ /// Create an empty [`Caches`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn new() -> Self {
+ Self(StaticVec::new_const())
+ }
+ /// Get the number of function resolution cache(s) in the stack.
+ #[inline(always)]
+ #[must_use]
+ pub fn fn_resolution_caches_len(&self) -> usize {
+ self.0.len()
+ }
+ /// Get a mutable reference to the current function resolution cache.
+ #[inline]
+ #[must_use]
+ pub fn fn_resolution_cache_mut(&mut self) -> &mut FnResolutionCache {
+ // Push a new function resolution cache if the stack is empty
+ if self.0.is_empty() {
+ self.push_fn_resolution_cache();
+ }
+ self.0.last_mut().unwrap()
+ }
+ /// Push an empty function resolution cache onto the stack and make it current.
+ #[inline(always)]
+ pub fn push_fn_resolution_cache(&mut self) {
+ self.0.push(FnResolutionCache::default());
+ }
+ /// Rewind the function resolution caches stack to a particular size.
+ #[inline(always)]
+ pub fn rewind_fn_resolution_caches(&mut self, len: usize) {
+ self.0.truncate(len);
+ }
+}
diff --git a/rhai/src/eval/chaining.rs b/rhai/src/eval/chaining.rs
new file mode 100644
index 0000000..103b636
--- /dev/null
+++ b/rhai/src/eval/chaining.rs
@@ -0,0 +1,1022 @@
+//! Types to support chaining operations (i.e. indexing and dotting).
+#![cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+
+use super::{Caches, GlobalRuntimeState, Target};
+use crate::ast::{ASTFlags, BinaryExpr, Expr, OpAssignment};
+use crate::config::hashing::SusLock;
+use crate::engine::{FN_IDX_GET, FN_IDX_SET};
+use crate::types::dynamic::Union;
+use crate::{
+ calc_fn_hash, Dynamic, Engine, FnArgsVec, Position, RhaiResult, RhaiResultOf, Scope, ERR,
+};
+use std::hash::Hash;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// Function call hashes to index getters and setters.
+///
+/// # Safety
+///
+/// Uses the extremely unsafe [`SusLock`]. Change to [`OnceCell`] when it is stabilized.
+static INDEXER_HASHES: SusLock<(u64, u64)> = SusLock::new();
+
+/// Get the pre-calculated index getter/setter hashes.
+#[inline(always)]
+#[must_use]
+fn hash_idx() -> (u64, u64) {
+ *INDEXER_HASHES.get_or_init(|| {
+ (
+ calc_fn_hash(None, FN_IDX_GET, 2),
+ calc_fn_hash(None, FN_IDX_SET, 3),
+ )
+ })
+}
+
+/// Method of chaining.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
+pub enum ChainType {
+ /// Indexing.
+ #[cfg(not(feature = "no_index"))]
+ Indexing,
+ /// Dotting.
+ #[cfg(not(feature = "no_object"))]
+ Dotting,
+}
+
+impl From<&Expr> for ChainType {
+ #[inline(always)]
+ fn from(expr: &Expr) -> Self {
+ match expr {
+ #[cfg(not(feature = "no_index"))]
+ Expr::Index(..) => Self::Indexing,
+ #[cfg(not(feature = "no_object"))]
+ Expr::Dot(..) => Self::Dotting,
+ expr => unreachable!("Expr::Index or Expr::Dot expected but gets {:?}", expr),
+ }
+ }
+}
+
+impl Engine {
+ /// Call a get indexer.
+ #[inline]
+ fn call_indexer_get(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ target: &mut Dynamic,
+ idx: &mut Dynamic,
+ pos: Position,
+ ) -> RhaiResultOf<Dynamic> {
+ defer! { let orig_level = global.level; global.level += 1 }
+
+ let hash = hash_idx().0;
+ let args = &mut [target, idx];
+
+ self.exec_native_fn_call(global, caches, FN_IDX_GET, None, hash, args, true, pos)
+ .map(|(r, ..)| r)
+ }
+
+ /// Call a set indexer.
+ #[inline]
+ fn call_indexer_set(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ target: &mut Dynamic,
+ idx: &mut Dynamic,
+ new_val: &mut Dynamic,
+ is_ref_mut: bool,
+ pos: Position,
+ ) -> RhaiResultOf<(Dynamic, bool)> {
+ defer! { let orig_level = global.level; global.level += 1 }
+
+ let hash = hash_idx().1;
+ let args = &mut [target, idx, new_val];
+
+ self.exec_native_fn_call(
+ global, caches, FN_IDX_SET, None, hash, args, is_ref_mut, pos,
+ )
+ }
+
+ /// Get the value at the indexed position of a base type.
+ fn get_indexed_mut<'t>(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ target: &'t mut Dynamic,
+ idx: &mut Dynamic,
+ idx_pos: Position,
+ op_pos: Position,
+ _add_if_not_found: bool,
+ use_indexers: bool,
+ ) -> RhaiResultOf<Target<'t>> {
+ self.track_operation(global, Position::NONE)?;
+
+ match target {
+ #[cfg(not(feature = "no_index"))]
+ Dynamic(Union::Array(arr, ..)) => {
+ // val_array[idx]
+ let index = idx
+ .as_int()
+ .map_err(|typ| self.make_type_mismatch_err::<crate::INT>(typ, idx_pos))?;
+ let len = arr.len();
+ let arr_idx = super::calc_index(len, index, true, || {
+ ERR::ErrorArrayBounds(len, index, idx_pos).into()
+ })?;
+
+ Ok(arr.get_mut(arr_idx).map(Target::from).unwrap())
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ Dynamic(Union::Blob(arr, ..)) => {
+ // val_blob[idx]
+ let index = idx
+ .as_int()
+ .map_err(|typ| self.make_type_mismatch_err::<crate::INT>(typ, idx_pos))?;
+ let len = arr.len();
+ let arr_idx = super::calc_index(len, index, true, || {
+ ERR::ErrorArrayBounds(len, index, idx_pos).into()
+ })?;
+
+ let value = arr.get(arr_idx).map(|&v| (v as crate::INT).into()).unwrap();
+
+ Ok(Target::BlobByte {
+ source: target,
+ value,
+ index: arr_idx,
+ })
+ }
+
+ #[cfg(not(feature = "no_object"))]
+ Dynamic(Union::Map(map, ..)) => {
+ // val_map[idx]
+ let index = idx.read_lock::<crate::ImmutableString>().ok_or_else(|| {
+ self.make_type_mismatch_err::<crate::ImmutableString>(idx.type_name(), idx_pos)
+ })?;
+
+ if _add_if_not_found && (map.is_empty() || !map.contains_key(index.as_str())) {
+ map.insert(index.clone().into(), Dynamic::UNIT);
+ }
+
+ map.get_mut(index.as_str()).map_or_else(
+ || {
+ if self.fail_on_invalid_map_property() {
+ Err(ERR::ErrorPropertyNotFound(index.to_string(), idx_pos).into())
+ } else {
+ Ok(Target::from(Dynamic::UNIT))
+ }
+ },
+ |value| Ok(Target::from(value)),
+ )
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ Dynamic(Union::Int(value, ..))
+ if idx.is::<crate::ExclusiveRange>() || idx.is::<crate::InclusiveRange>() =>
+ {
+ // val_int[range]
+ let (shift, mask) = if let Some(range) = idx.read_lock::<crate::ExclusiveRange>() {
+ let start = range.start;
+ let end = range.end;
+
+ let start = super::calc_index(crate::INT_BITS, start, false, || {
+ ERR::ErrorBitFieldBounds(crate::INT_BITS, start, idx_pos).into()
+ })?;
+ let end = super::calc_index(crate::INT_BITS, end, false, || {
+ ERR::ErrorBitFieldBounds(crate::INT_BITS, end, idx_pos).into()
+ })?;
+
+ #[allow(clippy::cast_possible_truncation)]
+ if end <= start {
+ (0, 0)
+ } else if end == crate::INT_BITS && start == 0 {
+ // -1 = all bits set
+ (0, -1)
+ } else {
+ (
+ start as u8,
+ // 2^bits - 1
+ (((2 as crate::UNSIGNED_INT).pow((end - start) as u32) - 1)
+ as crate::INT)
+ << start,
+ )
+ }
+ } else if let Some(range) = idx.read_lock::<crate::InclusiveRange>() {
+ let start = *range.start();
+ let end = *range.end();
+
+ let start = super::calc_index(crate::INT_BITS, start, false, || {
+ ERR::ErrorBitFieldBounds(crate::INT_BITS, start, idx_pos).into()
+ })?;
+ let end = super::calc_index(crate::INT_BITS, end, false, || {
+ ERR::ErrorBitFieldBounds(crate::INT_BITS, end, idx_pos).into()
+ })?;
+
+ #[allow(clippy::cast_possible_truncation)]
+ if end < start {
+ (0, 0)
+ } else if end == crate::INT_BITS - 1 && start == 0 {
+ // -1 = all bits set
+ (0, -1)
+ } else {
+ (
+ start as u8,
+ // 2^bits - 1
+ (((2 as crate::UNSIGNED_INT).pow((end - start + 1) as u32) - 1)
+ as crate::INT)
+ << start,
+ )
+ }
+ } else {
+ unreachable!("Range or RangeInclusive expected but gets {:?}", idx);
+ };
+
+ let field_value = (*value & mask) >> shift;
+
+ Ok(Target::BitField {
+ source: target,
+ value: field_value.into(),
+ mask,
+ shift,
+ })
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ Dynamic(Union::Int(value, ..)) => {
+ // val_int[idx]
+ let index = idx
+ .as_int()
+ .map_err(|typ| self.make_type_mismatch_err::<crate::INT>(typ, idx_pos))?;
+
+ let bit = super::calc_index(crate::INT_BITS, index, true, || {
+ ERR::ErrorBitFieldBounds(crate::INT_BITS, index, idx_pos).into()
+ })?;
+
+ let bit_value = (*value & (1 << bit)) != 0;
+ #[allow(clippy::cast_possible_truncation)]
+ let bit = bit as u8;
+
+ Ok(Target::Bit {
+ source: target,
+ value: bit_value.into(),
+ bit,
+ })
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ Dynamic(Union::Str(s, ..)) => {
+ // val_string[idx]
+ let index = idx
+ .as_int()
+ .map_err(|typ| self.make_type_mismatch_err::<crate::INT>(typ, idx_pos))?;
+
+ let (ch, offset) = if index >= 0 {
+ #[allow(clippy::absurd_extreme_comparisons)]
+ if index >= crate::MAX_USIZE_INT {
+ return Err(
+ ERR::ErrorStringBounds(s.chars().count(), index, idx_pos).into()
+ );
+ }
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let offset = index as usize;
+ (
+ s.chars().nth(offset).ok_or_else(|| {
+ ERR::ErrorStringBounds(s.chars().count(), index, idx_pos)
+ })?,
+ offset,
+ )
+ } else {
+ let abs_index = index.unsigned_abs();
+
+ if abs_index as u64 > usize::MAX as u64 {
+ return Err(
+ ERR::ErrorStringBounds(s.chars().count(), index, idx_pos).into()
+ );
+ }
+
+ #[allow(clippy::cast_possible_truncation)]
+ let offset = abs_index as usize;
+ (
+ // Count from end if negative
+ s.chars().rev().nth(offset - 1).ok_or_else(|| {
+ ERR::ErrorStringBounds(s.chars().count(), index, idx_pos)
+ })?,
+ offset,
+ )
+ };
+
+ Ok(Target::StringChar {
+ source: target,
+ value: ch.into(),
+ index: offset,
+ })
+ }
+
+ #[cfg(not(feature = "no_closure"))]
+ Dynamic(Union::Shared(..)) => {
+ unreachable!("`get_indexed_mut` cannot handle shared values")
+ }
+
+ _ if use_indexers => self
+ .call_indexer_get(global, caches, target, idx, op_pos)
+ .map(Into::into),
+
+ _ => Err(ERR::ErrorIndexingType(
+ format!(
+ "{} [{}]",
+ self.map_type_name(target.type_name()),
+ self.map_type_name(idx.type_name())
+ ),
+ op_pos,
+ )
+ .into()),
+ }
+ }
+
+ /// Evaluate a dot/index chain.
+ pub(crate) fn eval_dot_index_chain(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ mut this_ptr: Option<&mut Dynamic>,
+ expr: &Expr,
+ new_val: Option<(Dynamic, &OpAssignment)>,
+ ) -> RhaiResult {
+ let BinaryExpr { lhs, rhs } = match expr {
+ #[cfg(not(feature = "no_index"))]
+ Expr::Index(x, ..) => &**x,
+ #[cfg(not(feature = "no_object"))]
+ Expr::Dot(x, ..) => &**x,
+ expr => unreachable!("Expr::Index or Expr::Dot expected but gets {:?}", expr),
+ };
+
+ let idx_values = &mut FnArgsVec::new_const();
+
+ match (rhs, ChainType::from(expr)) {
+ // Short-circuit for simple property access: {expr}.prop
+ #[cfg(not(feature = "no_object"))]
+ (Expr::Property(..), ChainType::Dotting) => (),
+ #[cfg(not(feature = "no_object"))]
+ (Expr::Property(..), ..) => {
+ unreachable!("unexpected Expr::Property for indexing")
+ }
+ // Short-circuit for indexing with literal: {expr}[1]
+ #[cfg(not(feature = "no_index"))]
+ (_, ChainType::Indexing) if rhs.is_constant() => {
+ idx_values.push(rhs.get_literal_value().unwrap())
+ }
+ // Short-circuit for simple method call: {expr}.func()
+ #[cfg(not(feature = "no_object"))]
+ (Expr::MethodCall(x, ..), ChainType::Dotting) if x.args.is_empty() => (),
+ // All other patterns - evaluate the arguments chain
+ _ => self.eval_dot_index_chain_arguments(
+ global,
+ caches,
+ scope,
+ this_ptr.as_deref_mut(),
+ expr,
+ rhs,
+ idx_values,
+ )?,
+ }
+
+ #[cfg(feature = "debugging")]
+ let scope2 = &mut Scope::new();
+ #[cfg(not(feature = "debugging"))]
+ let scope2 = ();
+
+ match (lhs, new_val) {
+ // this.??? or this[???]
+ (Expr::ThisPtr(var_pos), new_val) => {
+ self.track_operation(global, *var_pos)?;
+
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr.as_deref_mut(), lhs)?;
+
+ if let Some(this_ptr) = this_ptr {
+ let target = &mut this_ptr.into();
+
+ self.eval_dot_index_chain_raw(
+ global, caches, scope2, None, lhs, expr, target, rhs, idx_values, new_val,
+ )
+ } else {
+ Err(ERR::ErrorUnboundThis(*var_pos).into())
+ }
+ }
+ // id.??? or id[???]
+ (Expr::Variable(.., var_pos), new_val) => {
+ self.track_operation(global, *var_pos)?;
+
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr.as_deref_mut(), lhs)?;
+
+ let target = &mut self.search_namespace(global, caches, scope, this_ptr, lhs)?;
+
+ self.eval_dot_index_chain_raw(
+ global, caches, scope2, None, lhs, expr, target, rhs, idx_values, new_val,
+ )
+ }
+ // {expr}.??? = ??? or {expr}[???] = ???
+ (_, Some(..)) => unreachable!("cannot assign to an expression"),
+ // {expr}.??? or {expr}[???]
+ (lhs_expr, None) => {
+ let value = self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), lhs_expr)?
+ .flatten();
+ let obj_ptr = &mut value.into();
+
+ self.eval_dot_index_chain_raw(
+ global, caches, scope2, this_ptr, lhs_expr, expr, obj_ptr, rhs, idx_values,
+ None,
+ )
+ }
+ }
+ .map(|(v, ..)| v)
+ }
+
+ /// Evaluate a chain of indexes and store the results in a [`FnArgsVec`].
+ fn eval_dot_index_chain_arguments(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ mut this_ptr: Option<&mut Dynamic>,
+ parent: &Expr,
+ expr: &Expr,
+ idx_values: &mut FnArgsVec<Dynamic>,
+ ) -> RhaiResultOf<()> {
+ self.track_operation(global, expr.position())?;
+
+ match (expr, ChainType::from(parent)) {
+ #[cfg(not(feature = "no_object"))]
+ (Expr::MethodCall(x, ..), ChainType::Dotting) => {
+ debug_assert!(
+ !x.is_qualified(),
+ "method call in dot chain should not be namespace-qualified"
+ );
+
+ for expr in &x.args {
+ let arg_value =
+ self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?;
+ idx_values.push(arg_value.0.flatten());
+ }
+ }
+
+ #[cfg(not(feature = "no_object"))]
+ (Expr::Property(..), ChainType::Dotting) => (),
+
+ (Expr::Index(x, ..) | Expr::Dot(x, ..), chain_type)
+ if !parent.options().contains(ASTFlags::BREAK) =>
+ {
+ let BinaryExpr { lhs, rhs, .. } = &**x;
+
+ let mut _arg_values = FnArgsVec::new_const();
+
+ // Evaluate in left-to-right order
+ match (lhs, chain_type) {
+ #[cfg(not(feature = "no_object"))]
+ (Expr::Property(..), ChainType::Dotting) => (),
+
+ #[cfg(not(feature = "no_object"))]
+ (Expr::MethodCall(x, ..), ChainType::Dotting) => {
+ debug_assert!(
+ !x.is_qualified(),
+ "method call in dot chain should not be namespace-qualified"
+ );
+
+ for expr in &x.args {
+ let tp = this_ptr.as_deref_mut();
+ let arg_value = self.get_arg_value(global, caches, scope, tp, expr)?;
+ _arg_values.push(arg_value.0.flatten());
+ }
+ }
+ #[cfg(not(feature = "no_index"))]
+ (_, ChainType::Indexing) => {
+ _arg_values.push(
+ self.eval_expr(global, caches, scope, this_ptr.as_deref_mut(), lhs)?
+ .flatten(),
+ );
+ }
+ #[allow(unreachable_patterns)]
+ (expr, chain_type) => {
+ unreachable!("unknown {:?} expression: {:?}", chain_type, expr)
+ }
+ }
+
+ // Push in reverse order
+ self.eval_dot_index_chain_arguments(
+ global, caches, scope, this_ptr, expr, rhs, idx_values,
+ )?;
+
+ idx_values.extend(_arg_values);
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ (_, ChainType::Indexing) => idx_values.push(
+ self.eval_expr(global, caches, scope, this_ptr, expr)?
+ .flatten(),
+ ),
+ #[allow(unreachable_patterns)]
+ (expr, chain_type) => unreachable!("unknown {:?} expression: {:?}", chain_type, expr),
+ }
+
+ Ok(())
+ }
+
+ /// Chain-evaluate a dot/index chain.
+ fn eval_dot_index_chain_raw(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ #[cfg(feature = "debugging")] scope: &mut Scope,
+ #[cfg(not(feature = "debugging"))] scope: (),
+ this_ptr: Option<&mut Dynamic>,
+ root: &Expr,
+ parent: &Expr,
+ target: &mut Target,
+ rhs: &Expr,
+ idx_values: &mut FnArgsVec<Dynamic>,
+ new_val: Option<(Dynamic, &OpAssignment)>,
+ ) -> RhaiResultOf<(Dynamic, bool)> {
+ let is_ref_mut = target.is_ref();
+ let op_pos = parent.position();
+
+ #[cfg(feature = "debugging")]
+ #[allow(unused_mut)]
+ let mut this_ptr = this_ptr;
+
+ match ChainType::from(parent) {
+ #[cfg(not(feature = "no_index"))]
+ ChainType::Indexing => {
+ // Check for existence with the null conditional operator
+ if parent.options().contains(ASTFlags::NEGATED) && target.is_unit() {
+ return Ok((Dynamic::UNIT, false));
+ }
+
+ let pos = rhs.start_position();
+
+ match (rhs, new_val) {
+ // xxx[idx].expr... | xxx[idx][expr]...
+ (Expr::Dot(x, ..) | Expr::Index(x, ..), new_val)
+ if !parent.options().contains(ASTFlags::BREAK) =>
+ {
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr.as_deref_mut(), parent)?;
+
+ let idx_val = &mut idx_values.pop().unwrap();
+ let mut idx_val_for_setter = idx_val.clone();
+ let idx_pos = x.lhs.start_position();
+
+ let (try_setter, result) = {
+ let mut obj = self.get_indexed_mut(
+ global, caches, target, idx_val, idx_pos, op_pos, false, true,
+ )?;
+ let is_obj_temp_val = obj.is_temp_value();
+ let obj_ptr = &mut obj;
+
+ match self.eval_dot_index_chain_raw(
+ global, caches, scope, this_ptr, root, rhs, obj_ptr, &x.rhs,
+ idx_values, new_val,
+ ) {
+ Ok((result, true)) if is_obj_temp_val => {
+ (Some(obj.take_or_clone()), (result, true))
+ }
+ Ok(result) => (None, result),
+ Err(err) => return Err(err),
+ }
+ };
+
+ if let Some(mut new_val) = try_setter {
+ // Try to call index setter if value is changed
+ let idx = &mut idx_val_for_setter;
+ let new_val = &mut new_val;
+ // The return value of a indexer setter (usually `()`) is thrown away and not used.
+ let _ = self
+ .call_indexer_set(
+ global, caches, target, idx, new_val, is_ref_mut, op_pos,
+ )
+ .or_else(|e| match *e {
+ ERR::ErrorIndexingType(..) => Ok((Dynamic::UNIT, false)),
+ _ => Err(e),
+ })?;
+ }
+
+ Ok(result)
+ }
+ // xxx[rhs] op= new_val
+ (_, Some((new_val, op_info))) => {
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr, parent)?;
+
+ let idx_val = &mut idx_values.pop().unwrap();
+ let idx = &mut idx_val.clone();
+
+ let try_setter = match self
+ .get_indexed_mut(global, caches, target, idx, pos, op_pos, true, false)
+ {
+ // Indexed value is not a temp value - update directly
+ Ok(ref mut obj_ptr) => {
+ self.eval_op_assignment(
+ global, caches, op_info, root, obj_ptr, new_val,
+ )?;
+ self.check_data_size(obj_ptr.as_ref(), op_info.position())?;
+ None
+ }
+ // Indexed value cannot be referenced - use indexer
+ #[cfg(not(feature = "no_index"))]
+ Err(err) if matches!(*err, ERR::ErrorIndexingType(..)) => Some(new_val),
+ // Any other error
+ Err(err) => return Err(err),
+ };
+
+ if let Some(mut new_val) = try_setter {
+ // Is this an op-assignment?
+ if op_info.is_op_assignment() {
+ let idx = &mut idx_val.clone();
+
+ // Call the index getter to get the current value
+ if let Ok(val) =
+ self.call_indexer_get(global, caches, target, idx, op_pos)
+ {
+ let mut val = val.into();
+ // Run the op-assignment
+ self.eval_op_assignment(
+ global, caches, op_info, root, &mut val, new_val,
+ )?;
+ // Replace new value
+ new_val = val.take_or_clone();
+ self.check_data_size(&new_val, op_info.position())?;
+ }
+ }
+
+ // Try to call index setter
+ let new_val = &mut new_val;
+ // The return value of a indexer setter (usually `()`) is thrown away and not used.
+ let _ = self.call_indexer_set(
+ global, caches, target, idx_val, new_val, is_ref_mut, op_pos,
+ )?;
+ }
+
+ Ok((Dynamic::UNIT, true))
+ }
+ // xxx[rhs]
+ (_, None) => {
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr, parent)?;
+
+ let idx_val = &mut idx_values.pop().unwrap();
+
+ self.get_indexed_mut(
+ global, caches, target, idx_val, pos, op_pos, false, true,
+ )
+ .map(|v| (v.take_or_clone(), false))
+ }
+ }
+ }
+
+ #[cfg(not(feature = "no_object"))]
+ ChainType::Dotting => {
+ // Check for existence with the Elvis operator
+ if parent.options().contains(ASTFlags::NEGATED) && target.is_unit() {
+ return Ok((Dynamic::UNIT, false));
+ }
+
+ match (rhs, new_val, target.is_map()) {
+ // xxx.fn_name(...) = ???
+ (Expr::MethodCall(..), Some(..), ..) => {
+ unreachable!("method call cannot be assigned to")
+ }
+ // xxx.fn_name(arg_expr_list)
+ (Expr::MethodCall(x, pos), None, ..) => {
+ debug_assert!(
+ !x.is_qualified(),
+ "method call in dot chain should not be namespace-qualified"
+ );
+
+ #[cfg(feature = "debugging")]
+ let reset =
+ self.run_debugger_with_reset(global, caches, scope, this_ptr, rhs)?;
+ #[cfg(feature = "debugging")]
+ defer! { global if Some(reset) => move |g| g.debugger_mut().reset_status(reset) }
+
+ let crate::ast::FnCallExpr {
+ name, hashes, args, ..
+ } = &**x;
+
+ // Truncate the index values upon exit
+ defer! { idx_values => truncate; let offset = idx_values.len() - args.len(); }
+
+ let call_args = &mut idx_values[offset..];
+ let arg1_pos = args.get(0).map_or(Position::NONE, Expr::position);
+
+ self.make_method_call(
+ global, caches, name, *hashes, target, call_args, arg1_pos, *pos,
+ )
+ }
+ // {xxx:map}.id op= ???
+ (Expr::Property(x, pos), Some((new_val, op_info)), true) => {
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr, rhs)?;
+
+ let index = &mut x.2.clone().into();
+ {
+ let val_target = &mut self.get_indexed_mut(
+ global, caches, target, index, *pos, op_pos, true, false,
+ )?;
+ self.eval_op_assignment(
+ global, caches, op_info, root, val_target, new_val,
+ )?;
+ }
+ self.check_data_size(target.source(), op_info.position())?;
+ Ok((Dynamic::UNIT, true))
+ }
+ // {xxx:map}.id
+ (Expr::Property(x, pos), None, true) => {
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr, rhs)?;
+
+ let index = &mut x.2.clone().into();
+ let val = self.get_indexed_mut(
+ global, caches, target, index, *pos, op_pos, false, false,
+ )?;
+ Ok((val.take_or_clone(), false))
+ }
+ // xxx.id op= ???
+ (Expr::Property(x, pos), Some((mut new_val, op_info)), false) => {
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr, rhs)?;
+
+ let ((getter, hash_get), (setter, hash_set), name) = &**x;
+
+ if op_info.is_op_assignment() {
+ let args = &mut [target.as_mut()];
+
+ let (mut orig_val, ..) = self
+ .exec_native_fn_call(
+ global, caches, getter, None, *hash_get, args, is_ref_mut, *pos,
+ )
+ .or_else(|err| match *err {
+ // Try an indexer if property does not exist
+ ERR::ErrorDotExpr(..) => {
+ let mut prop = name.into();
+ self.call_indexer_get(
+ global, caches, target, &mut prop, op_pos,
+ )
+ .map(|r| (r, false))
+ .map_err(|e| {
+ match *e {
+ ERR::ErrorIndexingType(..) => err,
+ _ => e,
+ }
+ })
+ }
+ _ => Err(err),
+ })?;
+
+ {
+ let orig_val = &mut (&mut orig_val).into();
+
+ self.eval_op_assignment(
+ global, caches, op_info, root, orig_val, new_val,
+ )?;
+ }
+
+ new_val = orig_val;
+ }
+
+ let args = &mut [target.as_mut(), &mut new_val];
+
+ self.exec_native_fn_call(
+ global, caches, setter, None, *hash_set, args, is_ref_mut, *pos,
+ )
+ .or_else(|err| match *err {
+ // Try an indexer if property does not exist
+ ERR::ErrorDotExpr(..) => {
+ let idx = &mut name.into();
+ let new_val = &mut new_val;
+ self.call_indexer_set(
+ global, caches, target, idx, new_val, is_ref_mut, op_pos,
+ )
+ .map_err(|e| match *e {
+ ERR::ErrorIndexingType(..) => err,
+ _ => e,
+ })
+ }
+ _ => Err(err),
+ })
+ }
+ // xxx.id
+ (Expr::Property(x, pos), None, false) => {
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr, rhs)?;
+
+ let ((getter, hash_get), _, name) = &**x;
+ let args = &mut [target.as_mut()];
+
+ self.exec_native_fn_call(
+ global, caches, getter, None, *hash_get, args, is_ref_mut, *pos,
+ )
+ .map_or_else(
+ |err| match *err {
+ // Try an indexer if property does not exist
+ ERR::ErrorDotExpr(..) => {
+ let mut prop = name.into();
+ self.call_indexer_get(global, caches, target, &mut prop, op_pos)
+ .map(|r| (r, false))
+ .map_err(|e| match *e {
+ ERR::ErrorIndexingType(..) => err,
+ _ => e,
+ })
+ }
+ _ => Err(err),
+ },
+ // Assume getters are always pure
+ |(v, ..)| Ok((v, false)),
+ )
+ }
+ // {xxx:map}.sub_lhs[expr] | {xxx:map}.sub_lhs.expr
+ (Expr::Index(x, ..) | Expr::Dot(x, ..), new_val, true) => {
+ let _node = &x.lhs;
+ let mut _this_ptr = this_ptr;
+ let _tp = _this_ptr.as_deref_mut();
+
+ let val_target = &mut match x.lhs {
+ Expr::Property(ref p, pos) => {
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, _tp, _node)?;
+
+ let index = &mut p.2.clone().into();
+ self.get_indexed_mut(
+ global, caches, target, index, pos, op_pos, false, true,
+ )?
+ }
+ // {xxx:map}.fn_name(arg_expr_list)[expr] | {xxx:map}.fn_name(arg_expr_list).expr
+ Expr::MethodCall(ref x, pos) => {
+ debug_assert!(
+ !x.is_qualified(),
+ "method call in dot chain should not be namespace-qualified"
+ );
+
+ #[cfg(feature = "debugging")]
+ let reset = self
+ .run_debugger_with_reset(global, caches, scope, _tp, _node)?;
+ #[cfg(feature = "debugging")]
+ defer! { global if Some(reset) => move |g| g.debugger_mut().reset_status(reset) }
+
+ let crate::ast::FnCallExpr {
+ name, hashes, args, ..
+ } = &**x;
+
+ // Truncate the index values upon exit
+ defer! { idx_values => truncate; let offset = idx_values.len() - args.len(); }
+
+ let call_args = &mut idx_values[offset..];
+ let arg1_pos = args.get(0).map_or(Position::NONE, Expr::position);
+
+ self.make_method_call(
+ global, caches, name, *hashes, target, call_args, arg1_pos, pos,
+ )?
+ .0
+ .into()
+ }
+ // Others - syntax error
+ ref expr => unreachable!("invalid dot expression: {:?}", expr),
+ };
+
+ self.eval_dot_index_chain_raw(
+ global, caches, scope, _this_ptr, root, rhs, val_target, &x.rhs,
+ idx_values, new_val,
+ )
+ }
+ // xxx.sub_lhs[expr] | xxx.sub_lhs.expr
+ (Expr::Index(x, ..) | Expr::Dot(x, ..), new_val, ..) => {
+ let _node = &x.lhs;
+ let mut _this_ptr = this_ptr;
+ let _tp = _this_ptr.as_deref_mut();
+
+ match x.lhs {
+ // xxx.prop[expr] | xxx.prop.expr
+ Expr::Property(ref p, pos) => {
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, _tp, _node)?;
+
+ let ((getter, hash_get), (setter, hash_set), name) = &**p;
+ let args = &mut [target.as_mut()];
+
+ // Assume getters are always pure
+ let (mut val, ..) = self
+ .exec_native_fn_call(
+ global, caches, getter, None, *hash_get, args, is_ref_mut,
+ pos,
+ )
+ .or_else(|err| match *err {
+ // Try an indexer if property does not exist
+ ERR::ErrorDotExpr(..) => {
+ let mut prop = name.into();
+ self.call_indexer_get(
+ global, caches, target, &mut prop, op_pos,
+ )
+ .map(|r| (r, false))
+ .map_err(
+ |e| match *e {
+ ERR::ErrorIndexingType(..) => err,
+ _ => e,
+ },
+ )
+ }
+ _ => Err(err),
+ })?;
+
+ let val = &mut (&mut val).into();
+
+ let (result, may_be_changed) = self.eval_dot_index_chain_raw(
+ global, caches, scope, _this_ptr, root, rhs, val, &x.rhs,
+ idx_values, new_val,
+ )?;
+
+ // Feed the value back via a setter just in case it has been updated
+ if may_be_changed {
+ // Re-use args because the first &mut parameter will not be consumed
+ let args = &mut [target.as_mut(), val.as_mut()];
+
+ // The return value is thrown away and not used.
+ let _ = self
+ .exec_native_fn_call(
+ global, caches, setter, None, *hash_set, args,
+ is_ref_mut, pos,
+ )
+ .or_else(|err| match *err {
+ // Try an indexer if property does not exist
+ ERR::ErrorDotExpr(..) => {
+ let idx = &mut name.into();
+ let new_val = val;
+ self.call_indexer_set(
+ global, caches, target, idx, new_val,
+ is_ref_mut, op_pos,
+ )
+ .or_else(|e| match *e {
+ // If there is no setter, no need to feed it
+ // back because the property is read-only
+ ERR::ErrorIndexingType(..) => {
+ Ok((Dynamic::UNIT, false))
+ }
+ _ => Err(e),
+ })
+ }
+ _ => Err(err),
+ })?;
+ }
+
+ Ok((result, may_be_changed))
+ }
+ // xxx.fn_name(arg_expr_list)[expr] | xxx.fn_name(arg_expr_list).expr
+ Expr::MethodCall(ref f, pos) => {
+ debug_assert!(
+ !f.is_qualified(),
+ "method call in dot chain should not be namespace-qualified"
+ );
+
+ let val = {
+ #[cfg(feature = "debugging")]
+ let reset = self.run_debugger_with_reset(
+ global, caches, scope, _tp, _node,
+ )?;
+ #[cfg(feature = "debugging")]
+ defer! { global if Some(reset) => move |g| g.debugger_mut().reset_status(reset) }
+
+ let crate::ast::FnCallExpr {
+ name, hashes, args, ..
+ } = &**f;
+
+ // Truncate the index values upon exit
+ defer! { idx_values => truncate; let offset = idx_values.len() - args.len(); }
+
+ let call_args = &mut idx_values[offset..];
+ let pos1 = args.get(0).map_or(Position::NONE, Expr::position);
+
+ self.make_method_call(
+ global, caches, name, *hashes, target, call_args, pos1, pos,
+ )?
+ .0
+ };
+
+ let val = &mut val.into();
+
+ self.eval_dot_index_chain_raw(
+ global, caches, scope, _this_ptr, root, rhs, val, &x.rhs,
+ idx_values, new_val,
+ )
+ }
+ // Others - syntax error
+ ref expr => unreachable!("invalid dot expression: {:?}", expr),
+ }
+ }
+ // Syntax error
+ (expr, ..) => unreachable!("invalid chaining expression: {:?}", expr),
+ }
+ }
+ }
+ }
+}
diff --git a/rhai/src/eval/data_check.rs b/rhai/src/eval/data_check.rs
new file mode 100644
index 0000000..1bcbefe
--- /dev/null
+++ b/rhai/src/eval/data_check.rs
@@ -0,0 +1,215 @@
+//! Data size checks during evaluation.
+#![cfg(not(feature = "unchecked"))]
+
+use super::GlobalRuntimeState;
+use crate::types::dynamic::Union;
+use crate::{Dynamic, Engine, Position, RhaiResultOf, ERR};
+use std::borrow::Borrow;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// Recursively calculate the sizes of an array.
+///
+/// Sizes returned are `(` [`Array`][crate::Array], [`Map`][crate::Map] and [`String`] `)`.
+///
+/// # Panics
+///
+/// Panics if any interior data is shared (should never happen).
+#[cfg(not(feature = "no_index"))]
+#[inline]
+pub fn calc_array_sizes(array: &crate::Array) -> (usize, usize, usize) {
+ let (mut ax, mut mx, mut sx) = (0, 0, 0);
+
+ for value in array {
+ ax += 1;
+
+ match value.0 {
+ Union::Array(ref a, ..) => {
+ let (a, m, s) = calc_array_sizes(a);
+ ax += a;
+ mx += m;
+ sx += s;
+ }
+ Union::Blob(ref a, ..) => ax += 1 + a.len(),
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(ref m, ..) => {
+ let (a, m, s) = calc_map_sizes(m);
+ ax += a;
+ mx += m;
+ sx += s;
+ }
+ Union::Str(ref s, ..) => sx += s.len(),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(..) => {
+ unreachable!("shared values discovered within data")
+ }
+ _ => (),
+ }
+ }
+
+ (ax, mx, sx)
+}
+/// Recursively calculate the sizes of a map.
+///
+/// Sizes returned are `(` [`Array`][crate::Array], [`Map`][crate::Map] and [`String`] `)`.
+///
+/// # Panics
+///
+/// Panics if any interior data is shared (should never happen).
+#[cfg(not(feature = "no_object"))]
+#[inline]
+pub fn calc_map_sizes(map: &crate::Map) -> (usize, usize, usize) {
+ let (mut ax, mut mx, mut sx) = (0, 0, 0);
+
+ for value in map.values() {
+ mx += 1;
+
+ match value.0 {
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(ref a, ..) => {
+ let (a, m, s) = calc_array_sizes(a);
+ ax += a;
+ mx += m;
+ sx += s;
+ }
+ #[cfg(not(feature = "no_index"))]
+ Union::Blob(ref a, ..) => ax += 1 + a.len(),
+ Union::Map(ref m, ..) => {
+ let (a, m, s) = calc_map_sizes(m);
+ ax += a;
+ mx += m;
+ sx += s;
+ }
+ Union::Str(ref s, ..) => sx += s.len(),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(..) => {
+ unreachable!("shared values discovered within data")
+ }
+ _ => (),
+ }
+ }
+
+ (ax, mx, sx)
+}
+
+impl Dynamic {
+ /// Recursively calculate the sizes of a value.
+ ///
+ /// Sizes returned are `(` [`Array`][crate::Array], [`Map`][crate::Map] and [`String`] `)`.
+ ///
+ /// # Panics
+ ///
+ /// Panics if any interior data is shared (should never happen).
+ #[inline]
+ pub(crate) fn calc_data_sizes(&self, _top: bool) -> (usize, usize, usize) {
+ match self.0 {
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(ref arr, ..) => calc_array_sizes(arr),
+ #[cfg(not(feature = "no_index"))]
+ Union::Blob(ref blob, ..) => (blob.len(), 0, 0),
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(ref map, ..) => calc_map_sizes(map),
+ Union::Str(ref s, ..) => (0, 0, s.len()),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(..) if _top => self.read_lock::<Self>().unwrap().calc_data_sizes(true),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(..) => {
+ unreachable!("shared values discovered within data: {}", self)
+ }
+ _ => (0, 0, 0),
+ }
+ }
+}
+
+impl Engine {
+ /// Raise an error if any data size exceeds limit.
+ ///
+ /// [`Position`] in [`EvalAltResult`][crate::EvalAltResult] is always [`NONE`][Position::NONE]
+ /// and should be set afterwards.
+ pub(crate) fn throw_on_size(&self, (_arr, _map, s): (usize, usize, usize)) -> RhaiResultOf<()> {
+ if self
+ .limits
+ .max_string_len
+ .map_or(false, |max| s > max.get())
+ {
+ return Err(
+ ERR::ErrorDataTooLarge("Length of string".to_string(), Position::NONE).into(),
+ );
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ if self
+ .limits
+ .max_array_size
+ .map_or(false, |max| _arr > max.get())
+ {
+ return Err(
+ ERR::ErrorDataTooLarge("Size of array/BLOB".to_string(), Position::NONE).into(),
+ );
+ }
+
+ #[cfg(not(feature = "no_object"))]
+ if self
+ .limits
+ .max_map_size
+ .map_or(false, |max| _map > max.get())
+ {
+ return Err(
+ ERR::ErrorDataTooLarge("Size of object map".to_string(), Position::NONE).into(),
+ );
+ }
+
+ Ok(())
+ }
+
+ /// Check whether the size of a [`Dynamic`] is within limits.
+ #[inline]
+ pub(crate) fn check_data_size<T: Borrow<Dynamic>>(
+ &self,
+ value: T,
+ pos: Position,
+ ) -> RhaiResultOf<T> {
+ // If no data size limits, just return
+ if !self.has_data_size_limit() {
+ return Ok(value);
+ }
+
+ let sizes = value.borrow().calc_data_sizes(true);
+
+ self.throw_on_size(sizes)
+ .map_err(|err| err.fill_position(pos))?;
+
+ Ok(value)
+ }
+
+ /// Raise an error if the size of a [`Dynamic`] is out of limits (if any).
+ ///
+ /// Not available under `unchecked`.
+ #[inline(always)]
+ pub fn ensure_data_size_within_limits(&self, value: &Dynamic) -> RhaiResultOf<()> {
+ self.check_data_size(value, Position::NONE).map(|_| ())
+ }
+
+ /// Check if the number of operations stay within limit.
+ #[inline(always)]
+ pub(crate) fn track_operation(
+ &self,
+ global: &mut GlobalRuntimeState,
+ pos: Position,
+ ) -> RhaiResultOf<()> {
+ global.num_operations += 1;
+
+ // Guard against too many operations
+ if self.max_operations() > 0 && global.num_operations > self.max_operations() {
+ Err(ERR::ErrorTooManyOperations(pos).into())
+ } else {
+ self.progress
+ .as_ref()
+ .and_then(|progress| {
+ progress(global.num_operations)
+ .map(|token| Err(ERR::ErrorTerminated(token, pos).into()))
+ })
+ .unwrap_or(Ok(()))
+ }
+ }
+}
diff --git a/rhai/src/eval/debugger.rs b/rhai/src/eval/debugger.rs
new file mode 100644
index 0000000..de0bdd6
--- /dev/null
+++ b/rhai/src/eval/debugger.rs
@@ -0,0 +1,558 @@
+//! Module defining the debugging interface.
+#![cfg(feature = "debugging")]
+
+use super::{Caches, EvalContext, GlobalRuntimeState};
+use crate::ast::{ASTNode, Expr, Stmt};
+use crate::{Dynamic, Engine, EvalAltResult, ImmutableString, Position, RhaiResultOf, Scope};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{fmt, iter::repeat, mem};
+
+/// Callback function to initialize the debugger.
+#[cfg(not(feature = "sync"))]
+pub type OnDebuggingInit = dyn Fn(&Engine, Debugger) -> Debugger;
+/// Callback function to initialize the debugger.
+#[cfg(feature = "sync")]
+pub type OnDebuggingInit = dyn Fn(&Engine, Debugger) -> Debugger + Send + Sync;
+
+/// Callback function for debugging.
+#[cfg(not(feature = "sync"))]
+pub type OnDebuggerCallback = dyn Fn(
+ EvalContext,
+ DebuggerEvent,
+ ASTNode,
+ Option<&str>,
+ Position,
+) -> RhaiResultOf<DebuggerCommand>;
+/// Callback function for debugging.
+#[cfg(feature = "sync")]
+pub type OnDebuggerCallback = dyn Fn(EvalContext, DebuggerEvent, ASTNode, Option<&str>, Position) -> RhaiResultOf<DebuggerCommand>
+ + Send
+ + Sync;
+
+/// A command for the debugger on the next iteration.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
+#[non_exhaustive]
+pub enum DebuggerCommand {
+ /// Continue normal execution.
+ Continue,
+ /// Step into the next expression, diving into functions.
+ StepInto,
+ /// Run to the next expression or statement, stepping over functions.
+ StepOver,
+ /// Run to the next statement, skipping over functions.
+ Next,
+ /// Run to the end of the current function call.
+ FunctionExit,
+}
+
+impl Default for DebuggerCommand {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::Continue
+ }
+}
+
+/// The debugger status.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
+#[non_exhaustive]
+pub enum DebuggerStatus {
+ // Script evaluation starts.
+ Init,
+ // Stop at the next statement or expression.
+ Next(bool, bool),
+ // Run to the end of the current level of function call.
+ FunctionExit(usize),
+ // Script evaluation ends.
+ Terminate,
+}
+
+impl DebuggerStatus {
+ pub const CONTINUE: Self = Self::Next(false, false);
+ pub const STEP: Self = Self::Next(true, true);
+ pub const NEXT: Self = Self::Next(true, false);
+ pub const INTO: Self = Self::Next(false, true);
+}
+
+/// A event that triggers the debugger.
+#[derive(Debug, Clone, Copy)]
+#[non_exhaustive]
+pub enum DebuggerEvent<'a> {
+ /// Script evaluation starts.
+ Start,
+ /// Break on next step.
+ Step,
+ /// Break on break-point.
+ BreakPoint(usize),
+ /// Return from a function with a value.
+ FunctionExitWithValue(&'a Dynamic),
+ /// Return from a function with a value.
+ FunctionExitWithError(&'a EvalAltResult),
+ /// Script evaluation ends.
+ End,
+}
+
+/// A break-point for debugging.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+#[non_exhaustive]
+pub enum BreakPoint {
+ /// Break at a particular position under a particular source.
+ ///
+ /// Not available under `no_position`.
+ #[cfg(not(feature = "no_position"))]
+ AtPosition {
+ /// Source (empty if not available) of the break-point.
+ source: Option<ImmutableString>,
+ /// [Position] of the break-point.
+ pos: Position,
+ /// Is the break-point enabled?
+ enabled: bool,
+ },
+ /// Break at a particular function call.
+ AtFunctionName {
+ /// Function name.
+ name: ImmutableString,
+ /// Is the break-point enabled?
+ enabled: bool,
+ },
+ /// Break at a particular function call with a particular number of arguments.
+ AtFunctionCall {
+ /// Function name.
+ name: ImmutableString,
+ /// Number of arguments.
+ args: usize,
+ /// Is the break-point enabled?
+ enabled: bool,
+ },
+ /// Break at a particular property .
+ ///
+ /// Not available under `no_object`.
+ #[cfg(not(feature = "no_object"))]
+ AtProperty {
+ /// Property name.
+ name: ImmutableString,
+ /// Is the break-point enabled?
+ enabled: bool,
+ },
+}
+
+impl fmt::Display for BreakPoint {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ #[cfg(not(feature = "no_position"))]
+ Self::AtPosition {
+ source,
+ pos,
+ enabled,
+ } => {
+ if let Some(ref source) = source {
+ write!(f, "{source} ")?;
+ }
+ write!(f, "@ {pos:?}")?;
+ if !*enabled {
+ f.write_str(" (disabled)")?;
+ }
+ Ok(())
+ }
+ Self::AtFunctionName { name, enabled } => {
+ write!(f, "{name} (...)")?;
+ if !*enabled {
+ f.write_str(" (disabled)")?;
+ }
+ Ok(())
+ }
+ Self::AtFunctionCall {
+ name,
+ args,
+ enabled,
+ } => {
+ write!(
+ f,
+ "{name} ({})",
+ repeat("_").take(*args).collect::<Vec<_>>().join(", ")
+ )?;
+ if !*enabled {
+ f.write_str(" (disabled)")?;
+ }
+ Ok(())
+ }
+ #[cfg(not(feature = "no_object"))]
+ Self::AtProperty { name, enabled } => {
+ write!(f, ".{name}")?;
+ if !*enabled {
+ f.write_str(" (disabled)")?;
+ }
+ Ok(())
+ }
+ }
+ }
+}
+
+impl BreakPoint {
+ /// Is this [`BreakPoint`] enabled?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_enabled(&self) -> bool {
+ match self {
+ #[cfg(not(feature = "no_position"))]
+ Self::AtPosition { enabled, .. } => *enabled,
+ Self::AtFunctionName { enabled, .. } | Self::AtFunctionCall { enabled, .. } => *enabled,
+ #[cfg(not(feature = "no_object"))]
+ Self::AtProperty { enabled, .. } => *enabled,
+ }
+ }
+ /// Enable/disable this [`BreakPoint`].
+ #[inline(always)]
+ pub fn enable(&mut self, value: bool) {
+ match self {
+ #[cfg(not(feature = "no_position"))]
+ Self::AtPosition { enabled, .. } => *enabled = value,
+ Self::AtFunctionName { enabled, .. } | Self::AtFunctionCall { enabled, .. } => {
+ *enabled = value
+ }
+ #[cfg(not(feature = "no_object"))]
+ Self::AtProperty { enabled, .. } => *enabled = value,
+ }
+ }
+}
+
+/// A function call.
+#[derive(Debug, Clone, Hash)]
+pub struct CallStackFrame {
+ /// Function name.
+ pub fn_name: ImmutableString,
+ /// Copies of function call arguments, if any.
+ pub args: crate::StaticVec<Dynamic>,
+ /// Source of the function.
+ pub source: Option<ImmutableString>,
+ /// [Position][`Position`] of the function call.
+ pub pos: Position,
+}
+
+impl fmt::Display for CallStackFrame {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut fp = f.debug_tuple(&self.fn_name);
+
+ for arg in &self.args {
+ fp.field(arg);
+ }
+
+ fp.finish()?;
+
+ if !self.pos.is_none() {
+ if let Some(ref source) = self.source {
+ write!(f, ": {source}")?;
+ }
+ write!(f, " @ {:?}", self.pos)?;
+ }
+
+ Ok(())
+ }
+}
+
+/// A type providing debugging facilities.
+#[derive(Debug, Clone, Hash)]
+pub struct Debugger {
+ /// The current status command.
+ pub(crate) status: DebuggerStatus,
+ /// The current set of break-points.
+ break_points: Vec<BreakPoint>,
+ /// The current function call stack.
+ call_stack: Vec<CallStackFrame>,
+ /// The current state.
+ state: Dynamic,
+}
+
+impl Debugger {
+ /// Create a new [`Debugger`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn new(status: DebuggerStatus) -> Self {
+ Self {
+ status,
+ break_points: Vec::new(),
+ call_stack: Vec::new(),
+ state: Dynamic::UNIT,
+ }
+ }
+ /// Get the current call stack.
+ #[inline(always)]
+ #[must_use]
+ pub fn call_stack(&self) -> &[CallStackFrame] {
+ &self.call_stack
+ }
+ /// Rewind the function call stack to a particular depth.
+ #[inline(always)]
+ pub(crate) fn rewind_call_stack(&mut self, len: usize) {
+ self.call_stack.truncate(len);
+ }
+ /// Add a new frame to the function call stack.
+ #[inline(always)]
+ pub(crate) fn push_call_stack_frame(
+ &mut self,
+ fn_name: ImmutableString,
+ args: crate::StaticVec<Dynamic>,
+ source: Option<ImmutableString>,
+ pos: Position,
+ ) {
+ self.call_stack.push(CallStackFrame {
+ fn_name,
+ args,
+ source,
+ pos,
+ });
+ }
+ /// Change the current status to [`CONTINUE`][DebuggerStatus::CONTINUE] and return the previous status.
+ pub(crate) fn clear_status_if(
+ &mut self,
+ filter: impl FnOnce(&DebuggerStatus) -> bool,
+ ) -> Option<DebuggerStatus> {
+ if filter(&self.status) {
+ Some(mem::replace(&mut self.status, DebuggerStatus::CONTINUE))
+ } else {
+ None
+ }
+ }
+ /// Override the status of this [`Debugger`] if the current status is
+ /// [`CONTINUE`][DebuggerStatus::CONTINUE].
+ #[inline(always)]
+ pub(crate) fn reset_status(&mut self, status: DebuggerStatus) {
+ if self.status == DebuggerStatus::CONTINUE {
+ self.status = status;
+ }
+ }
+ /// Returns the first break-point triggered by a particular [`AST` Node][ASTNode].
+ #[must_use]
+ pub fn is_break_point(&self, src: Option<&str>, node: ASTNode) -> Option<usize> {
+ let _src = src;
+
+ self.break_points()
+ .iter()
+ .enumerate()
+ .filter(|&(.., bp)| bp.is_enabled())
+ .find(|&(.., bp)| match bp {
+ #[cfg(not(feature = "no_position"))]
+ BreakPoint::AtPosition { pos, .. } if pos.is_none() => false,
+ #[cfg(not(feature = "no_position"))]
+ BreakPoint::AtPosition { source, pos, .. } if pos.is_beginning_of_line() => {
+ node.position().line().unwrap_or(0) == pos.line().unwrap()
+ && _src == source.as_ref().map(|s| s.as_str())
+ }
+ #[cfg(not(feature = "no_position"))]
+ BreakPoint::AtPosition { source, pos, .. } => {
+ node.position() == *pos && _src == source.as_ref().map(|s| s.as_str())
+ }
+ BreakPoint::AtFunctionName { name, .. } => match node {
+ ASTNode::Expr(Expr::FnCall(x, ..)) | ASTNode::Stmt(Stmt::FnCall(x, ..)) => {
+ x.name == *name
+ }
+ ASTNode::Stmt(Stmt::Expr(e)) => match &**e {
+ Expr::FnCall(x, ..) => x.name == *name,
+ _ => false,
+ },
+ _ => false,
+ },
+ BreakPoint::AtFunctionCall { name, args, .. } => match node {
+ ASTNode::Expr(Expr::FnCall(x, ..)) | ASTNode::Stmt(Stmt::FnCall(x, ..)) => {
+ x.args.len() == *args && x.name == *name
+ }
+ ASTNode::Stmt(Stmt::Expr(e)) => match &**e {
+ Expr::FnCall(x, ..) => x.args.len() == *args && x.name == *name,
+ _ => false,
+ },
+ _ => false,
+ },
+ #[cfg(not(feature = "no_object"))]
+ BreakPoint::AtProperty { name, .. } => match node {
+ ASTNode::Expr(Expr::Property(x, ..)) => x.2 == *name,
+ _ => false,
+ },
+ })
+ .map(|(i, ..)| i)
+ }
+ /// Get a slice of all [`BreakPoint`]'s.
+ #[inline(always)]
+ #[must_use]
+ pub fn break_points(&self) -> &[BreakPoint] {
+ &self.break_points
+ }
+ /// Get the underlying [`Vec`] holding all [`BreakPoint`]'s.
+ #[inline(always)]
+ #[must_use]
+ pub fn break_points_mut(&mut self) -> &mut Vec<BreakPoint> {
+ &mut self.break_points
+ }
+ /// Get the custom state.
+ #[inline(always)]
+ pub const fn state(&self) -> &Dynamic {
+ &self.state
+ }
+ /// Get a mutable reference to the custom state.
+ #[inline(always)]
+ pub fn state_mut(&mut self) -> &mut Dynamic {
+ &mut self.state
+ }
+ /// Set the custom state.
+ #[inline(always)]
+ pub fn set_state(&mut self, state: impl Into<Dynamic>) {
+ self.state = state.into();
+ }
+}
+
+impl Engine {
+ /// Run the debugger callback if there is a debugging interface registered.
+ #[inline(always)]
+ pub(crate) fn run_debugger<'a>(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ this_ptr: Option<&mut Dynamic>,
+ node: impl Into<ASTNode<'a>>,
+ ) -> RhaiResultOf<()> {
+ if self.is_debugger_registered() {
+ if let Some(cmd) =
+ self.run_debugger_with_reset_raw(global, caches, scope, this_ptr, node)?
+ {
+ global.debugger_mut().status = cmd;
+ }
+ }
+
+ Ok(())
+ }
+ /// Run the debugger callback if there is a debugging interface registered.
+ ///
+ /// Returns [`Some`] if the debugger needs to be reactivated at the end of the block, statement or
+ /// function call.
+ ///
+ /// It is up to the [`Engine`] to reactivate the debugger.
+ #[inline(always)]
+ pub(crate) fn run_debugger_with_reset<'a>(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ this_ptr: Option<&mut Dynamic>,
+ node: impl Into<ASTNode<'a>>,
+ ) -> RhaiResultOf<Option<DebuggerStatus>> {
+ if self.is_debugger_registered() {
+ self.run_debugger_with_reset_raw(global, caches, scope, this_ptr, node)
+ } else {
+ Ok(None)
+ }
+ }
+ /// Run the debugger callback.
+ ///
+ /// Returns [`Some`] if the debugger needs to be reactivated at the end of the block, statement or
+ /// function call.
+ ///
+ /// It is up to the [`Engine`] to reactivate the debugger.
+ #[inline]
+ pub(crate) fn run_debugger_with_reset_raw<'a>(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ this_ptr: Option<&mut Dynamic>,
+ node: impl Into<ASTNode<'a>>,
+ ) -> RhaiResultOf<Option<DebuggerStatus>> {
+ let node = node.into();
+
+ // Skip transitive nodes
+ match node {
+ ASTNode::Expr(Expr::Stmt(..)) | ASTNode::Stmt(Stmt::Expr(..)) => return Ok(None),
+ _ => (),
+ }
+
+ if let Some(ref dbg) = global.debugger {
+ let event = match dbg.status {
+ DebuggerStatus::Init => Some(DebuggerEvent::Start),
+ DebuggerStatus::NEXT if node.is_stmt() => Some(DebuggerEvent::Step),
+ DebuggerStatus::INTO if node.is_expr() => Some(DebuggerEvent::Step),
+ DebuggerStatus::STEP => Some(DebuggerEvent::Step),
+ DebuggerStatus::Terminate => Some(DebuggerEvent::End),
+ _ => None,
+ };
+
+ let event = match event {
+ Some(e) => e,
+ None => match dbg.is_break_point(global.source(), node) {
+ Some(bp) => DebuggerEvent::BreakPoint(bp),
+ None => return Ok(None),
+ },
+ };
+
+ self.run_debugger_raw(global, caches, scope, this_ptr, node, event)
+ } else {
+ Ok(None)
+ }
+ }
+ /// Run the debugger callback unconditionally.
+ ///
+ /// Returns [`Some`] if the debugger needs to be reactivated at the end of the block, statement or
+ /// function call.
+ ///
+ /// It is up to the [`Engine`] to reactivate the debugger.
+ #[inline]
+ pub(crate) fn run_debugger_raw(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ this_ptr: Option<&mut Dynamic>,
+ node: ASTNode,
+ event: DebuggerEvent,
+ ) -> Result<Option<DebuggerStatus>, Box<crate::EvalAltResult>> {
+ if let Some(ref x) = self.debugger_interface {
+ let orig_scope_len = scope.len();
+
+ let src = global.source_raw().cloned();
+ let src = src.as_ref().map(|s| s.as_str());
+ let context = EvalContext::new(self, global, caches, scope, this_ptr);
+ let (.., ref on_debugger) = *x;
+
+ let command = on_debugger(context, event, node, src, node.position());
+
+ if orig_scope_len != scope.len() {
+ // The scope is changed, always search from now on
+ global.always_search_scope = true;
+ }
+
+ match command? {
+ DebuggerCommand::Continue => {
+ global.debugger_mut().status = DebuggerStatus::CONTINUE;
+ Ok(None)
+ }
+ DebuggerCommand::Next => {
+ global.debugger_mut().status = DebuggerStatus::CONTINUE;
+ Ok(Some(DebuggerStatus::NEXT))
+ }
+ DebuggerCommand::StepOver => {
+ global.debugger_mut().status = DebuggerStatus::CONTINUE;
+ Ok(Some(DebuggerStatus::STEP))
+ }
+ DebuggerCommand::StepInto => {
+ global.debugger_mut().status = DebuggerStatus::STEP;
+ Ok(None)
+ }
+ DebuggerCommand::FunctionExit => {
+ // Bump a level if it is a function call
+ let level = match node {
+ ASTNode::Expr(Expr::FnCall(..)) | ASTNode::Stmt(Stmt::FnCall(..)) => {
+ global.level + 1
+ }
+ ASTNode::Stmt(Stmt::Expr(e)) if matches!(**e, Expr::FnCall(..)) => {
+ global.level + 1
+ }
+ _ => global.level,
+ };
+ global.debugger_mut().status = DebuggerStatus::FunctionExit(level);
+ Ok(None)
+ }
+ }
+ } else {
+ Ok(None)
+ }
+ }
+}
diff --git a/rhai/src/eval/eval_context.rs b/rhai/src/eval/eval_context.rs
new file mode 100644
index 0000000..9f4350b
--- /dev/null
+++ b/rhai/src/eval/eval_context.rs
@@ -0,0 +1,186 @@
+//! Evaluation context.
+
+use super::{Caches, GlobalRuntimeState};
+use crate::{Dynamic, Engine, Scope};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// Context of a script evaluation process.
+#[allow(dead_code)]
+pub struct EvalContext<'a, 's, 'ps, 'g, 'c, 't> {
+ /// The current [`Engine`].
+ engine: &'a Engine,
+ /// The current [`GlobalRuntimeState`].
+ global: &'g mut GlobalRuntimeState,
+ /// The current [caches][Caches], if available.
+ caches: &'c mut Caches,
+ /// The current [`Scope`].
+ scope: &'s mut Scope<'ps>,
+ /// The current bound `this` pointer, if any.
+ this_ptr: Option<&'t mut Dynamic>,
+}
+
+impl<'a, 's, 'ps, 'g, 'c, 't> EvalContext<'a, 's, 'ps, 'g, 'c, 't> {
+ /// Create a new [`EvalContext`].
+ #[inline(always)]
+ #[must_use]
+ pub fn new(
+ engine: &'a Engine,
+ global: &'g mut GlobalRuntimeState,
+ caches: &'c mut Caches,
+ scope: &'s mut Scope<'ps>,
+ this_ptr: Option<&'t mut Dynamic>,
+ ) -> Self {
+ Self {
+ engine,
+ global,
+ caches,
+ scope,
+ this_ptr,
+ }
+ }
+ /// The current [`Engine`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn engine(&self) -> &'a Engine {
+ self.engine
+ }
+ /// The current source.
+ #[inline(always)]
+ #[must_use]
+ pub fn source(&self) -> Option<&str> {
+ self.global.source()
+ }
+ /// The current [`Scope`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn scope(&self) -> &Scope<'ps> {
+ self.scope
+ }
+ /// Get a mutable reference to the current [`Scope`].
+ #[inline(always)]
+ #[must_use]
+ pub fn scope_mut(&mut self) -> &mut Scope<'ps> {
+ self.scope
+ }
+ /// Get an iterator over the current set of modules imported via `import` statements,
+ /// in reverse order (i.e. modules imported last come first).
+ #[cfg(not(feature = "no_module"))]
+ #[inline(always)]
+ pub fn iter_imports(&self) -> impl Iterator<Item = (&str, &crate::Module)> {
+ self.global.iter_imports()
+ }
+ /// Custom state kept in a [`Dynamic`].
+ #[inline(always)]
+ pub const fn tag(&self) -> &Dynamic {
+ &self.global.tag
+ }
+ /// Mutable reference to the custom state kept in a [`Dynamic`].
+ #[inline(always)]
+ pub fn tag_mut(&mut self) -> &mut Dynamic {
+ &mut self.global.tag
+ }
+ /// _(internals)_ The current [`GlobalRuntimeState`].
+ /// Exported under the `internals` feature only.
+ #[cfg(feature = "internals")]
+ #[inline(always)]
+ #[must_use]
+ pub const fn global_runtime_state(&self) -> &GlobalRuntimeState {
+ self.global
+ }
+ /// _(internals)_ Get a mutable reference to the current [`GlobalRuntimeState`].
+ /// Exported under the `internals` feature only.
+ #[cfg(feature = "internals")]
+ #[inline(always)]
+ #[must_use]
+ pub fn global_runtime_state_mut(&mut self) -> &mut GlobalRuntimeState {
+ self.global
+ }
+ /// Get an iterator over the namespaces containing definition of all script-defined functions.
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ #[inline]
+ pub fn iter_namespaces(&self) -> impl Iterator<Item = &crate::Module> {
+ self.global.lib.iter().map(AsRef::as_ref)
+ }
+ /// _(internals)_ The current set of namespaces containing definitions of all script-defined functions.
+ /// Exported under the `internals` feature only.
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(feature = "internals")]
+ #[inline(always)]
+ #[must_use]
+ pub fn namespaces(&self) -> &[crate::SharedModule] {
+ &self.global.lib
+ }
+ /// The current bound `this` pointer, if any.
+ #[inline(always)]
+ #[must_use]
+ pub fn this_ptr(&self) -> Option<&Dynamic> {
+ self.this_ptr.as_deref()
+ }
+ /// Mutable reference to the current bound `this` pointer, if any.
+ #[inline(always)]
+ #[must_use]
+ pub fn this_ptr_mut(&mut self) -> Option<&mut Dynamic> {
+ self.this_ptr.as_deref_mut()
+ }
+ /// The current nesting level of function calls.
+ #[inline(always)]
+ #[must_use]
+ pub const fn call_level(&self) -> usize {
+ self.global.level
+ }
+
+ /// Evaluate an [expression tree][crate::Expression] within this [evaluation context][`EvalContext`].
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is very low level. It evaluates an expression from an [`AST`][crate::AST].
+ #[cfg(not(feature = "no_custom_syntax"))]
+ #[inline(always)]
+ pub fn eval_expression_tree(&mut self, expr: &crate::Expression) -> crate::RhaiResult {
+ #[allow(deprecated)]
+ self.eval_expression_tree_raw(expr, true)
+ }
+ /// Evaluate an [expression tree][crate::Expression] within this [evaluation context][`EvalContext`].
+ ///
+ /// The following option is available:
+ ///
+ /// * whether to rewind the [`Scope`] after evaluation if the expression is a [`StmtBlock`][crate::ast::StmtBlock]
+ ///
+ /// # WARNING - Unstable API
+ ///
+ /// This API is volatile and may change in the future.
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is _extremely_ low level. It evaluates an expression from an [`AST`][crate::AST].
+ #[cfg(not(feature = "no_custom_syntax"))]
+ #[deprecated = "This API is NOT deprecated, but it is considered volatile and may change in the future."]
+ #[inline]
+ pub fn eval_expression_tree_raw(
+ &mut self,
+ expr: &crate::Expression,
+ rewind_scope: bool,
+ ) -> crate::RhaiResult {
+ let expr: &crate::ast::Expr = expr;
+ let this_ptr = self.this_ptr.as_deref_mut();
+
+ match expr {
+ crate::ast::Expr::Stmt(stmts) => self.engine.eval_stmt_block(
+ self.global,
+ self.caches,
+ self.scope,
+ this_ptr,
+ stmts,
+ rewind_scope,
+ ),
+ _ => self
+ .engine
+ .eval_expr(self.global, self.caches, self.scope, this_ptr, expr),
+ }
+ }
+}
diff --git a/rhai/src/eval/expr.rs b/rhai/src/eval/expr.rs
new file mode 100644
index 0000000..066fdf0
--- /dev/null
+++ b/rhai/src/eval/expr.rs
@@ -0,0 +1,417 @@
+//! Module defining functions for evaluating an expression.
+
+use super::{Caches, EvalContext, GlobalRuntimeState, Target};
+use crate::ast::Expr;
+use crate::packages::string_basic::{print_with_func, FUNC_TO_STRING};
+use crate::types::dynamic::AccessMode;
+use crate::{Dynamic, Engine, RhaiResult, RhaiResultOf, Scope, SmartString, ERR};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{fmt::Write, num::NonZeroUsize};
+
+impl Engine {
+ /// Search for a module within an imports stack.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ #[must_use]
+ pub(crate) fn search_imports(
+ &self,
+ global: &GlobalRuntimeState,
+ namespace: &crate::ast::Namespace,
+ ) -> Option<crate::SharedModule> {
+ debug_assert!(!namespace.is_empty());
+
+ let root = namespace.root();
+
+ // Qualified - check if the root module is directly indexed
+ if !global.always_search_scope {
+ if let Some(index) = namespace.index() {
+ let offset = global.num_imports() - index.get();
+
+ if let m @ Some(_) = global.get_shared_import(offset) {
+ return m;
+ }
+ }
+ }
+
+ // Do a text-match search if the index doesn't work
+ global.find_import(root).map_or_else(
+ || {
+ self.global_sub_modules
+ .as_ref()
+ .and_then(|m| m.get(root))
+ .cloned()
+ },
+ |offset| global.get_shared_import(offset),
+ )
+ }
+
+ /// Search for a variable within the scope or within imports,
+ /// depending on whether the variable name is namespace-qualified.
+ pub(crate) fn search_namespace<'s>(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &'s mut Scope,
+ this_ptr: Option<&'s mut Dynamic>,
+ expr: &Expr,
+ ) -> RhaiResultOf<Target<'s>> {
+ match expr {
+ Expr::Variable(_, Some(_), _) => {
+ self.search_scope_only(global, caches, scope, this_ptr, expr)
+ }
+ Expr::Variable(v, None, ..) => match &**v {
+ // Normal variable access
+ (_, ns, ..) if ns.is_empty() => {
+ self.search_scope_only(global, caches, scope, this_ptr, expr)
+ }
+
+ // Qualified variable access
+ #[cfg(not(feature = "no_module"))]
+ (_, ns, hash_var, var_name) => {
+ // foo:bar::baz::VARIABLE
+ if let Some(module) = self.search_imports(global, ns) {
+ return module.get_qualified_var(*hash_var).map_or_else(
+ || {
+ let sep = crate::engine::NAMESPACE_SEPARATOR;
+
+ Err(ERR::ErrorVariableNotFound(
+ format!("{ns}{sep}{var_name}"),
+ ns.position(),
+ )
+ .into())
+ },
+ |mut target| {
+ // Module variables are constant
+ target.set_access_mode(AccessMode::ReadOnly);
+ Ok(target.into())
+ },
+ );
+ }
+
+ // global::VARIABLE
+ #[cfg(not(feature = "no_function"))]
+ if ns.len() == 1 && ns.root() == crate::engine::KEYWORD_GLOBAL {
+ if let Some(ref constants) = global.constants {
+ if let Some(value) =
+ crate::func::locked_write(constants).get_mut(var_name.as_str())
+ {
+ let mut target: Target = value.clone().into();
+ // Module variables are constant
+ target.set_access_mode(AccessMode::ReadOnly);
+ return Ok(target);
+ }
+ }
+
+ let sep = crate::engine::NAMESPACE_SEPARATOR;
+
+ return Err(ERR::ErrorVariableNotFound(
+ format!("{ns}{sep}{var_name}"),
+ ns.position(),
+ )
+ .into());
+ }
+
+ Err(ERR::ErrorModuleNotFound(ns.to_string(), ns.position()).into())
+ }
+
+ #[cfg(feature = "no_module")]
+ _ => unreachable!("Invalid expression {:?}", expr),
+ },
+ _ => unreachable!("Expr::Variable expected but gets {:?}", expr),
+ }
+ }
+
+ /// Search for a variable within the scope
+ ///
+ /// # Panics
+ ///
+ /// Panics if `expr` is not [`Expr::Variable`].
+ pub(crate) fn search_scope_only<'s>(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &'s mut Scope,
+ this_ptr: Option<&'s mut Dynamic>,
+ expr: &Expr,
+ ) -> RhaiResultOf<Target<'s>> {
+ // Make sure that the pointer indirection is taken only when absolutely necessary.
+
+ let index = match expr {
+ // Check if the variable is `this`
+ Expr::ThisPtr(..) => unreachable!("Expr::ThisPtr should have been handled outside"),
+
+ _ if global.always_search_scope => 0,
+
+ Expr::Variable(_, Some(i), ..) => i.get() as usize,
+ Expr::Variable(v, None, ..) => {
+ // Scripted function with the same name
+ #[cfg(not(feature = "no_function"))]
+ if let Some(fn_def) = global.lib.iter().flat_map(|m| m.iter_script_fn()).find_map(
+ |(_, _, f, _, func)| if f == v.3.as_str() { Some(func) } else { None },
+ ) {
+ let mut fn_ptr =
+ crate::FnPtr::new_unchecked(v.3.clone(), crate::StaticVec::new_const());
+ fn_ptr.set_fn_def(Some(fn_def.clone()));
+ let val: Dynamic = fn_ptr.into();
+ return Ok(val.into());
+ }
+
+ v.0.map_or(0, NonZeroUsize::get)
+ }
+
+ _ => unreachable!("Expr::Variable expected but gets {:?}", expr),
+ };
+
+ // Check the variable resolver, if any
+ if let Some(ref resolve_var) = self.resolve_var {
+ let orig_scope_len = scope.len();
+
+ let context = EvalContext::new(self, global, caches, scope, this_ptr);
+ let var_name = expr.get_variable_name(true).expect("`Expr::Variable`");
+ let resolved_var = resolve_var(var_name, index, context);
+
+ if orig_scope_len != scope.len() {
+ // The scope is changed, always search from now on
+ global.always_search_scope = true;
+ }
+
+ match resolved_var {
+ Ok(Some(mut result)) => {
+ result.set_access_mode(AccessMode::ReadOnly);
+ return Ok(result.into());
+ }
+ Ok(None) => (),
+ Err(err) => return Err(err.fill_position(expr.position())),
+ }
+ }
+
+ let index = if index > 0 {
+ scope.len() - index
+ } else {
+ // Find the variable in the scope
+ let var_name = expr.get_variable_name(true).expect("`Expr::Variable`");
+
+ match scope.search(var_name) {
+ Some(index) => index,
+ None => {
+ return self
+ .global_modules
+ .iter()
+ .find_map(|m| m.get_var(var_name))
+ .map_or_else(
+ || {
+ Err(ERR::ErrorVariableNotFound(
+ var_name.to_string(),
+ expr.position(),
+ )
+ .into())
+ },
+ |val| Ok(val.into()),
+ )
+ }
+ }
+ };
+
+ let val = scope.get_mut_by_index(index);
+
+ Ok(val.into())
+ }
+
+ /// Evaluate an expression.
+ pub(crate) fn eval_expr(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ mut this_ptr: Option<&mut Dynamic>,
+ expr: &Expr,
+ ) -> RhaiResult {
+ self.track_operation(global, expr.position())?;
+
+ #[cfg(feature = "debugging")]
+ let reset =
+ self.run_debugger_with_reset(global, caches, scope, this_ptr.as_deref_mut(), expr)?;
+ #[cfg(feature = "debugging")]
+ defer! { global if Some(reset) => move |g| g.debugger_mut().reset_status(reset) }
+
+ match expr {
+ // Constants
+ Expr::IntegerConstant(x, ..) => Ok((*x).into()),
+ Expr::StringConstant(x, ..) => Ok(x.clone().into()),
+ Expr::BoolConstant(x, ..) => Ok((*x).into()),
+ #[cfg(not(feature = "no_float"))]
+ Expr::FloatConstant(x, ..) => Ok((*x).into()),
+ Expr::CharConstant(x, ..) => Ok((*x).into()),
+ Expr::Unit(..) => Ok(Dynamic::UNIT),
+ Expr::DynamicConstant(x, ..) => Ok(x.as_ref().clone()),
+
+ Expr::FnCall(x, pos) => {
+ self.eval_fn_call_expr(global, caches, scope, this_ptr, x, *pos)
+ }
+
+ Expr::ThisPtr(var_pos) => this_ptr
+ .ok_or_else(|| ERR::ErrorUnboundThis(*var_pos).into())
+ .cloned(),
+
+ Expr::Variable(..) => self
+ .search_namespace(global, caches, scope, this_ptr, expr)
+ .map(Target::take_or_clone),
+
+ Expr::InterpolatedString(x, _) => {
+ let mut concat = SmartString::new_const();
+
+ for expr in &**x {
+ let item = &mut self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), expr)?
+ .flatten();
+ let pos = expr.position();
+
+ if item.is_string() {
+ write!(concat, "{item}").unwrap();
+ } else {
+ let source = global.source();
+ let context = &(self, FUNC_TO_STRING, source, &*global, pos).into();
+ let display = print_with_func(FUNC_TO_STRING, context, item);
+ write!(concat, "{display}").unwrap();
+ }
+
+ #[cfg(not(feature = "unchecked"))]
+ self.throw_on_size((0, 0, concat.len()))
+ .map_err(|err| err.fill_position(pos))?;
+ }
+
+ Ok(self.get_interned_string(concat).into())
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ Expr::Array(x, ..) => {
+ let mut array = crate::Array::with_capacity(x.len());
+
+ #[cfg(not(feature = "unchecked"))]
+ let mut total_data_sizes = (0, 0, 0);
+
+ for item_expr in &**x {
+ let value = self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), item_expr)?
+ .flatten();
+
+ #[cfg(not(feature = "unchecked"))]
+ if self.has_data_size_limit() {
+ let val_sizes = value.calc_data_sizes(true);
+
+ total_data_sizes = (
+ total_data_sizes.0 + val_sizes.0 + 1,
+ total_data_sizes.1 + val_sizes.1,
+ total_data_sizes.2 + val_sizes.2,
+ );
+ self.throw_on_size(total_data_sizes)
+ .map_err(|err| err.fill_position(item_expr.position()))?;
+ }
+
+ array.push(value);
+ }
+
+ Ok(Dynamic::from_array(array))
+ }
+
+ #[cfg(not(feature = "no_object"))]
+ Expr::Map(x, ..) => {
+ let mut map = x.1.clone();
+
+ #[cfg(not(feature = "unchecked"))]
+ let mut total_data_sizes = (0, 0, 0);
+
+ for (key, value_expr) in &x.0 {
+ let value = self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), value_expr)?
+ .flatten();
+
+ #[cfg(not(feature = "unchecked"))]
+ if self.has_data_size_limit() {
+ let delta = value.calc_data_sizes(true);
+ total_data_sizes = (
+ total_data_sizes.0 + delta.0,
+ total_data_sizes.1 + delta.1 + 1,
+ total_data_sizes.2 + delta.2,
+ );
+ self.throw_on_size(total_data_sizes)
+ .map_err(|err| err.fill_position(value_expr.position()))?;
+ }
+
+ *map.get_mut(key.as_str()).unwrap() = value;
+ }
+
+ Ok(Dynamic::from_map(map))
+ }
+
+ Expr::And(x, ..) => Ok((self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), &x.lhs)?
+ .as_bool()
+ .map_err(|typ| self.make_type_mismatch_err::<bool>(typ, x.lhs.position()))?
+ && self
+ .eval_expr(global, caches, scope, this_ptr, &x.rhs)?
+ .as_bool()
+ .map_err(|typ| self.make_type_mismatch_err::<bool>(typ, x.rhs.position()))?)
+ .into()),
+
+ Expr::Or(x, ..) => Ok((self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), &x.lhs)?
+ .as_bool()
+ .map_err(|typ| self.make_type_mismatch_err::<bool>(typ, x.lhs.position()))?
+ || self
+ .eval_expr(global, caches, scope, this_ptr, &x.rhs)?
+ .as_bool()
+ .map_err(|typ| self.make_type_mismatch_err::<bool>(typ, x.rhs.position()))?)
+ .into()),
+
+ Expr::Coalesce(x, ..) => {
+ let value =
+ self.eval_expr(global, caches, scope, this_ptr.as_deref_mut(), &x.lhs)?;
+
+ if value.is_unit() {
+ self.eval_expr(global, caches, scope, this_ptr, &x.rhs)
+ } else {
+ Ok(value)
+ }
+ }
+
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Expr::Custom(custom, pos) => {
+ let expressions: crate::StaticVec<_> =
+ custom.inputs.iter().map(Into::into).collect();
+ // The first token acts as the custom syntax's key
+ let key_token = custom.tokens.first().unwrap();
+ // The key should exist, unless the AST is compiled in a different Engine
+ let custom_def = self
+ .custom_syntax
+ .as_ref()
+ .and_then(|m| m.get(key_token.as_str()))
+ .ok_or_else(|| {
+ Box::new(ERR::ErrorCustomSyntax(
+ format!("Invalid custom syntax prefix: {key_token}"),
+ custom.tokens.iter().map(<_>::to_string).collect(),
+ *pos,
+ ))
+ })?;
+ let mut context = EvalContext::new(self, global, caches, scope, this_ptr);
+
+ (custom_def.func)(&mut context, &expressions, &custom.state)
+ .and_then(|r| self.check_data_size(r, expr.start_position()))
+ }
+
+ Expr::Stmt(x) => self.eval_stmt_block(global, caches, scope, this_ptr, x, true),
+
+ #[cfg(not(feature = "no_index"))]
+ Expr::Index(..) => {
+ self.eval_dot_index_chain(global, caches, scope, this_ptr, expr, None)
+ }
+
+ #[cfg(not(feature = "no_object"))]
+ Expr::Dot(..) => self.eval_dot_index_chain(global, caches, scope, this_ptr, expr, None),
+
+ #[allow(unreachable_patterns)]
+ _ => unreachable!("expression cannot be evaluated: {:?}", expr),
+ }
+ }
+}
diff --git a/rhai/src/eval/global_state.rs b/rhai/src/eval/global_state.rs
new file mode 100644
index 0000000..6f253f6
--- /dev/null
+++ b/rhai/src/eval/global_state.rs
@@ -0,0 +1,381 @@
+//! Global runtime state.
+
+use crate::{Dynamic, Engine, ImmutableString};
+use std::fmt;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// Collection of globally-defined constants.
+#[cfg(not(feature = "no_module"))]
+#[cfg(not(feature = "no_function"))]
+pub type SharedGlobalConstants =
+ crate::Shared<crate::Locked<std::collections::BTreeMap<ImmutableString, Dynamic>>>;
+
+/// _(internals)_ Global runtime states.
+/// Exported under the `internals` feature only.
+//
+// # Implementation Notes
+//
+// This implementation for imported [modules][crate::Module] splits the module names from the shared
+// modules to improve data locality.
+//
+// Most usage will be looking up a particular key from the list and then getting the module that
+// corresponds to that key.
+#[derive(Clone)]
+pub struct GlobalRuntimeState {
+ /// Names of imported [modules][crate::Module].
+ #[cfg(not(feature = "no_module"))]
+ imports: Option<Box<crate::StaticVec<ImmutableString>>>,
+ /// Stack of imported [modules][crate::Module].
+ #[cfg(not(feature = "no_module"))]
+ modules: Option<Box<crate::StaticVec<crate::SharedModule>>>,
+
+ /// The current stack of loaded [modules][crate::Module] containing script-defined functions.
+ #[cfg(not(feature = "no_function"))]
+ pub lib: crate::StaticVec<crate::SharedModule>,
+ /// Source of the current context.
+ ///
+ /// No source if the string is empty.
+ pub source: Option<ImmutableString>,
+ /// Number of operations performed.
+ pub num_operations: u64,
+ /// Number of modules loaded.
+ #[cfg(not(feature = "no_module"))]
+ pub num_modules_loaded: usize,
+ /// The current nesting level of function calls.
+ pub level: usize,
+ /// Level of the current scope.
+ ///
+ /// The global (root) level is zero, a new block (or function call) is one level higher, and so on.
+ pub scope_level: usize,
+ /// Force a [`Scope`][crate::Scope] search by name.
+ ///
+ /// Normally, access to variables are parsed with a relative offset into the
+ /// [`Scope`][crate::Scope] to avoid a lookup.
+ ///
+ /// In some situation, e.g. after running an `eval` statement, or after a custom syntax
+ /// statement, subsequent offsets may become mis-aligned.
+ ///
+ /// When that happens, this flag is turned on.
+ pub always_search_scope: bool,
+ /// Embedded [module][crate::Module] resolver.
+ #[cfg(not(feature = "no_module"))]
+ pub embedded_module_resolver:
+ Option<crate::Shared<crate::module::resolvers::StaticModuleResolver>>,
+ /// Cache of globally-defined constants.
+ ///
+ /// Interior mutability is needed because it is shared in order to aid in cloning.
+ #[cfg(not(feature = "no_module"))]
+ #[cfg(not(feature = "no_function"))]
+ pub constants: Option<SharedGlobalConstants>,
+ /// Custom state that can be used by the external host.
+ pub tag: Dynamic,
+ /// Debugging interface.
+ #[cfg(feature = "debugging")]
+ pub(crate) debugger: Option<Box<super::Debugger>>,
+}
+
+impl GlobalRuntimeState {
+ /// Create a new [`GlobalRuntimeState`] based on an [`Engine`].
+ #[inline(always)]
+ #[must_use]
+ pub fn new(engine: &Engine) -> Self {
+ Self {
+ #[cfg(not(feature = "no_module"))]
+ imports: None,
+ #[cfg(not(feature = "no_module"))]
+ modules: None,
+ #[cfg(not(feature = "no_function"))]
+ lib: crate::StaticVec::new_const(),
+ source: None,
+ num_operations: 0,
+ #[cfg(not(feature = "no_module"))]
+ num_modules_loaded: 0,
+ scope_level: 0,
+ level: 0,
+ always_search_scope: false,
+ #[cfg(not(feature = "no_module"))]
+ embedded_module_resolver: None,
+ #[cfg(not(feature = "no_module"))]
+ #[cfg(not(feature = "no_function"))]
+ constants: None,
+
+ tag: engine.default_tag().clone(),
+
+ #[cfg(feature = "debugging")]
+ debugger: engine.debugger_interface.as_ref().map(|x| {
+ let dbg = crate::eval::Debugger::new(crate::eval::DebuggerStatus::Init);
+ (x.0)(engine, dbg).into()
+ }),
+ }
+ }
+ /// Get the length of the stack of globally-imported [modules][crate::Module].
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ #[must_use]
+ pub fn num_imports(&self) -> usize {
+ self.modules.as_deref().map_or(0, crate::StaticVec::len)
+ }
+ /// Get the globally-imported [module][crate::Module] at a particular index.
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ #[must_use]
+ pub fn get_shared_import(&self, index: usize) -> Option<crate::SharedModule> {
+ self.modules.as_ref().and_then(|m| m.get(index).cloned())
+ }
+ /// Get the index of a globally-imported [module][crate::Module] by name.
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ #[must_use]
+ pub fn find_import(&self, name: &str) -> Option<usize> {
+ self.imports.as_ref().and_then(|imports| {
+ imports
+ .iter()
+ .rev()
+ .position(|key| key.as_str() == name)
+ .map(|i| imports.len() - 1 - i)
+ })
+ }
+ /// Push an imported [module][crate::Module] onto the stack.
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ pub fn push_import(
+ &mut self,
+ name: impl Into<ImmutableString>,
+ module: impl Into<crate::SharedModule>,
+ ) {
+ self.imports
+ .get_or_insert_with(|| crate::StaticVec::new_const().into())
+ .push(name.into());
+
+ self.modules
+ .get_or_insert_with(|| crate::StaticVec::new_const().into())
+ .push(module.into());
+ }
+ /// Truncate the stack of globally-imported [modules][crate::Module] to a particular length.
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ pub fn truncate_imports(&mut self, size: usize) {
+ if size == 0 {
+ self.imports = None;
+ self.modules = None;
+ } else if self.imports.is_some() {
+ self.imports.as_deref_mut().unwrap().truncate(size);
+ self.modules.as_deref_mut().unwrap().truncate(size);
+ }
+ }
+ /// Get an iterator to the stack of globally-imported [modules][crate::Module] in reverse order.
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ pub fn iter_imports(&self) -> impl Iterator<Item = (&str, &crate::Module)> {
+ self.imports
+ .as_deref()
+ .into_iter()
+ .flatten()
+ .rev()
+ .zip(self.modules.as_deref().into_iter().flatten().rev())
+ .map(|(name, module)| (name.as_str(), &**module))
+ }
+ /// Get an iterator to the stack of globally-imported [modules][crate::Module] in reverse order.
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ pub(crate) fn iter_imports_raw(
+ &self,
+ ) -> impl Iterator<Item = (&ImmutableString, &crate::SharedModule)> {
+ self.imports
+ .as_deref()
+ .into_iter()
+ .flatten()
+ .rev()
+ .zip(self.modules.as_deref().into_iter().flatten())
+ }
+ /// Get an iterator to the stack of globally-imported [modules][crate::Module] in forward order.
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ pub fn scan_imports_raw(
+ &self,
+ ) -> impl Iterator<Item = (&ImmutableString, &crate::SharedModule)> {
+ self.imports
+ .as_deref()
+ .into_iter()
+ .flatten()
+ .zip(self.modules.as_deref().into_iter().flatten())
+ }
+ /// Can the particular function with [`Dynamic`] parameter(s) exist in the stack of
+ /// globally-imported [modules][crate::Module]?
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ pub(crate) fn may_contain_dynamic_fn(&self, hash_script: u64) -> bool {
+ self.modules.as_deref().map_or(false, |m| {
+ m.iter().any(|m| m.may_contain_dynamic_fn(hash_script))
+ })
+ }
+ /// Does the specified function hash key exist in the stack of globally-imported
+ /// [modules][crate::Module]?
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[allow(dead_code)]
+ #[inline]
+ #[must_use]
+ pub fn contains_qualified_fn(&self, hash: u64) -> bool {
+ self.modules
+ .as_ref()
+ .map_or(false, |m| m.iter().any(|m| m.contains_qualified_fn(hash)))
+ }
+ /// Get the specified function via its hash key from the stack of globally-imported
+ /// [modules][crate::Module].
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ #[must_use]
+ pub fn get_qualified_fn(
+ &self,
+ hash: u64,
+ global_namespace_only: bool,
+ ) -> Option<(&crate::func::CallableFunction, Option<&ImmutableString>)> {
+ if global_namespace_only {
+ self.modules.as_ref().and_then(|m| {
+ m.iter()
+ .rev()
+ .filter(|m| m.contains_indexed_global_functions())
+ .find_map(|m| m.get_qualified_fn(hash).map(|f| (f, m.id_raw())))
+ })
+ } else {
+ self.modules.as_ref().and_then(|m| {
+ m.iter()
+ .rev()
+ .find_map(|m| m.get_qualified_fn(hash).map(|f| (f, m.id_raw())))
+ })
+ }
+ }
+ /// Does the specified [`TypeId`][std::any::TypeId] iterator exist in the stack of
+ /// globally-imported [modules][crate::Module]?
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[allow(dead_code)]
+ #[inline]
+ #[must_use]
+ pub fn contains_iter(&self, id: std::any::TypeId) -> bool {
+ self.modules
+ .as_ref()
+ .map_or(false, |m| m.iter().any(|m| m.contains_qualified_iter(id)))
+ }
+ /// Get the specified [`TypeId`][std::any::TypeId] iterator from the stack of globally-imported
+ /// [modules][crate::Module].
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ #[must_use]
+ pub fn get_iter(&self, id: std::any::TypeId) -> Option<&crate::func::IteratorFn> {
+ self.modules
+ .as_ref()
+ .and_then(|m| m.iter().rev().find_map(|m| m.get_qualified_iter(id)))
+ }
+ /// Get the current source.
+ #[inline(always)]
+ #[must_use]
+ pub fn source(&self) -> Option<&str> {
+ self.source.as_ref().map(|s| s.as_str())
+ }
+ /// Get the current source.
+ #[inline(always)]
+ #[must_use]
+ #[allow(dead_code)]
+ pub(crate) const fn source_raw(&self) -> Option<&ImmutableString> {
+ self.source.as_ref()
+ }
+
+ /// Return a reference to the debugging interface.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the debugging interface is not set.
+ #[cfg(feature = "debugging")]
+ #[must_use]
+ pub fn debugger(&self) -> &super::Debugger {
+ self.debugger.as_ref().unwrap()
+ }
+ /// Return a mutable reference to the debugging interface.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the debugging interface is not set.
+ #[cfg(feature = "debugging")]
+ #[must_use]
+ pub fn debugger_mut(&mut self) -> &mut super::Debugger {
+ self.debugger.as_deref_mut().unwrap()
+ }
+}
+
+#[cfg(not(feature = "no_module"))]
+impl<K: Into<ImmutableString>, M: Into<crate::SharedModule>> Extend<(K, M)> for GlobalRuntimeState {
+ #[inline]
+ fn extend<T: IntoIterator<Item = (K, M)>>(&mut self, iter: T) {
+ let imports = self
+ .imports
+ .get_or_insert_with(|| crate::StaticVec::new_const().into());
+ let modules = self
+ .modules
+ .get_or_insert_with(|| crate::StaticVec::new_const().into());
+
+ for (k, m) in iter {
+ imports.push(k.into());
+ modules.push(m.into());
+ }
+ }
+}
+
+impl fmt::Debug for GlobalRuntimeState {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut f = f.debug_struct("GlobalRuntimeState");
+
+ #[cfg(not(feature = "no_module"))]
+ f.field("imports", &self.scan_imports_raw().collect::<Vec<_>>())
+ .field("num_modules_loaded", &self.num_modules_loaded)
+ .field("embedded_module_resolver", &self.embedded_module_resolver);
+
+ #[cfg(not(feature = "no_function"))]
+ f.field("lib", &self.lib);
+
+ f.field("source", &self.source)
+ .field("num_operations", &self.num_operations)
+ .field("level", &self.level)
+ .field("scope_level", &self.scope_level)
+ .field("always_search_scope", &self.always_search_scope);
+
+ #[cfg(not(feature = "no_module"))]
+ #[cfg(not(feature = "no_function"))]
+ f.field("constants", &self.constants);
+
+ f.field("tag", &self.tag);
+
+ #[cfg(feature = "debugging")]
+ f.field("debugger", &self.debugger);
+
+ f.finish()
+ }
+}
diff --git a/rhai/src/eval/mod.rs b/rhai/src/eval/mod.rs
new file mode 100644
index 0000000..814baaf
--- /dev/null
+++ b/rhai/src/eval/mod.rs
@@ -0,0 +1,62 @@
+mod cache;
+mod chaining;
+mod data_check;
+mod debugger;
+mod eval_context;
+mod expr;
+mod global_state;
+mod stmt;
+mod target;
+
+pub use cache::{Caches, FnResolutionCache, FnResolutionCacheEntry};
+#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+pub use chaining::ChainType;
+#[cfg(not(feature = "unchecked"))]
+#[cfg(not(feature = "no_index"))]
+pub use data_check::calc_array_sizes;
+#[cfg(not(feature = "unchecked"))]
+#[cfg(not(feature = "no_object"))]
+pub use data_check::calc_map_sizes;
+#[cfg(feature = "debugging")]
+pub use debugger::{
+ BreakPoint, CallStackFrame, Debugger, DebuggerCommand, DebuggerEvent, DebuggerStatus,
+ OnDebuggerCallback, OnDebuggingInit,
+};
+pub use eval_context::EvalContext;
+pub use global_state::GlobalRuntimeState;
+#[cfg(not(feature = "no_module"))]
+#[cfg(not(feature = "no_function"))]
+pub use global_state::SharedGlobalConstants;
+#[cfg(not(feature = "no_index"))]
+pub use target::calc_offset_len;
+pub use target::{calc_index, Target};
+
+#[cfg(feature = "unchecked")]
+mod unchecked {
+ use crate::{eval::GlobalRuntimeState, Dynamic, Engine, Position, RhaiResultOf};
+ use std::borrow::Borrow;
+ #[cfg(feature = "no_std")]
+ use std::prelude::v1::*;
+
+ impl Engine {
+ /// Check if the number of operations stay within limit.
+ #[inline(always)]
+ pub(crate) const fn track_operation(
+ &self,
+ _: &GlobalRuntimeState,
+ _: Position,
+ ) -> RhaiResultOf<()> {
+ Ok(())
+ }
+
+ /// Check whether the size of a [`Dynamic`] is within limits.
+ #[inline(always)]
+ pub(crate) const fn check_data_size<T: Borrow<Dynamic>>(
+ &self,
+ value: T,
+ _: Position,
+ ) -> RhaiResultOf<T> {
+ Ok(value)
+ }
+ }
+}
diff --git a/rhai/src/eval/stmt.rs b/rhai/src/eval/stmt.rs
new file mode 100644
index 0000000..e747d8f
--- /dev/null
+++ b/rhai/src/eval/stmt.rs
@@ -0,0 +1,999 @@
+//! Module defining functions for evaluating a statement.
+
+use super::{Caches, EvalContext, GlobalRuntimeState, Target};
+use crate::api::events::VarDefInfo;
+use crate::ast::{
+ ASTFlags, BinaryExpr, ConditionalExpr, Expr, FlowControl, OpAssignment, Stmt,
+ SwitchCasesCollection,
+};
+use crate::func::{get_builtin_op_assignment_fn, get_hasher};
+use crate::tokenizer::Token;
+use crate::types::dynamic::{AccessMode, Union};
+use crate::{Dynamic, Engine, RhaiResult, RhaiResultOf, Scope, ERR, INT};
+use std::hash::{Hash, Hasher};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+impl Dynamic {
+ /// If the value is a string, intern it.
+ #[inline(always)]
+ fn intern_string(self, engine: &Engine) -> Self {
+ match self.0 {
+ Union::Str(..) => engine
+ .get_interned_string(self.into_immutable_string().expect("`ImmutableString`"))
+ .into(),
+ _ => self,
+ }
+ }
+}
+
+impl Engine {
+ /// Evaluate a statements block.
+ pub(crate) fn eval_stmt_block(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ mut this_ptr: Option<&mut Dynamic>,
+ statements: &[Stmt],
+ restore_orig_state: bool,
+ ) -> RhaiResult {
+ if statements.is_empty() {
+ return Ok(Dynamic::UNIT);
+ }
+
+ // Restore scope at end of block if necessary
+ defer! { scope if restore_orig_state => rewind; let orig_scope_len = scope.len(); }
+
+ // Restore global state at end of block if necessary
+ let orig_always_search_scope = global.always_search_scope;
+ #[cfg(not(feature = "no_module"))]
+ let orig_imports_len = global.num_imports();
+
+ if restore_orig_state {
+ global.scope_level += 1;
+ }
+
+ defer! { global if restore_orig_state => move |g| {
+ g.scope_level -= 1;
+
+ #[cfg(not(feature = "no_module"))]
+ g.truncate_imports(orig_imports_len);
+
+ // The impact of new local variables goes away at the end of a block
+ // because any new variables introduced will go out of scope
+ g.always_search_scope = orig_always_search_scope;
+ }}
+
+ // Pop new function resolution caches at end of block
+ defer! {
+ caches => rewind_fn_resolution_caches;
+ let orig_fn_resolution_caches_len = caches.fn_resolution_caches_len();
+ }
+
+ // Run the statements
+ statements.iter().try_fold(Dynamic::UNIT, |_, stmt| {
+ let this_ptr = this_ptr.as_deref_mut();
+
+ #[cfg(not(feature = "no_module"))]
+ let imports_len = global.num_imports();
+
+ let result =
+ self.eval_stmt(global, caches, scope, this_ptr, stmt, restore_orig_state)?;
+
+ #[cfg(not(feature = "no_module"))]
+ if matches!(stmt, Stmt::Import(..)) {
+ // Get the extra modules - see if any functions are marked global.
+ // Without global functions, the extra modules never affect function resolution.
+ if global
+ .scan_imports_raw()
+ .skip(imports_len)
+ .any(|(.., m)| m.contains_indexed_global_functions())
+ {
+ // Different scenarios where the cache must be cleared - notice that this is
+ // expensive as all function resolutions must start again
+ if caches.fn_resolution_caches_len() > orig_fn_resolution_caches_len {
+ // When new module is imported with global functions and there is already
+ // a new cache, just clear it
+ caches.fn_resolution_cache_mut().clear();
+ } else if restore_orig_state {
+ // When new module is imported with global functions, push a new cache
+ caches.push_fn_resolution_cache();
+ } else {
+ // When the block is to be evaluated in-place, just clear the current cache
+ caches.fn_resolution_cache_mut().clear();
+ }
+ }
+ }
+
+ Ok(result)
+ })
+ }
+
+ /// Evaluate an op-assignment statement.
+ pub(crate) fn eval_op_assignment(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ op_info: &OpAssignment,
+ root: &Expr,
+ target: &mut Target,
+ mut new_val: Dynamic,
+ ) -> RhaiResultOf<()> {
+ // Assignment to constant variable?
+ if target.is_read_only() {
+ let name = root.get_variable_name(false).unwrap_or_default();
+ let pos = root.start_position();
+ return Err(ERR::ErrorAssignmentToConstant(name.to_string(), pos).into());
+ }
+
+ let pos = op_info.position();
+
+ if let Some((hash_x, hash, op_x, op_x_str, op, op_str)) = op_info.get_op_assignment_info() {
+ let mut lock_guard = target.write_lock::<Dynamic>().unwrap();
+ let mut done = false;
+
+ // Short-circuit built-in op-assignments if under Fast Operators mode
+ if self.fast_operators() {
+ #[allow(clippy::wildcard_imports)]
+ use Token::*;
+
+ done = true;
+
+ // For extremely simple primary data operations, do it directly
+ // to avoid the overhead of calling a function.
+ match (&mut lock_guard.0, &mut new_val.0) {
+ (Union::Bool(b1, ..), Union::Bool(b2, ..)) => match op_x {
+ AndAssign => *b1 = *b1 && *b2,
+ OrAssign => *b1 = *b1 || *b2,
+ XOrAssign => *b1 = *b1 ^ *b2,
+ _ => done = false,
+ },
+ (Union::Int(n1, ..), Union::Int(n2, ..)) => {
+ #[cfg(not(feature = "unchecked"))]
+ #[allow(clippy::wildcard_imports)]
+ use crate::packages::arithmetic::arith_basic::INT::functions::*;
+
+ #[cfg(not(feature = "unchecked"))]
+ match op_x {
+ PlusAssign => {
+ *n1 = add(*n1, *n2).map_err(|err| err.fill_position(pos))?
+ }
+ MinusAssign => {
+ *n1 = subtract(*n1, *n2).map_err(|err| err.fill_position(pos))?
+ }
+ MultiplyAssign => {
+ *n1 = multiply(*n1, *n2).map_err(|err| err.fill_position(pos))?
+ }
+ DivideAssign => {
+ *n1 = divide(*n1, *n2).map_err(|err| err.fill_position(pos))?
+ }
+ ModuloAssign => {
+ *n1 = modulo(*n1, *n2).map_err(|err| err.fill_position(pos))?
+ }
+ _ => done = false,
+ }
+ #[cfg(feature = "unchecked")]
+ match op_x {
+ PlusAssign => *n1 += *n2,
+ MinusAssign => *n1 -= *n2,
+ MultiplyAssign => *n1 *= *n2,
+ DivideAssign => *n1 /= *n2,
+ ModuloAssign => *n1 %= *n2,
+ _ => done = false,
+ }
+ }
+ #[cfg(not(feature = "no_float"))]
+ (Union::Float(f1, ..), Union::Float(f2, ..)) => match op_x {
+ PlusAssign => **f1 += **f2,
+ MinusAssign => **f1 -= **f2,
+ MultiplyAssign => **f1 *= **f2,
+ DivideAssign => **f1 /= **f2,
+ ModuloAssign => **f1 %= **f2,
+ _ => done = false,
+ },
+ #[cfg(not(feature = "no_float"))]
+ (Union::Float(f1, ..), Union::Int(n2, ..)) => match op_x {
+ PlusAssign => **f1 += *n2 as crate::FLOAT,
+ MinusAssign => **f1 -= *n2 as crate::FLOAT,
+ MultiplyAssign => **f1 *= *n2 as crate::FLOAT,
+ DivideAssign => **f1 /= *n2 as crate::FLOAT,
+ ModuloAssign => **f1 %= *n2 as crate::FLOAT,
+ _ => done = false,
+ },
+ _ => done = false,
+ }
+
+ if !done {
+ if let Some((func, need_context)) =
+ get_builtin_op_assignment_fn(op_x, &*lock_guard, &new_val)
+ {
+ // We may not need to bump the level because built-in's do not need it.
+ //defer! { let orig_level = global.level; global.level += 1 }
+
+ let args = &mut [&mut *lock_guard, &mut new_val];
+ let context = need_context
+ .then(|| (self, op_x_str, global.source(), &*global, pos).into());
+ let _ = func(context, args).map_err(|err| err.fill_position(pos))?;
+ done = true;
+ }
+ }
+ }
+
+ if !done {
+ let opx = Some(op_x);
+ let args = &mut [&mut *lock_guard, &mut new_val];
+
+ match self
+ .exec_native_fn_call(global, caches, op_x_str, opx, hash_x, args, true, pos)
+ {
+ Ok(_) => (),
+ Err(err) if matches!(*err, ERR::ErrorFunctionNotFound(ref f, ..) if f.starts_with(op_x_str)) =>
+ {
+ // Expand to `var = var op rhs`
+ let op = Some(op);
+
+ *args[0] = self
+ .exec_native_fn_call(global, caches, op_str, op, hash, args, true, pos)?
+ .0;
+ }
+ Err(err) => return Err(err),
+ }
+
+ self.check_data_size(&*args[0], root.position())?;
+ }
+ } else {
+ // Normal assignment
+ match target {
+ // Lock it again just in case it is shared
+ Target::RefMut(_) | Target::TempValue(_) => {
+ *target.write_lock::<Dynamic>().unwrap() = new_val
+ }
+ #[allow(unreachable_patterns)]
+ _ => **target = new_val,
+ }
+ }
+
+ target.propagate_changed_value(pos)
+ }
+
+ /// Evaluate a statement.
+ pub(crate) fn eval_stmt(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ mut this_ptr: Option<&mut Dynamic>,
+ stmt: &Stmt,
+ rewind_scope: bool,
+ ) -> RhaiResult {
+ self.track_operation(global, stmt.position())?;
+
+ #[cfg(feature = "debugging")]
+ let reset =
+ self.run_debugger_with_reset(global, caches, scope, this_ptr.as_deref_mut(), stmt)?;
+ #[cfg(feature = "debugging")]
+ defer! { global if Some(reset) => move |g| g.debugger_mut().reset_status(reset) }
+
+ match stmt {
+ // No-op
+ Stmt::Noop(..) => Ok(Dynamic::UNIT),
+
+ // Expression as statement
+ Stmt::Expr(expr) => self
+ .eval_expr(global, caches, scope, this_ptr, expr)
+ .map(Dynamic::flatten),
+
+ // Block scope
+ Stmt::Block(statements, ..) => {
+ if statements.is_empty() {
+ Ok(Dynamic::UNIT)
+ } else {
+ self.eval_stmt_block(global, caches, scope, this_ptr, statements, true)
+ }
+ }
+
+ // Function call
+ Stmt::FnCall(x, pos) => {
+ self.eval_fn_call_expr(global, caches, scope, this_ptr, x, *pos)
+ }
+
+ // Assignment
+ Stmt::Assignment(x, ..) => {
+ let (op_info, BinaryExpr { lhs, rhs }) = &**x;
+
+ if let Expr::ThisPtr(..) = lhs {
+ if this_ptr.is_none() {
+ return Err(ERR::ErrorUnboundThis(lhs.position()).into());
+ }
+
+ #[cfg(not(feature = "no_function"))]
+ {
+ let rhs_val = self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), rhs)?
+ .flatten();
+
+ self.track_operation(global, lhs.position())?;
+
+ let target = &mut this_ptr.unwrap().into();
+
+ self.eval_op_assignment(global, caches, op_info, lhs, target, rhs_val)?;
+ }
+ #[cfg(feature = "no_function")]
+ unreachable!();
+ } else if let Expr::Variable(x, ..) = lhs {
+ let rhs_val = self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), rhs)?
+ .flatten();
+
+ self.track_operation(global, lhs.position())?;
+
+ let mut target = self.search_namespace(global, caches, scope, this_ptr, lhs)?;
+
+ let is_temp_result = !target.is_ref();
+ let var_name = x.3.as_str();
+
+ #[cfg(not(feature = "no_closure"))]
+ // Also handle case where target is a `Dynamic` shared value
+ // (returned by a variable resolver, for example)
+ let is_temp_result = is_temp_result && !target.is_shared();
+
+ // Cannot assign to temp result from expression
+ if is_temp_result {
+ return Err(ERR::ErrorAssignmentToConstant(
+ var_name.to_string(),
+ lhs.position(),
+ )
+ .into());
+ }
+
+ self.eval_op_assignment(global, caches, op_info, lhs, &mut target, rhs_val)?;
+ } else {
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ {
+ let rhs_val = self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), rhs)?
+ .flatten()
+ .intern_string(self);
+
+ let _new_val = Some((rhs_val, op_info));
+
+ // Must be either `var[index] op= val` or `var.prop op= val`.
+ // The return value of any op-assignment (should be `()`) is thrown away and not used.
+ let _ = match lhs {
+ // this op= rhs (handled above)
+ Expr::ThisPtr(..) => {
+ unreachable!("Expr::ThisPtr case is already handled")
+ }
+ // name op= rhs (handled above)
+ Expr::Variable(..) => {
+ unreachable!("Expr::Variable case is already handled")
+ }
+ // idx_lhs[idx_expr] op= rhs
+ #[cfg(not(feature = "no_index"))]
+ Expr::Index(..) => self.eval_dot_index_chain(
+ global, caches, scope, this_ptr, lhs, _new_val,
+ ),
+ // dot_lhs.dot_rhs op= rhs
+ #[cfg(not(feature = "no_object"))]
+ Expr::Dot(..) => self.eval_dot_index_chain(
+ global, caches, scope, this_ptr, lhs, _new_val,
+ ),
+ _ => unreachable!("cannot assign to expression: {:?}", lhs),
+ }?;
+ }
+ }
+
+ Ok(Dynamic::UNIT)
+ }
+
+ // Variable definition
+ Stmt::Var(x, options, pos) => {
+ if !self.allow_shadowing() && scope.contains(&x.0) {
+ return Err(ERR::ErrorVariableExists(x.0.to_string(), *pos).into());
+ }
+
+ // Let/const statement
+ let (var_name, expr, index) = &**x;
+
+ let access = if options.contains(ASTFlags::CONSTANT) {
+ AccessMode::ReadOnly
+ } else {
+ AccessMode::ReadWrite
+ };
+ let export = options.contains(ASTFlags::EXPORTED);
+
+ // Check variable definition filter
+ if let Some(ref filter) = self.def_var_filter {
+ let will_shadow = scope.contains(var_name);
+ let is_const = access == AccessMode::ReadOnly;
+ let info = VarDefInfo {
+ name: var_name,
+ is_const,
+ nesting_level: global.scope_level,
+ will_shadow,
+ };
+ let orig_scope_len = scope.len();
+ let context =
+ EvalContext::new(self, global, caches, scope, this_ptr.as_deref_mut());
+ let filter_result = filter(true, info, context);
+
+ if orig_scope_len != scope.len() {
+ // The scope is changed, always search from now on
+ global.always_search_scope = true;
+ }
+
+ if !filter_result? {
+ return Err(ERR::ErrorForbiddenVariable(var_name.to_string(), *pos).into());
+ }
+ }
+
+ // Evaluate initial value
+ let mut value = self
+ .eval_expr(global, caches, scope, this_ptr, expr)?
+ .flatten()
+ .intern_string(self);
+
+ let _alias = if !rewind_scope {
+ // Put global constants into global module
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "no_module"))]
+ if global.scope_level == 0
+ && access == AccessMode::ReadOnly
+ && global.lib.iter().any(|m| !m.is_empty())
+ {
+ crate::func::locked_write(global.constants.get_or_insert_with(|| {
+ crate::Shared::new(
+ crate::Locked::new(std::collections::BTreeMap::new()),
+ )
+ }))
+ .insert(var_name.name.clone(), value.clone());
+ }
+
+ if export {
+ Some(var_name)
+ } else {
+ None
+ }
+ } else if export {
+ unreachable!("exported variable not on global level");
+ } else {
+ None
+ };
+
+ if let Some(index) = index {
+ value.set_access_mode(access);
+ *scope.get_mut_by_index(scope.len() - index.get()) = value;
+ } else {
+ scope.push_entry(var_name.name.clone(), access, value);
+ }
+
+ #[cfg(not(feature = "no_module"))]
+ if let Some(alias) = _alias {
+ scope.add_alias_by_index(scope.len() - 1, alias.as_str().into());
+ }
+
+ Ok(Dynamic::UNIT)
+ }
+
+ // If statement
+ Stmt::If(x, ..) => {
+ let FlowControl {
+ expr,
+ body: if_block,
+ branch: else_block,
+ } = &**x;
+
+ let guard_val = self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), expr)?
+ .as_bool()
+ .map_err(|typ| self.make_type_mismatch_err::<bool>(typ, expr.position()))?;
+
+ match guard_val {
+ true if !if_block.is_empty() => {
+ self.eval_stmt_block(global, caches, scope, this_ptr, if_block, true)
+ }
+ false if !else_block.is_empty() => {
+ self.eval_stmt_block(global, caches, scope, this_ptr, else_block, true)
+ }
+ _ => Ok(Dynamic::UNIT),
+ }
+ }
+
+ // Switch statement
+ Stmt::Switch(x, ..) => {
+ let (
+ expr,
+ SwitchCasesCollection {
+ expressions,
+ cases,
+ def_case,
+ ranges,
+ },
+ ) = &**x;
+
+ let mut result = None;
+
+ let value = self.eval_expr(global, caches, scope, this_ptr.as_deref_mut(), expr)?;
+
+ if value.is_hashable() {
+ let hasher = &mut get_hasher();
+ value.hash(hasher);
+ let hash = hasher.finish();
+
+ // First check hashes
+ if let Some(case_blocks_list) = cases.get(&hash) {
+ debug_assert!(!case_blocks_list.is_empty());
+
+ for &index in case_blocks_list {
+ let block = &expressions[index];
+
+ let cond_result = match block.condition {
+ Expr::BoolConstant(b, ..) => b,
+ ref c => self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), c)?
+ .as_bool()
+ .map_err(|typ| {
+ self.make_type_mismatch_err::<bool>(typ, c.position())
+ })?,
+ };
+
+ if cond_result {
+ result = Some(&block.expr);
+ break;
+ }
+ }
+ } else if !ranges.is_empty() {
+ // Then check integer ranges
+ for r in ranges.iter().filter(|r| r.contains(&value)) {
+ let ConditionalExpr { condition, expr } = &expressions[r.index()];
+
+ let cond_result = match condition {
+ Expr::BoolConstant(b, ..) => *b,
+ ref c => self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), c)?
+ .as_bool()
+ .map_err(|typ| {
+ self.make_type_mismatch_err::<bool>(typ, c.position())
+ })?,
+ };
+
+ if cond_result {
+ result = Some(expr);
+ break;
+ }
+ }
+ }
+ }
+
+ result
+ .or_else(|| def_case.as_ref().map(|&index| &expressions[index].expr))
+ .map_or(Ok(Dynamic::UNIT), |expr| {
+ self.eval_expr(global, caches, scope, this_ptr, expr)
+ })
+ }
+
+ // Loop
+ Stmt::While(x, ..)
+ if matches!(x.expr, Expr::Unit(..) | Expr::BoolConstant(true, ..)) =>
+ {
+ let FlowControl { body, .. } = &**x;
+
+ if body.is_empty() {
+ loop {
+ self.track_operation(global, body.position())?;
+ }
+ }
+
+ loop {
+ let this_ptr = this_ptr.as_deref_mut();
+
+ match self.eval_stmt_block(global, caches, scope, this_ptr, body, true) {
+ Ok(..) => (),
+ Err(err) => match *err {
+ ERR::LoopBreak(false, ..) => (),
+ ERR::LoopBreak(true, value, ..) => break Ok(value),
+ _ => break Err(err),
+ },
+ }
+ }
+ }
+
+ // While loop
+ Stmt::While(x, ..) => {
+ let FlowControl { expr, body, .. } = &**x;
+
+ loop {
+ let condition = self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), expr)?
+ .as_bool()
+ .map_err(|typ| self.make_type_mismatch_err::<bool>(typ, expr.position()))?;
+
+ if !condition {
+ break Ok(Dynamic::UNIT);
+ }
+
+ if body.is_empty() {
+ continue;
+ }
+
+ let this_ptr = this_ptr.as_deref_mut();
+
+ match self.eval_stmt_block(global, caches, scope, this_ptr, body, true) {
+ Ok(..) => (),
+ Err(err) => match *err {
+ ERR::LoopBreak(false, ..) => (),
+ ERR::LoopBreak(true, value, ..) => break Ok(value),
+ _ => break Err(err),
+ },
+ }
+ }
+ }
+
+ // Do loop
+ Stmt::Do(x, options, ..) => {
+ let FlowControl { expr, body, .. } = &**x;
+ let is_while = !options.contains(ASTFlags::NEGATED);
+
+ loop {
+ if !body.is_empty() {
+ let this_ptr = this_ptr.as_deref_mut();
+
+ match self.eval_stmt_block(global, caches, scope, this_ptr, body, true) {
+ Ok(..) => (),
+ Err(err) => match *err {
+ ERR::LoopBreak(false, ..) => continue,
+ ERR::LoopBreak(true, value, ..) => break Ok(value),
+ _ => break Err(err),
+ },
+ }
+ }
+
+ let condition = self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), expr)?
+ .as_bool()
+ .map_err(|typ| self.make_type_mismatch_err::<bool>(typ, expr.position()))?;
+
+ if condition ^ is_while {
+ break Ok(Dynamic::UNIT);
+ }
+ }
+ }
+
+ // For loop
+ Stmt::For(x, ..) => {
+ let (var_name, counter, FlowControl { expr, body, .. }) = &**x;
+
+ let iter_obj = self
+ .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), expr)?
+ .flatten();
+
+ let iter_type = iter_obj.type_id();
+
+ // lib should only contain scripts, so technically they cannot have iterators
+
+ // Search order:
+ // 1) Global namespace - functions registered via Engine::register_XXX
+ // 2) Global modules - packages
+ // 3) Imported modules - functions marked with global namespace
+ // 4) Global sub-modules - functions marked with global namespace
+ let iter_func = self
+ .global_modules
+ .iter()
+ .find_map(|m| m.get_iter(iter_type));
+
+ #[cfg(not(feature = "no_module"))]
+ let iter_func = iter_func
+ .or_else(|| global.get_iter(iter_type))
+ .or_else(|| {
+ self.global_sub_modules
+ .as_ref()
+ .into_iter()
+ .flatten()
+ .find_map(|(_, m)| m.get_qualified_iter(iter_type))
+ });
+
+ let iter_func = iter_func.ok_or_else(|| ERR::ErrorFor(expr.start_position()))?;
+
+ // Restore scope at end of statement
+ defer! { scope => rewind; let orig_scope_len = scope.len(); }
+
+ // Add the loop variables
+ let counter_index = (!counter.is_empty()).then(|| {
+ scope.push(counter.name.clone(), 0 as INT);
+ scope.len() - 1
+ });
+
+ scope.push(var_name.name.clone(), ());
+ let index = scope.len() - 1;
+
+ let mut result = Dynamic::UNIT;
+
+ if body.is_empty() {
+ for _ in iter_func(iter_obj) {
+ self.track_operation(global, body.position())?;
+ }
+ } else {
+ for (x, iter_value) in iter_func(iter_obj).enumerate() {
+ // Increment counter
+ if let Some(counter_index) = counter_index {
+ // As the variable increments from 0, this should always work
+ // since any overflow will first be caught below.
+ let index_value = x as INT;
+
+ #[cfg(not(feature = "unchecked"))]
+ #[allow(clippy::absurd_extreme_comparisons)]
+ if index_value > crate::MAX_USIZE_INT {
+ return Err(ERR::ErrorArithmetic(
+ format!("for-loop counter overflow: {x}"),
+ counter.pos,
+ )
+ .into());
+ }
+
+ *scope.get_mut_by_index(counter_index).write_lock().unwrap() =
+ Dynamic::from_int(index_value);
+ }
+
+ // Set loop value
+ let value = iter_value
+ .map_err(|err| err.fill_position(expr.position()))?
+ .flatten();
+
+ *scope.get_mut_by_index(index).write_lock().unwrap() = value;
+
+ // Run block
+ let this_ptr = this_ptr.as_deref_mut();
+
+ match self.eval_stmt_block(global, caches, scope, this_ptr, body, true) {
+ Ok(_) => (),
+ Err(err) => match *err {
+ ERR::LoopBreak(false, ..) => (),
+ ERR::LoopBreak(true, value, ..) => {
+ result = value;
+ break;
+ }
+ _ => return Err(err),
+ },
+ }
+ }
+ }
+
+ Ok(result)
+ }
+
+ // Continue/Break statement
+ Stmt::BreakLoop(expr, options, pos) => {
+ let is_break = options.contains(ASTFlags::BREAK);
+
+ let value = if let Some(ref expr) = expr {
+ self.eval_expr(global, caches, scope, this_ptr, expr)?
+ } else {
+ Dynamic::UNIT
+ };
+
+ Err(ERR::LoopBreak(is_break, value, *pos).into())
+ }
+
+ // Try/Catch statement
+ Stmt::TryCatch(x, ..) => {
+ let FlowControl {
+ body: try_block,
+ expr: catch_var,
+ branch: catch_block,
+ } = &**x;
+
+ match self.eval_stmt_block(
+ global,
+ caches,
+ scope,
+ this_ptr.as_deref_mut(),
+ try_block,
+ true,
+ ) {
+ r @ Ok(_) => r,
+ Err(err) if err.is_pseudo_error() => Err(err),
+ Err(err) if !err.is_catchable() => Err(err),
+ Err(mut err) => {
+ let err_value = match err.unwrap_inner() {
+ ERR::ErrorRuntime(x, ..) => x.clone(),
+
+ #[cfg(feature = "no_object")]
+ _ => {
+ let _ = err.take_position();
+ err.to_string().into()
+ }
+ #[cfg(not(feature = "no_object"))]
+ _ => {
+ let mut err_map = crate::Map::new();
+ let err_pos = err.take_position();
+
+ err_map.insert("message".into(), err.to_string().into());
+
+ if let Some(ref source) = global.source {
+ err_map.insert("source".into(), source.into());
+ }
+
+ if !err_pos.is_none() {
+ err_map.insert(
+ "line".into(),
+ (err_pos.line().unwrap() as INT).into(),
+ );
+ err_map.insert(
+ "position".into(),
+ (err_pos.position().unwrap_or(0) as INT).into(),
+ );
+ }
+
+ err.dump_fields(&mut err_map);
+ err_map.into()
+ }
+ };
+
+ // Restore scope at end of block
+ defer! { scope if !catch_var.is_unit() => rewind; let orig_scope_len = scope.len(); }
+
+ if let Expr::Variable(x, ..) = catch_var {
+ scope.push(x.3.clone(), err_value);
+ }
+
+ let this_ptr = this_ptr.as_deref_mut();
+
+ self.eval_stmt_block(global, caches, scope, this_ptr, catch_block, true)
+ .map(|_| Dynamic::UNIT)
+ .map_err(|result_err| match *result_err {
+ // Re-throw exception
+ ERR::ErrorRuntime(v, pos) if v.is_unit() => {
+ err.set_position(pos);
+ err
+ }
+ _ => result_err,
+ })
+ }
+ }
+ }
+
+ // Throw value
+ Stmt::Return(Some(expr), options, pos) if options.contains(ASTFlags::BREAK) => self
+ .eval_expr(global, caches, scope, this_ptr, expr)
+ .and_then(|v| Err(ERR::ErrorRuntime(v.flatten(), *pos).into())),
+
+ // Empty throw
+ Stmt::Return(None, options, pos) if options.contains(ASTFlags::BREAK) => {
+ Err(ERR::ErrorRuntime(Dynamic::UNIT, *pos).into())
+ }
+
+ // Return value
+ Stmt::Return(Some(expr), .., pos) => self
+ .eval_expr(global, caches, scope, this_ptr, expr)
+ .and_then(|v| Err(ERR::Return(v.flatten(), *pos).into())),
+
+ // Empty return
+ Stmt::Return(None, .., pos) => Err(ERR::Return(Dynamic::UNIT, *pos).into()),
+
+ // Import statement
+ #[cfg(not(feature = "no_module"))]
+ Stmt::Import(x, _pos) => {
+ use crate::ModuleResolver;
+
+ let (expr, export) = &**x;
+
+ // Guard against too many modules
+ if global.num_modules_loaded >= self.max_modules() {
+ return Err(ERR::ErrorTooManyModules(*_pos).into());
+ }
+
+ let v = self.eval_expr(global, caches, scope, this_ptr, expr)?;
+
+ let path = v.try_cast_raw::<crate::ImmutableString>().map_err(|v| {
+ self.make_type_mismatch_err::<crate::ImmutableString>(
+ v.type_name(),
+ expr.position(),
+ )
+ })?;
+
+ let path_pos = expr.start_position();
+
+ let resolver = global.embedded_module_resolver.clone();
+
+ let module = resolver
+ .as_ref()
+ .and_then(
+ |r| match r.resolve_raw(self, global, scope, &path, path_pos) {
+ Err(err) if matches!(*err, ERR::ErrorModuleNotFound(..)) => None,
+ result => Some(result),
+ },
+ )
+ .or_else(|| {
+ Some(
+ self.module_resolver()
+ .resolve_raw(self, global, scope, &path, path_pos),
+ )
+ })
+ .unwrap_or_else(|| {
+ Err(ERR::ErrorModuleNotFound(path.to_string(), path_pos).into())
+ })?;
+
+ let (export, must_be_indexed) = if export.is_empty() {
+ (self.const_empty_string(), false)
+ } else {
+ (export.name.clone(), true)
+ };
+
+ if !must_be_indexed || module.is_indexed() {
+ global.push_import(export, module);
+ } else {
+ // Index the module (making a clone copy if necessary) if it is not indexed
+ let mut m = crate::func::shared_take_or_clone(module);
+ m.build_index();
+ global.push_import(export, m);
+ }
+
+ global.num_modules_loaded += 1;
+
+ Ok(Dynamic::UNIT)
+ }
+
+ // Export statement
+ #[cfg(not(feature = "no_module"))]
+ Stmt::Export(x, ..) => {
+ use crate::ast::Ident;
+ let (Ident { name, pos, .. }, Ident { name: alias, .. }) = &**x;
+ // Mark scope variables as public
+ scope.search(name).map_or_else(
+ || Err(ERR::ErrorVariableNotFound(name.to_string(), *pos).into()),
+ |index| {
+ let alias = if alias.is_empty() { name } else { alias }.clone();
+ scope.add_alias_by_index(index, alias);
+ Ok(Dynamic::UNIT)
+ },
+ )
+ }
+
+ // Share statement
+ #[cfg(not(feature = "no_closure"))]
+ Stmt::Share(x) => {
+ for (var, index) in &**x {
+ let index = index
+ .map(|n| scope.len() - n.get())
+ .or_else(|| scope.search(&var.name))
+ .ok_or_else(|| {
+ Box::new(ERR::ErrorVariableNotFound(var.name.to_string(), var.pos))
+ })?;
+
+ let val = scope.get_mut_by_index(index);
+
+ if !val.is_shared() {
+ // Replace the variable with a shared value.
+ *val = val.take().into_shared();
+ }
+ }
+
+ Ok(Dynamic::UNIT)
+ }
+
+ #[allow(unreachable_patterns)]
+ _ => unreachable!("statement cannot be evaluated: {:?}", stmt),
+ }
+ }
+
+ /// Evaluate a list of statements with no `this` pointer.
+ /// This is commonly used to evaluate a list of statements in an [`AST`][crate::AST] or a script function body.
+ #[inline(always)]
+ pub(crate) fn eval_global_statements(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ statements: &[Stmt],
+ ) -> RhaiResult {
+ self.eval_stmt_block(global, caches, scope, None, statements, false)
+ .or_else(|err| match *err {
+ ERR::Return(out, ..) => Ok(out),
+ ERR::LoopBreak(..) => {
+ unreachable!("no outer loop scope to break out of")
+ }
+ _ => Err(err),
+ })
+ }
+}
diff --git a/rhai/src/eval/target.rs b/rhai/src/eval/target.rs
new file mode 100644
index 0000000..6679d61
--- /dev/null
+++ b/rhai/src/eval/target.rs
@@ -0,0 +1,442 @@
+//! Type to hold a mutable reference to the target of an evaluation.
+
+use crate::{Dynamic, Position, RhaiResultOf};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ borrow::Borrow,
+ ops::{Deref, DerefMut},
+};
+
+/// Calculate an offset+len pair given an actual length of the underlying array.
+///
+/// Negative starting positions count from the end.
+///
+/// Values going over bounds are limited to the actual length.
+#[cfg(not(feature = "no_index"))]
+#[inline]
+#[allow(
+ clippy::cast_sign_loss,
+ clippy::absurd_extreme_comparisons,
+ clippy::cast_possible_truncation
+)]
+pub fn calc_offset_len(length: usize, start: crate::INT, len: crate::INT) -> (usize, usize) {
+ let start = if start < 0 {
+ let abs_start = start.unsigned_abs();
+ if abs_start as u64 > crate::MAX_USIZE_INT as u64 {
+ 0
+ } else {
+ length - usize::min(abs_start as usize, length)
+ }
+ } else if start > crate::MAX_USIZE_INT || start as usize >= length {
+ return (length, 0);
+ } else {
+ start as usize
+ };
+
+ let len = if len <= 0 {
+ 0
+ } else if len > crate::MAX_USIZE_INT || len as usize > length - start {
+ length - start
+ } else {
+ len as usize
+ };
+
+ (start, len)
+}
+
+/// Calculate an offset+len pair given an actual length of the underlying array.
+///
+/// Negative starting positions count from the end.
+///
+/// Values going over bounds call the provided closure to return a default value or an error.
+#[inline]
+#[allow(dead_code)]
+#[allow(
+ clippy::cast_sign_loss,
+ clippy::cast_possible_truncation,
+ clippy::absurd_extreme_comparisons
+)]
+pub fn calc_index<E>(
+ length: usize,
+ start: crate::INT,
+ negative_count_from_end: bool,
+ err_func: impl FnOnce() -> Result<usize, E>,
+) -> Result<usize, E> {
+ if start < 0 && negative_count_from_end {
+ let abs_start = start.unsigned_abs();
+ return Ok(if abs_start as u64 > crate::MAX_USIZE_INT as u64 {
+ 0
+ } else {
+ length - usize::min(abs_start as usize, length)
+ });
+ }
+
+ if start <= crate::MAX_USIZE_INT && (start as usize) < length {
+ return Ok(start as usize);
+ }
+
+ err_func()
+}
+
+/// A type that encapsulates a mutation target for an expression with side effects.
+#[derive(Debug)]
+#[must_use]
+pub enum Target<'a> {
+ /// The target is a mutable reference to a [`Dynamic`].
+ RefMut(&'a mut Dynamic),
+ /// The target is a mutable reference to a _shared_ [`Dynamic`].
+ #[cfg(not(feature = "no_closure"))]
+ SharedValue {
+ /// Lock guard to the shared [`Dynamic`].
+ guard: crate::types::dynamic::DynamicWriteLock<'a, Dynamic>,
+ /// Copy of the shared value.
+ shared_value: Dynamic,
+ },
+ /// The target is a temporary [`Dynamic`] value (i.e. its mutation can cause no side effects).
+ TempValue(Dynamic),
+ /// The target is a bit inside an [`INT`][crate::INT].
+ /// This is necessary because directly pointing to a bit inside an [`INT`][crate::INT] is impossible.
+ #[cfg(not(feature = "no_index"))]
+ Bit {
+ /// Mutable reference to the source [`Dynamic`].
+ source: &'a mut Dynamic,
+ /// Copy of the boolean bit, as a [`Dynamic`].
+ value: Dynamic,
+ /// Bit offset.
+ bit: u8,
+ },
+ /// The target is a range of bits inside an [`INT`][crate::INT].
+ /// This is necessary because directly pointing to a range of bits inside an [`INT`][crate::INT] is impossible.
+ #[cfg(not(feature = "no_index"))]
+ BitField {
+ /// Mutable reference to the source [`Dynamic`].
+ source: &'a mut Dynamic,
+ /// Copy of the integer value of the bits, as a [`Dynamic`].
+ value: Dynamic,
+ /// Bitmask to apply to the source value (i.e. shifted)
+ mask: crate::INT,
+ /// Number of bits to right-shift the source value.
+ shift: u8,
+ },
+ /// The target is a byte inside a [`Blob`][crate::Blob].
+ /// This is necessary because directly pointing to a byte (in [`Dynamic`] form) inside a blob is impossible.
+ #[cfg(not(feature = "no_index"))]
+ BlobByte {
+ /// Mutable reference to the source [`Dynamic`].
+ source: &'a mut Dynamic,
+ /// Copy of the byte at the index, as a [`Dynamic`].
+ value: Dynamic,
+ /// Offset index.
+ index: usize,
+ },
+ /// The target is a character inside a string.
+ /// This is necessary because directly pointing to a char inside a String is impossible.
+ #[cfg(not(feature = "no_index"))]
+ StringChar {
+ /// Mutable reference to the source [`Dynamic`].
+ source: &'a mut Dynamic,
+ /// Copy of the character at the offset, as a [`Dynamic`].
+ value: Dynamic,
+ /// Offset index.
+ index: usize,
+ },
+}
+
+impl<'a> Target<'a> {
+ /// Is the [`Target`] a reference pointing to other data?
+ #[allow(dead_code)]
+ #[inline]
+ #[must_use]
+ pub const fn is_ref(&self) -> bool {
+ match self {
+ Self::RefMut(..) => true,
+ #[cfg(not(feature = "no_closure"))]
+ Self::SharedValue { .. } => true,
+ Self::TempValue(..) => false,
+ #[cfg(not(feature = "no_index"))]
+ Self::Bit { .. }
+ | Self::BitField { .. }
+ | Self::BlobByte { .. }
+ | Self::StringChar { .. } => false,
+ }
+ }
+ /// Is the [`Target`] a temp value?
+ #[inline]
+ #[must_use]
+ pub const fn is_temp_value(&self) -> bool {
+ match self {
+ Self::RefMut(..) => false,
+ #[cfg(not(feature = "no_closure"))]
+ Self::SharedValue { .. } => false,
+ Self::TempValue(..) => true,
+ #[cfg(not(feature = "no_index"))]
+ Self::Bit { .. }
+ | Self::BitField { .. }
+ | Self::BlobByte { .. }
+ | Self::StringChar { .. } => false,
+ }
+ }
+ /// Is the [`Target`] a shared value?
+ #[inline]
+ #[must_use]
+ pub fn is_shared(&self) -> bool {
+ #[cfg(not(feature = "no_closure"))]
+ return match self {
+ Self::RefMut(r) => r.is_shared(),
+ Self::SharedValue { .. } => true,
+ Self::TempValue(value) => value.is_shared(),
+ #[cfg(not(feature = "no_index"))]
+ Self::Bit { .. }
+ | Self::BitField { .. }
+ | Self::BlobByte { .. }
+ | Self::StringChar { .. } => false,
+ };
+ #[cfg(feature = "no_closure")]
+ return false;
+ }
+ /// Get the value of the [`Target`] as a [`Dynamic`], cloning a referenced value if necessary.
+ #[inline]
+ pub fn take_or_clone(self) -> Dynamic {
+ match self {
+ Self::RefMut(r) => r.clone(), // Referenced value is cloned
+ #[cfg(not(feature = "no_closure"))]
+ Self::SharedValue { shared_value, .. } => shared_value, // Original shared value is simply taken
+ Self::TempValue(value) => value, // Owned value is simply taken
+ #[cfg(not(feature = "no_index"))]
+ Self::Bit { value, .. } => value, // boolean is taken
+ #[cfg(not(feature = "no_index"))]
+ Self::BitField { value, .. } => value, // INT is taken
+ #[cfg(not(feature = "no_index"))]
+ Self::BlobByte { value, .. } => value, // byte is taken
+ #[cfg(not(feature = "no_index"))]
+ Self::StringChar { value, .. } => value, // char is taken
+ }
+ }
+ /// Take a `&mut Dynamic` reference from the `Target`.
+ #[inline(always)]
+ #[must_use]
+ pub fn take_ref(self) -> Option<&'a mut Dynamic> {
+ match self {
+ Self::RefMut(r) => Some(r),
+ _ => None,
+ }
+ }
+ /// Convert a shared or reference [`Target`] into a target with an owned value.
+ #[inline(always)]
+ pub fn into_owned(self) -> Self {
+ match self {
+ Self::RefMut(r) => Self::TempValue(r.clone()),
+ #[cfg(not(feature = "no_closure"))]
+ Self::SharedValue { shared_value, .. } => Self::TempValue(shared_value),
+ _ => self,
+ }
+ }
+ /// Get the source [`Dynamic`] of the [`Target`].
+ #[allow(dead_code)]
+ #[inline]
+ pub fn source(&self) -> &Dynamic {
+ match self {
+ Self::RefMut(r) => r,
+ #[cfg(not(feature = "no_closure"))]
+ Self::SharedValue { guard, .. } => guard,
+ Self::TempValue(value) => value,
+ #[cfg(not(feature = "no_index"))]
+ Self::Bit { source, .. } => source,
+ #[cfg(not(feature = "no_index"))]
+ Self::BitField { source, .. } => source,
+ #[cfg(not(feature = "no_index"))]
+ Self::BlobByte { source, .. } => source,
+ #[cfg(not(feature = "no_index"))]
+ Self::StringChar { source, .. } => source,
+ }
+ }
+ /// Propagate a changed value back to the original source.
+ /// This has no effect for direct references.
+ #[inline]
+ pub fn propagate_changed_value(&mut self, _pos: Position) -> RhaiResultOf<()> {
+ match self {
+ Self::RefMut(..) | Self::TempValue(..) => (),
+ #[cfg(not(feature = "no_closure"))]
+ Self::SharedValue { .. } => (),
+ #[cfg(not(feature = "no_index"))]
+ Self::Bit { source, value, bit } => {
+ // Replace the bit at the specified index position
+ let new_bit = value.as_bool().map_err(|err| {
+ Box::new(crate::ERR::ErrorMismatchDataType(
+ "bool".to_string(),
+ err.to_string(),
+ _pos,
+ ))
+ })?;
+
+ let value = &mut *source.write_lock::<crate::INT>().expect("`INT`");
+
+ let index = *bit;
+
+ let mask = 1 << index;
+ if new_bit {
+ *value |= mask;
+ } else {
+ *value &= !mask;
+ }
+ }
+ #[cfg(not(feature = "no_index"))]
+ Self::BitField {
+ source,
+ value,
+ mask,
+ shift,
+ } => {
+ let shift = *shift;
+ let mask = *mask;
+
+ // Replace the bit at the specified index position
+ let new_value = value.as_int().map_err(|err| {
+ Box::new(crate::ERR::ErrorMismatchDataType(
+ "integer".to_string(),
+ err.to_string(),
+ _pos,
+ ))
+ })?;
+
+ let new_value = (new_value << shift) & mask;
+ let value = &mut *source.write_lock::<crate::INT>().expect("`INT`");
+
+ *value &= !mask;
+ *value |= new_value;
+ }
+ #[cfg(not(feature = "no_index"))]
+ Self::BlobByte {
+ source,
+ value,
+ index,
+ } => {
+ // Replace the byte at the specified index position
+ let new_byte = value.as_int().map_err(|err| {
+ Box::new(crate::ERR::ErrorMismatchDataType(
+ "INT".to_string(),
+ err.to_string(),
+ _pos,
+ ))
+ })?;
+
+ let value = &mut *source.write_lock::<crate::Blob>().expect("`Blob`");
+
+ #[allow(clippy::cast_sign_loss)]
+ {
+ value[*index] = (new_byte & 0x00ff) as u8;
+ }
+ }
+ #[cfg(not(feature = "no_index"))]
+ Self::StringChar {
+ source,
+ value,
+ index,
+ } => {
+ // Replace the character at the specified index position
+ let new_ch = value.as_char().map_err(|err| {
+ Box::new(crate::ERR::ErrorMismatchDataType(
+ "char".to_string(),
+ err.to_string(),
+ _pos,
+ ))
+ })?;
+
+ let s = &mut *source
+ .write_lock::<crate::ImmutableString>()
+ .expect("`ImmutableString`");
+
+ *s = s
+ .chars()
+ .enumerate()
+ .map(|(i, ch)| if i == *index { new_ch } else { ch })
+ .collect();
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl<'a> From<&'a mut Dynamic> for Target<'a> {
+ #[inline]
+ fn from(value: &'a mut Dynamic) -> Self {
+ #[cfg(not(feature = "no_closure"))]
+ if value.is_shared() {
+ // Cloning is cheap for a shared value
+ let shared_value = value.clone();
+ let guard = value.write_lock::<Dynamic>().expect("`Dynamic`");
+ return Self::SharedValue {
+ guard,
+ shared_value,
+ };
+ }
+
+ Self::RefMut(value)
+ }
+}
+
+impl Deref for Target<'_> {
+ type Target = Dynamic;
+
+ #[inline]
+ fn deref(&self) -> &Dynamic {
+ match self {
+ Self::RefMut(r) => r,
+ #[cfg(not(feature = "no_closure"))]
+ Self::SharedValue { guard, .. } => guard,
+ Self::TempValue(ref value) => value,
+ #[cfg(not(feature = "no_index"))]
+ Self::Bit { ref value, .. }
+ | Self::BitField { ref value, .. }
+ | Self::BlobByte { ref value, .. }
+ | Self::StringChar { ref value, .. } => value,
+ }
+ }
+}
+
+impl AsRef<Dynamic> for Target<'_> {
+ #[inline(always)]
+ fn as_ref(&self) -> &Dynamic {
+ self
+ }
+}
+
+impl Borrow<Dynamic> for Target<'_> {
+ #[inline(always)]
+ fn borrow(&self) -> &Dynamic {
+ self
+ }
+}
+
+impl DerefMut for Target<'_> {
+ #[inline]
+ fn deref_mut(&mut self) -> &mut Dynamic {
+ match self {
+ Self::RefMut(r) => r,
+ #[cfg(not(feature = "no_closure"))]
+ Self::SharedValue { guard, .. } => &mut *guard,
+ Self::TempValue(ref mut value) => value,
+ #[cfg(not(feature = "no_index"))]
+ Self::Bit { ref mut value, .. }
+ | Self::BitField { ref mut value, .. }
+ | Self::BlobByte { ref mut value, .. }
+ | Self::StringChar { ref mut value, .. } => value,
+ }
+ }
+}
+
+impl AsMut<Dynamic> for Target<'_> {
+ #[inline(always)]
+ fn as_mut(&mut self) -> &mut Dynamic {
+ self
+ }
+}
+
+impl<T: Into<Dynamic>> From<T> for Target<'_> {
+ #[inline(always)]
+ fn from(value: T) -> Self {
+ Self::TempValue(value.into())
+ }
+}
diff --git a/rhai/src/func/args.rs b/rhai/src/func/args.rs
new file mode 100644
index 0000000..2413c0c
--- /dev/null
+++ b/rhai/src/func/args.rs
@@ -0,0 +1,101 @@
+//! Helper module which defines [`FuncArgs`] to make function calling easier.
+
+#![allow(non_snake_case)]
+
+use crate::types::dynamic::Variant;
+use crate::Dynamic;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// Trait that parses arguments to a function call.
+///
+/// Any data type can implement this trait in order to pass arguments to
+/// [`Engine::call_fn`][crate::Engine::call_fn].
+pub trait FuncArgs {
+ /// Parse function call arguments into a container.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::{Engine, Dynamic, FuncArgs, Scope};
+ ///
+ /// // A struct containing function arguments
+ /// struct Options {
+ /// pub foo: bool,
+ /// pub bar: String,
+ /// pub baz: i64,
+ /// }
+ ///
+ /// impl FuncArgs for Options {
+ /// fn parse<ARGS: Extend<Dynamic>>(self, args: &mut ARGS) {
+ /// args.extend(Some(self.foo.into()));
+ /// args.extend(Some(self.bar.into()));
+ /// args.extend(Some(self.baz.into()));
+ /// }
+ /// }
+ ///
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// # #[cfg(not(feature = "no_function"))]
+ /// # {
+ /// let options = Options { foo: false, bar: "world".to_string(), baz: 42 };
+ ///
+ /// let engine = Engine::new();
+ /// let mut scope = Scope::new();
+ ///
+ /// let ast = engine.compile(
+ /// "
+ /// fn hello(x, y, z) {
+ /// if x { `hello ${y}` } else { y + z }
+ /// }
+ /// ")?;
+ ///
+ /// let result: String = engine.call_fn(&mut scope, &ast, "hello", options)?;
+ ///
+ /// assert_eq!(result, "world42");
+ /// # }
+ /// # Ok(())
+ /// # }
+ /// ```
+ fn parse<ARGS: Extend<Dynamic>>(self, args: &mut ARGS);
+}
+
+impl<T: Variant + Clone> FuncArgs for Vec<T> {
+ #[inline]
+ fn parse<ARGS: Extend<Dynamic>>(self, args: &mut ARGS) {
+ args.extend(self.into_iter().map(Dynamic::from));
+ }
+}
+
+impl<T: Variant + Clone, const N: usize> FuncArgs for [T; N] {
+ #[inline]
+ fn parse<ARGS: Extend<Dynamic>>(self, args: &mut ARGS) {
+ args.extend(IntoIterator::into_iter(self).map(Dynamic::from));
+ }
+}
+
+/// Macro to implement [`FuncArgs`] for tuples of standard types (each can be converted into a [`Dynamic`]).
+macro_rules! impl_args {
+ ($($p:ident),*) => {
+ impl<$($p: Variant + Clone),*> FuncArgs for ($($p,)*)
+ {
+ #[inline]
+ #[allow(unused_variables)]
+ fn parse<ARGS: Extend<Dynamic>>(self, args: &mut ARGS) {
+ let ($($p,)*) = self;
+ $(args.extend(Some(Dynamic::from($p)));)*
+ }
+ }
+
+ impl_args!(@pop $($p),*);
+ };
+ (@pop) => {
+ };
+ (@pop $head:ident) => {
+ impl_args!();
+ };
+ (@pop $head:ident $(, $tail:ident)+) => {
+ impl_args!($($tail),*);
+ };
+}
+
+impl_args!(A, B, C, D, E, F, G, H, J, K, L, M, N, P, Q, R, S, T, U, V);
diff --git a/rhai/src/func/builtin.rs b/rhai/src/func/builtin.rs
new file mode 100644
index 0000000..ca78d79
--- /dev/null
+++ b/rhai/src/func/builtin.rs
@@ -0,0 +1,1035 @@
+//! Built-in implementations for common operators.
+
+#![allow(clippy::float_cmp)]
+
+use super::call::FnCallArgs;
+use super::native::FnBuiltin;
+#[allow(clippy::enum_glob_use)]
+use crate::tokenizer::{Token, Token::*};
+use crate::{
+ Dynamic, ExclusiveRange, ImmutableString, InclusiveRange, NativeCallContext, RhaiResult,
+ SmartString, INT,
+};
+use std::any::TypeId;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(not(feature = "no_float"))]
+use crate::FLOAT;
+
+#[cfg(not(feature = "no_float"))]
+#[cfg(feature = "no_std")]
+use num_traits::Float;
+
+#[cfg(feature = "decimal")]
+use rust_decimal::Decimal;
+
+/// The `unchecked` feature is not active.
+const CHECKED_BUILD: bool = cfg!(not(feature = "unchecked"));
+
+/// A function that returns `true`.
+#[inline(always)]
+#[allow(clippy::unnecessary_wraps)]
+fn const_true_fn(_: Option<NativeCallContext>, _: &mut [&mut Dynamic]) -> RhaiResult {
+ Ok(Dynamic::TRUE)
+}
+/// A function that returns `false`.
+#[inline(always)]
+#[allow(clippy::unnecessary_wraps)]
+fn const_false_fn(_: Option<NativeCallContext>, _: &mut [&mut Dynamic]) -> RhaiResult {
+ Ok(Dynamic::FALSE)
+}
+
+/// Build in common binary operator implementations to avoid the cost of calling a registered function.
+///
+/// The return function will be registered as a _method_, so the first parameter cannot be consumed.
+#[must_use]
+pub fn get_builtin_binary_op_fn(op: &Token, x: &Dynamic, y: &Dynamic) -> Option<FnBuiltin> {
+ let type1 = x.type_id();
+ let type2 = y.type_id();
+
+ macro_rules! impl_op {
+ ($xx:ident $op:tt $yy:ident) => { Some((|_, args| {
+ let x = &*args[0].read_lock::<$xx>().unwrap();
+ let y = &*args[1].read_lock::<$yy>().unwrap();
+ Ok((x $op y).into())
+ }, false)) };
+ ($xx:ident . $func:ident ( $yy:ty )) => { Some((|_, args| {
+ let x = &*args[0].read_lock::<$xx>().unwrap();
+ let y = &*args[1].read_lock::<$yy>().unwrap();
+ Ok(x.$func(y).into())
+ }, false)) };
+ ($xx:ident . $func:ident ( $yy:ident . $yyy:ident () )) => { Some((|_, args| {
+ let x = &*args[0].read_lock::<$xx>().unwrap();
+ let y = &*args[1].read_lock::<$yy>().unwrap();
+ Ok(x.$func(y.$yyy()).into())
+ }, false)) };
+ ($func:ident ( $op:tt )) => { Some((|_, args| {
+ let (x, y) = $func(args);
+ Ok((x $op y).into())
+ }, false)) };
+ ($base:ty => $xx:ident $op:tt $yy:ident) => { Some((|_, args| {
+ let x = args[0].$xx().unwrap() as $base;
+ let y = args[1].$yy().unwrap() as $base;
+ Ok((x $op y).into())
+ }, false)) };
+ ($base:ty => $xx:ident . $func:ident ( $yy:ident as $yyy:ty)) => { Some((|_, args| {
+ let x = args[0].$xx().unwrap() as $base;
+ let y = args[1].$yy().unwrap() as $base;
+ Ok(x.$func(y as $yyy).into())
+ }, false)) };
+ ($base:ty => Ok($func:ident ( $xx:ident, $yy:ident ))) => { Some((|_, args| {
+ let x = args[0].$xx().unwrap() as $base;
+ let y = args[1].$yy().unwrap() as $base;
+ Ok($func(x, y).into())
+ }, false)) };
+ ($base:ty => $func:ident ( $xx:ident, $yy:ident )) => { Some((|_, args| {
+ let x = args[0].$xx().unwrap() as $base;
+ let y = args[1].$yy().unwrap() as $base;
+ $func(x, y).map(Into::into)
+ }, false)) };
+ (from $base:ty => $xx:ident $op:tt $yy:ident) => { Some((|_, args| {
+ let x = <$base>::from(args[0].$xx().unwrap());
+ let y = <$base>::from(args[1].$yy().unwrap());
+ Ok((x $op y).into())
+ }, false)) };
+ (from $base:ty => $xx:ident . $func:ident ( $yy:ident )) => { Some((|_, args| {
+ let x = <$base>::from(args[0].$xx().unwrap());
+ let y = <$base>::from(args[1].$yy().unwrap());
+ Ok(x.$func(y).into())
+ }, false)) };
+ (from $base:ty => Ok($func:ident ( $xx:ident, $yy:ident ))) => { Some((|_, args| {
+ let x = <$base>::from(args[0].$xx().unwrap());
+ let y = <$base>::from(args[1].$yy().unwrap());
+ Ok($func(x, y).into())
+ }, false)) };
+ (from $base:ty => $func:ident ( $xx:ident, $yy:ident )) => { Some((|_, args| {
+ let x = <$base>::from(args[0].$xx().unwrap());
+ let y = <$base>::from(args[1].$yy().unwrap());
+ $func(x, y).map(Into::into)
+ }, false)) };
+ }
+
+ // Check for common patterns
+ if type1 == type2 {
+ if type1 == TypeId::of::<INT>() {
+ #[cfg(not(feature = "unchecked"))]
+ #[allow(clippy::wildcard_imports)]
+ use crate::packages::arithmetic::arith_basic::INT::functions::*;
+
+ #[cfg(not(feature = "unchecked"))]
+ match op {
+ Plus => return impl_op!(INT => add(as_int, as_int)),
+ Minus => return impl_op!(INT => subtract(as_int, as_int)),
+ Multiply => return impl_op!(INT => multiply(as_int, as_int)),
+ Divide => return impl_op!(INT => divide(as_int, as_int)),
+ Modulo => return impl_op!(INT => modulo(as_int, as_int)),
+ PowerOf => return impl_op!(INT => power(as_int, as_int)),
+ RightShift => return impl_op!(INT => Ok(shift_right(as_int, as_int))),
+ LeftShift => return impl_op!(INT => Ok(shift_left(as_int, as_int))),
+ _ => (),
+ }
+
+ #[cfg(feature = "unchecked")]
+ match op {
+ Plus => return impl_op!(INT => as_int + as_int),
+ Minus => return impl_op!(INT => as_int - as_int),
+ Multiply => return impl_op!(INT => as_int * as_int),
+ Divide => return impl_op!(INT => as_int / as_int),
+ Modulo => return impl_op!(INT => as_int % as_int),
+ PowerOf => return impl_op!(INT => as_int.pow(as_int as u32)),
+ RightShift => {
+ return Some((
+ |_, args| {
+ let x = args[0].as_int().unwrap();
+ let y = args[1].as_int().unwrap();
+ Ok((if y < 0 { x << -y } else { x >> y }).into())
+ },
+ false,
+ ))
+ }
+ LeftShift => {
+ return Some((
+ |_, args| {
+ let x = args[0].as_int().unwrap();
+ let y = args[1].as_int().unwrap();
+ Ok((if y < 0 { x >> -y } else { x << y }).into())
+ },
+ false,
+ ))
+ }
+ _ => (),
+ }
+
+ return match op {
+ EqualsTo => impl_op!(INT => as_int == as_int),
+ NotEqualsTo => impl_op!(INT => as_int != as_int),
+ GreaterThan => impl_op!(INT => as_int > as_int),
+ GreaterThanEqualsTo => impl_op!(INT => as_int >= as_int),
+ LessThan => impl_op!(INT => as_int < as_int),
+ LessThanEqualsTo => impl_op!(INT => as_int <= as_int),
+ Ampersand => impl_op!(INT => as_int & as_int),
+ Pipe => impl_op!(INT => as_int | as_int),
+ XOr => impl_op!(INT => as_int ^ as_int),
+ ExclusiveRange => impl_op!(INT => as_int .. as_int),
+ InclusiveRange => impl_op!(INT => as_int ..= as_int),
+ _ => None,
+ };
+ }
+
+ if type1 == TypeId::of::<bool>() {
+ return match op {
+ EqualsTo => impl_op!(bool => as_bool == as_bool),
+ NotEqualsTo => impl_op!(bool => as_bool != as_bool),
+ GreaterThan => impl_op!(bool => as_bool > as_bool),
+ GreaterThanEqualsTo => impl_op!(bool => as_bool >= as_bool),
+ LessThan => impl_op!(bool => as_bool < as_bool),
+ LessThanEqualsTo => impl_op!(bool => as_bool <= as_bool),
+ Ampersand => impl_op!(bool => as_bool & as_bool),
+ Pipe => impl_op!(bool => as_bool | as_bool),
+ XOr => impl_op!(bool => as_bool ^ as_bool),
+ _ => None,
+ };
+ }
+
+ if type1 == TypeId::of::<ImmutableString>() {
+ return match op {
+ Plus => Some((
+ |_ctx, args| {
+ let s1 = &*args[0].read_lock::<ImmutableString>().unwrap();
+ let s2 = &*args[1].read_lock::<ImmutableString>().unwrap();
+
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.unwrap()
+ .engine()
+ .throw_on_size((0, 0, s1.len() + s2.len()))?;
+
+ Ok((s1 + s2).into())
+ },
+ CHECKED_BUILD,
+ )),
+ Minus => impl_op!(ImmutableString - ImmutableString),
+ EqualsTo => impl_op!(ImmutableString == ImmutableString),
+ NotEqualsTo => impl_op!(ImmutableString != ImmutableString),
+ GreaterThan => impl_op!(ImmutableString > ImmutableString),
+ GreaterThanEqualsTo => impl_op!(ImmutableString >= ImmutableString),
+ LessThan => impl_op!(ImmutableString < ImmutableString),
+ LessThanEqualsTo => impl_op!(ImmutableString <= ImmutableString),
+ _ => None,
+ };
+ }
+
+ if type1 == TypeId::of::<char>() {
+ return match op {
+ Plus => Some((
+ |_ctx, args| {
+ let x = args[0].as_char().unwrap();
+ let y = args[1].as_char().unwrap();
+
+ let mut result = SmartString::new_const();
+ result.push(x);
+ result.push(y);
+
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.unwrap().engine().throw_on_size((0, 0, result.len()))?;
+
+ Ok(result.into())
+ },
+ CHECKED_BUILD,
+ )),
+ EqualsTo => impl_op!(char => as_char == as_char),
+ NotEqualsTo => impl_op!(char => as_char != as_char),
+ GreaterThan => impl_op!(char => as_char > as_char),
+ GreaterThanEqualsTo => impl_op!(char => as_char >= as_char),
+ LessThan => impl_op!(char => as_char < as_char),
+ LessThanEqualsTo => impl_op!(char => as_char <= as_char),
+ _ => None,
+ };
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ if type1 == TypeId::of::<crate::Blob>() {
+ use crate::Blob;
+
+ return match op {
+ Plus => Some((
+ |_ctx, args| {
+ let b2 = &*args[1].read_lock::<Blob>().unwrap();
+ if b2.is_empty() {
+ return Ok(args[0].flatten_clone());
+ }
+ let b1 = &*args[0].read_lock::<Blob>().unwrap();
+ if b1.is_empty() {
+ return Ok(args[1].flatten_clone());
+ }
+
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.unwrap()
+ .engine()
+ .throw_on_size((b1.len() + b2.len(), 0, 0))?;
+
+ let mut blob = b1.clone();
+ blob.extend(b2);
+ Ok(Dynamic::from_blob(blob))
+ },
+ CHECKED_BUILD,
+ )),
+ EqualsTo => impl_op!(Blob == Blob),
+ NotEqualsTo => impl_op!(Blob != Blob),
+ _ => None,
+ };
+ }
+
+ if type1 == TypeId::of::<()>() {
+ return match op {
+ EqualsTo => Some((const_true_fn, false)),
+ NotEqualsTo | GreaterThan | GreaterThanEqualsTo | LessThan | LessThanEqualsTo => {
+ Some((const_false_fn, false))
+ }
+ _ => None,
+ };
+ }
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ macro_rules! impl_float {
+ ($x:ty, $xx:ident, $y:ty, $yy:ident) => {
+ if (type1, type2) == (TypeId::of::<$x>(), TypeId::of::<$y>()) {
+ return match op {
+ Plus => impl_op!(FLOAT => $xx + $yy),
+ Minus => impl_op!(FLOAT => $xx - $yy),
+ Multiply => impl_op!(FLOAT => $xx * $yy),
+ Divide => impl_op!(FLOAT => $xx / $yy),
+ Modulo => impl_op!(FLOAT => $xx % $yy),
+ PowerOf => impl_op!(FLOAT => $xx.powf($yy as FLOAT)),
+ EqualsTo => impl_op!(FLOAT => $xx == $yy),
+ NotEqualsTo => impl_op!(FLOAT => $xx != $yy),
+ GreaterThan => impl_op!(FLOAT => $xx > $yy),
+ GreaterThanEqualsTo => impl_op!(FLOAT => $xx >= $yy),
+ LessThan => impl_op!(FLOAT => $xx < $yy),
+ LessThanEqualsTo => impl_op!(FLOAT => $xx <= $yy),
+ _ => None,
+ };
+ }
+ };
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ {
+ impl_float!(FLOAT, as_float, FLOAT, as_float);
+ impl_float!(FLOAT, as_float, INT, as_int);
+ impl_float!(INT, as_int, FLOAT, as_float);
+ }
+
+ #[cfg(feature = "decimal")]
+ macro_rules! impl_decimal {
+ ($x:ty, $xx:ident, $y:ty, $yy:ident) => {
+ if (type1, type2) == (TypeId::of::<$x>(), TypeId::of::<$y>()) {
+ #[cfg(not(feature = "unchecked"))]
+ #[allow(clippy::wildcard_imports)]
+ use crate::packages::arithmetic::decimal_functions::builtin::*;
+
+ #[cfg(not(feature = "unchecked"))]
+ match op {
+ Plus => return impl_op!(from Decimal => add($xx, $yy)),
+ Minus => return impl_op!(from Decimal => subtract($xx, $yy)),
+ Multiply => return impl_op!(from Decimal => multiply($xx, $yy)),
+ Divide => return impl_op!(from Decimal => divide($xx, $yy)),
+ Modulo => return impl_op!(from Decimal => modulo($xx, $yy)),
+ PowerOf => return impl_op!(from Decimal => power($xx, $yy)),
+ _ => ()
+ }
+
+ #[cfg(feature = "unchecked")]
+ use rust_decimal::MathematicalOps;
+
+ #[cfg(feature = "unchecked")]
+ match op {
+ Plus => return impl_op!(from Decimal => $xx + $yy),
+ Minus => return impl_op!(from Decimal => $xx - $yy),
+ Multiply => return impl_op!(from Decimal => $xx * $yy),
+ Divide => return impl_op!(from Decimal => $xx / $yy),
+ Modulo => return impl_op!(from Decimal => $xx % $yy),
+ PowerOf => return impl_op!(from Decimal => $xx.powd($yy)),
+ _ => ()
+ }
+
+ return match op {
+ EqualsTo => impl_op!(from Decimal => $xx == $yy),
+ NotEqualsTo => impl_op!(from Decimal => $xx != $yy),
+ GreaterThan => impl_op!(from Decimal => $xx > $yy),
+ GreaterThanEqualsTo => impl_op!(from Decimal => $xx >= $yy),
+ LessThan => impl_op!(from Decimal => $xx < $yy),
+ LessThanEqualsTo => impl_op!(from Decimal => $xx <= $yy),
+ _ => None
+ };
+ }
+ };
+ }
+
+ #[cfg(feature = "decimal")]
+ {
+ impl_decimal!(Decimal, as_decimal, Decimal, as_decimal);
+ impl_decimal!(Decimal, as_decimal, INT, as_int);
+ impl_decimal!(INT, as_int, Decimal, as_decimal);
+ }
+
+ // char op string
+ if (type1, type2) == (TypeId::of::<char>(), TypeId::of::<ImmutableString>()) {
+ fn get_s1s2(args: &FnCallArgs) -> ([char; 2], [char; 2]) {
+ let x = args[0].as_char().unwrap();
+ let y = &*args[1].read_lock::<ImmutableString>().unwrap();
+ let s1 = [x, '\0'];
+ let mut y = y.chars();
+ let s2 = [y.next().unwrap_or('\0'), y.next().unwrap_or('\0')];
+ (s1, s2)
+ }
+
+ return match op {
+ Plus => Some((
+ |_ctx, args| {
+ let x = args[0].as_char().unwrap();
+ let y = &*args[1].read_lock::<ImmutableString>().unwrap();
+
+ let mut result = SmartString::new_const();
+ result.push(x);
+ result.push_str(y);
+
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.unwrap().engine().throw_on_size((0, 0, result.len()))?;
+
+ Ok(result.into())
+ },
+ CHECKED_BUILD,
+ )),
+ EqualsTo => impl_op!(get_s1s2(==)),
+ NotEqualsTo => impl_op!(get_s1s2(!=)),
+ GreaterThan => impl_op!(get_s1s2(>)),
+ GreaterThanEqualsTo => impl_op!(get_s1s2(>=)),
+ LessThan => impl_op!(get_s1s2(<)),
+ LessThanEqualsTo => impl_op!(get_s1s2(<=)),
+ _ => None,
+ };
+ }
+ // string op char
+ if (type1, type2) == (TypeId::of::<ImmutableString>(), TypeId::of::<char>()) {
+ fn get_s1s2(args: &FnCallArgs) -> ([char; 2], [char; 2]) {
+ let x = &*args[0].read_lock::<ImmutableString>().unwrap();
+ let y = args[1].as_char().unwrap();
+ let mut x = x.chars();
+ let s1 = [x.next().unwrap_or('\0'), x.next().unwrap_or('\0')];
+ let s2 = [y, '\0'];
+ (s1, s2)
+ }
+
+ return match op {
+ Plus => Some((
+ |_ctx, args| {
+ let x = &*args[0].read_lock::<ImmutableString>().unwrap();
+ let y = args[1].as_char().unwrap();
+ let result = x + y;
+
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.unwrap().engine().throw_on_size((0, 0, result.len()))?;
+
+ Ok(result.into())
+ },
+ CHECKED_BUILD,
+ )),
+ Minus => Some((
+ |_, args| {
+ let x = &*args[0].read_lock::<ImmutableString>().unwrap();
+ let y = args[1].as_char().unwrap();
+ Ok((x - y).into())
+ },
+ false,
+ )),
+ EqualsTo => impl_op!(get_s1s2(==)),
+ NotEqualsTo => impl_op!(get_s1s2(!=)),
+ GreaterThan => impl_op!(get_s1s2(>)),
+ GreaterThanEqualsTo => impl_op!(get_s1s2(>=)),
+ LessThan => impl_op!(get_s1s2(<)),
+ LessThanEqualsTo => impl_op!(get_s1s2(<=)),
+ _ => None,
+ };
+ }
+ // () op string
+ if (type1, type2) == (TypeId::of::<()>(), TypeId::of::<ImmutableString>()) {
+ return match op {
+ Plus => Some((|_, args| Ok(args[1].clone()), false)),
+ EqualsTo | GreaterThan | GreaterThanEqualsTo | LessThan | LessThanEqualsTo => {
+ Some((const_false_fn, false))
+ }
+ NotEqualsTo => Some((const_true_fn, false)),
+ _ => None,
+ };
+ }
+ // string op ()
+ if (type1, type2) == (TypeId::of::<ImmutableString>(), TypeId::of::<()>()) {
+ return match op {
+ Plus => Some((|_, args| Ok(args[0].clone()), false)),
+ EqualsTo | GreaterThan | GreaterThanEqualsTo | LessThan | LessThanEqualsTo => {
+ Some((const_false_fn, false))
+ }
+ NotEqualsTo => Some((const_true_fn, false)),
+ _ => None,
+ };
+ }
+
+ // blob
+ #[cfg(not(feature = "no_index"))]
+ if type1 == TypeId::of::<crate::Blob>() {
+ use crate::Blob;
+
+ if type2 == TypeId::of::<char>() {
+ return match op {
+ Plus => Some((
+ |_ctx, args| {
+ let mut blob = args[0].read_lock::<Blob>().unwrap().clone();
+ let mut buf = [0_u8; 4];
+ let x = args[1].as_char().unwrap().encode_utf8(&mut buf);
+
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.unwrap()
+ .engine()
+ .throw_on_size((blob.len() + x.len(), 0, 0))?;
+
+ blob.extend(x.as_bytes());
+ Ok(Dynamic::from_blob(blob))
+ },
+ CHECKED_BUILD,
+ )),
+ _ => None,
+ };
+ }
+ }
+
+ // Non-compatible ranges
+ if (type1, type2)
+ == (
+ TypeId::of::<ExclusiveRange>(),
+ TypeId::of::<InclusiveRange>(),
+ )
+ || (type1, type2)
+ == (
+ TypeId::of::<InclusiveRange>(),
+ TypeId::of::<ExclusiveRange>(),
+ )
+ {
+ return match op {
+ NotEqualsTo => Some((const_true_fn, false)),
+ Equals => Some((const_false_fn, false)),
+ _ => None,
+ };
+ }
+
+ // Handle ranges here because ranges are implemented as custom type
+ if type1 == TypeId::of::<ExclusiveRange>() && type1 == type2 {
+ return match op {
+ EqualsTo => impl_op!(ExclusiveRange == ExclusiveRange),
+ NotEqualsTo => impl_op!(ExclusiveRange != ExclusiveRange),
+ _ => None,
+ };
+ }
+
+ if type1 == TypeId::of::<InclusiveRange>() && type1 == type2 {
+ return match op {
+ EqualsTo => impl_op!(InclusiveRange == InclusiveRange),
+ NotEqualsTo => impl_op!(InclusiveRange != InclusiveRange),
+ _ => None,
+ };
+ }
+
+ // One of the operands is a custom type, so it is never built-in
+ if x.is_variant() || y.is_variant() {
+ return None;
+ }
+
+ // Default comparison operators for different types
+ if type2 != type1 {
+ return match op {
+ NotEqualsTo => Some((const_true_fn, false)),
+ EqualsTo | GreaterThan | GreaterThanEqualsTo | LessThan | LessThanEqualsTo => {
+ Some((const_false_fn, false))
+ }
+ _ => None,
+ };
+ }
+
+ // Beyond here, type1 == type2
+ None
+}
+
+/// Build in common operator assignment implementations to avoid the cost of calling a registered function.
+///
+/// The return function is registered as a _method_, so the first parameter cannot be consumed.
+#[must_use]
+pub fn get_builtin_op_assignment_fn(op: &Token, x: &Dynamic, y: &Dynamic) -> Option<FnBuiltin> {
+ let type1 = x.type_id();
+ let type2 = y.type_id();
+
+ macro_rules! impl_op {
+ ($x:ty = x $op:tt $yy:ident) => { Some((|_, args| {
+ let x = args[0].$yy().unwrap();
+ let y = args[1].$yy().unwrap() as $x;
+ Ok((*args[0].write_lock::<$x>().unwrap() = x $op y).into())
+ }, false)) };
+ ($x:ident $op:tt $yy:ident) => { Some((|_, args| {
+ let y = args[1].$yy().unwrap() as $x;
+ Ok((*args[0].write_lock::<$x>().unwrap() $op y).into())
+ }, false)) };
+ ($x:ident $op:tt $yy:ident as $yyy:ty) => { Some((|_, args| {
+ let y = args[1].$yy().unwrap() as $yyy;
+ Ok((*args[0].write_lock::<$x>().unwrap() $op y).into())
+ }, false)) };
+ ($x:ty => $xx:ident . $func:ident ( $yy:ident as $yyy:ty )) => { Some((|_, args| {
+ let x = args[0].$xx().unwrap();
+ let y = args[1].$yy().unwrap() as $x;
+ Ok((*args[0].write_lock::<$x>().unwrap() = x.$func(y as $yyy)).into())
+ }, false)) };
+ ($x:ty => Ok($func:ident ( $xx:ident, $yy:ident ))) => { Some((|_, args| {
+ let x = args[0].$xx().unwrap();
+ let y = args[1].$yy().unwrap() as $x;
+ let v: Dynamic = $func(x, y).into();
+ Ok((*args[0].write_lock().unwrap() = v).into())
+ }, false)) };
+ ($x:ty => $func:ident ( $xx:ident, $yy:ident )) => { Some((|_, args| {
+ let x = args[0].$xx().unwrap();
+ let y = args[1].$yy().unwrap() as $x;
+ Ok((*args[0].write_lock().unwrap() = $func(x, y)?).into())
+ }, false)) };
+ (from $x:ident $op:tt $yy:ident) => { Some((|_, args| {
+ let y = <$x>::from(args[1].$yy().unwrap());
+ Ok((*args[0].write_lock::<$x>().unwrap() $op y).into())
+ }, false)) };
+ (from $x:ty => $xx:ident . $func:ident ( $yy:ident )) => { Some((|_, args| {
+ let x = args[0].$xx().unwrap();
+ let y = <$x>::from(args[1].$yy().unwrap());
+ Ok((*args[0].write_lock::<$x>().unwrap() = x.$func(y)).into())
+ }, false)) };
+ (from $x:ty => Ok($func:ident ( $xx:ident, $yy:ident ))) => { Some((|_, args| {
+ let x = args[0].$xx().unwrap();
+ let y = <$x>::from(args[1].$yy().unwrap());
+ Ok((*args[0].write_lock().unwrap() = $func(x, y).into()).into())
+ }, false)) };
+ (from $x:ty => $func:ident ( $xx:ident, $yy:ident )) => { Some((|_, args| {
+ let x = args[0].$xx().unwrap();
+ let y = <$x>::from(args[1].$yy().unwrap());
+ Ok((*args[0].write_lock().unwrap() = $func(x, y)?).into())
+ }, false)) };
+ }
+
+ // Check for common patterns
+ if type1 == type2 {
+ if type1 == TypeId::of::<INT>() {
+ #[cfg(not(feature = "unchecked"))]
+ #[allow(clippy::wildcard_imports)]
+ use crate::packages::arithmetic::arith_basic::INT::functions::*;
+
+ #[cfg(not(feature = "unchecked"))]
+ match op {
+ PlusAssign => return impl_op!(INT => add(as_int, as_int)),
+ MinusAssign => return impl_op!(INT => subtract(as_int, as_int)),
+ MultiplyAssign => return impl_op!(INT => multiply(as_int, as_int)),
+ DivideAssign => return impl_op!(INT => divide(as_int, as_int)),
+ ModuloAssign => return impl_op!(INT => modulo(as_int, as_int)),
+ PowerOfAssign => return impl_op!(INT => power(as_int, as_int)),
+ RightShiftAssign => return impl_op!(INT => Ok(shift_right(as_int, as_int))),
+ LeftShiftAssign => return impl_op!(INT => Ok(shift_left(as_int, as_int))),
+ _ => (),
+ }
+
+ #[cfg(feature = "unchecked")]
+ match op {
+ PlusAssign => return impl_op!(INT += as_int),
+ MinusAssign => return impl_op!(INT -= as_int),
+ MultiplyAssign => return impl_op!(INT *= as_int),
+ DivideAssign => return impl_op!(INT /= as_int),
+ ModuloAssign => return impl_op!(INT %= as_int),
+ PowerOfAssign => return impl_op!(INT => as_int.pow(as_int as u32)),
+ RightShiftAssign => {
+ return Some((
+ |_, args| {
+ let x = args[0].as_int().unwrap();
+ let y = args[1].as_int().unwrap();
+ let v = if y < 0 { x << -y } else { x >> y };
+ Ok((*args[0].write_lock::<Dynamic>().unwrap() = v.into()).into())
+ },
+ false,
+ ))
+ }
+ LeftShiftAssign => {
+ return Some((
+ |_, args| {
+ let x = args[0].as_int().unwrap();
+ let y = args[1].as_int().unwrap();
+ let v = if y < 0 { x >> -y } else { x << y };
+ Ok((*args[0].write_lock::<Dynamic>().unwrap() = v.into()).into())
+ },
+ false,
+ ))
+ }
+ _ => (),
+ }
+
+ return match op {
+ AndAssign => impl_op!(INT &= as_int),
+ OrAssign => impl_op!(INT |= as_int),
+ XOrAssign => impl_op!(INT ^= as_int),
+ _ => None,
+ };
+ }
+
+ if type1 == TypeId::of::<bool>() {
+ return match op {
+ AndAssign => impl_op!(bool = x && as_bool),
+ OrAssign => impl_op!(bool = x || as_bool),
+ XOrAssign => impl_op!(bool = x ^ as_bool),
+ _ => None,
+ };
+ }
+
+ if type1 == TypeId::of::<char>() {
+ return match op {
+ PlusAssign => Some((
+ |_, args| {
+ let y = args[1].as_char().unwrap();
+ let x = &mut *args[0].write_lock::<Dynamic>().unwrap();
+
+ let mut buf = SmartString::new_const();
+ buf.push(x.as_char().unwrap());
+ buf.push(y);
+
+ Ok((*x = buf.into()).into())
+ },
+ false,
+ )),
+ _ => None,
+ };
+ }
+
+ if type1 == TypeId::of::<ImmutableString>() {
+ return match op {
+ PlusAssign => Some((
+ |_ctx, args| {
+ let (first, second) = args.split_first_mut().unwrap();
+ let x = &mut *first.write_lock::<ImmutableString>().unwrap();
+ let y = &*second[0].read_lock::<ImmutableString>().unwrap();
+
+ #[cfg(not(feature = "unchecked"))]
+ if !x.is_empty() && !y.is_empty() {
+ let total_len = x.len() + y.len();
+ _ctx.unwrap().engine().throw_on_size((0, 0, total_len))?;
+ }
+
+ Ok((*x += y).into())
+ },
+ CHECKED_BUILD,
+ )),
+ MinusAssign => Some((
+ |_, args| {
+ let (first, second) = args.split_first_mut().unwrap();
+ let x = &mut *first.write_lock::<ImmutableString>().unwrap();
+ let y = &*second[0].read_lock::<ImmutableString>().unwrap();
+ Ok((*x -= y).into())
+ },
+ false,
+ )),
+ _ => None,
+ };
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ if type1 == TypeId::of::<crate::Array>() {
+ #[allow(clippy::wildcard_imports)]
+ use crate::packages::array_basic::array_functions::*;
+ use crate::Array;
+
+ return match op {
+ PlusAssign => Some((
+ |_ctx, args| {
+ let x = args[1].take().into_array().unwrap();
+
+ if x.is_empty() {
+ return Ok(Dynamic::UNIT);
+ }
+
+ let _array_is_empty = args[0].read_lock::<Array>().unwrap().is_empty();
+
+ #[cfg(not(feature = "unchecked"))]
+ if !_array_is_empty {
+ _ctx.unwrap().engine().check_data_size(
+ &*args[0].read_lock().unwrap(),
+ crate::Position::NONE,
+ )?;
+ }
+
+ let array = &mut *args[0].write_lock::<Array>().unwrap();
+
+ Ok(append(array, x).into())
+ },
+ CHECKED_BUILD,
+ )),
+ _ => None,
+ };
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ if type1 == TypeId::of::<crate::Blob>() {
+ #[allow(clippy::wildcard_imports)]
+ use crate::packages::blob_basic::blob_functions::*;
+ use crate::Blob;
+
+ return match op {
+ PlusAssign => Some((
+ |_ctx, args| {
+ let blob2 = args[1].take().into_blob().unwrap();
+ let blob1 = &mut *args[0].write_lock::<Blob>().unwrap();
+
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.unwrap()
+ .engine()
+ .throw_on_size((blob1.len() + blob2.len(), 0, 0))?;
+
+ Ok(append(blob1, blob2).into())
+ },
+ CHECKED_BUILD,
+ )),
+ _ => None,
+ };
+ }
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ macro_rules! impl_float {
+ ($x:ident, $xx:ident, $y:ty, $yy:ident) => {
+ if (type1, type2) == (TypeId::of::<$x>(), TypeId::of::<$y>()) {
+ return match op {
+ PlusAssign => impl_op!($x += $yy),
+ MinusAssign => impl_op!($x -= $yy),
+ MultiplyAssign => impl_op!($x *= $yy),
+ DivideAssign => impl_op!($x /= $yy),
+ ModuloAssign => impl_op!($x %= $yy),
+ PowerOfAssign => impl_op!($x => $xx.powf($yy as $x)),
+ _ => None,
+ };
+ }
+ }
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ {
+ impl_float!(FLOAT, as_float, FLOAT, as_float);
+ impl_float!(FLOAT, as_float, INT, as_int);
+ }
+
+ #[cfg(feature = "decimal")]
+ macro_rules! impl_decimal {
+ ($x:ident, $xx:ident, $y:ty, $yy:ident) => {
+ if (type1, type2) == (TypeId::of::<$x>(), TypeId::of::<$y>()) {
+ #[cfg(not(feature = "unchecked"))]
+ #[allow(clippy::wildcard_imports)]
+ use crate::packages::arithmetic::decimal_functions::builtin::*;
+
+ #[cfg(not(feature = "unchecked"))]
+ return match op {
+ PlusAssign => impl_op!(from $x => add($xx, $yy)),
+ MinusAssign => impl_op!(from $x => subtract($xx, $yy)),
+ MultiplyAssign => impl_op!(from $x => multiply($xx, $yy)),
+ DivideAssign => impl_op!(from $x => divide($xx, $yy)),
+ ModuloAssign => impl_op!(from $x => modulo($xx, $yy)),
+ PowerOfAssign => impl_op!(from $x => power($xx, $yy)),
+ _ => None,
+ };
+
+ #[cfg(feature = "unchecked")]
+ use rust_decimal::MathematicalOps;
+
+ #[cfg(feature = "unchecked")]
+ return match op {
+ PlusAssign => impl_op!(from $x += $yy),
+ MinusAssign => impl_op!(from $x -= $yy),
+ MultiplyAssign => impl_op!(from $x *= $yy),
+ DivideAssign => impl_op!(from $x /= $yy),
+ ModuloAssign => impl_op!(from $x %= $yy),
+ PowerOfAssign => impl_op!(from $x => $xx.powd($yy)),
+ _ => None,
+ };
+ }
+ };
+ }
+
+ #[cfg(feature = "decimal")]
+ {
+ impl_decimal!(Decimal, as_decimal, Decimal, as_decimal);
+ impl_decimal!(Decimal, as_decimal, INT, as_int);
+ }
+
+ // string op= char
+ if (type1, type2) == (TypeId::of::<ImmutableString>(), TypeId::of::<char>()) {
+ return match op {
+ PlusAssign => Some((
+ |_ctx, args| {
+ let mut buf = [0_u8; 4];
+ let ch = &*args[1].as_char().unwrap().encode_utf8(&mut buf);
+ let mut x = args[0].write_lock::<ImmutableString>().unwrap();
+
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.unwrap()
+ .engine()
+ .throw_on_size((0, 0, x.len() + ch.len()))?;
+
+ Ok((*x += ch).into())
+ },
+ CHECKED_BUILD,
+ )),
+ MinusAssign => impl_op!(ImmutableString -= as_char as char),
+ _ => None,
+ };
+ }
+ // char op= string
+ if (type1, type2) == (TypeId::of::<char>(), TypeId::of::<ImmutableString>()) {
+ return match op {
+ PlusAssign => Some((
+ |_ctx, args| {
+ let ch = {
+ let s = &*args[1].read_lock::<ImmutableString>().unwrap();
+
+ if s.is_empty() {
+ return Ok(Dynamic::UNIT);
+ }
+
+ let mut ch = args[0].as_char().unwrap().to_string();
+
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.unwrap()
+ .engine()
+ .throw_on_size((0, 0, ch.len() + s.len()))?;
+
+ ch.push_str(s);
+ ch
+ };
+
+ *args[0].write_lock::<Dynamic>().unwrap() = ch.into();
+
+ Ok(Dynamic::UNIT)
+ },
+ CHECKED_BUILD,
+ )),
+ _ => None,
+ };
+ }
+
+ // array op= any
+ #[cfg(not(feature = "no_index"))]
+ if type1 == TypeId::of::<crate::Array>() {
+ #[allow(clippy::wildcard_imports)]
+ use crate::packages::array_basic::array_functions::*;
+ use crate::Array;
+
+ return match op {
+ PlusAssign => Some((
+ |_ctx, args| {
+ {
+ let x = args[1].take();
+ let array = &mut *args[0].write_lock::<Array>().unwrap();
+ push(array, x);
+ }
+
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.unwrap()
+ .engine()
+ .check_data_size(&*args[0].read_lock().unwrap(), crate::Position::NONE)?;
+
+ Ok(Dynamic::UNIT)
+ },
+ CHECKED_BUILD,
+ )),
+ _ => None,
+ };
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ {
+ use crate::Blob;
+
+ // blob op= int
+ if (type1, type2) == (TypeId::of::<Blob>(), TypeId::of::<INT>()) {
+ #[allow(clippy::wildcard_imports)]
+ use crate::packages::blob_basic::blob_functions::*;
+
+ return match op {
+ PlusAssign => Some((
+ |_ctx, args| {
+ let x = args[1].as_int().unwrap();
+ let blob = &mut *args[0].write_lock::<Blob>().unwrap();
+
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.unwrap()
+ .engine()
+ .throw_on_size((blob.len() + 1, 0, 0))?;
+
+ Ok(push(blob, x).into())
+ },
+ CHECKED_BUILD,
+ )),
+ _ => None,
+ };
+ }
+
+ // blob op= char
+ if (type1, type2) == (TypeId::of::<Blob>(), TypeId::of::<char>()) {
+ #[allow(clippy::wildcard_imports)]
+ use crate::packages::blob_basic::blob_functions::*;
+
+ return match op {
+ PlusAssign => Some((
+ |_ctx, args| {
+ let x = args[1].as_char().unwrap();
+ let blob = &mut *args[0].write_lock::<Blob>().unwrap();
+
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.unwrap()
+ .engine()
+ .throw_on_size((blob.len() + 1, 0, 0))?;
+
+ Ok(append_char(blob, x).into())
+ },
+ CHECKED_BUILD,
+ )),
+ _ => None,
+ };
+ }
+
+ // blob op= string
+ if (type1, type2) == (TypeId::of::<Blob>(), TypeId::of::<ImmutableString>()) {
+ #[allow(clippy::wildcard_imports)]
+ use crate::packages::blob_basic::blob_functions::*;
+
+ return match op {
+ PlusAssign => Some((
+ |_ctx, args| {
+ let (first, second) = args.split_first_mut().unwrap();
+ let blob = &mut *first.write_lock::<Blob>().unwrap();
+ let s = &*second[0].read_lock::<ImmutableString>().unwrap();
+
+ if s.is_empty() {
+ return Ok(Dynamic::UNIT);
+ }
+
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.unwrap()
+ .engine()
+ .throw_on_size((blob.len() + s.len(), 0, 0))?;
+
+ Ok(append_str(blob, s).into())
+ },
+ CHECKED_BUILD,
+ )),
+ _ => None,
+ };
+ }
+ }
+
+ None
+}
diff --git a/rhai/src/func/call.rs b/rhai/src/func/call.rs
new file mode 100644
index 0000000..9e7e153
--- /dev/null
+++ b/rhai/src/func/call.rs
@@ -0,0 +1,1813 @@
+//! Implement function-calling mechanism for [`Engine`].
+
+use super::{get_builtin_binary_op_fn, get_builtin_op_assignment_fn, CallableFunction};
+use crate::api::default_limits::MAX_DYNAMIC_PARAMETERS;
+use crate::ast::{Expr, FnCallExpr, FnCallHashes};
+use crate::engine::{
+ KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL, KEYWORD_FN_PTR_CURRY,
+ KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, KEYWORD_TYPE_OF,
+};
+use crate::eval::{Caches, FnResolutionCacheEntry, GlobalRuntimeState};
+use crate::tokenizer::{is_valid_function_name, Token};
+use crate::types::dynamic::Union;
+use crate::{
+ calc_fn_hash, calc_fn_hash_full, Dynamic, Engine, FnArgsVec, FnPtr, ImmutableString,
+ OptimizationLevel, Position, RhaiResult, RhaiResultOf, Scope, Shared, ERR,
+};
+#[cfg(feature = "no_std")]
+use hashbrown::hash_map::Entry;
+#[cfg(not(feature = "no_std"))]
+use std::collections::hash_map::Entry;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ any::{type_name, TypeId},
+ convert::TryFrom,
+ mem,
+};
+
+/// Arguments to a function call, which is a list of [`&mut Dynamic`][Dynamic].
+pub type FnCallArgs<'a> = [&'a mut Dynamic];
+
+/// A type that temporarily stores a mutable reference to a `Dynamic`,
+/// replacing it with a cloned copy.
+#[derive(Debug)]
+struct ArgBackup<'a> {
+ orig_mut: Option<&'a mut Dynamic>,
+ value_copy: Dynamic,
+}
+
+impl<'a> ArgBackup<'a> {
+ /// Create a new `ArgBackup`.
+ #[inline(always)]
+ pub fn new() -> Self {
+ Self {
+ orig_mut: None,
+ value_copy: Dynamic::UNIT,
+ }
+ }
+ /// This function replaces the first argument of a method call with a clone copy.
+ /// This is to prevent a pure function unintentionally consuming the first argument.
+ ///
+ /// `restore_first_arg` must be called before the end of the scope to prevent the shorter
+ /// lifetime from leaking.
+ ///
+ /// # Safety
+ ///
+ /// This method blindly casts a reference to another lifetime, which saves allocation and
+ /// string cloning.
+ ///
+ /// As long as `restore_first_arg` is called before the end of the scope, the shorter lifetime
+ /// will not leak.
+ ///
+ /// # Panics
+ ///
+ /// Panics when `args` is empty.
+ #[inline(always)]
+ pub fn change_first_arg_to_copy(&mut self, args: &mut FnCallArgs<'a>) {
+ // Clone the original value.
+ self.value_copy = args[0].clone();
+
+ // Replace the first reference with a reference to the clone, force-casting the lifetime.
+ // Must remember to restore it later with `restore_first_arg`.
+ //
+ // SAFETY:
+ //
+ // Blindly casting a reference to another lifetime saves allocation and string cloning,
+ // but must be used with the utmost care.
+ //
+ // We can do this here because, before the end of this scope, we'd restore the original
+ // reference via `restore_first_arg`. Therefore this shorter lifetime does not leak.
+ self.orig_mut = Some(mem::replace(&mut args[0], unsafe {
+ mem::transmute(&mut self.value_copy)
+ }));
+ }
+ /// This function restores the first argument that was replaced by `change_first_arg_to_copy`.
+ ///
+ /// # Safety
+ ///
+ /// If `change_first_arg_to_copy` has been called, this function **MUST** be called _BEFORE_
+ /// exiting the current scope. Otherwise it is undefined behavior as the shorter lifetime will leak.
+ #[inline(always)]
+ pub fn restore_first_arg(&mut self, args: &mut FnCallArgs<'a>) {
+ args[0] = self.orig_mut.take().expect("`Some`");
+ }
+}
+
+impl Drop for ArgBackup<'_> {
+ #[inline(always)]
+ fn drop(&mut self) {
+ // Panic if the shorter lifetime leaks.
+ assert!(
+ self.orig_mut.is_none(),
+ "ArgBackup::restore_first_arg has not been called prior to existing this scope"
+ );
+ }
+}
+
+// Ensure no data races in function call arguments.
+#[cfg(not(feature = "no_closure"))]
+#[inline]
+pub fn ensure_no_data_race(fn_name: &str, args: &FnCallArgs, is_ref_mut: bool) -> RhaiResultOf<()> {
+ if let Some((n, ..)) = args
+ .iter()
+ .enumerate()
+ .skip(usize::from(is_ref_mut))
+ .find(|(.., a)| a.is_locked())
+ {
+ return Err(ERR::ErrorDataRace(
+ format!("argument #{} of function '{fn_name}'", n + 1),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ Ok(())
+}
+
+/// Is a function name an anonymous function?
+#[cfg(not(feature = "no_function"))]
+#[inline]
+#[must_use]
+pub fn is_anonymous_fn(name: &str) -> bool {
+ name.starts_with(crate::engine::FN_ANONYMOUS)
+}
+
+impl Engine {
+ /// Generate the signature for a function call.
+ #[inline]
+ #[must_use]
+ fn gen_fn_call_signature(&self, fn_name: &str, args: &[&mut Dynamic]) -> String {
+ format!(
+ "{fn_name} ({})",
+ args.iter()
+ .map(|a| if a.is_string() {
+ "&str | ImmutableString | String"
+ } else {
+ self.map_type_name(a.type_name())
+ })
+ .collect::<FnArgsVec<_>>()
+ .join(", ")
+ )
+ }
+
+ /// Resolve a normal (non-qualified) function call.
+ ///
+ /// Search order:
+ /// 1) AST - script functions in the AST
+ /// 2) Global namespace - functions registered via `Engine::register_XXX`
+ /// 3) Global registered modules - packages
+ /// 4) Imported modules - functions marked with global namespace
+ /// 5) Static registered modules
+ #[must_use]
+ fn resolve_fn<'s>(
+ &self,
+ _global: &GlobalRuntimeState,
+ caches: &'s mut Caches,
+ local_entry: &'s mut Option<FnResolutionCacheEntry>,
+ op_token: Option<&Token>,
+ hash_base: u64,
+ args: Option<&mut FnCallArgs>,
+ allow_dynamic: bool,
+ ) -> Option<&'s FnResolutionCacheEntry> {
+ let mut hash = args.as_deref().map_or(hash_base, |args| {
+ calc_fn_hash_full(hash_base, args.iter().map(|a| a.type_id()))
+ });
+
+ let cache = caches.fn_resolution_cache_mut();
+
+ match cache.map.entry(hash) {
+ Entry::Occupied(entry) => entry.into_mut().as_ref(),
+ Entry::Vacant(entry) => {
+ let num_args = args.as_deref().map_or(0, FnCallArgs::len);
+ let mut max_bitmask = 0; // One above maximum bitmask based on number of parameters.
+ // Set later when a specific matching function is not found.
+ let mut bitmask = 1usize; // Bitmask of which parameter to replace with `Dynamic`
+
+ loop {
+ #[cfg(not(feature = "no_function"))]
+ let func = _global
+ .lib
+ .iter()
+ .rev()
+ .chain(self.global_modules.iter())
+ .find_map(|m| m.get_fn(hash).map(|f| (f, m.id_raw())));
+ #[cfg(feature = "no_function")]
+ let func = None;
+
+ let func = func.or_else(|| {
+ self.global_modules
+ .iter()
+ .find_map(|m| m.get_fn(hash).map(|f| (f, m.id_raw())))
+ });
+
+ #[cfg(not(feature = "no_module"))]
+ let func = func
+ .or_else(|| _global.get_qualified_fn(hash, true))
+ .or_else(|| {
+ self.global_sub_modules
+ .as_ref()
+ .into_iter()
+ .flatten()
+ .filter(|(_, m)| m.contains_indexed_global_functions())
+ .find_map(|(_, m)| {
+ m.get_qualified_fn(hash).map(|f| (f, m.id_raw()))
+ })
+ });
+
+ if let Some((f, s)) = func {
+ // Specific version found
+ let new_entry = FnResolutionCacheEntry {
+ func: f.clone(),
+ source: s.cloned(),
+ };
+ return if cache.filter.is_absent_and_set(hash) {
+ // Do not cache "one-hit wonders"
+ *local_entry = Some(new_entry);
+ local_entry.as_ref()
+ } else {
+ // Cache entry
+ entry.insert(Some(new_entry)).as_ref()
+ };
+ }
+
+ // Check `Dynamic` parameters for functions with parameters
+ if allow_dynamic && max_bitmask == 0 && num_args > 0 {
+ let is_dynamic = self
+ .global_modules
+ .iter()
+ .any(|m| m.may_contain_dynamic_fn(hash_base));
+
+ #[cfg(not(feature = "no_function"))]
+ let is_dynamic = is_dynamic
+ || _global
+ .lib
+ .iter()
+ .any(|m| m.may_contain_dynamic_fn(hash_base));
+
+ #[cfg(not(feature = "no_module"))]
+ let is_dynamic = is_dynamic
+ || _global.may_contain_dynamic_fn(hash_base)
+ || self.global_sub_modules.as_ref().map_or(false, |m| {
+ m.values().any(|m| m.may_contain_dynamic_fn(hash_base))
+ });
+
+ // Set maximum bitmask when there are dynamic versions of the function
+ if is_dynamic {
+ max_bitmask = 1usize << usize::min(num_args, MAX_DYNAMIC_PARAMETERS);
+ }
+ }
+
+ // Stop when all permutations are exhausted
+ if bitmask >= max_bitmask {
+ if num_args != 2 {
+ return None;
+ }
+
+ // Try to find a built-in version
+ let builtin =
+ args.and_then(|args| match op_token {
+ None => None,
+ Some(token) if token.is_op_assignment() => {
+ let (first_arg, rest_args) = args.split_first().unwrap();
+
+ get_builtin_op_assignment_fn(token, first_arg, rest_args[0])
+ .map(|(f, has_context)| FnResolutionCacheEntry {
+ func: CallableFunction::Method {
+ func: Shared::new(f),
+ has_context,
+ is_pure: false,
+ },
+ source: None,
+ })
+ }
+ Some(token) => get_builtin_binary_op_fn(token, args[0], args[1])
+ .map(|(f, has_context)| FnResolutionCacheEntry {
+ func: CallableFunction::Method {
+ func: Shared::new(f),
+ has_context,
+ is_pure: true,
+ },
+ source: None,
+ }),
+ });
+
+ return if cache.filter.is_absent_and_set(hash) {
+ // Do not cache "one-hit wonders"
+ *local_entry = builtin;
+ local_entry.as_ref()
+ } else {
+ // Cache entry
+ entry.insert(builtin).as_ref()
+ };
+ }
+
+ // Try all permutations with `Dynamic` wildcards
+ hash = calc_fn_hash_full(
+ hash_base,
+ args.as_ref()
+ .expect("no permutations")
+ .iter()
+ .enumerate()
+ .map(|(i, a)| {
+ let mask = 1usize << (num_args - i - 1);
+ if bitmask & mask == 0 {
+ a.type_id()
+ } else {
+ // Replace with `Dynamic`
+ TypeId::of::<Dynamic>()
+ }
+ }),
+ );
+
+ bitmask += 1;
+ }
+ }
+ }
+ }
+
+ /// # Main Entry-Point (Native by Name)
+ ///
+ /// Call a native Rust function registered with the [`Engine`] by name.
+ ///
+ /// # WARNING
+ ///
+ /// Function call arguments be _consumed_ when the function requires them to be passed by value.
+ /// All function arguments not in the first position are always passed by value and thus consumed.
+ ///
+ /// **DO NOT** reuse the argument values except for the first `&mut` argument - all others are silently replaced by `()`!
+ pub(crate) fn exec_native_fn_call(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ name: &str,
+ op_token: Option<&Token>,
+ hash: u64,
+ args: &mut FnCallArgs,
+ is_ref_mut: bool,
+ pos: Position,
+ ) -> RhaiResultOf<(Dynamic, bool)> {
+ self.track_operation(global, pos)?;
+
+ // Check if function access already in the cache
+ let local_entry = &mut None;
+
+ let func = self.resolve_fn(
+ global,
+ caches,
+ local_entry,
+ op_token,
+ hash,
+ Some(args),
+ true,
+ );
+
+ if let Some(FnResolutionCacheEntry { func, source }) = func {
+ debug_assert!(func.is_native());
+
+ // Push a new call stack frame
+ #[cfg(feature = "debugging")]
+ let orig_call_stack_len = global
+ .debugger
+ .as_ref()
+ .map_or(0, |dbg| dbg.call_stack().len());
+
+ let backup = &mut ArgBackup::new();
+
+ // Calling non-method function but the first argument is a reference?
+ let swap = is_ref_mut && !func.is_method() && !args.is_empty();
+
+ if swap {
+ // Clone the first argument
+ backup.change_first_arg_to_copy(args);
+ }
+
+ #[cfg(feature = "debugging")]
+ if self.is_debugger_registered() {
+ let source = source.clone().or_else(|| global.source.clone());
+
+ global.debugger_mut().push_call_stack_frame(
+ self.get_interned_string(name),
+ args.iter().map(|v| (*v).clone()).collect(),
+ source,
+ pos,
+ );
+ }
+
+ // Run external function
+ let is_method = func.is_method();
+ let src = source.as_ref().map(|s| s.as_str());
+
+ let context = func
+ .has_context()
+ .then(|| (self, name, src, &*global, pos).into());
+
+ let mut _result = if !func.is_pure() && !args.is_empty() && args[0].is_read_only() {
+ // If function is not pure, there must be at least one argument
+ Err(ERR::ErrorNonPureMethodCallOnConstant(name.to_string(), pos).into())
+ } else if let Some(f) = func.get_plugin_fn() {
+ f.call(context, args)
+ } else if let Some(f) = func.get_native_fn() {
+ f(context, args)
+ } else {
+ unreachable!();
+ }
+ .and_then(|r| self.check_data_size(r, pos))
+ .map_err(|err| err.fill_position(pos));
+
+ if swap {
+ backup.restore_first_arg(args);
+ }
+
+ #[cfg(feature = "debugging")]
+ if self.is_debugger_registered() {
+ use crate::eval::{DebuggerEvent, DebuggerStatus};
+
+ let trigger = match global.debugger().status {
+ DebuggerStatus::FunctionExit(n) => n >= global.level,
+ DebuggerStatus::Next(.., true) => true,
+ _ => false,
+ };
+ if trigger {
+ let scope = &mut Scope::new();
+ let node = crate::ast::Stmt::Noop(pos);
+ let node = (&node).into();
+ let event = match _result {
+ Ok(ref r) => DebuggerEvent::FunctionExitWithValue(r),
+ Err(ref err) => DebuggerEvent::FunctionExitWithError(err),
+ };
+
+ match self.run_debugger_raw(global, caches, scope, None, node, event) {
+ Ok(..) => (),
+ Err(err) => _result = Err(err),
+ }
+ }
+
+ // Pop the call stack
+ global.debugger_mut().rewind_call_stack(orig_call_stack_len);
+ }
+
+ let result = _result?;
+
+ // Check the data size of any `&mut` object, which may be changed.
+ #[cfg(not(feature = "unchecked"))]
+ if is_ref_mut && !args.is_empty() {
+ self.check_data_size(&*args[0], pos)?;
+ }
+
+ // See if the function match print/debug (which requires special processing)
+ return Ok(match name {
+ KEYWORD_PRINT => {
+ if let Some(ref print) = self.print {
+ let text = result.into_immutable_string().map_err(|typ| {
+ let t = self.map_type_name(type_name::<ImmutableString>()).into();
+ ERR::ErrorMismatchOutputType(t, typ.into(), pos)
+ })?;
+ (print(&text).into(), false)
+ } else {
+ (Dynamic::UNIT, false)
+ }
+ }
+ KEYWORD_DEBUG => {
+ if let Some(ref debug) = self.debug {
+ let text = result.into_immutable_string().map_err(|typ| {
+ let t = self.map_type_name(type_name::<ImmutableString>()).into();
+ ERR::ErrorMismatchOutputType(t, typ.into(), pos)
+ })?;
+ (debug(&text, global.source(), pos).into(), false)
+ } else {
+ (Dynamic::UNIT, false)
+ }
+ }
+ _ => (result, is_method),
+ });
+ }
+
+ // Error handling
+
+ match name {
+ // index getter function not found?
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ crate::engine::FN_IDX_GET => {
+ debug_assert_eq!(args.len(), 2);
+
+ let t0 = self.map_type_name(args[0].type_name());
+ let t1 = self.map_type_name(args[1].type_name());
+
+ Err(ERR::ErrorIndexingType(format!("{t0} [{t1}]"), pos).into())
+ }
+
+ // index setter function not found?
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ crate::engine::FN_IDX_SET => {
+ debug_assert_eq!(args.len(), 3);
+
+ let t0 = self.map_type_name(args[0].type_name());
+ let t1 = self.map_type_name(args[1].type_name());
+ let t2 = self.map_type_name(args[2].type_name());
+
+ Err(ERR::ErrorIndexingType(format!("{t0} [{t1}] = {t2}"), pos).into())
+ }
+
+ // Getter function not found?
+ #[cfg(not(feature = "no_object"))]
+ _ if name.starts_with(crate::engine::FN_GET) => {
+ debug_assert_eq!(args.len(), 1);
+
+ let prop = &name[crate::engine::FN_GET.len()..];
+ let t0 = self.map_type_name(args[0].type_name());
+
+ Err(ERR::ErrorDotExpr(
+ format!(
+ "Unknown property '{prop}' - a getter is not registered for type '{t0}'"
+ ),
+ pos,
+ )
+ .into())
+ }
+
+ // Setter function not found?
+ #[cfg(not(feature = "no_object"))]
+ _ if name.starts_with(crate::engine::FN_SET) => {
+ debug_assert_eq!(args.len(), 2);
+
+ let prop = &name[crate::engine::FN_SET.len()..];
+ let t0 = self.map_type_name(args[0].type_name());
+ let t1 = self.map_type_name(args[1].type_name());
+
+ Err(ERR::ErrorDotExpr(
+ format!(
+ "No writable property '{prop}' - a setter is not registered for type '{t0}' to handle '{t1}'"
+ ),
+ pos,
+ )
+ .into())
+ }
+
+ // Raise error
+ _ => {
+ Err(ERR::ErrorFunctionNotFound(self.gen_fn_call_signature(name, args), pos).into())
+ }
+ }
+ }
+
+ /// # Main Entry-Point (By Name)
+ ///
+ /// Perform an actual function call, native Rust or scripted, by name, taking care of special functions.
+ ///
+ /// # WARNING
+ ///
+ /// Function call arguments may be _consumed_ when the function requires them to be passed by
+ /// value. All function arguments not in the first position are always passed by value and thus consumed.
+ ///
+ /// **DO NOT** reuse the argument values except for the first `&mut` argument - all others are silently replaced by `()`!
+ pub(crate) fn exec_fn_call(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ _scope: Option<&mut Scope>,
+ fn_name: &str,
+ op_token: Option<&Token>,
+ hashes: FnCallHashes,
+ args: &mut FnCallArgs,
+ is_ref_mut: bool,
+ _is_method_call: bool,
+ pos: Position,
+ ) -> RhaiResultOf<(Dynamic, bool)> {
+ // These may be redirected from method style calls.
+ if hashes.is_native_only() {
+ loop {
+ match fn_name {
+ // Handle type_of()
+ KEYWORD_TYPE_OF if args.len() == 1 => {
+ let typ = self.get_interned_string(self.map_type_name(args[0].type_name()));
+ return Ok((typ.into(), false));
+ }
+
+ #[cfg(not(feature = "no_closure"))]
+ crate::engine::KEYWORD_IS_SHARED if args.len() == 1 => {
+ return Ok((args[0].is_shared().into(), false))
+ }
+ #[cfg(not(feature = "no_closure"))]
+ crate::engine::KEYWORD_IS_SHARED => (),
+
+ #[cfg(not(feature = "no_function"))]
+ crate::engine::KEYWORD_IS_DEF_FN => (),
+
+ KEYWORD_TYPE_OF | KEYWORD_FN_PTR | KEYWORD_EVAL | KEYWORD_IS_DEF_VAR
+ | KEYWORD_FN_PTR_CALL | KEYWORD_FN_PTR_CURRY => (),
+
+ _ => break,
+ }
+
+ let sig = self.gen_fn_call_signature(fn_name, args);
+ return Err(ERR::ErrorFunctionNotFound(sig, pos).into());
+ }
+ }
+
+ // Check for data race.
+ #[cfg(not(feature = "no_closure"))]
+ ensure_no_data_race(fn_name, args, is_ref_mut)?;
+
+ defer! { let orig_level = global.level; global.level += 1 }
+
+ // Script-defined function call?
+ #[cfg(not(feature = "no_function"))]
+ if !hashes.is_native_only() {
+ let hash = hashes.script();
+ let local_entry = &mut None;
+
+ let mut resolved = None;
+ #[cfg(not(feature = "no_object"))]
+ if _is_method_call && !args.is_empty() {
+ let typed_hash =
+ crate::calc_typed_method_hash(hash, self.map_type_name(args[0].type_name()));
+ resolved =
+ self.resolve_fn(global, caches, local_entry, None, typed_hash, None, false);
+ }
+
+ if resolved.is_none() {
+ resolved = self.resolve_fn(global, caches, local_entry, None, hash, None, false);
+ }
+
+ if let Some(FnResolutionCacheEntry { func, source }) = resolved.cloned() {
+ // Script function call
+ debug_assert!(func.is_script());
+
+ let environ = func.get_encapsulated_environ();
+ let func = func.get_script_fn_def().expect("script-defined function");
+
+ if func.body.is_empty() {
+ return Ok((Dynamic::UNIT, false));
+ }
+
+ let mut empty_scope;
+ let scope = if let Some(scope) = _scope {
+ scope
+ } else {
+ empty_scope = Scope::new();
+ &mut empty_scope
+ };
+
+ let orig_source = mem::replace(&mut global.source, source);
+ defer! { global => move |g| g.source = orig_source }
+
+ return if _is_method_call {
+ use std::ops::DerefMut;
+
+ // Method call of script function - map first argument to `this`
+ let (first_arg, args) = args.split_first_mut().unwrap();
+ let this_ptr = Some(first_arg.deref_mut());
+ self.call_script_fn(
+ global, caches, scope, this_ptr, environ, func, args, true, pos,
+ )
+ } else {
+ // Normal call of script function
+ let backup = &mut ArgBackup::new();
+
+ // The first argument is a reference?
+ let swap = is_ref_mut && !args.is_empty();
+
+ if swap {
+ backup.change_first_arg_to_copy(args);
+ }
+
+ defer! { args = (args) if swap => move |a| backup.restore_first_arg(a) }
+
+ self.call_script_fn(global, caches, scope, None, environ, func, args, true, pos)
+ }
+ .map(|r| (r, false));
+ }
+ }
+
+ // Native function call
+ let hash = hashes.native();
+
+ self.exec_native_fn_call(
+ global, caches, fn_name, op_token, hash, args, is_ref_mut, pos,
+ )
+ }
+
+ /// Evaluate an argument.
+ #[inline]
+ pub(crate) fn get_arg_value(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ this_ptr: Option<&mut Dynamic>,
+ arg_expr: &Expr,
+ ) -> RhaiResultOf<(Dynamic, Position)> {
+ // Literal values
+ if let Some(value) = arg_expr.get_literal_value() {
+ self.track_operation(global, arg_expr.start_position())?;
+
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr, arg_expr)?;
+
+ return Ok((value, arg_expr.start_position()));
+ }
+
+ // Do not match function exit for arguments
+ #[cfg(feature = "debugging")]
+ let reset = global.debugger.as_deref_mut().and_then(|dbg| {
+ dbg.clear_status_if(|status| {
+ matches!(status, crate::eval::DebuggerStatus::FunctionExit(..))
+ })
+ });
+ #[cfg(feature = "debugging")]
+ defer! { global if Some(reset) => move |g| g.debugger_mut().reset_status(reset) }
+
+ self.eval_expr(global, caches, scope, this_ptr, arg_expr)
+ .map(|r| (r, arg_expr.start_position()))
+ }
+
+ /// Call a dot method.
+ #[cfg(not(feature = "no_object"))]
+ pub(crate) fn make_method_call(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ fn_name: &str,
+ mut hash: FnCallHashes,
+ target: &mut crate::eval::Target,
+ call_args: &mut [Dynamic],
+ first_arg_pos: Position,
+ pos: Position,
+ ) -> RhaiResultOf<(Dynamic, bool)> {
+ let (result, updated) = match fn_name {
+ // Handle fn_ptr.call(...)
+ KEYWORD_FN_PTR_CALL if target.is_fnptr() => {
+ let fn_ptr = target.read_lock::<FnPtr>().expect("`FnPtr`");
+
+ // Arguments are passed as-is, adding the curried arguments
+ let mut curry = fn_ptr.curry().iter().cloned().collect::<FnArgsVec<_>>();
+ let args = &mut curry
+ .iter_mut()
+ .chain(call_args.iter_mut())
+ .collect::<FnArgsVec<_>>();
+
+ let _fn_def = ();
+ #[cfg(not(feature = "no_function"))]
+ let _fn_def = fn_ptr.fn_def();
+
+ match _fn_def {
+ // Linked to scripted function - short-circuit
+ #[cfg(not(feature = "no_function"))]
+ Some(fn_def) if fn_def.params.len() == args.len() => {
+ let scope = &mut Scope::new();
+ let environ = fn_ptr.encapsulated_environ().map(|r| r.as_ref());
+
+ self.call_script_fn(
+ global, caches, scope, None, environ, fn_def, args, true, pos,
+ )
+ .map(|v| (v, false))
+ }
+ _ => {
+ let _is_anon = false;
+ #[cfg(not(feature = "no_function"))]
+ let _is_anon = fn_ptr.is_anonymous();
+
+ // Redirect function name
+ let fn_name = fn_ptr.fn_name();
+ // Recalculate hashes
+ let new_hash = if !_is_anon && !is_valid_function_name(fn_name) {
+ FnCallHashes::from_native_only(calc_fn_hash(None, fn_name, args.len()))
+ } else {
+ FnCallHashes::from_hash(calc_fn_hash(None, fn_name, args.len()))
+ };
+
+ // Map it to name(args) in function-call style
+ self.exec_fn_call(
+ global, caches, None, fn_name, None, new_hash, args, false, false, pos,
+ )
+ }
+ }
+ }
+
+ // Handle obj.call()
+ KEYWORD_FN_PTR_CALL if call_args.is_empty() => {
+ return Err(self
+ .make_type_mismatch_err::<FnPtr>(self.map_type_name(target.type_name()), pos))
+ }
+
+ // Handle obj.call(fn_ptr, ...)
+ KEYWORD_FN_PTR_CALL => {
+ debug_assert!(!call_args.is_empty());
+
+ // FnPtr call on object
+ let fn_ptr = call_args[0].take().try_cast_raw::<FnPtr>().map_err(|v| {
+ self.make_type_mismatch_err::<FnPtr>(
+ self.map_type_name(v.type_name()),
+ first_arg_pos,
+ )
+ })?;
+
+ #[cfg(not(feature = "no_function"))]
+ let (is_anon, (fn_name, fn_curry, environ, fn_def)) =
+ (fn_ptr.is_anonymous(), fn_ptr.take_data());
+ #[cfg(feature = "no_function")]
+ let (is_anon, (fn_name, fn_curry, _), fn_def) = (false, fn_ptr.take_data(), ());
+
+ // Adding the curried arguments and the remaining arguments
+ let mut curry = fn_curry.into_iter().collect::<FnArgsVec<_>>();
+ let args = &mut FnArgsVec::with_capacity(curry.len() + call_args.len());
+ args.extend(curry.iter_mut());
+ args.extend(call_args.iter_mut().skip(1));
+
+ match fn_def {
+ // Linked to scripted function - short-circuit
+ #[cfg(not(feature = "no_function"))]
+ Some(fn_def) if fn_def.params.len() == args.len() => {
+ // Check for data race.
+ #[cfg(not(feature = "no_closure"))]
+ ensure_no_data_race(&fn_def.name, args, false)?;
+
+ let scope = &mut Scope::new();
+ let this_ptr = Some(target.as_mut());
+ let environ = environ.as_deref();
+
+ self.call_script_fn(
+ global, caches, scope, this_ptr, environ, &fn_def, args, true, pos,
+ )
+ .map(|v| (v, false))
+ }
+ _ => {
+ let name = fn_name.as_str();
+ let is_ref_mut = target.is_ref();
+
+ // Add the first argument with the object pointer
+ args.insert(0, target.as_mut());
+
+ // Recalculate hash
+ let num_args = args.len();
+
+ let new_hash = match is_anon {
+ false if !is_valid_function_name(name) => {
+ FnCallHashes::from_native_only(calc_fn_hash(None, name, num_args))
+ }
+ #[cfg(not(feature = "no_function"))]
+ _ => FnCallHashes::from_script_and_native(
+ calc_fn_hash(None, name, num_args - 1),
+ calc_fn_hash(None, name, num_args),
+ ),
+ #[cfg(feature = "no_function")]
+ _ => FnCallHashes::from_native_only(calc_fn_hash(None, name, num_args)),
+ };
+
+ // Map it to name(args) in function-call style
+ self.exec_fn_call(
+ global, caches, None, name, None, new_hash, args, is_ref_mut, true, pos,
+ )
+ }
+ }
+ }
+
+ // Handle fn_ptr.curry(...)
+ KEYWORD_FN_PTR_CURRY => {
+ let typ = target.type_name();
+ let mut fn_ptr = target
+ .read_lock::<FnPtr>()
+ .ok_or_else(|| {
+ self.make_type_mismatch_err::<FnPtr>(self.map_type_name(typ), pos)
+ })?
+ .clone();
+
+ // Append the new curried arguments to the existing list.
+ fn_ptr.extend(call_args.iter_mut().map(mem::take));
+
+ Ok((fn_ptr.into(), false))
+ }
+
+ // Handle var.is_shared()
+ #[cfg(not(feature = "no_closure"))]
+ crate::engine::KEYWORD_IS_SHARED if call_args.is_empty() => {
+ return Ok((target.is_shared().into(), false));
+ }
+
+ _ => {
+ let mut fn_name = fn_name;
+ let _redirected;
+ let mut _linked = None;
+ let mut _arg_values;
+ let mut call_args = call_args;
+
+ // Check if it is a map method call in OOP style
+
+ #[cfg(not(feature = "no_object"))]
+ if let Some(map) = target.read_lock::<crate::Map>() {
+ if let Some(val) = map.get(fn_name) {
+ if let Some(fn_ptr) = val.read_lock::<FnPtr>() {
+ // Remap the function name
+ _redirected = fn_ptr.fn_name_raw().clone();
+ fn_name = &_redirected;
+ // Add curried arguments
+ if fn_ptr.is_curried() {
+ _arg_values = fn_ptr
+ .curry()
+ .iter()
+ .cloned()
+ .chain(call_args.iter_mut().map(mem::take))
+ .collect::<FnArgsVec<_>>();
+ call_args = &mut _arg_values;
+ }
+
+ let _fn_def = ();
+ #[cfg(not(feature = "no_function"))]
+ let _fn_def = fn_ptr.fn_def();
+
+ match _fn_def {
+ // Linked to scripted function
+ #[cfg(not(feature = "no_function"))]
+ Some(fn_def) if fn_def.params.len() == call_args.len() => {
+ _linked = Some((
+ fn_def.clone(),
+ fn_ptr.encapsulated_environ().cloned(),
+ ))
+ }
+ _ => {
+ let _is_anon = false;
+ #[cfg(not(feature = "no_function"))]
+ let _is_anon = fn_ptr.is_anonymous();
+
+ // Recalculate the hash based on the new function name and new arguments
+ let num_args = call_args.len() + 1;
+
+ hash = match _is_anon {
+ false if !is_valid_function_name(fn_name) => {
+ FnCallHashes::from_native_only(calc_fn_hash(
+ None, fn_name, num_args,
+ ))
+ }
+ #[cfg(not(feature = "no_function"))]
+ _ => FnCallHashes::from_script_and_native(
+ calc_fn_hash(None, fn_name, num_args - 1),
+ calc_fn_hash(None, fn_name, num_args),
+ ),
+ #[cfg(feature = "no_function")]
+ _ => FnCallHashes::from_native_only(calc_fn_hash(
+ None, fn_name, num_args,
+ )),
+ };
+ }
+ }
+ }
+ }
+ }
+
+ match _linked {
+ #[cfg(not(feature = "no_function"))]
+ Some((fn_def, environ)) => {
+ // Linked to scripted function - short-circuit
+ let scope = &mut Scope::new();
+ let environ = environ.as_deref();
+ let this_ptr = Some(target.as_mut());
+ let args = &mut call_args.iter_mut().collect::<FnArgsVec<_>>();
+
+ self.call_script_fn(
+ global, caches, scope, this_ptr, environ, &*fn_def, args, true, pos,
+ )
+ .map(|v| (v, false))
+ }
+ #[cfg(feature = "no_function")]
+ Some(()) => unreachable!(),
+ None => {
+ let is_ref_mut = target.is_ref();
+
+ // Attached object pointer in front of the arguments
+ let args = &mut std::iter::once(target.as_mut())
+ .chain(call_args.iter_mut())
+ .collect::<FnArgsVec<_>>();
+
+ self.exec_fn_call(
+ global, caches, None, fn_name, None, hash, args, is_ref_mut, true, pos,
+ )
+ }
+ }
+ }
+ }?;
+
+ // Propagate the changed value back to the source if necessary
+ if updated {
+ target.propagate_changed_value(pos)?;
+ }
+
+ Ok((result, updated))
+ }
+
+ /// Call a function in normal function-call style.
+ pub(crate) fn make_function_call(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ mut this_ptr: Option<&mut Dynamic>,
+ fn_name: &str,
+ op_token: Option<&Token>,
+ first_arg: Option<&Expr>,
+ args_expr: &[Expr],
+ hashes: FnCallHashes,
+ capture_scope: bool,
+ pos: Position,
+ ) -> RhaiResult {
+ let mut first_arg = first_arg;
+ let mut args_expr = args_expr;
+ let mut num_args = usize::from(first_arg.is_some()) + args_expr.len();
+ let mut curry = FnArgsVec::new_const();
+ let mut name = fn_name;
+ let mut hashes = hashes;
+ let redirected; // Handle call() - Redirect function call
+
+ match name {
+ _ if op_token.is_some() => (),
+
+ // Handle call(fn_ptr, ...)
+ KEYWORD_FN_PTR_CALL if num_args >= 1 => {
+ let arg = first_arg.unwrap();
+ let (first_arg_value, first_arg_pos) =
+ self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), arg)?;
+
+ let fn_ptr = first_arg_value.try_cast_raw::<FnPtr>().map_err(|v| {
+ self.make_type_mismatch_err::<FnPtr>(
+ self.map_type_name(v.type_name()),
+ first_arg_pos,
+ )
+ })?;
+
+ #[cfg(not(feature = "no_function"))]
+ let (is_anon, (fn_name, fn_curry, _environ, fn_def)) =
+ (fn_ptr.is_anonymous(), fn_ptr.take_data());
+ #[cfg(feature = "no_function")]
+ let (is_anon, (fn_name, fn_curry, _environ)) = (false, fn_ptr.take_data());
+
+ curry.extend(fn_curry.into_iter());
+
+ // Linked to scripted function - short-circuit
+ #[cfg(not(feature = "no_function"))]
+ if let Some(fn_def) = fn_def {
+ if fn_def.params.len() == curry.len() + args_expr.len() {
+ // Evaluate arguments
+ let mut arg_values =
+ FnArgsVec::with_capacity(curry.len() + args_expr.len());
+ arg_values.extend(curry);
+ for expr in args_expr {
+ let this_ptr = this_ptr.as_deref_mut();
+ let (value, _) =
+ self.get_arg_value(global, caches, scope, this_ptr, expr)?;
+ arg_values.push(value);
+ }
+ let args = &mut arg_values.iter_mut().collect::<FnArgsVec<_>>();
+ let scope = &mut Scope::new();
+ let environ = _environ.as_deref();
+
+ return self.call_script_fn(
+ global, caches, scope, None, environ, &fn_def, args, true, pos,
+ );
+ }
+ }
+
+ // Redirect function name
+ redirected = fn_name;
+ name = &redirected;
+
+ // Shift the arguments
+ first_arg = args_expr.get(0);
+ if !args_expr.is_empty() {
+ args_expr = &args_expr[1..];
+ }
+ num_args -= 1;
+
+ // Recalculate hash
+ let args_len = num_args + curry.len();
+
+ hashes = if !is_anon && !is_valid_function_name(name) {
+ FnCallHashes::from_native_only(calc_fn_hash(None, name, args_len))
+ } else {
+ FnCallHashes::from_hash(calc_fn_hash(None, name, args_len))
+ };
+ }
+
+ // Handle Fn(fn_name)
+ KEYWORD_FN_PTR if num_args == 1 => {
+ let arg = first_arg.unwrap();
+ let (arg_value, arg_pos) =
+ self.get_arg_value(global, caches, scope, this_ptr, arg)?;
+
+ // Fn - only in function call style
+ return arg_value
+ .into_immutable_string()
+ .map_err(|typ| self.make_type_mismatch_err::<ImmutableString>(typ, arg_pos))
+ .and_then(FnPtr::try_from)
+ .map(Into::into)
+ .map_err(|err| err.fill_position(arg_pos));
+ }
+
+ // Handle curry(x, ...)
+ KEYWORD_FN_PTR_CURRY if num_args > 1 => {
+ let first = first_arg.unwrap();
+ let (first_arg_value, first_arg_pos) =
+ self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), first)?;
+
+ let mut fn_ptr = first_arg_value.try_cast_raw::<FnPtr>().map_err(|v| {
+ self.make_type_mismatch_err::<FnPtr>(
+ self.map_type_name(v.type_name()),
+ first_arg_pos,
+ )
+ })?;
+
+ // Append the new curried arguments to the existing list.
+ for expr in args_expr {
+ let (value, ..) =
+ self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?;
+ fn_ptr.add_curry(value);
+ }
+
+ return Ok(fn_ptr.into());
+ }
+
+ // Handle is_shared(var)
+ #[cfg(not(feature = "no_closure"))]
+ crate::engine::KEYWORD_IS_SHARED if num_args == 1 => {
+ let arg = first_arg.unwrap();
+ let (arg_value, ..) =
+ self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), arg)?;
+ return Ok(arg_value.is_shared().into());
+ }
+
+ // Handle is_def_fn(fn_name, arity)
+ #[cfg(not(feature = "no_function"))]
+ crate::engine::KEYWORD_IS_DEF_FN if num_args == 2 => {
+ let first = first_arg.unwrap();
+ let (arg_value, arg_pos) =
+ self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), first)?;
+
+ let fn_name = arg_value
+ .into_immutable_string()
+ .map_err(|typ| self.make_type_mismatch_err::<ImmutableString>(typ, arg_pos))?;
+
+ let (arg_value, arg_pos) =
+ self.get_arg_value(global, caches, scope, this_ptr, &args_expr[0])?;
+
+ let num_params = arg_value
+ .as_int()
+ .map_err(|typ| self.make_type_mismatch_err::<crate::INT>(typ, arg_pos))?;
+
+ return Ok(if num_params >= 0 {
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let hash_script = calc_fn_hash(None, &fn_name, num_params as usize);
+ self.has_script_fn(global, caches, hash_script).into()
+ } else {
+ Dynamic::FALSE
+ });
+ }
+
+ // Handle is_def_fn(this_type, fn_name, arity)
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "no_object"))]
+ crate::engine::KEYWORD_IS_DEF_FN if num_args == 3 => {
+ let first = first_arg.unwrap();
+ let (arg_value, arg_pos) =
+ self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), first)?;
+
+ let this_type = arg_value
+ .into_immutable_string()
+ .map_err(|typ| self.make_type_mismatch_err::<ImmutableString>(typ, arg_pos))?;
+
+ let (arg_value, arg_pos) = self.get_arg_value(
+ global,
+ caches,
+ scope,
+ this_ptr.as_deref_mut(),
+ &args_expr[0],
+ )?;
+
+ let fn_name = arg_value
+ .into_immutable_string()
+ .map_err(|typ| self.make_type_mismatch_err::<ImmutableString>(typ, arg_pos))?;
+
+ let (arg_value, arg_pos) =
+ self.get_arg_value(global, caches, scope, this_ptr, &args_expr[1])?;
+
+ let num_params = arg_value
+ .as_int()
+ .map_err(|typ| self.make_type_mismatch_err::<crate::INT>(typ, arg_pos))?;
+
+ return Ok(if num_params >= 0 {
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let hash_script = crate::calc_typed_method_hash(
+ calc_fn_hash(None, &fn_name, num_params as usize),
+ &this_type,
+ );
+ self.has_script_fn(global, caches, hash_script).into()
+ } else {
+ Dynamic::FALSE
+ });
+ }
+
+ // Handle is_def_var(fn_name)
+ KEYWORD_IS_DEF_VAR if num_args == 1 => {
+ let arg = first_arg.unwrap();
+ let (arg_value, arg_pos) =
+ self.get_arg_value(global, caches, scope, this_ptr, arg)?;
+ let var_name = arg_value
+ .into_immutable_string()
+ .map_err(|typ| self.make_type_mismatch_err::<ImmutableString>(typ, arg_pos))?;
+ return Ok(scope.contains(&var_name).into());
+ }
+
+ // Handle eval(script)
+ KEYWORD_EVAL if num_args == 1 => {
+ // eval - only in function call style
+ let orig_scope_len = scope.len();
+ #[cfg(not(feature = "no_module"))]
+ let orig_imports_len = global.num_imports();
+ let arg = first_arg.unwrap();
+ let (arg_value, pos) = self.get_arg_value(global, caches, scope, this_ptr, arg)?;
+ let s = &arg_value
+ .into_immutable_string()
+ .map_err(|typ| self.make_type_mismatch_err::<ImmutableString>(typ, pos))?;
+
+ let orig_level = global.level;
+ global.level += 1;
+
+ let result = self.eval_script_expr_in_place(global, caches, scope, s, pos);
+
+ // IMPORTANT! If the eval defines new variables in the current scope,
+ // all variable offsets from this point on will be mis-aligned.
+ // The same is true for imports.
+ let scope_changed = scope.len() != orig_scope_len;
+ #[cfg(not(feature = "no_module"))]
+ let scope_changed = scope_changed || global.num_imports() != orig_imports_len;
+
+ if scope_changed {
+ global.always_search_scope = true;
+ }
+ global.level = orig_level;
+
+ return result.map_err(|err| {
+ ERR::ErrorInFunctionCall(
+ KEYWORD_EVAL.to_string(),
+ global.source().unwrap_or("").to_string(),
+ err,
+ pos,
+ )
+ .into()
+ });
+ }
+
+ _ => (),
+ }
+
+ // Normal function call - except for Fn, curry, call and eval (handled above)
+ let mut arg_values = FnArgsVec::with_capacity(num_args);
+ let mut args = FnArgsVec::with_capacity(num_args + curry.len());
+ let mut is_ref_mut = false;
+
+ // Capture parent scope?
+ //
+ // If so, do it separately because we cannot convert the first argument (if it is a simple
+ // variable access) to &mut because `scope` is needed.
+ if capture_scope && !scope.is_empty() {
+ for expr in first_arg.iter().copied().chain(args_expr.iter()) {
+ let (value, ..) =
+ self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?;
+ arg_values.push(value.flatten());
+ }
+ args.extend(curry.iter_mut());
+ args.extend(arg_values.iter_mut());
+
+ // Use parent scope
+ let scope = Some(scope);
+
+ return self
+ .exec_fn_call(
+ global, caches, scope, name, op_token, hashes, &mut args, is_ref_mut, false,
+ pos,
+ )
+ .map(|(v, ..)| v);
+ }
+
+ // Call with blank scope
+ #[cfg(not(feature = "no_closure"))]
+ let this_ptr_not_shared = this_ptr.as_ref().map_or(false, |v| !v.is_shared());
+ #[cfg(feature = "no_closure")]
+ let this_ptr_not_shared = true;
+
+ // If the first argument is a variable, and there are no curried arguments,
+ // convert to method-call style in order to leverage potential &mut first argument
+ // and avoid cloning the value.
+ match first_arg {
+ Some(_first_expr @ Expr::ThisPtr(pos)) if curry.is_empty() && this_ptr_not_shared => {
+ // Turn it into a method call only if the object is not shared
+ self.track_operation(global, *pos)?;
+
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr.as_deref_mut(), _first_expr)?;
+
+ // func(x, ...) -> x.func(...)
+ for expr in args_expr {
+ let (value, ..) =
+ self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?;
+ arg_values.push(value.flatten());
+ }
+
+ is_ref_mut = true;
+ args.push(this_ptr.unwrap());
+ }
+ Some(first_expr @ Expr::Variable(.., pos)) if curry.is_empty() => {
+ self.track_operation(global, *pos)?;
+
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr.as_deref_mut(), first_expr)?;
+
+ // func(x, ...) -> x.func(...)
+ for expr in args_expr {
+ let (value, ..) =
+ self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?;
+ arg_values.push(value.flatten());
+ }
+
+ let mut target =
+ self.search_namespace(global, caches, scope, this_ptr, first_expr)?;
+
+ if target.is_read_only() {
+ target = target.into_owned();
+ }
+
+ if target.is_shared() || target.is_temp_value() {
+ arg_values.insert(0, target.take_or_clone().flatten());
+ } else {
+ // Turn it into a method call only if the object is not shared and not a simple value
+ is_ref_mut = true;
+ let obj_ref = target.take_ref().expect("ref");
+ args.push(obj_ref);
+ }
+ }
+ _ => {
+ // func(..., ...)
+ for expr in first_arg.into_iter().chain(args_expr.iter()) {
+ let (value, ..) =
+ self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?;
+ arg_values.push(value.flatten());
+ }
+ args.extend(curry.iter_mut());
+ }
+ }
+
+ args.extend(arg_values.iter_mut());
+
+ self.exec_fn_call(
+ global, caches, None, name, op_token, hashes, &mut args, is_ref_mut, false, pos,
+ )
+ .map(|(v, ..)| v)
+ }
+
+ /// Call a namespace-qualified function in normal function-call style.
+ #[cfg(not(feature = "no_module"))]
+ pub(crate) fn make_qualified_function_call(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ mut this_ptr: Option<&mut Dynamic>,
+ namespace: &crate::ast::Namespace,
+ fn_name: &str,
+ args_expr: &[Expr],
+ hash: u64,
+ pos: Position,
+ ) -> RhaiResult {
+ let mut arg_values = FnArgsVec::with_capacity(args_expr.len());
+ let args = &mut FnArgsVec::with_capacity(args_expr.len());
+ let mut first_arg_value = None;
+
+ #[cfg(not(feature = "no_closure"))]
+ let this_ptr_not_shared = this_ptr.as_ref().map_or(false, |v| !v.is_shared());
+ #[cfg(feature = "no_closure")]
+ let this_ptr_not_shared = true;
+
+ // See if the first argument is a variable.
+ // If so, convert to method-call style in order to leverage potential
+ // &mut first argument and avoid cloning the value.
+ match args_expr.get(0) {
+ Some(_first_expr @ Expr::ThisPtr(pos)) if this_ptr_not_shared => {
+ self.track_operation(global, *pos)?;
+
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr.as_deref_mut(), _first_expr)?;
+
+ // Turn it into a method call only if the object is not shared
+ let (first, rest) = arg_values.split_first_mut().unwrap();
+ first_arg_value = Some(first);
+ args.push(this_ptr.unwrap());
+ args.extend(rest.iter_mut());
+ }
+ Some(first_expr @ Expr::Variable(.., pos)) => {
+ self.track_operation(global, *pos)?;
+
+ #[cfg(feature = "debugging")]
+ self.run_debugger(global, caches, scope, this_ptr.as_deref_mut(), first_expr)?;
+
+ // func(x, ...) -> x.func(...)
+ arg_values.push(Dynamic::UNIT);
+
+ for expr in args_expr.iter().skip(1) {
+ let (value, ..) =
+ self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?;
+ arg_values.push(value.flatten());
+ }
+
+ let target = self.search_namespace(global, caches, scope, this_ptr, first_expr)?;
+
+ if target.is_shared() || target.is_temp_value() {
+ arg_values[0] = target.take_or_clone().flatten();
+ args.extend(arg_values.iter_mut());
+ } else {
+ // Turn it into a method call only if the object is not shared and not a simple value
+ let (first, rest) = arg_values.split_first_mut().unwrap();
+ first_arg_value = Some(first);
+ let obj_ref = target.take_ref().expect("ref");
+ args.push(obj_ref);
+ args.extend(rest.iter_mut());
+ }
+ }
+ Some(_) => {
+ // func(..., ...) or func(mod::x, ...)
+ for expr in args_expr {
+ let (value, ..) =
+ self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?;
+ arg_values.push(value.flatten());
+ }
+ args.extend(arg_values.iter_mut());
+ }
+ None => (),
+ }
+
+ // Search for the root namespace
+ let module = self
+ .search_imports(global, namespace)
+ .ok_or_else(|| ERR::ErrorModuleNotFound(namespace.to_string(), namespace.position()))?;
+
+ // First search script-defined functions in namespace (can override built-in)
+ let mut func = module.get_qualified_fn(hash).or_else(|| {
+ // Then search native Rust functions
+ let hash_qualified_fn = calc_fn_hash_full(hash, args.iter().map(|a| a.type_id()));
+ module.get_qualified_fn(hash_qualified_fn)
+ });
+
+ // Check for `Dynamic` parameters.
+ //
+ // Note - This is done during every function call mismatch without cache,
+ // so hopefully the number of arguments should not be too many
+ // (expected because closures cannot be qualified).
+ if func.is_none() && !args.is_empty() {
+ let num_args = args.len();
+ let max_bitmask = 1usize << usize::min(num_args, MAX_DYNAMIC_PARAMETERS);
+ let mut bitmask = 1usize; // Bitmask of which parameter to replace with `Dynamic`
+
+ // Try all permutations with `Dynamic` wildcards
+ while bitmask < max_bitmask {
+ let hash_qualified_fn = calc_fn_hash_full(
+ hash,
+ args.iter().enumerate().map(|(i, a)| {
+ let mask = 1usize << (num_args - i - 1);
+ if bitmask & mask == 0 {
+ a.type_id()
+ } else {
+ // Replace with `Dynamic`
+ TypeId::of::<Dynamic>()
+ }
+ }),
+ );
+
+ if let Some(f) = module.get_qualified_fn(hash_qualified_fn) {
+ func = Some(f);
+ break;
+ }
+
+ bitmask += 1;
+ }
+ }
+
+ // Clone first argument if the function is not a method after-all
+ if !func.map_or(true, CallableFunction::is_method) {
+ if let Some(first) = first_arg_value {
+ *first = args[0].clone();
+ args[0] = first;
+ }
+ }
+
+ defer! { let orig_level = global.level; global.level += 1 }
+
+ match func {
+ #[cfg(not(feature = "no_function"))]
+ Some(func) if func.is_script() => {
+ let f = func.get_script_fn_def().expect("script-defined function");
+
+ let environ = func.get_encapsulated_environ();
+ let scope = &mut Scope::new();
+
+ let orig_source = mem::replace(&mut global.source, module.id_raw().cloned());
+ defer! { global => move |g| g.source = orig_source }
+
+ self.call_script_fn(global, caches, scope, None, environ, f, args, true, pos)
+ }
+
+ Some(f) if !f.is_pure() && args[0].is_read_only() => {
+ // If function is not pure, there must be at least one argument
+ Err(ERR::ErrorNonPureMethodCallOnConstant(fn_name.to_string(), pos).into())
+ }
+
+ Some(f) if f.is_plugin_fn() => {
+ let f = f.get_plugin_fn().expect("plugin function");
+ let context = f
+ .has_context()
+ .then(|| (self, fn_name, module.id(), &*global, pos).into());
+ f.call(context, args)
+ .and_then(|r| self.check_data_size(r, pos))
+ }
+
+ Some(f) if f.is_native() => {
+ let func = f.get_native_fn().expect("native function");
+ let context = f
+ .has_context()
+ .then(|| (self, fn_name, module.id(), &*global, pos).into());
+ func(context, args).and_then(|r| self.check_data_size(r, pos))
+ }
+
+ Some(f) => unreachable!("unknown function type: {:?}", f),
+
+ None => Err(ERR::ErrorFunctionNotFound(
+ if namespace.is_empty() {
+ self.gen_fn_call_signature(fn_name, args)
+ } else {
+ format!(
+ "{namespace}{}{}",
+ crate::engine::NAMESPACE_SEPARATOR,
+ self.gen_fn_call_signature(fn_name, args)
+ )
+ },
+ pos,
+ )
+ .into()),
+ }
+ }
+
+ /// Evaluate a text script in place - used primarily for 'eval'.
+ pub(crate) fn eval_script_expr_in_place(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ script: &str,
+ _pos: Position,
+ ) -> RhaiResult {
+ self.track_operation(global, _pos)?;
+
+ let script = script.trim();
+
+ if script.is_empty() {
+ return Ok(Dynamic::UNIT);
+ }
+
+ // Compile the script text
+ // No optimizations because we only run it once
+ let ast = self.compile_with_scope_and_optimization_level(
+ None,
+ [script],
+ #[cfg(not(feature = "no_optimize"))]
+ OptimizationLevel::None,
+ #[cfg(feature = "no_optimize")]
+ OptimizationLevel::default(),
+ )?;
+
+ // If new functions are defined within the eval string, it is an error
+ #[cfg(not(feature = "no_function"))]
+ if ast.has_functions() {
+ return Err(crate::PERR::WrongFnDefinition.into());
+ }
+
+ let statements = ast.statements();
+ if statements.is_empty() {
+ return Ok(Dynamic::UNIT);
+ }
+
+ // Evaluate the AST
+ self.eval_global_statements(global, caches, scope, statements)
+ }
+
+ /// # Main Entry-Point (`FnCallExpr`)
+ ///
+ /// Evaluate a function call expression.
+ ///
+ /// This method tries to short-circuit function resolution under Fast Operators mode if the
+ /// function call is an operator.
+ pub(crate) fn eval_fn_call_expr(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ mut this_ptr: Option<&mut Dynamic>,
+ expr: &FnCallExpr,
+ pos: Position,
+ ) -> RhaiResult {
+ let FnCallExpr {
+ #[cfg(not(feature = "no_module"))]
+ namespace,
+ name,
+ hashes,
+ args,
+ op_token,
+ capture_parent_scope: capture,
+ ..
+ } = expr;
+
+ let op_token = op_token.as_ref();
+
+ // Short-circuit native unary operator call if under Fast Operators mode
+ if self.fast_operators() && args.len() == 1 && op_token == Some(&Token::Bang) {
+ let mut value = self
+ .get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), &args[0])?
+ .0
+ .flatten();
+
+ return match value.0 {
+ Union::Bool(b, ..) => Ok((!b).into()),
+ _ => {
+ let operand = &mut [&mut value];
+ self.exec_fn_call(
+ global, caches, None, name, op_token, *hashes, operand, false, false, pos,
+ )
+ .map(|(v, ..)| v)
+ }
+ };
+ }
+
+ // Short-circuit native binary operator call if under Fast Operators mode
+ if op_token.is_some() && self.fast_operators() && args.len() == 2 {
+ #[allow(clippy::wildcard_imports)]
+ use Token::*;
+
+ let mut lhs = self
+ .get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), &args[0])?
+ .0
+ .flatten();
+
+ let mut rhs = self
+ .get_arg_value(global, caches, scope, this_ptr, &args[1])?
+ .0
+ .flatten();
+
+ // For extremely simple primary data operations, do it directly
+ // to avoid the overhead of calling a function.
+ match (&lhs.0, &rhs.0) {
+ (Union::Unit(..), Union::Unit(..)) => match op_token.unwrap() {
+ EqualsTo => return Ok(Dynamic::TRUE),
+ NotEqualsTo | GreaterThan | GreaterThanEqualsTo | LessThan
+ | LessThanEqualsTo => return Ok(Dynamic::FALSE),
+ _ => (),
+ },
+ (Union::Bool(b1, ..), Union::Bool(b2, ..)) => match op_token.unwrap() {
+ EqualsTo => return Ok((*b1 == *b2).into()),
+ NotEqualsTo => return Ok((*b1 != *b2).into()),
+ GreaterThan | GreaterThanEqualsTo | LessThan | LessThanEqualsTo => {
+ return Ok(Dynamic::FALSE)
+ }
+ Pipe => return Ok((*b1 || *b2).into()),
+ Ampersand => return Ok((*b1 && *b2).into()),
+ _ => (),
+ },
+ (Union::Int(n1, ..), Union::Int(n2, ..)) => {
+ #[cfg(not(feature = "unchecked"))]
+ #[allow(clippy::wildcard_imports)]
+ use crate::packages::arithmetic::arith_basic::INT::functions::*;
+
+ #[cfg(not(feature = "unchecked"))]
+ match op_token.unwrap() {
+ EqualsTo => return Ok((*n1 == *n2).into()),
+ NotEqualsTo => return Ok((*n1 != *n2).into()),
+ GreaterThan => return Ok((*n1 > *n2).into()),
+ GreaterThanEqualsTo => return Ok((*n1 >= *n2).into()),
+ LessThan => return Ok((*n1 < *n2).into()),
+ LessThanEqualsTo => return Ok((*n1 <= *n2).into()),
+ Plus => return add(*n1, *n2).map(Into::into),
+ Minus => return subtract(*n1, *n2).map(Into::into),
+ Multiply => return multiply(*n1, *n2).map(Into::into),
+ Divide => return divide(*n1, *n2).map(Into::into),
+ Modulo => return modulo(*n1, *n2).map(Into::into),
+ _ => (),
+ }
+ #[cfg(feature = "unchecked")]
+ match op_token.unwrap() {
+ EqualsTo => return Ok((*n1 == *n2).into()),
+ NotEqualsTo => return Ok((*n1 != *n2).into()),
+ GreaterThan => return Ok((*n1 > *n2).into()),
+ GreaterThanEqualsTo => return Ok((*n1 >= *n2).into()),
+ LessThan => return Ok((*n1 < *n2).into()),
+ LessThanEqualsTo => return Ok((*n1 <= *n2).into()),
+ Plus => return Ok((*n1 + *n2).into()),
+ Minus => return Ok((*n1 - *n2).into()),
+ Multiply => return Ok((*n1 * *n2).into()),
+ Divide => return Ok((*n1 / *n2).into()),
+ Modulo => return Ok((*n1 % *n2).into()),
+ _ => (),
+ }
+ }
+ #[cfg(not(feature = "no_float"))]
+ (Union::Float(f1, ..), Union::Float(f2, ..)) => match op_token.unwrap() {
+ EqualsTo => return Ok((**f1 == **f2).into()),
+ NotEqualsTo => return Ok((**f1 != **f2).into()),
+ GreaterThan => return Ok((**f1 > **f2).into()),
+ GreaterThanEqualsTo => return Ok((**f1 >= **f2).into()),
+ LessThan => return Ok((**f1 < **f2).into()),
+ LessThanEqualsTo => return Ok((**f1 <= **f2).into()),
+ Plus => return Ok((**f1 + **f2).into()),
+ Minus => return Ok((**f1 - **f2).into()),
+ Multiply => return Ok((**f1 * **f2).into()),
+ Divide => return Ok((**f1 / **f2).into()),
+ Modulo => return Ok((**f1 % **f2).into()),
+ _ => (),
+ },
+ #[cfg(not(feature = "no_float"))]
+ (Union::Float(f1, ..), Union::Int(n2, ..)) => match op_token.unwrap() {
+ EqualsTo => return Ok((**f1 == (*n2 as crate::FLOAT)).into()),
+ NotEqualsTo => return Ok((**f1 != (*n2 as crate::FLOAT)).into()),
+ GreaterThan => return Ok((**f1 > (*n2 as crate::FLOAT)).into()),
+ GreaterThanEqualsTo => return Ok((**f1 >= (*n2 as crate::FLOAT)).into()),
+ LessThan => return Ok((**f1 < (*n2 as crate::FLOAT)).into()),
+ LessThanEqualsTo => return Ok((**f1 <= (*n2 as crate::FLOAT)).into()),
+ Plus => return Ok((**f1 + (*n2 as crate::FLOAT)).into()),
+ Minus => return Ok((**f1 - (*n2 as crate::FLOAT)).into()),
+ Multiply => return Ok((**f1 * (*n2 as crate::FLOAT)).into()),
+ Divide => return Ok((**f1 / (*n2 as crate::FLOAT)).into()),
+ Modulo => return Ok((**f1 % (*n2 as crate::FLOAT)).into()),
+ _ => (),
+ },
+ #[cfg(not(feature = "no_float"))]
+ (Union::Int(n1, ..), Union::Float(f2, ..)) => match op_token.unwrap() {
+ EqualsTo => return Ok(((*n1 as crate::FLOAT) == **f2).into()),
+ NotEqualsTo => return Ok(((*n1 as crate::FLOAT) != **f2).into()),
+ GreaterThan => return Ok(((*n1 as crate::FLOAT) > **f2).into()),
+ GreaterThanEqualsTo => return Ok(((*n1 as crate::FLOAT) >= **f2).into()),
+ LessThan => return Ok(((*n1 as crate::FLOAT) < **f2).into()),
+ LessThanEqualsTo => return Ok(((*n1 as crate::FLOAT) <= **f2).into()),
+ Plus => return Ok(((*n1 as crate::FLOAT) + **f2).into()),
+ Minus => return Ok(((*n1 as crate::FLOAT) - **f2).into()),
+ Multiply => return Ok(((*n1 as crate::FLOAT) * **f2).into()),
+ Divide => return Ok(((*n1 as crate::FLOAT) / **f2).into()),
+ Modulo => return Ok(((*n1 as crate::FLOAT) % **f2).into()),
+ _ => (),
+ },
+ (Union::Str(s1, ..), Union::Str(s2, ..)) => match op_token.unwrap() {
+ EqualsTo => return Ok((s1 == s2).into()),
+ NotEqualsTo => return Ok((s1 != s2).into()),
+ GreaterThan => return Ok((s1 > s2).into()),
+ GreaterThanEqualsTo => return Ok((s1 >= s2).into()),
+ LessThan => return Ok((s1 < s2).into()),
+ LessThanEqualsTo => return Ok((s1 <= s2).into()),
+ _ => (),
+ },
+ _ => (),
+ }
+
+ let operands = &mut [&mut lhs, &mut rhs];
+
+ if let Some((func, need_context)) =
+ get_builtin_binary_op_fn(op_token.as_ref().unwrap(), operands[0], operands[1])
+ {
+ // We may not need to bump the level because built-in's do not need it.
+ //defer! { let orig_level = global.level; global.level += 1 }
+
+ let context =
+ need_context.then(|| (self, name.as_str(), None, &*global, pos).into());
+ return func(context, operands);
+ }
+
+ return self
+ .exec_fn_call(
+ global, caches, None, name, op_token, *hashes, operands, false, false, pos,
+ )
+ .map(|(v, ..)| v);
+ }
+
+ #[cfg(not(feature = "no_module"))]
+ if !namespace.is_empty() {
+ // Qualified function call
+ let hash = hashes.native();
+
+ return self.make_qualified_function_call(
+ global, caches, scope, this_ptr, namespace, name, args, hash, pos,
+ );
+ }
+
+ // Normal function call
+ let (first_arg, rest_args) = args.split_first().map_or_else(
+ || (None, args.as_ref()),
+ |(first, rest)| (Some(first), rest),
+ );
+
+ self.make_function_call(
+ global, caches, scope, this_ptr, name, op_token, first_arg, rest_args, *hashes,
+ *capture, pos,
+ )
+ }
+}
diff --git a/rhai/src/func/callable_function.rs b/rhai/src/func/callable_function.rs
new file mode 100644
index 0000000..3b66d9a
--- /dev/null
+++ b/rhai/src/func/callable_function.rs
@@ -0,0 +1,324 @@
+//! Module defining the standard Rhai function type.
+
+use super::native::{FnAny, FnPlugin, IteratorFn, SendSync};
+use crate::ast::FnAccess;
+use crate::plugin::PluginFunction;
+use crate::Shared;
+use std::fmt;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// _(internals)_ Encapsulated AST environment.
+/// Exported under the `internals` feature only.
+///
+/// 1) functions defined within the same AST
+/// 2) the stack of imported [modules][crate::Module]
+/// 3) global constants
+#[derive(Debug, Clone)]
+pub struct EncapsulatedEnviron {
+ /// Functions defined within the same [`AST`][crate::AST].
+ #[cfg(not(feature = "no_function"))]
+ pub lib: crate::SharedModule,
+ /// Imported [modules][crate::Module].
+ #[cfg(not(feature = "no_module"))]
+ pub imports: Box<crate::StaticVec<(crate::ImmutableString, crate::SharedModule)>>,
+ /// Globally-defined constants.
+ #[cfg(not(feature = "no_module"))]
+ #[cfg(not(feature = "no_function"))]
+ pub constants: Option<crate::eval::SharedGlobalConstants>,
+}
+
+/// _(internals)_ A type encapsulating a function callable by Rhai.
+/// Exported under the `internals` feature only.
+#[derive(Clone)]
+#[non_exhaustive]
+pub enum CallableFunction {
+ /// A pure native Rust function with all arguments passed by value.
+ Pure {
+ /// Shared function pointer.
+ func: Shared<FnAny>,
+ /// Does the function take a [`NativeCallContext`][crate::NativeCallContext] parameter?
+ has_context: bool,
+ /// This is a dummy field and is not used.
+ is_pure: bool,
+ },
+ /// A native Rust object method with the first argument passed by reference,
+ /// and the rest passed by value.
+ Method {
+ /// Shared function pointer.
+ func: Shared<FnAny>,
+ /// Does the function take a [`NativeCallContext`][crate::NativeCallContext] parameter?
+ has_context: bool,
+ /// Allow operating on constants?
+ is_pure: bool,
+ },
+ /// An iterator function.
+ Iterator {
+ /// Shared function pointer.
+ func: Shared<IteratorFn>,
+ },
+ /// A plugin function,
+ Plugin {
+ /// Shared function pointer.
+ func: Shared<FnPlugin>,
+ },
+ /// A script-defined function.
+ #[cfg(not(feature = "no_function"))]
+ Script {
+ /// Shared reference to the [`ScriptFnDef`][crate::ast::ScriptFnDef] function definition.
+ fn_def: Shared<crate::ast::ScriptFnDef>,
+ /// Encapsulated environment, if any.
+ environ: Option<Shared<EncapsulatedEnviron>>,
+ },
+}
+
+impl fmt::Debug for CallableFunction {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Pure { .. } => f.write_str("NativePureFunction"),
+ Self::Method { .. } => f.write_str("NativeMethod"),
+ Self::Iterator { .. } => f.write_str("NativeIterator"),
+ Self::Plugin { .. } => f.write_str("PluginFunction"),
+
+ #[cfg(not(feature = "no_function"))]
+ Self::Script { fn_def, .. } => fmt::Debug::fmt(fn_def, f),
+ }
+ }
+}
+
+impl fmt::Display for CallableFunction {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Pure { .. } => f.write_str("NativePureFunction"),
+ Self::Method { .. } => f.write_str("NativeMethod"),
+ Self::Iterator { .. } => f.write_str("NativeIterator"),
+ Self::Plugin { .. } => f.write_str("PluginFunction"),
+
+ #[cfg(not(feature = "no_function"))]
+ Self::Script { fn_def, .. } => fmt::Display::fmt(fn_def, f),
+ }
+ }
+}
+
+impl CallableFunction {
+ /// Is this a pure native Rust function?
+ #[inline]
+ #[must_use]
+ pub fn is_pure(&self) -> bool {
+ match self {
+ Self::Pure { .. } => true,
+ Self::Method { is_pure, .. } => *is_pure,
+ Self::Iterator { .. } => true,
+
+ Self::Plugin { func, .. } => func.is_pure(),
+
+ #[cfg(not(feature = "no_function"))]
+ Self::Script { .. } => false,
+ }
+ }
+ /// Is this a native Rust method function?
+ #[inline]
+ #[must_use]
+ pub fn is_method(&self) -> bool {
+ match self {
+ Self::Method { .. } => true,
+ Self::Pure { .. } | Self::Iterator { .. } => false,
+
+ Self::Plugin { func, .. } => func.is_method_call(),
+
+ #[cfg(not(feature = "no_function"))]
+ Self::Script { .. } => false,
+ }
+ }
+ /// Is this an iterator function?
+ #[inline]
+ #[must_use]
+ pub const fn is_iter(&self) -> bool {
+ match self {
+ Self::Iterator { .. } => true,
+ Self::Pure { .. } | Self::Method { .. } | Self::Plugin { .. } => false,
+
+ #[cfg(not(feature = "no_function"))]
+ Self::Script { .. } => false,
+ }
+ }
+ /// Is this a script-defined function?
+ #[inline]
+ #[must_use]
+ pub const fn is_script(&self) -> bool {
+ #[cfg(feature = "no_function")]
+ return false;
+
+ #[cfg(not(feature = "no_function"))]
+ match self {
+ Self::Script { .. } => true,
+ Self::Pure { .. }
+ | Self::Method { .. }
+ | Self::Iterator { .. }
+ | Self::Plugin { .. } => false,
+ }
+ }
+ /// Is this a plugin function?
+ #[inline]
+ #[must_use]
+ pub const fn is_plugin_fn(&self) -> bool {
+ match self {
+ Self::Plugin { .. } => true,
+ Self::Pure { .. } | Self::Method { .. } | Self::Iterator { .. } => false,
+
+ #[cfg(not(feature = "no_function"))]
+ Self::Script { .. } => false,
+ }
+ }
+ /// Is this a native Rust function?
+ #[inline]
+ #[must_use]
+ pub const fn is_native(&self) -> bool {
+ #[cfg(feature = "no_function")]
+ return true;
+
+ #[cfg(not(feature = "no_function"))]
+ match self {
+ Self::Pure { .. }
+ | Self::Method { .. }
+ | Self::Plugin { .. }
+ | Self::Iterator { .. } => true,
+ Self::Script { .. } => false,
+ }
+ }
+ /// Is there a [`NativeCallContext`][crate::NativeCallContext] parameter?
+ #[inline]
+ #[must_use]
+ pub fn has_context(&self) -> bool {
+ match self {
+ Self::Pure { has_context, .. } | Self::Method { has_context, .. } => *has_context,
+ Self::Plugin { func, .. } => func.has_context(),
+ Self::Iterator { .. } => false,
+ #[cfg(not(feature = "no_function"))]
+ Self::Script { .. } => false,
+ }
+ }
+ /// Get the access mode.
+ #[inline]
+ #[must_use]
+ pub fn access(&self) -> FnAccess {
+ #[cfg(feature = "no_function")]
+ return FnAccess::Public;
+
+ #[cfg(not(feature = "no_function"))]
+ match self {
+ Self::Plugin { .. }
+ | Self::Pure { .. }
+ | Self::Method { .. }
+ | Self::Iterator { .. } => FnAccess::Public,
+ Self::Script { fn_def, .. } => fn_def.access,
+ }
+ }
+ /// Get a shared reference to a native Rust function.
+ #[inline]
+ #[must_use]
+ pub fn get_native_fn(&self) -> Option<&Shared<FnAny>> {
+ match self {
+ Self::Pure { func, .. } | Self::Method { func, .. } => Some(func),
+ Self::Iterator { .. } | Self::Plugin { .. } => None,
+
+ #[cfg(not(feature = "no_function"))]
+ Self::Script { .. } => None,
+ }
+ }
+ /// Get a shared reference to a script-defined function definition.
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ #[inline]
+ #[must_use]
+ pub const fn get_script_fn_def(&self) -> Option<&Shared<crate::ast::ScriptFnDef>> {
+ match self {
+ Self::Pure { .. }
+ | Self::Method { .. }
+ | Self::Iterator { .. }
+ | Self::Plugin { .. } => None,
+ Self::Script { fn_def, .. } => Some(fn_def),
+ }
+ }
+ /// Get a reference to the shared encapsulated environment of the function definition.
+ ///
+ /// Not available under `no_function` or `no_module`.
+ #[inline]
+ #[must_use]
+ pub fn get_encapsulated_environ(&self) -> Option<&EncapsulatedEnviron> {
+ match self {
+ Self::Pure { .. }
+ | Self::Method { .. }
+ | Self::Iterator { .. }
+ | Self::Plugin { .. } => None,
+
+ #[cfg(not(feature = "no_function"))]
+ Self::Script { environ, .. } => environ.as_deref(),
+ }
+ }
+ /// Get a reference to an iterator function.
+ #[inline]
+ #[must_use]
+ pub fn get_iter_fn(&self) -> Option<&IteratorFn> {
+ match self {
+ Self::Iterator { func, .. } => Some(&**func),
+ Self::Pure { .. } | Self::Method { .. } | Self::Plugin { .. } => None,
+
+ #[cfg(not(feature = "no_function"))]
+ Self::Script { .. } => None,
+ }
+ }
+ /// Get a shared reference to a plugin function.
+ #[inline]
+ #[must_use]
+ pub fn get_plugin_fn(&self) -> Option<&Shared<FnPlugin>> {
+ match self {
+ Self::Plugin { func, .. } => Some(func),
+ Self::Pure { .. } | Self::Method { .. } | Self::Iterator { .. } => None,
+
+ #[cfg(not(feature = "no_function"))]
+ Self::Script { .. } => None,
+ }
+ }
+}
+
+#[cfg(not(feature = "no_function"))]
+impl From<crate::ast::ScriptFnDef> for CallableFunction {
+ #[inline(always)]
+ fn from(fn_def: crate::ast::ScriptFnDef) -> Self {
+ Self::Script {
+ fn_def: fn_def.into(),
+ environ: None,
+ }
+ }
+}
+
+#[cfg(not(feature = "no_function"))]
+impl From<Shared<crate::ast::ScriptFnDef>> for CallableFunction {
+ #[inline(always)]
+ fn from(fn_def: Shared<crate::ast::ScriptFnDef>) -> Self {
+ Self::Script {
+ fn_def,
+ environ: None,
+ }
+ }
+}
+
+impl<T: PluginFunction + 'static + SendSync> From<T> for CallableFunction {
+ #[inline(always)]
+ fn from(func: T) -> Self {
+ Self::Plugin {
+ func: Shared::new(func),
+ }
+ }
+}
+
+impl From<Shared<FnPlugin>> for CallableFunction {
+ #[inline(always)]
+ fn from(func: Shared<FnPlugin>) -> Self {
+ Self::Plugin { func }
+ }
+}
diff --git a/rhai/src/func/func.rs b/rhai/src/func/func.rs
new file mode 100644
index 0000000..f357389
--- /dev/null
+++ b/rhai/src/func/func.rs
@@ -0,0 +1,117 @@
+//! Module which defines the function registration mechanism.
+
+#![cfg(not(feature = "no_function"))]
+#![allow(non_snake_case)]
+
+use crate::parser::ParseResult;
+use crate::types::dynamic::Variant;
+use crate::{Engine, RhaiResultOf, Scope, SmartString, AST};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// Trait to create a Rust closure from a script.
+///
+/// Not available under `no_function`.
+pub trait Func<A, R> {
+ /// The closure's output type.
+ type Output;
+
+ /// Create a Rust closure from an [`AST`].
+ ///
+ /// The [`Engine`] and [`AST`] are consumed and basically embedded into the closure.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Func}; // use 'Func' for 'create_from_ast'
+ ///
+ /// let engine = Engine::new(); // create a new 'Engine' just for this
+ ///
+ /// let ast = engine.compile("fn calc(x, y) { x + len(y) < 42 }")?;
+ ///
+ /// // Func takes two type parameters:
+ /// // 1) a tuple made up of the types of the script function's parameters
+ /// // 2) the return type of the script function
+ ///
+ /// // 'func' will have type Box<dyn Fn(i64, String) -> Result<bool, Box<EvalAltResult>>> and is callable!
+ /// let func = Func::<(i64, &str), bool>::create_from_ast(
+ /// // ^^^^^^^^^^^ function parameter types in tuple
+ ///
+ /// engine, // the 'Engine' is consumed into the closure
+ /// ast, // the 'AST'
+ /// "calc" // the entry-point function name
+ /// );
+ ///
+ /// func(123, "hello")? == false; // call the anonymous function
+ /// # Ok(())
+ /// # }
+ #[must_use]
+ fn create_from_ast(self, ast: AST, entry_point: &str) -> Self::Output;
+
+ /// Create a Rust closure from a script.
+ ///
+ /// The [`Engine`] is consumed and basically embedded into the closure.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Func}; // use 'Func' for 'create_from_script'
+ ///
+ /// let engine = Engine::new(); // create a new 'Engine' just for this
+ ///
+ /// let script = "fn calc(x, y) { x + len(y) < 42 }";
+ ///
+ /// // Func takes two type parameters:
+ /// // 1) a tuple made up of the types of the script function's parameters
+ /// // 2) the return type of the script function
+ ///
+ /// // 'func' will have type Box<dyn Fn(i64, String) -> Result<bool, Box<EvalAltResult>>> and is callable!
+ /// let func = Func::<(i64, &str), bool>::create_from_script(
+ /// // ^^^^^^^^^^^ function parameter types in tuple
+ ///
+ /// engine, // the 'Engine' is consumed into the closure
+ /// script, // the script, notice number of parameters must match
+ /// "calc" // the entry-point function name
+ /// )?;
+ ///
+ /// func(123, "hello")? == false; // call the anonymous function
+ /// # Ok(())
+ /// # }
+ /// ```
+ fn create_from_script(self, script: &str, entry_point: &str) -> ParseResult<Self::Output>;
+}
+
+macro_rules! def_anonymous_fn {
+ () => {
+ def_anonymous_fn!(imp);
+ };
+ (imp $($par:ident),*) => {
+ impl<$($par: Variant + Clone,)* RET: Variant + Clone> Func<($($par,)*), RET> for Engine
+ {
+ #[cfg(feature = "sync")]
+ type Output = Box<dyn Fn($($par,)*) -> RhaiResultOf<RET> + Send + Sync>;
+ #[cfg(not(feature = "sync"))]
+ type Output = Box<dyn Fn($($par,)*) -> RhaiResultOf<RET>>;
+
+ #[inline]
+ fn create_from_ast(self, ast: AST, entry_point: &str) -> Self::Output {
+ let fn_name: SmartString = entry_point.into();
+ Box::new(move |$($par,)*| self.call_fn(&mut Scope::new(), &ast, &fn_name, ($($par,)*)))
+ }
+
+ #[inline]
+ fn create_from_script(self, script: &str, entry_point: &str) -> ParseResult<Self::Output> {
+ let ast = self.compile(script)?;
+ Ok(Func::<($($par,)*), RET>::create_from_ast(self, ast, entry_point))
+ }
+ }
+ };
+ ($p0:ident $(, $p:ident)*) => {
+ def_anonymous_fn!(imp $p0 $(, $p)*);
+ def_anonymous_fn!($($p),*);
+ };
+}
+
+def_anonymous_fn!(A, B, C, D, E, F, G, H, J, K, L, M, N, P, Q, R, S, T, U, V);
diff --git a/rhai/src/func/hashing.rs b/rhai/src/func/hashing.rs
new file mode 100644
index 0000000..227921b
--- /dev/null
+++ b/rhai/src/func/hashing.rs
@@ -0,0 +1,168 @@
+//! Module containing utilities to hash functions and function calls.
+
+use crate::config;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ any::TypeId,
+ hash::{BuildHasher, Hash, Hasher},
+};
+
+#[cfg(feature = "no_std")]
+pub type StraightHashMap<V> = hashbrown::HashMap<u64, V, StraightHasherBuilder>;
+
+#[cfg(not(feature = "no_std"))]
+pub type StraightHashMap<V> = std::collections::HashMap<u64, V, StraightHasherBuilder>;
+/// A hasher that only takes one single [`u64`] and returns it as a hash key.
+///
+/// # Panics
+///
+/// Panics when hashing any data type other than a [`u64`].
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct StraightHasher(u64);
+
+impl Hasher for StraightHasher {
+ #[inline(always)]
+ #[must_use]
+ fn finish(&self) -> u64 {
+ self.0
+ }
+ #[cold]
+ #[inline(never)]
+ fn write(&mut self, _bytes: &[u8]) {
+ panic!("StraightHasher can only hash u64 values");
+ }
+ #[inline(always)]
+ fn write_u64(&mut self, i: u64) {
+ self.0 = i;
+ }
+}
+
+/// A hash builder for `StraightHasher`.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Default)]
+pub struct StraightHasherBuilder;
+
+impl BuildHasher for StraightHasherBuilder {
+ type Hasher = StraightHasher;
+
+ #[inline(always)]
+ #[must_use]
+ fn build_hasher(&self) -> Self::Hasher {
+ StraightHasher(0)
+ }
+}
+
+/// Create an instance of the default hasher.
+#[inline(always)]
+#[must_use]
+pub fn get_hasher() -> ahash::AHasher {
+ match config::hashing::get_ahash_seed() {
+ Some([seed1, seed2, seed3, seed4]) if (seed1 | seed2 | seed3 | seed4) != 0 => {
+ ahash::RandomState::with_seeds(*seed1, *seed2, *seed3, *seed4).build_hasher()
+ }
+ _ => ahash::AHasher::default(),
+ }
+}
+
+/// Calculate a [`u64`] hash key from a namespace-qualified variable name.
+///
+/// Module names are passed in via `&str` references from an iterator.
+/// Parameter types are passed in via [`TypeId`] values from an iterator.
+///
+/// # Note
+///
+/// The first module name is skipped. Hashing starts from the _second_ module in the chain.
+#[inline]
+#[must_use]
+pub fn calc_var_hash<'a>(namespace: impl IntoIterator<Item = &'a str>, var_name: &str) -> u64 {
+ let s = &mut get_hasher();
+
+ s.write_u8(b'V'); // hash a discriminant
+
+ let mut count = 0;
+
+ // We always skip the first module
+ namespace.into_iter().for_each(|m| {
+ // We always skip the first module
+ if count > 0 {
+ m.hash(s);
+ }
+ count += 1;
+ });
+ s.write_usize(count);
+ var_name.hash(s);
+
+ s.finish()
+}
+
+/// Calculate a [`u64`] hash key from a namespace-qualified function name
+/// and the number of parameters, but no parameter types.
+///
+/// Module names making up the namespace are passed in via `&str` references from an iterator.
+/// Parameter types are passed in via [`TypeId`] values from an iterator.
+///
+/// If the function is not namespace-qualified, pass [`None`] as the namespace.
+///
+/// # Note
+///
+/// The first module name is skipped. Hashing starts from the _second_ module in the chain.
+#[inline]
+#[must_use]
+pub fn calc_fn_hash<'a>(
+ namespace: impl IntoIterator<Item = &'a str>,
+ fn_name: &str,
+ num: usize,
+) -> u64 {
+ let s = &mut get_hasher();
+
+ s.write_u8(b'F'); // hash a discriminant
+
+ let mut count = 0;
+
+ namespace.into_iter().for_each(|m| {
+ // We always skip the first module
+ if count > 0 {
+ m.hash(s);
+ }
+ count += 1;
+ });
+ s.write_usize(count);
+ fn_name.hash(s);
+ s.write_usize(num);
+
+ s.finish()
+}
+
+/// Calculate a [`u64`] hash key from a base [`u64`] hash key and a list of parameter types.
+///
+/// Parameter types are passed in via [`TypeId`] values from an iterator.
+#[inline]
+#[must_use]
+pub fn calc_fn_hash_full(base: u64, params: impl IntoIterator<Item = TypeId>) -> u64 {
+ let s = &mut get_hasher();
+
+ s.write_u8(b'A'); // hash a discriminant
+
+ let mut count = 0;
+ params.into_iter().for_each(|t| {
+ t.hash(s);
+ count += 1;
+ });
+ s.write_usize(count);
+
+ s.finish() ^ base
+}
+
+/// Calculate a [`u64`] hash key from a base [`u64`] hash key and the type of the `this` pointer.
+#[cfg(not(feature = "no_object"))]
+#[cfg(not(feature = "no_function"))]
+#[inline]
+#[must_use]
+pub fn calc_typed_method_hash(base: u64, this_type: &str) -> u64 {
+ let s = &mut get_hasher();
+
+ s.write_u8(b'T'); // hash a discriminant
+ this_type.hash(s);
+
+ s.finish() ^ base
+}
diff --git a/rhai/src/func/mod.rs b/rhai/src/func/mod.rs
new file mode 100644
index 0000000..bf9f60c
--- /dev/null
+++ b/rhai/src/func/mod.rs
@@ -0,0 +1,36 @@
+//! Module defining mechanisms to handle function calls in Rhai.
+
+pub mod args;
+pub mod builtin;
+pub mod call;
+pub mod callable_function;
+pub mod func;
+pub mod hashing;
+pub mod native;
+pub mod plugin;
+pub mod register;
+pub mod script;
+
+pub use args::FuncArgs;
+pub use builtin::{get_builtin_binary_op_fn, get_builtin_op_assignment_fn};
+#[cfg(not(feature = "no_closure"))]
+pub use call::ensure_no_data_race;
+#[cfg(not(feature = "no_function"))]
+pub use call::is_anonymous_fn;
+pub use call::FnCallArgs;
+pub use callable_function::{CallableFunction, EncapsulatedEnviron};
+#[cfg(not(feature = "no_function"))]
+pub use func::Func;
+#[cfg(not(feature = "no_object"))]
+#[cfg(not(feature = "no_function"))]
+pub use hashing::calc_typed_method_hash;
+pub use hashing::{calc_fn_hash, calc_fn_hash_full, calc_var_hash, get_hasher, StraightHashMap};
+#[cfg(feature = "internals")]
+#[allow(deprecated)]
+pub use native::NativeCallContextStore;
+pub use native::{
+ locked_read, locked_write, shared_get_mut, shared_make_mut, shared_take, shared_take_or_clone,
+ shared_try_take, FnAny, FnPlugin, IteratorFn, Locked, NativeCallContext, SendSync, Shared,
+};
+pub use plugin::PluginFunction;
+pub use register::RegisterNativeFunction;
diff --git a/rhai/src/func/native.rs b/rhai/src/func/native.rs
new file mode 100644
index 0000000..2886363
--- /dev/null
+++ b/rhai/src/func/native.rs
@@ -0,0 +1,634 @@
+//! Module defining interfaces to native-Rust functions.
+
+use super::call::FnCallArgs;
+use crate::ast::FnCallHashes;
+use crate::eval::{Caches, GlobalRuntimeState};
+use crate::plugin::PluginFunction;
+use crate::tokenizer::{is_valid_function_name, Token, TokenizeState};
+use crate::types::dynamic::Variant;
+use crate::{
+ calc_fn_hash, Dynamic, Engine, EvalContext, FnArgsVec, FuncArgs, Position, RhaiResult,
+ RhaiResultOf, StaticVec, VarDefInfo, ERR,
+};
+use std::any::type_name;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// Trait that maps to `Send + Sync` only under the `sync` feature.
+#[cfg(feature = "sync")]
+pub trait SendSync: Send + Sync {}
+/// Trait that maps to `Send + Sync` only under the `sync` feature.
+#[cfg(feature = "sync")]
+impl<T: Send + Sync> SendSync for T {}
+
+/// Trait that maps to `Send + Sync` only under the `sync` feature.
+#[cfg(not(feature = "sync"))]
+pub trait SendSync {}
+/// Trait that maps to `Send + Sync` only under the `sync` feature.
+#[cfg(not(feature = "sync"))]
+impl<T> SendSync for T {}
+
+/// Immutable reference-counted container.
+#[cfg(not(feature = "sync"))]
+pub use std::rc::Rc as Shared;
+/// Immutable reference-counted container.
+#[cfg(feature = "sync")]
+pub use std::sync::Arc as Shared;
+
+/// Synchronized shared object.
+#[cfg(not(feature = "sync"))]
+pub use std::cell::RefCell as Locked;
+
+/// Read-only lock guard for synchronized shared object.
+#[cfg(not(feature = "sync"))]
+pub type LockGuard<'a, T> = std::cell::Ref<'a, T>;
+
+/// Mutable lock guard for synchronized shared object.
+#[cfg(not(feature = "sync"))]
+pub type LockGuardMut<'a, T> = std::cell::RefMut<'a, T>;
+
+/// Synchronized shared object.
+#[cfg(feature = "sync")]
+#[allow(dead_code)]
+pub use std::sync::RwLock as Locked;
+
+/// Read-only lock guard for synchronized shared object.
+#[cfg(feature = "sync")]
+#[allow(dead_code)]
+pub type LockGuard<'a, T> = std::sync::RwLockReadGuard<'a, T>;
+
+/// Mutable lock guard for synchronized shared object.
+#[cfg(feature = "sync")]
+#[allow(dead_code)]
+pub type LockGuardMut<'a, T> = std::sync::RwLockWriteGuard<'a, T>;
+
+/// Context of a native Rust function call.
+#[derive(Debug)]
+pub struct NativeCallContext<'a> {
+ /// The current [`Engine`].
+ engine: &'a Engine,
+ /// Name of function called.
+ fn_name: &'a str,
+ /// Function source, if any.
+ source: Option<&'a str>,
+ /// The current [`GlobalRuntimeState`], if any.
+ global: &'a GlobalRuntimeState,
+ /// [Position] of the function call.
+ pos: Position,
+}
+
+/// _(internals)_ Context of a native Rust function call.
+/// Exported under the `internals` feature only.
+///
+/// # WARNING - Volatile Type
+///
+/// This type is volatile and may change in the future.
+#[deprecated = "This type is NOT deprecated, but it is considered volatile and may change in the future."]
+#[cfg(feature = "internals")]
+#[derive(Debug, Clone)]
+pub struct NativeCallContextStore {
+ /// Name of function called.
+ pub fn_name: String,
+ /// Function source, if any.
+ pub source: Option<String>,
+ /// The current [`GlobalRuntimeState`], if any.
+ pub global: GlobalRuntimeState,
+ /// [Position] of the function call.
+ pub pos: Position,
+}
+
+#[cfg(feature = "internals")]
+#[allow(deprecated)]
+impl NativeCallContextStore {
+ /// Create a [`NativeCallContext`] from a [`NativeCallContextStore`].
+ ///
+ /// # WARNING - Unstable API
+ ///
+ /// This API is volatile and may change in the future.
+ #[deprecated = "This API is NOT deprecated, but it is considered volatile and may change in the future."]
+ #[inline(always)]
+ #[must_use]
+ pub fn create_context<'a>(&'a self, engine: &'a Engine) -> NativeCallContext<'a> {
+ NativeCallContext::from_stored_data(engine, self)
+ }
+}
+
+impl<'a>
+ From<(
+ &'a Engine,
+ &'a str,
+ Option<&'a str>,
+ &'a GlobalRuntimeState,
+ Position,
+ )> for NativeCallContext<'a>
+{
+ #[inline(always)]
+ fn from(
+ value: (
+ &'a Engine,
+ &'a str,
+ Option<&'a str>,
+ &'a GlobalRuntimeState,
+ Position,
+ ),
+ ) -> Self {
+ Self {
+ engine: value.0,
+ fn_name: value.1,
+ source: value.2,
+ global: value.3,
+ pos: value.4,
+ }
+ }
+}
+
+impl<'a> NativeCallContext<'a> {
+ /// _(internals)_ Create a new [`NativeCallContext`].
+ /// Exported under the `internals` feature only.
+ ///
+ /// Not available under `no_module`.
+ #[cfg(feature = "internals")]
+ #[cfg(not(feature = "no_module"))]
+ #[inline(always)]
+ #[must_use]
+ pub const fn new_with_all_fields(
+ engine: &'a Engine,
+ fn_name: &'a str,
+ source: Option<&'a str>,
+ global: &'a GlobalRuntimeState,
+ pos: Position,
+ ) -> Self {
+ Self {
+ engine,
+ fn_name,
+ source,
+ global,
+ pos,
+ }
+ }
+
+ /// _(internals)_ Create a [`NativeCallContext`] from a [`NativeCallContextStore`].
+ /// Exported under the `internals` feature only.
+ ///
+ /// # WARNING - Unstable API
+ ///
+ /// This API is volatile and may change in the future.
+ #[deprecated = "This API is NOT deprecated, but it is considered volatile and may change in the future."]
+ #[cfg(feature = "internals")]
+ #[inline]
+ #[must_use]
+ #[allow(deprecated)]
+ pub fn from_stored_data(engine: &'a Engine, context: &'a NativeCallContextStore) -> Self {
+ Self {
+ engine,
+ fn_name: &context.fn_name,
+ source: context.source.as_deref(),
+ global: &context.global,
+ pos: context.pos,
+ }
+ }
+ /// _(internals)_ Store this [`NativeCallContext`] into a [`NativeCallContextStore`].
+ /// Exported under the `internals` feature only.
+ ///
+ /// # WARNING - Unstable API
+ ///
+ /// This API is volatile and may change in the future.
+ #[deprecated = "This API is NOT deprecated, but it is considered volatile and may change in the future."]
+ #[cfg(feature = "internals")]
+ #[inline]
+ #[must_use]
+ #[allow(deprecated)]
+ pub fn store_data(&self) -> NativeCallContextStore {
+ NativeCallContextStore {
+ fn_name: self.fn_name.to_string(),
+ source: self.source.map(ToString::to_string),
+ global: self.global.clone(),
+ pos: self.pos,
+ }
+ }
+
+ /// The current [`Engine`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn engine(&self) -> &Engine {
+ self.engine
+ }
+ /// Name of the function called.
+ #[inline(always)]
+ #[must_use]
+ pub const fn fn_name(&self) -> &str {
+ self.fn_name
+ }
+ /// [Position] of the function call.
+ #[inline(always)]
+ #[must_use]
+ pub const fn position(&self) -> Position {
+ self.pos
+ }
+ /// Current nesting level of function calls.
+ #[inline(always)]
+ #[must_use]
+ pub const fn call_level(&self) -> usize {
+ self.global.level
+ }
+ /// The current source.
+ #[inline(always)]
+ #[must_use]
+ pub const fn source(&self) -> Option<&str> {
+ self.source
+ }
+ /// Custom state kept in a [`Dynamic`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn tag(&self) -> Option<&Dynamic> {
+ Some(&self.global.tag)
+ }
+ /// Get an iterator over the current set of modules imported via `import` statements
+ /// in reverse order.
+ ///
+ /// Not available under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ pub fn iter_imports(&self) -> impl Iterator<Item = (&str, &crate::Module)> {
+ self.global.iter_imports()
+ }
+ /// Get an iterator over the current set of modules imported via `import` statements in reverse order.
+ #[cfg(not(feature = "no_module"))]
+ #[allow(dead_code)]
+ #[inline]
+ pub(crate) fn iter_imports_raw(
+ &self,
+ ) -> impl Iterator<Item = (&crate::ImmutableString, &crate::SharedModule)> {
+ self.global.iter_imports_raw()
+ }
+ /// _(internals)_ The current [`GlobalRuntimeState`], if any.
+ /// Exported under the `internals` feature only.
+ ///
+ /// Not available under `no_module`.
+ #[cfg(feature = "internals")]
+ #[inline(always)]
+ #[must_use]
+ pub const fn global_runtime_state(&self) -> &GlobalRuntimeState {
+ self.global
+ }
+ /// _(internals)_ The current [`GlobalRuntimeState`], if any.
+ #[cfg(not(feature = "internals"))]
+ #[inline(always)]
+ #[must_use]
+ #[allow(dead_code)]
+ pub(crate) const fn global_runtime_state(&self) -> &GlobalRuntimeState {
+ self.global
+ }
+ /// Get an iterator over the namespaces containing definitions of all script-defined functions
+ /// in reverse order (i.e. parent namespaces are iterated after child namespaces).
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ #[inline]
+ pub fn iter_namespaces(&self) -> impl Iterator<Item = &crate::Module> {
+ self.global.lib.iter().map(AsRef::as_ref)
+ }
+ /// _(internals)_ The current stack of namespaces containing definitions of all script-defined functions.
+ /// Exported under the `internals` feature only.
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(feature = "internals")]
+ #[inline(always)]
+ #[must_use]
+ pub fn namespaces(&self) -> &[crate::SharedModule] {
+ &self.global.lib
+ }
+ /// Call a function inside the call context with the provided arguments.
+ #[inline]
+ pub fn call_fn<T: Variant + Clone>(
+ &self,
+ fn_name: impl AsRef<str>,
+ args: impl FuncArgs,
+ ) -> RhaiResultOf<T> {
+ let mut arg_values = StaticVec::new_const();
+ args.parse(&mut arg_values);
+
+ let args = &mut arg_values.iter_mut().collect::<FnArgsVec<_>>();
+
+ self._call_fn_raw(fn_name, args, false, false, false)
+ .and_then(|result| {
+ result.try_cast_raw().map_err(|r| {
+ let result_type = self.engine().map_type_name(r.type_name());
+ let cast_type = match type_name::<T>() {
+ typ @ _ if typ.contains("::") => self.engine.map_type_name(typ),
+ typ @ _ => typ,
+ };
+ ERR::ErrorMismatchOutputType(
+ cast_type.into(),
+ result_type.into(),
+ Position::NONE,
+ )
+ .into()
+ })
+ })
+ }
+ /// Call a registered native Rust function inside the call context with the provided arguments.
+ ///
+ /// This is often useful because Rust functions typically only want to cross-call other
+ /// registered Rust functions and not have to worry about scripted functions hijacking the
+ /// process unknowingly (or deliberately).
+ #[inline]
+ pub fn call_native_fn<T: Variant + Clone>(
+ &self,
+ fn_name: impl AsRef<str>,
+ args: impl FuncArgs,
+ ) -> RhaiResultOf<T> {
+ let mut arg_values = StaticVec::new_const();
+ args.parse(&mut arg_values);
+
+ let args = &mut arg_values.iter_mut().collect::<FnArgsVec<_>>();
+
+ self._call_fn_raw(fn_name, args, true, false, false)
+ .and_then(|result| {
+ result.try_cast_raw().map_err(|r| {
+ let result_type = self.engine().map_type_name(r.type_name());
+ let cast_type = match type_name::<T>() {
+ typ @ _ if typ.contains("::") => self.engine.map_type_name(typ),
+ typ @ _ => typ,
+ };
+ ERR::ErrorMismatchOutputType(
+ cast_type.into(),
+ result_type.into(),
+ Position::NONE,
+ )
+ .into()
+ })
+ })
+ }
+ /// Call a function (native Rust or scripted) inside the call context.
+ ///
+ /// If `is_method_call` is [`true`], the first argument is assumed to be the `this` pointer for
+ /// a script-defined function (or the object of a method call).
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is very low level.
+ ///
+ /// # Arguments
+ ///
+ /// All arguments may be _consumed_, meaning that they may be replaced by `()`. This is to avoid
+ /// unnecessarily cloning the arguments.
+ ///
+ /// **DO NOT** reuse the arguments after this call. If they are needed afterwards, clone them
+ /// _before_ calling this function.
+ ///
+ /// If `is_ref_mut` is [`true`], the first argument is assumed to be passed by reference and is
+ /// not consumed.
+ #[inline(always)]
+ pub fn call_fn_raw(
+ &self,
+ fn_name: impl AsRef<str>,
+ is_ref_mut: bool,
+ is_method_call: bool,
+ args: &mut [&mut Dynamic],
+ ) -> RhaiResult {
+ let name = fn_name.as_ref();
+ let native_only = !is_valid_function_name(name);
+ #[cfg(not(feature = "no_function"))]
+ let native_only = native_only && !crate::parser::is_anonymous_fn(name);
+
+ self._call_fn_raw(fn_name, args, native_only, is_ref_mut, is_method_call)
+ }
+ /// Call a registered native Rust function inside the call context.
+ ///
+ /// This is often useful because Rust functions typically only want to cross-call other
+ /// registered Rust functions and not have to worry about scripted functions hijacking the
+ /// process unknowingly (or deliberately).
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is very low level.
+ ///
+ /// # Arguments
+ ///
+ /// All arguments may be _consumed_, meaning that they may be replaced by `()`. This is to avoid
+ /// unnecessarily cloning the arguments.
+ ///
+ /// **DO NOT** reuse the arguments after this call. If they are needed afterwards, clone them
+ /// _before_ calling this function.
+ ///
+ /// If `is_ref_mut` is [`true`], the first argument is assumed to be passed by reference and is
+ /// not consumed.
+ #[inline(always)]
+ pub fn call_native_fn_raw(
+ &self,
+ fn_name: impl AsRef<str>,
+ is_ref_mut: bool,
+ args: &mut [&mut Dynamic],
+ ) -> RhaiResult {
+ self._call_fn_raw(fn_name, args, true, is_ref_mut, false)
+ }
+
+ /// Call a function (native Rust or scripted) inside the call context.
+ fn _call_fn_raw(
+ &self,
+ fn_name: impl AsRef<str>,
+ args: &mut [&mut Dynamic],
+ native_only: bool,
+ is_ref_mut: bool,
+ is_method_call: bool,
+ ) -> RhaiResult {
+ let global = &mut self.global.clone();
+ global.level += 1;
+
+ let caches = &mut Caches::new();
+
+ let fn_name = fn_name.as_ref();
+ let op_token = Token::lookup_symbol_from_syntax(fn_name);
+ let args_len = args.len();
+
+ if native_only {
+ return self
+ .engine()
+ .exec_native_fn_call(
+ global,
+ caches,
+ fn_name,
+ op_token.as_ref(),
+ calc_fn_hash(None, fn_name, args_len),
+ args,
+ is_ref_mut,
+ Position::NONE,
+ )
+ .map(|(r, ..)| r);
+ }
+
+ // Native or script
+
+ let hash = match is_method_call {
+ #[cfg(not(feature = "no_function"))]
+ true => FnCallHashes::from_script_and_native(
+ calc_fn_hash(None, fn_name, args_len - 1),
+ calc_fn_hash(None, fn_name, args_len),
+ ),
+ #[cfg(feature = "no_function")]
+ true => FnCallHashes::from_native_only(calc_fn_hash(None, fn_name, args_len)),
+ _ => FnCallHashes::from_hash(calc_fn_hash(None, fn_name, args_len)),
+ };
+
+ self.engine()
+ .exec_fn_call(
+ global,
+ caches,
+ None,
+ fn_name,
+ op_token.as_ref(),
+ hash,
+ args,
+ is_ref_mut,
+ is_method_call,
+ Position::NONE,
+ )
+ .map(|(r, ..)| r)
+ }
+}
+
+/// Return a mutable reference to the wrapped value of a [`Shared`] resource.
+/// If the resource is shared (i.e. has other outstanding references), a cloned copy is used.
+#[inline(always)]
+#[must_use]
+#[allow(dead_code)]
+pub fn shared_make_mut<T: Clone>(value: &mut Shared<T>) -> &mut T {
+ Shared::make_mut(value)
+}
+
+/// Return a mutable reference to the wrapped value of a [`Shared`] resource.
+#[inline(always)]
+#[must_use]
+#[allow(dead_code)]
+pub fn shared_get_mut<T: Clone>(value: &mut Shared<T>) -> Option<&mut T> {
+ Shared::get_mut(value)
+}
+
+/// Consume a [`Shared`] resource if is unique (i.e. not shared), or clone it otherwise.
+#[inline]
+#[must_use]
+#[allow(dead_code)]
+pub fn shared_take_or_clone<T: Clone>(value: Shared<T>) -> T {
+ shared_try_take(value).unwrap_or_else(|v| v.as_ref().clone())
+}
+
+/// Consume a [`Shared`] resource if is unique (i.e. not shared).
+#[inline(always)]
+#[allow(dead_code)]
+pub fn shared_try_take<T>(value: Shared<T>) -> Result<T, Shared<T>> {
+ Shared::try_unwrap(value)
+}
+
+/// Consume a [`Shared`] resource, assuming that it is unique (i.e. not shared).
+///
+/// # Panics
+///
+/// Panics if the resource is shared (i.e. has other outstanding references).
+#[inline]
+#[must_use]
+#[allow(dead_code)]
+pub fn shared_take<T>(value: Shared<T>) -> T {
+ shared_try_take(value).ok().expect("not shared")
+}
+
+/// _(internals)_ Lock a [`Locked`] resource for mutable access.
+/// Exported under the `internals` feature only.
+#[inline(always)]
+#[must_use]
+#[allow(dead_code)]
+pub fn locked_read<T>(value: &Locked<T>) -> LockGuard<T> {
+ #[cfg(not(feature = "sync"))]
+ return value.borrow();
+
+ #[cfg(feature = "sync")]
+ return value.read().unwrap();
+}
+
+/// _(internals)_ Lock a [`Locked`] resource for mutable access.
+/// Exported under the `internals` feature only.
+#[inline(always)]
+#[must_use]
+#[allow(dead_code)]
+pub fn locked_write<T>(value: &Locked<T>) -> LockGuardMut<T> {
+ #[cfg(not(feature = "sync"))]
+ return value.borrow_mut();
+
+ #[cfg(feature = "sync")]
+ return value.write().unwrap();
+}
+
+/// General Rust function trail object.
+#[cfg(not(feature = "sync"))]
+pub type FnAny = dyn Fn(Option<NativeCallContext>, &mut FnCallArgs) -> RhaiResult;
+/// General Rust function trail object.
+#[cfg(feature = "sync")]
+pub type FnAny = dyn Fn(Option<NativeCallContext>, &mut FnCallArgs) -> RhaiResult + Send + Sync;
+
+/// Built-in function trait object.
+pub type FnBuiltin = (
+ fn(Option<NativeCallContext>, &mut FnCallArgs) -> RhaiResult,
+ bool,
+);
+
+/// Function that gets an iterator from a type.
+#[cfg(not(feature = "sync"))]
+pub type IteratorFn = dyn Fn(Dynamic) -> Box<dyn Iterator<Item = RhaiResultOf<Dynamic>>>;
+/// Function that gets an iterator from a type.
+#[cfg(feature = "sync")]
+pub type IteratorFn =
+ dyn Fn(Dynamic) -> Box<dyn Iterator<Item = RhaiResultOf<Dynamic>>> + Send + Sync;
+
+/// Plugin function trait object.
+#[cfg(not(feature = "sync"))]
+pub type FnPlugin = dyn PluginFunction;
+/// Plugin function trait object.
+#[cfg(feature = "sync")]
+pub type FnPlugin = dyn PluginFunction + Send + Sync;
+
+/// Callback function for progress reporting.
+#[cfg(not(feature = "unchecked"))]
+#[cfg(not(feature = "sync"))]
+pub type OnProgressCallback = dyn Fn(u64) -> Option<Dynamic>;
+/// Callback function for progress reporting.
+#[cfg(not(feature = "unchecked"))]
+#[cfg(feature = "sync")]
+pub type OnProgressCallback = dyn Fn(u64) -> Option<Dynamic> + Send + Sync;
+
+/// Callback function for printing.
+#[cfg(not(feature = "sync"))]
+pub type OnPrintCallback = dyn Fn(&str);
+/// Callback function for printing.
+#[cfg(feature = "sync")]
+pub type OnPrintCallback = dyn Fn(&str) + Send + Sync;
+
+/// Callback function for debugging.
+#[cfg(not(feature = "sync"))]
+pub type OnDebugCallback = dyn Fn(&str, Option<&str>, Position);
+/// Callback function for debugging.
+#[cfg(feature = "sync")]
+pub type OnDebugCallback = dyn Fn(&str, Option<&str>, Position) + Send + Sync;
+
+/// Callback function for mapping tokens during parsing.
+#[cfg(not(feature = "sync"))]
+pub type OnParseTokenCallback = dyn Fn(Token, Position, &TokenizeState) -> Token;
+/// Callback function for mapping tokens during parsing.
+#[cfg(feature = "sync")]
+pub type OnParseTokenCallback = dyn Fn(Token, Position, &TokenizeState) -> Token + Send + Sync;
+
+/// Callback function for variable access.
+#[cfg(not(feature = "sync"))]
+pub type OnVarCallback = dyn Fn(&str, usize, EvalContext) -> RhaiResultOf<Option<Dynamic>>;
+/// Callback function for variable access.
+#[cfg(feature = "sync")]
+pub type OnVarCallback =
+ dyn Fn(&str, usize, EvalContext) -> RhaiResultOf<Option<Dynamic>> + Send + Sync;
+
+/// Callback function for variable definition.
+#[cfg(not(feature = "sync"))]
+pub type OnDefVarCallback = dyn Fn(bool, VarDefInfo, EvalContext) -> RhaiResultOf<bool>;
+/// Callback function for variable definition.
+#[cfg(feature = "sync")]
+pub type OnDefVarCallback =
+ dyn Fn(bool, VarDefInfo, EvalContext) -> RhaiResultOf<bool> + Send + Sync;
diff --git a/rhai/src/func/plugin.rs b/rhai/src/func/plugin.rs
new file mode 100644
index 0000000..831e691
--- /dev/null
+++ b/rhai/src/func/plugin.rs
@@ -0,0 +1,46 @@
+//! Module defining macros for developing _plugins_.
+
+pub use super::CallableFunction;
+use super::FnCallArgs;
+pub use crate::{
+ Dynamic, Engine, EvalAltResult, FnAccess, FnNamespace, ImmutableString, Module,
+ NativeCallContext, Position,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+pub use std::{any::TypeId, mem};
+
+/// Result of a Rhai function.
+pub type RhaiResult = crate::RhaiResult;
+
+#[cfg(not(features = "no_module"))]
+pub use rhai_codegen::*;
+#[cfg(features = "no_module")]
+pub use rhai_codegen::{export_fn, register_exported_fn};
+
+/// Trait implemented by a _plugin function_.
+///
+/// This trait should not be used directly.
+/// Use the `#[export_module]` and `#[export_fn]` procedural attributes instead.
+pub trait PluginFunction {
+ /// Call the plugin function with the arguments provided.
+ fn call(&self, context: Option<NativeCallContext>, args: &mut FnCallArgs) -> RhaiResult;
+
+ /// Is this plugin function a method?
+ #[must_use]
+ fn is_method_call(&self) -> bool;
+
+ /// Does this plugin function contain a [`NativeCallContext`] parameter?
+ #[must_use]
+ fn has_context(&self) -> bool;
+
+ /// Is this plugin function pure?
+ ///
+ /// This defaults to `true` such that any old implementation that has constant-checking code
+ /// inside the function itself will continue to work.
+ #[inline(always)]
+ #[must_use]
+ fn is_pure(&self) -> bool {
+ true
+ }
+}
diff --git a/rhai/src/func/register.rs b/rhai/src/func/register.rs
new file mode 100644
index 0000000..9f5901e
--- /dev/null
+++ b/rhai/src/func/register.rs
@@ -0,0 +1,248 @@
+//! Module which defines the function registration mechanism.
+
+#![allow(non_snake_case)]
+#![allow(unused_imports)]
+#![allow(unused_mut)]
+#![allow(unused_variables)]
+
+use super::call::FnCallArgs;
+use super::callable_function::CallableFunction;
+use super::native::{SendSync, Shared};
+use crate::types::dynamic::{DynamicWriteLock, Variant};
+use crate::{Dynamic, Identifier, NativeCallContext, RhaiResultOf};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ any::{type_name, TypeId},
+ mem,
+};
+
+/// These types are used to build a unique _marker_ tuple type for each combination
+/// of function parameter types in order to make each trait implementation unique.
+///
+/// That is because stable Rust currently does not allow distinguishing implementations
+/// based purely on parameter types of traits (`Fn`, `FnOnce` and `FnMut`).
+///
+/// # Examples
+///
+/// `RegisterNativeFunction<(Mut<A>, B, Ref<C>), 3, false, R, false>` = `Fn(&mut A, B, &C) -> R`
+///
+/// `RegisterNativeFunction<(Mut<A>, B, Ref<C>), 3, true, R, false>` = `Fn(NativeCallContext, &mut A, B, &C) -> R`
+///
+/// `RegisterNativeFunction<(Mut<A>, B, Ref<C>), 3, false, R, true>` = `Fn(&mut A, B, &C) -> Result<R, Box<EvalAltResult>>`
+///
+/// `RegisterNativeFunction<(Mut<A>, B, Ref<C>), 3, true, R, true>` = `Fn(NativeCallContext, &mut A, B, &C) -> Result<R, Box<EvalAltResult>>`
+///
+/// These types are not actually used anywhere.
+pub struct Mut<T>(T);
+//pub struct Ref<T>(T);
+
+/// Dereference into [`DynamicWriteLock`]
+#[inline(always)]
+pub fn by_ref<T: Variant + Clone>(data: &mut Dynamic) -> DynamicWriteLock<T> {
+ // Directly cast the &mut Dynamic into DynamicWriteLock to access the underlying data.
+ data.write_lock::<T>().expect("checked")
+}
+
+/// Dereference into value.
+#[inline(always)]
+#[must_use]
+pub fn by_value<T: Variant + Clone>(data: &mut Dynamic) -> T {
+ if TypeId::of::<T>() == TypeId::of::<&str>() {
+ // If T is `&str`, data must be `ImmutableString`, so map directly to it
+ data.flatten_in_place();
+ let ref_str = data.as_str_ref().expect("&str");
+ // SAFETY: We already checked that `T` is `&str`, so it is safe to cast here.
+ return unsafe { mem::transmute_copy::<_, T>(&ref_str) };
+ }
+ if TypeId::of::<T>() == TypeId::of::<String>() {
+ // If T is `String`, data must be `ImmutableString`, so map directly to it
+ return reify! { data.take().into_string().expect("`ImmutableString`") => !!! T };
+ }
+
+ // We consume the argument and then replace it with () - the argument is not supposed to be used again.
+ // This way, we avoid having to clone the argument again, because it is already a clone when passed here.
+ data.take().cast::<T>()
+}
+
+/// Trait to register custom Rust functions.
+///
+/// # Type Parameters
+///
+/// * `A` - a tuple containing parameter types, with `&mut T` represented by `Mut<T>`.
+/// * `N` - a constant generic containing the number of parameters, must be consistent with `ARGS`.
+/// * `X` - a constant boolean generic indicating whether there is a `NativeCallContext` parameter.
+/// * `R` - return type of the function; if the function returns `Result`, it is the unwrapped inner value type.
+/// * `F` - a constant boolean generic indicating whether the function is fallible (i.e. returns `Result<T, Box<EvalAltResult>>`).
+pub trait RegisterNativeFunction<
+ A: 'static,
+ const N: usize,
+ const X: bool,
+ R: 'static,
+ const F: bool,
+>
+{
+ /// Convert this function into a [`CallableFunction`].
+ #[must_use]
+ fn into_callable_function(self, name: Identifier, is_pure: bool) -> CallableFunction;
+ /// Get the type ID's of this function's parameters.
+ #[must_use]
+ fn param_types() -> [TypeId; N];
+ /// Get the number of parameters for this function.
+ #[inline(always)]
+ #[must_use]
+ fn num_params() -> usize {
+ N
+ }
+ /// Is there a [`NativeCallContext`] parameter for this function?
+ #[inline(always)]
+ #[must_use]
+ fn has_context() -> bool {
+ X
+ }
+ /// _(metadata)_ Get the type names of this function's parameters.
+ /// Exported under the `metadata` feature only.
+ #[cfg(feature = "metadata")]
+ #[must_use]
+ fn param_names() -> [&'static str; N];
+ /// _(metadata)_ Get the type ID of this function's return value.
+ /// Exported under the `metadata` feature only.
+ #[cfg(feature = "metadata")]
+ #[inline(always)]
+ #[must_use]
+ fn return_type() -> TypeId {
+ if F {
+ TypeId::of::<RhaiResultOf<R>>()
+ } else {
+ TypeId::of::<R>()
+ }
+ }
+ /// _(metadata)_ Get the type name of this function's return value.
+ /// Exported under the `metadata` feature only.
+ #[cfg(feature = "metadata")]
+ #[inline(always)]
+ #[must_use]
+ fn return_type_name() -> &'static str {
+ type_name::<R>()
+ }
+}
+
+macro_rules! def_register {
+ () => {
+ def_register!(imp Pure : 0;);
+ };
+ (imp $abi:ident : $n:expr ; $($par:ident => $arg:expr => $mark:ty => $param:ty => $clone:expr),*) => {
+ // ^ function ABI type
+ // ^ number of parameters
+ // ^ function parameter generic type name (A, B, C etc.)
+ // ^ call argument(like A, *B, &mut C etc)
+ // ^ function parameter marker type (A, Ref<B> or Mut<C>)
+ // ^ function parameter actual type (A, &B or &mut C)
+ // ^ parameter access function (by_value or by_ref)
+
+ impl<
+ FN: Fn($($param),*) -> RET + SendSync + 'static,
+ $($par: Variant + Clone,)*
+ RET: Variant + Clone,
+ > RegisterNativeFunction<($($mark,)*), $n, false, RET, false> for FN {
+ #[inline(always)] fn param_types() -> [TypeId;$n] { [$(TypeId::of::<$par>()),*] }
+ #[cfg(feature = "metadata")] #[inline(always)] fn param_names() -> [&'static str;$n] { [$(type_name::<$param>()),*] }
+ #[inline(always)] fn into_callable_function(self, fn_name: Identifier, is_pure: bool) -> CallableFunction {
+ CallableFunction::$abi { func: Shared::new(move |_, args: &mut FnCallArgs| {
+ // The arguments are assumed to be of the correct number and types!
+ let mut drain = args.iter_mut();
+ $(let mut $par = $clone(drain.next().unwrap()); )*
+
+ // Call the function with each argument value
+ let r = self($($arg),*);
+
+ // Map the result
+ Ok(Dynamic::from(r))
+ }), has_context: false, is_pure }
+ }
+ }
+
+ impl<
+ FN: for<'a> Fn(NativeCallContext<'a>, $($param),*) -> RET + SendSync + 'static,
+ $($par: Variant + Clone,)*
+ RET: Variant + Clone,
+ > RegisterNativeFunction<($($mark,)*), $n, true, RET, false> for FN {
+ #[inline(always)] fn param_types() -> [TypeId;$n] { [$(TypeId::of::<$par>()),*] }
+ #[cfg(feature = "metadata")] #[inline(always)] fn param_names() -> [&'static str;$n] { [$(type_name::<$param>()),*] }
+ #[inline(always)] fn into_callable_function(self, fn_name: Identifier, is_pure: bool) -> CallableFunction {
+ CallableFunction::$abi { func: Shared::new(move |ctx: Option<NativeCallContext>, args: &mut FnCallArgs| {
+ let ctx = ctx.unwrap();
+
+ // The arguments are assumed to be of the correct number and types!
+ let mut drain = args.iter_mut();
+ $(let mut $par = $clone(drain.next().unwrap()); )*
+
+ // Call the function with each argument value
+ let r = self(ctx, $($arg),*);
+
+ // Map the result
+ Ok(Dynamic::from(r))
+ }), has_context: true, is_pure }
+ }
+ }
+
+ impl<
+ FN: Fn($($param),*) -> RhaiResultOf<RET> + SendSync + 'static,
+ $($par: Variant + Clone,)*
+ RET: Variant + Clone
+ > RegisterNativeFunction<($($mark,)*), $n, false, RET, true> for FN {
+ #[inline(always)] fn param_types() -> [TypeId;$n] { [$(TypeId::of::<$par>()),*] }
+ #[cfg(feature = "metadata")] #[inline(always)] fn param_names() -> [&'static str;$n] { [$(type_name::<$param>()),*] }
+ #[cfg(feature = "metadata")] #[inline(always)] fn return_type_name() -> &'static str { type_name::<RhaiResultOf<RET>>() }
+ #[inline(always)] fn into_callable_function(self, fn_name: Identifier, is_pure: bool) -> CallableFunction {
+ CallableFunction::$abi { func: Shared::new(move |_, args: &mut FnCallArgs| {
+ // The arguments are assumed to be of the correct number and types!
+ let mut drain = args.iter_mut();
+ $(let mut $par = $clone(drain.next().unwrap()); )*
+
+ // Call the function with each argument value
+ self($($arg),*).map(Dynamic::from)
+ }), has_context: false, is_pure }
+ }
+ }
+
+ impl<
+ FN: for<'a> Fn(NativeCallContext<'a>, $($param),*) -> RhaiResultOf<RET> + SendSync + 'static,
+ $($par: Variant + Clone,)*
+ RET: Variant + Clone
+ > RegisterNativeFunction<($($mark,)*), $n, true, RET, true> for FN {
+ #[inline(always)] fn param_types() -> [TypeId;$n] { [$(TypeId::of::<$par>()),*] }
+ #[cfg(feature = "metadata")] #[inline(always)] fn param_names() -> [&'static str;$n] { [$(type_name::<$param>()),*] }
+ #[cfg(feature = "metadata")] #[inline(always)] fn return_type_name() -> &'static str { type_name::<RhaiResultOf<RET>>() }
+ #[inline(always)] fn into_callable_function(self, fn_name: Identifier, is_pure: bool) -> CallableFunction {
+ CallableFunction::$abi { func: Shared::new(move |ctx: Option<NativeCallContext>, args: &mut FnCallArgs| {
+ let ctx = ctx.unwrap();
+
+ // The arguments are assumed to be of the correct number and types!
+ let mut drain = args.iter_mut();
+ $(let mut $par = $clone(drain.next().unwrap()); )*
+
+ // Call the function with each argument value
+ self(ctx, $($arg),*).map(Dynamic::from)
+ }), has_context: true, is_pure }
+ }
+ }
+
+ //def_register!(imp_pop $($par => $mark => $param),*);
+ };
+ ($p0:ident:$n0:expr $(, $p:ident: $n:expr)*) => {
+ def_register!(imp Pure : $n0 ; $p0 => $p0 => $p0 => $p0 => by_value $(, $p => $p => $p => $p => by_value)*);
+ def_register!(imp Method : $n0 ; $p0 => &mut $p0 => Mut<$p0> => &mut $p0 => by_ref $(, $p => $p => $p => $p => by_value)*);
+ // ^ CallableFunction constructor
+ // ^ number of arguments ^ first parameter passed through
+ // ^ others passed by value (by_value)
+
+ // Currently does not support first argument which is a reference, as there will be
+ // conflicting implementations since &T: Any and T: Any cannot be distinguished
+ //def_register!(imp $p0 => Ref<$p0> => &$p0 => by_ref $(, $p => $p => $p => by_value)*);
+
+ def_register!($($p: $n),*);
+ };
+}
+
+def_register!(A:20, B:19, C:18, D:17, E:16, F:15, G:14, H:13, J:12, K:11, L:10, M:9, N:8, P:7, Q:6, R:5, S:4, T:3, U:2, V:1);
diff --git a/rhai/src/func/script.rs b/rhai/src/func/script.rs
new file mode 100644
index 0000000..8da0e8b
--- /dev/null
+++ b/rhai/src/func/script.rs
@@ -0,0 +1,232 @@
+//! Implement script function-calling mechanism for [`Engine`].
+#![cfg(not(feature = "no_function"))]
+
+use super::call::FnCallArgs;
+use crate::ast::ScriptFnDef;
+use crate::eval::{Caches, GlobalRuntimeState};
+use crate::func::EncapsulatedEnviron;
+use crate::{Dynamic, Engine, Position, RhaiResult, Scope, ERR};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+impl Engine {
+ /// # Main Entry-Point
+ ///
+ /// Call a script-defined function.
+ ///
+ /// If `rewind_scope` is `false`, arguments are removed from the scope but new variables are not.
+ ///
+ /// # WARNING
+ ///
+ /// Function call arguments may be _consumed_ when the function requires them to be passed by value.
+ /// All function arguments not in the first position are always passed by value and thus consumed.
+ ///
+ /// **DO NOT** reuse the argument values except for the first `&mut` argument - all others are silently replaced by `()`!
+ pub(crate) fn call_script_fn(
+ &self,
+ global: &mut GlobalRuntimeState,
+ caches: &mut Caches,
+ scope: &mut Scope,
+ mut this_ptr: Option<&mut Dynamic>,
+ _environ: Option<&EncapsulatedEnviron>,
+ fn_def: &ScriptFnDef,
+ args: &mut FnCallArgs,
+ rewind_scope: bool,
+ pos: Position,
+ ) -> RhaiResult {
+ debug_assert_eq!(fn_def.params.len(), args.len());
+
+ self.track_operation(global, pos)?;
+
+ // Check for stack overflow
+ if global.level > self.max_call_levels() {
+ return Err(ERR::ErrorStackOverflow(pos).into());
+ }
+
+ #[cfg(feature = "debugging")]
+ if self.debugger_interface.is_none() && fn_def.body.is_empty() {
+ return Ok(Dynamic::UNIT);
+ }
+ #[cfg(not(feature = "debugging"))]
+ if fn_def.body.is_empty() {
+ return Ok(Dynamic::UNIT);
+ }
+
+ let orig_scope_len = scope.len();
+ let orig_lib_len = global.lib.len();
+ #[cfg(not(feature = "no_module"))]
+ let orig_imports_len = global.num_imports();
+
+ #[cfg(feature = "debugging")]
+ let orig_call_stack_len = global
+ .debugger
+ .as_ref()
+ .map_or(0, |dbg| dbg.call_stack().len());
+
+ // Put arguments into scope as variables
+ scope.extend(fn_def.params.iter().cloned().zip(args.iter_mut().map(|v| {
+ // Actually consume the arguments instead of cloning them
+ v.take()
+ })));
+
+ // Push a new call stack frame
+ #[cfg(feature = "debugging")]
+ if self.is_debugger_registered() {
+ let fn_name = fn_def.name.clone();
+ let args = scope.iter().skip(orig_scope_len).map(|(.., v)| v).collect();
+ let source = global.source.clone();
+
+ global
+ .debugger_mut()
+ .push_call_stack_frame(fn_name, args, source, pos);
+ }
+
+ // Merge in encapsulated environment, if any
+ let orig_fn_resolution_caches_len = caches.fn_resolution_caches_len();
+
+ #[cfg(not(feature = "no_module"))]
+ let orig_constants = _environ.map(|environ| {
+ let EncapsulatedEnviron {
+ lib,
+ imports,
+ constants,
+ } = environ;
+
+ imports
+ .iter()
+ .cloned()
+ .for_each(|(n, m)| global.push_import(n, m));
+
+ global.lib.push(lib.clone());
+
+ std::mem::replace(&mut global.constants, constants.clone())
+ });
+
+ #[cfg(feature = "debugging")]
+ if self.is_debugger_registered() {
+ let node = crate::ast::Stmt::Noop(fn_def.body.position());
+ self.run_debugger(global, caches, scope, this_ptr.as_deref_mut(), &node)?;
+ }
+
+ // Evaluate the function
+ let mut _result: RhaiResult = self
+ .eval_stmt_block(
+ global,
+ caches,
+ scope,
+ this_ptr.as_deref_mut(),
+ &fn_def.body,
+ rewind_scope,
+ )
+ .or_else(|err| match *err {
+ // Convert return statement to return value
+ ERR::Return(x, ..) => Ok(x),
+ // System errors are passed straight-through
+ mut err if err.is_system_exception() => {
+ err.set_position(pos);
+ Err(err.into())
+ }
+ // Other errors are wrapped in `ErrorInFunctionCall`
+ _ => Err(ERR::ErrorInFunctionCall(
+ fn_def.name.to_string(),
+ #[cfg(not(feature = "no_module"))]
+ _environ
+ .and_then(|environ| environ.lib.id())
+ .unwrap_or_else(|| global.source().unwrap_or(""))
+ .to_string(),
+ #[cfg(feature = "no_module")]
+ global.source().unwrap_or("").to_string(),
+ err,
+ pos,
+ )
+ .into()),
+ });
+
+ #[cfg(feature = "debugging")]
+ if self.is_debugger_registered() {
+ let trigger = match global.debugger_mut().status {
+ crate::eval::DebuggerStatus::FunctionExit(n) => n >= global.level,
+ crate::eval::DebuggerStatus::Next(.., true) => true,
+ _ => false,
+ };
+
+ if trigger {
+ let node = crate::ast::Stmt::Noop(fn_def.body.end_position().or_else(pos));
+ let node = (&node).into();
+ let event = match _result {
+ Ok(ref r) => crate::eval::DebuggerEvent::FunctionExitWithValue(r),
+ Err(ref err) => crate::eval::DebuggerEvent::FunctionExitWithError(err),
+ };
+ match self.run_debugger_raw(global, caches, scope, this_ptr, node, event) {
+ Ok(_) => (),
+ Err(err) => _result = Err(err),
+ }
+ }
+
+ // Pop the call stack
+ global
+ .debugger
+ .as_mut()
+ .unwrap()
+ .rewind_call_stack(orig_call_stack_len);
+ }
+
+ // Remove all local variables and imported modules
+ if rewind_scope {
+ scope.rewind(orig_scope_len);
+ } else if !args.is_empty() {
+ // Remove arguments only, leaving new variables in the scope
+ scope.remove_range(orig_scope_len, args.len());
+ }
+ global.lib.truncate(orig_lib_len);
+ #[cfg(not(feature = "no_module"))]
+ global.truncate_imports(orig_imports_len);
+
+ // Restore constants
+ #[cfg(not(feature = "no_module"))]
+ if let Some(constants) = orig_constants {
+ global.constants = constants;
+ }
+
+ // Restore state
+ caches.rewind_fn_resolution_caches(orig_fn_resolution_caches_len);
+
+ _result
+ }
+
+ // Does a script-defined function exist?
+ #[must_use]
+ pub(crate) fn has_script_fn(
+ &self,
+ global: &GlobalRuntimeState,
+ caches: &mut Caches,
+ hash_script: u64,
+ ) -> bool {
+ let cache = caches.fn_resolution_cache_mut();
+
+ if let Some(result) = cache.map.get(&hash_script).map(Option::is_some) {
+ return result;
+ }
+
+ // First check script-defined functions
+ let r = global.lib.iter().any(|m| m.contains_fn(hash_script))
+ // Then check the global namespace and packages
+ || self.global_modules.iter().any(|m| m.contains_fn(hash_script));
+
+ #[cfg(not(feature = "no_module"))]
+ let r = r ||
+ // Then check imported modules
+ global.contains_qualified_fn(hash_script)
+ // Then check sub-modules
+ || self.global_sub_modules.as_ref().map_or(false, |m| {
+ m.values().any(|m| m.contains_qualified_fn(hash_script))
+ });
+
+ if !r && !cache.filter.is_absent_and_set(hash_script) {
+ // Do not cache "one-hit wonders"
+ cache.map.insert(hash_script, None);
+ }
+
+ r
+ }
+}
diff --git a/rhai/src/lib.rs b/rhai/src/lib.rs
new file mode 100644
index 0000000..01397dd
--- /dev/null
+++ b/rhai/src/lib.rs
@@ -0,0 +1,501 @@
+//! # Rhai - embedded scripting for Rust
+//!
+//! 
+//!
+//! Rhai is a tiny, simple and fast embedded scripting language for Rust
+//! that gives you a safe and easy way to add scripting to your applications.
+//!
+//! It provides a familiar syntax based on JavaScript+Rust and a simple Rust interface.
+//!
+//! # A Quick Example
+//!
+//! ## Contents of `my_script.rhai`
+//!
+//! ```rhai
+//! /// Brute force factorial function
+//! fn factorial(x) {
+//! if x == 1 { return 1; }
+//! x * factorial(x - 1)
+//! }
+//!
+//! // Calling an external function 'compute'
+//! compute(factorial(10))
+//! ```
+//!
+//! ## The Rust part
+//!
+//! ```no_run
+//! use rhai::{Engine, EvalAltResult};
+//!
+//! fn main() -> Result<(), Box<EvalAltResult>>
+//! {
+//! // Define external function
+//! fn compute_something(x: i64) -> bool {
+//! (x % 40) == 0
+//! }
+//!
+//! // Create scripting engine
+//! let mut engine = Engine::new();
+//!
+//! // Register external function as 'compute'
+//! engine.register_fn("compute", compute_something);
+//!
+//! # #[cfg(not(feature = "no_std"))]
+//! # #[cfg(not(target_family = "wasm"))]
+//! #
+//! // Evaluate the script, expecting a 'bool' result
+//! let result: bool = engine.eval_file("my_script.rhai".into())?;
+//!
+//! assert_eq!(result, true);
+//!
+//! Ok(())
+//! }
+//! ```
+//!
+//! # Features
+//!
+#![cfg_attr(feature = "document-features", doc = document_features::document_features!(feature_label = "<span id=\"feature-{feature}\">**`{feature}`**</span>"))]
+//!
+//! # On-Line Documentation
+//!
+//! See [The Rhai Book](https://rhai.rs/book) for details on the Rhai scripting engine and language.
+
+#![cfg_attr(feature = "no_std", no_std)]
+#![deny(missing_docs)]
+// #![warn(clippy::all)]
+// #![warn(clippy::pedantic)]
+// #![warn(clippy::nursery)]
+#![warn(clippy::cargo)]
+// #![warn(clippy::undocumented_unsafe_blocks)]
+#![allow(clippy::unit_arg)]
+#![allow(clippy::missing_errors_doc)]
+#![allow(clippy::missing_panics_doc)]
+#![allow(clippy::used_underscore_binding)]
+#![allow(clippy::inline_always)]
+#![allow(clippy::module_name_repetitions)]
+#![allow(clippy::negative_feature_names)]
+#![allow(clippy::module_inception)]
+#![allow(clippy::box_collection)]
+#![allow(clippy::too_many_arguments)]
+#![allow(clippy::upper_case_acronyms)]
+#![allow(clippy::match_same_arms)]
+// The lints below can be turned off to reduce signal/noise ratio
+#![allow(clippy::too_many_lines)]
+#![allow(clippy::let_underscore_drop)]
+#![allow(clippy::absurd_extreme_comparisons)]
+#![allow(clippy::unnecessary_cast)]
+#![allow(clippy::wildcard_imports)]
+#![allow(clippy::no_effect_underscore_binding)]
+#![allow(clippy::semicolon_if_nothing_returned)]
+#![allow(clippy::type_complexity)]
+
+#[cfg(feature = "no_std")]
+extern crate alloc;
+
+#[cfg(feature = "no_std")]
+extern crate no_std_compat as std;
+
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+// Internal modules
+#[macro_use]
+mod reify;
+#[macro_use]
+mod defer;
+
+mod api;
+mod ast;
+pub mod config;
+mod engine;
+mod eval;
+mod func;
+mod module;
+mod optimizer;
+pub mod packages;
+mod parser;
+#[cfg(feature = "serde")]
+pub mod serde;
+mod tests;
+mod tokenizer;
+mod types;
+
+/// Error encountered when parsing a script.
+type PERR = ParseErrorType;
+/// Evaluation result.
+type ERR = EvalAltResult;
+/// General evaluation error for Rhai scripts.
+type RhaiError = Box<ERR>;
+/// Generic [`Result`] type for Rhai functions.
+type RhaiResultOf<T> = Result<T, RhaiError>;
+/// General [`Result`] type for Rhai functions returning [`Dynamic`] values.
+type RhaiResult = RhaiResultOf<Dynamic>;
+
+/// The system integer type. It is defined as [`i64`].
+///
+/// If the `only_i32` feature is enabled, this will be [`i32`] instead.
+#[cfg(not(feature = "only_i32"))]
+pub type INT = i64;
+
+/// The system integer type.
+/// It is defined as [`i32`] since the `only_i32` feature is used.
+///
+/// If the `only_i32` feature is not used, this will be `i64` instead.
+#[cfg(feature = "only_i32")]
+pub type INT = i32;
+
+/// The unsigned system base integer type. It is defined as [`u64`].
+///
+/// If the `only_i32` feature is enabled, this will be [`u32`] instead.
+#[cfg(not(feature = "only_i32"))]
+#[allow(non_camel_case_types)]
+type UNSIGNED_INT = u64;
+
+/// The unsigned system integer base type.
+/// It is defined as [`u32`] since the `only_i32` feature is used.
+///
+/// If the `only_i32` feature is not used, this will be `u64` instead.
+#[cfg(feature = "only_i32")]
+#[allow(non_camel_case_types)]
+type UNSIGNED_INT = u32;
+
+/// The maximum integer that can fit into a [`usize`].
+#[cfg(not(target_pointer_width = "32"))]
+const MAX_USIZE_INT: INT = INT::MAX;
+
+/// The maximum integer that can fit into a [`usize`].
+#[cfg(not(feature = "only_i32"))]
+#[cfg(target_pointer_width = "32")]
+const MAX_USIZE_INT: INT = usize::MAX as INT;
+
+/// The maximum integer that can fit into a [`usize`].
+#[cfg(feature = "only_i32")]
+#[cfg(target_pointer_width = "32")]
+const MAX_USIZE_INT: INT = INT::MAX;
+
+/// Number of bits in [`INT`].
+///
+/// It is 64 unless the `only_i32` feature is enabled when it will be 32.
+const INT_BITS: usize = std::mem::size_of::<INT>() * 8;
+
+/// Number of bytes that make up an [`INT`].
+///
+/// It is 8 unless the `only_i32` feature is enabled when it will be 4.
+#[cfg(not(feature = "no_index"))]
+const INT_BYTES: usize = std::mem::size_of::<INT>();
+
+/// The system floating-point type. It is defined as [`f64`].
+///
+/// Not available under `no_float`.
+///
+/// If the `f32_float` feature is enabled, this will be [`f32`] instead.
+#[cfg(not(feature = "no_float"))]
+#[cfg(not(feature = "f32_float"))]
+pub type FLOAT = f64;
+
+/// The system floating-point type.
+/// It is defined as [`f32`] since the `f32_float` feature is used.
+///
+/// Not available under `no_float`.
+///
+/// If the `f32_float` feature is not used, this will be `f64` instead.
+#[cfg(not(feature = "no_float"))]
+#[cfg(feature = "f32_float")]
+pub type FLOAT = f32;
+
+/// Number of bytes that make up a [`FLOAT`].
+///
+/// It is 8 unless the `f32_float` feature is enabled when it will be 4.
+#[cfg(not(feature = "no_float"))]
+#[cfg(not(feature = "no_index"))]
+const FLOAT_BYTES: usize = std::mem::size_of::<FLOAT>();
+
+/// An exclusive integer range.
+type ExclusiveRange = std::ops::Range<INT>;
+
+/// An inclusive integer range.
+type InclusiveRange = std::ops::RangeInclusive<INT>;
+
+#[allow(deprecated)]
+pub use api::build_type::{CustomType, TypeBuilder};
+#[cfg(not(feature = "no_custom_syntax"))]
+pub use api::custom_syntax::Expression;
+#[cfg(not(feature = "no_std"))]
+#[cfg(not(target_family = "wasm"))]
+#[cfg(not(target_vendor = "teaclave"))]
+pub use api::files::{eval_file, run_file};
+pub use api::{eval::eval, events::VarDefInfo, run::run};
+pub use ast::{FnAccess, AST};
+use defer::Deferred;
+pub use engine::{Engine, OP_CONTAINS, OP_EQUALS};
+pub use eval::EvalContext;
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_object"))]
+use func::calc_typed_method_hash;
+use func::{calc_fn_hash, calc_fn_hash_full, calc_var_hash};
+pub use func::{plugin, FuncArgs, NativeCallContext, RegisterNativeFunction};
+pub use module::{FnNamespace, Module};
+pub use rhai_codegen::*;
+#[cfg(not(feature = "no_time"))]
+pub use types::Instant;
+pub use types::{
+ Dynamic, EvalAltResult, FnPtr, ImmutableString, LexError, ParseError, ParseErrorType, Position,
+ Scope,
+};
+
+/// _(debugging)_ Module containing types for debugging.
+/// Exported under the `debugging` feature only.
+#[cfg(feature = "debugging")]
+pub mod debugger {
+ #[cfg(not(feature = "no_function"))]
+ pub use super::eval::CallStackFrame;
+ pub use super::eval::{BreakPoint, Debugger, DebuggerCommand, DebuggerEvent};
+}
+
+/// An identifier in Rhai. [`SmartString`](https://crates.io/crates/smartstring) is used because most
+/// identifiers are ASCII and short, fewer than 23 characters, so they can be stored inline.
+#[cfg(not(feature = "internals"))]
+type Identifier = SmartString;
+
+/// An identifier in Rhai. [`SmartString`](https://crates.io/crates/smartstring) is used because most
+/// identifiers are ASCII and short, fewer than 23 characters, so they can be stored inline.
+#[cfg(feature = "internals")]
+pub type Identifier = SmartString;
+
+/// Alias to [`Rc`][std::rc::Rc] or [`Arc`][std::sync::Arc] depending on the `sync` feature flag.
+pub use func::Shared;
+
+/// Alias to [`RefCell`][std::cell::RefCell] or [`RwLock`][std::sync::RwLock] depending on the `sync` feature flag.
+pub use func::Locked;
+
+/// A shared [`Module`].
+type SharedModule = Shared<Module>;
+
+#[cfg(not(feature = "no_function"))]
+pub use func::Func;
+
+#[cfg(not(feature = "no_function"))]
+pub use ast::ScriptFnMetadata;
+
+#[cfg(not(feature = "no_function"))]
+pub use api::call_fn::CallFnOptions;
+
+/// Variable-sized array of [`Dynamic`] values.
+///
+/// Not available under `no_index`.
+#[cfg(not(feature = "no_index"))]
+pub type Array = Vec<Dynamic>;
+
+/// Variable-sized array of [`u8`] values (byte array).
+///
+/// Not available under `no_index`.
+#[cfg(not(feature = "no_index"))]
+pub type Blob = Vec<u8>;
+
+/// A dictionary of [`Dynamic`] values with string keys.
+///
+/// Not available under `no_object`.
+///
+/// [`SmartString`](https://crates.io/crates/smartstring) is used as the key type because most
+/// property names are ASCII and short, fewer than 23 characters, so they can be stored inline.
+#[cfg(not(feature = "no_object"))]
+pub type Map = std::collections::BTreeMap<Identifier, Dynamic>;
+
+#[cfg(not(feature = "no_object"))]
+pub use api::json::format_map_as_json;
+
+#[cfg(not(feature = "no_module"))]
+pub use module::ModuleResolver;
+
+/// Module containing all built-in _module resolvers_ available to Rhai.
+#[cfg(not(feature = "no_module"))]
+pub use module::resolvers as module_resolvers;
+
+#[cfg(not(feature = "no_optimize"))]
+pub use optimizer::OptimizationLevel;
+
+/// Placeholder for the optimization level.
+#[cfg(feature = "no_optimize")]
+pub type OptimizationLevel = ();
+
+// Expose internal data structures.
+
+#[cfg(feature = "internals")]
+pub use types::dynamic::{AccessMode, DynamicReadLock, DynamicWriteLock, Variant};
+
+#[cfg(feature = "internals")]
+#[cfg(not(feature = "no_float"))]
+pub use types::FloatWrapper;
+
+#[cfg(feature = "internals")]
+pub use types::{Span, StringsInterner};
+
+#[cfg(feature = "internals")]
+pub use tokenizer::{
+ get_next_token, is_valid_function_name, is_valid_identifier, parse_string_literal, InputStream,
+ MultiInputsStream, Token, TokenIterator, TokenizeState, TokenizerControl,
+ TokenizerControlBlock,
+};
+
+#[cfg(feature = "internals")]
+pub use parser::ParseState;
+
+#[cfg(feature = "internals")]
+pub use ast::{
+ ASTFlags, ASTNode, BinaryExpr, ConditionalExpr, Expr, FlowControl, FnCallExpr, FnCallHashes,
+ Ident, OpAssignment, RangeCase, ScriptFnDef, Stmt, StmtBlock, SwitchCasesCollection,
+};
+
+#[cfg(feature = "internals")]
+#[cfg(not(feature = "no_custom_syntax"))]
+pub use ast::CustomExpr;
+
+#[cfg(feature = "internals")]
+#[cfg(not(feature = "no_module"))]
+pub use ast::Namespace;
+
+#[cfg(feature = "internals")]
+pub use func::EncapsulatedEnviron;
+
+#[cfg(feature = "internals")]
+pub use eval::{Caches, FnResolutionCache, FnResolutionCacheEntry, GlobalRuntimeState};
+
+#[cfg(feature = "internals")]
+#[allow(deprecated)]
+pub use func::{locked_read, locked_write, CallableFunction, NativeCallContextStore};
+
+#[cfg(feature = "internals")]
+#[cfg(feature = "metadata")]
+pub use api::definitions::Definitions;
+
+/// Number of items to keep inline for [`StaticVec`].
+const STATIC_VEC_INLINE_SIZE: usize = 3;
+
+/// Alias to [`smallvec::SmallVec<[T; 3]>`](https://crates.io/crates/smallvec), which is a
+/// specialized [`Vec`] backed by a small, inline, fixed-size array when there are ≤ 3 items stored.
+///
+/// # History
+///
+/// And Saint Attila raised the `SmallVec` up on high, saying, "O Lord, bless this Thy `SmallVec`
+/// that, with it, Thou mayest blow Thine allocation costs to tiny bits in Thy mercy."
+///
+/// And the Lord did grin, and the people did feast upon the lambs and sloths and carp and anchovies
+/// and orangutans and breakfast cereals and fruit bats and large chu...
+///
+/// And the Lord spake, saying, "First shalt thou depend on the [`smallvec`](https://crates.io/crates/smallvec) crate.
+/// Then, shalt thou keep three inline. No more. No less. Three shalt be the number thou shalt keep inline,
+/// and the number to keep inline shalt be three. Four shalt thou not keep inline, nor either keep inline
+/// thou two, excepting that thou then proceed to three. Five is right out. Once the number three,
+/// being the third number, be reached, then, lobbest thou thy `SmallVec` towards thy heap, who,
+/// being slow and cache-naughty in My sight, shall snuff it."
+///
+/// # Why Three
+///
+/// `StaticVec` is used frequently to keep small lists of items in inline (non-heap) storage in
+/// order to improve cache friendliness and reduce indirections.
+///
+/// The number 3, other than being the holy number, is carefully chosen for a balance between
+/// storage space and reduce allocations. That is because most function calls (and most functions,
+/// for that matter) contain fewer than 4 arguments, the exception being closures that capture a
+/// large number of external variables.
+///
+/// In addition, most script blocks either contain many statements, or just one or two lines;
+/// most scripts load fewer than 4 external modules; most module paths contain fewer than 4 levels
+/// (e.g. `std::collections::map::HashMap` is 4 levels and it is just about as long as they get).
+#[cfg(not(feature = "internals"))]
+type StaticVec<T> = smallvec::SmallVec<[T; STATIC_VEC_INLINE_SIZE]>;
+
+/// _(internals)_ Alias to [`smallvec::SmallVec<[T; 3]>`](https://crates.io/crates/smallvec),
+/// which is a [`Vec`] backed by a small, inline, fixed-size array when there are ≤ 3 items stored.
+/// Exported under the `internals` feature only.
+///
+/// # History
+///
+/// And Saint Attila raised the `SmallVec` up on high, saying, "O Lord, bless this Thy `SmallVec`
+/// that, with it, Thou mayest blow Thine allocation costs to tiny bits in Thy mercy."
+///
+/// And the Lord did grin, and the people did feast upon the lambs and sloths and carp and anchovies
+/// and orangutans and breakfast cereals and fruit bats and large chu...
+///
+/// And the Lord spake, saying, "First shalt thou depend on the [`smallvec`](https://crates.io/crates/smallvec) crate.
+/// Then, shalt thou keep three inline. No more. No less. Three shalt be the number thou shalt keep inline,
+/// and the number to keep inline shalt be three. Four shalt thou not keep inline, nor either keep inline
+/// thou two, excepting that thou then proceed to three. Five is right out. Once the number three,
+/// being the third number, be reached, then, lobbest thou thy `SmallVec` towards thy heap, who,
+/// being slow and cache-naughty in My sight, shall snuff it."
+///
+/// # Why Three
+///
+/// `StaticVec` is used frequently to keep small lists of items in inline (non-heap) storage in
+/// order to improve cache friendliness and reduce indirections.
+///
+/// The number 3, other than being the holy number, is carefully chosen for a balance between
+/// storage space and reduce allocations. That is because most function calls (and most functions,
+/// for that matter) contain fewer than 4 arguments, the exception being closures that capture a
+/// large number of external variables.
+///
+/// In addition, most script blocks either contain many statements, or just one or two lines;
+/// most scripts load fewer than 4 external modules; most module paths contain fewer than 4 levels
+/// (e.g. `std::collections::map::HashMap` is 4 levels and it is just about as long as they get).
+#[cfg(feature = "internals")]
+pub type StaticVec<T> = smallvec::SmallVec<[T; STATIC_VEC_INLINE_SIZE]>;
+
+/// Number of items to keep inline for [`FnArgsVec`].
+#[cfg(not(feature = "no_closure"))]
+const FN_ARGS_VEC_INLINE_SIZE: usize = 5;
+
+/// Inline arguments storage for function calls.
+///
+/// # Notes
+///
+/// Since most usage of this is during a function call to gather up arguments, this is mostly
+/// allocated on the stack, so we can tolerate a larger number of values stored inline.
+///
+/// Most functions have few parameters, but closures with a lot of captured variables can
+/// potentially have many. Having a larger inline storage for arguments reduces allocations in
+/// scripts with heavy closure usage.
+///
+/// Under `no_closure`, this type aliases to [`StaticVec`][crate::StaticVec] instead.
+#[cfg(not(feature = "no_closure"))]
+type FnArgsVec<T> = smallvec::SmallVec<[T; FN_ARGS_VEC_INLINE_SIZE]>;
+
+/// Inline arguments storage for function calls.
+/// This type aliases to [`StaticVec`][crate::StaticVec].
+#[cfg(feature = "no_closure")]
+type FnArgsVec<T> = crate::StaticVec<T>;
+
+type SmartString = smartstring::SmartString<smartstring::LazyCompact>;
+
+// Compiler guards against mutually-exclusive feature flags
+
+#[cfg(feature = "no_float")]
+#[cfg(feature = "f32_float")]
+compile_error!("`f32_float` cannot be used with `no_float`");
+
+#[cfg(feature = "only_i32")]
+#[cfg(feature = "only_i64")]
+compile_error!("`only_i32` and `only_i64` cannot be used together");
+
+#[cfg(feature = "no_std")]
+#[cfg(feature = "wasm-bindgen")]
+compile_error!("`wasm-bindgen` cannot be used with `no-std`");
+
+#[cfg(feature = "no_std")]
+#[cfg(feature = "stdweb")]
+compile_error!("`stdweb` cannot be used with `no-std`");
+
+#[cfg(target_family = "wasm")]
+#[cfg(feature = "no_std")]
+compile_error!("`no_std` cannot be used for WASM target");
+
+#[cfg(not(target_family = "wasm"))]
+#[cfg(feature = "wasm-bindgen")]
+compile_error!("`wasm-bindgen` cannot be used for non-WASM target");
+
+#[cfg(not(target_family = "wasm"))]
+#[cfg(feature = "stdweb")]
+compile_error!("`stdweb` cannot be used non-WASM target");
+
+#[cfg(feature = "wasm-bindgen")]
+#[cfg(feature = "stdweb")]
+compile_error!("`wasm-bindgen` and `stdweb` cannot be used together");
diff --git a/rhai/src/module/mod.rs b/rhai/src/module/mod.rs
new file mode 100644
index 0000000..201527c
--- /dev/null
+++ b/rhai/src/module/mod.rs
@@ -0,0 +1,2584 @@
+//! Module defining external-loaded modules for Rhai.
+
+#[cfg(feature = "metadata")]
+use crate::api::formatting::format_type;
+use crate::ast::FnAccess;
+use crate::func::{
+ shared_take_or_clone, CallableFunction, FnCallArgs, IteratorFn, RegisterNativeFunction,
+ SendSync, StraightHashMap,
+};
+use crate::types::{dynamic::Variant, BloomFilterU64, CustomTypesCollection};
+use crate::{
+ calc_fn_hash, calc_fn_hash_full, Dynamic, FnArgsVec, Identifier, ImmutableString,
+ NativeCallContext, RhaiResultOf, Shared, SharedModule, SmartString,
+};
+use bitflags::bitflags;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ any::TypeId,
+ collections::BTreeMap,
+ fmt,
+ ops::{Add, AddAssign},
+};
+
+#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+use crate::func::register::Mut;
+
+/// Initial capacity of the hashmap for functions.
+const FN_MAP_SIZE: usize = 16;
+
+/// A type representing the namespace of a function.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
+#[cfg_attr(feature = "metadata", derive(serde::Serialize))]
+#[cfg_attr(feature = "metadata", serde(rename_all = "camelCase"))]
+#[non_exhaustive]
+pub enum FnNamespace {
+ /// Module namespace only.
+ ///
+ /// Ignored under `no_module`.
+ Internal,
+ /// Expose to global namespace.
+ Global,
+}
+
+impl FnNamespace {
+ /// Is this a module namespace?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_module_namespace(self) -> bool {
+ match self {
+ Self::Internal => true,
+ Self::Global => false,
+ }
+ }
+ /// Is this a global namespace?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_global_namespace(self) -> bool {
+ match self {
+ Self::Internal => false,
+ Self::Global => true,
+ }
+ }
+}
+
+/// A type containing the metadata of a single registered function.
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct FuncInfoMetadata {
+ /// Function namespace.
+ pub namespace: FnNamespace,
+ /// Function access mode.
+ pub access: FnAccess,
+ /// Function name.
+ pub name: Identifier,
+ #[cfg(not(feature = "no_object"))]
+ /// Type of `this` pointer, if any.
+ pub this_type: Option<ImmutableString>,
+ /// Number of parameters.
+ pub num_params: usize,
+ /// Parameter types (if applicable).
+ pub param_types: FnArgsVec<TypeId>,
+ /// Parameter names and types (if available).
+ #[cfg(feature = "metadata")]
+ pub params_info: FnArgsVec<Identifier>,
+ /// Return type name.
+ #[cfg(feature = "metadata")]
+ pub return_type: Identifier,
+ /// Comments.
+ #[cfg(feature = "metadata")]
+ pub comments: Box<[SmartString]>,
+}
+
+/// A type containing a single registered function.
+#[derive(Debug, Clone)]
+pub struct FuncInfo {
+ /// Function instance.
+ pub func: CallableFunction,
+ /// Function metadata.
+ pub metadata: Box<FuncInfoMetadata>,
+}
+
+impl FuncInfo {
+ /// _(metadata)_ Generate a signature of the function.
+ /// Exported under the `metadata` feature only.
+ #[cfg(feature = "metadata")]
+ #[must_use]
+ pub fn gen_signature(&self) -> String {
+ let mut signature = format!("{}(", self.metadata.name);
+
+ let return_type = format_type(&self.metadata.return_type, true);
+
+ if self.metadata.params_info.is_empty() {
+ for x in 0..self.metadata.num_params {
+ signature.push('_');
+ if x < self.metadata.num_params - 1 {
+ signature.push_str(", ");
+ }
+ }
+ } else {
+ let params = self
+ .metadata
+ .params_info
+ .iter()
+ .map(|param| {
+ let mut segment = param.splitn(2, ':');
+ let name = match segment.next().unwrap().trim() {
+ "" => "_",
+ s => s,
+ };
+ let result: std::borrow::Cow<str> = match segment.next() {
+ Some(typ) => format!("{name}: {}", format_type(typ, false)).into(),
+ None => name.into(),
+ };
+ result
+ })
+ .collect::<FnArgsVec<_>>();
+ signature.push_str(¶ms.join(", "));
+ }
+ signature.push(')');
+
+ if !self.func.is_script() && !return_type.is_empty() {
+ signature.push_str(" -> ");
+ signature.push_str(&return_type);
+ }
+
+ signature
+ }
+}
+
+/// _(internals)_ Calculate a [`u64`] hash key from a namespace-qualified function name and parameter types.
+/// Exported under the `internals` feature only.
+///
+/// Module names are passed in via `&str` references from an iterator.
+/// Parameter types are passed in via [`TypeId`] values from an iterator.
+///
+/// # Note
+///
+/// The first module name is skipped. Hashing starts from the _second_ module in the chain.
+#[inline]
+pub fn calc_native_fn_hash<'a>(
+ modules: impl IntoIterator<Item = &'a str, IntoIter = impl ExactSizeIterator<Item = &'a str>>,
+ fn_name: &str,
+ params: &[TypeId],
+) -> u64 {
+ calc_fn_hash_full(
+ calc_fn_hash(modules, fn_name, params.len()),
+ params.iter().copied(),
+ )
+}
+
+bitflags! {
+ /// Bit-flags containing all status for [`Module`].
+ pub struct ModuleFlags: u8 {
+ /// Is the [`Module`] internal?
+ const INTERNAL = 0b0000_0001;
+ /// Is the [`Module`] part of a standard library?
+ const STANDARD_LIB = 0b0000_0010;
+ /// Is the [`Module`] indexed?
+ const INDEXED = 0b0000_0100;
+ /// Does the [`Module`] contain indexed functions that have been exposed to the global namespace?
+ const INDEXED_GLOBAL_FUNCTIONS = 0b0000_1000;
+ }
+}
+
+/// A module which may contain variables, sub-modules, external Rust functions,
+/// and/or script-defined functions.
+#[derive(Clone)]
+pub struct Module {
+ /// ID identifying the module.
+ id: Option<ImmutableString>,
+ /// Module documentation.
+ #[cfg(feature = "metadata")]
+ doc: Option<Box<SmartString>>,
+ /// Custom types.
+ custom_types: Option<Box<CustomTypesCollection>>,
+ /// Sub-modules.
+ modules: Option<BTreeMap<Identifier, SharedModule>>,
+ /// [`Module`] variables.
+ variables: Option<BTreeMap<Identifier, Dynamic>>,
+ /// Flattened collection of all [`Module`] variables, including those in sub-modules.
+ all_variables: Option<StraightHashMap<Dynamic>>,
+ /// Functions (both native Rust and scripted).
+ functions: Option<StraightHashMap<FuncInfo>>,
+ /// Flattened collection of all functions, native Rust and scripted.
+ /// including those in sub-modules.
+ all_functions: Option<StraightHashMap<CallableFunction>>,
+ /// Bloom filter on native Rust functions (in scripted hash format) that contain [`Dynamic`] parameters.
+ dynamic_functions_filter: Option<Box<BloomFilterU64>>,
+ /// Iterator functions, keyed by the type producing the iterator.
+ type_iterators: Option<BTreeMap<TypeId, Shared<IteratorFn>>>,
+ /// Flattened collection of iterator functions, including those in sub-modules.
+ all_type_iterators: Option<BTreeMap<TypeId, Shared<IteratorFn>>>,
+ /// Flags.
+ pub(crate) flags: ModuleFlags,
+}
+
+impl Default for Module {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl fmt::Debug for Module {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut d = f.debug_struct("Module");
+
+ d.field("id", &self.id)
+ .field(
+ "modules",
+ &self
+ .modules
+ .as_ref()
+ .into_iter()
+ .flat_map(BTreeMap::keys)
+ .map(SmartString::as_str)
+ .collect::<Vec<_>>(),
+ )
+ .field("vars", &self.variables)
+ .field(
+ "functions",
+ &self
+ .iter_fn()
+ .map(|f| f.func.to_string())
+ .collect::<Vec<_>>(),
+ );
+
+ #[cfg(feature = "metadata")]
+ d.field("doc", &self.doc);
+
+ d.finish()
+ }
+}
+
+#[cfg(not(feature = "no_function"))]
+impl<T: IntoIterator<Item = Shared<crate::ast::ScriptFnDef>>> From<T> for Module {
+ fn from(iter: T) -> Self {
+ let mut module = Self::new();
+ iter.into_iter().for_each(|fn_def| {
+ module.set_script_fn(fn_def);
+ });
+ module
+ }
+}
+
+impl<M: AsRef<Module>> Add<M> for &Module {
+ type Output = Module;
+
+ #[inline]
+ fn add(self, rhs: M) -> Self::Output {
+ let mut module = self.clone();
+ module.merge(rhs.as_ref());
+ module
+ }
+}
+
+impl<M: AsRef<Module>> Add<M> for Module {
+ type Output = Self;
+
+ #[inline(always)]
+ fn add(mut self, rhs: M) -> Self::Output {
+ self.merge(rhs.as_ref());
+ self
+ }
+}
+
+impl<M: Into<Module>> AddAssign<M> for Module {
+ #[inline(always)]
+ fn add_assign(&mut self, rhs: M) {
+ self.combine(rhs.into());
+ }
+}
+
+#[inline(always)]
+fn new_hash_map<T>(size: usize) -> StraightHashMap<T> {
+ StraightHashMap::with_capacity_and_hasher(size, Default::default())
+}
+
+impl Module {
+ /// Create a new [`Module`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// module.set_var("answer", 42_i64);
+ /// assert_eq!(module.get_var_value::<i64>("answer").expect("answer should exist"), 42);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub const fn new() -> Self {
+ Self {
+ id: None,
+ #[cfg(feature = "metadata")]
+ doc: None,
+ custom_types: None,
+ modules: None,
+ variables: None,
+ all_variables: None,
+ functions: None,
+ all_functions: None,
+ dynamic_functions_filter: None,
+ type_iterators: None,
+ all_type_iterators: None,
+ flags: ModuleFlags::INDEXED,
+ }
+ }
+
+ /// Get the ID of the [`Module`], if any.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// module.set_id("hello");
+ /// assert_eq!(module.id(), Some("hello"));
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn id(&self) -> Option<&str> {
+ self.id.as_ref().map(|s| s.as_str())
+ }
+
+ /// Get the ID of the [`Module`] as an [`Identifier`], if any.
+ #[inline(always)]
+ #[must_use]
+ pub(crate) const fn id_raw(&self) -> Option<&ImmutableString> {
+ self.id.as_ref()
+ }
+
+ /// Set the ID of the [`Module`].
+ ///
+ /// If the string is empty, it is equivalent to clearing the ID.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// module.set_id("hello");
+ /// assert_eq!(module.id(), Some("hello"));
+ /// ```
+ #[inline(always)]
+ pub fn set_id(&mut self, id: impl Into<ImmutableString>) -> &mut Self {
+ let id = id.into();
+ self.id = (!id.is_empty()).then(|| id);
+ self
+ }
+
+ /// Clear the ID of the [`Module`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// module.set_id("hello");
+ /// assert_eq!(module.id(), Some("hello"));
+ /// module.clear_id();
+ /// assert_eq!(module.id(), None);
+ /// ```
+ #[inline(always)]
+ pub fn clear_id(&mut self) -> &mut Self {
+ self.id = None;
+ self
+ }
+
+ /// Get the documentation of the [`Module`], if any.
+ /// Exported under the `metadata` feature only.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// module.set_doc("//! This is my special module.");
+ /// assert_eq!(module.doc(), "//! This is my special module.");
+ /// ```
+ #[cfg(feature = "metadata")]
+ #[inline]
+ #[must_use]
+ pub fn doc(&self) -> &str {
+ self.doc.as_deref().map_or("", SmartString::as_str)
+ }
+
+ /// Set the documentation of the [`Module`].
+ /// Exported under the `metadata` feature only.
+ ///
+ /// If the string is empty, it is equivalent to clearing the documentation.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// module.set_doc("//! This is my special module.");
+ /// assert_eq!(module.doc(), "//! This is my special module.");
+ /// ```
+ #[cfg(feature = "metadata")]
+ #[inline(always)]
+ pub fn set_doc(&mut self, doc: impl Into<crate::SmartString>) -> &mut Self {
+ self.doc = Some(Box::new(doc.into()));
+ self
+ }
+
+ /// Clear the documentation of the [`Module`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// module.set_doc("//! This is my special module.");
+ /// assert_eq!(module.doc(), "//! This is my special module.");
+ /// module.clear_doc();
+ /// assert_eq!(module.doc(), "");
+ /// ```
+ #[cfg(feature = "metadata")]
+ #[inline(always)]
+ pub fn clear_doc(&mut self) -> &mut Self {
+ self.doc = None;
+ self
+ }
+
+ /// Clear the [`Module`].
+ #[inline(always)]
+ pub fn clear(&mut self) {
+ #[cfg(feature = "metadata")]
+ {
+ self.doc = None;
+ }
+ self.custom_types = None;
+ self.modules = None;
+ self.variables = None;
+ self.all_variables = None;
+ self.functions = None;
+ self.all_functions = None;
+ self.dynamic_functions_filter = None;
+ self.type_iterators = None;
+ self.all_type_iterators = None;
+ self.flags
+ .remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS);
+ }
+
+ /// Map a custom type to a friendly display name.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// #[derive(Clone)]
+ /// struct TestStruct;
+ ///
+ /// let name = std::any::type_name::<TestStruct>();
+ ///
+ /// let mut module = Module::new();
+ ///
+ /// module.set_custom_type::<TestStruct>("MyType");
+ ///
+ /// assert_eq!(module.get_custom_type(name), Some("MyType"));
+ /// ```
+ #[inline(always)]
+ pub fn set_custom_type<T>(&mut self, name: &str) -> &mut Self {
+ self.custom_types
+ .get_or_insert_with(Default::default)
+ .add_type::<T>(name);
+ self
+ }
+ /// Map a custom type to a friendly display name.
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// #[derive(Clone)]
+ /// struct TestStruct;
+ ///
+ /// let name = std::any::type_name::<TestStruct>();
+ ///
+ /// let mut module = Module::new();
+ ///
+ /// module.set_custom_type_raw(name, "MyType");
+ ///
+ /// assert_eq!(module.get_custom_type(name), Some("MyType"));
+ /// ```
+ #[inline(always)]
+ pub fn set_custom_type_raw(
+ &mut self,
+ type_path: impl Into<Identifier>,
+ name: impl Into<Identifier>,
+ ) -> &mut Self {
+ self.custom_types
+ .get_or_insert_with(Default::default)
+ .add(type_path, name);
+ self
+ }
+ /// Get the display name of a registered custom type.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// #[derive(Clone)]
+ /// struct TestStruct;
+ ///
+ /// let name = std::any::type_name::<TestStruct>();
+ ///
+ /// let mut module = Module::new();
+ ///
+ /// module.set_custom_type::<TestStruct>("MyType");
+ ///
+ /// assert_eq!(module.get_custom_type(name), Some("MyType"));
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn get_custom_type(&self, key: &str) -> Option<&str> {
+ self.custom_types
+ .as_ref()
+ .and_then(|c| c.get(key))
+ .map(|t| t.display_name.as_str())
+ }
+
+ /// Returns `true` if this [`Module`] contains no items.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let module = Module::new();
+ /// assert!(module.is_empty());
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ !self.flags.contains(ModuleFlags::INDEXED_GLOBAL_FUNCTIONS)
+ && self
+ .functions
+ .as_ref()
+ .map_or(true, StraightHashMap::is_empty)
+ && self.variables.as_ref().map_or(true, BTreeMap::is_empty)
+ && self.modules.as_ref().map_or(true, BTreeMap::is_empty)
+ && self
+ .type_iterators
+ .as_ref()
+ .map_or(true, BTreeMap::is_empty)
+ && self
+ .all_functions
+ .as_ref()
+ .map_or(true, StraightHashMap::is_empty)
+ && self
+ .all_variables
+ .as_ref()
+ .map_or(true, StraightHashMap::is_empty)
+ && self
+ .all_type_iterators
+ .as_ref()
+ .map_or(true, BTreeMap::is_empty)
+ }
+
+ /// Is the [`Module`] indexed?
+ ///
+ /// A module must be indexed before it can be used in an `import` statement.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// assert!(module.is_indexed());
+ ///
+ /// module.set_native_fn("foo", |x: &mut i64, y: i64| { *x = y; Ok(()) });
+ /// assert!(!module.is_indexed());
+ ///
+ /// # #[cfg(not(feature = "no_module"))]
+ /// # {
+ /// module.build_index();
+ /// assert!(module.is_indexed());
+ /// # }
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_indexed(&self) -> bool {
+ self.flags.contains(ModuleFlags::INDEXED)
+ }
+
+ /// _(metadata)_ Generate signatures for all the non-private functions in the [`Module`].
+ /// Exported under the `metadata` feature only.
+ #[cfg(feature = "metadata")]
+ #[inline]
+ pub fn gen_fn_signatures(&self) -> impl Iterator<Item = String> + '_ {
+ self.iter_fn()
+ .filter(|&f| match f.metadata.access {
+ FnAccess::Public => true,
+ FnAccess::Private => false,
+ })
+ .map(FuncInfo::gen_signature)
+ }
+
+ /// Does a variable exist in the [`Module`]?
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// module.set_var("answer", 42_i64);
+ /// assert!(module.contains_var("answer"));
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn contains_var(&self, name: &str) -> bool {
+ self.variables
+ .as_ref()
+ .map_or(false, |m| m.contains_key(name))
+ }
+
+ /// Get the value of a [`Module`] variable.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// module.set_var("answer", 42_i64);
+ /// assert_eq!(module.get_var_value::<i64>("answer").expect("answer should exist"), 42);
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn get_var_value<T: Variant + Clone>(&self, name: &str) -> Option<T> {
+ self.get_var(name).and_then(Dynamic::try_cast::<T>)
+ }
+
+ /// Get a [`Module`] variable as a [`Dynamic`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// module.set_var("answer", 42_i64);
+ /// assert_eq!(module.get_var("answer").expect("answer should exist").cast::<i64>(), 42);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn get_var(&self, name: &str) -> Option<Dynamic> {
+ self.variables.as_ref().and_then(|m| m.get(name)).cloned()
+ }
+
+ /// Set a variable into the [`Module`].
+ ///
+ /// If there is an existing variable of the same name, it is replaced.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// module.set_var("answer", 42_i64);
+ /// assert_eq!(module.get_var_value::<i64>("answer").expect("answer should exist"), 42);
+ /// ```
+ #[inline]
+ pub fn set_var(
+ &mut self,
+ name: impl Into<Identifier>,
+ value: impl Variant + Clone,
+ ) -> &mut Self {
+ let ident = name.into();
+ let value = Dynamic::from(value);
+
+ if self.is_indexed() {
+ let hash_var = crate::calc_var_hash(Some(""), &ident);
+
+ // Catch hash collisions in testing environment only.
+ #[cfg(feature = "testing-environ")]
+ if let Some(_) = self.all_variables.as_ref().and_then(|f| f.get(&hash_var)) {
+ panic!(
+ "Hash {} already exists when registering variable {}",
+ hash_var, ident
+ );
+ }
+
+ self.all_variables
+ .get_or_insert_with(Default::default)
+ .insert(hash_var, value.clone());
+ }
+ self.variables
+ .get_or_insert_with(Default::default)
+ .insert(ident, value);
+ self
+ }
+
+ /// Get a namespace-qualified [`Module`] variable as a [`Dynamic`].
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ pub(crate) fn get_qualified_var(&self, hash_var: u64) -> Option<Dynamic> {
+ self.all_variables
+ .as_ref()
+ .and_then(|c| c.get(&hash_var).cloned())
+ }
+
+ /// Set a script-defined function into the [`Module`].
+ ///
+ /// If there is an existing function of the same name and number of arguments, it is replaced.
+ #[cfg(not(feature = "no_function"))]
+ #[inline]
+ pub fn set_script_fn(&mut self, fn_def: impl Into<Shared<crate::ast::ScriptFnDef>>) -> u64 {
+ let fn_def = fn_def.into();
+
+ // None + function name + number of arguments.
+ let namespace = FnNamespace::Internal;
+ let num_params = fn_def.params.len();
+ let hash_script = crate::calc_fn_hash(None, &fn_def.name, num_params);
+ #[cfg(not(feature = "no_object"))]
+ let (hash_script, namespace) = if let Some(ref this_type) = fn_def.this_type {
+ let hash = crate::calc_typed_method_hash(hash_script, this_type);
+ (hash, FnNamespace::Global)
+ } else {
+ (hash_script, namespace)
+ };
+
+ // Catch hash collisions in testing environment only.
+ #[cfg(feature = "testing-environ")]
+ if let Some(f) = self.functions.as_ref().and_then(|f| f.get(&hash_script)) {
+ panic!(
+ "Hash {} already exists when registering function {:#?}:\n{:#?}",
+ hash_script, fn_def, f
+ );
+ }
+
+ #[cfg(feature = "metadata")]
+ let params_info = fn_def.params.iter().map(Into::into).collect();
+
+ self.functions
+ .get_or_insert_with(|| new_hash_map(FN_MAP_SIZE))
+ .insert(
+ hash_script,
+ FuncInfo {
+ metadata: FuncInfoMetadata {
+ name: fn_def.name.as_str().into(),
+ namespace,
+ access: fn_def.access,
+ #[cfg(not(feature = "no_object"))]
+ this_type: fn_def.this_type.clone(),
+ num_params,
+ param_types: FnArgsVec::new_const(),
+ #[cfg(feature = "metadata")]
+ params_info,
+ #[cfg(feature = "metadata")]
+ return_type: "".into(),
+ #[cfg(feature = "metadata")]
+ comments: Box::default(),
+ }
+ .into(),
+ func: fn_def.into(),
+ },
+ );
+
+ self.flags
+ .remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS);
+
+ hash_script
+ }
+
+ /// Get a shared reference to the script-defined function in the [`Module`] based on name
+ /// and number of parameters.
+ #[cfg(not(feature = "no_function"))]
+ #[inline]
+ #[must_use]
+ pub fn get_script_fn(
+ &self,
+ name: impl AsRef<str>,
+ num_params: usize,
+ ) -> Option<&Shared<crate::ast::ScriptFnDef>> {
+ self.functions.as_ref().and_then(|lib| {
+ let name = name.as_ref();
+
+ lib.values()
+ .find(|&f| f.metadata.num_params == num_params && f.metadata.name == name)
+ .and_then(|f| f.func.get_script_fn_def())
+ })
+ }
+
+ /// Get a mutable reference to the underlying [`BTreeMap`] of sub-modules,
+ /// creating one if empty.
+ ///
+ /// # WARNING
+ ///
+ /// By taking a mutable reference, it is assumed that some sub-modules will be modified.
+ /// Thus the [`Module`] is automatically set to be non-indexed.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ #[must_use]
+ pub(crate) fn get_sub_modules_mut(&mut self) -> &mut BTreeMap<Identifier, SharedModule> {
+ // We must assume that the user has changed the sub-modules
+ // (otherwise why take a mutable reference?)
+ self.all_functions = None;
+ self.all_variables = None;
+ self.all_type_iterators = None;
+ self.flags
+ .remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS);
+
+ self.modules.get_or_insert_with(Default::default)
+ }
+
+ /// Does a sub-module exist in the [`Module`]?
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// let sub_module = Module::new();
+ /// module.set_sub_module("question", sub_module);
+ /// assert!(module.contains_sub_module("question"));
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn contains_sub_module(&self, name: &str) -> bool {
+ self.modules
+ .as_ref()
+ .map_or(false, |m| m.contains_key(name))
+ }
+
+ /// Get a sub-module in the [`Module`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// let sub_module = Module::new();
+ /// module.set_sub_module("question", sub_module);
+ /// assert!(module.get_sub_module("question").is_some());
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn get_sub_module(&self, name: &str) -> Option<&Module> {
+ self.modules
+ .as_ref()
+ .and_then(|m| m.get(name))
+ .map(|m| &**m)
+ }
+
+ /// Set a sub-module into the [`Module`].
+ ///
+ /// If there is an existing sub-module of the same name, it is replaced.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// let sub_module = Module::new();
+ /// module.set_sub_module("question", sub_module);
+ /// assert!(module.get_sub_module("question").is_some());
+ /// ```
+ #[inline]
+ pub fn set_sub_module(
+ &mut self,
+ name: impl Into<Identifier>,
+ sub_module: impl Into<SharedModule>,
+ ) -> &mut Self {
+ self.modules
+ .get_or_insert_with(Default::default)
+ .insert(name.into(), sub_module.into());
+ self.flags
+ .remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS);
+ self
+ }
+
+ /// Does the particular Rust function exist in the [`Module`]?
+ ///
+ /// The [`u64`] hash is returned by the [`set_native_fn`][Module::set_native_fn] call.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// let hash = module.set_native_fn("calc", || Ok(42_i64));
+ /// assert!(module.contains_fn(hash));
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn contains_fn(&self, hash_fn: u64) -> bool {
+ self.functions
+ .as_ref()
+ .map_or(false, |m| m.contains_key(&hash_fn))
+ }
+
+ /// _(metadata)_ Update the metadata (parameter names/types and return type) of a registered function.
+ /// Exported under the `metadata` feature only.
+ ///
+ /// The [`u64`] hash is returned by the [`set_native_fn`][Module::set_native_fn] call.
+ ///
+ /// ## Parameter Names and Types
+ ///
+ /// Each parameter name/type pair should be a single string of the format: `var_name: type`.
+ ///
+ /// ## Return Type
+ ///
+ /// The _last entry_ in the list should be the _return type_ of the function.
+ /// In other words, the number of entries should be one larger than the number of parameters.
+ #[cfg(feature = "metadata")]
+ #[inline]
+ pub fn update_fn_metadata<S: Into<Identifier>>(
+ &mut self,
+ hash_fn: u64,
+ arg_names: impl IntoIterator<Item = S>,
+ ) -> &mut Self {
+ let mut param_names = arg_names
+ .into_iter()
+ .map(Into::into)
+ .collect::<FnArgsVec<_>>();
+
+ if let Some(f) = self.functions.as_mut().and_then(|m| m.get_mut(&hash_fn)) {
+ let (param_names, return_type_name) = if param_names.len() > f.metadata.num_params {
+ let return_type = param_names.pop().unwrap();
+ (param_names, return_type)
+ } else {
+ (param_names, crate::SmartString::new_const())
+ };
+ f.metadata.params_info = param_names;
+ f.metadata.return_type = return_type_name;
+ }
+
+ self
+ }
+
+ /// _(metadata)_ Update the metadata (parameter names/types, return type and doc-comments) of a registered function.
+ /// Exported under the `metadata` feature only.
+ ///
+ /// The [`u64`] hash is returned by the [`set_native_fn`][Module::set_native_fn] call.
+ ///
+ /// ## Parameter Names and Types
+ ///
+ /// Each parameter name/type pair should be a single string of the format: `var_name: type`.
+ ///
+ /// ## Return Type
+ ///
+ /// The _last entry_ in the list should be the _return type_ of the function. In other words,
+ /// the number of entries should be one larger than the number of parameters.
+ ///
+ /// ## Comments
+ ///
+ /// Block doc-comments should be kept in a separate string slice.
+ ///
+ /// Line doc-comments should be merged, with line-breaks, into a single string slice without a final termination line-break.
+ ///
+ /// Leading white-spaces should be stripped, and each string slice always starts with the corresponding
+ /// doc-comment leader: `///` or `/**`.
+ ///
+ /// Each line in non-block doc-comments should start with `///`.
+ #[cfg(feature = "metadata")]
+ #[inline]
+ pub fn update_fn_metadata_with_comments<A: Into<Identifier>, C: Into<Identifier>>(
+ &mut self,
+ hash_fn: u64,
+ arg_names: impl IntoIterator<Item = A>,
+ comments: impl IntoIterator<Item = C>,
+ ) -> &mut Self {
+ self.update_fn_metadata(hash_fn, arg_names);
+
+ self.functions
+ .as_mut()
+ .and_then(|m| m.get_mut(&hash_fn))
+ .unwrap()
+ .metadata
+ .comments = comments.into_iter().map(Into::into).collect();
+
+ self
+ }
+
+ /// Update the namespace of a registered function.
+ ///
+ /// The [`u64`] hash is returned by the [`set_native_fn`][Module::set_native_fn] call.
+ #[inline]
+ pub fn update_fn_namespace(&mut self, hash_fn: u64, namespace: FnNamespace) -> &mut Self {
+ if let Some(f) = self.functions.as_mut().and_then(|m| m.get_mut(&hash_fn)) {
+ f.metadata.namespace = namespace;
+ self.flags
+ .remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS);
+ }
+ self
+ }
+
+ /// Remap type ID.
+ #[inline]
+ #[must_use]
+ fn map_type(map: bool, type_id: TypeId) -> TypeId {
+ if !map {
+ return type_id;
+ }
+ if type_id == TypeId::of::<&str>() {
+ // Map &str to ImmutableString
+ return TypeId::of::<ImmutableString>();
+ }
+ if type_id == TypeId::of::<String>() {
+ // Map String to ImmutableString
+ return TypeId::of::<ImmutableString>();
+ }
+
+ type_id
+ }
+
+ /// Set a native Rust function into the [`Module`], returning a [`u64`] hash key.
+ ///
+ /// If there is an existing Rust function of the same hash, it is replaced.
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is very low level.
+ ///
+ /// ## Parameter Names and Types
+ ///
+ /// Each parameter name/type pair should be a single string of the format: `var_name: type`.
+ ///
+ /// ## Return Type
+ ///
+ /// The _last entry_ in the list should be the _return type_ of the function.
+ /// In other words, the number of entries should be one larger than the number of parameters.
+ #[inline]
+ pub fn set_fn(
+ &mut self,
+ name: impl AsRef<str>,
+ namespace: FnNamespace,
+ access: FnAccess,
+ arg_names: Option<&[&str]>,
+ arg_types: impl AsRef<[TypeId]>,
+ func: CallableFunction,
+ ) -> u64 {
+ let _arg_names = arg_names;
+ let is_method = func.is_method();
+
+ let param_types = arg_types
+ .as_ref()
+ .iter()
+ .enumerate()
+ .map(|(i, &type_id)| Self::map_type(!is_method || i > 0, type_id))
+ .collect::<FnArgsVec<_>>();
+
+ let is_dynamic = param_types
+ .iter()
+ .any(|&type_id| type_id == TypeId::of::<Dynamic>());
+
+ #[cfg(feature = "metadata")]
+ let (param_names, return_type_name) = {
+ let mut names = _arg_names
+ .into_iter()
+ .flatten()
+ .map(|&s| s.into())
+ .collect::<FnArgsVec<_>>();
+ let return_type = if names.len() > param_types.len() {
+ names.pop().unwrap()
+ } else {
+ crate::SmartString::new_const()
+ };
+ names.shrink_to_fit();
+ (names, return_type)
+ };
+
+ let name = name.as_ref();
+ let hash_base = calc_fn_hash(None, name, param_types.len());
+ let hash_fn = calc_fn_hash_full(hash_base, param_types.iter().copied());
+
+ // Catch hash collisions in testing environment only.
+ #[cfg(feature = "testing-environ")]
+ if let Some(f) = self.functions.as_ref().and_then(|f| f.get(&hash_base)) {
+ panic!(
+ "Hash {} already exists when registering function {}:\n{:#?}",
+ hash_base, name, f
+ );
+ }
+
+ if is_dynamic {
+ self.dynamic_functions_filter
+ .get_or_insert_with(Default::default)
+ .mark(hash_base);
+ }
+
+ self.functions
+ .get_or_insert_with(|| new_hash_map(FN_MAP_SIZE))
+ .insert(
+ hash_fn,
+ FuncInfo {
+ func,
+ metadata: FuncInfoMetadata {
+ name: name.into(),
+ namespace,
+ access,
+ #[cfg(not(feature = "no_object"))]
+ this_type: None,
+ num_params: param_types.len(),
+ param_types,
+ #[cfg(feature = "metadata")]
+ params_info: param_names,
+ #[cfg(feature = "metadata")]
+ return_type: return_type_name,
+ #[cfg(feature = "metadata")]
+ comments: Box::default(),
+ }
+ .into(),
+ },
+ );
+
+ self.flags
+ .remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS);
+
+ hash_fn
+ }
+
+ /// _(metadata)_ Set a native Rust function into the [`Module`], returning a [`u64`] hash key.
+ /// Exported under the `metadata` feature only.
+ ///
+ /// If there is an existing Rust function of the same hash, it is replaced.
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is very low level.
+ ///
+ /// ## Parameter Names and Types
+ ///
+ /// Each parameter name/type pair should be a single string of the format: `var_name: type`.
+ ///
+ /// ## Return Type
+ ///
+ /// The _last entry_ in the list should be the _return type_ of the function.
+ /// In other words, the number of entries should be one larger than the number of parameters.
+ ///
+ /// ## Comments
+ ///
+ /// Block doc-comments should be kept in a separate string slice.
+ ///
+ /// Line doc-comments should be merged, with line-breaks, into a single string slice without a final termination line-break.
+ ///
+ /// Leading white-spaces should be stripped, and each string slice always starts with the corresponding
+ /// doc-comment leader: `///` or `/**`.
+ ///
+ /// Each line in non-block doc-comments should start with `///`.
+ #[cfg(feature = "metadata")]
+ #[inline]
+ pub fn set_fn_with_comments<S: AsRef<str>>(
+ &mut self,
+ name: impl AsRef<str>,
+ namespace: FnNamespace,
+ access: FnAccess,
+ arg_names: Option<&[&str]>,
+ arg_types: impl AsRef<[TypeId]>,
+ comments: impl IntoIterator<Item = S>,
+ func: CallableFunction,
+ ) -> u64 {
+ let hash = self.set_fn(name, namespace, access, arg_names, arg_types, func);
+
+ self.functions
+ .as_mut()
+ .unwrap()
+ .get_mut(&hash)
+ .unwrap()
+ .metadata
+ .comments = comments.into_iter().map(|s| s.as_ref().into()).collect();
+
+ hash
+ }
+
+ /// Set a native Rust function into the [`Module`], returning a [`u64`] hash key.
+ ///
+ /// If there is a similar existing Rust function, it is replaced.
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is very low level.
+ ///
+ /// # Arguments
+ ///
+ /// A list of [`TypeId`]'s is taken as the argument types.
+ ///
+ /// Arguments are simply passed in as a mutable array of [`&mut Dynamic`][Dynamic],
+ /// which is guaranteed to contain enough arguments of the correct types.
+ ///
+ /// The function is assumed to be a _method_, meaning that the first argument should not be consumed.
+ /// All other arguments can be consumed.
+ ///
+ /// To access a primary argument value (i.e. cloning is cheap), use: `args[n].as_xxx().unwrap()`
+ ///
+ /// To access an argument value and avoid cloning, use `args[n].take().cast::<T>()`.
+ /// Notice that this will _consume_ the argument, replacing it with `()`.
+ ///
+ /// To access the first mutable argument, use `args.get_mut(0).unwrap()`
+ ///
+ /// # Function Metadata
+ ///
+ /// No metadata for the function is registered. Use [`update_fn_metadata`][Module::update_fn_metadata] to add metadata.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::{Module, FnNamespace, FnAccess};
+ ///
+ /// let mut module = Module::new();
+ /// let hash = module.set_raw_fn("double_or_not", FnNamespace::Internal, FnAccess::Public,
+ /// // Pass parameter types via a slice with TypeId's
+ /// &[std::any::TypeId::of::<i64>(), std::any::TypeId::of::<bool>()],
+ /// // Fixed closure signature
+ /// |context, args| {
+ /// // 'args' is guaranteed to be the right length and of the correct types
+ ///
+ /// // Get the second parameter by 'consuming' it
+ /// let double = args[1].take().cast::<bool>();
+ /// // Since it is a primary type, it can also be cheaply copied
+ /// let double = args[1].clone_cast::<bool>();
+ /// // Get a mutable reference to the first argument.
+ /// let mut x = args[0].write_lock::<i64>().unwrap();
+ ///
+ /// let orig = *x;
+ ///
+ /// if double {
+ /// *x *= 2; // the first argument can be mutated
+ /// }
+ ///
+ /// Ok(orig) // return RhaiResult<T>
+ /// });
+ ///
+ /// assert!(module.contains_fn(hash));
+ /// ```
+ #[inline(always)]
+ pub fn set_raw_fn<T: Variant + Clone>(
+ &mut self,
+ name: impl AsRef<str>,
+ namespace: FnNamespace,
+ access: FnAccess,
+ arg_types: impl AsRef<[TypeId]>,
+ func: impl Fn(NativeCallContext, &mut FnCallArgs) -> RhaiResultOf<T> + SendSync + 'static,
+ ) -> u64 {
+ let f = move |ctx: Option<NativeCallContext>, args: &mut FnCallArgs| {
+ func(ctx.unwrap(), args).map(Dynamic::from)
+ };
+
+ self.set_fn(
+ name,
+ namespace,
+ access,
+ None,
+ arg_types,
+ CallableFunction::Method {
+ func: Shared::new(f),
+ has_context: true,
+ is_pure: false,
+ },
+ )
+ }
+
+ /// Set a native Rust function into the [`Module`], returning a [`u64`] hash key.
+ ///
+ /// If there is a similar existing Rust function, it is replaced.
+ ///
+ /// # Function Namespace
+ ///
+ /// The default function namespace is [`FnNamespace::Internal`].
+ /// Use [`update_fn_namespace`][Module::update_fn_namespace] to change it.
+ ///
+ /// # Function Metadata
+ ///
+ /// No metadata for the function is registered.
+ /// Use [`update_fn_metadata`][Module::update_fn_metadata] to add metadata.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// let hash = module.set_native_fn("calc", || Ok(42_i64));
+ /// assert!(module.contains_fn(hash));
+ /// ```
+ #[inline(always)]
+ pub fn set_native_fn<A: 'static, const N: usize, const C: bool, T, F>(
+ &mut self,
+ name: impl AsRef<str> + Into<Identifier>,
+ func: F,
+ ) -> u64
+ where
+ T: Variant + Clone,
+ F: RegisterNativeFunction<A, N, C, T, true>,
+ {
+ let fn_name = name.into();
+ let is_pure = true;
+
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ let is_pure = is_pure && (F::num_params() != 3 || fn_name != crate::engine::FN_IDX_SET);
+ #[cfg(not(feature = "no_object"))]
+ let is_pure =
+ is_pure && (F::num_params() != 2 || !fn_name.starts_with(crate::engine::FN_SET));
+
+ let func = func.into_callable_function(fn_name.clone(), is_pure);
+
+ self.set_fn(
+ fn_name,
+ FnNamespace::Internal,
+ FnAccess::Public,
+ None,
+ F::param_types(),
+ func,
+ )
+ }
+
+ /// Set a Rust getter function taking one mutable parameter, returning a [`u64`] hash key.
+ /// This function is automatically exposed to the global namespace.
+ ///
+ /// If there is a similar existing Rust getter function, it is replaced.
+ ///
+ /// # Function Metadata
+ ///
+ /// No metadata for the function is registered.
+ /// Use [`update_fn_metadata`][Module::update_fn_metadata] to add metadata.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use rhai::Module;
+ /// let mut module = Module::new();
+ /// let hash = module.set_getter_fn("value", |x: &mut i64| { Ok(*x) });
+ /// assert!(module.contains_fn(hash));
+ /// ```
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ pub fn set_getter_fn<A, const C: bool, T, F>(&mut self, name: impl AsRef<str>, func: F) -> u64
+ where
+ A: Variant + Clone,
+ T: Variant + Clone,
+ F: RegisterNativeFunction<(Mut<A>,), 1, C, T, true> + SendSync + 'static,
+ {
+ let fn_name = crate::engine::make_getter(name.as_ref());
+ let func = func.into_callable_function(fn_name.clone(), true);
+
+ self.set_fn(
+ fn_name,
+ FnNamespace::Global,
+ FnAccess::Public,
+ None,
+ F::param_types(),
+ func,
+ )
+ }
+
+ /// Set a Rust setter function taking two parameters (the first one mutable) into the [`Module`],
+ /// returning a [`u64`] hash key.
+ /// This function is automatically exposed to the global namespace.
+ ///
+ /// If there is a similar existing setter Rust function, it is replaced.
+ ///
+ /// # Function Metadata
+ ///
+ /// No metadata for the function is registered.
+ /// Use [`update_fn_metadata`][Module::update_fn_metadata] to add metadata.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::{Module, ImmutableString};
+ ///
+ /// let mut module = Module::new();
+ /// let hash = module.set_setter_fn("value", |x: &mut i64, y: ImmutableString| {
+ /// *x = y.len() as i64;
+ /// Ok(())
+ /// });
+ /// assert!(module.contains_fn(hash));
+ /// ```
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ pub fn set_setter_fn<A, const C: bool, T, F>(&mut self, name: impl AsRef<str>, func: F) -> u64
+ where
+ A: Variant + Clone,
+ T: Variant + Clone,
+ F: RegisterNativeFunction<(Mut<A>, T), 2, C, (), true> + SendSync + 'static,
+ {
+ let fn_name = crate::engine::make_setter(name.as_ref());
+ let func = func.into_callable_function(fn_name.clone(), false);
+
+ self.set_fn(
+ fn_name,
+ FnNamespace::Global,
+ FnAccess::Public,
+ None,
+ F::param_types(),
+ func,
+ )
+ }
+
+ /// Set a pair of Rust getter and setter functions into the [`Module`], returning both [`u64`] hash keys.
+ /// This is a short-hand for [`set_getter_fn`][Module::set_getter_fn] and [`set_setter_fn`][Module::set_setter_fn].
+ ///
+ /// These function are automatically exposed to the global namespace.
+ ///
+ /// If there are similar existing Rust functions, they are replaced.
+ ///
+ /// # Function Metadata
+ ///
+ /// No metadata for the function is registered.
+ /// Use [`update_fn_metadata`][Module::update_fn_metadata] to add metadata.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::{Module, ImmutableString};
+ ///
+ /// let mut module = Module::new();
+ /// let (hash_get, hash_set) = module.set_getter_setter_fn("value",
+ /// |x: &mut i64| { Ok(x.to_string().into()) },
+ /// |x: &mut i64, y: ImmutableString| {
+ /// *x = y.len() as i64;
+ /// Ok(())
+ /// }
+ /// );
+ /// assert!(module.contains_fn(hash_get));
+ /// assert!(module.contains_fn(hash_set));
+ /// ```
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ pub fn set_getter_setter_fn<
+ A: Variant + Clone,
+ const C1: bool,
+ const C2: bool,
+ T: Variant + Clone,
+ >(
+ &mut self,
+ name: impl AsRef<str>,
+ getter: impl RegisterNativeFunction<(Mut<A>,), 1, C1, T, true> + SendSync + 'static,
+ setter: impl RegisterNativeFunction<(Mut<A>, T), 2, C2, (), true> + SendSync + 'static,
+ ) -> (u64, u64) {
+ (
+ self.set_getter_fn(name.as_ref(), getter),
+ self.set_setter_fn(name.as_ref(), setter),
+ )
+ }
+
+ /// Set a Rust index getter taking two parameters (the first one mutable) into the [`Module`],
+ /// returning a [`u64`] hash key.
+ /// This function is automatically exposed to the global namespace.
+ ///
+ /// If there is a similar existing setter Rust function, it is replaced.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the type is [`Array`][crate::Array] or [`Map`][crate::Map].
+ /// Indexers for arrays, object maps and strings cannot be registered.
+ ///
+ /// # Function Metadata
+ ///
+ /// No metadata for the function is registered.
+ /// Use [`update_fn_metadata`][Module::update_fn_metadata] to add metadata.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::{Module, ImmutableString};
+ ///
+ /// let mut module = Module::new();
+ /// let hash = module.set_indexer_get_fn(|x: &mut i64, y: ImmutableString| {
+ /// Ok(*x + y.len() as i64)
+ /// });
+ /// assert!(module.contains_fn(hash));
+ /// ```
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ #[inline]
+ pub fn set_indexer_get_fn<A, B, const C: bool, T, F>(&mut self, func: F) -> u64
+ where
+ A: Variant + Clone,
+ B: Variant + Clone,
+ T: Variant + Clone,
+ F: RegisterNativeFunction<(Mut<A>, B), 2, C, T, true> + SendSync + 'static,
+ {
+ #[cfg(not(feature = "no_index"))]
+ if TypeId::of::<A>() == TypeId::of::<crate::Array>() {
+ panic!("Cannot register indexer for arrays.");
+ }
+ #[cfg(not(feature = "no_object"))]
+ if TypeId::of::<A>() == TypeId::of::<crate::Map>() {
+ panic!("Cannot register indexer for object maps.");
+ }
+ if TypeId::of::<A>() == TypeId::of::<String>()
+ || TypeId::of::<A>() == TypeId::of::<&str>()
+ || TypeId::of::<A>() == TypeId::of::<ImmutableString>()
+ {
+ panic!("Cannot register indexer for strings.");
+ }
+
+ self.set_fn(
+ crate::engine::FN_IDX_GET,
+ FnNamespace::Global,
+ FnAccess::Public,
+ None,
+ F::param_types(),
+ func.into_callable_function(crate::engine::FN_IDX_GET.into(), true),
+ )
+ }
+
+ /// Set a Rust index setter taking three parameters (the first one mutable) into the [`Module`],
+ /// returning a [`u64`] hash key.
+ /// This function is automatically exposed to the global namespace.
+ ///
+ /// If there is a similar existing Rust function, it is replaced.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the type is [`Array`][crate::Array] or [`Map`][crate::Map].
+ /// Indexers for arrays, object maps and strings cannot be registered.
+ ///
+ /// # Function Metadata
+ ///
+ /// No metadata for the function is registered.
+ /// Use [`update_fn_metadata`][Module::update_fn_metadata] to add metadata.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::{Module, ImmutableString};
+ ///
+ /// let mut module = Module::new();
+ /// let hash = module.set_indexer_set_fn(|x: &mut i64, y: ImmutableString, value: i64| {
+ /// *x = y.len() as i64 + value; Ok(())
+ /// });
+ /// assert!(module.contains_fn(hash));
+ /// ```
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ #[inline]
+ pub fn set_indexer_set_fn<A, B, const C: bool, T, F>(&mut self, func: F) -> u64
+ where
+ A: Variant + Clone,
+ B: Variant + Clone,
+ T: Variant + Clone,
+ F: RegisterNativeFunction<(Mut<A>, B, T), 3, C, (), true> + SendSync + 'static,
+ {
+ #[cfg(not(feature = "no_index"))]
+ if TypeId::of::<A>() == TypeId::of::<crate::Array>() {
+ panic!("Cannot register indexer for arrays.");
+ }
+ #[cfg(not(feature = "no_object"))]
+ if TypeId::of::<A>() == TypeId::of::<crate::Map>() {
+ panic!("Cannot register indexer for object maps.");
+ }
+ if TypeId::of::<A>() == TypeId::of::<String>()
+ || TypeId::of::<A>() == TypeId::of::<&str>()
+ || TypeId::of::<A>() == TypeId::of::<ImmutableString>()
+ {
+ panic!("Cannot register indexer for strings.");
+ }
+
+ self.set_fn(
+ crate::engine::FN_IDX_SET,
+ FnNamespace::Global,
+ FnAccess::Public,
+ None,
+ F::param_types(),
+ func.into_callable_function(crate::engine::FN_IDX_SET.into(), false),
+ )
+ }
+
+ /// Set a pair of Rust index getter and setter functions into the [`Module`], returning both [`u64`] hash keys.
+ /// This is a short-hand for [`set_indexer_get_fn`][Module::set_indexer_get_fn] and
+ /// [`set_indexer_set_fn`][Module::set_indexer_set_fn].
+ ///
+ /// These functions are automatically exposed to the global namespace.
+ ///
+ /// If there are similar existing Rust functions, they are replaced.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the type is [`Array`][crate::Array] or [`Map`][crate::Map].
+ /// Indexers for arrays, object maps and strings cannot be registered.
+ ///
+ /// # Function Metadata
+ ///
+ /// No metadata for the function is registered.
+ /// Use [`update_fn_metadata`][Module::update_fn_metadata] to add metadata.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::{Module, ImmutableString};
+ ///
+ /// let mut module = Module::new();
+ /// let (hash_get, hash_set) = module.set_indexer_get_set_fn(
+ /// |x: &mut i64, y: ImmutableString| {
+ /// Ok(*x + y.len() as i64)
+ /// },
+ /// |x: &mut i64, y: ImmutableString, value: i64| {
+ /// *x = y.len() as i64 + value; Ok(())
+ /// }
+ /// );
+ /// assert!(module.contains_fn(hash_get));
+ /// assert!(module.contains_fn(hash_set));
+ /// ```
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ #[inline(always)]
+ pub fn set_indexer_get_set_fn<
+ A: Variant + Clone,
+ B: Variant + Clone,
+ const C1: bool,
+ const C2: bool,
+ T: Variant + Clone,
+ >(
+ &mut self,
+ get_fn: impl RegisterNativeFunction<(Mut<A>, B), 2, C1, T, true> + SendSync + 'static,
+ set_fn: impl RegisterNativeFunction<(Mut<A>, B, T), 3, C2, (), true> + SendSync + 'static,
+ ) -> (u64, u64) {
+ (
+ self.set_indexer_get_fn(get_fn),
+ self.set_indexer_set_fn(set_fn),
+ )
+ }
+
+ /// Look up a native Rust function by hash.
+ ///
+ /// The [`u64`] hash is returned by the [`set_native_fn`][Module::set_native_fn] call.
+ #[inline]
+ #[must_use]
+ pub(crate) fn get_fn(&self, hash_native: u64) -> Option<&CallableFunction> {
+ self.functions
+ .as_ref()
+ .and_then(|m| m.get(&hash_native))
+ .map(|f| &f.func)
+ }
+
+ /// Can the particular function with [`Dynamic`] parameter(s) exist in the [`Module`]?
+ ///
+ /// A `true` return value does not automatically imply that the function _must_ exist.
+ #[inline]
+ #[must_use]
+ pub(crate) fn may_contain_dynamic_fn(&self, hash_script: u64) -> bool {
+ !self
+ .dynamic_functions_filter
+ .as_ref()
+ .map_or(false, |f| f.is_absent(hash_script))
+ }
+
+ /// Does the particular namespace-qualified function exist in the [`Module`]?
+ ///
+ /// The [`u64`] hash is calculated by [`build_index`][Module::build_index].
+ #[inline(always)]
+ #[must_use]
+ pub fn contains_qualified_fn(&self, hash_fn: u64) -> bool {
+ self.all_functions
+ .as_ref()
+ .map_or(false, |m| m.contains_key(&hash_fn))
+ }
+
+ /// Get a namespace-qualified function.
+ ///
+ /// The [`u64`] hash is calculated by [`build_index`][Module::build_index].
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ #[must_use]
+ pub(crate) fn get_qualified_fn(&self, hash_qualified_fn: u64) -> Option<&CallableFunction> {
+ self.all_functions
+ .as_ref()
+ .and_then(|m| m.get(&hash_qualified_fn))
+ }
+
+ /// Combine another [`Module`] into this [`Module`].
+ /// The other [`Module`] is _consumed_ to merge into this [`Module`].
+ #[inline]
+ pub fn combine(&mut self, other: Self) -> &mut Self {
+ match self.modules {
+ Some(ref mut m) if other.modules.is_some() => {
+ m.extend(other.modules.unwrap().into_iter())
+ }
+ Some(_) => (),
+ None => self.modules = other.modules,
+ }
+ match self.variables {
+ Some(ref mut m) if other.variables.is_some() => {
+ m.extend(other.variables.unwrap().into_iter())
+ }
+ Some(_) => (),
+ None => self.variables = other.variables,
+ }
+ match self.functions {
+ Some(ref mut m) if other.functions.is_some() => {
+ m.extend(other.functions.unwrap().into_iter())
+ }
+ Some(_) => (),
+ None => self.functions = other.functions,
+ }
+ match self.dynamic_functions_filter {
+ Some(ref mut m) if other.dynamic_functions_filter.is_some() => {
+ **m += &**other.dynamic_functions_filter.as_ref().unwrap()
+ }
+ Some(_) => (),
+ None => self.dynamic_functions_filter = other.dynamic_functions_filter,
+ }
+ match self.type_iterators {
+ Some(ref mut m) if other.type_iterators.is_some() => {
+ m.extend(other.type_iterators.unwrap().into_iter())
+ }
+ Some(_) => (),
+ None => self.type_iterators = other.type_iterators,
+ }
+ self.all_functions = None;
+ self.all_variables = None;
+ self.all_type_iterators = None;
+ self.flags
+ .remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS);
+
+ #[cfg(feature = "metadata")]
+ if !other.doc.as_deref().map_or(true, SmartString::is_empty) {
+ if !self.doc.as_deref().map_or(true, SmartString::is_empty) {
+ self.doc.get_or_insert_with(Default::default).push('\n');
+ }
+ self.doc
+ .get_or_insert_with(Default::default)
+ .push_str(other.doc.as_ref().unwrap());
+ }
+
+ self
+ }
+
+ /// Combine another [`Module`] into this [`Module`].
+ /// The other [`Module`] is _consumed_ to merge into this [`Module`].
+ /// Sub-modules are flattened onto the root [`Module`], with higher level overriding lower level.
+ #[inline]
+ pub fn combine_flatten(&mut self, other: Self) -> &mut Self {
+ if let Some(modules) = other.modules {
+ for m in modules.into_values() {
+ self.combine_flatten(shared_take_or_clone(m));
+ }
+ }
+ match self.variables {
+ Some(ref mut m) if other.variables.is_some() => {
+ m.extend(other.variables.unwrap().into_iter())
+ }
+ Some(_) => (),
+ None => self.variables = other.variables,
+ }
+ match self.functions {
+ Some(ref mut m) if other.functions.is_some() => {
+ m.extend(other.functions.unwrap().into_iter())
+ }
+ Some(_) => (),
+ None => self.functions = other.functions,
+ }
+ match self.dynamic_functions_filter {
+ Some(ref mut m) if other.dynamic_functions_filter.is_some() => {
+ **m += &**other.dynamic_functions_filter.as_ref().unwrap()
+ }
+ Some(_) => (),
+ None => self.dynamic_functions_filter = other.dynamic_functions_filter,
+ }
+ match self.type_iterators {
+ Some(ref mut m) if other.type_iterators.is_some() => {
+ m.extend(other.type_iterators.unwrap().into_iter())
+ }
+ Some(_) => (),
+ None => self.type_iterators = other.type_iterators,
+ }
+ self.all_functions = None;
+ self.all_variables = None;
+ self.all_type_iterators = None;
+ self.flags
+ .remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS);
+
+ #[cfg(feature = "metadata")]
+ if !other.doc.as_deref().map_or(true, SmartString::is_empty) {
+ if !self.doc.as_deref().map_or(true, SmartString::is_empty) {
+ self.doc.get_or_insert_with(Default::default).push('\n');
+ }
+ self.doc
+ .get_or_insert_with(Default::default)
+ .push_str(other.doc.as_ref().unwrap());
+ }
+
+ self
+ }
+
+ /// Polyfill this [`Module`] with another [`Module`].
+ /// Only items not existing in this [`Module`] are added.
+ #[inline]
+ pub fn fill_with(&mut self, other: &Self) -> &mut Self {
+ if let Some(ref modules) = other.modules {
+ let m = self.modules.get_or_insert_with(Default::default);
+
+ for (k, v) in modules.iter() {
+ if !m.contains_key(k) {
+ m.insert(k.clone(), v.clone());
+ }
+ }
+ }
+ if let Some(ref variables) = other.variables {
+ for (k, v) in variables.iter() {
+ let map = self.variables.get_or_insert_with(Default::default);
+
+ if !map.contains_key(k) {
+ map.insert(k.clone(), v.clone());
+ }
+ }
+ }
+ if let Some(ref functions) = other.functions {
+ let others_len = functions.len();
+
+ for (&k, f) in functions.iter() {
+ let map = self
+ .functions
+ .get_or_insert_with(|| new_hash_map(FN_MAP_SIZE));
+ map.reserve(others_len);
+ map.entry(k).or_insert_with(|| f.clone());
+ }
+ }
+ match self.dynamic_functions_filter {
+ Some(ref mut m) if other.dynamic_functions_filter.is_some() => {
+ **m += &**other.dynamic_functions_filter.as_ref().unwrap()
+ }
+ Some(_) => (),
+ None => self.dynamic_functions_filter = other.dynamic_functions_filter.clone(),
+ }
+ if let Some(ref type_iterators) = other.type_iterators {
+ let t = self.type_iterators.get_or_insert_with(Default::default);
+
+ for (&k, v) in type_iterators.iter() {
+ t.entry(k).or_insert_with(|| v.clone());
+ }
+ }
+ self.all_functions = None;
+ self.all_variables = None;
+ self.all_type_iterators = None;
+ self.flags
+ .remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS);
+
+ #[cfg(feature = "metadata")]
+ if !other.doc.as_deref().map_or(true, SmartString::is_empty) {
+ if !self.doc.as_deref().map_or(true, SmartString::is_empty) {
+ self.doc.get_or_insert_with(Default::default).push('\n');
+ }
+ self.doc
+ .get_or_insert_with(Default::default)
+ .push_str(other.doc.as_ref().unwrap());
+ }
+
+ self
+ }
+
+ /// Merge another [`Module`] into this [`Module`].
+ #[inline(always)]
+ pub fn merge(&mut self, other: &Self) -> &mut Self {
+ self.merge_filtered(other, |_, _, _, _, _| true)
+ }
+
+ /// Merge another [`Module`] into this [`Module`] based on a filter predicate.
+ pub(crate) fn merge_filtered(
+ &mut self,
+ other: &Self,
+ _filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool + Copy,
+ ) -> &mut Self {
+ if let Some(ref modules) = other.modules {
+ for (k, v) in modules.iter() {
+ let mut m = Self::new();
+ m.merge_filtered(v, _filter);
+ self.set_sub_module(k.clone(), m);
+ }
+ #[cfg(feature = "no_function")]
+ if let Some(ref mut m) = self.modules {
+ m.extend(modules.iter().map(|(k, v)| (k.clone(), v.clone())));
+ } else {
+ self.modules = Some(modules.clone());
+ }
+ }
+
+ if let Some(ref variables) = other.variables {
+ match self.variables {
+ Some(ref mut m) => m.extend(variables.iter().map(|(k, v)| (k.clone(), v.clone()))),
+ None => self.variables = other.variables.clone(),
+ }
+ }
+ if let Some(ref functions) = other.functions {
+ match self.functions {
+ Some(ref mut m) => m.extend(
+ functions
+ .iter()
+ .filter(|&(.., f)| {
+ _filter(
+ f.metadata.namespace,
+ f.metadata.access,
+ f.func.is_script(),
+ f.metadata.name.as_str(),
+ f.metadata.num_params,
+ )
+ })
+ .map(|(&k, f)| (k, f.clone())),
+ ),
+ None => self.functions = other.functions.clone(),
+ }
+ }
+ match self.dynamic_functions_filter {
+ Some(ref mut m) if other.dynamic_functions_filter.is_some() => {
+ **m += &**other.dynamic_functions_filter.as_ref().unwrap()
+ }
+ Some(_) => (),
+ None => self.dynamic_functions_filter = other.dynamic_functions_filter.clone(),
+ }
+
+ if let Some(ref type_iterators) = other.type_iterators {
+ match self.type_iterators {
+ Some(ref mut t) => t.extend(type_iterators.iter().map(|(&k, v)| (k, v.clone()))),
+ None => self.type_iterators = other.type_iterators.clone(),
+ }
+ }
+ self.all_functions = None;
+ self.all_variables = None;
+ self.all_type_iterators = None;
+ self.flags
+ .remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS);
+
+ #[cfg(feature = "metadata")]
+ if !other.doc.as_deref().map_or(true, SmartString::is_empty) {
+ if !self.doc.as_deref().map_or(true, SmartString::is_empty) {
+ self.doc.get_or_insert_with(Default::default).push('\n');
+ }
+ self.doc
+ .get_or_insert_with(Default::default)
+ .push_str(other.doc.as_ref().unwrap());
+ }
+
+ self
+ }
+
+ /// Filter out the functions, retaining only some script-defined functions based on a filter predicate.
+ #[cfg(not(feature = "no_function"))]
+ #[inline]
+ pub(crate) fn retain_script_functions(
+ &mut self,
+ filter: impl Fn(FnNamespace, FnAccess, &str, usize) -> bool,
+ ) -> &mut Self {
+ self.functions = std::mem::take(&mut self.functions).map(|m| {
+ m.into_iter()
+ .filter(|(.., f)| {
+ if f.func.is_script() {
+ filter(
+ f.metadata.namespace,
+ f.metadata.access,
+ f.metadata.name.as_str(),
+ f.metadata.num_params,
+ )
+ } else {
+ false
+ }
+ })
+ .collect()
+ });
+
+ self.dynamic_functions_filter = None;
+ self.all_functions = None;
+ self.all_variables = None;
+ self.all_type_iterators = None;
+ self.flags
+ .remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS);
+ self
+ }
+
+ /// Get the number of variables, functions and type iterators in the [`Module`].
+ #[inline(always)]
+ #[must_use]
+ pub fn count(&self) -> (usize, usize, usize) {
+ (
+ self.variables.as_ref().map_or(0, BTreeMap::len),
+ self.functions.as_ref().map_or(0, StraightHashMap::len),
+ self.type_iterators.as_ref().map_or(0, BTreeMap::len),
+ )
+ }
+
+ /// Get an iterator to the sub-modules in the [`Module`].
+ #[inline(always)]
+ pub fn iter_sub_modules(&self) -> impl Iterator<Item = (&str, &SharedModule)> {
+ self.iter_sub_modules_raw().map(|(k, m)| (k.as_str(), m))
+ }
+ /// Get an iterator to the sub-modules in the [`Module`].
+ #[inline]
+ pub(crate) fn iter_sub_modules_raw(
+ &self,
+ ) -> impl Iterator<Item = (&Identifier, &SharedModule)> {
+ self.modules
+ .as_ref()
+ .into_iter()
+ .flatten()
+ .map(|(k, m)| (k, m))
+ }
+
+ /// Get an iterator to the variables in the [`Module`].
+ #[inline(always)]
+ pub fn iter_var(&self) -> impl Iterator<Item = (&str, &Dynamic)> {
+ self.iter_var_raw().map(|(k, v)| (k.as_str(), v))
+ }
+ /// Get an iterator to the variables in the [`Module`].
+ #[inline]
+ pub(crate) fn iter_var_raw(&self) -> impl Iterator<Item = (&Identifier, &Dynamic)> {
+ self.variables
+ .as_ref()
+ .into_iter()
+ .flatten()
+ .map(|(k, v)| (k, v))
+ }
+
+ /// Get an iterator to the functions in the [`Module`].
+ #[inline]
+ #[allow(dead_code)]
+ pub(crate) fn iter_fn(&self) -> impl Iterator<Item = &FuncInfo> {
+ self.functions.iter().flat_map(StraightHashMap::values)
+ }
+
+ /// Get an iterator over all script-defined functions in the [`Module`].
+ ///
+ /// Function metadata includes:
+ /// 1) Namespace ([`FnNamespace::Global`] or [`FnNamespace::Internal`]).
+ /// 2) Access mode ([`FnAccess::Public`] or [`FnAccess::Private`]).
+ /// 3) Function name (as string slice).
+ /// 4) Number of parameters.
+ /// 5) Shared reference to function definition [`ScriptFnDef`][crate::ast::ScriptFnDef].
+ #[cfg(not(feature = "no_function"))]
+ #[inline]
+ pub(crate) fn iter_script_fn(
+ &self,
+ ) -> impl Iterator<
+ Item = (
+ FnNamespace,
+ FnAccess,
+ &str,
+ usize,
+ &Shared<crate::ast::ScriptFnDef>,
+ ),
+ > + '_ {
+ self.iter_fn().filter(|&f| f.func.is_script()).map(|f| {
+ (
+ f.metadata.namespace,
+ f.metadata.access,
+ f.metadata.name.as_str(),
+ f.metadata.num_params,
+ f.func.get_script_fn_def().expect("script-defined function"),
+ )
+ })
+ }
+
+ /// Get an iterator over all script-defined functions in the [`Module`].
+ ///
+ /// Function metadata includes:
+ /// 1) Namespace ([`FnNamespace::Global`] or [`FnNamespace::Internal`]).
+ /// 2) Access mode ([`FnAccess::Public`] or [`FnAccess::Private`]).
+ /// 3) Function name (as string slice).
+ /// 4) Number of parameters.
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "internals"))]
+ #[inline]
+ pub fn iter_script_fn_info(
+ &self,
+ ) -> impl Iterator<Item = (FnNamespace, FnAccess, &str, usize)> {
+ self.iter_fn().filter(|&f| f.func.is_script()).map(|f| {
+ (
+ f.metadata.namespace,
+ f.metadata.access,
+ f.metadata.name.as_str(),
+ f.metadata.num_params,
+ )
+ })
+ }
+
+ /// _(internals)_ Get an iterator over all script-defined functions in the [`Module`].
+ /// Exported under the `internals` feature only.
+ ///
+ /// Function metadata includes:
+ /// 1) Namespace ([`FnNamespace::Global`] or [`FnNamespace::Internal`]).
+ /// 2) Access mode ([`FnAccess::Public`] or [`FnAccess::Private`]).
+ /// 3) Function name (as string slice).
+ /// 4) Number of parameters.
+ /// 5) _(internals)_ Shared reference to function definition [`ScriptFnDef`][crate::ast::ScriptFnDef].
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(feature = "internals")]
+ #[inline(always)]
+ pub fn iter_script_fn_info(
+ &self,
+ ) -> impl Iterator<
+ Item = (
+ FnNamespace,
+ FnAccess,
+ &str,
+ usize,
+ &Shared<crate::ast::ScriptFnDef>,
+ ),
+ > {
+ self.iter_script_fn()
+ }
+
+ /// Create a new [`Module`] by evaluating an [`AST`][crate::AST].
+ ///
+ /// The entire [`AST`][crate::AST] is encapsulated into each function, allowing functions to
+ /// cross-call each other.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// use rhai::{Engine, Module, Scope};
+ ///
+ /// let engine = Engine::new();
+ /// let ast = engine.compile("let answer = 42; export answer;")?;
+ /// let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
+ /// assert!(module.contains_var("answer"));
+ /// assert_eq!(module.get_var_value::<i64>("answer").expect("answer should exist"), 42);
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[cfg(not(feature = "no_module"))]
+ #[inline(always)]
+ pub fn eval_ast_as_new(
+ scope: crate::Scope,
+ ast: &crate::AST,
+ engine: &crate::Engine,
+ ) -> RhaiResultOf<Self> {
+ let mut scope = scope;
+ let global = &mut crate::eval::GlobalRuntimeState::new(engine);
+
+ Self::eval_ast_as_new_raw(engine, &mut scope, global, ast)
+ }
+ /// Create a new [`Module`] by evaluating an [`AST`][crate::AST].
+ ///
+ /// The entire [`AST`][crate::AST] is encapsulated into each function, allowing functions to
+ /// cross-call each other.
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is very low level.
+ ///
+ /// In particular, the [`global`][crate::GlobalRuntimeState] parameter allows the entire
+ /// calling environment to be encapsulated, including automatic global constants.
+ #[cfg(not(feature = "no_module"))]
+ pub fn eval_ast_as_new_raw(
+ engine: &crate::Engine,
+ scope: &mut crate::Scope,
+ global: &mut crate::eval::GlobalRuntimeState,
+ ast: &crate::AST,
+ ) -> RhaiResultOf<Self> {
+ // Save global state
+ let orig_scope_len = scope.len();
+ let orig_imports_len = global.num_imports();
+ let orig_source = global.source.clone();
+
+ #[cfg(not(feature = "no_function"))]
+ let orig_lib_len = global.lib.len();
+
+ #[cfg(not(feature = "no_function"))]
+ let orig_constants = std::mem::take(&mut global.constants);
+
+ // Run the script
+ let caches = &mut crate::eval::Caches::new();
+
+ let result = engine.eval_ast_with_scope_raw(global, caches, scope, ast);
+
+ // Create new module
+ let mut module = Module::new();
+
+ // Extra modules left become sub-modules
+ let mut imports = crate::StaticVec::new_const();
+
+ if result.is_ok() {
+ global
+ .scan_imports_raw()
+ .skip(orig_imports_len)
+ .for_each(|(k, m)| {
+ imports.push((k.clone(), m.clone()));
+ module.set_sub_module(k.clone(), m.clone());
+ });
+ }
+
+ // Restore global state
+ #[cfg(not(feature = "no_function"))]
+ let constants = std::mem::replace(&mut global.constants, orig_constants);
+
+ global.truncate_imports(orig_imports_len);
+
+ #[cfg(not(feature = "no_function"))]
+ global.lib.truncate(orig_lib_len);
+
+ global.source = orig_source;
+
+ // The return value is thrown away and not used
+ let _ = result?;
+
+ // Encapsulated environment
+ let environ = Shared::new(crate::func::EncapsulatedEnviron {
+ #[cfg(not(feature = "no_function"))]
+ lib: ast.shared_lib().clone(),
+ imports: imports.into(),
+ #[cfg(not(feature = "no_function"))]
+ constants,
+ });
+
+ // Variables with an alias left in the scope become module variables
+ let mut i = scope.len();
+ while i > 0 {
+ i -= 1;
+
+ let (mut value, mut aliases) = if i >= orig_scope_len {
+ let (_, v, a) = scope.pop_entry().expect("not empty");
+ (v, a)
+ } else {
+ let (_, v, a) = scope.get_entry_by_index(i);
+ (v.clone(), a.to_vec())
+ };
+
+ value.deep_scan(|v| {
+ if let Some(fn_ptr) = v.downcast_mut::<crate::FnPtr>() {
+ fn_ptr.set_encapsulated_environ(Some(environ.clone()));
+ }
+ });
+
+ match aliases.len() {
+ 0 => (),
+ 1 => {
+ let alias = aliases.pop().unwrap();
+ if !module.contains_var(&alias) {
+ module.set_var(alias, value);
+ }
+ }
+ _ => {
+ // Avoid cloning the last value
+ let mut first_alias = None;
+
+ for alias in aliases {
+ if module.contains_var(&alias) {
+ continue;
+ }
+ if first_alias.is_none() {
+ first_alias = Some(alias);
+ } else {
+ module.set_var(alias, value.clone());
+ }
+ }
+
+ if let Some(alias) = first_alias {
+ module.set_var(alias, value);
+ }
+ }
+ }
+ }
+
+ // Non-private functions defined become module functions
+ #[cfg(not(feature = "no_function"))]
+ ast.iter_fn_def()
+ .filter(|&f| match f.access {
+ FnAccess::Public => true,
+ FnAccess::Private => false,
+ })
+ .for_each(|f| {
+ let hash = module.set_script_fn(f.clone());
+ let f = module.functions.as_mut().unwrap().get_mut(&hash).unwrap();
+
+ // Encapsulate AST environment
+ if let CallableFunction::Script {
+ environ: ref mut e, ..
+ } = f.func
+ {
+ *e = Some(environ.clone());
+ }
+ });
+
+ module.id = ast.source_raw().cloned();
+
+ #[cfg(feature = "metadata")]
+ module.set_doc(ast.doc());
+
+ module.build_index();
+
+ Ok(module)
+ }
+
+ /// Does the [`Module`] contain indexed functions that have been exposed to the global namespace?
+ ///
+ /// # Panics
+ ///
+ /// Panics if the [`Module`] is not yet indexed via [`build_index`][Module::build_index].
+ #[inline(always)]
+ #[must_use]
+ pub fn contains_indexed_global_functions(&self) -> bool {
+ self.flags.contains(ModuleFlags::INDEXED_GLOBAL_FUNCTIONS)
+ }
+
+ /// Scan through all the sub-modules in the [`Module`] and build a hash index of all
+ /// variables and functions as one flattened namespace.
+ ///
+ /// If the [`Module`] is already indexed, this method has no effect.
+ pub fn build_index(&mut self) -> &mut Self {
+ // Collect a particular module.
+ fn index_module<'a>(
+ module: &'a Module,
+ path: &mut Vec<&'a str>,
+ variables: &mut StraightHashMap<Dynamic>,
+ functions: &mut StraightHashMap<CallableFunction>,
+ type_iterators: &mut BTreeMap<TypeId, Shared<IteratorFn>>,
+ ) -> bool {
+ let mut contains_indexed_global_functions = false;
+
+ if let Some(ref modules) = module.modules {
+ for (name, m) in modules.iter() {
+ // Index all the sub-modules first.
+ path.push(name);
+ if index_module(m, path, variables, functions, type_iterators) {
+ contains_indexed_global_functions = true;
+ }
+ path.pop();
+ }
+ }
+
+ // Index all variables
+ if let Some(ref v) = module.variables {
+ for (var_name, value) in v.iter() {
+ let hash_var = crate::calc_var_hash(path.iter().copied(), var_name);
+
+ // Catch hash collisions in testing environment only.
+ #[cfg(feature = "testing-environ")]
+ if let Some(_) = variables.get(&hash_var) {
+ panic!(
+ "Hash {} already exists when indexing variable {}",
+ hash_var, var_name
+ );
+ }
+
+ variables.insert(hash_var, value.clone());
+ }
+ }
+
+ // Index all type iterators
+ if let Some(ref t) = module.type_iterators {
+ for (&type_id, func) in t.iter() {
+ type_iterators.insert(type_id, func.clone());
+ }
+ }
+
+ // Index all functions
+ for (&hash, f) in module.functions.iter().flatten() {
+ match f.metadata.namespace {
+ FnNamespace::Global => {
+ // Catch hash collisions in testing environment only.
+ #[cfg(feature = "testing-environ")]
+ if let Some(fx) = functions.get(&hash) {
+ panic!(
+ "Hash {} already exists when indexing function {:#?}:\n{:#?}",
+ hash, f.func, fx
+ );
+ }
+
+ // Flatten all functions with global namespace
+ functions.insert(hash, f.func.clone());
+ contains_indexed_global_functions = true;
+ }
+ FnNamespace::Internal => (),
+ }
+ match f.metadata.access {
+ FnAccess::Public => (),
+ FnAccess::Private => continue, // Do not index private functions
+ }
+
+ if !f.func.is_script() {
+ let hash_qualified_fn = calc_native_fn_hash(
+ path.iter().copied(),
+ f.metadata.name.as_str(),
+ &f.metadata.param_types,
+ );
+
+ // Catch hash collisions in testing environment only.
+ #[cfg(feature = "testing-environ")]
+ if let Some(fx) = functions.get(&hash_qualified_fn) {
+ panic!(
+ "Hash {} already exists when indexing function {:#?}:\n{:#?}",
+ hash_qualified_fn, f.func, fx
+ );
+ }
+
+ functions.insert(hash_qualified_fn, f.func.clone());
+ } else {
+ #[cfg(not(feature = "no_function"))]
+ {
+ let hash_qualified_script = crate::calc_fn_hash(
+ path.iter().copied(),
+ &f.metadata.name,
+ f.metadata.num_params,
+ );
+ #[cfg(not(feature = "no_object"))]
+ let hash_qualified_script =
+ if let Some(ref this_type) = f.metadata.this_type {
+ crate::calc_typed_method_hash(hash_qualified_script, this_type)
+ } else {
+ hash_qualified_script
+ };
+
+ // Catch hash collisions in testing environment only.
+ #[cfg(feature = "testing-environ")]
+ if let Some(fx) = functions.get(&hash_qualified_script) {
+ panic!(
+ "Hash {} already exists when indexing function {:#?}:\n{:#?}",
+ hash_qualified_script, f.func, fx
+ );
+ }
+
+ functions.insert(hash_qualified_script, f.func.clone());
+ }
+ }
+ }
+
+ contains_indexed_global_functions
+ }
+
+ if !self.is_indexed() {
+ let mut path = Vec::with_capacity(4);
+ let mut variables = new_hash_map(self.variables.as_ref().map_or(0, BTreeMap::len));
+ let mut functions =
+ new_hash_map(self.functions.as_ref().map_or(0, StraightHashMap::len));
+ let mut type_iterators = BTreeMap::new();
+
+ path.push("");
+
+ let has_global_functions = index_module(
+ self,
+ &mut path,
+ &mut variables,
+ &mut functions,
+ &mut type_iterators,
+ );
+
+ self.flags
+ .set(ModuleFlags::INDEXED_GLOBAL_FUNCTIONS, has_global_functions);
+
+ self.all_variables = (!variables.is_empty()).then(|| variables.into());
+ self.all_functions = (!functions.is_empty()).then(|| functions.into());
+ self.all_type_iterators = (!type_iterators.is_empty()).then(|| type_iterators.into());
+
+ self.flags |= ModuleFlags::INDEXED;
+ }
+
+ self
+ }
+
+ /// Does a type iterator exist in the entire module tree?
+ #[inline(always)]
+ #[must_use]
+ pub fn contains_qualified_iter(&self, id: TypeId) -> bool {
+ self.all_type_iterators
+ .as_ref()
+ .map_or(false, |t| t.contains_key(&id))
+ }
+
+ /// Does a type iterator exist in the module?
+ #[inline(always)]
+ #[must_use]
+ pub fn contains_iter(&self, id: TypeId) -> bool {
+ self.type_iterators
+ .as_ref()
+ .map_or(false, |t| t.contains_key(&id))
+ }
+
+ /// Set a type iterator into the [`Module`].
+ #[inline(always)]
+ pub fn set_iter(
+ &mut self,
+ type_id: TypeId,
+ func: impl Fn(Dynamic) -> Box<dyn Iterator<Item = Dynamic>> + SendSync + 'static,
+ ) -> &mut Self {
+ self.set_iter_result(type_id, move |x| {
+ Box::new(func(x).map(Ok)) as Box<dyn Iterator<Item = RhaiResultOf<Dynamic>>>
+ })
+ }
+
+ /// Set a fallible type iterator into the [`Module`].
+ #[inline]
+ pub fn set_iter_result(
+ &mut self,
+ type_id: TypeId,
+ func: impl Fn(Dynamic) -> Box<dyn Iterator<Item = RhaiResultOf<Dynamic>>> + SendSync + 'static,
+ ) -> &mut Self {
+ let func = Shared::new(func);
+ if self.is_indexed() {
+ self.all_type_iterators
+ .get_or_insert_with(Default::default)
+ .insert(type_id, func.clone());
+ }
+ self.type_iterators
+ .get_or_insert_with(Default::default)
+ .insert(type_id, func);
+ self
+ }
+
+ /// Set a type iterator into the [`Module`].
+ #[inline(always)]
+ pub fn set_iterable<T>(&mut self) -> &mut Self
+ where
+ T: Variant + Clone + IntoIterator,
+ <T as IntoIterator>::Item: Variant + Clone,
+ {
+ self.set_iter(TypeId::of::<T>(), |obj: Dynamic| {
+ Box::new(obj.cast::<T>().into_iter().map(Dynamic::from))
+ })
+ }
+
+ /// Set a fallible type iterator into the [`Module`].
+ #[inline(always)]
+ pub fn set_iterable_result<T, X>(&mut self) -> &mut Self
+ where
+ T: Variant + Clone + IntoIterator<Item = RhaiResultOf<X>>,
+ X: Variant + Clone,
+ {
+ self.set_iter_result(TypeId::of::<T>(), |obj: Dynamic| {
+ Box::new(obj.cast::<T>().into_iter().map(|v| v.map(Dynamic::from)))
+ })
+ }
+
+ /// Set an iterator type into the [`Module`] as a type iterator.
+ #[inline(always)]
+ pub fn set_iterator<T>(&mut self) -> &mut Self
+ where
+ T: Variant + Clone + Iterator,
+ <T as Iterator>::Item: Variant + Clone,
+ {
+ self.set_iter(TypeId::of::<T>(), |obj: Dynamic| {
+ Box::new(obj.cast::<T>().map(Dynamic::from))
+ })
+ }
+
+ /// Set a iterator type into the [`Module`] as a fallible type iterator.
+ #[inline(always)]
+ pub fn set_iterator_result<T, X>(&mut self) -> &mut Self
+ where
+ T: Variant + Clone + Iterator<Item = RhaiResultOf<X>>,
+ X: Variant + Clone,
+ {
+ self.set_iter_result(TypeId::of::<T>(), |obj: Dynamic| {
+ Box::new(obj.cast::<T>().map(|v| v.map(Dynamic::from)))
+ })
+ }
+
+ /// Get the specified type iterator.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ #[must_use]
+ pub(crate) fn get_qualified_iter(&self, id: TypeId) -> Option<&IteratorFn> {
+ self.all_type_iterators
+ .as_ref()
+ .and_then(|t| t.get(&id))
+ .map(|f| &**f)
+ }
+
+ /// Get the specified type iterator.
+ #[inline]
+ #[must_use]
+ pub(crate) fn get_iter(&self, id: TypeId) -> Option<&IteratorFn> {
+ self.type_iterators
+ .as_ref()
+ .and_then(|t| t.get(&id))
+ .map(|f| &**f)
+ }
+}
+
+/// Module containing all built-in [module resolvers][ModuleResolver].
+#[cfg(not(feature = "no_module"))]
+pub mod resolvers;
+
+#[cfg(not(feature = "no_module"))]
+pub use resolvers::ModuleResolver;
diff --git a/rhai/src/module/resolvers/collection.rs b/rhai/src/module/resolvers/collection.rs
new file mode 100644
index 0000000..4ca3af6
--- /dev/null
+++ b/rhai/src/module/resolvers/collection.rs
@@ -0,0 +1,162 @@
+use crate::{
+ Engine, ModuleResolver, Position, RhaiResultOf, SharedModule, StaticVec, ERR,
+ STATIC_VEC_INLINE_SIZE,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{ops::AddAssign, slice::Iter};
+
+/// [Module][crate::Module] resolution service that holds a collection of module resolvers,
+/// to be searched in sequential order.
+///
+/// # Example
+///
+/// ```
+/// use rhai::{Engine, Module};
+/// use rhai::module_resolvers::{StaticModuleResolver, ModuleResolversCollection};
+///
+/// let mut collection = ModuleResolversCollection::new();
+///
+/// let resolver = StaticModuleResolver::new();
+/// collection.push(resolver);
+///
+/// let mut engine = Engine::new();
+/// engine.set_module_resolver(collection);
+/// ```
+#[derive(Default)]
+pub struct ModuleResolversCollection(StaticVec<Box<dyn ModuleResolver>>);
+
+impl ModuleResolversCollection {
+ /// Create a new [`ModuleResolversCollection`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::{Engine, Module};
+ /// use rhai::module_resolvers::{StaticModuleResolver, ModuleResolversCollection};
+ ///
+ /// let mut collection = ModuleResolversCollection::new();
+ ///
+ /// let resolver = StaticModuleResolver::new();
+ /// collection.push(resolver);
+ ///
+ /// let mut engine = Engine::new();
+ /// engine.set_module_resolver(collection);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub const fn new() -> Self {
+ Self(StaticVec::new_const())
+ }
+ /// Append a [module resolver][ModuleResolver] to the end.
+ #[inline(always)]
+ pub fn push(&mut self, resolver: impl ModuleResolver + 'static) -> &mut Self {
+ self.0.push(Box::new(resolver));
+ self
+ }
+ /// Insert a [module resolver][ModuleResolver] to an offset index.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the index is out of bounds.
+ #[inline(always)]
+ pub fn insert(&mut self, index: usize, resolver: impl ModuleResolver + 'static) -> &mut Self {
+ self.0.insert(index, Box::new(resolver));
+ self
+ }
+ /// Remove the last [module resolver][ModuleResolver] from the end, if any.
+ #[inline(always)]
+ pub fn pop(&mut self) -> Option<Box<dyn ModuleResolver>> {
+ self.0.pop()
+ }
+ /// Remove a [module resolver][ModuleResolver] at an offset index.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the index is out of bounds.
+ #[inline(always)]
+ pub fn remove(&mut self, index: usize) -> Box<dyn ModuleResolver> {
+ self.0.remove(index)
+ }
+ /// Get an iterator of all the [module resolvers][ModuleResolver].
+ #[inline]
+ pub fn iter(&self) -> impl Iterator<Item = &dyn ModuleResolver> {
+ self.0.iter().map(<_>::as_ref)
+ }
+ /// Remove all [module resolvers][ModuleResolver].
+ #[inline(always)]
+ pub fn clear(&mut self) -> &mut Self {
+ self.0.clear();
+ self
+ }
+ /// Returns `true` if this [`ModuleResolversCollection`] contains no module resolvers.
+ #[inline(always)]
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+ /// Get the number of [module resolvers][ModuleResolver] in this [`ModuleResolversCollection`].
+ #[inline(always)]
+ #[must_use]
+ pub fn len(&self) -> usize {
+ self.0.len()
+ }
+ /// Add another [`ModuleResolversCollection`] to the end of this collection.
+ /// The other [`ModuleResolversCollection`] is consumed.
+ #[inline]
+ pub fn append(&mut self, other: Self) -> &mut Self {
+ self.0.extend(other.0.into_iter());
+ self
+ }
+}
+
+impl IntoIterator for ModuleResolversCollection {
+ type Item = Box<dyn ModuleResolver>;
+ type IntoIter = smallvec::IntoIter<[Box<dyn ModuleResolver>; STATIC_VEC_INLINE_SIZE]>;
+
+ #[inline(always)]
+ #[must_use]
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.into_iter()
+ }
+}
+
+impl<'a> IntoIterator for &'a ModuleResolversCollection {
+ type Item = &'a Box<dyn ModuleResolver>;
+ type IntoIter = Iter<'a, Box<dyn ModuleResolver>>;
+
+ #[inline(always)]
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.iter()
+ }
+}
+
+impl ModuleResolver for ModuleResolversCollection {
+ fn resolve(
+ &self,
+ engine: &Engine,
+ source_path: Option<&str>,
+ path: &str,
+ pos: Position,
+ ) -> RhaiResultOf<SharedModule> {
+ for resolver in &self.0 {
+ match resolver.resolve(engine, source_path, path, pos) {
+ Ok(module) => return Ok(module),
+ Err(err) => match *err {
+ ERR::ErrorModuleNotFound(..) => continue,
+ ERR::ErrorInModule(_, err, _) => return Err(err),
+ _ => panic!("ModuleResolver::resolve returns error that is not ErrorModuleNotFound or ErrorInModule"),
+ },
+ }
+ }
+
+ Err(ERR::ErrorModuleNotFound(path.into(), pos).into())
+ }
+}
+
+impl<M: ModuleResolver + 'static> AddAssign<M> for ModuleResolversCollection {
+ #[inline(always)]
+ fn add_assign(&mut self, rhs: M) {
+ self.push(rhs);
+ }
+}
diff --git a/rhai/src/module/resolvers/dummy.rs b/rhai/src/module/resolvers/dummy.rs
new file mode 100644
index 0000000..dab31c4
--- /dev/null
+++ b/rhai/src/module/resolvers/dummy.rs
@@ -0,0 +1,51 @@
+use crate::{Engine, ModuleResolver, Position, RhaiResultOf, SharedModule, ERR};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// Empty/disabled [module][crate::Module] resolution service that acts as a dummy.
+///
+/// # Example
+///
+/// ```
+/// use rhai::{Engine, Module};
+/// use rhai::module_resolvers::DummyModuleResolver;
+///
+/// let resolver = DummyModuleResolver::new();
+/// let mut engine = Engine::new();
+/// engine.set_module_resolver(resolver);
+/// ```
+#[derive(Debug, Copy, Eq, PartialEq, Clone, Default, Hash)]
+pub struct DummyModuleResolver;
+
+impl DummyModuleResolver {
+ /// Create a new [`DummyModuleResolver`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::{Engine, Module};
+ /// use rhai::module_resolvers::DummyModuleResolver;
+ ///
+ /// let resolver = DummyModuleResolver::new();
+ /// let mut engine = Engine::new();
+ /// engine.set_module_resolver(resolver);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub const fn new() -> Self {
+ Self
+ }
+}
+
+impl ModuleResolver for DummyModuleResolver {
+ #[inline(always)]
+ fn resolve(
+ &self,
+ _: &Engine,
+ _: Option<&str>,
+ path: &str,
+ pos: Position,
+ ) -> RhaiResultOf<SharedModule> {
+ Err(ERR::ErrorModuleNotFound(path.into(), pos).into())
+ }
+}
diff --git a/rhai/src/module/resolvers/file.rs b/rhai/src/module/resolvers/file.rs
new file mode 100644
index 0000000..4ac9a47
--- /dev/null
+++ b/rhai/src/module/resolvers/file.rs
@@ -0,0 +1,383 @@
+#![cfg(not(feature = "no_std"))]
+#![cfg(not(target_family = "wasm"))]
+
+use crate::eval::GlobalRuntimeState;
+use crate::func::{locked_read, locked_write};
+use crate::{
+ Engine, Identifier, Locked, Module, ModuleResolver, Position, RhaiResultOf, Scope, Shared,
+ SharedModule, ERR,
+};
+
+use std::{
+ collections::BTreeMap,
+ io::Error as IoError,
+ path::{Path, PathBuf},
+};
+
+pub const RHAI_SCRIPT_EXTENSION: &str = "rhai";
+
+/// A [module][Module] resolution service that loads [module][Module] script files from the file system.
+///
+/// ## Caching
+///
+/// Resolved [Modules][Module] are cached internally so script files are not reloaded and recompiled
+/// for subsequent requests.
+///
+/// Use [`clear_cache`][FileModuleResolver::clear_cache] or
+/// [`clear_cache_for_path`][FileModuleResolver::clear_cache_for_path] to clear the internal cache.
+///
+/// ## Namespace
+///
+/// When a function within a script file module is called, all functions defined within the same
+/// script are available, evan `private` ones. In other words, functions defined in a module script
+/// can always cross-call each other.
+///
+/// # Example
+///
+/// ```
+/// use rhai::Engine;
+/// use rhai::module_resolvers::FileModuleResolver;
+///
+/// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory
+/// // with file extension '.x'.
+/// let resolver = FileModuleResolver::new_with_path_and_extension("./scripts", "x");
+///
+/// let mut engine = Engine::new();
+///
+/// engine.set_module_resolver(resolver);
+/// ```
+#[derive(Debug)]
+pub struct FileModuleResolver {
+ base_path: Option<PathBuf>,
+ extension: Identifier,
+ cache_enabled: bool,
+ scope: Scope<'static>,
+ cache: Locked<BTreeMap<PathBuf, SharedModule>>,
+}
+
+impl Default for FileModuleResolver {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl FileModuleResolver {
+ /// Create a new [`FileModuleResolver`] with the current directory as base path.
+ ///
+ /// The default extension is `.rhai`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Engine;
+ /// use rhai::module_resolvers::FileModuleResolver;
+ ///
+ /// // Create a new 'FileModuleResolver' loading scripts from the current directory
+ /// // with file extension '.rhai' (the default).
+ /// let resolver = FileModuleResolver::new();
+ ///
+ /// let mut engine = Engine::new();
+ /// engine.set_module_resolver(resolver);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn new() -> Self {
+ Self::new_with_extension(RHAI_SCRIPT_EXTENSION)
+ }
+
+ /// Create a new [`FileModuleResolver`] with a specific base path.
+ ///
+ /// The default extension is `.rhai`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Engine;
+ /// use rhai::module_resolvers::FileModuleResolver;
+ ///
+ /// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory
+ /// // with file extension '.rhai' (the default).
+ /// let resolver = FileModuleResolver::new_with_path("./scripts");
+ ///
+ /// let mut engine = Engine::new();
+ /// engine.set_module_resolver(resolver);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn new_with_path(path: impl Into<PathBuf>) -> Self {
+ Self::new_with_path_and_extension(path, RHAI_SCRIPT_EXTENSION)
+ }
+
+ /// Create a new [`FileModuleResolver`] with a file extension.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Engine;
+ /// use rhai::module_resolvers::FileModuleResolver;
+ ///
+ /// // Create a new 'FileModuleResolver' loading scripts with file extension '.rhai' (the default).
+ /// let resolver = FileModuleResolver::new_with_extension("rhai");
+ ///
+ /// let mut engine = Engine::new();
+ /// engine.set_module_resolver(resolver);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn new_with_extension(extension: impl Into<Identifier>) -> Self {
+ Self {
+ base_path: None,
+ extension: extension.into(),
+ cache_enabled: true,
+ cache: BTreeMap::new().into(),
+ scope: Scope::new(),
+ }
+ }
+
+ /// Create a new [`FileModuleResolver`] with a specific base path and file extension.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Engine;
+ /// use rhai::module_resolvers::FileModuleResolver;
+ ///
+ /// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory
+ /// // with file extension '.x'.
+ /// let resolver = FileModuleResolver::new_with_path_and_extension("./scripts", "x");
+ ///
+ /// let mut engine = Engine::new();
+ /// engine.set_module_resolver(resolver);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn new_with_path_and_extension(
+ path: impl Into<PathBuf>,
+ extension: impl Into<Identifier>,
+ ) -> Self {
+ Self {
+ base_path: Some(path.into()),
+ extension: extension.into(),
+ cache_enabled: true,
+ cache: BTreeMap::new().into(),
+ scope: Scope::new(),
+ }
+ }
+
+ /// Get the base path for script files.
+ #[inline(always)]
+ #[must_use]
+ pub fn base_path(&self) -> Option<&Path> {
+ self.base_path.as_deref()
+ }
+ /// Set the base path for script files.
+ #[inline(always)]
+ pub fn set_base_path(&mut self, path: impl Into<PathBuf>) -> &mut Self {
+ self.base_path = Some(path.into());
+ self
+ }
+
+ /// Get the script file extension.
+ #[inline(always)]
+ #[must_use]
+ pub fn extension(&self) -> &str {
+ &self.extension
+ }
+
+ /// Set the script file extension.
+ #[inline(always)]
+ pub fn set_extension(&mut self, extension: impl Into<Identifier>) -> &mut Self {
+ self.extension = extension.into();
+ self
+ }
+
+ /// Get a reference to the file module resolver's [scope][Scope].
+ ///
+ /// The [scope][Scope] is used for compiling module scripts.
+ #[inline(always)]
+ #[must_use]
+ pub const fn scope(&self) -> &Scope {
+ &self.scope
+ }
+
+ /// Set the file module resolver's [scope][Scope].
+ ///
+ /// The [scope][Scope] is used for compiling module scripts.
+ #[inline(always)]
+ pub fn set_scope(&mut self, scope: Scope<'static>) {
+ self.scope = scope;
+ }
+
+ /// Get a mutable reference to the file module resolver's [scope][Scope].
+ ///
+ /// The [scope][Scope] is used for compiling module scripts.
+ #[inline(always)]
+ #[must_use]
+ pub fn scope_mut(&mut self) -> &mut Scope<'static> {
+ &mut self.scope
+ }
+
+ /// Enable/disable the cache.
+ #[inline(always)]
+ pub fn enable_cache(&mut self, enable: bool) -> &mut Self {
+ self.cache_enabled = enable;
+ self
+ }
+ /// Is the cache enabled?
+ #[inline(always)]
+ #[must_use]
+ pub fn is_cache_enabled(&self) -> bool {
+ self.cache_enabled
+ }
+
+ /// Is a particular path cached?
+ #[inline]
+ #[must_use]
+ pub fn is_cached(&self, path: impl AsRef<Path>) -> bool {
+ if !self.cache_enabled {
+ return false;
+ }
+ locked_read(&self.cache).contains_key(path.as_ref())
+ }
+ /// Empty the internal cache.
+ #[inline]
+ pub fn clear_cache(&mut self) -> &mut Self {
+ locked_write(&self.cache).clear();
+ self
+ }
+ /// Remove the specified path from internal cache.
+ ///
+ /// The next time this path is resolved, the script file will be loaded once again.
+ #[inline]
+ #[must_use]
+ pub fn clear_cache_for_path(&mut self, path: impl AsRef<Path>) -> Option<SharedModule> {
+ locked_write(&self.cache)
+ .remove_entry(path.as_ref())
+ .map(|(.., v)| v)
+ }
+ /// Construct a full file path.
+ #[must_use]
+ pub fn get_file_path(&self, path: &str, source_path: Option<&Path>) -> PathBuf {
+ let path = Path::new(path);
+
+ let mut file_path;
+
+ if path.is_relative() {
+ file_path = self
+ .base_path
+ .clone()
+ .or_else(|| source_path.map(Into::into))
+ .unwrap_or_default();
+ file_path.push(path);
+ } else {
+ file_path = path.into();
+ }
+
+ file_path.set_extension(self.extension.as_str()); // Force extension
+ file_path
+ }
+
+ /// Resolve a module based on a path.
+ fn impl_resolve(
+ &self,
+ engine: &Engine,
+ global: &mut GlobalRuntimeState,
+ scope: &mut Scope,
+ source: Option<&str>,
+ path: &str,
+ pos: Position,
+ ) -> Result<SharedModule, Box<crate::EvalAltResult>> {
+ // Load relative paths from source if there is no base path specified
+ let source_path = global
+ .source()
+ .or(source)
+ .and_then(|p| Path::new(p).parent());
+
+ let file_path = self.get_file_path(path, source_path);
+
+ if self.is_cache_enabled() {
+ if let Some(module) = locked_read(&self.cache).get(&file_path) {
+ return Ok(module.clone());
+ }
+ }
+
+ let mut ast = engine
+ .compile_file_with_scope(&self.scope, file_path.clone())
+ .map_err(|err| match *err {
+ ERR::ErrorSystem(.., err) if err.is::<IoError>() => {
+ Box::new(ERR::ErrorModuleNotFound(path.to_string(), pos))
+ }
+ _ => Box::new(ERR::ErrorInModule(path.to_string(), err, pos)),
+ })?;
+
+ ast.set_source(path);
+
+ let m: Shared<_> = Module::eval_ast_as_new_raw(engine, scope, global, &ast)
+ .map_err(|err| Box::new(ERR::ErrorInModule(path.to_string(), err, pos)))?
+ .into();
+
+ if self.is_cache_enabled() {
+ locked_write(&self.cache).insert(file_path, m.clone());
+ }
+
+ Ok(m)
+ }
+}
+
+impl ModuleResolver for FileModuleResolver {
+ fn resolve_raw(
+ &self,
+ engine: &Engine,
+ global: &mut GlobalRuntimeState,
+ scope: &mut Scope,
+ path: &str,
+ pos: Position,
+ ) -> RhaiResultOf<SharedModule> {
+ self.impl_resolve(engine, global, scope, None, path, pos)
+ }
+
+ #[inline(always)]
+ fn resolve(
+ &self,
+ engine: &Engine,
+ source: Option<&str>,
+ path: &str,
+ pos: Position,
+ ) -> RhaiResultOf<SharedModule> {
+ let global = &mut GlobalRuntimeState::new(engine);
+ let scope = &mut Scope::new();
+ self.impl_resolve(engine, global, scope, source, path, pos)
+ }
+
+ /// Resolve an `AST` based on a path string.
+ ///
+ /// The file system is accessed during each call; the internal cache is by-passed.
+ fn resolve_ast(
+ &self,
+ engine: &Engine,
+ source_path: Option<&str>,
+ path: &str,
+ pos: Position,
+ ) -> Option<RhaiResultOf<crate::AST>> {
+ // Construct the script file path
+ let file_path = self.get_file_path(path, source_path.map(Path::new));
+
+ // Load the script file and compile it
+ Some(
+ engine
+ .compile_file(file_path)
+ .map(|mut ast| {
+ ast.set_source(path);
+ ast
+ })
+ .map_err(|err| match *err {
+ ERR::ErrorSystem(.., err) if err.is::<IoError>() => {
+ ERR::ErrorModuleNotFound(path.to_string(), pos).into()
+ }
+ _ => ERR::ErrorInModule(path.to_string(), err, pos).into(),
+ }),
+ )
+ }
+}
diff --git a/rhai/src/module/resolvers/mod.rs b/rhai/src/module/resolvers/mod.rs
new file mode 100644
index 0000000..7e70245
--- /dev/null
+++ b/rhai/src/module/resolvers/mod.rs
@@ -0,0 +1,67 @@
+use crate::eval::GlobalRuntimeState;
+use crate::func::SendSync;
+use crate::{Engine, Position, RhaiResultOf, Scope, SharedModule, AST};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+mod collection;
+mod dummy;
+mod file;
+mod stat;
+
+pub use collection::ModuleResolversCollection;
+pub use dummy::DummyModuleResolver;
+#[cfg(not(feature = "no_std"))]
+#[cfg(not(target_family = "wasm"))]
+pub use file::FileModuleResolver;
+pub use stat::StaticModuleResolver;
+
+/// Trait that encapsulates a module resolution service.
+pub trait ModuleResolver: SendSync {
+ /// Resolve a module based on a path string.
+ fn resolve(
+ &self,
+ engine: &Engine,
+ source: Option<&str>,
+ path: &str,
+ pos: Position,
+ ) -> RhaiResultOf<SharedModule>;
+
+ /// Resolve a module based on a path string, given a [`GlobalRuntimeState`] and the current [`Scope`].
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is very low level.
+ #[allow(unused_variables)]
+ fn resolve_raw(
+ &self,
+ engine: &Engine,
+ global: &mut GlobalRuntimeState,
+ scope: &mut Scope,
+ path: &str,
+ pos: Position,
+ ) -> RhaiResultOf<SharedModule> {
+ self.resolve(engine, global.source(), path, pos)
+ }
+
+ /// Resolve an `AST` based on a path string.
+ ///
+ /// Returns [`None`] (default) if such resolution is not supported
+ /// (e.g. if the module is Rust-based).
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// Override the default implementation of this method if the module resolver
+ /// serves modules based on compiled Rhai scripts.
+ #[allow(unused_variables)]
+ #[must_use]
+ fn resolve_ast(
+ &self,
+ engine: &Engine,
+ source: Option<&str>,
+ path: &str,
+ pos: Position,
+ ) -> Option<RhaiResultOf<AST>> {
+ None
+ }
+}
diff --git a/rhai/src/module/resolvers/stat.rs b/rhai/src/module/resolvers/stat.rs
new file mode 100644
index 0000000..0444958
--- /dev/null
+++ b/rhai/src/module/resolvers/stat.rs
@@ -0,0 +1,169 @@
+use crate::{
+ Engine, Identifier, Module, ModuleResolver, Position, RhaiResultOf, SharedModule, SmartString,
+ ERR,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ collections::btree_map::{IntoIter, Iter},
+ collections::BTreeMap,
+ ops::AddAssign,
+};
+
+/// A static [module][Module] resolution service that serves [modules][Module] added into it.
+///
+/// # Example
+///
+/// ```
+/// use rhai::{Engine, Module};
+/// use rhai::module_resolvers::StaticModuleResolver;
+///
+/// let mut resolver = StaticModuleResolver::new();
+///
+/// let module = Module::new();
+/// resolver.insert("hello", module);
+///
+/// let mut engine = Engine::new();
+///
+/// engine.set_module_resolver(resolver);
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct StaticModuleResolver(BTreeMap<Identifier, SharedModule>);
+
+impl StaticModuleResolver {
+ /// Create a new [`StaticModuleResolver`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::{Engine, Module};
+ /// use rhai::module_resolvers::StaticModuleResolver;
+ ///
+ /// let mut resolver = StaticModuleResolver::new();
+ ///
+ /// let module = Module::new();
+ /// resolver.insert("hello", module);
+ ///
+ /// let mut engine = Engine::new();
+ /// engine.set_module_resolver(resolver);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn new() -> Self {
+ Self(BTreeMap::new())
+ }
+ /// Add a [module][Module] keyed by its path.
+ #[inline]
+ pub fn insert(&mut self, path: impl Into<Identifier>, mut module: Module) {
+ let path = path.into();
+
+ if module.id().is_none() {
+ module.set_id(path.clone());
+ }
+
+ module.build_index();
+ self.0.insert(path, module.into());
+ }
+ /// Remove a [module][Module] given its path.
+ #[inline(always)]
+ pub fn remove(&mut self, path: &str) -> Option<SharedModule> {
+ self.0.remove(path)
+ }
+ /// Does the path exist?
+ #[inline(always)]
+ #[must_use]
+ pub fn contains_path(&self, path: &str) -> bool {
+ self.0.contains_key(path)
+ }
+ /// Get an iterator of all the [modules][Module].
+ #[inline]
+ pub fn iter(&self) -> impl Iterator<Item = (&str, &SharedModule)> {
+ self.0.iter().map(|(k, v)| (k.as_str(), v))
+ }
+ /// Get a mutable iterator of all the [modules][Module].
+ #[inline]
+ pub fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut SharedModule)> {
+ self.0.iter_mut().map(|(k, v)| (k.as_str(), v))
+ }
+ /// Get an iterator of all the [module][Module] paths.
+ #[inline]
+ pub fn paths(&self) -> impl Iterator<Item = &str> {
+ self.0.keys().map(SmartString::as_str)
+ }
+ /// Get an iterator of all the [modules][Module].
+ #[inline(always)]
+ pub fn values(&self) -> impl Iterator<Item = &SharedModule> {
+ self.0.values()
+ }
+ /// Remove all [modules][Module].
+ #[inline(always)]
+ pub fn clear(&mut self) -> &mut Self {
+ self.0.clear();
+ self
+ }
+ /// Returns `true` if this [`StaticModuleResolver`] contains no module resolvers.
+ #[inline(always)]
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+ /// Get the number of [modules][Module] in this [`StaticModuleResolver`].
+ #[inline(always)]
+ #[must_use]
+ pub fn len(&self) -> usize {
+ self.0.len()
+ }
+ /// Merge another [`StaticModuleResolver`] into this.
+ /// The other [`StaticModuleResolver`] is consumed.
+ ///
+ /// Existing modules of the same path name are overwritten.
+ #[inline]
+ pub fn merge(&mut self, other: Self) -> &mut Self {
+ self.0.extend(other.0.into_iter());
+ self
+ }
+}
+
+impl IntoIterator for StaticModuleResolver {
+ type Item = (Identifier, SharedModule);
+ type IntoIter = IntoIter<SmartString, SharedModule>;
+
+ #[inline(always)]
+ #[must_use]
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.into_iter()
+ }
+}
+
+impl<'a> IntoIterator for &'a StaticModuleResolver {
+ type Item = (&'a Identifier, &'a SharedModule);
+ type IntoIter = Iter<'a, SmartString, SharedModule>;
+
+ #[inline(always)]
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.iter()
+ }
+}
+
+impl ModuleResolver for StaticModuleResolver {
+ #[inline]
+ fn resolve(
+ &self,
+ _: &Engine,
+ _: Option<&str>,
+ path: &str,
+ pos: Position,
+ ) -> RhaiResultOf<SharedModule> {
+ self.0
+ .get(path)
+ .cloned()
+ .ok_or_else(|| ERR::ErrorModuleNotFound(path.into(), pos).into())
+ }
+}
+
+impl AddAssign<Self> for StaticModuleResolver {
+ #[inline(always)]
+ fn add_assign(&mut self, rhs: Self) {
+ self.merge(rhs);
+ }
+}
diff --git a/rhai/src/optimizer.rs b/rhai/src/optimizer.rs
new file mode 100644
index 0000000..9bfe523
--- /dev/null
+++ b/rhai/src/optimizer.rs
@@ -0,0 +1,1406 @@
+//! Module implementing the [`AST`] optimizer.
+#![cfg(not(feature = "no_optimize"))]
+
+use crate::ast::{
+ ASTFlags, Expr, FlowControl, OpAssignment, Stmt, StmtBlock, StmtBlockContainer,
+ SwitchCasesCollection,
+};
+use crate::engine::{
+ KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CURRY, KEYWORD_PRINT,
+ KEYWORD_TYPE_OF, OP_NOT,
+};
+use crate::eval::{Caches, GlobalRuntimeState};
+use crate::func::builtin::get_builtin_binary_op_fn;
+use crate::func::hashing::get_hasher;
+use crate::module::ModuleFlags;
+use crate::tokenizer::Token;
+use crate::types::scope::SCOPE_ENTRIES_INLINED;
+use crate::{
+ calc_fn_hash, calc_fn_hash_full, Dynamic, Engine, FnArgsVec, FnPtr, ImmutableString, Position,
+ Scope, AST,
+};
+use smallvec::SmallVec;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ any::TypeId,
+ convert::TryFrom,
+ hash::{Hash, Hasher},
+ mem,
+};
+
+/// Level of optimization performed.
+#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
+#[non_exhaustive]
+pub enum OptimizationLevel {
+ /// No optimization performed.
+ None,
+ /// Only perform simple optimizations without evaluating functions.
+ Simple,
+ /// Full optimizations performed, including evaluating functions.
+ /// Take care that this may cause side effects as it essentially assumes that all functions are pure.
+ Full,
+}
+
+impl Default for OptimizationLevel {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::Simple
+ }
+}
+
+/// Mutable state throughout an optimization pass.
+#[derive(Debug, Clone)]
+struct OptimizerState<'a> {
+ /// Has the [`AST`] been changed during this pass?
+ is_dirty: bool,
+ /// Stack of variables/constants for constants propagation.
+ variables: SmallVec<[(ImmutableString, Option<Dynamic>); SCOPE_ENTRIES_INLINED]>,
+ /// Activate constants propagation?
+ propagate_constants: bool,
+ /// [`Engine`] instance for eager function evaluation.
+ engine: &'a Engine,
+ /// The global runtime state.
+ global: GlobalRuntimeState,
+ /// Function resolution caches.
+ caches: Caches,
+ /// Optimization level.
+ optimization_level: OptimizationLevel,
+}
+
+impl<'a> OptimizerState<'a> {
+ /// Create a new [`OptimizerState`].
+ #[inline(always)]
+ pub fn new(
+ engine: &'a Engine,
+ lib: &'a [crate::SharedModule],
+ optimization_level: OptimizationLevel,
+ ) -> Self {
+ let mut _global = GlobalRuntimeState::new(engine);
+ let _lib = lib;
+
+ #[cfg(not(feature = "no_function"))]
+ {
+ _global.lib = _lib.iter().cloned().collect();
+ }
+
+ Self {
+ is_dirty: false,
+ variables: SmallVec::new_const(),
+ propagate_constants: true,
+ engine,
+ global: _global,
+ caches: Caches::new(),
+ optimization_level,
+ }
+ }
+ /// Set the [`AST`] state to be dirty (i.e. changed).
+ #[inline(always)]
+ pub fn set_dirty(&mut self) {
+ self.is_dirty = true;
+ }
+ /// Set the [`AST`] state to be not dirty (i.e. unchanged).
+ #[inline(always)]
+ pub fn clear_dirty(&mut self) {
+ self.is_dirty = false;
+ }
+ /// Is the [`AST`] dirty (i.e. changed)?
+ #[inline(always)]
+ pub const fn is_dirty(&self) -> bool {
+ self.is_dirty
+ }
+ /// Rewind the variables stack back to a specified size.
+ #[inline(always)]
+ pub fn rewind_var(&mut self, len: usize) {
+ self.variables.truncate(len);
+ }
+ /// Add a new variable to the stack.
+ ///
+ /// `Some(value)` if literal constant (which can be used for constants propagation), `None` otherwise.
+ #[inline(always)]
+ pub fn push_var(&mut self, name: ImmutableString, value: Option<Dynamic>) {
+ self.variables.push((name, value));
+ }
+ /// Look up a literal constant from the variables stack.
+ #[inline]
+ pub fn find_literal_constant(&self, name: &str) -> Option<&Dynamic> {
+ self.variables
+ .iter()
+ .rev()
+ .find(|(n, _)| n.as_str() == name)
+ .and_then(|(_, value)| value.as_ref())
+ }
+ /// Call a registered function
+ #[inline]
+ pub fn call_fn_with_const_args(
+ &mut self,
+ fn_name: &str,
+ op_token: Option<&Token>,
+ arg_values: &mut [Dynamic],
+ ) -> Option<Dynamic> {
+ self.engine
+ .exec_native_fn_call(
+ &mut self.global,
+ &mut self.caches,
+ fn_name,
+ op_token,
+ calc_fn_hash(None, fn_name, arg_values.len()),
+ &mut arg_values.iter_mut().collect::<FnArgsVec<_>>(),
+ false,
+ Position::NONE,
+ )
+ .ok()
+ .map(|(v, ..)| v)
+ }
+}
+
+/// Optimize a block of [statements][Stmt].
+fn optimize_stmt_block(
+ mut statements: StmtBlockContainer,
+ state: &mut OptimizerState,
+ preserve_result: bool,
+ is_internal: bool,
+ reduce_return: bool,
+) -> StmtBlockContainer {
+ if statements.is_empty() {
+ return statements;
+ }
+
+ let mut is_dirty = state.is_dirty();
+
+ let is_pure = if is_internal {
+ Stmt::is_internally_pure
+ } else {
+ Stmt::is_pure
+ };
+
+ // Flatten blocks
+ while let Some(n) = statements.iter().position(
+ |s| matches!(s, Stmt::Block(block, ..) if !block.iter().any(Stmt::is_block_dependent)),
+ ) {
+ let (first, second) = statements.split_at_mut(n);
+ let stmt = second[0].take();
+ let mut stmts = match stmt {
+ Stmt::Block(block, ..) => block,
+ stmt => unreachable!("Stmt::Block expected but gets {:?}", stmt),
+ };
+ statements = first
+ .iter_mut()
+ .map(mem::take)
+ .chain(stmts.iter_mut().map(mem::take))
+ .chain(second.iter_mut().skip(1).map(mem::take))
+ .collect();
+
+ is_dirty = true;
+ }
+
+ // Optimize
+ loop {
+ state.clear_dirty();
+
+ let orig_constants_len = state.variables.len(); // Original number of constants in the state, for restore later
+ let orig_propagate_constants = state.propagate_constants;
+
+ // Remove everything following control flow breaking statements
+ let mut dead_code = false;
+
+ statements.retain(|stmt| {
+ if dead_code {
+ state.set_dirty();
+ false
+ } else if stmt.is_control_flow_break() {
+ dead_code = true;
+ true
+ } else {
+ true
+ }
+ });
+
+ // Optimize each statement in the block
+ statements.iter_mut().for_each(|stmt| {
+ match stmt {
+ Stmt::Var(x, options, ..) => {
+ optimize_expr(&mut x.1, state, false);
+
+ let value = if options.contains(ASTFlags::CONSTANT) && x.1.is_constant() {
+ // constant literal
+ Some(x.1.get_literal_value().unwrap())
+ } else {
+ // variable
+ None
+ };
+ state.push_var(x.0.name.clone(), value);
+ }
+ // Optimize the statement
+ _ => optimize_stmt(stmt, state, preserve_result),
+ }
+ });
+
+ // Remove all pure statements except the last one
+ let mut index = 0;
+ let mut first_non_constant = statements
+ .iter()
+ .rev()
+ .enumerate()
+ .find_map(|(i, stmt)| match stmt {
+ stmt if !is_pure(stmt) => Some(i),
+
+ Stmt::Var(x, ..) if x.1.is_constant() => Some(i),
+ Stmt::Expr(e) if !e.is_constant() => Some(i),
+
+ #[cfg(not(feature = "no_module"))]
+ Stmt::Import(x, ..) if !x.0.is_constant() => Some(i),
+
+ _ => None,
+ })
+ .map_or(0, |n| statements.len() - n - 1);
+
+ while index < statements.len() {
+ if preserve_result && index >= statements.len() - 1 {
+ break;
+ }
+ match statements[index] {
+ ref stmt if is_pure(stmt) && index >= first_non_constant => {
+ state.set_dirty();
+ statements.remove(index);
+ }
+ ref stmt if stmt.is_pure() => {
+ state.set_dirty();
+ if index < first_non_constant {
+ first_non_constant -= 1;
+ }
+ statements.remove(index);
+ }
+ _ => index += 1,
+ }
+ }
+
+ // Remove all pure statements that do not return values at the end of a block.
+ // We cannot remove anything for non-pure statements due to potential side-effects.
+ if preserve_result {
+ loop {
+ match statements[..] {
+ // { return; } -> {}
+ [Stmt::Return(None, options, ..)]
+ if reduce_return && !options.contains(ASTFlags::BREAK) =>
+ {
+ state.set_dirty();
+ statements.clear();
+ }
+ [ref stmt] if !stmt.returns_value() && is_pure(stmt) => {
+ state.set_dirty();
+ statements.clear();
+ }
+ // { ...; return; } -> { ... }
+ [.., ref last_stmt, Stmt::Return(None, options, ..)]
+ if reduce_return
+ && !options.contains(ASTFlags::BREAK)
+ && !last_stmt.returns_value() =>
+ {
+ state.set_dirty();
+ statements.pop().unwrap();
+ }
+ // { ...; return val; } -> { ...; val }
+ [.., Stmt::Return(ref mut expr, options, pos)]
+ if reduce_return && !options.contains(ASTFlags::BREAK) =>
+ {
+ state.set_dirty();
+ *statements.last_mut().unwrap() = expr
+ .as_mut()
+ .map_or_else(|| Stmt::Noop(pos), |e| Stmt::Expr(mem::take(e)));
+ }
+ // { ...; stmt; noop } -> done
+ [.., ref second_last_stmt, Stmt::Noop(..)]
+ if second_last_stmt.returns_value() =>
+ {
+ break
+ }
+ // { ...; stmt_that_returns; pure_non_value_stmt } -> { ...; stmt_that_returns; noop }
+ // { ...; stmt; pure_non_value_stmt } -> { ...; stmt }
+ [.., ref second_last_stmt, ref last_stmt]
+ if !last_stmt.returns_value() && is_pure(last_stmt) =>
+ {
+ state.set_dirty();
+ if second_last_stmt.returns_value() {
+ *statements.last_mut().unwrap() = Stmt::Noop(last_stmt.position());
+ } else {
+ statements.pop().unwrap();
+ }
+ }
+ _ => break,
+ }
+ }
+ } else {
+ loop {
+ match statements[..] {
+ [ref stmt] if is_pure(stmt) => {
+ state.set_dirty();
+ statements.clear();
+ }
+ // { ...; return; } -> { ... }
+ [.., Stmt::Return(None, options, ..)]
+ if reduce_return && !options.contains(ASTFlags::BREAK) =>
+ {
+ state.set_dirty();
+ statements.pop().unwrap();
+ }
+ // { ...; return pure_val; } -> { ... }
+ [.., Stmt::Return(Some(ref expr), options, ..)]
+ if reduce_return
+ && !options.contains(ASTFlags::BREAK)
+ && expr.is_pure() =>
+ {
+ state.set_dirty();
+ statements.pop().unwrap();
+ }
+ [.., ref last_stmt] if is_pure(last_stmt) => {
+ state.set_dirty();
+ statements.pop().unwrap();
+ }
+ _ => break,
+ }
+ }
+ }
+
+ // Pop the stack and remove all the local constants
+ state.rewind_var(orig_constants_len);
+ state.propagate_constants = orig_propagate_constants;
+
+ if !state.is_dirty() {
+ break;
+ }
+
+ is_dirty = true;
+ }
+
+ if is_dirty {
+ state.set_dirty();
+ }
+
+ statements.shrink_to_fit();
+ statements
+}
+
+/// Optimize a [statement][Stmt].
+fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: bool) {
+ match stmt {
+ // var = var op expr => var op= expr
+ Stmt::Assignment(x, ..)
+ if !x.0.is_op_assignment()
+ && x.1.lhs.is_variable_access(true)
+ && matches!(&x.1.rhs, Expr::FnCall(x2, ..)
+ if Token::lookup_symbol_from_syntax(&x2.name).map_or(false, |t| t.has_op_assignment())
+ && x2.args.len() == 2
+ && x2.args[0].get_variable_name(true) == x.1.lhs.get_variable_name(true)
+ ) =>
+ {
+ match x.1.rhs {
+ Expr::FnCall(ref mut x2, pos) => {
+ state.set_dirty();
+ x.0 = OpAssignment::new_op_assignment_from_base(&x2.name, pos);
+ x.1.rhs = x2.args[1].take();
+ }
+ ref expr => unreachable!("Expr::FnCall expected but gets {:?}", expr),
+ }
+ }
+
+ // expr op= expr
+ Stmt::Assignment(x, ..) => {
+ if !x.1.lhs.is_variable_access(false) {
+ optimize_expr(&mut x.1.lhs, state, false);
+ }
+ optimize_expr(&mut x.1.rhs, state, false);
+ }
+
+ // if expr {}
+ Stmt::If(x, ..) if x.body.is_empty() && x.branch.is_empty() => {
+ let condition = &mut x.expr;
+ state.set_dirty();
+
+ let pos = condition.start_position();
+ let mut expr = condition.take();
+ optimize_expr(&mut expr, state, false);
+
+ *stmt = if preserve_result {
+ // -> { expr, Noop }
+ (
+ [Stmt::Expr(expr.into()), Stmt::Noop(pos)],
+ pos,
+ Position::NONE,
+ )
+ .into()
+ } else {
+ // -> expr
+ Stmt::Expr(expr.into())
+ };
+ }
+ // if false { if_block } -> Noop
+ Stmt::If(x, ..)
+ if matches!(x.expr, Expr::BoolConstant(false, ..)) && x.branch.is_empty() =>
+ {
+ if let Expr::BoolConstant(false, pos) = x.expr {
+ state.set_dirty();
+ *stmt = Stmt::Noop(pos);
+ } else {
+ unreachable!("`Expr::BoolConstant`");
+ }
+ }
+ // if false { if_block } else { else_block } -> else_block
+ Stmt::If(x, ..) if matches!(x.expr, Expr::BoolConstant(false, ..)) => {
+ state.set_dirty();
+ let body = x.branch.take_statements();
+ *stmt = match optimize_stmt_block(body, state, preserve_result, true, false) {
+ statements if statements.is_empty() => Stmt::Noop(x.branch.position()),
+ statements => (statements, x.branch.span()).into(),
+ }
+ }
+ // if true { if_block } else { else_block } -> if_block
+ Stmt::If(x, ..) if matches!(x.expr, Expr::BoolConstant(true, ..)) => {
+ state.set_dirty();
+ let body = x.body.take_statements();
+ *stmt = match optimize_stmt_block(body, state, preserve_result, true, false) {
+ statements if statements.is_empty() => Stmt::Noop(x.body.position()),
+ statements => (statements, x.body.span()).into(),
+ }
+ }
+ // if expr { if_block } else { else_block }
+ Stmt::If(x, ..) => {
+ let FlowControl { expr, body, branch } = &mut **x;
+ optimize_expr(expr, state, false);
+ let statements = body.take_statements();
+ **body = optimize_stmt_block(statements, state, preserve_result, true, false);
+ let statements = branch.take_statements();
+ **branch = optimize_stmt_block(statements, state, preserve_result, true, false);
+ }
+
+ // switch const { ... }
+ Stmt::Switch(x, pos) if x.0.is_constant() => {
+ let (
+ match_expr,
+ SwitchCasesCollection {
+ expressions,
+ cases,
+ ranges,
+ def_case,
+ },
+ ) = &mut **x;
+
+ let value = match_expr.get_literal_value().unwrap();
+ let hasher = &mut get_hasher();
+ value.hash(hasher);
+ let hash = hasher.finish();
+
+ // First check hashes
+ if let Some(case_blocks_list) = cases.get(&hash) {
+ match &case_blocks_list[..] {
+ [] => (),
+ [index] => {
+ let mut b = mem::take(&mut expressions[*index]);
+ cases.clear();
+
+ if b.is_always_true() {
+ // Promote the matched case
+ let mut statements = Stmt::Expr(b.expr.take().into());
+ optimize_stmt(&mut statements, state, true);
+ *stmt = statements;
+ } else {
+ // switch const { case if condition => stmt, _ => def } => if condition { stmt } else { def }
+ optimize_expr(&mut b.condition, state, false);
+
+ let branch = match def_case {
+ Some(index) => {
+ let mut def_stmt =
+ Stmt::Expr(expressions[*index].expr.take().into());
+ optimize_stmt(&mut def_stmt, state, true);
+ def_stmt.into()
+ }
+ _ => StmtBlock::NONE,
+ };
+ let body = Stmt::Expr(b.expr.take().into()).into();
+ let expr = b.condition.take();
+
+ *stmt = Stmt::If(
+ FlowControl { expr, body, branch }.into(),
+ match_expr.start_position(),
+ );
+ }
+
+ state.set_dirty();
+ return;
+ }
+ _ => {
+ for &index in case_blocks_list {
+ let mut b = mem::take(&mut expressions[index]);
+
+ if b.is_always_true() {
+ // Promote the matched case
+ let mut statements = Stmt::Expr(b.expr.take().into());
+ optimize_stmt(&mut statements, state, true);
+ *stmt = statements;
+ state.set_dirty();
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ // Then check ranges
+ if !ranges.is_empty() {
+ // Only one range or all ranges without conditions
+ if ranges.len() == 1
+ || ranges
+ .iter()
+ .all(|r| expressions[r.index()].is_always_true())
+ {
+ if let Some(r) = ranges.iter().find(|r| r.contains(&value)) {
+ let range_block = &mut expressions[r.index()];
+
+ if range_block.is_always_true() {
+ // Promote the matched case
+ let block = &mut expressions[r.index()];
+ let mut statements = Stmt::Expr(block.expr.take().into());
+ optimize_stmt(&mut statements, state, true);
+ *stmt = statements;
+ } else {
+ let mut expr = range_block.condition.take();
+
+ // switch const { range if condition => stmt, _ => def } => if condition { stmt } else { def }
+ optimize_expr(&mut expr, state, false);
+
+ let branch = match def_case {
+ Some(index) => {
+ let mut def_stmt =
+ Stmt::Expr(expressions[*index].expr.take().into());
+ optimize_stmt(&mut def_stmt, state, true);
+ def_stmt.into()
+ }
+ _ => StmtBlock::NONE,
+ };
+
+ let body = Stmt::Expr(expressions[r.index()].expr.take().into()).into();
+
+ *stmt = Stmt::If(
+ FlowControl { expr, body, branch }.into(),
+ match_expr.start_position(),
+ );
+ }
+
+ state.set_dirty();
+ return;
+ }
+ } else {
+ // Multiple ranges - clear the table and just keep the right ranges
+ if !cases.is_empty() {
+ state.set_dirty();
+ cases.clear();
+ }
+
+ let old_ranges_len = ranges.len();
+
+ ranges.retain(|r| r.contains(&value));
+
+ if ranges.len() != old_ranges_len {
+ state.set_dirty();
+ }
+
+ ranges.iter().for_each(|r| {
+ let b = &mut expressions[r.index()];
+ optimize_expr(&mut b.condition, state, false);
+ optimize_expr(&mut b.expr, state, false);
+ });
+ return;
+ }
+ }
+
+ // Promote the default case
+ state.set_dirty();
+
+ match def_case {
+ Some(index) => {
+ let mut def_stmt = Stmt::Expr(expressions[*index].expr.take().into());
+ optimize_stmt(&mut def_stmt, state, true);
+ *stmt = def_stmt;
+ }
+ _ => *stmt = StmtBlock::empty(*pos).into(),
+ }
+ }
+ // switch
+ Stmt::Switch(x, ..) => {
+ let (
+ match_expr,
+ SwitchCasesCollection {
+ expressions,
+ cases,
+ ranges,
+ def_case,
+ ..
+ },
+ ) = &mut **x;
+
+ optimize_expr(match_expr, state, false);
+
+ // Optimize blocks
+ expressions.iter_mut().for_each(|b| {
+ optimize_expr(&mut b.condition, state, false);
+ optimize_expr(&mut b.expr, state, false);
+
+ if b.is_always_false() && !b.expr.is_unit() {
+ b.expr = Expr::Unit(b.expr.position());
+ state.set_dirty();
+ }
+ });
+
+ // Remove false cases
+ cases.retain(|_, list| {
+ // Remove all entries that have false conditions
+ list.retain(|index| {
+ if expressions[*index].is_always_false() {
+ state.set_dirty();
+ false
+ } else {
+ true
+ }
+ });
+ // Remove all entries after a `true` condition
+ if let Some(n) = list
+ .iter()
+ .position(|&index| expressions[index].is_always_true())
+ {
+ if n + 1 < list.len() {
+ state.set_dirty();
+ list.truncate(n + 1);
+ }
+ }
+ // Remove if no entry left
+ if list.is_empty() {
+ state.set_dirty();
+ false
+ } else {
+ true
+ }
+ });
+
+ // Remove false ranges
+ ranges.retain(|r| {
+ if expressions[r.index()].is_always_false() {
+ state.set_dirty();
+ false
+ } else {
+ true
+ }
+ });
+
+ if let Some(index) = def_case {
+ optimize_expr(&mut expressions[*index].expr, state, false);
+ }
+
+ // Remove unused block statements
+ (0..expressions.len()).into_iter().for_each(|index| {
+ if *def_case == Some(index)
+ || cases.values().flat_map(|c| c.iter()).any(|&n| n == index)
+ || ranges.iter().any(|r| r.index() == index)
+ {
+ return;
+ }
+
+ let b = &mut expressions[index];
+
+ if !b.expr.is_unit() {
+ b.expr = Expr::Unit(b.expr.position());
+ state.set_dirty();
+ }
+ });
+ }
+
+ // while false { block } -> Noop
+ Stmt::While(x, ..) if matches!(x.expr, Expr::BoolConstant(false, ..)) => match x.expr {
+ Expr::BoolConstant(false, pos) => {
+ state.set_dirty();
+ *stmt = Stmt::Noop(pos);
+ }
+ _ => unreachable!("`Expr::BoolConstant"),
+ },
+ // while expr { block }
+ Stmt::While(x, ..) => {
+ let FlowControl { expr, body, .. } = &mut **x;
+ optimize_expr(expr, state, false);
+ if let Expr::BoolConstant(true, pos) = expr {
+ *expr = Expr::Unit(*pos);
+ }
+ **body = optimize_stmt_block(body.take_statements(), state, false, true, false);
+ }
+ // do { block } while|until expr
+ Stmt::Do(x, ..) => {
+ optimize_expr(&mut x.expr, state, false);
+ *x.body = optimize_stmt_block(x.body.take_statements(), state, false, true, false);
+ }
+ // for id in expr { block }
+ Stmt::For(x, ..) => {
+ optimize_expr(&mut x.2.expr, state, false);
+ *x.2.body = optimize_stmt_block(x.2.body.take_statements(), state, false, true, false);
+ }
+ // let id = expr;
+ Stmt::Var(x, options, ..) if !options.contains(ASTFlags::CONSTANT) => {
+ optimize_expr(&mut x.1, state, false);
+ }
+ // import expr as var;
+ #[cfg(not(feature = "no_module"))]
+ Stmt::Import(x, ..) => optimize_expr(&mut x.0, state, false),
+ // { block }
+ Stmt::Block(block) => {
+ let span = block.span();
+ let statements = block.take_statements().into_vec().into();
+ let mut block = optimize_stmt_block(statements, state, preserve_result, true, false);
+
+ match block.as_mut_slice() {
+ [] => {
+ state.set_dirty();
+ *stmt = Stmt::Noop(span.start());
+ }
+ // Only one statement which is not block-dependent - promote
+ [s] if !s.is_block_dependent() => {
+ state.set_dirty();
+ *stmt = s.take();
+ }
+ _ => *stmt = (block, span).into(),
+ }
+ }
+ // try { pure try_block } catch ( var ) { catch_block } -> try_block
+ Stmt::TryCatch(x, ..) if x.body.iter().all(Stmt::is_pure) => {
+ // If try block is pure, there will never be any exceptions
+ state.set_dirty();
+ *stmt = (
+ optimize_stmt_block(x.body.take_statements(), state, false, true, false),
+ x.body.span(),
+ )
+ .into();
+ }
+ // try { try_block } catch ( var ) { catch_block }
+ Stmt::TryCatch(x, ..) => {
+ *x.body = optimize_stmt_block(x.body.take_statements(), state, false, true, false);
+ *x.branch = optimize_stmt_block(x.branch.take_statements(), state, false, true, false);
+ }
+
+ // expr(stmt)
+ Stmt::Expr(expr) if matches!(**expr, Expr::Stmt(..)) => {
+ state.set_dirty();
+ match expr.as_mut() {
+ Expr::Stmt(block) if !block.is_empty() => {
+ let mut stmt_block = *mem::take(block);
+ *stmt_block =
+ optimize_stmt_block(stmt_block.take_statements(), state, true, true, false);
+ *stmt = stmt_block.into();
+ }
+ Expr::Stmt(..) => *stmt = Stmt::Noop(expr.position()),
+ _ => unreachable!("`Expr::Stmt`"),
+ }
+ }
+
+ // expr(func())
+ Stmt::Expr(expr) if matches!(**expr, Expr::FnCall(..)) => {
+ state.set_dirty();
+ match expr.take() {
+ Expr::FnCall(x, pos) => *stmt = Stmt::FnCall(x, pos),
+ _ => unreachable!(),
+ }
+ }
+
+ Stmt::Expr(expr) => optimize_expr(expr, state, false),
+
+ // func(...)
+ Stmt::FnCall(..) => {
+ if let Stmt::FnCall(x, pos) = stmt.take() {
+ let mut expr = Expr::FnCall(x, pos);
+ optimize_expr(&mut expr, state, false);
+ *stmt = match expr {
+ Expr::FnCall(x, pos) => Stmt::FnCall(x, pos),
+ _ => Stmt::Expr(expr.into()),
+ }
+ } else {
+ unreachable!();
+ }
+ }
+
+ // break expr;
+ Stmt::BreakLoop(Some(ref mut expr), ..) => optimize_expr(expr, state, false),
+
+ // return expr;
+ Stmt::Return(Some(ref mut expr), ..) => optimize_expr(expr, state, false),
+
+ // Share nothing
+ #[cfg(not(feature = "no_closure"))]
+ Stmt::Share(x) if x.is_empty() => {
+ state.set_dirty();
+ *stmt = Stmt::Noop(Position::NONE);
+ }
+ // Share constants
+ #[cfg(not(feature = "no_closure"))]
+ Stmt::Share(x) => {
+ let orig_len = x.len();
+
+ if state.propagate_constants {
+ x.retain(|(v, _)| state.find_literal_constant(v).is_none());
+
+ if x.len() != orig_len {
+ state.set_dirty();
+ }
+ }
+ }
+
+ // All other statements - skip
+ _ => (),
+ }
+}
+
+/// Optimize an [expression][Expr].
+fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
+ // These keywords are handled specially
+ const DONT_EVAL_KEYWORDS: &[&str] = &[
+ KEYWORD_PRINT, // side effects
+ KEYWORD_DEBUG, // side effects
+ KEYWORD_EVAL, // arbitrary scripts
+ ];
+
+ match expr {
+ // {}
+ Expr::Stmt(x) if x.is_empty() => { state.set_dirty(); *expr = Expr::Unit(x.position()) }
+ Expr::Stmt(x) if x.len() == 1 && matches!(x.statements()[0], Stmt::Expr(..)) => {
+ state.set_dirty();
+ match x.take_statements().remove(0) {
+ Stmt::Expr(mut e) => {
+ optimize_expr(&mut e, state, false);
+ *expr = *e;
+ }
+ _ => unreachable!("`Expr::Stmt`")
+ }
+ }
+ // { stmt; ... } - do not count promotion as dirty because it gets turned back into an array
+ Expr::Stmt(x) => {
+ ***x = optimize_stmt_block(x.take_statements(), state, true, true, false);
+
+ // { Stmt(Expr) } - promote
+ if let [ Stmt::Expr(e) ] = &mut ****x { state.set_dirty(); *expr = e.take(); }
+ }
+ // ()?.rhs
+ #[cfg(not(feature = "no_object"))]
+ Expr::Dot(x, options, ..) if options.contains(ASTFlags::NEGATED) && matches!(x.lhs, Expr::Unit(..)) => {
+ state.set_dirty();
+ *expr = x.lhs.take();
+ }
+ // lhs.rhs
+ #[cfg(not(feature = "no_object"))]
+ Expr::Dot(x, ..) if !_chaining => match (&mut x.lhs, &mut x.rhs) {
+ // map.string
+ (Expr::Map(m, pos), Expr::Property(p, ..)) if m.0.iter().all(|(.., x)| x.is_pure()) => {
+ let prop = p.2.as_str();
+ // Map literal where everything is pure - promote the indexed item.
+ // All other items can be thrown away.
+ state.set_dirty();
+ *expr = mem::take(&mut m.0).into_iter().find(|(x, ..)| x.as_str() == prop)
+ .map_or_else(|| Expr::Unit(*pos), |(.., mut expr)| { expr.set_position(*pos); expr });
+ }
+ // var.rhs or this.rhs
+ (Expr::Variable(..) | Expr::ThisPtr(..), rhs) => optimize_expr(rhs, state, true),
+ // const.type_of()
+ (lhs, Expr::MethodCall(x, pos)) if lhs.is_constant() && x.name == KEYWORD_TYPE_OF && x.args.is_empty() => {
+ if let Some(value) = lhs.get_literal_value() {
+ state.set_dirty();
+ let typ = state.engine.map_type_name(value.type_name()).into();
+ *expr = Expr::from_dynamic(typ, *pos);
+ }
+ }
+ // const.is_shared()
+ #[cfg(not(feature = "no_closure"))]
+ (lhs, Expr::MethodCall(x, pos)) if lhs.is_constant() && x.name == crate::engine::KEYWORD_IS_SHARED && x.args.is_empty() => {
+ if let Some(..) = lhs.get_literal_value() {
+ state.set_dirty();
+ *expr = Expr::from_dynamic(Dynamic::FALSE, *pos);
+ }
+ }
+ // lhs.rhs
+ (lhs, rhs) => { optimize_expr(lhs, state, false); optimize_expr(rhs, state, true); }
+ }
+ // ....lhs.rhs
+ #[cfg(not(feature = "no_object"))]
+ Expr::Dot(x,..) => { optimize_expr(&mut x.lhs, state, false); optimize_expr(&mut x.rhs, state, _chaining); }
+
+ // ()?[rhs]
+ #[cfg(not(feature = "no_index"))]
+ Expr::Index(x, options, ..) if options.contains(ASTFlags::NEGATED) && matches!(x.lhs, Expr::Unit(..)) => {
+ state.set_dirty();
+ *expr = x.lhs.take();
+ }
+ // lhs[rhs]
+ #[cfg(not(feature = "no_index"))]
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ Expr::Index(x, ..) if !_chaining => match (&mut x.lhs, &mut x.rhs) {
+ // array[int]
+ (Expr::Array(a, pos), Expr::IntegerConstant(i, ..)) if *i >= 0 && *i <= crate::MAX_USIZE_INT && (*i as usize) < a.len() && a.iter().all(Expr::is_pure) => {
+ // Array literal where everything is pure - promote the indexed item.
+ // All other items can be thrown away.
+ state.set_dirty();
+ let mut result = a[*i as usize].take();
+ result.set_position(*pos);
+ *expr = result;
+ }
+ // array[-int]
+ (Expr::Array(a, pos), Expr::IntegerConstant(i, ..)) if *i < 0 && i.unsigned_abs() as u64 <= a.len() as u64 && a.iter().all(Expr::is_pure) => {
+ // Array literal where everything is pure - promote the indexed item.
+ // All other items can be thrown away.
+ state.set_dirty();
+ let index = a.len() - i.unsigned_abs() as usize;
+ let mut result = a[index].take();
+ result.set_position(*pos);
+ *expr = result;
+ }
+ // map[string]
+ (Expr::Map(m, pos), Expr::StringConstant(s, ..)) if m.0.iter().all(|(.., x)| x.is_pure()) => {
+ // Map literal where everything is pure - promote the indexed item.
+ // All other items can be thrown away.
+ state.set_dirty();
+ *expr = mem::take(&mut m.0).into_iter().find(|(x, ..)| x.as_str() == s.as_str())
+ .map_or_else(|| Expr::Unit(*pos), |(.., mut expr)| { expr.set_position(*pos); expr });
+ }
+ // int[int]
+ (Expr::IntegerConstant(n, pos), Expr::IntegerConstant(i, ..)) if *i >= 0 && *i <= crate::MAX_USIZE_INT && (*i as usize) < crate::INT_BITS => {
+ // Bit-field literal indexing - get the bit
+ state.set_dirty();
+ *expr = Expr::BoolConstant((*n & (1 << (*i as usize))) != 0, *pos);
+ }
+ // int[-int]
+ (Expr::IntegerConstant(n, pos), Expr::IntegerConstant(i, ..)) if *i < 0 && i.unsigned_abs() as u64 <= crate::INT_BITS as u64 => {
+ // Bit-field literal indexing - get the bit
+ state.set_dirty();
+ *expr = Expr::BoolConstant((*n & (1 << (crate::INT_BITS - i.unsigned_abs() as usize))) != 0, *pos);
+ }
+ // string[int]
+ (Expr::StringConstant(s, pos), Expr::IntegerConstant(i, ..)) if *i >= 0 && *i <= crate::MAX_USIZE_INT && (*i as usize) < s.chars().count() => {
+ // String literal indexing - get the character
+ state.set_dirty();
+ *expr = Expr::CharConstant(s.chars().nth(*i as usize).unwrap(), *pos);
+ }
+ // string[-int]
+ (Expr::StringConstant(s, pos), Expr::IntegerConstant(i, ..)) if *i < 0 && i.unsigned_abs() as u64 <= s.chars().count() as u64 => {
+ // String literal indexing - get the character
+ state.set_dirty();
+ *expr = Expr::CharConstant(s.chars().rev().nth(i.unsigned_abs() as usize - 1).unwrap(), *pos);
+ }
+ // var[rhs] or this[rhs]
+ (Expr::Variable(..) | Expr::ThisPtr(..), rhs) => optimize_expr(rhs, state, true),
+ // lhs[rhs]
+ (lhs, rhs) => { optimize_expr(lhs, state, false); optimize_expr(rhs, state, true); }
+ },
+ // ...[lhs][rhs]
+ #[cfg(not(feature = "no_index"))]
+ Expr::Index(x, ..) => { optimize_expr(&mut x.lhs, state, false); optimize_expr(&mut x.rhs, state, _chaining); }
+ // ``
+ Expr::InterpolatedString(x, pos) if x.is_empty() => {
+ state.set_dirty();
+ *expr = Expr::StringConstant(state.engine.const_empty_string(), *pos);
+ }
+ // `... ${const} ...`
+ Expr::InterpolatedString(..) if expr.is_constant() => {
+ state.set_dirty();
+ *expr = Expr::StringConstant(expr.get_literal_value().unwrap().cast::<ImmutableString>(), expr.position());
+ }
+ // `... ${ ... } ...`
+ Expr::InterpolatedString(x, ..) => {
+ x.iter_mut().for_each(|expr| optimize_expr(expr, state, false));
+
+ let mut n = 0;
+
+ // Merge consecutive strings
+ while n < x.len() - 1 {
+ match (x[n].take(),x[n+1].take()) {
+ (Expr::StringConstant(mut s1, pos), Expr::StringConstant(s2, ..)) => { s1 += s2; x[n] = Expr::StringConstant(s1, pos); x.remove(n+1); state.set_dirty(); }
+ (expr1, Expr::Unit(..)) => { x[n] = expr1; x.remove(n+1); state.set_dirty(); }
+ (Expr::Unit(..), expr2) => { x[n+1] = expr2; x.remove(n); state.set_dirty(); }
+ (expr1, Expr::StringConstant(s, ..)) if s.is_empty() => { x[n] = expr1; x.remove(n+1); state.set_dirty(); }
+ (Expr::StringConstant(s, ..), expr2) if s.is_empty()=> { x[n+1] = expr2; x.remove(n); state.set_dirty(); }
+ (expr1, expr2) => { x[n] = expr1; x[n+1] = expr2; n += 1; }
+ }
+ }
+
+ x.shrink_to_fit();
+ }
+ // [ constant .. ]
+ #[cfg(not(feature = "no_index"))]
+ Expr::Array(..) if expr.is_constant() => {
+ state.set_dirty();
+ *expr = Expr::DynamicConstant(expr.get_literal_value().unwrap().into(), expr.position());
+ }
+ // [ items .. ]
+ #[cfg(not(feature = "no_index"))]
+ Expr::Array(x, ..) => x.iter_mut().for_each(|expr| optimize_expr(expr, state, false)),
+ // #{ key:constant, .. }
+ #[cfg(not(feature = "no_object"))]
+ Expr::Map(..) if expr.is_constant() => {
+ state.set_dirty();
+ *expr = Expr::DynamicConstant(expr.get_literal_value().unwrap().into(), expr.position());
+ }
+ // #{ key:value, .. }
+ #[cfg(not(feature = "no_object"))]
+ Expr::Map(x, ..) => x.0.iter_mut().for_each(|(.., expr)| optimize_expr(expr, state, false)),
+ // lhs && rhs
+ Expr::And(x, ..) => match (&mut x.lhs, &mut x.rhs) {
+ // true && rhs -> rhs
+ (Expr::BoolConstant(true, ..), rhs) => { state.set_dirty(); optimize_expr(rhs, state, false); *expr = rhs.take(); }
+ // false && rhs -> false
+ (Expr::BoolConstant(false, pos), ..) => { state.set_dirty(); *expr = Expr::BoolConstant(false, *pos); }
+ // lhs && true -> lhs
+ (lhs, Expr::BoolConstant(true, ..)) => { state.set_dirty(); optimize_expr(lhs, state, false); *expr = lhs.take(); }
+ // lhs && rhs
+ (lhs, rhs) => { optimize_expr(lhs, state, false); optimize_expr(rhs, state, false); }
+ },
+ // lhs || rhs
+ Expr::Or(ref mut x, ..) => match (&mut x.lhs, &mut x.rhs) {
+ // false || rhs -> rhs
+ (Expr::BoolConstant(false, ..), rhs) => { state.set_dirty(); optimize_expr(rhs, state, false); *expr = rhs.take(); }
+ // true || rhs -> true
+ (Expr::BoolConstant(true, pos), ..) => { state.set_dirty(); *expr = Expr::BoolConstant(true, *pos); }
+ // lhs || false
+ (lhs, Expr::BoolConstant(false, ..)) => { state.set_dirty(); optimize_expr(lhs, state, false); *expr = lhs.take(); }
+ // lhs || rhs
+ (lhs, rhs) => { optimize_expr(lhs, state, false); optimize_expr(rhs, state, false); }
+ },
+ // () ?? rhs -> rhs
+ Expr::Coalesce(x, ..) if matches!(x.lhs, Expr::Unit(..)) => {
+ state.set_dirty();
+ *expr = x.rhs.take();
+ },
+ // lhs:constant ?? rhs -> lhs
+ Expr::Coalesce(x, ..) if x.lhs.is_constant() => {
+ state.set_dirty();
+ *expr = x.lhs.take();
+ },
+
+ // !true or !false
+ Expr::FnCall(x,..)
+ if x.name == OP_NOT
+ && x.args.len() == 1
+ && matches!(x.args[0], Expr::BoolConstant(..))
+ => {
+ state.set_dirty();
+ if let Expr::BoolConstant(b, pos) = x.args[0] {
+ *expr = Expr::BoolConstant(!b, pos)
+ } else {
+ unreachable!()
+ }
+ }
+
+ // eval!
+ Expr::FnCall(x, ..) if x.name == KEYWORD_EVAL => {
+ state.propagate_constants = false;
+ }
+ // Fn
+ Expr::FnCall(x, pos)
+ if !x.is_qualified() // Non-qualified
+ && x.args.len() == 1
+ && x.name == KEYWORD_FN_PTR
+ && x.constant_args()
+ => {
+ let fn_name = match x.args[0] {
+ Expr::StringConstant(ref s, ..) => s.clone().into(),
+ _ => Dynamic::UNIT
+ };
+
+ if let Ok(fn_ptr) = fn_name.into_immutable_string().map_err(Into::into).and_then(FnPtr::try_from) {
+ state.set_dirty();
+ *expr = Expr::DynamicConstant(Box::new(fn_ptr.into()), *pos);
+ } else {
+ optimize_expr(&mut x.args[0], state, false);
+ }
+ }
+ // curry(FnPtr, constants...)
+ Expr::FnCall(x, pos)
+ if !x.is_qualified() // Non-qualified
+ && x.args.len() >= 2
+ && x.name == KEYWORD_FN_PTR_CURRY
+ && matches!(x.args[0], Expr::DynamicConstant(ref v, ..) if v.is_fnptr())
+ && x.constant_args()
+ => {
+ let mut fn_ptr = x.args[0].get_literal_value().unwrap().cast::<FnPtr>();
+ fn_ptr.extend(x.args.iter().skip(1).map(|arg_expr| arg_expr.get_literal_value().unwrap()));
+ state.set_dirty();
+ *expr = Expr::DynamicConstant(Box::new(fn_ptr.into()), *pos);
+ }
+
+ // Do not call some special keywords that may have side effects
+ Expr::FnCall(x, ..) if DONT_EVAL_KEYWORDS.contains(&x.name.as_str()) => {
+ x.args.iter_mut().for_each(|arg_expr| optimize_expr(arg_expr, state, false));
+ }
+
+ // Call built-in operators
+ Expr::FnCall(x, pos)
+ if !x.is_qualified() // Non-qualified
+ && state.optimization_level == OptimizationLevel::Simple // simple optimizations
+ && x.constant_args() // all arguments are constants
+ => {
+ let arg_values = &mut x.args.iter().map(|arg_expr| arg_expr.get_literal_value().unwrap()).collect::<FnArgsVec<_>>();
+ let arg_types = arg_values.iter().map(Dynamic::type_id).collect::<FnArgsVec<_>>();
+
+ match x.name.as_str() {
+ KEYWORD_TYPE_OF if arg_values.len() == 1 => {
+ state.set_dirty();
+ let typ = state.engine.map_type_name(arg_values[0].type_name()).into();
+ *expr = Expr::from_dynamic(typ, *pos);
+ return;
+ }
+ #[cfg(not(feature = "no_closure"))]
+ crate::engine::KEYWORD_IS_SHARED if arg_values.len() == 1 => {
+ state.set_dirty();
+ *expr = Expr::from_dynamic(Dynamic::FALSE, *pos);
+ return;
+ }
+ // Overloaded operators can override built-in.
+ _ if x.args.len() == 2 && x.op_token.is_some() && (state.engine.fast_operators() || !state.engine.has_native_fn_override(x.hashes.native(), &arg_types)) => {
+ if let Some(result) = get_builtin_binary_op_fn(x.op_token.as_ref().unwrap(), &arg_values[0], &arg_values[1])
+ .and_then(|(f, ctx)| {
+ let context = ctx.then(|| (state.engine, x.name.as_str(), None, &state.global, *pos).into());
+ let (first, second) = arg_values.split_first_mut().unwrap();
+ f(context, &mut [ first, &mut second[0] ]).ok()
+ }) {
+ state.set_dirty();
+ *expr = Expr::from_dynamic(result, *pos);
+ return;
+ }
+ }
+ _ => ()
+ }
+
+ x.args.iter_mut().for_each(|arg_expr| optimize_expr(arg_expr, state, false));
+
+ // Move constant arguments
+ x.args.iter_mut().for_each(|arg_expr| match arg_expr {
+ Expr::DynamicConstant(..) | Expr::Unit(..)
+ | Expr::StringConstant(..) | Expr::CharConstant(..)
+ | Expr::BoolConstant(..) | Expr::IntegerConstant(..) => (),
+
+ #[cfg(not(feature = "no_float"))]
+ Expr:: FloatConstant(..) => (),
+
+ _ => if let Some(value) = arg_expr.get_literal_value() {
+ state.set_dirty();
+ *arg_expr = Expr::DynamicConstant(value.into(), arg_expr.start_position());
+ },
+ });
+ }
+
+ // Eagerly call functions
+ Expr::FnCall(x, pos)
+ if !x.is_qualified() // non-qualified
+ && state.optimization_level == OptimizationLevel::Full // full optimizations
+ && x.constant_args() // all arguments are constants
+ => {
+ // First search for script-defined functions (can override built-in)
+ let _has_script_fn = false;
+ #[cfg(not(feature = "no_function"))]
+ let _has_script_fn = !x.hashes.is_native_only() && state.global.lib.iter().find_map(|m| m.get_script_fn(&x.name, x.args.len())).is_some();
+
+ if !_has_script_fn {
+ let arg_values = &mut x.args.iter().map(Expr::get_literal_value).collect::<Option<FnArgsVec<_>>>().unwrap();
+
+ let result = match x.name.as_str() {
+ KEYWORD_TYPE_OF if arg_values.len() == 1 => Some(state.engine.map_type_name(arg_values[0].type_name()).into()),
+ #[cfg(not(feature = "no_closure"))]
+ crate::engine::KEYWORD_IS_SHARED if arg_values.len() == 1 => Some(Dynamic::FALSE),
+ _ => state.call_fn_with_const_args(&x.name, x.op_token.as_ref(), arg_values)
+ };
+
+ if let Some(r) = result {
+ state.set_dirty();
+ *expr = Expr::from_dynamic(r, *pos);
+ return;
+ }
+ }
+
+ x.args.iter_mut().for_each(|a| optimize_expr(a, state, false));
+ }
+
+ // id(args ..) or xxx.id(args ..) -> optimize function call arguments
+ Expr::FnCall(x, ..) | Expr::MethodCall(x, ..) => x.args.iter_mut().for_each(|arg_expr| {
+ optimize_expr(arg_expr, state, false);
+
+ // Move constant arguments
+ match arg_expr {
+ Expr::DynamicConstant(..) | Expr::Unit(..)
+ | Expr::StringConstant(..) | Expr::CharConstant(..)
+ | Expr::BoolConstant(..) | Expr::IntegerConstant(..) => (),
+
+ #[cfg(not(feature = "no_float"))]
+ Expr:: FloatConstant(..) => (),
+
+ _ => if let Some(value) = arg_expr.get_literal_value() {
+ state.set_dirty();
+ *arg_expr = Expr::DynamicConstant(value.into(), arg_expr.start_position());
+ },
+ }
+ }),
+
+ // constant-name
+ #[cfg(not(feature = "no_module"))]
+ Expr::Variable(x, ..) if !x.1.is_empty() => (),
+ Expr::Variable(x, .., pos) if state.propagate_constants && state.find_literal_constant(&x.3).is_some() => {
+ // Replace constant with value
+ *expr = Expr::from_dynamic(state.find_literal_constant(&x.3).unwrap().clone(), *pos);
+ state.set_dirty();
+ }
+
+ // Custom syntax
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Expr::Custom(x, ..) => {
+ if x.scope_may_be_changed {
+ state.propagate_constants = false;
+ }
+ // Do not optimize custom syntax expressions as you won't know how they would be called
+ }
+
+ // All other expressions - skip
+ _ => (),
+ }
+}
+
+impl Engine {
+ /// Has a system function a Rust-native override?
+ fn has_native_fn_override(&self, hash_script: u64, arg_types: impl AsRef<[TypeId]>) -> bool {
+ let hash = calc_fn_hash_full(hash_script, arg_types.as_ref().iter().copied());
+
+ // First check the global namespace and packages, but skip modules that are standard because
+ // they should never conflict with system functions.
+ if self
+ .global_modules
+ .iter()
+ .filter(|m| !m.flags.contains(ModuleFlags::STANDARD_LIB))
+ .any(|m| m.contains_fn(hash))
+ {
+ return true;
+ }
+
+ // Then check sub-modules
+ #[cfg(not(feature = "no_module"))]
+ if self
+ .global_sub_modules
+ .as_ref()
+ .into_iter()
+ .flatten()
+ .any(|(_, m)| m.contains_qualified_fn(hash))
+ {
+ return true;
+ }
+
+ false
+ }
+
+ /// Optimize a block of [statements][Stmt] at top level.
+ ///
+ /// Constants and variables from the scope are added.
+ fn optimize_top_level(
+ &self,
+ statements: StmtBlockContainer,
+ scope: Option<&Scope>,
+ lib: &[crate::SharedModule],
+ optimization_level: OptimizationLevel,
+ ) -> StmtBlockContainer {
+ let mut statements = statements;
+
+ // If optimization level is None then skip optimizing
+ if optimization_level == OptimizationLevel::None {
+ statements.shrink_to_fit();
+ return statements;
+ }
+
+ // Set up the state
+ let mut state = OptimizerState::new(self, lib, optimization_level);
+
+ // Add constants from global modules
+ self.global_modules
+ .iter()
+ .rev()
+ .flat_map(|m| m.iter_var())
+ .for_each(|(name, value)| state.push_var(name.into(), Some(value.clone())));
+
+ // Add constants and variables from the scope
+ scope
+ .into_iter()
+ .flat_map(Scope::iter)
+ .for_each(|(name, constant, value)| {
+ state.push_var(name.into(), if constant { Some(value) } else { None });
+ });
+
+ optimize_stmt_block(statements, &mut state, true, false, true)
+ }
+
+ /// Optimize a collection of statements and functions into an [`AST`].
+ pub(crate) fn optimize_into_ast(
+ &self,
+ scope: Option<&Scope>,
+ statements: StmtBlockContainer,
+ #[cfg(not(feature = "no_function"))] functions: crate::StaticVec<
+ crate::Shared<crate::ast::ScriptFnDef>,
+ >,
+ optimization_level: OptimizationLevel,
+ ) -> AST {
+ let mut statements = statements;
+
+ #[cfg(not(feature = "no_function"))]
+ let lib: crate::Shared<_> = {
+ let mut module = crate::Module::new();
+
+ if optimization_level == OptimizationLevel::None {
+ functions.into_iter().for_each(|fn_def| {
+ module.set_script_fn(fn_def);
+ });
+ } else {
+ // We only need the script library's signatures for optimization purposes
+ let mut lib2 = crate::Module::new();
+
+ functions
+ .iter()
+ .map(|fn_def| crate::ast::ScriptFnDef {
+ name: fn_def.name.clone(),
+ access: fn_def.access,
+ body: crate::ast::StmtBlock::NONE,
+ #[cfg(not(feature = "no_object"))]
+ this_type: fn_def.this_type.clone(),
+ params: fn_def.params.clone(),
+ #[cfg(feature = "metadata")]
+ comments: Box::default(),
+ })
+ .for_each(|script_def| {
+ lib2.set_script_fn(script_def);
+ });
+
+ let lib2 = &[lib2.into()];
+
+ functions.into_iter().for_each(|fn_def| {
+ let mut fn_def = crate::func::shared_take_or_clone(fn_def);
+ // Optimize the function body
+ let body = fn_def.body.take_statements();
+
+ *fn_def.body = self.optimize_top_level(body, scope, lib2, optimization_level);
+
+ module.set_script_fn(fn_def);
+ });
+ }
+
+ module.into()
+ };
+ #[cfg(feature = "no_function")]
+ let lib: crate::Shared<_> = crate::Module::new().into();
+
+ statements.shrink_to_fit();
+
+ AST::new(
+ match optimization_level {
+ OptimizationLevel::None => statements,
+ OptimizationLevel::Simple | OptimizationLevel::Full => {
+ self.optimize_top_level(statements, scope, &[lib.clone()], optimization_level)
+ }
+ },
+ #[cfg(not(feature = "no_function"))]
+ lib,
+ )
+ }
+}
diff --git a/rhai/src/packages/arithmetic.rs b/rhai/src/packages/arithmetic.rs
new file mode 100644
index 0000000..5d8b14e
--- /dev/null
+++ b/rhai/src/packages/arithmetic.rs
@@ -0,0 +1,571 @@
+#![allow(non_snake_case)]
+
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+use crate::{def_package, Position, RhaiError, RhaiResultOf, ERR, INT};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(feature = "no_std")]
+#[cfg(not(feature = "no_float"))]
+use num_traits::Float;
+
+#[cold]
+#[inline(never)]
+pub fn make_err(msg: impl Into<String>) -> RhaiError {
+ ERR::ErrorArithmetic(msg.into(), Position::NONE).into()
+}
+
+macro_rules! gen_arithmetic_functions {
+ ($root:ident => $($arg_type:ident),+) => {
+ pub mod $root { $(pub mod $arg_type {
+ use super::super::*;
+
+ #[export_module]
+ pub mod functions {
+ #[rhai_fn(name = "+", return_raw)]
+ pub fn add(x: $arg_type, y: $arg_type) -> RhaiResultOf<$arg_type> {
+ if cfg!(not(feature = "unchecked")) {
+ x.checked_add(y).ok_or_else(|| make_err(format!("Addition overflow: {x} + {y}")))
+ } else {
+ Ok(x + y)
+ }
+ }
+ #[rhai_fn(name = "-", return_raw)]
+ pub fn subtract(x: $arg_type, y: $arg_type) -> RhaiResultOf<$arg_type> {
+ if cfg!(not(feature = "unchecked")) {
+ x.checked_sub(y).ok_or_else(|| make_err(format!("Subtraction overflow: {x} - {y}")))
+ } else {
+ Ok(x - y)
+ }
+ }
+ #[rhai_fn(name = "*", return_raw)]
+ pub fn multiply(x: $arg_type, y: $arg_type) -> RhaiResultOf<$arg_type> {
+ if cfg!(not(feature = "unchecked")) {
+ x.checked_mul(y).ok_or_else(|| make_err(format!("Multiplication overflow: {x} * {y}")))
+ } else {
+ Ok(x * y)
+ }
+ }
+ #[rhai_fn(name = "/", return_raw)]
+ pub fn divide(x: $arg_type, y: $arg_type) -> RhaiResultOf<$arg_type> {
+ if cfg!(not(feature = "unchecked")) {
+ // Detect division by zero
+ if y == 0 {
+ Err(make_err(format!("Division by zero: {x} / {y}")))
+ } else {
+ x.checked_div(y).ok_or_else(|| make_err(format!("Division overflow: {x} / {y}")))
+ }
+ } else {
+ Ok(x / y)
+ }
+ }
+ #[rhai_fn(name = "%", return_raw)]
+ pub fn modulo(x: $arg_type, y: $arg_type) -> RhaiResultOf<$arg_type> {
+ if cfg!(not(feature = "unchecked")) {
+ x.checked_rem(y).ok_or_else(|| make_err(format!("Modulo division by zero or overflow: {x} % {y}")))
+ } else {
+ Ok(x % y)
+ }
+ }
+ #[rhai_fn(name = "**", return_raw)]
+ pub fn power(x: $arg_type, y: INT) -> RhaiResultOf<$arg_type> {
+ if cfg!(not(feature = "unchecked")) {
+ if cfg!(not(feature = "only_i32")) && y > (u32::MAX as INT) {
+ Err(make_err(format!("Exponential overflow: {x} ** {y}")))
+ } else if y < 0 {
+ Err(make_err(format!("Integer raised to a negative power: {x} ** {y}")))
+ } else {
+ x.checked_pow(y as u32).ok_or_else(|| make_err(format!("Exponential overflow: {x} ** {y}")))
+ }
+ } else {
+ Ok(x.pow(y as u32))
+ }
+ }
+
+ #[rhai_fn(name = "<<")]
+ pub fn shift_left(x: $arg_type, y: INT) -> $arg_type {
+ if cfg!(not(feature = "unchecked")) {
+ if cfg!(not(feature = "only_i32")) && y > (u32::MAX as INT) {
+ 0
+ } else if y < 0 {
+ shift_right(x, y.checked_abs().unwrap_or(INT::MAX))
+ } else {
+ x.checked_shl(y as u32).unwrap_or_else(|| 0)
+ }
+ } else if y < 0 {
+ x >> -y
+ } else {
+ x << y
+ }
+ }
+ #[rhai_fn(name = ">>")]
+ pub fn shift_right(x: $arg_type, y: INT) -> $arg_type {
+ if cfg!(not(feature = "unchecked")) {
+ if cfg!(not(feature = "only_i32")) && y > (u32::MAX as INT) {
+ x.wrapping_shr(u32::MAX)
+ } else if y < 0 {
+ shift_left(x, y.checked_abs().unwrap_or(INT::MAX))
+ } else {
+ x.checked_shr(y as u32).unwrap_or_else(|| x.wrapping_shr(u32::MAX))
+ }
+ } else if y < 0 {
+ x << -y
+ } else {
+ x >> y
+ }
+ }
+ #[rhai_fn(name = "&")]
+ pub fn binary_and(x: $arg_type, y: $arg_type) -> $arg_type {
+ x & y
+ }
+ #[rhai_fn(name = "|")]
+ pub fn binary_or(x: $arg_type, y: $arg_type) -> $arg_type {
+ x | y
+ }
+ #[rhai_fn(name = "^")]
+ pub fn binary_xor(x: $arg_type, y: $arg_type) -> $arg_type {
+ x ^ y
+ }
+ /// Return true if the number is zero.
+ #[rhai_fn(get = "is_zero", name = "is_zero")]
+ pub fn is_zero(x: $arg_type) -> bool {
+ x == 0
+ }
+ /// Return true if the number is odd.
+ #[rhai_fn(get = "is_odd", name = "is_odd")]
+ pub fn is_odd(x: $arg_type) -> bool {
+ x % 2 != 0
+ }
+ /// Return true if the number is even.
+ #[rhai_fn(get = "is_even", name = "is_even")]
+ pub fn is_even(x: $arg_type) -> bool {
+ x % 2 == 0
+ }
+ }
+ })* }
+ }
+}
+
+macro_rules! gen_signed_functions {
+ ($root:ident => $($arg_type:ident),+) => {
+ pub mod $root { $(pub mod $arg_type {
+ use super::super::*;
+
+ #[export_module]
+ pub mod functions {
+ #[rhai_fn(name = "-", return_raw)]
+ pub fn neg(x: $arg_type) -> RhaiResultOf<$arg_type> {
+ if cfg!(not(feature = "unchecked")) {
+ x.checked_neg().ok_or_else(|| make_err(format!("Negation overflow: -{x}")))
+ } else {
+ Ok(-x)
+ }
+ }
+ #[rhai_fn(name = "+")]
+ pub fn plus(x: $arg_type) -> $arg_type {
+ x
+ }
+ /// Return the absolute value of the number.
+ #[rhai_fn(return_raw)]
+ pub fn abs(x: $arg_type) -> RhaiResultOf<$arg_type> {
+ if cfg!(not(feature = "unchecked")) {
+ x.checked_abs().ok_or_else(|| make_err(format!("Negation overflow: -{x}")))
+ } else {
+ Ok(x.abs())
+ }
+ }
+ /// Return the sign (as an integer) of the number according to the following:
+ ///
+ /// * `0` if the number is zero
+ /// * `1` if the number is positive
+ /// * `-1` if the number is negative
+ pub fn sign(x: $arg_type) -> INT {
+ x.signum() as INT
+ }
+ }
+ })* }
+ }
+}
+
+macro_rules! reg_functions {
+ ($mod_name:ident += $root:ident ; $($arg_type:ident),+ ) => { $(
+ combine_with_exported_module!($mod_name, "arithmetic", $root::$arg_type::functions);
+ )* }
+}
+
+def_package! {
+ /// Basic arithmetic package.
+ pub ArithmeticPackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ combine_with_exported_module!(lib, "int", int_functions);
+ reg_functions!(lib += signed_basic; INT);
+
+ #[cfg(not(feature = "only_i32"))]
+ #[cfg(not(feature = "only_i64"))]
+ {
+ reg_functions!(lib += arith_numbers; i8, u8, i16, u16, i32, u32, u64);
+ reg_functions!(lib += signed_numbers; i8, i16, i32);
+
+ #[cfg(not(target_family = "wasm"))]
+
+ {
+ reg_functions!(lib += arith_num_128; i128, u128);
+ reg_functions!(lib += signed_num_128; i128);
+ }
+ }
+
+ // Basic arithmetic for floating-point
+ #[cfg(not(feature = "no_float"))]
+ {
+ combine_with_exported_module!(lib, "f32", f32_functions);
+ combine_with_exported_module!(lib, "f64", f64_functions);
+ }
+
+ // Decimal functions
+ #[cfg(feature = "decimal")]
+ combine_with_exported_module!(lib, "decimal", decimal_functions);
+ }
+}
+
+#[export_module]
+mod int_functions {
+ /// Return true if the number is zero.
+ #[rhai_fn(get = "is_zero", name = "is_zero")]
+ pub fn is_zero(x: INT) -> bool {
+ x == 0
+ }
+ /// Return true if the number is odd.
+ #[rhai_fn(get = "is_odd", name = "is_odd")]
+ pub fn is_odd(x: INT) -> bool {
+ x % 2 != 0
+ }
+ /// Return true if the number is even.
+ #[rhai_fn(get = "is_even", name = "is_even")]
+ pub fn is_even(x: INT) -> bool {
+ x % 2 == 0
+ }
+}
+
+gen_arithmetic_functions!(arith_basic => INT);
+
+#[cfg(not(feature = "only_i32"))]
+#[cfg(not(feature = "only_i64"))]
+gen_arithmetic_functions!(arith_numbers => i8, u8, i16, u16, i32, u32, u64);
+
+#[cfg(not(feature = "only_i32"))]
+#[cfg(not(feature = "only_i64"))]
+#[cfg(not(target_family = "wasm"))]
+
+gen_arithmetic_functions!(arith_num_128 => i128, u128);
+
+gen_signed_functions!(signed_basic => INT);
+
+#[cfg(not(feature = "only_i32"))]
+#[cfg(not(feature = "only_i64"))]
+gen_signed_functions!(signed_numbers => i8, i16, i32);
+
+#[cfg(not(feature = "only_i32"))]
+#[cfg(not(feature = "only_i64"))]
+#[cfg(not(target_family = "wasm"))]
+
+gen_signed_functions!(signed_num_128 => i128);
+
+#[cfg(not(feature = "no_float"))]
+#[export_module]
+mod f32_functions {
+ #[cfg(not(feature = "f32_float"))]
+ #[allow(clippy::cast_precision_loss)]
+ pub mod basic_arithmetic {
+ #[rhai_fn(name = "+")]
+ pub fn add(x: f32, y: f32) -> f32 {
+ x + y
+ }
+ #[rhai_fn(name = "-")]
+ pub fn subtract(x: f32, y: f32) -> f32 {
+ x - y
+ }
+ #[rhai_fn(name = "*")]
+ pub fn multiply(x: f32, y: f32) -> f32 {
+ x * y
+ }
+ #[rhai_fn(name = "/")]
+ pub fn divide(x: f32, y: f32) -> f32 {
+ x / y
+ }
+ #[rhai_fn(name = "%")]
+ pub fn modulo(x: f32, y: f32) -> f32 {
+ x % y
+ }
+ #[rhai_fn(name = "**")]
+ pub fn pow_f_f(x: f32, y: f32) -> f32 {
+ x.powf(y)
+ }
+
+ #[rhai_fn(name = "+")]
+ pub fn add_if(x: INT, y: f32) -> f32 {
+ (x as f32) + (y as f32)
+ }
+ #[rhai_fn(name = "+")]
+ pub fn add_fi(x: f32, y: INT) -> f32 {
+ (x as f32) + (y as f32)
+ }
+ #[rhai_fn(name = "-")]
+ pub fn subtract_if(x: INT, y: f32) -> f32 {
+ (x as f32) - (y as f32)
+ }
+ #[rhai_fn(name = "-")]
+ pub fn subtract_fi(x: f32, y: INT) -> f32 {
+ (x as f32) - (y as f32)
+ }
+ #[rhai_fn(name = "*")]
+ pub fn multiply_if(x: INT, y: f32) -> f32 {
+ (x as f32) * (y as f32)
+ }
+ #[rhai_fn(name = "*")]
+ pub fn multiply_fi(x: f32, y: INT) -> f32 {
+ (x as f32) * (y as f32)
+ }
+ #[rhai_fn(name = "/")]
+ pub fn divide_if(x: INT, y: f32) -> f32 {
+ (x as f32) / (y as f32)
+ }
+ #[rhai_fn(name = "/")]
+ pub fn divide_fi(x: f32, y: INT) -> f32 {
+ (x as f32) / (y as f32)
+ }
+ #[rhai_fn(name = "%")]
+ pub fn modulo_if(x: INT, y: f32) -> f32 {
+ (x as f32) % (y as f32)
+ }
+ #[rhai_fn(name = "%")]
+ pub fn modulo_fi(x: f32, y: INT) -> f32 {
+ (x as f32) % (y as f32)
+ }
+ }
+
+ #[rhai_fn(name = "-")]
+ pub fn neg(x: f32) -> f32 {
+ -x
+ }
+ #[rhai_fn(name = "+")]
+ pub fn plus(x: f32) -> f32 {
+ x
+ }
+ /// Return the absolute value of the floating-point number.
+ pub fn abs(x: f32) -> f32 {
+ x.abs()
+ }
+ /// Return the sign (as an integer) of the floating-point number according to the following:
+ ///
+ /// * `0` if the number is zero
+ /// * `1` if the number is positive
+ /// * `-1` if the number is negative
+ #[rhai_fn(return_raw)]
+ pub fn sign(x: f32) -> RhaiResultOf<INT> {
+ match x.signum() {
+ _ if x == 0.0 => Ok(0),
+ x if x.is_nan() => Err(make_err("Sign of NaN is undefined")),
+ x => Ok(x as INT),
+ }
+ }
+ /// Return true if the floating-point number is zero.
+ #[rhai_fn(get = "is_zero", name = "is_zero")]
+ pub fn is_zero(x: f32) -> bool {
+ x == 0.0
+ }
+ #[rhai_fn(name = "**", return_raw)]
+ pub fn pow_f_i(x: f32, y: INT) -> RhaiResultOf<f32> {
+ if cfg!(not(feature = "unchecked")) && y > (i32::MAX as INT) {
+ Err(make_err(format!(
+ "Number raised to too large an index: {x} ** {y}"
+ )))
+ } else {
+ #[allow(clippy::cast_possible_truncation)]
+ Ok(x.powi(y as i32))
+ }
+ }
+}
+
+#[cfg(not(feature = "no_float"))]
+#[export_module]
+mod f64_functions {
+ #[cfg(feature = "f32_float")]
+ pub mod basic_arithmetic {
+ #[rhai_fn(name = "+")]
+ pub fn add(x: f64, y: f64) -> f64 {
+ x + y
+ }
+ #[rhai_fn(name = "-")]
+ pub fn subtract(x: f64, y: f64) -> f64 {
+ x - y
+ }
+ #[rhai_fn(name = "*")]
+ pub fn multiply(x: f64, y: f64) -> f64 {
+ x * y
+ }
+ #[rhai_fn(name = "/")]
+ pub fn divide(x: f64, y: f64) -> f64 {
+ x / y
+ }
+ #[rhai_fn(name = "%")]
+ pub fn modulo(x: f64, y: f64) -> f64 {
+ x % y
+ }
+ #[rhai_fn(name = "**")]
+ pub fn pow_f_f(x: f64, y: f64) -> f64 {
+ x.powf(y)
+ }
+
+ #[rhai_fn(name = "+")]
+ pub fn add_if(x: INT, y: f64) -> f64 {
+ (x as f64) + (y as f64)
+ }
+ #[rhai_fn(name = "+")]
+ pub fn add_fi(x: f64, y: INT) -> f64 {
+ (x as f64) + (y as f64)
+ }
+ #[rhai_fn(name = "-")]
+ pub fn subtract_if(x: INT, y: f64) -> f64 {
+ (x as f64) - (y as f64)
+ }
+ #[rhai_fn(name = "-")]
+ pub fn subtract_fi(x: f64, y: INT) -> f64 {
+ (x as f64) - (y as f64)
+ }
+ #[rhai_fn(name = "*")]
+ pub fn multiply_if(x: INT, y: f64) -> f64 {
+ (x as f64) * (y as f64)
+ }
+ #[rhai_fn(name = "*")]
+ pub fn multiply_fi(x: f64, y: INT) -> f64 {
+ (x as f64) * (y as f64)
+ }
+ #[rhai_fn(name = "/")]
+ pub fn divide_if(x: INT, y: f64) -> f64 {
+ (x as f64) / (y as f64)
+ }
+ #[rhai_fn(name = "/")]
+ pub fn divide_fi(x: f64, y: INT) -> f64 {
+ (x as f64) / (y as f64)
+ }
+ #[rhai_fn(name = "%")]
+ pub fn modulo_if(x: INT, y: f64) -> f64 {
+ (x as f64) % (y as f64)
+ }
+ #[rhai_fn(name = "%")]
+ pub fn modulo_fi(x: f64, y: INT) -> f64 {
+ (x as f64) % (y as f64)
+ }
+ }
+
+ #[rhai_fn(name = "-")]
+ pub fn neg(x: f64) -> f64 {
+ -x
+ }
+ #[rhai_fn(name = "+")]
+ pub fn plus(x: f64) -> f64 {
+ x
+ }
+ /// Return the absolute value of the floating-point number.
+ pub fn abs(x: f64) -> f64 {
+ x.abs()
+ }
+ /// Return the sign (as an integer) of the floating-point number according to the following:
+ ///
+ /// * `0` if the number is zero
+ /// * `1` if the number is positive
+ /// * `-1` if the number is negative
+ #[rhai_fn(return_raw)]
+ pub fn sign(x: f64) -> RhaiResultOf<INT> {
+ match x.signum() {
+ _ if x == 0.0 => Ok(0),
+ x if x.is_nan() => Err(make_err("Sign of NaN is undefined")),
+ x => Ok(x as INT),
+ }
+ }
+ /// Return true if the floating-point number is zero.
+ #[rhai_fn(get = "is_zero", name = "is_zero")]
+ pub fn is_zero(x: f64) -> bool {
+ x == 0.0
+ }
+}
+
+#[cfg(feature = "decimal")]
+#[export_module]
+pub mod decimal_functions {
+ use rust_decimal::{prelude::Zero, Decimal};
+
+ #[cfg(not(feature = "unchecked"))]
+ pub mod builtin {
+ use rust_decimal::MathematicalOps;
+
+ #[rhai_fn(return_raw)]
+ pub fn add(x: Decimal, y: Decimal) -> RhaiResultOf<Decimal> {
+ x.checked_add(y)
+ .ok_or_else(|| make_err(format!("Addition overflow: {x} + {y}")))
+ }
+ #[rhai_fn(return_raw)]
+ pub fn subtract(x: Decimal, y: Decimal) -> RhaiResultOf<Decimal> {
+ x.checked_sub(y)
+ .ok_or_else(|| make_err(format!("Subtraction overflow: {x} - {y}")))
+ }
+ #[rhai_fn(return_raw)]
+ pub fn multiply(x: Decimal, y: Decimal) -> RhaiResultOf<Decimal> {
+ x.checked_mul(y)
+ .ok_or_else(|| make_err(format!("Multiplication overflow: {x} * {y}")))
+ }
+ #[rhai_fn(return_raw)]
+ pub fn divide(x: Decimal, y: Decimal) -> RhaiResultOf<Decimal> {
+ // Detect division by zero
+ if y == Decimal::zero() {
+ Err(make_err(format!("Division by zero: {x} / {y}")))
+ } else {
+ x.checked_div(y)
+ .ok_or_else(|| make_err(format!("Division overflow: {x} / {y}")))
+ }
+ }
+ #[rhai_fn(return_raw)]
+ pub fn modulo(x: Decimal, y: Decimal) -> RhaiResultOf<Decimal> {
+ x.checked_rem(y)
+ .ok_or_else(|| make_err(format!("Modulo division by zero or overflow: {x} % {y}")))
+ }
+ #[rhai_fn(return_raw)]
+ pub fn power(x: Decimal, y: Decimal) -> RhaiResultOf<Decimal> {
+ x.checked_powd(y)
+ .ok_or_else(|| make_err(format!("Exponential overflow: {x} ** {y}")))
+ }
+ }
+ #[rhai_fn(name = "-")]
+ pub fn neg(x: Decimal) -> Decimal {
+ -x
+ }
+ #[rhai_fn(name = "+")]
+ pub fn plus(x: Decimal) -> Decimal {
+ x
+ }
+ /// Return the absolute value of the decimal number.
+ pub fn abs(x: Decimal) -> Decimal {
+ x.abs()
+ }
+ /// Return the sign (as an integer) of the decimal number according to the following:
+ ///
+ /// * `0` if the number is zero
+ /// * `1` if the number is positive
+ /// * `-1` if the number is negative
+ pub fn sign(x: Decimal) -> INT {
+ if x == Decimal::zero() {
+ 0
+ } else if x.is_sign_negative() {
+ -1
+ } else {
+ 1
+ }
+ }
+ /// Return true if the decimal number is zero.
+ #[rhai_fn(get = "is_zero", name = "is_zero")]
+ pub fn is_zero(x: Decimal) -> bool {
+ x.is_zero()
+ }
+}
diff --git a/rhai/src/packages/array_basic.rs b/rhai/src/packages/array_basic.rs
new file mode 100644
index 0000000..6e2c208
--- /dev/null
+++ b/rhai/src/packages/array_basic.rs
@@ -0,0 +1,1959 @@
+#![cfg(not(feature = "no_index"))]
+
+use crate::api::deprecated::deprecated_array_functions;
+use crate::engine::OP_EQUALS;
+use crate::eval::{calc_index, calc_offset_len};
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+
+use crate::{
+ def_package, Array, Dynamic, ExclusiveRange, FnPtr, InclusiveRange, NativeCallContext,
+ Position, RhaiResultOf, StaticVec, ERR, INT, MAX_USIZE_INT,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{any::TypeId, cmp::Ordering, mem};
+
+def_package! {
+ /// Package of basic array utilities.
+ pub BasicArrayPackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ combine_with_exported_module!(lib, "array", array_functions);
+ combine_with_exported_module!(lib, "deprecated_array", deprecated_array_functions);
+
+ // Register array iterator
+ lib.set_iterable::<Array>();
+ }
+}
+
+#[export_module]
+pub mod array_functions {
+ /// Number of elements in the array.
+ #[rhai_fn(name = "len", get = "len", pure)]
+ pub fn len(array: &mut Array) -> INT {
+ array.len() as INT
+ }
+ /// Return true if the array is empty.
+ #[rhai_fn(name = "is_empty", get = "is_empty", pure)]
+ pub fn is_empty(array: &mut Array) -> bool {
+ array.len() == 0
+ }
+ /// Get a copy of the element at the `index` position in the array.
+ ///
+ /// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `index` < -length of array, `()` is returned.
+ /// * If `index` ≥ length of array, `()` is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3];
+ ///
+ /// print(x.get(0)); // prints 1
+ ///
+ /// print(x.get(-1)); // prints 3
+ ///
+ /// print(x.get(99)); // prints empty (for '()')
+ /// ```
+ pub fn get(array: &mut Array, index: INT) -> Dynamic {
+ if array.is_empty() {
+ return Dynamic::UNIT;
+ }
+
+ let (index, ..) = calc_offset_len(array.len(), index, 0);
+
+ if index >= array.len() {
+ Dynamic::UNIT
+ } else {
+ array[index].clone()
+ }
+ }
+ /// Set the element at the `index` position in the array to a new `value`.
+ ///
+ /// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `index` < -length of array, the array is not modified.
+ /// * If `index` ≥ length of array, the array is not modified.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3];
+ ///
+ /// x.set(0, 42);
+ ///
+ /// print(x); // prints "[42, 2, 3]"
+ ///
+ /// x.set(-3, 0);
+ ///
+ /// print(x); // prints "[0, 2, 3]"
+ ///
+ /// x.set(99, 123);
+ ///
+ /// print(x); // prints "[0, 2, 3]"
+ /// ```
+ pub fn set(array: &mut Array, index: INT, value: Dynamic) {
+ if array.is_empty() {
+ return;
+ }
+
+ let (index, ..) = calc_offset_len(array.len(), index, 0);
+
+ if index < array.len() {
+ array[index] = value;
+ }
+ }
+ /// Add a new element, which is not another array, to the end of the array.
+ ///
+ /// If `item` is `Array`, then `append` is more specific and will be called instead.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3];
+ ///
+ /// x.push("hello");
+ ///
+ /// print(x); // prints [1, 2, 3, "hello"]
+ /// ```
+ pub fn push(array: &mut Array, item: Dynamic) {
+ array.push(item);
+ }
+ /// Add all the elements of another array to the end of the array.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3];
+ /// let y = [true, 'x'];
+ ///
+ /// x.append(y);
+ ///
+ /// print(x); // prints "[1, 2, 3, true, 'x']"
+ /// ```
+ pub fn append(array: &mut Array, new_array: Array) {
+ if !new_array.is_empty() {
+ if array.is_empty() {
+ *array = new_array;
+ } else {
+ array.extend(new_array);
+ }
+ }
+ }
+ /// Combine two arrays into a new array and return it.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3];
+ /// let y = [true, 'x'];
+ ///
+ /// print(x + y); // prints "[1, 2, 3, true, 'x']"
+ ///
+ /// print(x); // prints "[1, 2, 3"
+ /// ```
+ #[rhai_fn(name = "+")]
+ pub fn concat(array1: Array, array2: Array) -> Array {
+ if array2.is_empty() {
+ array1
+ } else if array1.is_empty() {
+ array2
+ } else {
+ let mut array = array1;
+ array.extend(array2);
+ array
+ }
+ }
+ /// Add a new element into the array at a particular `index` position.
+ ///
+ /// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `index` < -length of array, the element is added to the beginning of the array.
+ /// * If `index` ≥ length of array, the element is appended to the end of the array.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3];
+ ///
+ /// x.insert(0, "hello");
+ ///
+ /// x.insert(2, true);
+ ///
+ /// x.insert(-2, 42);
+ ///
+ /// print(x); // prints ["hello", 1, true, 2, 42, 3]
+ /// ```
+ pub fn insert(array: &mut Array, index: INT, item: Dynamic) {
+ if array.is_empty() {
+ array.push(item);
+ return;
+ }
+
+ let (index, ..) = calc_offset_len(array.len(), index, 0);
+
+ if index >= array.len() {
+ array.push(item);
+ } else {
+ array.insert(index, item);
+ }
+ }
+ /// Pad the array to at least the specified length with copies of a specified element.
+ ///
+ /// If `len` ≤ length of array, no padding is done.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3];
+ ///
+ /// x.pad(5, 42);
+ ///
+ /// print(x); // prints "[1, 2, 3, 42, 42]"
+ ///
+ /// x.pad(3, 123);
+ ///
+ /// print(x); // prints "[1, 2, 3, 42, 42]"
+ /// ```
+ #[rhai_fn(return_raw)]
+ pub fn pad(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ len: INT,
+ item: Dynamic,
+ ) -> RhaiResultOf<()> {
+ if len <= 0 {
+ return Ok(());
+ }
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let len = len.min(MAX_USIZE_INT) as usize;
+
+ if len <= array.len() {
+ return Ok(());
+ }
+
+ let _ctx = ctx;
+
+ // Check if array will be over max size limit
+ #[cfg(not(feature = "unchecked"))]
+ if _ctx.engine().max_array_size() > 0 {
+ let pad = len - array.len();
+ let (a, m, s) = crate::eval::calc_array_sizes(array);
+ let (ax, mx, sx) = item.calc_data_sizes(true);
+
+ _ctx.engine()
+ .throw_on_size((a + pad + ax * pad, m + mx * pad, s + sx * pad))?;
+ }
+
+ array.resize(len, item);
+
+ Ok(())
+ }
+ /// Remove the last element from the array and return it.
+ ///
+ /// If the array is empty, `()` is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3];
+ ///
+ /// print(x.pop()); // prints 3
+ ///
+ /// print(x); // prints "[1, 2]"
+ /// ```
+ pub fn pop(array: &mut Array) -> Dynamic {
+ if array.is_empty() {
+ Dynamic::UNIT
+ } else {
+ array.pop().unwrap_or(Dynamic::UNIT)
+ }
+ }
+ /// Remove the first element from the array and return it.
+ ///
+ /// If the array is empty, `()` is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3];
+ ///
+ /// print(x.shift()); // prints 1
+ ///
+ /// print(x); // prints "[2, 3]"
+ /// ```
+ pub fn shift(array: &mut Array) -> Dynamic {
+ if array.is_empty() {
+ Dynamic::UNIT
+ } else {
+ array.remove(0)
+ }
+ }
+ /// Remove the element at the specified `index` from the array and return it.
+ ///
+ /// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `index` < -length of array, `()` is returned.
+ /// * If `index` ≥ length of array, `()` is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3];
+ ///
+ /// print(x.remove(1)); // prints 2
+ ///
+ /// print(x); // prints "[1, 3]"
+ ///
+ /// print(x.remove(-2)); // prints 1
+ ///
+ /// print(x); // prints "[3]"
+ /// ```
+ pub fn remove(array: &mut Array, index: INT) -> Dynamic {
+ let index = match calc_index(array.len(), index, true, || Err(())) {
+ Ok(n) => n,
+ Err(_) => return Dynamic::UNIT,
+ };
+
+ array.remove(index)
+ }
+ /// Clear the array.
+ pub fn clear(array: &mut Array) {
+ if !array.is_empty() {
+ array.clear();
+ }
+ }
+ /// Cut off the array at the specified length.
+ ///
+ /// * If `len` ≤ 0, the array is cleared.
+ /// * If `len` ≥ length of array, the array is not truncated.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// x.truncate(3);
+ ///
+ /// print(x); // prints "[1, 2, 3]"
+ ///
+ /// x.truncate(10);
+ ///
+ /// print(x); // prints "[1, 2, 3]"
+ /// ```
+ pub fn truncate(array: &mut Array, len: INT) {
+ if len <= 0 {
+ array.clear();
+ return;
+ }
+ if !array.is_empty() {
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let len = len.min(MAX_USIZE_INT) as usize;
+
+ if len > 0 {
+ array.truncate(len);
+ } else {
+ array.clear();
+ }
+ }
+ }
+ /// Cut off the head of the array, leaving a tail of the specified length.
+ ///
+ /// * If `len` ≤ 0, the array is cleared.
+ /// * If `len` ≥ length of array, the array is not modified.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// x.chop(3);
+ ///
+ /// print(x); // prints "[3, 4, 5]"
+ ///
+ /// x.chop(10);
+ ///
+ /// print(x); // prints "[3, 4, 5]"
+ /// ```
+ pub fn chop(array: &mut Array, len: INT) {
+ if len <= 0 {
+ array.clear();
+ return;
+ }
+ if !array.is_empty() {
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let len = len.min(MAX_USIZE_INT) as usize;
+
+ if len <= 0 {
+ array.clear();
+ } else if len < array.len() {
+ array.drain(0..array.len() - len);
+ }
+ }
+ }
+ /// Reverse all the elements in the array.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// x.reverse();
+ ///
+ /// print(x); // prints "[5, 4, 3, 2, 1]"
+ /// ```
+ pub fn reverse(array: &mut Array) {
+ if !array.is_empty() {
+ array.reverse();
+ }
+ }
+ /// Replace an exclusive range of the array with another array.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ /// let y = [7, 8, 9, 10];
+ ///
+ /// x.splice(1..3, y);
+ ///
+ /// print(x); // prints "[1, 7, 8, 9, 10, 4, 5]"
+ /// ```
+ #[rhai_fn(name = "splice")]
+ pub fn splice_range(array: &mut Array, range: ExclusiveRange, replace: Array) {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ splice(array, start, end - start, replace);
+ }
+ /// Replace an inclusive range of the array with another array.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ /// let y = [7, 8, 9, 10];
+ ///
+ /// x.splice(1..=3, y);
+ ///
+ /// print(x); // prints "[1, 7, 8, 9, 10, 5]"
+ /// ```
+ #[rhai_fn(name = "splice")]
+ pub fn splice_inclusive_range(array: &mut Array, range: InclusiveRange, replace: Array) {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ splice(array, start, end - start + 1, replace);
+ }
+ /// Replace a portion of the array with another array.
+ ///
+ /// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `start` < -length of array, position counts from the beginning of the array.
+ /// * If `start` ≥ length of array, the other array is appended to the end of the array.
+ /// * If `len` ≤ 0, the other array is inserted into the array at the `start` position without replacing any element.
+ /// * If `start` position + `len` ≥ length of array, entire portion of the array after the `start` position is replaced.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ /// let y = [7, 8, 9, 10];
+ ///
+ /// x.splice(1, 2, y);
+ ///
+ /// print(x); // prints "[1, 7, 8, 9, 10, 4, 5]"
+ ///
+ /// x.splice(-5, 4, y);
+ ///
+ /// print(x); // prints "[1, 7, 7, 8, 9, 10, 5]"
+ /// ```
+ pub fn splice(array: &mut Array, start: INT, len: INT, replace: Array) {
+ if array.is_empty() {
+ *array = replace;
+ return;
+ }
+
+ let (start, len) = calc_offset_len(array.len(), start, len);
+
+ if start >= array.len() {
+ array.extend(replace);
+ } else {
+ array.splice(start..start + len, replace);
+ }
+ }
+ /// Copy an exclusive range of the array and return it as a new array.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// print(x.extract(1..3)); // prints "[2, 3]"
+ ///
+ /// print(x); // prints "[1, 2, 3, 4, 5]"
+ /// ```
+ #[rhai_fn(name = "extract")]
+ pub fn extract_range(array: &mut Array, range: ExclusiveRange) -> Array {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ extract(array, start, end - start)
+ }
+ /// Copy an inclusive range of the array and return it as a new array.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// print(x.extract(1..=3)); // prints "[2, 3, 4]"
+ ///
+ /// print(x); // prints "[1, 2, 3, 4, 5]"
+ /// ```
+ #[rhai_fn(name = "extract")]
+ pub fn extract_inclusive_range(array: &mut Array, range: InclusiveRange) -> Array {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ extract(array, start, end - start + 1)
+ }
+ /// Copy a portion of the array and return it as a new array.
+ ///
+ /// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `start` < -length of array, position counts from the beginning of the array.
+ /// * If `start` ≥ length of array, an empty array is returned.
+ /// * If `len` ≤ 0, an empty array is returned.
+ /// * If `start` position + `len` ≥ length of array, entire portion of the array after the `start` position is copied and returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// print(x.extract(1, 3)); // prints "[2, 3, 4]"
+ ///
+ /// print(x.extract(-3, 2)); // prints "[3, 4]"
+ ///
+ /// print(x); // prints "[1, 2, 3, 4, 5]"
+ /// ```
+ pub fn extract(array: &mut Array, start: INT, len: INT) -> Array {
+ if array.is_empty() || len <= 0 {
+ return Array::new();
+ }
+
+ let (start, len) = calc_offset_len(array.len(), start, len);
+
+ if len == 0 {
+ Array::new()
+ } else {
+ array[start..start + len].to_vec()
+ }
+ }
+ /// Copy a portion of the array beginning at the `start` position till the end and return it as
+ /// a new array.
+ ///
+ /// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `start` < -length of array, the entire array is copied and returned.
+ /// * If `start` ≥ length of array, an empty array is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// print(x.extract(2)); // prints "[3, 4, 5]"
+ ///
+ /// print(x.extract(-3)); // prints "[3, 4, 5]"
+ ///
+ /// print(x); // prints "[1, 2, 3, 4, 5]"
+ /// ```
+ #[rhai_fn(name = "extract")]
+ pub fn extract_tail(array: &mut Array, start: INT) -> Array {
+ extract(array, start, INT::MAX)
+ }
+ /// Cut off the array at `index` and return it as a new array.
+ ///
+ /// * If `index` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `index` is zero, the entire array is cut and returned.
+ /// * If `index` < -length of array, the entire array is cut and returned.
+ /// * If `index` ≥ length of array, nothing is cut from the array and an empty array is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.split(2);
+ ///
+ /// print(y); // prints "[3, 4, 5]"
+ ///
+ /// print(x); // prints "[1, 2]"
+ /// ```
+ #[rhai_fn(name = "split")]
+ pub fn split_at(array: &mut Array, index: INT) -> Array {
+ if array.is_empty() {
+ return Array::new();
+ }
+
+ let (start, len) = calc_offset_len(array.len(), index, INT::MAX);
+
+ if start == 0 {
+ if len >= array.len() {
+ mem::take(array)
+ } else {
+ let mut result = Array::new();
+ result.extend(array.drain(array.len() - len..));
+ result
+ }
+ } else if start >= array.len() {
+ Array::new()
+ } else {
+ let mut result = Array::new();
+ result.extend(array.drain(start as usize..));
+ result
+ }
+ }
+
+ /// Iterate through all the elements in the array, applying a `process` function to each element in turn.
+ /// Each element is bound to `this` before calling the function.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `this`: bound to array element (mutable)
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// x.for_each(|| this *= this);
+ ///
+ /// print(x); // prints "[1, 4, 9, 16, 25]"
+ ///
+ /// x.for_each(|i| this *= i);
+ ///
+ /// print(x); // prints "[0, 2, 6, 12, 20]"
+ /// ```
+ #[rhai_fn(return_raw)]
+ pub fn for_each(ctx: NativeCallContext, array: &mut Array, map: FnPtr) -> RhaiResultOf<()> {
+ if array.is_empty() {
+ return Ok(());
+ }
+
+ for (i, item) in array.iter_mut().enumerate() {
+ let ex = [(i as INT).into()];
+
+ let _ = map.call_raw_with_extra_args("map", &ctx, Some(item), [], ex, None)?;
+ }
+
+ Ok(())
+ }
+
+ /// Iterate through all the elements in the array, applying a `mapper` function to each element
+ /// in turn, and return the results as a new array.
+ ///
+ /// # No Function Parameter
+ ///
+ /// Array element (mutable) is bound to `this`.
+ ///
+ /// This method is marked _pure_; the `mapper` function should not mutate array elements.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.map(|v| v * v);
+ ///
+ /// print(y); // prints "[1, 4, 9, 16, 25]"
+ ///
+ /// let y = x.map(|v, i| v * i);
+ ///
+ /// print(y); // prints "[0, 2, 6, 12, 20]"
+ /// ```
+ #[rhai_fn(return_raw, pure)]
+ pub fn map(ctx: NativeCallContext, array: &mut Array, map: FnPtr) -> RhaiResultOf<Array> {
+ if array.is_empty() {
+ return Ok(Array::new());
+ }
+
+ let mut ar = Array::with_capacity(array.len());
+
+ for (i, item) in array.iter_mut().enumerate() {
+ let ex = [(i as INT).into()];
+ ar.push(map.call_raw_with_extra_args("map", &ctx, Some(item), [], ex, Some(0))?);
+ }
+
+ Ok(ar)
+ }
+
+ /// Iterate through all the elements in the array, applying a `filter` function to each element
+ /// in turn, and return a copy of all elements (in order) that return `true` as a new array.
+ ///
+ /// # No Function Parameter
+ ///
+ /// Array element (mutable) is bound to `this`.
+ ///
+ /// This method is marked _pure_; the `filter` function should not mutate array elements.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.filter(|v| v >= 3);
+ ///
+ /// print(y); // prints "[3, 4, 5]"
+ ///
+ /// let y = x.filter(|v, i| v * i >= 10);
+ ///
+ /// print(y); // prints "[12, 20]"
+ /// ```
+ #[rhai_fn(return_raw, pure)]
+ pub fn filter(ctx: NativeCallContext, array: &mut Array, filter: FnPtr) -> RhaiResultOf<Array> {
+ if array.is_empty() {
+ return Ok(Array::new());
+ }
+
+ let mut ar = Array::new();
+
+ for (i, item) in array.iter_mut().enumerate() {
+ let ex = [(i as INT).into()];
+
+ if filter
+ .call_raw_with_extra_args("filter", &ctx, Some(item), [], ex, Some(0))?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ ar.push(item.clone());
+ }
+ }
+
+ Ok(ar)
+ }
+ /// Return `true` if the array contains an element that equals `value`.
+ ///
+ /// The operator `==` is used to compare elements with `value` and must be defined,
+ /// otherwise `false` is assumed.
+ ///
+ /// This function also drives the `in` operator.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// // The 'in' operator calls 'contains' in the background
+ /// if 4 in x {
+ /// print("found!");
+ /// }
+ /// ```
+ #[rhai_fn(return_raw, pure)]
+ pub fn contains(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ value: Dynamic,
+ ) -> RhaiResultOf<bool> {
+ if array.is_empty() {
+ return Ok(false);
+ }
+
+ for item in array {
+ if ctx
+ .call_native_fn_raw(OP_EQUALS, true, &mut [item, &mut value.clone()])
+ .or_else(|err| match *err {
+ ERR::ErrorFunctionNotFound(ref fn_sig, ..) if fn_sig.starts_with(OP_EQUALS) => {
+ if item.type_id() == value.type_id() {
+ // No default when comparing same type
+ Err(err)
+ } else {
+ Ok(Dynamic::FALSE)
+ }
+ }
+ _ => Err(err),
+ })?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ return Ok(true);
+ }
+ }
+
+ Ok(false)
+ }
+ /// Find the first element in the array that equals a particular `value` and return its index.
+ /// If no element equals `value`, `-1` is returned.
+ ///
+ /// The operator `==` is used to compare elements with `value` and must be defined,
+ /// otherwise `false` is assumed.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+ ///
+ /// print(x.index_of(4)); // prints 3 (first index)
+ ///
+ /// print(x.index_of(9)); // prints -1
+ ///
+ /// print(x.index_of("foo")); // prints -1: strings do not equal numbers
+ /// ```
+ #[rhai_fn(return_raw, pure)]
+ pub fn index_of(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ value: Dynamic,
+ ) -> RhaiResultOf<INT> {
+ if array.is_empty() {
+ Ok(-1)
+ } else {
+ index_of_starting_from(ctx, array, value, 0)
+ }
+ }
+ /// Find the first element in the array, starting from a particular `start` position, that
+ /// equals a particular `value` and return its index. If no element equals `value`, `-1` is returned.
+ ///
+ /// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `start` < -length of array, position counts from the beginning of the array.
+ /// * If `start` ≥ length of array, `-1` is returned.
+ ///
+ /// The operator `==` is used to compare elements with `value` and must be defined,
+ /// otherwise `false` is assumed.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+ ///
+ /// print(x.index_of(4, 2)); // prints 3
+ ///
+ /// print(x.index_of(4, 5)); // prints 7
+ ///
+ /// print(x.index_of(4, 15)); // prints -1: nothing found past end of array
+ ///
+ /// print(x.index_of(4, -5)); // prints 11: -5 = start from index 8
+ ///
+ /// print(x.index_of(9, 1)); // prints -1: nothing equals 9
+ ///
+ /// print(x.index_of("foo", 1)); // prints -1: strings do not equal numbers
+ /// ```
+ #[rhai_fn(name = "index_of", return_raw, pure)]
+ pub fn index_of_starting_from(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ value: Dynamic,
+ start: INT,
+ ) -> RhaiResultOf<INT> {
+ if array.is_empty() {
+ return Ok(-1);
+ }
+
+ let (start, ..) = calc_offset_len(array.len(), start, 0);
+
+ for (i, item) in array.iter_mut().enumerate().skip(start) {
+ if ctx
+ .call_native_fn_raw(OP_EQUALS, true, &mut [item, &mut value.clone()])
+ .or_else(|err| match *err {
+ ERR::ErrorFunctionNotFound(ref fn_sig, ..) if fn_sig.starts_with(OP_EQUALS) => {
+ if item.type_id() == value.type_id() {
+ // No default when comparing same type
+ Err(err)
+ } else {
+ Ok(Dynamic::FALSE)
+ }
+ }
+ _ => Err(err),
+ })?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ return Ok(i as INT);
+ }
+ }
+
+ Ok(-1 as INT)
+ }
+ /// Iterate through all the elements in the array, applying a `filter` function to each element
+ /// in turn, and return the index of the first element that returns `true`.
+ /// If no element returns `true`, `-1` is returned.
+ ///
+ /// # No Function Parameter
+ ///
+ /// Array element (mutable) is bound to `this`.
+ ///
+ /// This method is marked _pure_; the `filter` function should not mutate array elements.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+ ///
+ /// print(x.index_of(|v| v > 3)); // prints 3: 4 > 3
+ ///
+ /// print(x.index_of(|v| v > 8)); // prints -1: nothing is > 8
+ ///
+ /// print(x.index_of(|v, i| v * i > 20)); // prints 7: 4 * 7 > 20
+ /// ```
+ #[rhai_fn(name = "index_of", return_raw, pure)]
+ pub fn index_of_filter(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ filter: FnPtr,
+ ) -> RhaiResultOf<INT> {
+ if array.is_empty() {
+ Ok(-1)
+ } else {
+ index_of_filter_starting_from(ctx, array, filter, 0)
+ }
+ }
+ /// Iterate through all the elements in the array, starting from a particular `start` position,
+ /// applying a `filter` function to each element in turn, and return the index of the first
+ /// element that returns `true`. If no element returns `true`, `-1` is returned.
+ ///
+ /// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `start` < -length of array, position counts from the beginning of the array.
+ /// * If `start` ≥ length of array, `-1` is returned.
+ ///
+ /// # No Function Parameter
+ ///
+ /// Array element (mutable) is bound to `this`.
+ ///
+ /// This method is marked _pure_; the `filter` function should not mutate array elements.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+ ///
+ /// print(x.index_of(|v| v > 1, 3)); // prints 5: 2 > 1
+ ///
+ /// print(x.index_of(|v| v < 2, 9)); // prints -1: nothing < 2 past index 9
+ ///
+ /// print(x.index_of(|v| v > 1, 15)); // prints -1: nothing found past end of array
+ ///
+ /// print(x.index_of(|v| v > 1, -5)); // prints 9: -5 = start from index 8
+ ///
+ /// print(x.index_of(|v| v > 1, -99)); // prints 1: -99 = start from beginning
+ ///
+ /// print(x.index_of(|v, i| v * i > 20, 8)); // prints 10: 3 * 10 > 20
+ /// ```
+ #[rhai_fn(name = "index_of", return_raw, pure)]
+ pub fn index_of_filter_starting_from(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ filter: FnPtr,
+ start: INT,
+ ) -> RhaiResultOf<INT> {
+ if array.is_empty() {
+ return Ok(-1);
+ }
+
+ let (start, ..) = calc_offset_len(array.len(), start, 0);
+
+ for (i, item) in array.iter_mut().enumerate().skip(start) {
+ let ex = [(i as INT).into()];
+
+ if filter
+ .call_raw_with_extra_args("index_of", &ctx, Some(item), [], ex, Some(0))?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ return Ok(i as INT);
+ }
+ }
+
+ Ok(-1 as INT)
+ }
+ /// Iterate through all the elements in the array, applying a `filter` function to each element
+ /// in turn, and return a copy of the first element that returns `true`. If no element returns
+ /// `true`, `()` is returned.
+ ///
+ /// # No Function Parameter
+ ///
+ /// Array element (mutable) is bound to `this`.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 5, 8, 13];
+ ///
+ /// print(x.find(|v| v > 3)); // prints 5: 5 > 3
+ ///
+ /// print(x.find(|v| v > 13) ?? "not found"); // prints "not found": nothing is > 13
+ ///
+ /// print(x.find(|v, i| v * i > 13)); // prints 5: 3 * 5 > 13
+ /// ```
+ #[rhai_fn(return_raw, pure)]
+ pub fn find(ctx: NativeCallContext, array: &mut Array, filter: FnPtr) -> RhaiResult {
+ find_starting_from(ctx, array, filter, 0)
+ }
+ /// Iterate through all the elements in the array, starting from a particular `start` position,
+ /// applying a `filter` function to each element in turn, and return a copy of the first element
+ /// that returns `true`. If no element returns `true`, `()` is returned.
+ ///
+ /// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `start` < -length of array, position counts from the beginning of the array.
+ /// * If `start` ≥ length of array, `-1` is returned.
+ ///
+ /// # No Function Parameter
+ ///
+ /// Array element (mutable) is bound to `this`.
+ ///
+ /// This method is marked _pure_; the `filter` function should not mutate array elements.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 5, 8, 13];
+ ///
+ /// print(x.find(|v| v > 1, 2)); // prints 3: 3 > 1
+ ///
+ /// print(x.find(|v| v < 2, 3) ?? "not found"); // prints "not found": nothing < 2 past index 3
+ ///
+ /// print(x.find(|v| v > 1, 8) ?? "not found"); // prints "not found": nothing found past end of array
+ ///
+ /// print(x.find(|v| v > 1, -3)); // prints 5: -3 = start from index 4
+ ///
+ /// print(x.find(|v| v > 0, -99)); // prints 1: -99 = start from beginning
+ ///
+ /// print(x.find(|v, i| v * i > 6, 3)); // prints 5: 5 * 4 > 6
+ /// ```
+ #[rhai_fn(name = "find", return_raw, pure)]
+ pub fn find_starting_from(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ filter: FnPtr,
+ start: INT,
+ ) -> RhaiResult {
+ let index = index_of_filter_starting_from(ctx, array, filter, start)?;
+
+ if index < 0 {
+ return Ok(Dynamic::UNIT);
+ }
+
+ Ok(get(array, index))
+ }
+ /// Iterate through all the elements in the array, applying a `mapper` function to each element
+ /// in turn, and return the first result that is not `()`. Otherwise, `()` is returned.
+ ///
+ /// # No Function Parameter
+ ///
+ /// Array element (mutable) is bound to `this`.
+ ///
+ /// This method is marked _pure_; the `mapper` function should not mutate array elements.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [#{alice: 1}, #{bob: 2}, #{clara: 3}];
+ ///
+ /// print(x.find_map(|v| v.alice)); // prints 1
+ ///
+ /// print(x.find_map(|v| v.dave) ?? "not found"); // prints "not found"
+ ///
+ /// print(x.find_map(|| this.dave) ?? "not found"); // prints "not found"
+ /// ```
+ #[rhai_fn(return_raw, pure)]
+ pub fn find_map(ctx: NativeCallContext, array: &mut Array, filter: FnPtr) -> RhaiResult {
+ find_map_starting_from(ctx, array, filter, 0)
+ }
+ /// Iterate through all the elements in the array, starting from a particular `start` position,
+ /// applying a `mapper` function to each element in turn, and return the first result that is not `()`.
+ /// Otherwise, `()` is returned.
+ ///
+ /// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `start` < -length of array, position counts from the beginning of the array.
+ /// * If `start` ≥ length of array, `-1` is returned.
+ ///
+ /// # No Function Parameter
+ ///
+ /// Array element (mutable) is bound to `this`.
+ ///
+ /// This method is marked _pure_; the `mapper` function should not mutate array elements.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [#{alice: 1}, #{bob: 2}, #{bob: 3}, #{clara: 3}, #{alice: 0}, #{clara: 5}];
+ ///
+ /// print(x.find_map(|v| v.alice, 2)); // prints 0
+ ///
+ /// print(x.find_map(|v| v.bob, 4) ?? "not found"); // prints "not found"
+ ///
+ /// print(x.find_map(|v| v.alice, 8) ?? "not found"); // prints "not found"
+ ///
+ /// print(x.find_map(|| this.alice, 8) ?? "not found"); // prints "not found"
+ ///
+ /// print(x.find_map(|v| v.bob, -4)); // prints 3: -4 = start from index 2
+ ///
+ /// print(x.find_map(|v| v.alice, -99)); // prints 1: -99 = start from beginning
+ ///
+ /// print(x.find_map(|| this.alice, -99)); // prints 1: -99 = start from beginning
+ /// ```
+ #[rhai_fn(name = "find_map", return_raw, pure)]
+ pub fn find_map_starting_from(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ filter: FnPtr,
+ start: INT,
+ ) -> RhaiResult {
+ if array.is_empty() {
+ return Ok(Dynamic::UNIT);
+ }
+
+ let (start, ..) = calc_offset_len(array.len(), start, 0);
+
+ for (i, item) in array.iter_mut().enumerate().skip(start) {
+ let ex = [(i as INT).into()];
+
+ let value =
+ filter.call_raw_with_extra_args("find_map", &ctx, Some(item), [], ex, Some(0))?;
+
+ if !value.is_unit() {
+ return Ok(value);
+ }
+ }
+
+ Ok(Dynamic::UNIT)
+ }
+ /// Return `true` if any element in the array that returns `true` when applied the `filter` function.
+ ///
+ /// # No Function Parameter
+ ///
+ /// Array element (mutable) is bound to `this`.
+ ///
+ /// This method is marked _pure_; the `filter` function should not mutate array elements.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+ ///
+ /// print(x.some(|v| v > 3)); // prints true
+ ///
+ /// print(x.some(|v| v > 10)); // prints false
+ ///
+ /// print(x.some(|v, i| i > v)); // prints true
+ /// ```
+ #[rhai_fn(return_raw, pure)]
+ pub fn some(ctx: NativeCallContext, array: &mut Array, filter: FnPtr) -> RhaiResultOf<bool> {
+ if array.is_empty() {
+ return Ok(false);
+ }
+
+ for (i, item) in array.iter_mut().enumerate() {
+ let ex = [(i as INT).into()];
+
+ if filter
+ .call_raw_with_extra_args("some", &ctx, Some(item), [], ex, Some(0))?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ return Ok(true);
+ }
+ }
+
+ Ok(false)
+ }
+ /// Return `true` if all elements in the array return `true` when applied the `filter` function.
+ ///
+ /// # No Function Parameter
+ ///
+ /// Array element (mutable) is bound to `this`.
+ ///
+ /// This method is marked _pure_; the `filter` function should not mutate array elements.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 5];
+ ///
+ /// print(x.all(|v| v > 3)); // prints false
+ ///
+ /// print(x.all(|v| v > 1)); // prints true
+ ///
+ /// print(x.all(|v, i| i > v)); // prints false
+ /// ```
+ #[rhai_fn(return_raw, pure)]
+ pub fn all(ctx: NativeCallContext, array: &mut Array, filter: FnPtr) -> RhaiResultOf<bool> {
+ if array.is_empty() {
+ return Ok(true);
+ }
+
+ for (i, item) in array.iter_mut().enumerate() {
+ let ex = [(i as INT).into()];
+
+ if !filter
+ .call_raw_with_extra_args("all", &ctx, Some(item), [], ex, Some(0))?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ return Ok(false);
+ }
+ }
+
+ Ok(true)
+ }
+ /// Remove duplicated _consecutive_ elements from the array.
+ ///
+ /// The operator `==` is used to compare elements and must be defined,
+ /// otherwise `false` is assumed.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 2, 2, 3, 4, 3, 3, 2, 1];
+ ///
+ /// x.dedup();
+ ///
+ /// print(x); // prints "[1, 2, 3, 4, 3, 2, 1]"
+ /// ```
+ pub fn dedup(ctx: NativeCallContext, array: &mut Array) {
+ let comparer = FnPtr::new_unchecked(OP_EQUALS, StaticVec::new_const());
+ dedup_by_comparer(ctx, array, comparer);
+ }
+ /// Remove duplicated _consecutive_ elements from the array that return `true` when applied the
+ /// `comparer` function.
+ ///
+ /// No element is removed if the correct `comparer` function does not exist.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element1`: copy of the current array element to compare
+ /// * `element2`: copy of the next array element to compare
+ ///
+ /// ## Return Value
+ ///
+ /// `true` if `element1 == element2`, otherwise `false`.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 2, 2, 3, 1, 2, 3, 4, 3, 3, 2, 1];
+ ///
+ /// x.dedup(|a, b| a >= b);
+ ///
+ /// print(x); // prints "[1, 2, 3, 4]"
+ /// ```
+ #[rhai_fn(name = "dedup")]
+ pub fn dedup_by_comparer(ctx: NativeCallContext, array: &mut Array, comparer: FnPtr) {
+ if array.is_empty() {
+ return;
+ }
+
+ array.dedup_by(|x, y| {
+ comparer
+ .call_raw(&ctx, None, [y.clone(), x.clone()])
+ .unwrap_or(Dynamic::FALSE)
+ .as_bool()
+ .unwrap_or(false)
+ });
+ }
+ /// Reduce an array by iterating through all elements while applying the `reducer` function.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `result`: accumulated result, initially `()`
+ /// * `element`: copy of array element, or bound to `this` if omitted
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// This method is marked _pure_; the `reducer` function should not mutate array elements.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.reduce(|r, v| v + (r ?? 0));
+ ///
+ /// print(y); // prints 15
+ ///
+ /// let y = x.reduce(|r, v, i| v + i + (r ?? 0));
+ ///
+ /// print(y); // prints 25
+ /// ```
+ #[rhai_fn(return_raw, pure)]
+ pub fn reduce(ctx: NativeCallContext, array: &mut Array, reducer: FnPtr) -> RhaiResult {
+ reduce_with_initial(ctx, array, reducer, Dynamic::UNIT)
+ }
+ /// Reduce an array by iterating through all elements while applying the `reducer` function.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `result`: accumulated result, starting with the value of `initial`
+ /// * `element`: copy of array element, or bound to `this` if omitted
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// This method is marked _pure_; the `reducer` function should not mutate array elements.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.reduce(|r, v| v + r, 5);
+ ///
+ /// print(y); // prints 20
+ ///
+ /// let y = x.reduce(|r, v, i| v + i + r, 5);
+ ///
+ /// print(y); // prints 30
+ /// ```
+ #[rhai_fn(name = "reduce", return_raw, pure)]
+ pub fn reduce_with_initial(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ reducer: FnPtr,
+ initial: Dynamic,
+ ) -> RhaiResult {
+ if array.is_empty() {
+ return Ok(initial);
+ }
+
+ array
+ .iter_mut()
+ .enumerate()
+ .try_fold(initial, |result, (i, item)| {
+ let ex = [(i as INT).into()];
+ reducer.call_raw_with_extra_args("reduce", &ctx, Some(item), [result], ex, Some(1))
+ })
+ }
+ /// Reduce an array by iterating through all elements, in _reverse_ order,
+ /// while applying the `reducer` function.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `result`: accumulated result, initially `()`
+ /// * `element`: copy of array element, or bound to `this` if omitted
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// This method is marked _pure_; the `reducer` function should not mutate array elements.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.reduce_rev(|r, v| v + (r ?? 0));
+ ///
+ /// print(y); // prints 15
+ ///
+ /// let y = x.reduce_rev(|r, v, i| v + i + (r ?? 0));
+ ///
+ /// print(y); // prints 25
+ /// ```
+ #[rhai_fn(return_raw, pure)]
+ pub fn reduce_rev(ctx: NativeCallContext, array: &mut Array, reducer: FnPtr) -> RhaiResult {
+ reduce_rev_with_initial(ctx, array, reducer, Dynamic::UNIT)
+ }
+ /// Reduce an array by iterating through all elements, in _reverse_ order,
+ /// while applying the `reducer` function.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `result`: accumulated result, starting with the value of `initial`
+ /// * `element`: copy of array element, or bound to `this` if omitted
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// This method is marked _pure_; the `reducer` function should not mutate array elements.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.reduce_rev(|r, v| v + r, 5);
+ ///
+ /// print(y); // prints 20
+ ///
+ /// let y = x.reduce_rev(|r, v, i| v + i + r, 5);
+ ///
+ /// print(y); // prints 30
+ /// ```
+ #[rhai_fn(name = "reduce_rev", return_raw, pure)]
+ pub fn reduce_rev_with_initial(
+ ctx: NativeCallContext,
+ array: &mut Array,
+ reducer: FnPtr,
+ initial: Dynamic,
+ ) -> RhaiResult {
+ if array.is_empty() {
+ return Ok(initial);
+ }
+
+ let len = array.len();
+
+ array
+ .iter_mut()
+ .rev()
+ .enumerate()
+ .try_fold(initial, |result, (i, item)| {
+ let ex = [((len - 1 - i) as INT).into()];
+
+ reducer.call_raw_with_extra_args(
+ "reduce_rev",
+ &ctx,
+ Some(item),
+ [result],
+ ex,
+ Some(1),
+ )
+ })
+ }
+ /// Sort the array based on applying the `comparer` function.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element1`: copy of the current array element to compare
+ /// * `element2`: copy of the next array element to compare
+ ///
+ /// ## Return Value
+ ///
+ /// * Any integer > 0 if `element1 > element2`
+ /// * Zero if `element1 == element2`
+ /// * Any integer < 0 if `element1 < element2`
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10];
+ ///
+ /// // Do comparisons in reverse
+ /// x.sort(|a, b| if a > b { -1 } else if a < b { 1 } else { 0 });
+ ///
+ /// print(x); // prints "[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]"
+ /// ```
+ pub fn sort(ctx: NativeCallContext, array: &mut Array, comparer: FnPtr) {
+ if array.len() <= 1 {
+ return;
+ }
+
+ array.sort_by(|x, y| {
+ comparer
+ .call_raw(&ctx, None, [x.clone(), y.clone()])
+ .ok()
+ .and_then(|v| v.as_int().ok())
+ .map_or_else(
+ || x.type_id().cmp(&y.type_id()),
+ |v| match v {
+ v if v > 0 => Ordering::Greater,
+ v if v < 0 => Ordering::Less,
+ 0 => Ordering::Equal,
+ _ => unreachable!("v is {}", v),
+ },
+ )
+ });
+ }
+ /// Sort the array.
+ ///
+ /// All elements in the array must be of the same data type.
+ ///
+ /// # Supported Data Types
+ ///
+ /// * integer numbers
+ /// * floating-point numbers
+ /// * decimal numbers
+ /// * characters
+ /// * strings
+ /// * booleans
+ /// * `()`
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10];
+ ///
+ /// x.sort();
+ ///
+ /// print(x); // prints "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
+ /// ```
+ #[rhai_fn(name = "sort", return_raw)]
+ pub fn sort_with_builtin(array: &mut Array) -> RhaiResultOf<()> {
+ if array.len() <= 1 {
+ return Ok(());
+ }
+
+ let type_id = array[0].type_id();
+
+ if array.iter().any(|a| a.type_id() != type_id) {
+ return Err(ERR::ErrorFunctionNotFound(
+ "sort() cannot be called with elements of different types".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ if type_id == TypeId::of::<INT>() {
+ array.sort_by(|a, b| {
+ let a = a.as_int().expect("`INT`");
+ let b = b.as_int().expect("`INT`");
+ a.cmp(&b)
+ });
+ return Ok(());
+ }
+ if type_id == TypeId::of::<char>() {
+ array.sort_by(|a, b| {
+ let a = a.as_char().expect("char");
+ let b = b.as_char().expect("char");
+ a.cmp(&b)
+ });
+ return Ok(());
+ }
+ #[cfg(not(feature = "no_float"))]
+ if type_id == TypeId::of::<crate::FLOAT>() {
+ array.sort_by(|a, b| {
+ let a = a.as_float().expect("`FLOAT`");
+ let b = b.as_float().expect("`FLOAT`");
+ a.partial_cmp(&b).unwrap_or(Ordering::Equal)
+ });
+ return Ok(());
+ }
+ if type_id == TypeId::of::<ImmutableString>() {
+ array.sort_by(|a, b| {
+ let a = a.read_lock::<ImmutableString>().expect("`ImmutableString`");
+ let b = b.read_lock::<ImmutableString>().expect("`ImmutableString`");
+ a.as_str().cmp(b.as_str())
+ });
+ return Ok(());
+ }
+ #[cfg(feature = "decimal")]
+ if type_id == TypeId::of::<rust_decimal::Decimal>() {
+ array.sort_by(|a, b| {
+ let a = a.as_decimal().expect("`Decimal`");
+ let b = b.as_decimal().expect("`Decimal`");
+ a.cmp(&b)
+ });
+ return Ok(());
+ }
+ if type_id == TypeId::of::<bool>() {
+ array.sort_by(|a, b| {
+ let a = a.as_bool().expect("`bool`");
+ let b = b.as_bool().expect("`bool`");
+ a.cmp(&b)
+ });
+ return Ok(());
+ }
+ if type_id == TypeId::of::<()>() {
+ return Ok(());
+ }
+
+ Ok(())
+ }
+ /// Remove all elements in the array that returns `true` when applied the `filter` function and
+ /// return them as a new array.
+ ///
+ /// # No Function Parameter
+ ///
+ /// Array element (mutable) is bound to `this`.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.drain(|v| v < 3);
+ ///
+ /// print(x); // prints "[3, 4, 5]"
+ ///
+ /// print(y); // prints "[1, 2]"
+ ///
+ /// let z = x.drain(|v, i| v + i > 5);
+ ///
+ /// print(x); // prints "[3, 4]"
+ ///
+ /// print(z); // prints "[5]"
+ /// ```
+ #[rhai_fn(return_raw)]
+ pub fn drain(ctx: NativeCallContext, array: &mut Array, filter: FnPtr) -> RhaiResultOf<Array> {
+ if array.is_empty() {
+ return Ok(Array::new());
+ }
+
+ let mut drained = Array::with_capacity(array.len());
+
+ let mut i = 0;
+ let mut x = 0;
+
+ while x < array.len() {
+ let ex = [(i as INT).into()];
+
+ if filter
+ .call_raw_with_extra_args("drain", &ctx, Some(&mut array[x]), [], ex, Some(0))?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ drained.push(array.remove(x));
+ } else {
+ x += 1;
+ }
+
+ i += 1;
+ }
+
+ Ok(drained)
+ }
+ /// Remove all elements in the array within an exclusive `range` and return them as a new array.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.drain(1..3);
+ ///
+ /// print(x); // prints "[1, 4, 5]"
+ ///
+ /// print(y); // prints "[2, 3]"
+ ///
+ /// let z = x.drain(2..3);
+ ///
+ /// print(x); // prints "[1, 4]"
+ ///
+ /// print(z); // prints "[5]"
+ /// ```
+ #[rhai_fn(name = "drain")]
+ pub fn drain_exclusive_range(array: &mut Array, range: ExclusiveRange) -> Array {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ drain_range(array, start, end - start)
+ }
+ /// Remove all elements in the array within an inclusive `range` and return them as a new array.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.drain(1..=2);
+ ///
+ /// print(x); // prints "[1, 4, 5]"
+ ///
+ /// print(y); // prints "[2, 3]"
+ ///
+ /// let z = x.drain(2..=2);
+ ///
+ /// print(x); // prints "[1, 4]"
+ ///
+ /// print(z); // prints "[5]"
+ /// ```
+ #[rhai_fn(name = "drain")]
+ pub fn drain_inclusive_range(array: &mut Array, range: InclusiveRange) -> Array {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ drain_range(array, start, end - start + 1)
+ }
+ /// Remove all elements within a portion of the array and return them as a new array.
+ ///
+ /// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `start` < -length of array, position counts from the beginning of the array.
+ /// * If `start` ≥ length of array, no element is removed and an empty array is returned.
+ /// * If `len` ≤ 0, no element is removed and an empty array is returned.
+ /// * If `start` position + `len` ≥ length of array, entire portion of the array after the `start` position is removed and returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.drain(1, 2);
+ ///
+ /// print(x); // prints "[1, 4, 5]"
+ ///
+ /// print(y); // prints "[2, 3]"
+ ///
+ /// let z = x.drain(-1, 1);
+ ///
+ /// print(x); // prints "[1, 4]"
+ ///
+ /// print(z); // prints "[5]"
+ /// ```
+ #[rhai_fn(name = "drain")]
+ pub fn drain_range(array: &mut Array, start: INT, len: INT) -> Array {
+ if array.is_empty() || len <= 0 {
+ return Array::new();
+ }
+
+ let (start, len) = calc_offset_len(array.len(), start, len);
+
+ if len == 0 {
+ Array::new()
+ } else {
+ array.drain(start..start + len).collect()
+ }
+ }
+ /// Remove all elements in the array that do not return `true` when applied the `filter`
+ /// function and return them as a new array.
+ ///
+ /// # No Function Parameter
+ ///
+ /// Array element (mutable) is bound to `this`.
+ ///
+ /// # Function Parameters
+ ///
+ /// * `element`: copy of array element
+ /// * `index` _(optional)_: current index in the array
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.retain(|v| v >= 3);
+ ///
+ /// print(x); // prints "[3, 4, 5]"
+ ///
+ /// print(y); // prints "[1, 2]"
+ ///
+ /// let z = x.retain(|v, i| v + i <= 5);
+ ///
+ /// print(x); // prints "[3, 4]"
+ ///
+ /// print(z); // prints "[5]"
+ /// ```
+ #[rhai_fn(return_raw)]
+ pub fn retain(ctx: NativeCallContext, array: &mut Array, filter: FnPtr) -> RhaiResultOf<Array> {
+ if array.is_empty() {
+ return Ok(Array::new());
+ }
+
+ let mut drained = Array::new();
+
+ let mut i = 0;
+ let mut x = 0;
+
+ while x < array.len() {
+ let ex = [(i as INT).into()];
+
+ if filter
+ .call_raw_with_extra_args("retain", &ctx, Some(&mut array[x]), [], ex, Some(0))?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ x += 1;
+ } else {
+ drained.push(array.remove(x));
+ }
+
+ i += 1;
+ }
+
+ Ok(drained)
+ }
+ /// Remove all elements in the array not within an exclusive `range` and return them as a new array.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.retain(1..4);
+ ///
+ /// print(x); // prints "[2, 3, 4]"
+ ///
+ /// print(y); // prints "[1, 5]"
+ ///
+ /// let z = x.retain(1..3);
+ ///
+ /// print(x); // prints "[3, 4]"
+ ///
+ /// print(z); // prints "[1]"
+ /// ```
+ #[rhai_fn(name = "retain")]
+ pub fn retain_exclusive_range(array: &mut Array, range: ExclusiveRange) -> Array {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ retain_range(array, start, end - start)
+ }
+ /// Remove all elements in the array not within an inclusive `range` and return them as a new array.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.retain(1..=3);
+ ///
+ /// print(x); // prints "[2, 3, 4]"
+ ///
+ /// print(y); // prints "[1, 5]"
+ ///
+ /// let z = x.retain(1..=2);
+ ///
+ /// print(x); // prints "[3, 4]"
+ ///
+ /// print(z); // prints "[1]"
+ /// ```
+ #[rhai_fn(name = "retain")]
+ pub fn retain_inclusive_range(array: &mut Array, range: InclusiveRange) -> Array {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ retain_range(array, start, end - start + 1)
+ }
+ /// Remove all elements not within a portion of the array and return them as a new array.
+ ///
+ /// * If `start` < 0, position counts from the end of the array (`-1` is the last element).
+ /// * If `start` < -length of array, position counts from the beginning of the array.
+ /// * If `start` ≥ length of array, all elements are removed returned.
+ /// * If `len` ≤ 0, all elements are removed and returned.
+ /// * If `start` position + `len` ≥ length of array, entire portion of the array before the `start` position is removed and returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ ///
+ /// let y = x.retain(1, 2);
+ ///
+ /// print(x); // prints "[2, 3]"
+ ///
+ /// print(y); // prints "[1, 4, 5]"
+ ///
+ /// let z = x.retain(-1, 1);
+ ///
+ /// print(x); // prints "[3]"
+ ///
+ /// print(z); // prints "[2]"
+ /// ```
+ #[rhai_fn(name = "retain")]
+ pub fn retain_range(array: &mut Array, start: INT, len: INT) -> Array {
+ if array.is_empty() || len <= 0 {
+ return Array::new();
+ }
+
+ let (start, len) = calc_offset_len(array.len(), start, len);
+
+ if len == 0 {
+ Array::new()
+ } else {
+ let mut drained: Array = array.drain(..start).collect();
+ drained.extend(array.drain(len..));
+
+ drained
+ }
+ }
+ /// Return `true` if two arrays are equal (i.e. all elements are equal and in the same order).
+ ///
+ /// The operator `==` is used to compare elements and must be defined,
+ /// otherwise `false` is assumed.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ /// let y = [1, 2, 3, 4, 5];
+ /// let z = [1, 2, 3, 4];
+ ///
+ /// print(x == y); // prints true
+ ///
+ /// print(x == z); // prints false
+ /// ```
+ #[rhai_fn(name = "==", return_raw, pure)]
+ pub fn equals(ctx: NativeCallContext, array1: &mut Array, array2: Array) -> RhaiResultOf<bool> {
+ if array1.len() != array2.len() {
+ return Ok(false);
+ }
+ if array1.is_empty() {
+ return Ok(true);
+ }
+
+ let mut array2 = array2;
+
+ for (a1, a2) in array1.iter_mut().zip(array2.iter_mut()) {
+ if !ctx
+ .call_native_fn_raw(OP_EQUALS, true, &mut [a1, a2])
+ .or_else(|err| match *err {
+ ERR::ErrorFunctionNotFound(ref fn_sig, ..) if fn_sig.starts_with(OP_EQUALS) => {
+ if a1.type_id() == a2.type_id() {
+ // No default when comparing same type
+ Err(err)
+ } else {
+ Ok(Dynamic::FALSE)
+ }
+ }
+ _ => Err(err),
+ })?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ return Ok(false);
+ }
+ }
+
+ Ok(true)
+ }
+ /// Return `true` if two arrays are not-equal (i.e. any element not equal or not in the same order).
+ ///
+ /// The operator `==` is used to compare elements and must be defined,
+ /// otherwise `false` is assumed.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = [1, 2, 3, 4, 5];
+ /// let y = [1, 2, 3, 4, 5];
+ /// let z = [1, 2, 3, 4];
+ ///
+ /// print(x != y); // prints false
+ ///
+ /// print(x != z); // prints true
+ /// ```
+ #[rhai_fn(name = "!=", return_raw, pure)]
+ pub fn not_equals(
+ ctx: NativeCallContext,
+ array1: &mut Array,
+ array2: Array,
+ ) -> RhaiResultOf<bool> {
+ equals(ctx, array1, array2).map(|r| !r)
+ }
+}
diff --git a/rhai/src/packages/bit_field.rs b/rhai/src/packages/bit_field.rs
new file mode 100644
index 0000000..50d705a
--- /dev/null
+++ b/rhai/src/packages/bit_field.rs
@@ -0,0 +1,244 @@
+use crate::eval::calc_index;
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+use crate::{
+ def_package, ExclusiveRange, InclusiveRange, Position, RhaiResultOf, ERR, INT, INT_BITS,
+ UNSIGNED_INT,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+def_package! {
+ /// Package of basic bit-field utilities.
+ pub BitFieldPackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ combine_with_exported_module!(lib, "bit_field", bit_field_functions);
+ }
+}
+
+#[export_module]
+mod bit_field_functions {
+ /// Return `true` if the specified `bit` in the number is set.
+ ///
+ /// If `bit` < 0, position counts from the MSB (Most Significant Bit).
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = 123456;
+ ///
+ /// print(x.get_bit(5)); // prints false
+ ///
+ /// print(x.get_bit(6)); // prints true
+ ///
+ /// print(x.get_bit(-48)); // prints true on 64-bit
+ /// ```
+ #[rhai_fn(return_raw)]
+ pub fn get_bit(value: INT, bit: INT) -> RhaiResultOf<bool> {
+ let bit = calc_index(INT_BITS, bit, true, || {
+ ERR::ErrorBitFieldBounds(INT_BITS, bit, Position::NONE).into()
+ })?;
+
+ Ok((value & (1 << bit)) != 0)
+ }
+ /// Set the specified `bit` in the number if the new value is `true`.
+ /// Clear the `bit` if the new value is `false`.
+ ///
+ /// If `bit` < 0, position counts from the MSB (Most Significant Bit).
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = 123456;
+ ///
+ /// x.set_bit(5, true);
+ ///
+ /// print(x); // prints 123488
+ ///
+ /// x.set_bit(6, false);
+ ///
+ /// print(x); // prints 123424
+ ///
+ /// x.set_bit(-48, false);
+ ///
+ /// print(x); // prints 57888 on 64-bit
+ /// ```
+ #[rhai_fn(return_raw)]
+ pub fn set_bit(value: &mut INT, bit: INT, new_value: bool) -> RhaiResultOf<()> {
+ let bit = calc_index(INT_BITS, bit, true, || {
+ ERR::ErrorBitFieldBounds(INT_BITS, bit, Position::NONE).into()
+ })?;
+
+ let mask = 1 << bit;
+ if new_value {
+ *value |= mask;
+ } else {
+ *value &= !mask;
+ }
+
+ Ok(())
+ }
+ /// Return an exclusive range of bits in the number as a new number.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = 123456;
+ ///
+ /// print(x.get_bits(5..10)); // print 18
+ /// ```
+ #[rhai_fn(name = "get_bits", return_raw)]
+ pub fn get_bits_range(value: INT, range: ExclusiveRange) -> RhaiResultOf<INT> {
+ let from = INT::max(range.start, 0);
+ let to = INT::max(range.end, from);
+ get_bits(value, from, to - from)
+ }
+ /// Return an inclusive range of bits in the number as a new number.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = 123456;
+ ///
+ /// print(x.get_bits(5..=9)); // print 18
+ /// ```
+ #[rhai_fn(name = "get_bits", return_raw)]
+ pub fn get_bits_range_inclusive(value: INT, range: InclusiveRange) -> RhaiResultOf<INT> {
+ let from = INT::max(*range.start(), 0);
+ let to = INT::max(*range.end(), from - 1);
+ get_bits(value, from, to - from + 1)
+ }
+ /// Return a portion of bits in the number as a new number.
+ ///
+ /// * If `start` < 0, position counts from the MSB (Most Significant Bit).
+ /// * If `bits` ≤ 0, zero is returned.
+ /// * If `start` position + `bits` ≥ total number of bits, the bits after the `start` position are returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = 123456;
+ ///
+ /// print(x.get_bits(5, 8)); // print 18
+ /// ```
+ #[rhai_fn(return_raw)]
+ pub fn get_bits(value: INT, start: INT, bits: INT) -> RhaiResultOf<INT> {
+ if bits <= 0 {
+ return Ok(0);
+ }
+
+ let bit = calc_index(INT_BITS, start, true, || {
+ ERR::ErrorBitFieldBounds(INT_BITS, start, Position::NONE).into()
+ })?;
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let bits = if bit + bits as usize > INT_BITS {
+ INT_BITS - bit
+ } else {
+ bits as usize
+ };
+
+ if bit == 0 && bits == INT_BITS {
+ return Ok(value);
+ }
+
+ // 2^bits - 1
+ #[allow(clippy::cast_possible_truncation)]
+ let mask = ((2 as UNSIGNED_INT).pow(bits as u32) - 1) as INT;
+
+ Ok(((value & (mask << bit)) >> bit) & mask)
+ }
+ /// Replace an exclusive range of bits in the number with a new value.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = 123456;
+ ///
+ /// x.set_bits(5..10, 42);
+ ///
+ /// print(x); // print 123200
+ /// ```
+ #[rhai_fn(name = "set_bits", return_raw)]
+ pub fn set_bits_range(
+ value: &mut INT,
+ range: ExclusiveRange,
+ new_value: INT,
+ ) -> RhaiResultOf<()> {
+ let from = INT::max(range.start, 0);
+ let to = INT::max(range.end, from);
+ set_bits(value, from, to - from, new_value)
+ }
+ /// Replace an inclusive range of bits in the number with a new value.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = 123456;
+ ///
+ /// x.set_bits(5..=9, 42);
+ ///
+ /// print(x); // print 123200
+ /// ```
+ #[rhai_fn(name = "set_bits", return_raw)]
+ pub fn set_bits_range_inclusive(
+ value: &mut INT,
+ range: InclusiveRange,
+ new_value: INT,
+ ) -> RhaiResultOf<()> {
+ let from = INT::max(*range.start(), 0);
+ let to = INT::max(*range.end(), from - 1);
+ set_bits(value, from, to - from + 1, new_value)
+ }
+ /// Replace a portion of bits in the number with a new value.
+ ///
+ /// * If `start` < 0, position counts from the MSB (Most Significant Bit).
+ /// * If `bits` ≤ 0, the number is not modified.
+ /// * If `start` position + `bits` ≥ total number of bits, the bits after the `start` position are replaced.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = 123456;
+ ///
+ /// x.set_bits(5, 8, 42);
+ ///
+ /// print(x); // prints 124224
+ ///
+ /// x.set_bits(-16, 10, 42);
+ ///
+ /// print(x); // prints 11821949021971776 on 64-bit
+ /// ```
+ #[rhai_fn(return_raw)]
+ pub fn set_bits(value: &mut INT, bit: INT, bits: INT, new_value: INT) -> RhaiResultOf<()> {
+ if bits <= 0 {
+ return Ok(());
+ }
+
+ let bit = calc_index(INT_BITS, bit, true, || {
+ ERR::ErrorBitFieldBounds(INT_BITS, bit, Position::NONE).into()
+ })?;
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let bits = if bit + bits as usize > INT_BITS {
+ INT_BITS - bit
+ } else {
+ bits as usize
+ };
+
+ if bit == 0 && bits == INT_BITS {
+ *value = new_value;
+ return Ok(());
+ }
+
+ // 2^bits - 1
+ #[allow(clippy::cast_possible_truncation)]
+ let mask = ((2 as UNSIGNED_INT).pow(bits as u32) - 1) as INT;
+
+ *value &= !(mask << bit);
+ *value |= (new_value & mask) << bit;
+
+ Ok(())
+ }
+}
diff --git a/rhai/src/packages/blob_basic.rs b/rhai/src/packages/blob_basic.rs
new file mode 100644
index 0000000..a5af529
--- /dev/null
+++ b/rhai/src/packages/blob_basic.rs
@@ -0,0 +1,1621 @@
+#![cfg(not(feature = "no_index"))]
+
+use crate::eval::{calc_index, calc_offset_len};
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+use crate::{
+ def_package, Array, Blob, Dynamic, ExclusiveRange, InclusiveRange, NativeCallContext, Position,
+ RhaiResultOf, ERR, INT, INT_BYTES, MAX_USIZE_INT,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{any::TypeId, borrow::Cow, mem};
+
+#[cfg(not(feature = "no_float"))]
+use crate::{FLOAT, FLOAT_BYTES};
+
+def_package! {
+ /// Package of basic BLOB utilities.
+ pub BasicBlobPackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ combine_with_exported_module!(lib, "blob", blob_functions);
+ combine_with_exported_module!(lib, "parse_int", parse_int_functions);
+ combine_with_exported_module!(lib, "write_int", write_int_functions);
+ combine_with_exported_module!(lib, "write_string", write_string_functions);
+
+ #[cfg(not(feature = "no_float"))]
+ {
+ combine_with_exported_module!(lib, "parse_float", parse_float_functions);
+ combine_with_exported_module!(lib, "write_float", write_float_functions);
+ }
+
+ // Register blob iterator
+ lib.set_iterable::<Blob>();
+ }
+}
+
+#[export_module]
+pub mod blob_functions {
+ /// Return a new, empty BLOB.
+ pub const fn blob() -> Blob {
+ Blob::new()
+ }
+ /// Return a new BLOB of the specified length, filled with zeros.
+ ///
+ /// If `len` ≤ 0, an empty BLOB is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob(10);
+ ///
+ /// print(b); // prints "[0000000000000000 0000]"
+ /// ```
+ #[rhai_fn(name = "blob", return_raw)]
+ pub fn blob_with_capacity(ctx: NativeCallContext, len: INT) -> RhaiResultOf<Blob> {
+ blob_with_capacity_and_value(ctx, len, 0)
+ }
+ /// Return a new BLOB of the specified length, filled with copies of the initial `value`.
+ ///
+ /// If `len` ≤ 0, an empty BLOB is returned.
+ ///
+ /// Only the lower 8 bits of the initial `value` are used; all other bits are ignored.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob(10, 0x42);
+ ///
+ /// print(b); // prints "[4242424242424242 4242]"
+ /// ```
+ #[rhai_fn(name = "blob", return_raw)]
+ pub fn blob_with_capacity_and_value(
+ ctx: NativeCallContext,
+ len: INT,
+ value: INT,
+ ) -> RhaiResultOf<Blob> {
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let len = len.clamp(0, MAX_USIZE_INT) as usize;
+ let _ctx = ctx;
+
+ // Check if blob will be over max size limit
+ #[cfg(not(feature = "unchecked"))]
+ _ctx.engine().throw_on_size((len, 0, 0))?;
+
+ let mut blob = Blob::new();
+ #[allow(clippy::cast_sign_loss)]
+ blob.resize(len, (value & 0x0000_00ff) as u8);
+ Ok(blob)
+ }
+ /// Convert the BLOB into an array of integers.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob(5, 0x42);
+ ///
+ /// let x = b.to_array();
+ ///
+ /// print(x); // prints "[66, 66, 66, 66, 66]"
+ /// ```
+ #[rhai_fn(pure)]
+ pub fn to_array(blob: &mut Blob) -> Array {
+ blob.iter().map(|&ch| (ch as INT).into()).collect()
+ }
+ /// Convert the BLOB into a string.
+ ///
+ /// The byte stream must be valid UTF-8, otherwise an error is raised.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob(5, 0x42);
+ ///
+ /// let x = b.as_string();
+ ///
+ /// print(x); // prints "FFFFF"
+ /// ```
+ pub fn as_string(blob: Blob) -> String {
+ let s = String::from_utf8_lossy(&blob);
+
+ match s {
+ Cow::Borrowed(_) => String::from_utf8(blob).unwrap(),
+ Cow::Owned(_) => s.into_owned(),
+ }
+ }
+ /// Return the length of the BLOB.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob(10, 0x42);
+ ///
+ /// print(b); // prints "[4242424242424242 4242]"
+ ///
+ /// print(b.len()); // prints 10
+ /// ```
+ #[rhai_fn(name = "len", get = "len", pure)]
+ pub fn len(blob: &mut Blob) -> INT {
+ blob.len() as INT
+ }
+ /// Return true if the BLOB is empty.
+ #[rhai_fn(name = "is_empty", get = "is_empty", pure)]
+ pub fn is_empty(blob: &mut Blob) -> bool {
+ blob.len() == 0
+ }
+ /// Return `true` if the BLOB contains a specified byte value.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.contains('h')); // prints true
+ ///
+ /// print(text.contains('x')); // prints false
+ /// ```
+ #[rhai_fn(name = "contains")]
+ pub fn contains(blob: &mut Blob, value: INT) -> bool {
+ #[allow(clippy::cast_sign_loss)]
+ blob.contains(&((value & 0x0000_00ff) as u8))
+ }
+ /// Get the byte value at the `index` position in the BLOB.
+ ///
+ /// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last element).
+ /// * If `index` < -length of BLOB, zero is returned.
+ /// * If `index` ≥ length of BLOB, zero is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// print(b.get(0)); // prints 1
+ ///
+ /// print(b.get(-1)); // prints 5
+ ///
+ /// print(b.get(99)); // prints 0
+ /// ```
+ pub fn get(blob: &mut Blob, index: INT) -> INT {
+ if blob.is_empty() {
+ return 0;
+ }
+
+ let (index, ..) = calc_offset_len(blob.len(), index, 0);
+
+ if index >= blob.len() {
+ 0
+ } else {
+ blob[index] as INT
+ }
+ }
+ /// Set the particular `index` position in the BLOB to a new byte `value`.
+ ///
+ /// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `index` < -length of BLOB, the BLOB is not modified.
+ /// * If `index` ≥ length of BLOB, the BLOB is not modified.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// b.set(0, 0x42);
+ ///
+ /// print(b); // prints "[4202030405]"
+ ///
+ /// b.set(-3, 0);
+ ///
+ /// print(b); // prints "[4202000405]"
+ ///
+ /// b.set(99, 123);
+ ///
+ /// print(b); // prints "[4202000405]"
+ /// ```
+ pub fn set(blob: &mut Blob, index: INT, value: INT) {
+ if blob.is_empty() {
+ return;
+ }
+
+ let (index, ..) = calc_offset_len(blob.len(), index, 0);
+
+ #[allow(clippy::cast_sign_loss)]
+ if index < blob.len() {
+ blob[index] = (value & 0x0000_00ff) as u8;
+ }
+ }
+ /// Add a new byte `value` to the end of the BLOB.
+ ///
+ /// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b.push(0x42);
+ ///
+ /// print(b); // prints "[42]"
+ /// ```
+ #[rhai_fn(name = "push", name = "append")]
+ pub fn push(blob: &mut Blob, value: INT) {
+ #[allow(clippy::cast_sign_loss)]
+ blob.push((value & 0x0000_00ff) as u8);
+ }
+ /// Add another BLOB to the end of the BLOB.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b1 = blob(5, 0x42);
+ /// let b2 = blob(3, 0x11);
+ ///
+ /// b1.push(b2);
+ ///
+ /// print(b1); // prints "[4242424242111111]"
+ /// ```
+ pub fn append(blob1: &mut Blob, blob2: Blob) {
+ if !blob2.is_empty() {
+ if blob1.is_empty() {
+ *blob1 = blob2;
+ } else {
+ blob1.extend(blob2);
+ }
+ }
+ }
+ /// Add a string (as UTF-8 encoded byte-stream) to the end of the BLOB
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob(5, 0x42);
+ ///
+ /// b.append("hello");
+ ///
+ /// print(b); // prints "[424242424268656c 6c6f]"
+ /// ```
+ #[rhai_fn(name = "append")]
+ pub fn append_str(blob: &mut Blob, string: &str) {
+ if !string.is_empty() {
+ blob.extend(string.as_bytes());
+ }
+ }
+ /// Add a character (as UTF-8 encoded byte-stream) to the end of the BLOB
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob(5, 0x42);
+ ///
+ /// b.append('!');
+ ///
+ /// print(b); // prints "[424242424221]"
+ /// ```
+ #[rhai_fn(name = "append")]
+ pub fn append_char(blob: &mut Blob, character: char) {
+ let mut buf = [0_u8; 4];
+ let x = character.encode_utf8(&mut buf);
+ blob.extend(x.as_bytes());
+ }
+ /// Add a byte `value` to the BLOB at a particular `index` position.
+ ///
+ /// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `index` < -length of BLOB, the byte value is added to the beginning of the BLOB.
+ /// * If `index` ≥ length of BLOB, the byte value is appended to the end of the BLOB.
+ ///
+ /// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob(5, 0x42);
+ ///
+ /// b.insert(2, 0x18);
+ ///
+ /// print(b); // prints "[4242184242]"
+ /// ```
+ pub fn insert(blob: &mut Blob, index: INT, value: INT) {
+ #[allow(clippy::cast_sign_loss)]
+ let value = (value & 0x0000_00ff) as u8;
+
+ if blob.is_empty() {
+ blob.push(value);
+ return;
+ }
+
+ let (index, ..) = calc_offset_len(blob.len(), index, 0);
+
+ if index >= blob.len() {
+ blob.push(value);
+ } else {
+ blob.insert(index, value);
+ }
+ }
+ /// Pad the BLOB to at least the specified length with copies of a specified byte `value`.
+ ///
+ /// If `len` ≤ length of BLOB, no padding is done.
+ ///
+ /// Only the lower 8 bits of the `value` are used; all other bits are ignored.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob(3, 0x42);
+ ///
+ /// b.pad(5, 0x18)
+ ///
+ /// print(b); // prints "[4242421818]"
+ ///
+ /// b.pad(3, 0xab)
+ ///
+ /// print(b); // prints "[4242421818]"
+ /// ```
+ #[rhai_fn(return_raw)]
+ pub fn pad(ctx: NativeCallContext, blob: &mut Blob, len: INT, value: INT) -> RhaiResultOf<()> {
+ if len <= 0 {
+ return Ok(());
+ }
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let len = len.min(MAX_USIZE_INT) as usize;
+
+ #[allow(clippy::cast_sign_loss)]
+ let value = (value & 0x0000_00ff) as u8;
+ let _ctx = ctx;
+
+ // Check if blob will be over max size limit
+ if _ctx.engine().max_array_size() > 0 && len > _ctx.engine().max_array_size() {
+ return Err(ERR::ErrorDataTooLarge("Size of BLOB".to_string(), Position::NONE).into());
+ }
+
+ if len > blob.len() {
+ blob.resize(len, value);
+ }
+
+ Ok(())
+ }
+ /// Remove the last byte from the BLOB and return it.
+ ///
+ /// If the BLOB is empty, zero is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// print(b.pop()); // prints 5
+ ///
+ /// print(b); // prints "[01020304]"
+ /// ```
+ pub fn pop(blob: &mut Blob) -> INT {
+ if blob.is_empty() {
+ 0
+ } else {
+ blob.pop().map_or_else(|| 0, |v| v as INT)
+ }
+ }
+ /// Remove the first byte from the BLOB and return it.
+ ///
+ /// If the BLOB is empty, zero is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// print(b.shift()); // prints 1
+ ///
+ /// print(b); // prints "[02030405]"
+ /// ```
+ pub fn shift(blob: &mut Blob) -> INT {
+ if blob.is_empty() {
+ 0
+ } else {
+ blob.remove(0) as INT
+ }
+ }
+ /// Remove the byte at the specified `index` from the BLOB and return it.
+ ///
+ /// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `index` < -length of BLOB, zero is returned.
+ /// * If `index` ≥ length of BLOB, zero is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// print(x.remove(1)); // prints 2
+ ///
+ /// print(x); // prints "[01030405]"
+ ///
+ /// print(x.remove(-2)); // prints 4
+ ///
+ /// print(x); // prints "[010305]"
+ /// ```
+ pub fn remove(blob: &mut Blob, index: INT) -> INT {
+ let index = match calc_index(blob.len(), index, true, || Err(())) {
+ Ok(n) => n,
+ Err(_) => return 0,
+ };
+
+ blob.remove(index) as INT
+ }
+ /// Clear the BLOB.
+ pub fn clear(blob: &mut Blob) {
+ if !blob.is_empty() {
+ blob.clear();
+ }
+ }
+ /// Cut off the BLOB at the specified length.
+ ///
+ /// * If `len` ≤ 0, the BLOB is cleared.
+ /// * If `len` ≥ length of BLOB, the BLOB is not truncated.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// b.truncate(3);
+ ///
+ /// print(b); // prints "[010203]"
+ ///
+ /// b.truncate(10);
+ ///
+ /// print(b); // prints "[010203]"
+ /// ```
+ pub fn truncate(blob: &mut Blob, len: INT) {
+ if len <= 0 {
+ blob.clear();
+ return;
+ }
+ if !blob.is_empty() {
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let len = len.min(MAX_USIZE_INT) as usize;
+
+ if len > 0 {
+ blob.truncate(len);
+ } else {
+ blob.clear();
+ }
+ }
+ }
+ /// Cut off the head of the BLOB, leaving a tail of the specified length.
+ ///
+ /// * If `len` ≤ 0, the BLOB is cleared.
+ /// * If `len` ≥ length of BLOB, the BLOB is not modified.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// b.chop(3);
+ ///
+ /// print(b); // prints "[030405]"
+ ///
+ /// b.chop(10);
+ ///
+ /// print(b); // prints "[030405]"
+ /// ```
+ #[allow(
+ clippy::cast_sign_loss,
+ clippy::needless_pass_by_value,
+ clippy::cast_possible_truncation
+ )]
+ pub fn chop(blob: &mut Blob, len: INT) {
+ if !blob.is_empty() {
+ if len <= 0 {
+ blob.clear();
+ } else if len > MAX_USIZE_INT {
+ // len > BLOB length
+ } else if (len as usize) < blob.len() {
+ blob.drain(0..blob.len() - len as usize);
+ }
+ }
+ }
+ /// Reverse the BLOB.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// print(b); // prints "[0102030405]"
+ ///
+ /// b.reverse();
+ ///
+ /// print(b); // prints "[0504030201]"
+ /// ```
+ pub fn reverse(blob: &mut Blob) {
+ if !blob.is_empty() {
+ blob.reverse();
+ }
+ }
+ /// Replace an exclusive `range` of the BLOB with another BLOB.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b1 = blob(10, 0x42);
+ /// let b2 = blob(5, 0x18);
+ ///
+ /// b1.splice(1..4, b2);
+ ///
+ /// print(b1); // prints "[4218181818184242 42424242]"
+ /// ```
+ #[rhai_fn(name = "splice")]
+ pub fn splice_range(blob: &mut Blob, range: ExclusiveRange, replace: Blob) {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ splice(blob, start, end - start, replace);
+ }
+ /// Replace an inclusive `range` of the BLOB with another BLOB.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b1 = blob(10, 0x42);
+ /// let b2 = blob(5, 0x18);
+ ///
+ /// b1.splice(1..=4, b2);
+ ///
+ /// print(b1); // prints "[4218181818184242 424242]"
+ /// ```
+ #[rhai_fn(name = "splice")]
+ pub fn splice_range_inclusive(blob: &mut Blob, range: InclusiveRange, replace: Blob) {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ splice(blob, start, end - start + 1, replace);
+ }
+ /// Replace a portion of the BLOB with another BLOB.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, the other BLOB is appended to the end of the BLOB.
+ /// * If `len` ≤ 0, the other BLOB is inserted into the BLOB at the `start` position without replacing anything.
+ /// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is replaced.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b1 = blob(10, 0x42);
+ /// let b2 = blob(5, 0x18);
+ ///
+ /// b1.splice(1, 3, b2);
+ ///
+ /// print(b1); // prints "[4218181818184242 42424242]"
+ ///
+ /// b1.splice(-5, 4, b2);
+ ///
+ /// print(b1); // prints "[4218181818184218 1818181842]"
+ /// ```
+ pub fn splice(blob: &mut Blob, start: INT, len: INT, replace: Blob) {
+ if blob.is_empty() {
+ *blob = replace;
+ return;
+ }
+
+ let (start, len) = calc_offset_len(blob.len(), start, len);
+
+ if len == 0 {
+ blob.extend(replace);
+ } else {
+ blob.splice(start..start + len, replace);
+ }
+ }
+ /// Copy an exclusive `range` of the BLOB and return it as a new BLOB.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// print(b.extract(1..3)); // prints "[0203]"
+ ///
+ /// print(b); // prints "[0102030405]"
+ /// ```
+ #[rhai_fn(name = "extract")]
+ pub fn extract_range(blob: &mut Blob, range: ExclusiveRange) -> Blob {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ extract(blob, start, end - start)
+ }
+ /// Copy an inclusive `range` of the BLOB and return it as a new BLOB.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// print(b.extract(1..=3)); // prints "[020304]"
+ ///
+ /// print(b); // prints "[0102030405]"
+ /// ```
+ #[rhai_fn(name = "extract")]
+ pub fn extract_range_inclusive(blob: &mut Blob, range: InclusiveRange) -> Blob {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ extract(blob, start, end - start + 1)
+ }
+ /// Copy a portion of the BLOB and return it as a new BLOB.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, an empty BLOB is returned.
+ /// * If `len` ≤ 0, an empty BLOB is returned.
+ /// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is copied and returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// print(b.extract(1, 3)); // prints "[020303]"
+ ///
+ /// print(b.extract(-3, 2)); // prints "[0304]"
+ ///
+ /// print(b); // prints "[0102030405]"
+ /// ```
+ pub fn extract(blob: &mut Blob, start: INT, len: INT) -> Blob {
+ if blob.is_empty() || len <= 0 {
+ return Blob::new();
+ }
+
+ let (start, len) = calc_offset_len(blob.len(), start, len);
+
+ if len == 0 {
+ Blob::new()
+ } else {
+ blob[start..start + len].to_vec()
+ }
+ }
+ /// Copy a portion of the BLOB beginning at the `start` position till the end and return it as
+ /// a new BLOB.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, the entire BLOB is copied and returned.
+ /// * If `start` ≥ length of BLOB, an empty BLOB is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// print(b.extract(2)); // prints "[030405]"
+ ///
+ /// print(b.extract(-3)); // prints "[030405]"
+ ///
+ /// print(b); // prints "[0102030405]"
+ /// ```
+ #[rhai_fn(name = "extract")]
+ pub fn extract_tail(blob: &mut Blob, start: INT) -> Blob {
+ extract(blob, start, INT::MAX)
+ }
+ /// Cut off the BLOB at `index` and return it as a new BLOB.
+ ///
+ /// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `index` is zero, the entire BLOB is cut and returned.
+ /// * If `index` < -length of BLOB, the entire BLOB is cut and returned.
+ /// * If `index` ≥ length of BLOB, nothing is cut from the BLOB and an empty BLOB is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b1 = blob();
+ ///
+ /// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+ ///
+ /// let b2 = b1.split(2);
+ ///
+ /// print(b2); // prints "[030405]"
+ ///
+ /// print(b1); // prints "[0102]"
+ /// ```
+ #[rhai_fn(name = "split")]
+ pub fn split_at(blob: &mut Blob, index: INT) -> Blob {
+ if blob.is_empty() {
+ return Blob::new();
+ }
+
+ let (index, len) = calc_offset_len(blob.len(), index, INT::MAX);
+
+ if index == 0 {
+ if len > blob.len() {
+ mem::take(blob)
+ } else {
+ let mut result = Blob::new();
+ result.extend(blob.drain(blob.len() - len..));
+ result
+ }
+ } else if index >= blob.len() {
+ Blob::new()
+ } else {
+ let mut result = Blob::new();
+ result.extend(blob.drain(index as usize..));
+ result
+ }
+ }
+ /// Remove all bytes in the BLOB within an exclusive `range` and return them as a new BLOB.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b1 = blob();
+ ///
+ /// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+ ///
+ /// let b2 = b1.drain(1..3);
+ ///
+ /// print(b1); // prints "[010405]"
+ ///
+ /// print(b2); // prints "[0203]"
+ ///
+ /// let b3 = b1.drain(2..3);
+ ///
+ /// print(b1); // prints "[0104]"
+ ///
+ /// print(b3); // prints "[05]"
+ /// ```
+ #[rhai_fn(name = "drain")]
+ pub fn drain_range(blob: &mut Blob, range: ExclusiveRange) -> Blob {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ drain(blob, start, end - start)
+ }
+ /// Remove all bytes in the BLOB within an inclusive `range` and return them as a new BLOB.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b1 = blob();
+ ///
+ /// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+ ///
+ /// let b2 = b1.drain(1..=2);
+ ///
+ /// print(b1); // prints "[010405]"
+ ///
+ /// print(b2); // prints "[0203]"
+ ///
+ /// let b3 = b1.drain(2..=2);
+ ///
+ /// print(b1); // prints "[0104]"
+ ///
+ /// print(b3); // prints "[05]"
+ /// ```
+ #[rhai_fn(name = "drain")]
+ pub fn drain_range_inclusive(blob: &mut Blob, range: InclusiveRange) -> Blob {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ drain(blob, start, end - start + 1)
+ }
+ /// Remove all bytes within a portion of the BLOB and return them as a new BLOB.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, nothing is removed and an empty BLOB is returned.
+ /// * If `len` ≤ 0, nothing is removed and an empty BLOB is returned.
+ /// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is removed and returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b1 = blob();
+ ///
+ /// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+ ///
+ /// let b2 = b1.drain(1, 2);
+ ///
+ /// print(b1); // prints "[010405]"
+ ///
+ /// print(b2); // prints "[0203]"
+ ///
+ /// let b3 = b1.drain(-1, 1);
+ ///
+ /// print(b3); // prints "[0104]"
+ ///
+ /// print(z); // prints "[5]"
+ /// ```
+ pub fn drain(blob: &mut Blob, start: INT, len: INT) -> Blob {
+ if blob.is_empty() || len <= 0 {
+ return Blob::new();
+ }
+
+ let (start, len) = calc_offset_len(blob.len(), start, len);
+
+ if len == 0 {
+ Blob::new()
+ } else {
+ blob.drain(start..start + len).collect()
+ }
+ }
+ /// Remove all bytes in the BLOB not within an exclusive `range` and return them as a new BLOB.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b1 = blob();
+ ///
+ /// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+ ///
+ /// let b2 = b1.retain(1..4);
+ ///
+ /// print(b1); // prints "[020304]"
+ ///
+ /// print(b2); // prints "[0105]"
+ ///
+ /// let b3 = b1.retain(1..3);
+ ///
+ /// print(b1); // prints "[0304]"
+ ///
+ /// print(b2); // prints "[01]"
+ /// ```
+ #[rhai_fn(name = "retain")]
+ pub fn retain_range(blob: &mut Blob, range: ExclusiveRange) -> Blob {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ retain(blob, start, end - start)
+ }
+ /// Remove all bytes in the BLOB not within an inclusive `range` and return them as a new BLOB.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b1 = blob();
+ ///
+ /// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+ ///
+ /// let b2 = b1.retain(1..=3);
+ ///
+ /// print(b1); // prints "[020304]"
+ ///
+ /// print(b2); // prints "[0105]"
+ ///
+ /// let b3 = b1.retain(1..=2);
+ ///
+ /// print(b1); // prints "[0304]"
+ ///
+ /// print(b2); // prints "[01]"
+ /// ```
+ #[rhai_fn(name = "retain")]
+ pub fn retain_range_inclusive(blob: &mut Blob, range: InclusiveRange) -> Blob {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ retain(blob, start, end - start + 1)
+ }
+ /// Remove all bytes not within a portion of the BLOB and return them as a new BLOB.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, all elements are removed returned.
+ /// * If `len` ≤ 0, all elements are removed and returned.
+ /// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB before the `start` position is removed and returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let b1 = blob();
+ ///
+ /// b1 += 1; b1 += 2; b1 += 3; b1 += 4; b1 += 5;
+ ///
+ /// let b2 = b1.retain(1, 2);
+ ///
+ /// print(b1); // prints "[0203]"
+ ///
+ /// print(b2); // prints "[010405]"
+ ///
+ /// let b3 = b1.retain(-1, 1);
+ ///
+ /// print(b1); // prints "[03]"
+ ///
+ /// print(b3); // prints "[02]"
+ /// ```
+ pub fn retain(blob: &mut Blob, start: INT, len: INT) -> Blob {
+ if blob.is_empty() || len <= 0 {
+ return Blob::new();
+ }
+
+ let (start, len) = calc_offset_len(blob.len(), start, len);
+
+ if len == 0 {
+ mem::take(blob)
+ } else {
+ let mut drained: Blob = blob.drain(..start).collect();
+ drained.extend(blob.drain(len..));
+
+ drained
+ }
+ }
+}
+
+#[export_module]
+mod parse_int_functions {
+ #[inline]
+ fn parse_int(blob: &mut Blob, start: INT, len: INT, is_le: bool) -> INT {
+ if blob.is_empty() || len <= 0 {
+ return 0;
+ }
+ let (start, len) = calc_offset_len(blob.len(), start, len);
+
+ if len == 0 {
+ return 0;
+ }
+
+ let len = usize::min(len, INT_BYTES);
+
+ let mut buf = [0_u8; INT_BYTES];
+
+ buf[..len].copy_from_slice(&blob[start..][..len]);
+
+ if is_le {
+ INT::from_le_bytes(buf)
+ } else {
+ INT::from_be_bytes(buf)
+ }
+ }
+
+ /// Parse the bytes within an exclusive `range` in the BLOB as an `INT`
+ /// in little-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+ /// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// let x = b.parse_le_int(1..3); // parse two bytes
+ ///
+ /// print(x.to_hex()); // prints "0302"
+ /// ```
+ #[rhai_fn(name = "parse_le_int")]
+ pub fn parse_le_int_range(blob: &mut Blob, range: ExclusiveRange) -> INT {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ parse_le_int(blob, start, end - start)
+ }
+ /// Parse the bytes within an inclusive `range` in the BLOB as an `INT`
+ /// in little-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+ /// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// let x = b.parse_le_int(1..=3); // parse three bytes
+ ///
+ /// print(x.to_hex()); // prints "040302"
+ /// ```
+ #[rhai_fn(name = "parse_le_int")]
+ pub fn parse_le_int_range_inclusive(blob: &mut Blob, range: InclusiveRange) -> INT {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ parse_le_int(blob, start, end - start + 1)
+ }
+ /// Parse the bytes beginning at the `start` position in the BLOB as an `INT`
+ /// in little-endian byte order.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, zero is returned.
+ /// * If `len` ≤ 0, zero is returned.
+ /// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+ ///
+ /// * If number of bytes in range < number of bytes for `INT`, zeros are padded.
+ /// * If number of bytes in range > number of bytes for `INT`, extra bytes are ignored.
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// let x = b.parse_le_int(1, 2);
+ ///
+ /// print(x.to_hex()); // prints "0302"
+ /// ```
+ pub fn parse_le_int(blob: &mut Blob, start: INT, len: INT) -> INT {
+ parse_int(blob, start, len, true)
+ }
+ /// Parse the bytes within an exclusive `range` in the BLOB as an `INT`
+ /// in big-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+ /// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// let x = b.parse_be_int(1..3); // parse two bytes
+ ///
+ /// print(x.to_hex()); // prints "02030000...00"
+ /// ```
+ #[rhai_fn(name = "parse_be_int")]
+ pub fn parse_be_int_range(blob: &mut Blob, range: ExclusiveRange) -> INT {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ parse_be_int(blob, start, end - start)
+ }
+ /// Parse the bytes within an inclusive `range` in the BLOB as an `INT`
+ /// in big-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `INT`, zeros are padded.
+ /// * If number of bytes in `range` > number of bytes for `INT`, extra bytes are ignored.
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// let x = b.parse_be_int(1..=3); // parse three bytes
+ ///
+ /// print(x.to_hex()); // prints "0203040000...00"
+ /// ```
+ #[rhai_fn(name = "parse_be_int")]
+ pub fn parse_be_int_range_inclusive(blob: &mut Blob, range: InclusiveRange) -> INT {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ parse_be_int(blob, start, end - start + 1)
+ }
+ /// Parse the bytes beginning at the `start` position in the BLOB as an `INT`
+ /// in big-endian byte order.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, zero is returned.
+ /// * If `len` ≤ 0, zero is returned.
+ /// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+ ///
+ /// * If number of bytes in range < number of bytes for `INT`, zeros are padded.
+ /// * If number of bytes in range > number of bytes for `INT`, extra bytes are ignored.
+ ///
+ /// ```rhai
+ /// let b = blob();
+ ///
+ /// b += 1; b += 2; b += 3; b += 4; b += 5;
+ ///
+ /// let x = b.parse_be_int(1, 2);
+ ///
+ /// print(x.to_hex()); // prints "02030000...00"
+ /// ```
+ pub fn parse_be_int(blob: &mut Blob, start: INT, len: INT) -> INT {
+ parse_int(blob, start, len, false)
+ }
+}
+
+#[cfg(not(feature = "no_float"))]
+#[export_module]
+mod parse_float_functions {
+ #[inline]
+ fn parse_float(blob: &mut Blob, start: INT, len: INT, is_le: bool) -> FLOAT {
+ if blob.is_empty() || len <= 0 {
+ return 0.0;
+ }
+
+ let (start, len) = calc_offset_len(blob.len(), start, len);
+
+ if len == 0 {
+ return 0.0;
+ }
+
+ let len = usize::min(len, FLOAT_BYTES);
+
+ let mut buf = [0_u8; FLOAT_BYTES];
+
+ buf[..len].copy_from_slice(&blob[start..][..len]);
+
+ if is_le {
+ FLOAT::from_le_bytes(buf)
+ } else {
+ FLOAT::from_be_bytes(buf)
+ }
+ }
+
+ /// Parse the bytes within an exclusive `range` in the BLOB as a `FLOAT`
+ /// in little-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+ /// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+ #[rhai_fn(name = "parse_le_float")]
+ pub fn parse_le_float_range(blob: &mut Blob, range: ExclusiveRange) -> FLOAT {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ parse_le_float(blob, start, end - start)
+ }
+ /// Parse the bytes within an inclusive `range` in the BLOB as a `FLOAT`
+ /// in little-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+ /// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+ #[rhai_fn(name = "parse_le_float")]
+ pub fn parse_le_float_range_inclusive(blob: &mut Blob, range: InclusiveRange) -> FLOAT {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ parse_le_float(blob, start, end - start + 1)
+ }
+ /// Parse the bytes beginning at the `start` position in the BLOB as a `FLOAT`
+ /// in little-endian byte order.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, zero is returned.
+ /// * If `len` ≤ 0, zero is returned.
+ /// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+ ///
+ /// * If number of bytes in range < number of bytes for `FLOAT`, zeros are padded.
+ /// * If number of bytes in range > number of bytes for `FLOAT`, extra bytes are ignored.
+ pub fn parse_le_float(blob: &mut Blob, start: INT, len: INT) -> FLOAT {
+ parse_float(blob, start, len, true)
+ }
+ /// Parse the bytes within an exclusive `range` in the BLOB as a `FLOAT`
+ /// in big-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+ /// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+ #[rhai_fn(name = "parse_be_float")]
+ pub fn parse_be_float_range(blob: &mut Blob, range: ExclusiveRange) -> FLOAT {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ parse_be_float(blob, start, end - start)
+ }
+ /// Parse the bytes within an inclusive `range` in the BLOB as a `FLOAT`
+ /// in big-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `FLOAT`, zeros are padded.
+ /// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes are ignored.
+ #[rhai_fn(name = "parse_be_float")]
+ pub fn parse_be_float_range_inclusive(blob: &mut Blob, range: InclusiveRange) -> FLOAT {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ parse_be_float(blob, start, end - start + 1)
+ }
+ /// Parse the bytes beginning at the `start` position in the BLOB as a `FLOAT`
+ /// in big-endian byte order.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, zero is returned.
+ /// * If `len` ≤ 0, zero is returned.
+ /// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+ ///
+ /// * If number of bytes in range < number of bytes for `FLOAT`, zeros are padded.
+ /// * If number of bytes in range > number of bytes for `FLOAT`, extra bytes are ignored.
+ pub fn parse_be_float(blob: &mut Blob, start: INT, len: INT) -> FLOAT {
+ parse_float(blob, start, len, false)
+ }
+}
+
+#[export_module]
+mod write_int_functions {
+ #[inline]
+ fn write_int(blob: &mut Blob, start: INT, len: INT, value: INT, is_le: bool) {
+ if blob.is_empty() || len <= 0 {
+ return;
+ }
+
+ let (start, len) = calc_offset_len(blob.len(), start, len);
+
+ if len == 0 {
+ return;
+ }
+
+ let len = usize::min(len, INT_BYTES);
+
+ let buf = if is_le {
+ value.to_le_bytes()
+ } else {
+ value.to_be_bytes()
+ };
+
+ blob[start..][..len].copy_from_slice(&buf[..len]);
+ }
+ /// Write an `INT` value to the bytes within an exclusive `range` in the BLOB
+ /// in little-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+ /// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+ ///
+ /// ```rhai
+ /// let b = blob(8);
+ ///
+ /// b.write_le_int(1..3, 0x12345678);
+ ///
+ /// print(b); // prints "[0078560000000000]"
+ /// ```
+ #[rhai_fn(name = "write_le")]
+ pub fn write_le_int_range(blob: &mut Blob, range: ExclusiveRange, value: INT) {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ write_le_int(blob, start, end - start, value);
+ }
+ /// Write an `INT` value to the bytes within an inclusive `range` in the BLOB
+ /// in little-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+ /// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+ ///
+ /// ```rhai
+ /// let b = blob(8);
+ ///
+ /// b.write_le_int(1..=3, 0x12345678);
+ ///
+ /// print(b); // prints "[0078563400000000]"
+ /// ```
+ #[rhai_fn(name = "write_le")]
+ pub fn write_le_int_range_inclusive(blob: &mut Blob, range: InclusiveRange, value: INT) {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ write_le_int(blob, start, end - start + 1, value);
+ }
+ /// Write an `INT` value to the bytes beginning at the `start` position in the BLOB
+ /// in little-endian byte order.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, zero is returned.
+ /// * If `len` ≤ 0, zero is returned.
+ /// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+ /// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+ ///
+ /// ```rhai
+ /// let b = blob(8);
+ ///
+ /// b.write_le_int(1, 3, 0x12345678);
+ ///
+ /// print(b); // prints "[0078563400000000]"
+ /// ```
+ #[rhai_fn(name = "write_le")]
+ pub fn write_le_int(blob: &mut Blob, start: INT, len: INT, value: INT) {
+ write_int(blob, start, len, value, true);
+ }
+ /// Write an `INT` value to the bytes within an exclusive `range` in the BLOB
+ /// in big-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+ /// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+ ///
+ /// ```rhai
+ /// let b = blob(8, 0x42);
+ ///
+ /// b.write_be_int(1..3, 0x99);
+ ///
+ /// print(b); // prints "[4200004242424242]"
+ /// ```
+ #[rhai_fn(name = "write_be")]
+ pub fn write_be_int_range(blob: &mut Blob, range: ExclusiveRange, value: INT) {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ write_be_int(blob, start, end - start, value);
+ }
+ /// Write an `INT` value to the bytes within an inclusive `range` in the BLOB
+ /// in big-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+ /// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+ ///
+ /// ```rhai
+ /// let b = blob(8, 0x42);
+ ///
+ /// b.write_be_int(1..=3, 0x99);
+ ///
+ /// print(b); // prints "[4200000042424242]"
+ /// ```
+ #[rhai_fn(name = "write_be")]
+ pub fn write_be_int_range_inclusive(blob: &mut Blob, range: InclusiveRange, value: INT) {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ write_be_int(blob, start, end - start + 1, value);
+ }
+ /// Write an `INT` value to the bytes beginning at the `start` position in the BLOB
+ /// in big-endian byte order.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, zero is returned.
+ /// * If `len` ≤ 0, zero is returned.
+ /// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `INT`, extra bytes in `INT` are not written.
+ /// * If number of bytes in `range` > number of bytes for `INT`, extra bytes in `range` are not modified.
+ ///
+ /// ```rhai
+ /// let b = blob(8, 0x42);
+ ///
+ /// b.write_be_int(1, 3, 0x99);
+ ///
+ /// print(b); // prints "[4200000042424242]"
+ /// ```
+ #[rhai_fn(name = "write_be")]
+ pub fn write_be_int(blob: &mut Blob, start: INT, len: INT, value: INT) {
+ write_int(blob, start, len, value, false);
+ }
+}
+
+#[cfg(not(feature = "no_float"))]
+#[export_module]
+mod write_float_functions {
+ #[inline]
+ fn write_float(blob: &mut Blob, start: INT, len: INT, value: FLOAT, is_le: bool) {
+ if blob.is_empty() || len <= 0 {
+ return;
+ }
+
+ let (start, len) = calc_offset_len(blob.len(), start, len);
+
+ if len == 0 {
+ return;
+ }
+
+ let len = usize::min(len, FLOAT_BYTES);
+ let buf = if is_le {
+ value.to_le_bytes()
+ } else {
+ value.to_be_bytes()
+ };
+
+ blob[start..][..len].copy_from_slice(&buf[..len]);
+ }
+ /// Write a `FLOAT` value to the bytes within an exclusive `range` in the BLOB
+ /// in little-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+ /// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+ #[rhai_fn(name = "write_le")]
+ pub fn write_le_float_range(blob: &mut Blob, range: ExclusiveRange, value: FLOAT) {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ write_le_float(blob, start, end - start, value);
+ }
+ /// Write a `FLOAT` value to the bytes within an inclusive `range` in the BLOB
+ /// in little-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+ /// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+ #[rhai_fn(name = "write_le")]
+ pub fn write_le_float_range_inclusive(blob: &mut Blob, range: InclusiveRange, value: FLOAT) {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ write_le_float(blob, start, end - start + 1, value);
+ }
+ /// Write a `FLOAT` value to the bytes beginning at the `start` position in the BLOB
+ /// in little-endian byte order.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, zero is returned.
+ /// * If `len` ≤ 0, zero is returned.
+ /// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+ /// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+ #[rhai_fn(name = "write_le")]
+ pub fn write_le_float(blob: &mut Blob, start: INT, len: INT, value: FLOAT) {
+ write_float(blob, start, len, value, true);
+ }
+ /// Write a `FLOAT` value to the bytes within an exclusive `range` in the BLOB
+ /// in big-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+ /// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+ #[rhai_fn(name = "write_be")]
+ pub fn write_be_float_range(blob: &mut Blob, range: ExclusiveRange, value: FLOAT) {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ write_be_float(blob, start, end - start, value);
+ }
+ /// Write a `FLOAT` value to the bytes within an inclusive `range` in the BLOB
+ /// in big-endian byte order.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+ /// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+ #[rhai_fn(name = "write_be")]
+ pub fn write_be_float_range_inclusive(blob: &mut Blob, range: InclusiveRange, value: FLOAT) {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ write_be_float(blob, start, end - start + 1, value);
+ }
+ /// Write a `FLOAT` value to the bytes beginning at the `start` position in the BLOB
+ /// in big-endian byte order.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, zero is returned.
+ /// * If `len` ≤ 0, zero is returned.
+ /// * If `start` position + `len` ≥ length of BLOB, entire portion of the BLOB after the `start` position is parsed.
+ ///
+ /// * If number of bytes in `range` < number of bytes for `FLOAT`, extra bytes in `FLOAT` are not written.
+ /// * If number of bytes in `range` > number of bytes for `FLOAT`, extra bytes in `range` are not modified.
+ #[rhai_fn(name = "write_be")]
+ pub fn write_be_float(blob: &mut Blob, start: INT, len: INT, value: FLOAT) {
+ write_float(blob, start, len, value, false);
+ }
+}
+
+#[export_module]
+mod write_string_functions {
+ #[inline]
+ fn write_string(blob: &mut Blob, start: INT, len: INT, string: &str, ascii_only: bool) {
+ if len <= 0 || blob.is_empty() || string.is_empty() {
+ return;
+ }
+
+ let (start, len) = calc_offset_len(blob.len(), start, len);
+
+ if len == 0 {
+ return;
+ }
+
+ let len = usize::min(len, string.len());
+
+ if ascii_only {
+ string
+ .chars()
+ .filter(char::is_ascii)
+ .take(len)
+ .map(|ch| ch as u8)
+ .enumerate()
+ .for_each(|(i, x)| blob[start + i] = x);
+ } else {
+ blob[start..][..len].copy_from_slice(&string.as_bytes()[..len]);
+ }
+ }
+ /// Write a string to the bytes within an exclusive `range` in the BLOB in UTF-8 encoding.
+ ///
+ /// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+ /// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+ ///
+ /// ```rhai
+ /// let b = blob(8);
+ ///
+ /// b.write_utf8(1..5, "朝には紅顔ありて夕べには白骨となる");
+ ///
+ /// print(b); // prints "[00e69c9de3000000]"
+ /// ```
+ #[rhai_fn(name = "write_utf8")]
+ pub fn write_utf8_string_range(blob: &mut Blob, range: ExclusiveRange, string: &str) {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ write_string(blob, start, end - start, string, false);
+ }
+ /// Write a string to the bytes within an inclusive `range` in the BLOB in UTF-8 encoding.
+ ///
+ /// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+ /// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+ ///
+ /// ```rhai
+ /// let b = blob(8);
+ ///
+ /// b.write_utf8(1..=5, "朝には紅顔ありて夕べには白骨となる");
+ ///
+ /// print(b); // prints "[00e69c9de3810000]"
+ /// ```
+ #[rhai_fn(name = "write_utf8")]
+ pub fn write_utf8_string_range_inclusive(blob: &mut Blob, range: InclusiveRange, string: &str) {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ write_string(blob, start, end - start + 1, string, false);
+ }
+ /// Write a string to the bytes within an inclusive `range` in the BLOB in UTF-8 encoding.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, the BLOB is not modified.
+ /// * If `len` ≤ 0, the BLOB is not modified.
+ /// * If `start` position + `len` ≥ length of BLOB, only the portion of the BLOB after the `start` position is modified.
+ ///
+ /// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+ /// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+ ///
+ /// ```rhai
+ /// let b = blob(8);
+ ///
+ /// b.write_utf8(1, 5, "朝には紅顔ありて夕べには白骨となる");
+ ///
+ /// print(b); // prints "[00e69c9de3810000]"
+ /// ```
+ #[rhai_fn(name = "write_utf8")]
+ pub fn write_utf8_string(blob: &mut Blob, start: INT, len: INT, string: &str) {
+ write_string(blob, start, len, string, false);
+ }
+ /// Write an ASCII string to the bytes within an exclusive `range` in the BLOB.
+ ///
+ /// Each ASCII character encodes to one single byte in the BLOB.
+ /// Non-ASCII characters are ignored.
+ ///
+ /// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+ /// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+ ///
+ /// ```rhai
+ /// let b = blob(8);
+ ///
+ /// b.write_ascii(1..5, "hello, world!");
+ ///
+ /// print(b); // prints "[0068656c6c000000]"
+ /// ```
+ #[rhai_fn(name = "write_ascii")]
+ pub fn write_ascii_string_range(blob: &mut Blob, range: ExclusiveRange, string: &str) {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ write_string(blob, start, end - start, string, true);
+ }
+ /// Write an ASCII string to the bytes within an inclusive `range` in the BLOB.
+ ///
+ /// Each ASCII character encodes to one single byte in the BLOB.
+ /// Non-ASCII characters are ignored.
+ ///
+ /// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+ /// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+ ///
+ /// ```rhai
+ /// let b = blob(8);
+ ///
+ /// b.write_ascii(1..=5, "hello, world!");
+ ///
+ /// print(b); // prints "[0068656c6c6f0000]"
+ /// ```
+ #[rhai_fn(name = "write_ascii")]
+ pub fn write_ascii_string_range_inclusive(
+ blob: &mut Blob,
+ range: InclusiveRange,
+ string: &str,
+ ) {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ write_string(blob, start, end - start + 1, string, true);
+ }
+ /// Write an ASCII string to the bytes within an exclusive `range` in the BLOB.
+ ///
+ /// * If `start` < 0, position counts from the end of the BLOB (`-1` is the last byte).
+ /// * If `start` < -length of BLOB, position counts from the beginning of the BLOB.
+ /// * If `start` ≥ length of BLOB, the BLOB is not modified.
+ /// * If `len` ≤ 0, the BLOB is not modified.
+ /// * If `start` position + `len` ≥ length of BLOB, only the portion of the BLOB after the `start` position is modified.
+ ///
+ /// * If number of bytes in `range` < length of `string`, extra bytes in `string` are not written.
+ /// * If number of bytes in `range` > length of `string`, extra bytes in `range` are not modified.
+ ///
+ /// ```rhai
+ /// let b = blob(8);
+ ///
+ /// b.write_ascii(1, 5, "hello, world!");
+ ///
+ /// print(b); // prints "[0068656c6c6f0000]"
+ /// ```
+ #[rhai_fn(name = "write_ascii")]
+ pub fn write_ascii_string(blob: &mut Blob, start: INT, len: INT, string: &str) {
+ write_string(blob, start, len, string, true);
+ }
+}
diff --git a/rhai/src/packages/debugging.rs b/rhai/src/packages/debugging.rs
new file mode 100644
index 0000000..c6b2a87
--- /dev/null
+++ b/rhai/src/packages/debugging.rs
@@ -0,0 +1,90 @@
+#![cfg(feature = "debugging")]
+
+use crate::def_package;
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_index"))]
+use crate::{Array, Dynamic, NativeCallContext};
+
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_index"))]
+#[cfg(not(feature = "no_object"))]
+use crate::Map;
+
+def_package! {
+ /// Package of basic debugging utilities.
+ pub DebuggingPackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ combine_with_exported_module!(lib, "debugging", debugging_functions);
+ }
+}
+
+#[export_module]
+mod debugging_functions {
+ /// Get an array of object maps containing the function calls stack.
+ ///
+ /// If there is no debugging interface registered, an empty array is returned.
+ ///
+ /// An array of strings is returned under `no_object`.
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "no_index"))]
+ pub fn back_trace(ctx: NativeCallContext) -> Array {
+ use crate::debugger::CallStackFrame;
+
+ if let Some(ref debugger) = ctx.global_runtime_state().debugger {
+ debugger
+ .call_stack()
+ .iter()
+ .rev()
+ .filter(|CallStackFrame { fn_name, args, .. }| {
+ fn_name.as_str() != "back_trace" || !args.is_empty()
+ })
+ .map(
+ |frame @ CallStackFrame {
+ fn_name: _fn_name,
+ args: _args,
+ source: _source,
+ pos: _pos,
+ }| {
+ let display = frame.to_string();
+
+ #[cfg(not(feature = "no_object"))]
+ {
+ use crate::INT;
+
+ let mut map = Map::new();
+ map.insert("display".into(), display.into());
+ map.insert("fn_name".into(), _fn_name.into());
+ if !_args.is_empty() {
+ map.insert(
+ "args".into(),
+ Dynamic::from_array(_args.clone().to_vec()),
+ );
+ }
+ if let Some(source) = _source {
+ map.insert("source".into(), source.into());
+ }
+ if !_pos.is_none() {
+ map.insert("line".into(), (_pos.line().unwrap() as INT).into());
+ map.insert(
+ "position".into(),
+ (_pos.position().unwrap_or(0) as INT).into(),
+ );
+ }
+ Dynamic::from_map(map)
+ }
+ #[cfg(feature = "no_object")]
+ display.into()
+ },
+ )
+ .collect()
+ } else {
+ Array::new()
+ }
+ }
+}
diff --git a/rhai/src/packages/fn_basic.rs b/rhai/src/packages/fn_basic.rs
new file mode 100644
index 0000000..5e2bfcb
--- /dev/null
+++ b/rhai/src/packages/fn_basic.rs
@@ -0,0 +1,48 @@
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+use crate::{def_package, FnPtr, ImmutableString, NativeCallContext};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+def_package! {
+ /// Package of basic function pointer utilities.
+ pub BasicFnPackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ combine_with_exported_module!(lib, "FnPtr", fn_ptr_functions);
+ }
+}
+
+#[export_module]
+mod fn_ptr_functions {
+ /// Return the name of the function.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// fn double(x) { x * 2 }
+ ///
+ /// let f = Fn("double");
+ ///
+ /// print(f.name); // prints "double"
+ /// ```
+ #[rhai_fn(name = "name", get = "name", pure)]
+ pub fn name(fn_ptr: &mut FnPtr) -> ImmutableString {
+ fn_ptr.fn_name_raw().clone()
+ }
+
+ /// Return `true` if the function is an anonymous function.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let f = |x| x * 2;
+ ///
+ /// print(f.is_anonymous); // prints true
+ /// ```
+ #[cfg(not(feature = "no_function"))]
+ #[rhai_fn(name = "is_anonymous", get = "is_anonymous", pure)]
+ pub fn is_anonymous(fn_ptr: &mut FnPtr) -> bool {
+ fn_ptr.is_anonymous()
+ }
+}
diff --git a/rhai/src/packages/iter_basic.rs b/rhai/src/packages/iter_basic.rs
new file mode 100644
index 0000000..50dc7b6
--- /dev/null
+++ b/rhai/src/packages/iter_basic.rs
@@ -0,0 +1,714 @@
+use crate::eval::calc_index;
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+use crate::{
+ def_package, ExclusiveRange, InclusiveRange, RhaiResultOf, ERR, INT, INT_BITS, MAX_USIZE_INT,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ any::type_name,
+ cmp::Ordering,
+ fmt::Debug,
+ iter::{ExactSizeIterator, FusedIterator},
+ ops::{Range, RangeInclusive},
+};
+
+#[cfg(not(feature = "no_float"))]
+use crate::FLOAT;
+
+#[cfg(feature = "decimal")]
+use rust_decimal::Decimal;
+
+#[cfg(not(feature = "unchecked"))]
+#[inline(always)]
+#[allow(clippy::needless_pass_by_value)]
+fn std_add<T>(x: T, y: T) -> Option<T>
+where
+ T: num_traits::CheckedAdd<Output = T>,
+{
+ x.checked_add(&y)
+}
+#[inline(always)]
+#[allow(dead_code)]
+#[allow(clippy::unnecessary_wraps, clippy::needless_pass_by_value)]
+fn regular_add<T>(x: T, y: T) -> Option<T>
+where
+ T: std::ops::Add<Output = T>,
+{
+ Some(x + y)
+}
+
+// Range iterator with step
+#[derive(Clone, Hash, Eq, PartialEq)]
+pub struct StepRange<T> {
+ pub from: T,
+ pub to: T,
+ pub step: T,
+ pub add: fn(T, T) -> Option<T>,
+ pub dir: i8,
+}
+
+impl<T: Debug> Debug for StepRange<T> {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_tuple(&format!("StepRange<{}>", type_name::<T>()))
+ .field(&self.from)
+ .field(&self.to)
+ .field(&self.step)
+ .finish()
+ }
+}
+
+impl<T: Copy + PartialOrd> StepRange<T> {
+ pub fn new(from: T, to: T, step: T, add: fn(T, T) -> Option<T>) -> RhaiResultOf<Self> {
+ let mut dir = 0;
+
+ if let Some(n) = add(from, step) {
+ #[cfg(not(feature = "unchecked"))]
+ if n == from {
+ return Err(ERR::ErrorInFunctionCall(
+ "range".to_string(),
+ String::new(),
+ ERR::ErrorArithmetic("step value cannot be zero".to_string(), Position::NONE)
+ .into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ match from.partial_cmp(&to).unwrap_or(Ordering::Equal) {
+ Ordering::Less if n > from => dir = 1,
+ Ordering::Greater if n < from => dir = -1,
+ _ => (),
+ }
+ }
+
+ Ok(Self {
+ from,
+ to,
+ step,
+ add,
+ dir,
+ })
+ }
+}
+
+impl<T: Copy + PartialOrd> Iterator for StepRange<T> {
+ type Item = T;
+
+ fn next(&mut self) -> Option<T> {
+ if self.dir == 0 {
+ return None;
+ }
+
+ let v = self.from;
+
+ self.from = (self.add)(self.from, self.step)?;
+
+ match self.dir.cmp(&0) {
+ Ordering::Greater if self.from >= self.to => self.dir = 0,
+ Ordering::Less if self.from <= self.to => self.dir = 0,
+ Ordering::Equal => unreachable!("`dir` != 0"),
+ _ => (),
+ }
+
+ Some(v)
+ }
+}
+
+impl<T: Copy + PartialOrd> FusedIterator for StepRange<T> {}
+
+// Bit-field iterator with step
+#[derive(Debug, Clone, Hash, Eq, PartialEq)]
+pub struct BitRange(INT, usize);
+
+impl BitRange {
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ pub fn new(value: INT, from: INT, len: INT) -> RhaiResultOf<Self> {
+ let from = calc_index(INT_BITS, from, true, || {
+ ERR::ErrorBitFieldBounds(INT_BITS, from, Position::NONE).into()
+ })?;
+
+ let len = if len < 0 {
+ 0
+ } else if from + (len as usize) > INT_BITS {
+ INT_BITS - from
+ } else {
+ len as usize
+ };
+
+ Ok(Self(value >> from, len))
+ }
+}
+
+impl Iterator for BitRange {
+ type Item = bool;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.1 == 0 {
+ None
+ } else {
+ let r = (self.0 & 0x0001) != 0;
+ self.0 >>= 1;
+ self.1 -= 1;
+ Some(r)
+ }
+ }
+
+ #[inline(always)]
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ (self.1, Some(self.1))
+ }
+}
+
+impl FusedIterator for BitRange {}
+
+impl ExactSizeIterator for BitRange {
+ #[inline(always)]
+ fn len(&self) -> usize {
+ self.1
+ }
+}
+
+// String iterator over characters
+#[derive(Debug, Clone, Hash, Eq, PartialEq)]
+pub struct CharsStream(Vec<char>, usize);
+
+impl CharsStream {
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ pub fn new(string: &str, from: INT, len: INT) -> Self {
+ if len <= 0 || from > MAX_USIZE_INT {
+ return Self(Vec::new(), 0);
+ }
+ let len = len.min(MAX_USIZE_INT) as usize;
+
+ if from >= 0 {
+ return Self(string.chars().skip(from as usize).take(len).collect(), 0);
+ }
+
+ let abs_from = from.unsigned_abs() as usize;
+ let num_chars = string.chars().count();
+ let offset = if num_chars < abs_from {
+ 0
+ } else {
+ num_chars - abs_from
+ };
+ Self(string.chars().skip(offset).take(len).collect(), 0)
+ }
+}
+
+impl Iterator for CharsStream {
+ type Item = char;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.1 >= self.0.len() {
+ None
+ } else {
+ let ch = self.0[self.1];
+ self.1 += 1;
+ Some(ch)
+ }
+ }
+
+ #[inline]
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ let remaining = self.0.len() - self.1;
+ (remaining, Some(remaining))
+ }
+}
+
+impl FusedIterator for CharsStream {}
+
+impl ExactSizeIterator for CharsStream {
+ #[inline]
+ fn len(&self) -> usize {
+ self.0.len() - self.1
+ }
+}
+
+macro_rules! reg_range {
+ ($lib:ident | $x:expr => $( $y:ty ),*) => {
+ $(
+ $lib.set_iterator::<Range<$y>>();
+ let _hash = $lib.set_native_fn($x, |from: $y, to: $y| Ok(from..to));
+
+ #[cfg(feature = "metadata")]
+ $lib.update_fn_metadata_with_comments(_hash, [
+ concat!("from: ", stringify!($y)),
+ concat!("to: ", stringify!($y)),
+ concat!("Iterator<", stringify!($y), ">"),
+ ], ["\
+ /// Return an iterator over the exclusive range of `from..to`.\n\
+ /// The value `to` is never included.\n\
+ ///\n\
+ /// # Example\n\
+ ///\n\
+ /// ```rhai\n\
+ /// // prints all values from 8 to 17\n\
+ /// for n in range(8, 18) {\n\
+ /// print(n);\n\
+ /// }\n\
+ /// ```"
+ ]);
+
+ $lib.set_iterator::<RangeInclusive<$y>>();
+ )*
+ };
+ ($lib:ident | step $x:expr => $( $y:ty ),*) => {
+ #[cfg(not(feature = "unchecked"))]
+ reg_range!($lib | step(std_add) $x => $( $y ),*);
+ #[cfg(feature = "unchecked")]
+ reg_range!($lib | step(regular_add) $x => $( $y ),*);
+ };
+ ($lib:ident | step ( $add:ident ) $x:expr => $( $y:ty ),*) => {
+ $(
+ $lib.set_iterator::<StepRange<$y>>();
+ let _hash = $lib.set_native_fn($x, |from: $y, to: $y, step: $y| StepRange::new(from, to, step, $add));
+
+ #[cfg(feature = "metadata")]
+ $lib.update_fn_metadata_with_comments(_hash, [
+ concat!("from: ", stringify!($y)),
+ concat!("to: ", stringify!($y)),
+ concat!("step: ", stringify!($y)),
+ concat!("Iterator<", stringify!($y), ">")
+ ], ["\
+ /// Return an iterator over the exclusive range of `from..to`, each iteration increasing by `step`.\n\
+ /// The value `to` is never included.\n\
+ ///\n\
+ /// If `from` > `to` and `step` < 0, iteration goes backwards.\n\
+ ///\n\
+ /// If `from` > `to` and `step` > 0 or `from` < `to` and `step` < 0, an empty iterator is returned.\n\
+ ///\n\
+ /// # Example\n\
+ ///\n\
+ /// ```rhai\n\
+ /// // prints all values from 8 to 17 in steps of 3\n\
+ /// for n in range(8, 18, 3) {\n\
+ /// print(n);\n\
+ /// }\n\
+ ///\n\
+ /// // prints all values down from 18 to 9 in steps of -3\n\
+ /// for n in range(18, 8, -3) {\n\
+ /// print(n);\n\
+ /// }\n\
+ /// ```"
+ ]);
+
+ let _hash = $lib.set_native_fn($x, |range: std::ops::Range<$y>, step: $y| StepRange::new(range.start, range.end, step, $add));
+
+ #[cfg(feature = "metadata")]
+ $lib.update_fn_metadata_with_comments(_hash, [
+ concat!("range: Range<", stringify!($y), ">"),
+ concat!("step: ", stringify!($y)),
+ concat!("Iterator<", stringify!($y), ">")
+ ], ["\
+ /// Return an iterator over an exclusive range, each iteration increasing by `step`.\n\
+ ///\n\
+ /// If `range` is reversed and `step` < 0, iteration goes backwards.\n\
+ ///\n\
+ /// Otherwise, if `range` is empty, an empty iterator is returned.\n\
+ ///\n\
+ /// # Example\n\
+ ///\n\
+ /// ```rhai\n\
+ /// // prints all values from 8 to 17 in steps of 3\n\
+ /// for n in range(8..18, 3) {\n\
+ /// print(n);\n\
+ /// }\n\
+ ///\n\
+ /// // prints all values down from 18 to 9 in steps of -3\n\
+ /// for n in range(18..8, -3) {\n\
+ /// print(n);\n\
+ /// }\n\
+ /// ```"
+ ]);
+ )*
+ };
+}
+
+def_package! {
+ /// Package of basic range iterators
+ pub BasicIteratorPackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ reg_range!(lib | "range" => INT);
+
+ #[cfg(not(feature = "only_i32"))]
+ #[cfg(not(feature = "only_i64"))]
+ {
+ reg_range!(lib | "range" => i8, u8, i16, u16, i32, u32, i64, u64);
+
+ #[cfg(not(target_family = "wasm"))]
+ reg_range!(lib | "range" => i128, u128);
+ }
+
+ reg_range!(lib | step "range" => INT);
+
+ #[cfg(not(feature = "only_i32"))]
+ #[cfg(not(feature = "only_i64"))]
+ {
+ reg_range!(lib | step "range" => i8, u8, i16, u16, i32, u32, i64, u64);
+
+ #[cfg(not(target_family = "wasm"))]
+ reg_range!(lib | step "range" => i128, u128);
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ reg_range!(lib | step(regular_add) "range" => FLOAT);
+
+ #[cfg(feature = "decimal")]
+ reg_range!(lib | step "range" => Decimal);
+
+ // Register string iterator
+ lib.set_iterator::<CharsStream>();
+
+ #[cfg(feature = "metadata")]
+ let (range_type, range_inclusive_type) = (
+ format!("range: Range<{}>", type_name::<INT>()),
+ format!("range: RangeInclusive<{}>", type_name::<INT>()),
+ );
+
+ let _hash = lib.set_native_fn("chars", |string, range: ExclusiveRange| {
+ let from = INT::max(range.start, 0);
+ let to = INT::max(range.end, from);
+ Ok(CharsStream::new(string, from, to - from))
+ });
+ #[cfg(feature = "metadata")]
+ lib.update_fn_metadata_with_comments(
+ _hash,
+ ["string: &str", &range_type, "Iterator<char>"],
+ [
+ "/// Return an iterator over an exclusive range of characters in the string.",
+ "///",
+ "/// # Example",
+ "///",
+ "/// ```rhai",
+ r#"/// for ch in "hello, world!".chars(2..5) {"#,
+ "/// print(ch);",
+ "/// }",
+ "/// ```"
+ ]
+ );
+
+ let _hash = lib.set_native_fn("chars", |string, range: InclusiveRange| {
+ let from = INT::max(*range.start(), 0);
+ let to = INT::max(*range.end(), from - 1);
+ Ok(CharsStream::new(string, from, to-from + 1))
+ });
+ #[cfg(feature = "metadata")]
+ lib.update_fn_metadata_with_comments(
+ _hash,
+ ["string: &str", &range_inclusive_type, "Iterator<char>"],
+ [
+ "/// Return an iterator over an inclusive range of characters in the string.",
+ "///",
+ "/// # Example",
+ "///",
+ "/// ```rhai",
+ r#"/// for ch in "hello, world!".chars(2..=6) {"#,
+ "/// print(ch);",
+ "/// }",
+ "/// ```"
+ ]
+ );
+
+ let _hash = lib.set_native_fn("chars", |string, from, len| Ok(CharsStream::new(string, from, len)));
+ #[cfg(feature = "metadata")]
+ lib.update_fn_metadata_with_comments(
+ _hash,
+ ["string: &str", "start: INT", "len: INT", "Iterator<char>"],
+ [
+ "/// Return an iterator over a portion of characters in the string.",
+ "///",
+ "/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).",
+ "/// * If `start` < -length of string, position counts from the beginning of the string.",
+ "/// * If `start` ≥ length of string, an empty iterator is returned.",
+ "/// * If `len` ≤ 0, an empty iterator is returned.",
+ "/// * If `start` position + `len` ≥ length of string, all characters of the string after the `start` position are iterated.",
+ "///",
+ "/// # Example",
+ "///",
+ "/// ```rhai",
+ r#"/// for ch in "hello, world!".chars(2, 4) {"#,
+ "/// print(ch);",
+ "/// }",
+ "/// ```"
+ ]
+ );
+
+ let _hash = lib.set_native_fn("chars", |string, from| Ok(CharsStream::new(string, from, INT::MAX)));
+ #[cfg(feature = "metadata")]
+ lib.update_fn_metadata_with_comments(
+ _hash,
+ ["string: &str", "from: INT", "Iterator<char>"],
+ [
+ "/// Return an iterator over the characters in the string starting from the `start` position.",
+ "///",
+ "/// * If `start` < 0, position counts from the end of the string (`-1` is the last character).",
+ "/// * If `start` < -length of string, position counts from the beginning of the string.",
+ "/// * If `start` ≥ length of string, an empty iterator is returned.",
+ "///",
+ "/// # Example",
+ "///",
+ "/// ```rhai",
+ r#"/// for ch in "hello, world!".chars(2) {"#,
+ "/// print(ch);",
+ "/// }",
+ "/// ```"
+ ]
+ );
+
+ let _hash = lib.set_native_fn("chars", |string| Ok(CharsStream::new(string, 0, INT::MAX)));
+ #[cfg(feature = "metadata")]
+ lib.update_fn_metadata_with_comments(
+ _hash,
+ ["string: &str", "Iterator<char>"],
+ [
+ "/// Return an iterator over the characters in the string.",
+ "///",
+ "/// # Example",
+ "///",
+ "/// ```rhai",
+ r#"/// for ch in "hello, world!".chars() {"#,
+ "/// print(ch);",
+ "/// }",
+ "/// ```"
+ ]
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ {
+ let _hash = lib.set_getter_fn("chars", |string: &mut ImmutableString| Ok(CharsStream::new(string, 0, INT::MAX)));
+ #[cfg(feature = "metadata")]
+ lib.update_fn_metadata_with_comments(
+ _hash,
+ ["string: &mut ImmutableString", "Iterator<char>"],
+ [
+ "/// Return an iterator over all the characters in the string.",
+ "///",
+ "/// # Example",
+ "///",
+ "/// ```rhai",
+ r#"/// for ch in "hello, world!".chars {"#,
+ "/// print(ch);",
+ "/// }",
+ "/// ```"
+ ]
+ );
+ }
+
+ // Register bit-field iterator
+ lib.set_iterator::<BitRange>();
+
+ let _hash = lib.set_native_fn("bits", |value, range: ExclusiveRange| {
+ let from = INT::max(range.start, 0);
+ let to = INT::max(range.end, from);
+ BitRange::new(value, from, to - from)
+ });
+ #[cfg(feature = "metadata")]
+ lib.update_fn_metadata_with_comments(
+ _hash,
+ ["value: INT", &range_type, "Iterator<bool>"],
+ [
+ "/// Return an iterator over an exclusive range of bits in the number.",
+ "///",
+ "/// # Example",
+ "///",
+ "/// ```rhai",
+ "/// let x = 123456;",
+ "///",
+ "/// for bit in x.bits(10..24) {",
+ "/// print(bit);",
+ "/// }",
+ "/// ```"
+ ]
+ );
+
+ let _hash = lib.set_native_fn("bits", |value, range: InclusiveRange| {
+ let from = INT::max(*range.start(), 0);
+ let to = INT::max(*range.end(), from - 1);
+ BitRange::new(value, from, to - from + 1)
+ });
+ #[cfg(feature = "metadata")]
+ lib.update_fn_metadata_with_comments(
+ _hash,
+ ["value: INT", &range_inclusive_type, "Iterator<bool>"],
+ [
+ "/// Return an iterator over an inclusive range of bits in the number.",
+ "///",
+ "/// # Example",
+ "///",
+ "/// ```rhai",
+ "/// let x = 123456;",
+ "///",
+ "/// for bit in x.bits(10..=23) {",
+ "/// print(bit);",
+ "/// }",
+ "/// ```"
+ ]
+ );
+
+ let _hash = lib.set_native_fn("bits", BitRange::new);
+ #[cfg(feature = "metadata")]
+ lib.update_fn_metadata_with_comments(
+ _hash,
+ ["value: INT", "from: INT", "len: INT", "Iterator<bool>"],
+ [
+ "/// Return an iterator over a portion of bits in the number.",
+ "///",
+ "/// * If `start` < 0, position counts from the MSB (Most Significant Bit)>.",
+ "/// * If `len` ≤ 0, an empty iterator is returned.",
+ "/// * If `start` position + `len` ≥ length of string, all bits of the number after the `start` position are iterated.",
+ "///",
+ "/// # Example",
+ "///",
+ "/// ```rhai",
+ "/// let x = 123456;",
+ "///",
+ "/// for bit in x.bits(10, 8) {",
+ "/// print(bit);",
+ "/// }",
+ "/// ```"
+ ]
+ );
+
+ let _hash = lib.set_native_fn("bits", |value, from| BitRange::new(value, from, INT::MAX));
+ #[cfg(feature = "metadata")]
+ lib.update_fn_metadata_with_comments(
+ _hash,
+ ["value: INT", "from: INT", "Iterator<bool>"],
+ [
+ "/// Return an iterator over the bits in the number starting from the specified `start` position.",
+ "///",
+ "/// If `start` < 0, position counts from the MSB (Most Significant Bit)>.",
+ "///",
+ "/// # Example",
+ "///",
+ "/// ```rhai",
+ "/// let x = 123456;",
+ "///",
+ "/// for bit in x.bits(10) {",
+ "/// print(bit);",
+ "/// }",
+ "/// ```"
+ ]
+ );
+
+ let _hash = lib.set_native_fn("bits", |value| BitRange::new(value, 0, INT::MAX) );
+ #[cfg(feature = "metadata")]
+ lib.update_fn_metadata_with_comments(
+ _hash,
+ ["value: INT", "Iterator<bool>"],
+ [
+ "/// Return an iterator over all the bits in the number.",
+ "///",
+ "/// # Example",
+ "///",
+ "/// ```rhai",
+ "/// let x = 123456;",
+ "///",
+ "/// for bit in x.bits() {",
+ "/// print(bit);",
+ "/// }",
+ "/// ```"
+ ]
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ {
+ let _hash = lib.set_getter_fn("bits", |value: &mut INT| BitRange::new(*value, 0, INT::MAX) );
+ #[cfg(feature = "metadata")]
+ lib.update_fn_metadata_with_comments(
+ _hash,
+ ["value: &mut INT", "Iterator<bool>"],
+ [
+ "/// Return an iterator over all the bits in the number.",
+ "///",
+ "/// # Example",
+ "///",
+ "/// ```rhai",
+ "/// let x = 123456;",
+ "///",
+ "/// for bit in x.bits {",
+ "/// print(bit);",
+ "/// }",
+ "/// ```"
+ ]
+ );
+ }
+
+ combine_with_exported_module!(lib, "range", range_functions);
+ }
+}
+
+#[export_module]
+mod range_functions {
+ /// Return the start of the exclusive range.
+ #[rhai_fn(get = "start", name = "start", pure)]
+ pub fn start(range: &mut ExclusiveRange) -> INT {
+ range.start
+ }
+ /// Return the end of the exclusive range.
+ #[rhai_fn(get = "end", name = "end", pure)]
+ pub fn end(range: &mut ExclusiveRange) -> INT {
+ range.end
+ }
+ /// Return `true` if the range is inclusive.
+ #[rhai_fn(get = "is_inclusive", name = "is_inclusive", pure)]
+ pub fn is_inclusive(range: &mut ExclusiveRange) -> bool {
+ let _ = range;
+ false
+ }
+ /// Return `true` if the range is exclusive.
+ #[rhai_fn(get = "is_exclusive", name = "is_exclusive", pure)]
+ pub fn is_exclusive(range: &mut ExclusiveRange) -> bool {
+ let _ = range;
+ true
+ }
+ /// Return true if the range contains no items.
+ #[rhai_fn(get = "is_empty", name = "is_empty", pure)]
+ #[allow(unstable_name_collisions)]
+ pub fn is_empty_exclusive(range: &mut ExclusiveRange) -> bool {
+ range.is_empty()
+ }
+ /// Return `true` if the range contains a specified value.
+ #[rhai_fn(name = "contains")]
+ pub fn contains_exclusive(range: &mut ExclusiveRange, value: INT) -> bool {
+ range.contains(&value)
+ }
+
+ /// Return the start of the inclusive range.
+ #[rhai_fn(get = "start", name = "start", pure)]
+ pub fn start_inclusive(range: &mut InclusiveRange) -> INT {
+ *range.start()
+ }
+ /// Return the end of the inclusive range.
+ #[rhai_fn(get = "end", name = "end", pure)]
+ pub fn end_inclusive(range: &mut InclusiveRange) -> INT {
+ *range.end()
+ }
+ /// Return `true` if the range is inclusive.
+ #[rhai_fn(get = "is_inclusive", name = "is_inclusive", pure)]
+ pub fn is_inclusive_inclusive(range: &mut InclusiveRange) -> bool {
+ let _ = range;
+ true
+ }
+ /// Return `true` if the range is exclusive.
+ #[rhai_fn(get = "is_exclusive", name = "is_exclusive", pure)]
+ pub fn is_exclusive_inclusive(range: &mut InclusiveRange) -> bool {
+ let _ = range;
+ false
+ }
+ /// Return true if the range contains no items.
+ #[rhai_fn(get = "is_empty", name = "is_empty", pure)]
+ pub fn is_empty_inclusive(range: &mut InclusiveRange) -> bool {
+ range.is_empty()
+ }
+ /// Return `true` if the range contains a specified value.
+ #[rhai_fn(name = "contains")]
+ pub fn contains_inclusive(range: &mut InclusiveRange, value: INT) -> bool {
+ range.contains(&value)
+ }
+}
diff --git a/rhai/src/packages/lang_core.rs b/rhai/src/packages/lang_core.rs
new file mode 100644
index 0000000..6020c7e
--- /dev/null
+++ b/rhai/src/packages/lang_core.rs
@@ -0,0 +1,349 @@
+use crate::def_package;
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+use crate::types::dynamic::Tag;
+use crate::{Dynamic, RhaiResultOf, ERR, INT};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(not(feature = "no_float"))]
+#[cfg(not(feature = "no_std"))]
+use crate::FLOAT;
+
+def_package! {
+ /// Package of core language features.
+ pub LanguageCorePackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ combine_with_exported_module!(lib, "core", core_functions);
+
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "no_index"))]
+ #[cfg(not(feature = "no_object"))]
+ combine_with_exported_module!(lib, "reflection", reflection_functions);
+ }
+}
+
+#[export_module]
+mod core_functions {
+ /// Take ownership of the data in a `Dynamic` value and return it.
+ /// The data is _NOT_ cloned.
+ ///
+ /// The original value is replaced with `()`.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = 42;
+ ///
+ /// print(take(x)); // prints 42
+ ///
+ /// print(x); // prints ()
+ /// ```
+ #[rhai_fn(return_raw)]
+ pub fn take(value: &mut Dynamic) -> RhaiResultOf<Dynamic> {
+ if value.is_read_only() {
+ return Err(
+ ERR::ErrorNonPureMethodCallOnConstant("take".to_string(), Position::NONE).into(),
+ );
+ }
+
+ Ok(std::mem::take(value))
+ }
+ /// Return the _tag_ of a `Dynamic` value.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = "hello, world!";
+ ///
+ /// x.tag = 42;
+ ///
+ /// print(x.tag); // prints 42
+ /// ```
+ #[rhai_fn(name = "tag", get = "tag", pure)]
+ pub fn get_tag(value: &mut Dynamic) -> INT {
+ value.tag() as INT
+ }
+ /// Set the _tag_ of a `Dynamic` value.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = "hello, world!";
+ ///
+ /// x.tag = 42;
+ ///
+ /// print(x.tag); // prints 42
+ /// ```
+ #[rhai_fn(name = "set_tag", set = "tag", return_raw)]
+ pub fn set_tag(value: &mut Dynamic, tag: INT) -> RhaiResultOf<()> {
+ const TAG_MIN: Tag = Tag::MIN;
+ const TAG_MAX: Tag = Tag::MAX;
+
+ if tag < TAG_MIN as INT {
+ Err(ERR::ErrorArithmetic(
+ format!(
+ "{tag} is too small to fit into a tag (must be between {TAG_MIN} and {TAG_MAX})"
+ ),
+ Position::NONE,
+ )
+ .into())
+ } else if tag > TAG_MAX as INT {
+ Err(ERR::ErrorArithmetic(
+ format!(
+ "{tag} is too large to fit into a tag (must be between {TAG_MIN} and {TAG_MAX})"
+ ),
+ Position::NONE,
+ )
+ .into())
+ } else {
+ value.set_tag(tag as Tag);
+ Ok(())
+ }
+ }
+
+ /// Block the current thread for a particular number of `seconds`.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// // Do nothing for 10 seconds!
+ /// sleep(10.0);
+ /// ```
+ #[cfg(not(feature = "no_float"))]
+ #[cfg(not(feature = "no_std"))]
+ #[rhai_fn(name = "sleep")]
+ pub fn sleep_float(seconds: FLOAT) {
+ if seconds <= 0.0 {
+ return;
+ }
+
+ #[cfg(not(feature = "f32_float"))]
+ std::thread::sleep(std::time::Duration::from_secs_f64(seconds));
+ #[cfg(feature = "f32_float")]
+ std::thread::sleep(std::time::Duration::from_secs_f32(seconds));
+ }
+ /// Block the current thread for a particular number of `seconds`.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// // Do nothing for 10 seconds!
+ /// sleep(10);
+ /// ```
+ #[cfg(not(feature = "no_std"))]
+ pub fn sleep(seconds: INT) {
+ if seconds > 0 {
+ #[allow(clippy::cast_sign_loss)]
+ std::thread::sleep(std::time::Duration::from_secs(seconds as u64));
+ }
+ }
+
+ /// Parse a JSON string into a value.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let m = parse_json(`{"a":1, "b":2, "c":3}`);
+ ///
+ /// print(m); // prints #{"a":1, "b":2, "c":3}
+ /// ```
+ #[cfg(not(feature = "no_index"))]
+ #[cfg(not(feature = "no_object"))]
+ #[cfg(feature = "metadata")]
+ #[rhai_fn(return_raw)]
+ pub fn parse_json(_ctx: NativeCallContext, json: &str) -> RhaiResultOf<Dynamic> {
+ serde_json::from_str(json).map_err(|err| err.to_string().into())
+ }
+}
+
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_index"))]
+#[cfg(not(feature = "no_object"))]
+#[export_module]
+mod reflection_functions {
+ use crate::Array;
+
+ /// Return an array of object maps containing metadata of all script-defined functions.
+ pub fn get_fn_metadata_list(ctx: NativeCallContext) -> Array {
+ collect_fn_metadata(&ctx, |_, _, _, _, _| true)
+ }
+ /// Return an array of object maps containing metadata of all script-defined functions
+ /// matching the specified name.
+ #[rhai_fn(name = "get_fn_metadata_list")]
+ pub fn get_fn_metadata(ctx: NativeCallContext, name: &str) -> Array {
+ collect_fn_metadata(&ctx, |_, _, n, _, _| n == name)
+ }
+ /// Return an array of object maps containing metadata of all script-defined functions
+ /// matching the specified name and arity (number of parameters).
+ #[rhai_fn(name = "get_fn_metadata_list")]
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ pub fn get_fn_metadata2(ctx: NativeCallContext, name: &str, params: INT) -> Array {
+ if (0..=crate::MAX_USIZE_INT).contains(¶ms) {
+ collect_fn_metadata(&ctx, |_, _, n, p, _| p == (params as usize) && n == name)
+ } else {
+ Array::new()
+ }
+ }
+}
+
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_index"))]
+#[cfg(not(feature = "no_object"))]
+fn collect_fn_metadata(
+ ctx: &NativeCallContext,
+ filter: impl Fn(FnNamespace, FnAccess, &str, usize, &crate::Shared<crate::ast::ScriptFnDef>) -> bool
+ + Copy,
+) -> crate::Array {
+ #[cfg(not(feature = "no_module"))]
+ use crate::Identifier;
+ use crate::{ast::ScriptFnDef, engine::FN_ANONYMOUS, Array, Map};
+
+ // Create a metadata record for a function.
+ fn make_metadata(
+ engine: &Engine,
+ #[cfg(not(feature = "no_module"))] namespace: Identifier,
+ func: &ScriptFnDef,
+ ) -> Map {
+ let mut map = Map::new();
+
+ #[cfg(not(feature = "no_module"))]
+ if !namespace.is_empty() {
+ map.insert(
+ "namespace".into(),
+ engine.get_interned_string(namespace).into(),
+ );
+ }
+ map.insert(
+ "name".into(),
+ engine.get_interned_string(func.name.clone()).into(),
+ );
+ map.insert(
+ "access".into(),
+ engine
+ .get_interned_string(match func.access {
+ FnAccess::Public => "public",
+ FnAccess::Private => "private",
+ })
+ .into(),
+ );
+ map.insert(
+ "is_anonymous".into(),
+ func.name.starts_with(FN_ANONYMOUS).into(),
+ );
+ #[cfg(not(feature = "no_object"))]
+ if let Some(ref this_type) = func.this_type {
+ map.insert("this_type".into(), this_type.into());
+ }
+ map.insert(
+ "params".into(),
+ func.params
+ .iter()
+ .map(|p| engine.get_interned_string(p.clone()).into())
+ .collect::<Array>()
+ .into(),
+ );
+ #[cfg(feature = "metadata")]
+ if !func.comments.is_empty() {
+ map.insert(
+ "comments".into(),
+ func.comments
+ .iter()
+ .map(|s| engine.get_interned_string(s.as_str()).into())
+ .collect::<Array>()
+ .into(),
+ );
+ }
+
+ map
+ }
+
+ let engine = ctx.engine();
+ let mut list = Array::new();
+
+ ctx.iter_namespaces()
+ .flat_map(Module::iter_script_fn)
+ .filter(|(s, a, n, p, f)| filter(*s, *a, n, *p, f))
+ .for_each(|(.., f)| {
+ list.push(
+ make_metadata(
+ engine,
+ #[cfg(not(feature = "no_module"))]
+ Identifier::new_const(),
+ f,
+ )
+ .into(),
+ );
+ });
+
+ ctx.engine()
+ .global_modules
+ .iter()
+ .flat_map(|m| m.iter_script_fn())
+ .filter(|(ns, a, n, p, f)| filter(*ns, *a, n, *p, f))
+ .for_each(|(.., f)| {
+ list.push(
+ make_metadata(
+ engine,
+ #[cfg(not(feature = "no_module"))]
+ Identifier::new_const(),
+ f,
+ )
+ .into(),
+ );
+ });
+
+ #[cfg(not(feature = "no_module"))]
+ ctx.engine()
+ .global_sub_modules
+ .as_ref()
+ .into_iter()
+ .flatten()
+ .flat_map(|(_, m)| m.iter_script_fn())
+ .filter(|(ns, a, n, p, f)| filter(*ns, *a, n, *p, f))
+ .for_each(|(.., f)| {
+ list.push(
+ make_metadata(
+ engine,
+ #[cfg(not(feature = "no_module"))]
+ Identifier::new_const(),
+ f,
+ )
+ .into(),
+ );
+ });
+
+ #[cfg(not(feature = "no_module"))]
+ {
+ use crate::engine::NAMESPACE_SEPARATOR;
+ use crate::{Shared, SmartString};
+
+ // Recursively scan modules for script-defined functions.
+ fn scan_module(
+ engine: &Engine,
+ list: &mut Array,
+ namespace: &str,
+ module: &Module,
+ filter: impl Fn(FnNamespace, FnAccess, &str, usize, &Shared<ScriptFnDef>) -> bool + Copy,
+ ) {
+ module
+ .iter_script_fn()
+ .filter(|(s, a, n, p, f)| filter(*s, *a, n, *p, f))
+ .for_each(|(.., f)| list.push(make_metadata(engine, namespace.into(), f).into()));
+ for (name, m) in module.iter_sub_modules() {
+ use std::fmt::Write;
+
+ let mut ns = SmartString::new_const();
+ write!(&mut ns, "{namespace}{}{name}", NAMESPACE_SEPARATOR).unwrap();
+ scan_module(engine, list, &ns, m, filter);
+ }
+ }
+
+ for (ns, m) in ctx.iter_imports_raw() {
+ scan_module(engine, &mut list, ns, m, filter);
+ }
+ }
+
+ list
+}
diff --git a/rhai/src/packages/logic.rs b/rhai/src/packages/logic.rs
new file mode 100644
index 0000000..e163eff
--- /dev/null
+++ b/rhai/src/packages/logic.rs
@@ -0,0 +1,415 @@
+use crate::def_package;
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(any(
+ not(feature = "no_float"),
+ all(not(feature = "only_i32"), not(feature = "only_i64"))
+))]
+macro_rules! gen_cmp_functions {
+ ($root:ident => $($arg_type:ident),+) => {
+ mod $root { $(pub mod $arg_type {
+ use super::super::*;
+
+ #[export_module]
+ pub mod functions {
+ #[rhai_fn(name = "<")] pub fn lt(x: $arg_type, y: $arg_type) -> bool { x < y }
+ #[rhai_fn(name = "<=")] pub fn lte(x: $arg_type, y: $arg_type) -> bool { x <= y }
+ #[rhai_fn(name = ">")] pub fn gt(x: $arg_type, y: $arg_type) -> bool { x > y }
+ #[rhai_fn(name = ">=")] pub fn gte(x: $arg_type, y: $arg_type) -> bool { x >= y }
+ #[rhai_fn(name = "==")] pub fn eq(x: $arg_type, y: $arg_type) -> bool { x == y }
+ #[rhai_fn(name = "!=")] pub fn ne(x: $arg_type, y: $arg_type) -> bool { x != y }
+ pub fn max(x: $arg_type, y: $arg_type) -> $arg_type { if x >= y { x } else { y } }
+ pub fn min(x: $arg_type, y: $arg_type) -> $arg_type { if x <= y { x } else { y } }
+ }
+ })* }
+ };
+}
+
+#[cfg(any(
+ not(feature = "no_float"),
+ all(not(feature = "only_i32"), not(feature = "only_i64"))
+))]
+macro_rules! reg_functions {
+ ($mod_name:ident += $root:ident ; $($arg_type:ident),+) => { $(
+ combine_with_exported_module!($mod_name, "logic", $root::$arg_type::functions);
+ )* }
+}
+
+def_package! {
+ /// Package of basic logic operators.
+ pub LogicPackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ #[cfg(not(feature = "only_i32"))]
+ #[cfg(not(feature = "only_i64"))]
+ {
+ reg_functions!(lib += numbers; i8, u8, i16, u16, i32, u32, u64);
+
+ #[cfg(not(target_family = "wasm"))]
+
+ reg_functions!(lib += num_128; i128, u128);
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ {
+ combine_with_exported_module!(lib, "float", float_functions);
+
+ #[cfg(not(feature = "f32_float"))]
+ {
+ reg_functions!(lib += float; f32);
+ combine_with_exported_module!(lib, "f32", f32_functions);
+ }
+ #[cfg(feature = "f32_float")]
+ {
+ reg_functions!(lib += float; f64);
+ combine_with_exported_module!(lib, "f64", f64_functions);
+ }
+ }
+
+ #[cfg(feature = "decimal")]
+ combine_with_exported_module!(lib, "decimal", decimal_functions);
+
+ combine_with_exported_module!(lib, "logic", logic_functions);
+ }
+}
+
+#[cfg(not(feature = "only_i32"))]
+#[cfg(not(feature = "only_i64"))]
+gen_cmp_functions!(numbers => i8, u8, i16, u16, i32, u32, u64);
+
+#[cfg(not(feature = "only_i32"))]
+#[cfg(not(feature = "only_i64"))]
+#[cfg(not(target_family = "wasm"))]
+
+gen_cmp_functions!(num_128 => i128, u128);
+
+#[cfg(not(feature = "no_float"))]
+#[cfg(not(feature = "f32_float"))]
+gen_cmp_functions!(float => f32);
+
+#[cfg(not(feature = "no_float"))]
+#[cfg(feature = "f32_float")]
+gen_cmp_functions!(float => f64);
+
+#[export_module]
+mod logic_functions {
+ #[rhai_fn(name = "!")]
+ pub fn not(x: bool) -> bool {
+ !x
+ }
+}
+
+#[cfg(not(feature = "no_float"))]
+#[allow(clippy::cast_precision_loss)]
+#[export_module]
+mod float_functions {
+ use crate::INT;
+
+ #[rhai_fn(name = "max")]
+ pub fn max_if_32(x: INT, y: f32) -> f32 {
+ let (x, y) = (x as f32, y as f32);
+ if x >= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "max")]
+ pub fn max_fi_32(x: f32, y: INT) -> f32 {
+ let (x, y) = (x as f32, y as f32);
+ if x >= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "min")]
+ pub fn min_if_32(x: INT, y: f32) -> f32 {
+ let (x, y) = (x as f32, y as f32);
+ if x <= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "min")]
+ pub fn min_fi_32(x: f32, y: INT) -> f32 {
+ let (x, y) = (x as f32, y as f32);
+ if x <= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "max")]
+ pub fn max_if_64(x: INT, y: f64) -> f64 {
+ let (x, y) = (x as f64, y as f64);
+ if x >= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "max")]
+ pub fn max_fi_64(x: f64, y: INT) -> f64 {
+ let (x, y) = (x as f64, y as f64);
+ if x >= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "min")]
+ pub fn min_if_64(x: INT, y: f64) -> f64 {
+ let (x, y) = (x as f64, y as f64);
+ if x <= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "min")]
+ pub fn min_fi_64(x: f64, y: INT) -> f64 {
+ let (x, y) = (x as f64, y as f64);
+ if x <= y {
+ x
+ } else {
+ y
+ }
+ }
+}
+
+#[cfg(not(feature = "no_float"))]
+#[cfg(not(feature = "f32_float"))]
+#[allow(clippy::cast_precision_loss)]
+#[export_module]
+mod f32_functions {
+ use crate::{FLOAT, INT};
+
+ #[rhai_fn(name = "==")]
+ pub fn eq_if(x: INT, y: f32) -> bool {
+ (x as f32) == (y as f32)
+ }
+ #[rhai_fn(name = "==")]
+ pub fn eq_fi(x: f32, y: INT) -> bool {
+ (x as f32) == (y as f32)
+ }
+ #[rhai_fn(name = "!=")]
+ pub fn neq_if(x: INT, y: f32) -> bool {
+ (x as f32) != (y as f32)
+ }
+ #[rhai_fn(name = "!=")]
+ pub fn neq_fi(x: f32, y: INT) -> bool {
+ (x as f32) != (y as f32)
+ }
+ #[rhai_fn(name = ">")]
+ pub fn gt_if(x: INT, y: f32) -> bool {
+ (x as f32) > (y as f32)
+ }
+ #[rhai_fn(name = ">")]
+ pub fn gt_fi(x: f32, y: INT) -> bool {
+ (x as f32) > (y as f32)
+ }
+ #[rhai_fn(name = ">=")]
+ pub fn gte_if(x: INT, y: f32) -> bool {
+ (x as f32) >= (y as f32)
+ }
+ #[rhai_fn(name = ">=")]
+ pub fn gte_fi(x: f32, y: INT) -> bool {
+ (x as f32) >= (y as f32)
+ }
+ #[rhai_fn(name = "<")]
+ pub fn lt_if(x: INT, y: f32) -> bool {
+ (x as f32) < (y as f32)
+ }
+ #[rhai_fn(name = "<")]
+ pub fn lt_fi(x: f32, y: INT) -> bool {
+ (x as f32) < (y as f32)
+ }
+ #[rhai_fn(name = "<=")]
+ pub fn lte_if(x: INT, y: f32) -> bool {
+ (x as f32) <= (y as f32)
+ }
+ #[rhai_fn(name = "<=")]
+ pub fn lte_fi(x: f32, y: INT) -> bool {
+ (x as f32) <= (y as f32)
+ }
+
+ #[rhai_fn(name = "max")]
+ pub fn max_64_32(x: FLOAT, y: f32) -> FLOAT {
+ let (x, y) = (x as FLOAT, y as FLOAT);
+ if x >= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "max")]
+ pub fn max_32_64(x: f32, y: FLOAT) -> FLOAT {
+ let (x, y) = (x as FLOAT, y as FLOAT);
+ if x >= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "min")]
+ pub fn min_64_32(x: FLOAT, y: f32) -> FLOAT {
+ let (x, y) = (x as FLOAT, y as FLOAT);
+ if x <= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "min")]
+ pub fn min_32_64(x: f32, y: FLOAT) -> FLOAT {
+ let (x, y) = (x as FLOAT, y as FLOAT);
+ if x <= y {
+ x
+ } else {
+ y
+ }
+ }
+}
+
+#[cfg(not(feature = "no_float"))]
+#[cfg(feature = "f32_float")]
+#[allow(clippy::cast_precision_loss)]
+#[export_module]
+mod f64_functions {
+ use crate::{FLOAT, INT};
+
+ #[rhai_fn(name = "==")]
+ pub fn eq_if(x: INT, y: f64) -> bool {
+ (x as f64) == (y as f64)
+ }
+ #[rhai_fn(name = "==")]
+ pub fn eq_fi(x: f64, y: INT) -> bool {
+ (x as f64) == (y as f64)
+ }
+ #[rhai_fn(name = "!=")]
+ pub fn neq_if(x: INT, y: f64) -> bool {
+ (x as f64) != (y as f64)
+ }
+ #[rhai_fn(name = "!=")]
+ pub fn neq_fi(x: f64, y: INT) -> bool {
+ (x as f64) != (y as f64)
+ }
+ #[rhai_fn(name = ">")]
+ pub fn gt_if(x: INT, y: f64) -> bool {
+ (x as f64) > (y as f64)
+ }
+ #[rhai_fn(name = ">")]
+ pub fn gt_fi(x: f64, y: INT) -> bool {
+ (x as f64) > (y as f64)
+ }
+ #[rhai_fn(name = ">=")]
+ pub fn gte_if(x: INT, y: f64) -> bool {
+ (x as f64) >= (y as f64)
+ }
+ #[rhai_fn(name = ">=")]
+ pub fn gte_fi(x: f64, y: INT) -> bool {
+ (x as f64) >= (y as f64)
+ }
+ #[rhai_fn(name = "<")]
+ pub fn lt_if(x: INT, y: f64) -> bool {
+ (x as f64) < (y as f64)
+ }
+ #[rhai_fn(name = "<")]
+ pub fn lt_fi(x: f64, y: INT) -> bool {
+ (x as f64) < (y as f64)
+ }
+ #[rhai_fn(name = "<=")]
+ pub fn lte_if(x: INT, y: f64) -> bool {
+ (x as f64) <= (y as f64)
+ }
+ #[rhai_fn(name = "<=")]
+ pub fn lte_fi(x: f64, y: INT) -> bool {
+ (x as f64) <= (y as f64)
+ }
+
+ #[rhai_fn(name = "max")]
+ pub fn max_32_64(x: FLOAT, y: f64) -> FLOAT {
+ let (x, y) = (x as FLOAT, y as FLOAT);
+ if x >= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "max")]
+ pub fn max_64_32(x: f64, y: FLOAT) -> FLOAT {
+ let (x, y) = (x as FLOAT, y as FLOAT);
+ if x >= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "min")]
+ pub fn min_32_64(x: FLOAT, y: f64) -> FLOAT {
+ let (x, y) = (x as FLOAT, y as FLOAT);
+ if x <= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "min")]
+ pub fn min_64_32(x: f64, y: FLOAT) -> FLOAT {
+ let (x, y) = (x as FLOAT, y as FLOAT);
+ if x <= y {
+ x
+ } else {
+ y
+ }
+ }
+}
+
+#[cfg(feature = "decimal")]
+#[export_module]
+mod decimal_functions {
+ use crate::INT;
+ use rust_decimal::Decimal;
+
+ #[rhai_fn(name = "max")]
+ pub fn max_id(x: INT, y: Decimal) -> Decimal {
+ let x = x.into();
+ if x >= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "max")]
+ pub fn max_di(x: Decimal, y: INT) -> Decimal {
+ let y = y.into();
+ if x >= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "min")]
+ pub fn min_id(x: INT, y: Decimal) -> Decimal {
+ let x = x.into();
+ if x <= y {
+ x
+ } else {
+ y
+ }
+ }
+ #[rhai_fn(name = "min")]
+ pub fn min_di(x: Decimal, y: INT) -> Decimal {
+ let y = y.into();
+ if x <= y {
+ x
+ } else {
+ y
+ }
+ }
+}
diff --git a/rhai/src/packages/map_basic.rs b/rhai/src/packages/map_basic.rs
new file mode 100644
index 0000000..8851efb
--- /dev/null
+++ b/rhai/src/packages/map_basic.rs
@@ -0,0 +1,314 @@
+#![cfg(not(feature = "no_object"))]
+
+use crate::engine::OP_EQUALS;
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+use crate::{def_package, Dynamic, ImmutableString, Map, NativeCallContext, RhaiResultOf, INT};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(not(feature = "no_index"))]
+use crate::Array;
+
+def_package! {
+ /// Package of basic object map utilities.
+ pub BasicMapPackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ combine_with_exported_module!(lib, "map", map_functions);
+ }
+}
+
+#[export_module]
+mod map_functions {
+ /// Return the number of properties in the object map.
+ #[rhai_fn(pure)]
+ pub fn len(map: &mut Map) -> INT {
+ map.len() as INT
+ }
+ /// Return true if the map is empty.
+ #[rhai_fn(pure)]
+ pub fn is_empty(map: &mut Map) -> bool {
+ map.len() == 0
+ }
+ /// Returns `true` if the object map contains a specified property.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let m = #{a: 1, b: 2, c: 3};
+ ///
+ /// print(m.contains("b")); // prints true
+ ///
+ /// print(m.contains("x")); // prints false
+ /// ```
+ pub fn contains(map: &mut Map, property: &str) -> bool {
+ map.contains_key(property)
+ }
+ /// Get the value of the `property` in the object map and return a copy.
+ ///
+ /// If `property` does not exist in the object map, `()` is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let m = #{a: 1, b: 2, c: 3};
+ ///
+ /// print(m.get("b")); // prints 2
+ ///
+ /// print(m.get("x")); // prints empty (for '()')
+ /// ```
+ pub fn get(map: &mut Map, property: &str) -> Dynamic {
+ if map.is_empty() {
+ return Dynamic::UNIT;
+ }
+
+ map.get(property).cloned().unwrap_or(Dynamic::UNIT)
+ }
+ /// Set the value of the `property` in the object map to a new `value`.
+ ///
+ /// If `property` does not exist in the object map, it is added.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let m = #{a: 1, b: 2, c: 3};
+ ///
+ /// m.set("b", 42)'
+ ///
+ /// print(m); // prints "#{a: 1, b: 42, c: 3}"
+ ///
+ /// x.set("x", 0);
+ ///
+ /// print(m); // prints "#{a: 1, b: 42, c: 3, x: 0}"
+ /// ```
+ pub fn set(map: &mut Map, property: &str, value: Dynamic) {
+ match map.get_mut(property) {
+ Some(value_ref) => *value_ref = value,
+ _ => {
+ map.insert(property.into(), value);
+ }
+ }
+ }
+ /// Clear the object map.
+ pub fn clear(map: &mut Map) {
+ if !map.is_empty() {
+ map.clear();
+ }
+ }
+ /// Remove any property of the specified `name` from the object map, returning its value.
+ ///
+ /// If the property does not exist, `()` is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let m = #{a:1, b:2, c:3};
+ ///
+ /// let x = m.remove("b");
+ ///
+ /// print(x); // prints 2
+ ///
+ /// print(m); // prints "#{a:1, c:3}"
+ /// ```
+ pub fn remove(map: &mut Map, property: &str) -> Dynamic {
+ if map.is_empty() {
+ Dynamic::UNIT
+ } else {
+ map.remove(property).unwrap_or(Dynamic::UNIT)
+ }
+ }
+ /// Add all property values of another object map into the object map.
+ /// Existing property values of the same names are replaced.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let m = #{a:1, b:2, c:3};
+ /// let n = #{a: 42, d:0};
+ ///
+ /// m.mixin(n);
+ ///
+ /// print(m); // prints "#{a:42, b:2, c:3, d:0}"
+ /// ```
+ #[rhai_fn(name = "mixin", name = "+=")]
+ pub fn mixin(map: &mut Map, map2: Map) {
+ if !map2.is_empty() {
+ map.extend(map2.into_iter());
+ }
+ }
+ /// Make a copy of the object map, add all property values of another object map
+ /// (existing property values of the same names are replaced), then returning it.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let m = #{a:1, b:2, c:3};
+ /// let n = #{a: 42, d:0};
+ ///
+ /// print(m + n); // prints "#{a:42, b:2, c:3, d:0}"
+ ///
+ /// print(m); // prints "#{a:1, b:2, c:3}"
+ /// ```
+ #[rhai_fn(name = "+")]
+ pub fn merge(map1: Map, map2: Map) -> Map {
+ if map2.is_empty() {
+ map1
+ } else if map1.is_empty() {
+ map2
+ } else {
+ let mut map1 = map1;
+ map1.extend(map2.into_iter());
+ map1
+ }
+ }
+ /// Add all property values of another object map into the object map.
+ /// Only properties that do not originally exist in the object map are added.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let m = #{a:1, b:2, c:3};
+ /// let n = #{a: 42, d:0};
+ ///
+ /// m.fill_with(n);
+ ///
+ /// print(m); // prints "#{a:1, b:2, c:3, d:0}"
+ /// ```
+ pub fn fill_with(map: &mut Map, map2: Map) {
+ if !map2.is_empty() {
+ if map.is_empty() {
+ *map = map2;
+ } else {
+ for (key, value) in map2 {
+ map.entry(key).or_insert(value);
+ }
+ }
+ }
+ }
+ /// Return `true` if two object maps are equal (i.e. all property values are equal).
+ ///
+ /// The operator `==` is used to compare property values and must be defined,
+ /// otherwise `false` is assumed.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let m1 = #{a:1, b:2, c:3};
+ /// let m2 = #{a:1, b:2, c:3};
+ /// let m3 = #{a:1, c:3};
+ ///
+ /// print(m1 == m2); // prints true
+ ///
+ /// print(m1 == m3); // prints false
+ /// ```
+ #[rhai_fn(name = "==", return_raw, pure)]
+ pub fn equals(ctx: NativeCallContext, map1: &mut Map, map2: Map) -> RhaiResultOf<bool> {
+ if map1.len() != map2.len() {
+ return Ok(false);
+ }
+
+ if !map1.is_empty() {
+ let mut map2 = map2;
+
+ for (m1, v1) in map1 {
+ if let Some(v2) = map2.get_mut(m1) {
+ let equals = ctx
+ .call_native_fn_raw(OP_EQUALS, true, &mut [v1, v2])?
+ .as_bool()
+ .unwrap_or(false);
+
+ if !equals {
+ return Ok(false);
+ }
+ } else {
+ return Ok(false);
+ }
+ }
+ }
+
+ Ok(true)
+ }
+ /// Return `true` if two object maps are not equal (i.e. at least one property value is not equal).
+ ///
+ /// The operator `==` is used to compare property values and must be defined,
+ /// otherwise `false` is assumed.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let m1 = #{a:1, b:2, c:3};
+ /// let m2 = #{a:1, b:2, c:3};
+ /// let m3 = #{a:1, c:3};
+ ///
+ /// print(m1 != m2); // prints false
+ ///
+ /// print(m1 != m3); // prints true
+ /// ```
+ #[rhai_fn(name = "!=", return_raw, pure)]
+ pub fn not_equals(ctx: NativeCallContext, map1: &mut Map, map2: Map) -> RhaiResultOf<bool> {
+ equals(ctx, map1, map2).map(|r| !r)
+ }
+
+ /// Return an array with all the property names in the object map.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let m = #{a:1, b:2, c:3};
+ ///
+ /// print(m.keys()); // prints ["a", "b", "c"]
+ /// ```
+ #[cfg(not(feature = "no_index"))]
+ #[rhai_fn(pure)]
+ pub fn keys(map: &mut Map) -> Array {
+ if map.is_empty() {
+ Array::new()
+ } else {
+ map.keys().cloned().map(Into::into).collect()
+ }
+ }
+ /// Return an array with all the property values in the object map.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let m = #{a:1, b:2, c:3};
+ ///
+ /// print(m.values()); // prints "[1, 2, 3]""
+ /// ```
+ #[cfg(not(feature = "no_index"))]
+ #[rhai_fn(pure)]
+ pub fn values(map: &mut Map) -> Array {
+ if map.is_empty() {
+ Array::new()
+ } else {
+ map.values().cloned().collect()
+ }
+ }
+ /// Return the JSON representation of the object map.
+ ///
+ /// # Data types
+ ///
+ /// Only the following data types should be kept inside the object map:
+ /// `INT`, `FLOAT`, `ImmutableString`, `char`, `bool`, `()`, `Array`, `Map`.
+ ///
+ /// # Errors
+ ///
+ /// Data types not supported by JSON serialize into formats that may
+ /// invalidate the result.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let m = #{a:1, b:2, c:3};
+ ///
+ /// print(m.to_json()); // prints {"a":1, "b":2, "c":3}
+ /// ```
+ pub fn to_json(map: &mut Map) -> String {
+ #[cfg(feature = "metadata")]
+ return serde_json::to_string(map).unwrap_or_else(|_| "ERROR".into());
+ #[cfg(not(feature = "metadata"))]
+ return crate::format_map_as_json(map);
+ }
+}
diff --git a/rhai/src/packages/math_basic.rs b/rhai/src/packages/math_basic.rs
new file mode 100644
index 0000000..25294e4
--- /dev/null
+++ b/rhai/src/packages/math_basic.rs
@@ -0,0 +1,684 @@
+#![allow(non_snake_case)]
+
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+use crate::{def_package, Position, RhaiResultOf, ERR, INT};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(not(feature = "no_float"))]
+use crate::FLOAT;
+
+#[cfg(feature = "no_std")]
+#[cfg(not(feature = "no_float"))]
+use num_traits::Float;
+
+#[cfg(feature = "decimal")]
+use rust_decimal::Decimal;
+
+#[cfg(feature = "decimal")]
+use super::arithmetic::make_err;
+
+macro_rules! gen_conversion_as_functions {
+ ($root:ident => $func_name:ident ( $($arg_type:ident),+ ) -> $result_type:ty) => {
+ pub mod $root { $(pub mod $arg_type {
+ use super::super::*;
+
+ #[export_fn]
+ pub fn $func_name(x: $arg_type) -> $result_type {
+ x as $result_type
+ }
+ })* }
+ }
+}
+
+#[cfg(feature = "decimal")]
+macro_rules! gen_conversion_into_functions {
+ ($root:ident => $func_name:ident ( $($arg_type:ident),+ ) -> $result_type:ty) => {
+ pub mod $root { $(pub mod $arg_type {
+ use super::super::*;
+
+ #[export_fn]
+ pub fn $func_name(x: $arg_type) -> $result_type {
+ x.into()
+ }
+ })* }
+ }
+}
+
+macro_rules! reg_functions {
+ ($mod_name:ident += $root:ident :: $func_name:ident ( $($arg_type:ident),+ ) ) => { $(
+ set_exported_fn!($mod_name, stringify!($func_name), $root::$arg_type::$func_name);
+ )* }
+}
+
+def_package! {
+ /// Basic mathematical package.
+ pub BasicMathPackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ // Integer functions
+ combine_with_exported_module!(lib, "int", int_functions);
+
+ reg_functions!(lib += basic_to_int::to_int(char));
+
+ #[cfg(not(feature = "only_i32"))]
+ #[cfg(not(feature = "only_i64"))]
+ {
+ reg_functions!(lib += numbers_to_int::to_int(i8, u8, i16, u16, i32, u32, i64, u64));
+
+ #[cfg(not(target_family = "wasm"))]
+
+ reg_functions!(lib += num_128_to_int::to_int(i128, u128));
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ {
+ // Floating point functions
+ combine_with_exported_module!(lib, "float", float_functions);
+
+ // Trig functions
+ combine_with_exported_module!(lib, "trig", trig_functions);
+
+ reg_functions!(lib += basic_to_float::to_float(INT));
+
+ #[cfg(not(feature = "only_i32"))]
+ #[cfg(not(feature = "only_i64"))]
+ {
+ reg_functions!(lib += numbers_to_float::to_float(i8, u8, i16, u16, i32, u32, i64, u32));
+
+ #[cfg(not(target_family = "wasm"))]
+
+ reg_functions!(lib += num_128_to_float::to_float(i128, u128));
+ }
+ }
+
+ // Decimal functions
+ #[cfg(feature = "decimal")]
+ {
+ combine_with_exported_module!(lib, "decimal", decimal_functions);
+
+ reg_functions!(lib += basic_to_decimal::to_decimal(INT));
+
+ #[cfg(not(feature = "only_i32"))]
+ #[cfg(not(feature = "only_i64"))]
+ reg_functions!(lib += numbers_to_decimal::to_decimal(i8, u8, i16, u16, i32, u32, i64, u64));
+ }
+ }
+}
+
+#[export_module]
+mod int_functions {
+ /// Parse a string into an integer number.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = parse_int("123");
+ ///
+ /// print(x); // prints 123
+ /// ```
+ #[rhai_fn(name = "parse_int", return_raw)]
+ pub fn parse_int(string: &str) -> RhaiResultOf<INT> {
+ parse_int_radix(string, 10)
+ }
+ /// Parse a string into an integer number of the specified `radix`.
+ ///
+ /// `radix` must be between 2 and 36.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = parse_int("123");
+ ///
+ /// print(x); // prints 123
+ ///
+ /// let y = parse_int("123abc", 16);
+ ///
+ /// print(y); // prints 1194684 (0x123abc)
+ /// ```
+ #[rhai_fn(name = "parse_int", return_raw)]
+ pub fn parse_int_radix(string: &str, radix: INT) -> RhaiResultOf<INT> {
+ if !(2..=36).contains(&radix) {
+ return Err(
+ ERR::ErrorArithmetic(format!("Invalid radix: '{radix}'"), Position::NONE).into(),
+ );
+ }
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ INT::from_str_radix(string.trim(), radix as u32).map_err(|err| {
+ ERR::ErrorArithmetic(
+ format!("Error parsing integer number '{string}': {err}"),
+ Position::NONE,
+ )
+ .into()
+ })
+ }
+}
+
+#[cfg(not(feature = "no_float"))]
+#[export_module]
+mod trig_functions {
+ /// Return the sine of the floating-point number in radians.
+ pub fn sin(x: FLOAT) -> FLOAT {
+ x.sin()
+ }
+ /// Return the cosine of the floating-point number in radians.
+ pub fn cos(x: FLOAT) -> FLOAT {
+ x.cos()
+ }
+ /// Return the tangent of the floating-point number in radians.
+ pub fn tan(x: FLOAT) -> FLOAT {
+ x.tan()
+ }
+ /// Return the hyperbolic sine of the floating-point number in radians.
+ pub fn sinh(x: FLOAT) -> FLOAT {
+ x.sinh()
+ }
+ /// Return the hyperbolic cosine of the floating-point number in radians.
+ pub fn cosh(x: FLOAT) -> FLOAT {
+ x.cosh()
+ }
+ /// Return the hyperbolic tangent of the floating-point number in radians.
+ pub fn tanh(x: FLOAT) -> FLOAT {
+ x.tanh()
+ }
+ /// Return the arc-sine of the floating-point number, in radians.
+ pub fn asin(x: FLOAT) -> FLOAT {
+ x.asin()
+ }
+ /// Return the arc-cosine of the floating-point number, in radians.
+ pub fn acos(x: FLOAT) -> FLOAT {
+ x.acos()
+ }
+ /// Return the arc-tangent of the floating-point number, in radians.
+ pub fn atan(x: FLOAT) -> FLOAT {
+ x.atan()
+ }
+ /// Return the arc-tangent of the floating-point numbers `x` and `y`, in radians.
+ #[rhai_fn(name = "atan")]
+ pub fn atan2(x: FLOAT, y: FLOAT) -> FLOAT {
+ x.atan2(y)
+ }
+ /// Return the arc-hyperbolic-sine of the floating-point number, in radians.
+ pub fn asinh(x: FLOAT) -> FLOAT {
+ x.asinh()
+ }
+ /// Return the arc-hyperbolic-cosine of the floating-point number, in radians.
+ pub fn acosh(x: FLOAT) -> FLOAT {
+ x.acosh()
+ }
+ /// Return the arc-hyperbolic-tangent of the floating-point number, in radians.
+ pub fn atanh(x: FLOAT) -> FLOAT {
+ x.atanh()
+ }
+ /// Return the hypotenuse of a triangle with sides `x` and `y`.
+ pub fn hypot(x: FLOAT, y: FLOAT) -> FLOAT {
+ x.hypot(y)
+ }
+}
+
+#[cfg(not(feature = "no_float"))]
+#[export_module]
+mod float_functions {
+ /// Return the natural number _e_.
+ #[rhai_fn(name = "E")]
+ pub fn e() -> FLOAT {
+ #[cfg(not(feature = "f32_float"))]
+ return std::f64::consts::E;
+ #[cfg(feature = "f32_float")]
+ return std::f32::consts::E;
+ }
+ /// Return the number π.
+ #[rhai_fn(name = "PI")]
+ pub fn pi() -> FLOAT {
+ #[cfg(not(feature = "f32_float"))]
+ return std::f64::consts::PI;
+ #[cfg(feature = "f32_float")]
+ return std::f32::consts::PI;
+ }
+ /// Convert degrees to radians.
+ pub fn to_radians(x: FLOAT) -> FLOAT {
+ x.to_radians()
+ }
+ /// Convert radians to degrees.
+ pub fn to_degrees(x: FLOAT) -> FLOAT {
+ x.to_degrees()
+ }
+ /// Return the square root of the floating-point number.
+ pub fn sqrt(x: FLOAT) -> FLOAT {
+ x.sqrt()
+ }
+ /// Return the exponential of the floating-point number.
+ pub fn exp(x: FLOAT) -> FLOAT {
+ x.exp()
+ }
+ /// Return the natural log of the floating-point number.
+ pub fn ln(x: FLOAT) -> FLOAT {
+ x.ln()
+ }
+ /// Return the log of the floating-point number with `base`.
+ pub fn log(x: FLOAT, base: FLOAT) -> FLOAT {
+ x.log(base)
+ }
+ /// Return the log of the floating-point number with base 10.
+ #[rhai_fn(name = "log")]
+ pub fn log10(x: FLOAT) -> FLOAT {
+ x.log10()
+ }
+ /// Return the largest whole number less than or equals to the floating-point number.
+ #[rhai_fn(name = "floor", get = "floor")]
+ pub fn floor(x: FLOAT) -> FLOAT {
+ x.floor()
+ }
+ /// Return the smallest whole number larger than or equals to the floating-point number.
+ #[rhai_fn(name = "ceiling", get = "ceiling")]
+ pub fn ceiling(x: FLOAT) -> FLOAT {
+ x.ceil()
+ }
+ /// Return the nearest whole number closest to the floating-point number.
+ /// Rounds away from zero.
+ #[rhai_fn(name = "round", get = "round")]
+ pub fn round(x: FLOAT) -> FLOAT {
+ x.round()
+ }
+ /// Return the integral part of the floating-point number.
+ #[rhai_fn(name = "int", get = "int")]
+ pub fn int(x: FLOAT) -> FLOAT {
+ x.trunc()
+ }
+ /// Return the fractional part of the floating-point number.
+ #[rhai_fn(name = "fraction", get = "fraction")]
+ pub fn fraction(x: FLOAT) -> FLOAT {
+ x.fract()
+ }
+ /// Return `true` if the floating-point number is `NaN` (Not A Number).
+ #[rhai_fn(name = "is_nan", get = "is_nan")]
+ pub fn is_nan(x: FLOAT) -> bool {
+ x.is_nan()
+ }
+ /// Return `true` if the floating-point number is finite.
+ #[rhai_fn(name = "is_finite", get = "is_finite")]
+ pub fn is_finite(x: FLOAT) -> bool {
+ x.is_finite()
+ }
+ /// Return `true` if the floating-point number is infinite.
+ #[rhai_fn(name = "is_infinite", get = "is_infinite")]
+ pub fn is_infinite(x: FLOAT) -> bool {
+ x.is_infinite()
+ }
+ /// Convert the floating-point number into an integer.
+ #[rhai_fn(name = "to_int", return_raw)]
+ pub fn f32_to_int(x: f32) -> RhaiResultOf<INT> {
+ #[allow(clippy::cast_precision_loss)]
+ if cfg!(not(feature = "unchecked")) && (x > (INT::MAX as f32) || x < (INT::MIN as f32)) {
+ Err(
+ ERR::ErrorArithmetic(format!("Integer overflow: to_int({x})"), Position::NONE)
+ .into(),
+ )
+ } else {
+ Ok(x.trunc() as INT)
+ }
+ }
+ /// Convert the floating-point number into an integer.
+ #[rhai_fn(name = "to_int", return_raw)]
+ pub fn f64_to_int(x: f64) -> RhaiResultOf<INT> {
+ #[allow(clippy::cast_precision_loss)]
+ if cfg!(not(feature = "unchecked")) && (x > (INT::MAX as f64) || x < (INT::MIN as f64)) {
+ Err(
+ ERR::ErrorArithmetic(format!("Integer overflow: to_int({x})"), Position::NONE)
+ .into(),
+ )
+ } else {
+ Ok(x.trunc() as INT)
+ }
+ }
+ /// Parse a string into a floating-point number.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = parse_int("123.456");
+ ///
+ /// print(x); // prints 123.456
+ /// ```
+ #[rhai_fn(return_raw)]
+ pub fn parse_float(string: &str) -> RhaiResultOf<FLOAT> {
+ string.trim().parse::<FLOAT>().map_err(|err| {
+ ERR::ErrorArithmetic(
+ format!("Error parsing floating-point number '{string}': {err}"),
+ Position::NONE,
+ )
+ .into()
+ })
+ }
+ /// Convert the 32-bit floating-point number to 64-bit.
+ #[cfg(not(feature = "f32_float"))]
+ #[rhai_fn(name = "to_float")]
+ pub fn f32_to_f64(x: f32) -> f64 {
+ x.into()
+ }
+}
+
+#[cfg(feature = "decimal")]
+#[export_module]
+mod decimal_functions {
+ use num_traits::ToPrimitive;
+ use rust_decimal::{
+ prelude::{FromStr, RoundingStrategy},
+ Decimal, MathematicalOps,
+ };
+ #[cfg(not(feature = "no_float"))]
+ use std::convert::TryFrom;
+
+ /// Return the natural number _e_.
+ #[cfg(feature = "no_float")]
+ #[rhai_fn(name = "PI")]
+ pub fn pi() -> Decimal {
+ Decimal::PI
+ }
+ /// Return the number π.
+ #[cfg(feature = "no_float")]
+ #[rhai_fn(name = "E")]
+ pub fn e() -> Decimal {
+ Decimal::E
+ }
+ /// Parse a string into a decimal number.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = parse_float("123.456");
+ ///
+ /// print(x); // prints 123.456
+ /// ```
+ #[cfg(feature = "no_float")]
+ #[rhai_fn(return_raw)]
+ pub fn parse_float(s: &str) -> RhaiResultOf<Decimal> {
+ parse_decimal(s)
+ }
+
+ /// Return the sine of the decimal number in radians.
+ pub fn sin(x: Decimal) -> Decimal {
+ x.sin()
+ }
+ /// Return the cosine of the decimal number in radians.
+ pub fn cos(x: Decimal) -> Decimal {
+ x.cos()
+ }
+ /// Return the tangent of the decimal number in radians.
+ pub fn tan(x: Decimal) -> Decimal {
+ x.tan()
+ }
+ /// Return the square root of the decimal number.
+ #[rhai_fn(return_raw)]
+ pub fn sqrt(x: Decimal) -> RhaiResultOf<Decimal> {
+ x.sqrt()
+ .ok_or_else(|| make_err(format!("Error taking the square root of {x}")))
+ }
+ /// Return the exponential of the decimal number.
+ #[rhai_fn(return_raw)]
+ pub fn exp(x: Decimal) -> RhaiResultOf<Decimal> {
+ if cfg!(not(feature = "unchecked")) {
+ x.checked_exp()
+ .ok_or_else(|| make_err(format!("Exponential overflow: e ** {x}")))
+ } else {
+ Ok(x.exp())
+ }
+ }
+ /// Return the natural log of the decimal number.
+ #[rhai_fn(return_raw)]
+ pub fn ln(x: Decimal) -> RhaiResultOf<Decimal> {
+ if cfg!(not(feature = "unchecked")) {
+ x.checked_ln()
+ .ok_or_else(|| make_err(format!("Error taking the natural log of {x}")))
+ } else {
+ Ok(x.ln())
+ }
+ }
+ /// Return the log of the decimal number with base 10.
+ #[rhai_fn(name = "log", return_raw)]
+ pub fn log10(x: Decimal) -> RhaiResultOf<Decimal> {
+ if cfg!(not(feature = "unchecked")) {
+ x.checked_log10()
+ .ok_or_else(|| make_err(format!("Error taking the log of {x}")))
+ } else {
+ Ok(x.log10())
+ }
+ }
+ /// Return the largest whole number less than or equals to the decimal number.
+ #[rhai_fn(name = "floor", get = "floor")]
+ pub fn floor(x: Decimal) -> Decimal {
+ x.floor()
+ }
+ /// Return the smallest whole number larger than or equals to the decimal number.
+ #[rhai_fn(name = "ceiling", get = "ceiling")]
+ pub fn ceiling(x: Decimal) -> Decimal {
+ x.ceil()
+ }
+ /// Return the nearest whole number closest to the decimal number.
+ /// Always round mid-point towards the closest even number.
+ #[rhai_fn(name = "round", get = "round")]
+ pub fn round(x: Decimal) -> Decimal {
+ x.round()
+ }
+ /// Round the decimal number to the specified number of `digits` after the decimal point and return it.
+ /// Always round mid-point towards the closest even number.
+ #[rhai_fn(name = "round", return_raw)]
+ pub fn round_dp(x: Decimal, digits: INT) -> RhaiResultOf<Decimal> {
+ if cfg!(not(feature = "unchecked")) {
+ if digits < 0 {
+ return Err(make_err(format!(
+ "Invalid number of digits for rounding: {digits}"
+ )));
+ }
+ if cfg!(not(feature = "only_i32")) && digits > (u32::MAX as INT) {
+ return Ok(x);
+ }
+ }
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ Ok(x.round_dp(digits as u32))
+ }
+ /// Round the decimal number to the specified number of `digits` after the decimal point and return it.
+ /// Always round away from zero.
+ #[rhai_fn(return_raw)]
+ pub fn round_up(x: Decimal, digits: INT) -> RhaiResultOf<Decimal> {
+ if cfg!(not(feature = "unchecked")) {
+ if digits < 0 {
+ return Err(make_err(format!(
+ "Invalid number of digits for rounding: {digits}"
+ )));
+ }
+ if cfg!(not(feature = "only_i32")) && digits > (u32::MAX as INT) {
+ return Ok(x);
+ }
+ }
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ Ok(x.round_dp_with_strategy(digits as u32, RoundingStrategy::AwayFromZero))
+ }
+ /// Round the decimal number to the specified number of `digits` after the decimal point and return it.
+ /// Always round towards zero.
+ #[rhai_fn(return_raw)]
+ pub fn round_down(x: Decimal, digits: INT) -> RhaiResultOf<Decimal> {
+ if cfg!(not(feature = "unchecked")) {
+ if digits < 0 {
+ return Err(make_err(format!(
+ "Invalid number of digits for rounding: {digits}"
+ )));
+ }
+ if cfg!(not(feature = "only_i32")) && digits > (u32::MAX as INT) {
+ return Ok(x);
+ }
+ }
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ Ok(x.round_dp_with_strategy(digits as u32, RoundingStrategy::ToZero))
+ }
+ /// Round the decimal number to the specified number of `digits` after the decimal point and return it.
+ /// Always round mid-points away from zero.
+ #[rhai_fn(return_raw)]
+ pub fn round_half_up(x: Decimal, digits: INT) -> RhaiResultOf<Decimal> {
+ if cfg!(not(feature = "unchecked")) {
+ if digits < 0 {
+ return Err(make_err(format!(
+ "Invalid number of digits for rounding: {digits}"
+ )));
+ }
+ if cfg!(not(feature = "only_i32")) && digits > (u32::MAX as INT) {
+ return Ok(x);
+ }
+ }
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ Ok(x.round_dp_with_strategy(digits as u32, RoundingStrategy::MidpointAwayFromZero))
+ }
+ /// Round the decimal number to the specified number of `digits` after the decimal point and return it.
+ /// Always round mid-points towards zero.
+ #[rhai_fn(return_raw)]
+ pub fn round_half_down(x: Decimal, digits: INT) -> RhaiResultOf<Decimal> {
+ if cfg!(not(feature = "unchecked")) {
+ if digits < 0 {
+ return Err(make_err(format!(
+ "Invalid number of digits for rounding: {digits}"
+ )));
+ }
+ if cfg!(not(feature = "only_i32")) && digits > (u32::MAX as INT) {
+ return Ok(x);
+ }
+ }
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ Ok(x.round_dp_with_strategy(digits as u32, RoundingStrategy::MidpointTowardZero))
+ }
+ /// Convert the decimal number into an integer.
+ #[rhai_fn(return_raw)]
+ pub fn to_int(x: Decimal) -> RhaiResultOf<INT> {
+ x.to_i64()
+ .and_then(|n| {
+ #[cfg(feature = "only_i32")]
+ return if n > (INT::MAX as i64) || n < (INT::MIN as i64) {
+ None
+ } else {
+ Some(n as i32)
+ };
+
+ #[cfg(not(feature = "only_i32"))]
+ return Some(n);
+ })
+ .map_or_else(
+ || {
+ Err(ERR::ErrorArithmetic(
+ format!("Integer overflow: to_int({x})"),
+ Position::NONE,
+ )
+ .into())
+ },
+ Ok,
+ )
+ }
+ /// Return the integral part of the decimal number.
+ #[rhai_fn(name = "int", get = "int")]
+ pub fn int(x: Decimal) -> Decimal {
+ x.trunc()
+ }
+ /// Return the fractional part of the decimal number.
+ #[rhai_fn(name = "fraction", get = "fraction")]
+ pub fn fraction(x: Decimal) -> Decimal {
+ x.fract()
+ }
+ /// Parse a string into a decimal number.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let x = parse_decimal("123.456");
+ ///
+ /// print(x); // prints 123.456
+ /// ```
+ #[rhai_fn(return_raw)]
+ pub fn parse_decimal(string: &str) -> RhaiResultOf<Decimal> {
+ Decimal::from_str(string)
+ .or_else(|_| Decimal::from_scientific(string))
+ .map_err(|err| {
+ ERR::ErrorArithmetic(
+ format!("Error parsing decimal number '{string}': {err}"),
+ Position::NONE,
+ )
+ .into()
+ })
+ }
+
+ /// Convert the floating-point number to decimal.
+ #[cfg(not(feature = "no_float"))]
+ #[rhai_fn(name = "to_decimal", return_raw)]
+ pub fn f32_to_decimal(x: f32) -> RhaiResultOf<Decimal> {
+ Decimal::try_from(x).map_err(|_| {
+ ERR::ErrorArithmetic(
+ format!("Cannot convert to Decimal: to_decimal({x})"),
+ Position::NONE,
+ )
+ .into()
+ })
+ }
+ /// Convert the floating-point number to decimal.
+ #[cfg(not(feature = "no_float"))]
+ #[rhai_fn(name = "to_decimal", return_raw)]
+ pub fn f64_to_decimal(x: f64) -> RhaiResultOf<Decimal> {
+ Decimal::try_from(x).map_err(|_| {
+ ERR::ErrorArithmetic(
+ format!("Cannot convert to Decimal: to_decimal({x})"),
+ Position::NONE,
+ )
+ .into()
+ })
+ }
+ /// Convert the decimal number to floating-point.
+ #[cfg(not(feature = "no_float"))]
+ #[rhai_fn(return_raw)]
+ pub fn to_float(x: Decimal) -> RhaiResultOf<FLOAT> {
+ FLOAT::try_from(x).map_err(|_| {
+ ERR::ErrorArithmetic(
+ format!("Cannot convert to floating-point: to_float({x})"),
+ Position::NONE,
+ )
+ .into()
+ })
+ }
+}
+
+#[cfg(not(feature = "no_float"))]
+gen_conversion_as_functions!(basic_to_float => to_float (INT) -> FLOAT);
+
+#[cfg(not(feature = "no_float"))]
+#[cfg(not(feature = "only_i32"))]
+#[cfg(not(feature = "only_i64"))]
+gen_conversion_as_functions!(numbers_to_float => to_float (i8, u8, i16, u16, i32, u32, i64, u64) -> FLOAT);
+
+#[cfg(not(feature = "no_float"))]
+#[cfg(not(feature = "only_i32"))]
+#[cfg(not(feature = "only_i64"))]
+#[cfg(not(target_family = "wasm"))]
+
+gen_conversion_as_functions!(num_128_to_float => to_float (i128, u128) -> FLOAT);
+
+gen_conversion_as_functions!(basic_to_int => to_int (char) -> INT);
+
+#[cfg(not(feature = "only_i32"))]
+#[cfg(not(feature = "only_i64"))]
+gen_conversion_as_functions!(numbers_to_int => to_int (i8, u8, i16, u16, i32, u32, i64, u64) -> INT);
+
+#[cfg(not(feature = "only_i32"))]
+#[cfg(not(feature = "only_i64"))]
+#[cfg(not(target_family = "wasm"))]
+
+gen_conversion_as_functions!(num_128_to_int => to_int (i128, u128) -> INT);
+
+#[cfg(feature = "decimal")]
+gen_conversion_into_functions!(basic_to_decimal => to_decimal (INT) -> Decimal);
+
+#[cfg(feature = "decimal")]
+#[cfg(not(feature = "only_i32"))]
+#[cfg(not(feature = "only_i64"))]
+gen_conversion_into_functions!(numbers_to_decimal => to_decimal (i8, u8, i16, u16, i32, u32, i64, u64) -> Decimal);
diff --git a/rhai/src/packages/mod.rs b/rhai/src/packages/mod.rs
new file mode 100644
index 0000000..dfa3d8d
--- /dev/null
+++ b/rhai/src/packages/mod.rs
@@ -0,0 +1,260 @@
+//! Module containing all built-in _packages_ available to Rhai, plus facilities to define custom packages.
+
+use crate::{Engine, Module, SharedModule};
+
+pub(crate) mod arithmetic;
+pub(crate) mod array_basic;
+pub(crate) mod bit_field;
+pub(crate) mod blob_basic;
+pub(crate) mod debugging;
+pub(crate) mod fn_basic;
+pub(crate) mod iter_basic;
+pub(crate) mod lang_core;
+pub(crate) mod logic;
+pub(crate) mod map_basic;
+pub(crate) mod math_basic;
+pub(crate) mod pkg_core;
+pub(crate) mod pkg_std;
+pub(crate) mod string_basic;
+pub(crate) mod string_more;
+pub(crate) mod time_basic;
+
+pub use arithmetic::ArithmeticPackage;
+#[cfg(not(feature = "no_index"))]
+pub use array_basic::BasicArrayPackage;
+pub use bit_field::BitFieldPackage;
+#[cfg(not(feature = "no_index"))]
+pub use blob_basic::BasicBlobPackage;
+#[cfg(feature = "debugging")]
+pub use debugging::DebuggingPackage;
+pub use fn_basic::BasicFnPackage;
+pub use iter_basic::BasicIteratorPackage;
+pub use lang_core::LanguageCorePackage;
+pub use logic::LogicPackage;
+#[cfg(not(feature = "no_object"))]
+pub use map_basic::BasicMapPackage;
+pub use math_basic::BasicMathPackage;
+pub use pkg_core::CorePackage;
+pub use pkg_std::StandardPackage;
+pub use string_basic::BasicStringPackage;
+pub use string_more::MoreStringPackage;
+#[cfg(not(feature = "no_time"))]
+pub use time_basic::BasicTimePackage;
+
+/// Trait that all packages must implement.
+pub trait Package {
+ /// Initialize the package.
+ /// Functions should be registered into `module` here.
+ #[cold]
+ fn init(module: &mut Module);
+
+ /// Initialize the package with an [`Engine`].
+ ///
+ /// Perform tasks such as registering custom operators/syntax.
+ #[cold]
+ #[inline]
+ #[allow(unused_variables)]
+ fn init_engine(engine: &mut Engine) {}
+
+ /// Register the package with an [`Engine`].
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// # use rhai::Engine;
+ /// # use rhai::packages::{Package, CorePackage};
+ /// let mut engine = Engine::new_raw();
+ /// let package = CorePackage::new();
+ ///
+ /// package.register_into_engine(&mut engine);
+ /// ```
+ #[cold]
+ #[inline]
+ fn register_into_engine(&self, engine: &mut Engine) -> &Self {
+ Self::init_engine(engine);
+ engine.register_global_module(self.as_shared_module());
+ self
+ }
+
+ /// Register the package with an [`Engine`] under a static namespace.
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// # use rhai::Engine;
+ /// # use rhai::packages::{Package, CorePackage};
+ /// let mut engine = Engine::new_raw();
+ /// let package = CorePackage::new();
+ ///
+ /// package.register_into_engine_as(&mut engine, "core");
+ /// ```
+ #[cfg(not(feature = "no_module"))]
+ #[cold]
+ #[inline]
+ fn register_into_engine_as(&self, engine: &mut Engine, name: &str) -> &Self {
+ Self::init_engine(engine);
+ engine.register_static_module(name, self.as_shared_module());
+ self
+ }
+
+ /// Get a reference to a shared module from this package.
+ #[must_use]
+ fn as_shared_module(&self) -> SharedModule;
+}
+
+/// Macro that makes it easy to define a _package_ (which is basically a shared [module][Module])
+/// and register functions into it.
+///
+/// Functions can be added to the package using [`Module::set_native_fn`].
+///
+/// # Example
+///
+/// Define a package named `MyPackage` with a single function named `my_add`:
+///
+/// ```
+/// use rhai::{Dynamic, EvalAltResult};
+/// use rhai::def_package;
+///
+/// fn add(x: i64, y: i64) -> Result<i64, Box<EvalAltResult>> { Ok(x + y) }
+///
+/// def_package! {
+/// /// My super-duper package.
+/// pub MyPackage(module) {
+/// // Load a native Rust function.
+/// module.set_native_fn("my_add", add);
+/// }
+/// }
+/// ```
+#[macro_export]
+macro_rules! def_package {
+ ($($(#[$outer:meta])* $mod:vis $package:ident($lib:ident)
+ $( : $($(#[$base_meta:meta])* $base_pkg:ty),+ )?
+ $block:block
+ $( |> | $engine:ident | $init_engine:block )?
+ )+) => { $(
+ $(#[$outer])*
+ $mod struct $package($crate::Shared<$crate::Module>);
+
+ impl $crate::packages::Package for $package {
+ #[inline(always)]
+ fn as_shared_module(&self) -> $crate::Shared<$crate::Module> {
+ self.0.clone()
+ }
+ fn init($lib: &mut $crate::Module) {
+ $($(
+ $(#[$base_meta])* { <$base_pkg>::init($lib); }
+ )*)*
+
+ $block
+ }
+ fn init_engine(_engine: &mut $crate::Engine) {
+ $($(
+ $(#[$base_meta])* { <$base_pkg>::init_engine(_engine); }
+ )*)*
+
+ $(
+ let $engine = _engine;
+ $init_engine
+ )*
+ }
+ }
+
+ impl Default for $package {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::new()
+ }
+ }
+
+ impl $package {
+ #[doc=concat!("Create a new `", stringify!($package), "`")]
+ #[inline]
+ #[must_use]
+ pub fn new() -> Self {
+ let mut module = $crate::Module::new();
+ <Self as $crate::packages::Package>::init(&mut module);
+ module.build_index();
+ Self(module.into())
+ }
+ }
+ )* };
+ ($($(#[$outer:meta])* $root:ident :: $package:ident => | $lib:ident | $block:block)+) => { $(
+ $(#[$outer])*
+ /// # Deprecated
+ ///
+ /// This old syntax of `def_package!` is deprecated. Use the new syntax instead.
+ ///
+ /// This syntax will be removed in the next major version.
+ #[deprecated(since = "1.5.0", note = "this is an old syntax of `def_package!` and is deprecated; use the new syntax of `def_package!` instead")]
+ pub struct $package($root::Shared<$root::Module>);
+
+ impl $root::packages::Package for $package {
+ fn as_shared_module(&self) -> $root::Shared<$root::Module> {
+ self.0.clone()
+ }
+ fn init($lib: &mut $root::Module) {
+ $block
+ }
+ }
+
+ impl Default for $package {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::new()
+ }
+ }
+
+ impl $package {
+ #[inline]
+ #[must_use]
+ pub fn new() -> Self {
+ let mut module = $root::Module::new();
+ <Self as $root::packages::Package>::init(&mut module);
+ module.build_index();
+ Self(module.into())
+ }
+ }
+ )* };
+ ($root:ident : $package:ident : $comment:expr , $lib:ident , $block:stmt) => {
+ #[doc=$comment]
+ ///
+ /// # Deprecated
+ ///
+ /// This old syntax of `def_package!` is deprecated. Use the new syntax instead.
+ ///
+ /// This syntax will be removed in the next major version.
+ #[deprecated(since = "1.4.0", note = "this is an old syntax of `def_package!` and is deprecated; use the new syntax of `def_package!` instead")]
+ pub struct $package($root::Shared<$root::Module>);
+
+ impl $root::packages::Package for $package {
+ fn as_shared_module(&self) -> $root::Shared<$root::Module> {
+ #[allow(deprecated)]
+ self.0.clone()
+ }
+ fn init($lib: &mut $root::Module) {
+ $block
+ }
+ }
+
+ impl Default for $package {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::new()
+ }
+ }
+
+ impl $package {
+ #[inline]
+ #[must_use]
+ pub fn new() -> Self {
+ let mut module = $root::Module::new();
+ <Self as $root::packages::Package>::init(&mut module);
+ module.build_index();
+ Self(module.into())
+ }
+ }
+ };
+}
diff --git a/rhai/src/packages/pkg_core.rs b/rhai/src/packages/pkg_core.rs
new file mode 100644
index 0000000..0bd2cb1
--- /dev/null
+++ b/rhai/src/packages/pkg_core.rs
@@ -0,0 +1,29 @@
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+use super::*;
+use crate::def_package;
+use crate::module::ModuleFlags;
+
+def_package! {
+ /// Core package containing basic facilities.
+ ///
+ /// # Contents
+ ///
+ /// * [`LanguageCorePackage`][super::LanguageCorePackage]
+ /// * [`ArithmeticPackage`][super::ArithmeticPackage]
+ /// * [`BasicStringPackage`][super::BasicStringPackage]
+ /// * [`BasicIteratorPackage`][super::BasicIteratorPackage]
+ /// * [`BasicFnPackage`][super::BasicFnPackage]
+ /// * [`DebuggingPackage`][super::DebuggingPackage]
+ pub CorePackage(lib) :
+ LanguageCorePackage,
+ ArithmeticPackage,
+ BasicStringPackage,
+ BasicIteratorPackage,
+ BasicFnPackage,
+ #[cfg(feature = "debugging")] DebuggingPackage
+ {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+ }
+}
diff --git a/rhai/src/packages/pkg_std.rs b/rhai/src/packages/pkg_std.rs
new file mode 100644
index 0000000..b52b058
--- /dev/null
+++ b/rhai/src/packages/pkg_std.rs
@@ -0,0 +1,35 @@
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+use super::*;
+use crate::def_package;
+use crate::module::ModuleFlags;
+
+def_package! {
+ /// Standard package containing all built-in features.
+ ///
+ /// # Contents
+ ///
+ /// * [`CorePackage`][super::CorePackage]
+ /// * [`BitFieldPackage`][super::BitFieldPackage]
+ /// * [`LogicPackage`][super::LogicPackage]
+ /// * [`BasicMathPackage`][super::BasicMathPackage]
+ /// * [`BasicArrayPackage`][super::BasicArrayPackage]
+ /// * [`BasicBlobPackage`][super::BasicBlobPackage]
+ /// * [`BasicMapPackage`][super::BasicMapPackage]
+ /// * [`BasicTimePackage`][super::BasicTimePackage]
+ /// * [`MoreStringPackage`][super::MoreStringPackage]
+ pub StandardPackage(lib) :
+ CorePackage,
+ BitFieldPackage,
+ LogicPackage,
+ BasicMathPackage,
+ #[cfg(not(feature = "no_index"))] BasicArrayPackage,
+ #[cfg(not(feature = "no_index"))] BasicBlobPackage,
+ #[cfg(not(feature = "no_object"))] BasicMapPackage,
+ #[cfg(not(feature = "no_time"))] BasicTimePackage,
+ MoreStringPackage
+ {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+ }
+}
diff --git a/rhai/src/packages/string_basic.rs b/rhai/src/packages/string_basic.rs
new file mode 100644
index 0000000..bb03f66
--- /dev/null
+++ b/rhai/src/packages/string_basic.rs
@@ -0,0 +1,436 @@
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+use crate::{def_package, FnPtr, ImmutableString, SmartString, INT};
+use std::any::TypeId;
+use std::fmt::{Binary, LowerHex, Octal, Write};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(not(feature = "no_index"))]
+use crate::Array;
+
+#[cfg(not(feature = "no_object"))]
+use crate::Map;
+
+pub const FUNC_TO_STRING: &str = "to_string";
+pub const FUNC_TO_DEBUG: &str = "to_debug";
+
+def_package! {
+ /// Package of basic string utilities (e.g. printing)
+ pub BasicStringPackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ combine_with_exported_module!(lib, "print_debug", print_debug_functions);
+ combine_with_exported_module!(lib, "number_formatting", number_formatting);
+
+ // Register characters iterator
+ #[cfg(not(feature = "no_index"))]
+ lib.set_iter(TypeId::of::<ImmutableString>(), |value| Box::new(
+ value.cast::<ImmutableString>().chars().map(Into::into).collect::<Array>().into_iter()
+ ));
+ }
+}
+
+// Register print and debug
+
+#[inline]
+pub fn print_with_func(
+ fn_name: &str,
+ ctx: &NativeCallContext,
+ value: &mut Dynamic,
+) -> ImmutableString {
+ match ctx.call_native_fn_raw(fn_name, true, &mut [value]) {
+ Ok(result) if result.is_string() => {
+ result.into_immutable_string().expect("`ImmutableString`")
+ }
+ Ok(result) => ctx.engine().map_type_name(result.type_name()).into(),
+ Err(_) => {
+ let mut buf = SmartString::new_const();
+ match fn_name {
+ FUNC_TO_DEBUG => write!(&mut buf, "{value:?}").unwrap(),
+ _ => write!(&mut buf, "{value}").unwrap(),
+ }
+ ctx.engine().map_type_name(&buf).into()
+ }
+ }
+}
+
+#[export_module]
+mod print_debug_functions {
+ /// Convert the value of the `item` into a string.
+ #[rhai_fn(name = "print", pure)]
+ pub fn print_generic(ctx: NativeCallContext, item: &mut Dynamic) -> ImmutableString {
+ print_with_func(FUNC_TO_STRING, &ctx, item)
+ }
+ /// Convert the value of the `item` into a string.
+ #[rhai_fn(name = "to_string", pure)]
+ pub fn to_string_generic(ctx: NativeCallContext, item: &mut Dynamic) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ write!(&mut buf, "{item}").unwrap();
+ ctx.engine().map_type_name(&buf).into()
+ }
+ /// Convert the value of the `item` into a string in debug format.
+ #[rhai_fn(name = "debug", pure)]
+ pub fn debug_generic(ctx: NativeCallContext, item: &mut Dynamic) -> ImmutableString {
+ print_with_func(FUNC_TO_DEBUG, &ctx, item)
+ }
+ /// Convert the value of the `item` into a string in debug format.
+ #[rhai_fn(name = "to_debug", pure)]
+ pub fn to_debug_generic(ctx: NativeCallContext, item: &mut Dynamic) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ write!(&mut buf, "{item:?}").unwrap();
+ ctx.engine().map_type_name(&buf).into()
+ }
+
+ /// Return the empty string.
+ #[rhai_fn(name = "print", name = "debug")]
+ pub fn print_empty_string(ctx: NativeCallContext) -> ImmutableString {
+ ctx.engine().const_empty_string()
+ }
+
+ /// Return the `string`.
+ #[rhai_fn(name = "print", name = "to_string")]
+ pub fn print_string(string: ImmutableString) -> ImmutableString {
+ string
+ }
+ /// Convert the string into debug format.
+ #[rhai_fn(name = "debug", name = "to_debug", pure)]
+ pub fn debug_string(string: &mut ImmutableString) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ write!(&mut buf, "{string:?}").unwrap();
+ buf.into()
+ }
+
+ /// Return the character into a string.
+ #[rhai_fn(name = "print", name = "to_string")]
+ pub fn print_char(character: char) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ buf.push(character);
+ buf.into()
+ }
+ /// Convert the string into debug format.
+ #[rhai_fn(name = "debug", name = "to_debug")]
+ pub fn debug_char(character: char) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ buf.push(character);
+ buf.into()
+ }
+
+ /// Convert the function pointer into a string in debug format.
+ #[rhai_fn(name = "debug", name = "to_debug", pure)]
+ pub fn debug_fn_ptr(f: &mut FnPtr) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ write!(&mut buf, "{f}").unwrap();
+ buf.into()
+ }
+
+ /// Return the boolean value into a string.
+ #[rhai_fn(name = "print", name = "to_string")]
+ pub fn print_bool(value: bool) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ write!(&mut buf, "{value}").unwrap();
+ buf.into()
+ }
+ /// Convert the boolean value into a string in debug format.
+ #[rhai_fn(name = "debug", name = "to_debug")]
+ pub fn debug_bool(value: bool) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ write!(&mut buf, "{value:?}").unwrap();
+ buf.into()
+ }
+
+ /// Return the empty string.
+ #[allow(unused_variables)]
+ #[rhai_fn(name = "print", name = "to_string")]
+ pub fn print_unit(ctx: NativeCallContext, unit: ()) -> ImmutableString {
+ ctx.engine().const_empty_string()
+ }
+ /// Convert the unit into a string in debug format.
+ #[allow(unused_variables)]
+ #[rhai_fn(name = "debug", name = "to_debug")]
+ pub fn debug_unit(unit: ()) -> ImmutableString {
+ "()".into()
+ }
+
+ /// Convert the value of `number` into a string.
+ #[cfg(not(feature = "no_float"))]
+ #[rhai_fn(name = "print", name = "to_string")]
+ pub fn print_f64(number: f64) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ write!(&mut buf, "{}", crate::types::FloatWrapper::new(number)).unwrap();
+ buf.into()
+ }
+ /// Convert the value of `number` into a string.
+ #[cfg(not(feature = "no_float"))]
+ #[rhai_fn(name = "print", name = "to_string")]
+ pub fn print_f32(number: f32) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ write!(&mut buf, "{}", crate::types::FloatWrapper::new(number)).unwrap();
+ buf.into()
+ }
+ /// Convert the value of `number` into a string.
+ #[cfg(not(feature = "no_float"))]
+ #[rhai_fn(name = "debug", name = "to_debug")]
+ pub fn debug_f64(number: f64) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ write!(&mut buf, "{:?}", crate::types::FloatWrapper::new(number)).unwrap();
+ buf.into()
+ }
+ /// Convert the value of `number` into a string.
+ #[cfg(not(feature = "no_float"))]
+ #[rhai_fn(name = "debug", name = "to_debug")]
+ pub fn debug_f32(number: f32) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ write!(&mut buf, "{:?}", crate::types::FloatWrapper::new(number)).unwrap();
+ buf.into()
+ }
+
+ /// Convert the array into a string.
+ #[cfg(not(feature = "no_index"))]
+ #[rhai_fn(
+ name = "print",
+ name = "to_string",
+ name = "debug",
+ name = "to_debug",
+ pure
+ )]
+ pub fn format_array(ctx: NativeCallContext, array: &mut Array) -> ImmutableString {
+ let len = array.len();
+ let mut result = SmartString::new_const();
+ result.push('[');
+
+ array.iter_mut().enumerate().for_each(|(i, x)| {
+ result.push_str(&print_with_func(FUNC_TO_DEBUG, &ctx, x));
+ if i < len - 1 {
+ result.push_str(", ");
+ }
+ });
+
+ result.push(']');
+ result.into()
+ }
+
+ /// Convert the object map into a string.
+ #[cfg(not(feature = "no_object"))]
+ #[rhai_fn(
+ name = "print",
+ name = "to_string",
+ name = "debug",
+ name = "to_debug",
+ pure
+ )]
+ pub fn format_map(ctx: NativeCallContext, map: &mut Map) -> ImmutableString {
+ let len = map.len();
+ let mut result = SmartString::new_const();
+ result.push_str("#{");
+
+ map.iter_mut().enumerate().for_each(|(i, (k, v))| {
+ write!(
+ result,
+ "{:?}: {}{}",
+ k,
+ &print_with_func(FUNC_TO_DEBUG, &ctx, v),
+ if i < len - 1 { ", " } else { "" }
+ )
+ .unwrap();
+ });
+
+ result.push('}');
+ result.into()
+ }
+}
+
+#[export_module]
+mod number_formatting {
+ fn to_hex<T: LowerHex>(value: T) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ write!(&mut buf, "{value:x}").unwrap();
+ buf.into()
+ }
+ fn to_octal<T: Octal>(value: T) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ write!(&mut buf, "{value:o}").unwrap();
+ buf.into()
+ }
+ fn to_binary<T: Binary>(value: T) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ write!(&mut buf, "{value:b}").unwrap();
+ buf.into()
+ }
+
+ /// Convert the `value` into a string in hex format.
+ #[rhai_fn(name = "to_hex")]
+ pub fn int_to_hex(value: INT) -> ImmutableString {
+ to_hex(value)
+ }
+ /// Convert the `value` into a string in octal format.
+ #[rhai_fn(name = "to_octal")]
+ pub fn int_to_octal(value: INT) -> ImmutableString {
+ to_octal(value)
+ }
+ /// Convert the `value` into a string in binary format.
+ #[rhai_fn(name = "to_binary")]
+ pub fn int_to_binary(value: INT) -> ImmutableString {
+ to_binary(value)
+ }
+
+ #[cfg(not(feature = "only_i32"))]
+ #[cfg(not(feature = "only_i64"))]
+ pub mod numbers {
+ /// Convert the `value` into a string in hex format.
+ #[rhai_fn(name = "to_hex")]
+ pub fn u8_to_hex(value: u8) -> ImmutableString {
+ to_hex(value)
+ }
+ /// Convert the `value` into a string in hex format.
+ #[rhai_fn(name = "to_hex")]
+ pub fn u16_to_hex(value: u16) -> ImmutableString {
+ to_hex(value)
+ }
+ /// Convert the `value` into a string in hex format.
+ #[rhai_fn(name = "to_hex")]
+ pub fn u32_to_hex(value: u32) -> ImmutableString {
+ to_hex(value)
+ }
+ /// Convert the `value` into a string in hex format.
+ #[rhai_fn(name = "to_hex")]
+ pub fn u64_to_hex(value: u64) -> ImmutableString {
+ to_hex(value)
+ }
+ /// Convert the `value` into a string in hex format.
+ #[rhai_fn(name = "to_hex")]
+ pub fn i8_to_hex(value: i8) -> ImmutableString {
+ to_hex(value)
+ }
+ /// Convert the `value` into a string in hex format.
+ #[rhai_fn(name = "to_hex")]
+ pub fn i16_to_hex(value: i16) -> ImmutableString {
+ to_hex(value)
+ }
+ /// Convert the `value` into a string in hex format.
+ #[rhai_fn(name = "to_hex")]
+ pub fn i32_to_hex(value: i32) -> ImmutableString {
+ to_hex(value)
+ }
+ /// Convert the `value` into a string in hex format.
+ #[rhai_fn(name = "to_hex")]
+ pub fn i64_to_hex(value: i64) -> ImmutableString {
+ to_hex(value)
+ }
+ /// Convert the `value` into a string in octal format.
+ #[rhai_fn(name = "to_octal")]
+ pub fn u8_to_octal(value: u8) -> ImmutableString {
+ to_octal(value)
+ }
+ /// Convert the `value` into a string in octal format.
+ #[rhai_fn(name = "to_octal")]
+ pub fn u16_to_octal(value: u16) -> ImmutableString {
+ to_octal(value)
+ }
+ /// Convert the `value` into a string in octal format.
+ #[rhai_fn(name = "to_octal")]
+ pub fn u32_to_octal(value: u32) -> ImmutableString {
+ to_octal(value)
+ }
+ /// Convert the `value` into a string in octal format.
+ #[rhai_fn(name = "to_octal")]
+ pub fn u64_to_octal(value: u64) -> ImmutableString {
+ to_octal(value)
+ }
+ /// Convert the `value` into a string in octal format.
+ #[rhai_fn(name = "to_octal")]
+ pub fn i8_to_octal(value: i8) -> ImmutableString {
+ to_octal(value)
+ }
+ /// Convert the `value` into a string in octal format.
+ #[rhai_fn(name = "to_octal")]
+ pub fn i16_to_octal(value: i16) -> ImmutableString {
+ to_octal(value)
+ }
+ /// Convert the `value` into a string in octal format.
+ #[rhai_fn(name = "to_octal")]
+ pub fn i32_to_octal(value: i32) -> ImmutableString {
+ to_octal(value)
+ }
+ /// Convert the `value` into a string in octal format.
+ #[rhai_fn(name = "to_octal")]
+ pub fn i64_to_octal(value: i64) -> ImmutableString {
+ to_octal(value)
+ }
+ /// Convert the `value` into a string in binary format.
+ #[rhai_fn(name = "to_binary")]
+ pub fn u8_to_binary(value: u8) -> ImmutableString {
+ to_binary(value)
+ }
+ /// Convert the `value` into a string in binary format.
+ #[rhai_fn(name = "to_binary")]
+ pub fn u16_to_binary(value: u16) -> ImmutableString {
+ to_binary(value)
+ }
+ /// Convert the `value` into a string in binary format.
+ #[rhai_fn(name = "to_binary")]
+ pub fn u32_to_binary(value: u32) -> ImmutableString {
+ to_binary(value)
+ }
+ /// Convert the `value` into a string in binary format.
+ #[rhai_fn(name = "to_binary")]
+ pub fn u64_to_binary(value: u64) -> ImmutableString {
+ to_binary(value)
+ }
+ /// Convert the `value` into a string in binary format.
+ #[rhai_fn(name = "to_binary")]
+ pub fn i8_to_binary(value: i8) -> ImmutableString {
+ to_binary(value)
+ }
+ /// Convert the `value` into a string in binary format.
+ #[rhai_fn(name = "to_binary")]
+ pub fn i16_to_binary(value: i16) -> ImmutableString {
+ to_binary(value)
+ }
+ /// Convert the `value` into a string in binary format.
+ #[rhai_fn(name = "to_binary")]
+ pub fn i32_to_binary(value: i32) -> ImmutableString {
+ to_binary(value)
+ }
+ /// Convert the `value` into a string in binary format.
+ #[rhai_fn(name = "to_binary")]
+ pub fn i64_to_binary(value: i64) -> ImmutableString {
+ to_binary(value)
+ }
+
+ #[cfg(not(target_family = "wasm"))]
+
+ pub mod num_128 {
+ /// Convert the `value` into a string in hex format.
+ #[rhai_fn(name = "to_hex")]
+ pub fn u128_to_hex(value: u128) -> ImmutableString {
+ to_hex(value)
+ }
+ /// Convert the `value` into a string in hex format.
+ #[rhai_fn(name = "to_hex")]
+ pub fn i128_to_hex(value: i128) -> ImmutableString {
+ to_hex(value)
+ }
+ /// Convert the `value` into a string in octal format.
+ #[rhai_fn(name = "to_octal")]
+ pub fn u128_to_octal(value: u128) -> ImmutableString {
+ to_octal(value)
+ }
+ /// Convert the `value` into a string in octal format.
+ #[rhai_fn(name = "to_octal")]
+ pub fn i128_to_octal(value: i128) -> ImmutableString {
+ to_octal(value)
+ }
+ /// Convert the `value` into a string in binary format.
+ #[rhai_fn(name = "to_binary")]
+ pub fn u128_to_binary(value: u128) -> ImmutableString {
+ to_binary(value)
+ }
+ /// Convert the `value` into a string in binary format.
+ #[rhai_fn(name = "to_binary")]
+ pub fn i128_to_binary(value: i128) -> ImmutableString {
+ to_binary(value)
+ }
+ }
+ }
+}
diff --git a/rhai/src/packages/string_more.rs b/rhai/src/packages/string_more.rs
new file mode 100644
index 0000000..7de4fec
--- /dev/null
+++ b/rhai/src/packages/string_more.rs
@@ -0,0 +1,1590 @@
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+use crate::{
+ def_package, Dynamic, ExclusiveRange, ImmutableString, InclusiveRange, Position, RhaiResultOf,
+ SmartString, StaticVec, ERR, INT, MAX_USIZE_INT,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{any::TypeId, mem};
+
+use super::string_basic::{print_with_func, FUNC_TO_STRING};
+
+def_package! {
+ /// Package of additional string utilities over [`BasicStringPackage`][super::BasicStringPackage]
+ pub MoreStringPackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ combine_with_exported_module!(lib, "string", string_functions);
+ }
+}
+
+#[export_module]
+mod string_functions {
+ #[rhai_fn(name = "+", pure)]
+ pub fn add_append(
+ ctx: NativeCallContext,
+ string: &mut ImmutableString,
+ mut item: Dynamic,
+ ) -> ImmutableString {
+ let s = print_with_func(FUNC_TO_STRING, &ctx, &mut item);
+
+ if s.is_empty() {
+ string.clone()
+ } else {
+ let mut buf = SmartString::from(string.as_str());
+ buf.push_str(&s);
+ buf.into()
+ }
+ }
+ #[rhai_fn(name = "+=", name = "append")]
+ pub fn add(ctx: NativeCallContext, string: &mut ImmutableString, mut item: Dynamic) {
+ let s = print_with_func(FUNC_TO_STRING, &ctx, &mut item);
+
+ if !s.is_empty() {
+ let mut buf = SmartString::from(string.as_str());
+ buf.push_str(&s);
+ *string = buf.into();
+ }
+ }
+ #[rhai_fn(name = "+", pure)]
+ pub fn add_prepend(
+ ctx: NativeCallContext,
+ item: &mut Dynamic,
+ string: &str,
+ ) -> ImmutableString {
+ let mut s = print_with_func(FUNC_TO_STRING, &ctx, item);
+
+ if !string.is_empty() {
+ s.make_mut().push_str(string);
+ }
+
+ s
+ }
+
+ // The following are needed in order to override the generic versions with `Dynamic` parameters.
+
+ #[rhai_fn(name = "+", pure)]
+ pub fn add_append_str(
+ string1: &mut ImmutableString,
+ string2: ImmutableString,
+ ) -> ImmutableString {
+ &*string1 + string2
+ }
+ #[rhai_fn(name = "+", pure)]
+ pub fn add_append_char(string: &mut ImmutableString, character: char) -> ImmutableString {
+ &*string + character
+ }
+ #[rhai_fn(name = "+")]
+ pub fn add_prepend_char(character: char, string: &str) -> ImmutableString {
+ let mut buf = SmartString::new_const();
+ buf.push(character);
+ buf.push_str(string);
+ buf.into()
+ }
+
+ #[allow(unused_variables)]
+ #[rhai_fn(name = "+")]
+ pub fn add_append_unit(string: ImmutableString, item: ()) -> ImmutableString {
+ string
+ }
+ #[allow(unused_variables)]
+ #[rhai_fn(name = "+")]
+ pub fn add_prepend_unit(item: (), string: ImmutableString) -> ImmutableString {
+ string
+ }
+
+ #[rhai_fn(name = "+=")]
+ pub fn add_assign_append_str(string1: &mut ImmutableString, string2: ImmutableString) {
+ *string1 += string2;
+ }
+ #[rhai_fn(name = "+=", pure)]
+ pub fn add_assign_append_char(string: &mut ImmutableString, character: char) {
+ *string += character;
+ }
+ #[allow(unused_variables)]
+ #[rhai_fn(name = "+=")]
+ pub fn add_assign_append_unit(string: &mut ImmutableString, item: ()) {}
+
+ #[cfg(not(feature = "no_index"))]
+ pub mod blob_functions {
+ use crate::Blob;
+
+ #[rhai_fn(name = "+", pure)]
+ pub fn add_append(string: &mut ImmutableString, utf8: Blob) -> ImmutableString {
+ if utf8.is_empty() {
+ return string.clone();
+ }
+
+ let s = String::from_utf8_lossy(&utf8);
+
+ if string.is_empty() {
+ match s {
+ std::borrow::Cow::Borrowed(_) => String::from_utf8(utf8).unwrap(),
+ std::borrow::Cow::Owned(_) => s.into_owned(),
+ }
+ .into()
+ } else {
+ let mut x = SmartString::from(string.as_str());
+ x.push_str(s.as_ref());
+ x.into()
+ }
+ }
+ #[rhai_fn(name = "+=", name = "append")]
+ pub fn add(string: &mut ImmutableString, utf8: Blob) {
+ let mut s = SmartString::from(string.as_str());
+ if !utf8.is_empty() {
+ s.push_str(&String::from_utf8_lossy(&utf8));
+ *string = s.into();
+ }
+ }
+ #[rhai_fn(name = "+")]
+ pub fn add_prepend(utf8: Blob, string: &str) -> ImmutableString {
+ let s = String::from_utf8_lossy(&utf8);
+ let mut s = match s {
+ std::borrow::Cow::Borrowed(_) => String::from_utf8(utf8).unwrap(),
+ std::borrow::Cow::Owned(_) => s.into_owned(),
+ };
+
+ if !string.is_empty() {
+ s.push_str(string);
+ }
+
+ s.into()
+ }
+
+ /// Convert the string into an UTF-8 encoded byte-stream as a BLOB.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "朝には紅顔ありて夕べには白骨となる";
+ ///
+ /// let bytes = text.to_blob();
+ ///
+ /// print(bytes.len()); // prints 51
+ /// ```
+ pub fn to_blob(string: &str) -> Blob {
+ if string.is_empty() {
+ Blob::new()
+ } else {
+ string.as_bytes().into()
+ }
+ }
+ }
+
+ /// Return the length of the string, in number of characters.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "朝には紅顔ありて夕べには白骨となる";
+ ///
+ /// print(text.len); // prints 17
+ /// ```
+ #[rhai_fn(name = "len", get = "len")]
+ pub fn len(string: &str) -> INT {
+ if string.is_empty() {
+ 0
+ } else {
+ string.chars().count() as INT
+ }
+ }
+ /// Return true if the string is empty.
+ #[rhai_fn(name = "is_empty", get = "is_empty")]
+ pub fn is_empty(string: &str) -> bool {
+ string.len() == 0
+ }
+ /// Return the length of the string, in number of bytes used to store it in UTF-8 encoding.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "朝には紅顔ありて夕べには白骨となる";
+ ///
+ /// print(text.bytes); // prints 51
+ /// ```
+ #[rhai_fn(name = "bytes", get = "bytes")]
+ pub fn bytes(string: &str) -> INT {
+ if string.is_empty() {
+ 0
+ } else {
+ string.len() as INT
+ }
+ }
+ /// Remove all occurrences of a sub-string from the string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foobar!";
+ ///
+ /// text.remove("hello");
+ ///
+ /// print(text); // prints ", world! , foobar!"
+ /// ```
+ pub fn remove(string: &mut ImmutableString, sub_string: &str) {
+ *string -= sub_string;
+ }
+ /// Remove all occurrences of a character from the string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foobar!";
+ ///
+ /// text.remove("o");
+ ///
+ /// print(text); // prints "hell, wrld! hell, fbar!"
+ /// ```
+ #[rhai_fn(name = "remove")]
+ pub fn remove_char(string: &mut ImmutableString, character: char) {
+ *string -= character;
+ }
+ /// Clear the string, making it empty.
+ pub fn clear(string: &mut ImmutableString) {
+ if !string.is_empty() {
+ if let Some(s) = string.get_mut() {
+ s.clear();
+ } else {
+ *string = ImmutableString::new();
+ }
+ }
+ }
+ /// Cut off the string at the specified number of characters.
+ ///
+ /// * If `len` ≤ 0, the string is cleared.
+ /// * If `len` ≥ length of string, the string is not truncated.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foobar!";
+ ///
+ /// text.truncate(13);
+ ///
+ /// print(text); // prints "hello, world!"
+ ///
+ /// text.truncate(10);
+ ///
+ /// print(text); // prints "hello, world!"
+ /// ```
+ pub fn truncate(string: &mut ImmutableString, len: INT) {
+ if len > 0 {
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let len = len.min(MAX_USIZE_INT) as usize;
+ if let Some((index, _)) = string.char_indices().nth(len) {
+ let copy = string.make_mut();
+ copy.truncate(index);
+ }
+ } else {
+ clear(string);
+ }
+ }
+ /// Remove whitespace characters from both ends of the string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = " hello ";
+ ///
+ /// text.trim();
+ ///
+ /// print(text); // prints "hello"
+ /// ```
+ pub fn trim(string: &mut ImmutableString) {
+ if let Some(s) = string.get_mut() {
+ let trimmed = s.trim();
+
+ if trimmed != s {
+ *s = trimmed.into();
+ }
+ } else {
+ let trimmed = string.trim();
+
+ if trimmed != string {
+ *string = trimmed.into();
+ }
+ }
+ }
+ /// Remove the last character from the string and return it.
+ ///
+ /// If the string is empty, `()` is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.pop()); // prints '!'
+ ///
+ /// print(text); // prints "hello, world"
+ /// ```
+ pub fn pop(string: &mut ImmutableString) -> Dynamic {
+ if string.is_empty() {
+ Dynamic::UNIT
+ } else {
+ match string.make_mut().pop() {
+ Some(c) => c.into(),
+ None => Dynamic::UNIT,
+ }
+ }
+ }
+ /// Remove a specified number of characters from the end of the string and return it as a
+ /// new string.
+ ///
+ /// * If `len` ≤ 0, the string is not modified and an empty string is returned.
+ /// * If `len` ≥ length of string, the string is cleared and the entire string returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.pop(4)); // prints "rld!"
+ ///
+ /// print(text); // prints "hello, wo"
+ /// ```
+ #[rhai_fn(name = "pop")]
+ pub fn pop_string(
+ ctx: NativeCallContext,
+ string: &mut ImmutableString,
+ len: INT,
+ ) -> ImmutableString {
+ if string.is_empty() || len <= 0 {
+ return ctx.engine().const_empty_string();
+ }
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let len = len.min(MAX_USIZE_INT) as usize;
+
+ let mut chars = StaticVec::<char>::with_capacity(len);
+
+ for _ in 0..len {
+ match string.make_mut().pop() {
+ Some(c) => chars.push(c),
+ None => break,
+ }
+ }
+
+ chars.into_iter().rev().collect::<SmartString>().into()
+ }
+
+ /// Convert the string to all upper-case and return it as a new string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!"
+ ///
+ /// print(text.to_upper()); // prints "HELLO, WORLD!"
+ ///
+ /// print(text); // prints "hello, world!"
+ /// ```
+ #[rhai_fn(pure)]
+ pub fn to_upper(string: &mut ImmutableString) -> ImmutableString {
+ if string.chars().all(char::is_uppercase) {
+ string.clone()
+ } else {
+ string.to_uppercase().into()
+ }
+ }
+ /// Convert the string to all upper-case.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!"
+ ///
+ /// text.make_upper();
+ ///
+ /// print(text); // prints "HELLO, WORLD!";
+ /// ```
+ pub fn make_upper(string: &mut ImmutableString) {
+ if !string.is_empty() && string.chars().any(|ch| !ch.is_uppercase()) {
+ *string = string.to_uppercase().into();
+ }
+ }
+ /// Convert the string to all lower-case and return it as a new string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "HELLO, WORLD!"
+ ///
+ /// print(text.to_lower()); // prints "hello, world!"
+ ///
+ /// print(text); // prints "HELLO, WORLD!"
+ /// ```
+ #[rhai_fn(pure)]
+ pub fn to_lower(string: &mut ImmutableString) -> ImmutableString {
+ if string.is_empty() || string.chars().all(char::is_lowercase) {
+ string.clone()
+ } else {
+ string.to_lowercase().into()
+ }
+ }
+ /// Convert the string to all lower-case.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "HELLO, WORLD!"
+ ///
+ /// text.make_lower();
+ ///
+ /// print(text); // prints "hello, world!";
+ /// ```
+ pub fn make_lower(string: &mut ImmutableString) {
+ if string.chars().any(|ch| !ch.is_lowercase()) {
+ *string = string.to_lowercase().into();
+ }
+ }
+
+ /// Convert the character to upper-case and return it as a new character.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let ch = 'a';
+ ///
+ /// print(ch.to_upper()); // prints 'A'
+ ///
+ /// print(ch); // prints 'a'
+ /// ```
+ #[rhai_fn(name = "to_upper")]
+ pub fn to_upper_char(character: char) -> char {
+ let mut stream = character.to_uppercase();
+ let ch = stream.next().unwrap();
+ if stream.next().is_some() {
+ character
+ } else {
+ ch
+ }
+ }
+ /// Convert the character to upper-case.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let ch = 'a';
+ ///
+ /// ch.make_upper();
+ ///
+ /// print(ch); // prints 'A'
+ /// ```
+ #[rhai_fn(name = "make_upper")]
+ pub fn make_upper_char(character: &mut char) {
+ *character = to_upper_char(*character);
+ }
+ /// Convert the character to lower-case and return it as a new character.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let ch = 'A';
+ ///
+ /// print(ch.to_lower()); // prints 'a'
+ ///
+ /// print(ch); // prints 'A'
+ /// ```
+ #[rhai_fn(name = "to_lower")]
+ pub fn to_lower_char(character: char) -> char {
+ let mut stream = character.to_lowercase();
+ let ch = stream.next().unwrap();
+ if stream.next().is_some() {
+ character
+ } else {
+ ch
+ }
+ }
+ /// Convert the character to lower-case.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let ch = 'A';
+ ///
+ /// ch.make_lower();
+ ///
+ /// print(ch); // prints 'a'
+ /// ```
+ #[rhai_fn(name = "make_lower")]
+ pub fn make_lower_char(character: &mut char) {
+ *character = to_lower_char(*character);
+ }
+
+ /// Return `true` if the string contains a specified string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.contains("hello")); // prints true
+ ///
+ /// print(text.contains("hey")); // prints false
+ /// ```
+ pub fn contains(string: &str, match_string: &str) -> bool {
+ string.contains(match_string)
+ }
+
+ /// Return `true` if the string contains a specified character.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.contains('h')); // prints true
+ ///
+ /// print(text.contains('x')); // prints false
+ /// ```
+ #[rhai_fn(name = "contains")]
+ pub fn contains_char(string: &str, character: char) -> bool {
+ string.contains(character)
+ }
+
+ /// Return `true` if the string starts with a specified string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.starts_with("hello")); // prints true
+ ///
+ /// print(text.starts_with("world")); // prints false
+ /// ```
+ pub fn starts_with(string: &str, match_string: &str) -> bool {
+ string.starts_with(match_string)
+ }
+ /// Return `true` if the string ends with a specified string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.ends_with("world!")); // prints true
+ ///
+ /// print(text.ends_with("hello")); // prints false
+ /// ```
+ pub fn ends_with(string: &str, match_string: &str) -> bool {
+ string.ends_with(match_string)
+ }
+
+ /// Find the specified `character` in the string, starting from the specified `start` position,
+ /// and return the first index where it is found.
+ /// If the `character` is not found, `-1` is returned.
+ ///
+ /// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+ /// * If `start` < -length of string, position counts from the beginning of the string.
+ /// * If `start` ≥ length of string, `-1` is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.index_of('l', 5)); // prints 10 (first index after 5)
+ ///
+ /// print(text.index_of('o', -7)); // prints 8
+ ///
+ /// print(text.index_of('x', 0)); // prints -1
+ /// ```
+ #[rhai_fn(name = "index_of")]
+ pub fn index_of_char_starting_from(string: &str, character: char, start: INT) -> INT {
+ if string.is_empty() {
+ return -1;
+ }
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let start = if start < 0 {
+ let abs_start = start.unsigned_abs();
+
+ if abs_start as u64 > MAX_USIZE_INT as u64 {
+ return -1 as INT;
+ }
+
+ let abs_start = abs_start as usize;
+ let chars: Vec<_> = string.chars().collect();
+ let num_chars = chars.len();
+
+ if abs_start > num_chars {
+ 0
+ } else {
+ chars
+ .into_iter()
+ .take(num_chars - abs_start)
+ .collect::<String>()
+ .len()
+ }
+ } else if start == 0 {
+ 0
+ } else if start > MAX_USIZE_INT || start as usize >= string.chars().count() {
+ return -1 as INT;
+ } else {
+ string
+ .chars()
+ .take(start as usize)
+ .collect::<String>()
+ .len()
+ };
+
+ string[start..].find(character).map_or(-1 as INT, |index| {
+ string[0..start + index].chars().count() as INT
+ })
+ }
+ /// Find the specified `character` in the string and return the first index where it is found.
+ /// If the `character` is not found, `-1` is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.index_of('l')); // prints 2 (first index)
+ ///
+ /// print(text.index_of('x')); // prints -1
+ /// ```
+ #[rhai_fn(name = "index_of")]
+ pub fn index_of_char(string: &str, character: char) -> INT {
+ if string.is_empty() {
+ -1
+ } else {
+ string
+ .find(character)
+ .map_or(-1 as INT, |index| string[0..index].chars().count() as INT)
+ }
+ }
+ /// Find the specified sub-string in the string, starting from the specified `start` position,
+ /// and return the first index where it is found.
+ /// If the sub-string is not found, `-1` is returned.
+ ///
+ /// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+ /// * If `start` < -length of string, position counts from the beginning of the string.
+ /// * If `start` ≥ length of string, `-1` is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foobar!";
+ ///
+ /// print(text.index_of("ll", 5)); // prints 16 (first index after 5)
+ ///
+ /// print(text.index_of("ll", -15)); // prints 16
+ ///
+ /// print(text.index_of("xx", 0)); // prints -1
+ /// ```
+ #[rhai_fn(name = "index_of")]
+ pub fn index_of_string_starting_from(string: &str, find_string: &str, start: INT) -> INT {
+ if string.is_empty() {
+ return -1;
+ }
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let start = if start < 0 {
+ let abs_start = start.unsigned_abs();
+
+ if abs_start as u64 > MAX_USIZE_INT as u64 {
+ return -1 as INT;
+ }
+
+ let abs_start = abs_start as usize;
+ let chars = string.chars().collect::<Vec<_>>();
+ let num_chars = chars.len();
+
+ if abs_start > num_chars {
+ 0
+ } else {
+ chars
+ .into_iter()
+ .take(num_chars - abs_start)
+ .collect::<String>()
+ .len()
+ }
+ } else if start == 0 {
+ 0
+ } else if start > MAX_USIZE_INT || start as usize >= string.chars().count() {
+ return -1 as INT;
+ } else {
+ string
+ .chars()
+ .take(start as usize)
+ .collect::<String>()
+ .len()
+ };
+
+ string[start..]
+ .find(find_string)
+ .map_or(-1 as INT, |index| {
+ string[0..start + index].chars().count() as INT
+ })
+ }
+ /// Find the specified `character` in the string and return the first index where it is found.
+ /// If the `character` is not found, `-1` is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foobar!";
+ ///
+ /// print(text.index_of("ll")); // prints 2 (first index)
+ ///
+ /// print(text.index_of("xx:)); // prints -1
+ /// ```
+ #[rhai_fn(name = "index_of")]
+ pub fn index_of(string: &str, find_string: &str) -> INT {
+ if string.is_empty() {
+ -1
+ } else {
+ string
+ .find(find_string)
+ .map_or(-1 as INT, |index| string[0..index].chars().count() as INT)
+ }
+ }
+
+ /// Get the character at the `index` position in the string.
+ ///
+ /// * If `index` < 0, position counts from the end of the string (`-1` is the last character).
+ /// * If `index` < -length of string, zero is returned.
+ /// * If `index` ≥ length of string, zero is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.get(0)); // prints 'h'
+ ///
+ /// print(text.get(-1)); // prints '!'
+ ///
+ /// print(text.get(99)); // prints empty (for '()')'
+ /// ```
+ pub fn get(string: &str, index: INT) -> Dynamic {
+ if index >= 0 {
+ if index > MAX_USIZE_INT {
+ return Dynamic::UNIT;
+ }
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let index = index as usize;
+
+ string
+ .chars()
+ .nth(index)
+ .map_or_else(|| Dynamic::UNIT, Into::into)
+ } else {
+ // Count from end if negative
+ let abs_index = index.unsigned_abs();
+
+ if abs_index as u64 > MAX_USIZE_INT as u64 {
+ return Dynamic::UNIT;
+ }
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let abs_index = abs_index as usize;
+
+ string
+ .chars()
+ .rev()
+ .nth(abs_index - 1)
+ .map_or_else(|| Dynamic::UNIT, Into::into)
+ }
+ }
+ /// Set the `index` position in the string to a new `character`.
+ ///
+ /// * If `index` < 0, position counts from the end of the string (`-1` is the last character).
+ /// * If `index` < -length of string, the string is not modified.
+ /// * If `index` ≥ length of string, the string is not modified.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// text.set(3, 'x');
+ ///
+ /// print(text); // prints "helxo, world!"
+ ///
+ /// text.set(-3, 'x');
+ ///
+ /// print(text); // prints "hello, worxd!"
+ ///
+ /// text.set(99, 'x');
+ ///
+ /// print(text); // prints "hello, worxd!"
+ /// ```
+ pub fn set(string: &mut ImmutableString, index: INT, character: char) {
+ if index >= 0 {
+ if index > MAX_USIZE_INT {
+ return;
+ }
+
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let index = index as usize;
+
+ *string = string
+ .chars()
+ .enumerate()
+ .map(|(i, ch)| if i == index { character } else { ch })
+ .collect();
+ } else {
+ let abs_index = index.unsigned_abs();
+
+ if abs_index as u64 > MAX_USIZE_INT as u64 {
+ return;
+ }
+
+ #[allow(clippy::cast_possible_truncation)]
+ let abs_index = abs_index as usize;
+ let string_len = string.chars().count();
+
+ if abs_index <= string_len {
+ let index = string_len - abs_index;
+ *string = string
+ .chars()
+ .enumerate()
+ .map(|(i, ch)| if i == index { character } else { ch })
+ .collect();
+ }
+ }
+ }
+
+ /// Copy an exclusive range of characters from the string and return it as a new string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.sub_string(3..7)); // prints "lo, "
+ /// ```
+ #[rhai_fn(name = "sub_string")]
+ pub fn sub_string_range(
+ ctx: NativeCallContext,
+ string: &str,
+ range: ExclusiveRange,
+ ) -> ImmutableString {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ sub_string(ctx, string, start, end - start)
+ }
+ /// Copy an inclusive range of characters from the string and return it as a new string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.sub_string(3..=7)); // prints "lo, w"
+ /// ```
+ #[rhai_fn(name = "sub_string")]
+ pub fn sub_string_inclusive_range(
+ ctx: NativeCallContext,
+ string: &str,
+ range: InclusiveRange,
+ ) -> ImmutableString {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ sub_string(ctx, string, start, end - start + 1)
+ }
+ /// Copy a portion of the string and return it as a new string.
+ ///
+ /// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+ /// * If `start` < -length of string, position counts from the beginning of the string.
+ /// * If `start` ≥ length of string, an empty string is returned.
+ /// * If `len` ≤ 0, an empty string is returned.
+ /// * If `start` position + `len` ≥ length of string, entire portion of the string after the `start` position is copied and returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.sub_string(3, 4)); // prints "lo, "
+ ///
+ /// print(text.sub_string(-8, 3)); // prints ", w"
+ /// ```
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ pub fn sub_string(
+ ctx: NativeCallContext,
+ string: &str,
+ start: INT,
+ len: INT,
+ ) -> ImmutableString {
+ if string.is_empty() || len <= 0 {
+ return ctx.engine().const_empty_string();
+ }
+
+ let mut chars = StaticVec::with_capacity(string.len());
+
+ let offset = if string.is_empty() || len <= 0 {
+ return ctx.engine().const_empty_string();
+ } else if start < 0 {
+ let abs_start = start.unsigned_abs();
+
+ if abs_start as u64 > MAX_USIZE_INT as u64 {
+ return ctx.engine().const_empty_string();
+ }
+
+ #[allow(clippy::cast_possible_truncation)]
+ let abs_start = abs_start as usize;
+
+ chars.extend(string.chars());
+
+ if abs_start > chars.len() {
+ 0
+ } else {
+ chars.len() - abs_start
+ }
+ } else if start > MAX_USIZE_INT || start as usize >= string.chars().count() {
+ return ctx.engine().const_empty_string();
+ } else {
+ start as usize
+ };
+
+ if chars.is_empty() {
+ chars.extend(string.chars());
+ }
+
+ let len = len.min(MAX_USIZE_INT) as usize;
+
+ let len = if offset + len > chars.len() {
+ chars.len() - offset
+ } else {
+ len
+ };
+
+ chars
+ .iter()
+ .skip(offset)
+ .take(len)
+ .copied()
+ .collect::<String>()
+ .into()
+ }
+ /// Copy a portion of the string beginning at the `start` position till the end and return it as
+ /// a new string.
+ ///
+ /// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+ /// * If `start` < -length of string, the entire string is copied and returned.
+ /// * If `start` ≥ length of string, an empty string is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.sub_string(5)); // prints ", world!"
+ ///
+ /// print(text.sub_string(-5)); // prints "orld!"
+ /// ```
+ #[rhai_fn(name = "sub_string")]
+ pub fn sub_string_starting_from(
+ ctx: NativeCallContext,
+ string: &str,
+ start: INT,
+ ) -> ImmutableString {
+ if string.is_empty() {
+ ctx.engine().const_empty_string()
+ } else {
+ let len = string.len() as INT;
+ sub_string(ctx, string, start, len)
+ }
+ }
+
+ /// Remove all characters from the string except those within an exclusive `range`.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// text.crop(2..8);
+ ///
+ /// print(text); // prints "llo, w"
+ /// ```
+ #[rhai_fn(name = "crop")]
+ pub fn crop_range(ctx: NativeCallContext, string: &mut ImmutableString, range: ExclusiveRange) {
+ let start = INT::max(range.start, 0);
+ let end = INT::max(range.end, start);
+ crop(ctx, string, start, end - start);
+ }
+ /// Remove all characters from the string except those within an inclusive `range`.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// text.crop(2..=8);
+ ///
+ /// print(text); // prints "llo, wo"
+ /// ```
+ #[rhai_fn(name = "crop")]
+ pub fn crop_inclusive_range(
+ ctx: NativeCallContext,
+ string: &mut ImmutableString,
+ range: InclusiveRange,
+ ) {
+ let start = INT::max(*range.start(), 0);
+ let end = INT::max(*range.end(), start);
+ crop(ctx, string, start, end - start + 1);
+ }
+
+ /// Remove all characters from the string except those within a range.
+ ///
+ /// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+ /// * If `start` < -length of string, position counts from the beginning of the string.
+ /// * If `start` ≥ length of string, the entire string is cleared.
+ /// * If `len` ≤ 0, the entire string is cleared.
+ /// * If `start` position + `len` ≥ length of string, only the portion of the string after the `start` position is retained.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// text.crop(2, 8);
+ ///
+ /// print(text); // prints "llo, wor"
+ ///
+ /// text.crop(-5, 3);
+ ///
+ /// print(text); // prints ", w"
+ /// ```
+ #[rhai_fn(name = "crop")]
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ pub fn crop(ctx: NativeCallContext, string: &mut ImmutableString, start: INT, len: INT) {
+ if string.is_empty() {
+ return;
+ }
+ if len <= 0 {
+ *string = ctx.engine().const_empty_string();
+ return;
+ }
+
+ let mut chars = StaticVec::with_capacity(string.len());
+
+ let offset = if string.is_empty() || len <= 0 {
+ string.make_mut().clear();
+ return;
+ } else if start < 0 {
+ let abs_start = start.unsigned_abs();
+
+ if abs_start as u64 > MAX_USIZE_INT as u64 {
+ return;
+ }
+
+ let abs_start = abs_start as usize;
+
+ chars.extend(string.chars());
+
+ if abs_start > chars.len() {
+ 0
+ } else {
+ chars.len() - abs_start
+ }
+ } else if start > MAX_USIZE_INT || start as usize >= string.chars().count() {
+ string.make_mut().clear();
+ return;
+ } else {
+ start as usize
+ };
+
+ if chars.is_empty() {
+ chars.extend(string.chars());
+ }
+
+ let len = len.min(MAX_USIZE_INT) as usize;
+
+ let len = if offset + len > chars.len() {
+ chars.len() - offset
+ } else {
+ len
+ };
+
+ let copy = string.make_mut();
+ copy.clear();
+ copy.extend(chars.iter().skip(offset).take(len));
+ }
+ /// Remove all characters from the string up to the `start` position.
+ ///
+ /// * If `start` < 0, position counts from the end of the string (`-1` is the last character).
+ /// * If `start` < -length of string, the string is not modified.
+ /// * If `start` ≥ length of string, the entire string is cleared.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// text.crop(5);
+ ///
+ /// print(text); // prints ", world!"
+ ///
+ /// text.crop(-3);
+ ///
+ /// print(text); // prints "ld!"
+ /// ```
+ #[rhai_fn(name = "crop")]
+ pub fn crop_string_starting_from(
+ ctx: NativeCallContext,
+ string: &mut ImmutableString,
+ start: INT,
+ ) {
+ crop(ctx, string, start, string.len() as INT);
+ }
+
+ /// Replace all occurrences of the specified sub-string in the string with another string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foobar!";
+ ///
+ /// text.replace("hello", "hey");
+ ///
+ /// print(text); // prints "hey, world! hey, foobar!"
+ /// ```
+ #[rhai_fn(name = "replace")]
+ pub fn replace(string: &mut ImmutableString, find_string: &str, substitute_string: &str) {
+ if !string.is_empty() {
+ *string = string.replace(find_string, substitute_string).into();
+ }
+ }
+ /// Replace all occurrences of the specified sub-string in the string with the specified character.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foobar!";
+ ///
+ /// text.replace("hello", '*');
+ ///
+ /// print(text); // prints "*, world! *, foobar!"
+ /// ```
+ #[rhai_fn(name = "replace")]
+ pub fn replace_string_with_char(
+ string: &mut ImmutableString,
+ find_string: &str,
+ substitute_character: char,
+ ) {
+ if !string.is_empty() {
+ *string = string
+ .replace(find_string, &substitute_character.to_string())
+ .into();
+ }
+ }
+ /// Replace all occurrences of the specified character in the string with another string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foobar!";
+ ///
+ /// text.replace('l', "(^)");
+ ///
+ /// print(text); // prints "he(^)(^)o, wor(^)d! he(^)(^)o, foobar!"
+ /// ```
+ #[rhai_fn(name = "replace")]
+ pub fn replace_char_with_string(
+ string: &mut ImmutableString,
+ find_character: char,
+ substitute_string: &str,
+ ) {
+ if !string.is_empty() {
+ *string = string
+ .replace(&find_character.to_string(), substitute_string)
+ .into();
+ }
+ }
+ /// Replace all occurrences of the specified character in the string with another character.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foobar!";
+ ///
+ /// text.replace("l", '*');
+ ///
+ /// print(text); // prints "he**o, wor*d! he**o, foobar!"
+ /// ```
+ #[rhai_fn(name = "replace")]
+ pub fn replace_char(
+ string: &mut ImmutableString,
+ find_character: char,
+ substitute_character: char,
+ ) {
+ if !string.is_empty() {
+ *string = string
+ .replace(
+ &find_character.to_string(),
+ &substitute_character.to_string(),
+ )
+ .into();
+ }
+ }
+
+ /// Pad the string to at least the specified number of characters with the specified `character`.
+ ///
+ /// If `len` ≤ length of string, no padding is done.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello";
+ ///
+ /// text.pad(8, '!');
+ ///
+ /// print(text); // prints "hello!!!"
+ ///
+ /// text.pad(5, '*');
+ ///
+ /// print(text); // prints "hello!!!"
+ /// ```
+ #[rhai_fn(return_raw)]
+ pub fn pad(
+ ctx: NativeCallContext,
+ string: &mut ImmutableString,
+ len: INT,
+ character: char,
+ ) -> RhaiResultOf<()> {
+ if len <= 0 {
+ return Ok(());
+ }
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let len = len.min(MAX_USIZE_INT) as usize;
+ let _ctx = ctx;
+
+ // Check if string will be over max size limit
+ if _ctx.engine().max_string_size() > 0 && len > _ctx.engine().max_string_size() {
+ return Err(
+ ERR::ErrorDataTooLarge("Length of string".to_string(), Position::NONE).into(),
+ );
+ }
+
+ let orig_len = string.chars().count();
+
+ if len > orig_len {
+ let p = string.make_mut();
+
+ for _ in 0..(len - orig_len) {
+ p.push(character);
+ }
+
+ if _ctx.engine().max_string_size() > 0 && string.len() > _ctx.engine().max_string_size()
+ {
+ return Err(
+ ERR::ErrorDataTooLarge("Length of string".to_string(), Position::NONE).into(),
+ );
+ }
+ }
+
+ Ok(())
+ }
+ /// Pad the string to at least the specified number of characters with the specified string.
+ ///
+ /// If `len` ≤ length of string, no padding is done.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello";
+ ///
+ /// text.pad(10, "(!)");
+ ///
+ /// print(text); // prints "hello(!)(!)"
+ ///
+ /// text.pad(8, '***');
+ ///
+ /// print(text); // prints "hello(!)(!)"
+ /// ```
+ #[rhai_fn(name = "pad", return_raw)]
+ pub fn pad_with_string(
+ ctx: NativeCallContext,
+ string: &mut ImmutableString,
+ len: INT,
+ padding: &str,
+ ) -> RhaiResultOf<()> {
+ if len <= 0 {
+ return Ok(());
+ }
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ let len = len.min(MAX_USIZE_INT) as usize;
+ let _ctx = ctx;
+
+ // Check if string will be over max size limit
+ if _ctx.engine().max_string_size() > 0 && len > _ctx.engine().max_string_size() {
+ return Err(
+ ERR::ErrorDataTooLarge("Length of string".to_string(), Position::NONE).into(),
+ );
+ }
+
+ let mut str_len = string.chars().count();
+ let padding_len = padding.chars().count();
+
+ if len > str_len {
+ let p = string.make_mut();
+
+ while str_len < len {
+ if str_len + padding_len <= len {
+ p.push_str(padding);
+ str_len += padding_len;
+ } else {
+ p.extend(padding.chars().take(len - str_len));
+ str_len = len;
+ }
+ }
+
+ if _ctx.engine().max_string_size() > 0 && string.len() > _ctx.engine().max_string_size()
+ {
+ return Err(
+ ERR::ErrorDataTooLarge("Length of string".to_string(), Position::NONE).into(),
+ );
+ }
+ }
+
+ Ok(())
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ pub mod arrays {
+ use crate::Array;
+
+ /// Split the string into two at the specified `index` position and return it both strings
+ /// as an array.
+ ///
+ /// The character at the `index` position (if any) is returned in the _second_ string.
+ ///
+ /// * If `index` < 0, position counts from the end of the string (`-1` is the last character).
+ /// * If `index` < -length of string, it is equivalent to cutting at position 0.
+ /// * If `index` ≥ length of string, it is equivalent to cutting at the end of the string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world!";
+ ///
+ /// print(text.split(6)); // prints ["hello,", " world!"]
+ ///
+ /// print(text.split(13)); // prints ["hello, world!", ""]
+ ///
+ /// print(text.split(-6)); // prints ["hello, ", "world!"]
+ ///
+ /// print(text.split(-99)); // prints ["", "hello, world!"]
+ /// ```
+ #[rhai_fn(name = "split")]
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ pub fn split_at(ctx: NativeCallContext, string: &mut ImmutableString, index: INT) -> Array {
+ if index <= 0 {
+ let abs_index = index.unsigned_abs();
+
+ if abs_index as u64 > MAX_USIZE_INT as u64 {
+ return vec![
+ ctx.engine().const_empty_string().into(),
+ string.as_str().into(),
+ ];
+ }
+ let abs_index = abs_index as usize;
+ let num_chars = string.chars().count();
+
+ if abs_index > num_chars {
+ vec![
+ ctx.engine().const_empty_string().into(),
+ string.as_str().into(),
+ ]
+ } else {
+ let prefix: String = string.chars().take(num_chars - abs_index).collect();
+ let prefix_len = prefix.len();
+ vec![prefix.into(), string[prefix_len..].into()]
+ }
+ } else if index > MAX_USIZE_INT {
+ vec![
+ string.as_str().into(),
+ ctx.engine().const_empty_string().into(),
+ ]
+ } else {
+ let prefix: String = string.chars().take(index as usize).collect();
+ let prefix_len = prefix.len();
+ vec![prefix.into(), string[prefix_len..].into()]
+ }
+ }
+ /// Return an array containing all the characters of the string.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello";
+ ///
+ /// print(text.to_chars()); // prints "['h', 'e', 'l', 'l', 'o']"
+ /// ```
+ #[rhai_fn(name = "to_chars")]
+ pub fn to_chars(string: &str) -> Array {
+ if string.is_empty() {
+ Array::new()
+ } else {
+ string.chars().map(Into::into).collect()
+ }
+ }
+ /// Split the string into segments based on whitespaces, returning an array of the segments.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foo!";
+ ///
+ /// print(text.split()); // prints ["hello,", "world!", "hello,", "foo!"]
+ /// ```
+ #[rhai_fn(name = "split")]
+ pub fn split_whitespace(string: &str) -> Array {
+ if string.is_empty() {
+ Array::new()
+ } else {
+ string.split_whitespace().map(Into::into).collect()
+ }
+ }
+ /// Split the string into segments based on a `delimiter` string, returning an array of the segments.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foo!";
+ ///
+ /// print(text.split("ll")); // prints ["he", "o, world! he", "o, foo!"]
+ /// ```
+ pub fn split(string: &str, delimiter: &str) -> Array {
+ string.split(delimiter).map(Into::into).collect()
+ }
+ /// Split the string into at most the specified number of `segments` based on a `delimiter` string,
+ /// returning an array of the segments.
+ ///
+ /// If `segments` < 1, only one segment is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foo!";
+ ///
+ /// print(text.split("ll", 2)); // prints ["he", "o, world! hello, foo!"]
+ /// ```
+ #[rhai_fn(name = "split")]
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ pub fn splitn(string: &str, delimiter: &str, segments: INT) -> Array {
+ if segments < 1 {
+ return [string.into()].into();
+ }
+ let segments = segments.min(MAX_USIZE_INT) as usize;
+ let pieces: usize = if segments < 1 { 1 } else { segments };
+ string.splitn(pieces, delimiter).map(Into::into).collect()
+ }
+ /// Split the string into segments based on a `delimiter` character, returning an array of the segments.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foo!";
+ ///
+ /// print(text.split('l')); // prints ["he", "", "o, wor", "d! he", "", "o, foo!"]
+ /// ```
+ #[rhai_fn(name = "split")]
+ pub fn split_char(string: &str, delimiter: char) -> Array {
+ string.split(delimiter).map(Into::into).collect()
+ }
+ /// Split the string into at most the specified number of `segments` based on a `delimiter` character,
+ /// returning an array of the segments.
+ ///
+ /// If `segments` < 1, only one segment is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foo!";
+ ///
+ /// print(text.split('l', 3)); // prints ["he", "", "o, world! hello, foo!"]
+ /// ```
+ #[rhai_fn(name = "split")]
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ pub fn splitn_char(string: &str, delimiter: char, segments: INT) -> Array {
+ if segments < 1 {
+ return [string.into()].into();
+ }
+ let segments = segments.min(MAX_USIZE_INT) as usize;
+ let pieces: usize = if segments < 1 { 1 } else { segments };
+ string.splitn(pieces, delimiter).map(Into::into).collect()
+ }
+ /// Split the string into segments based on a `delimiter` string, returning an array of the
+ /// segments in _reverse_ order.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foo!";
+ ///
+ /// print(text.split_rev("ll")); // prints ["o, foo!", "o, world! he", "he"]
+ /// ```
+ #[rhai_fn(name = "split_rev")]
+ pub fn rsplit(string: &str, delimiter: &str) -> Array {
+ string.rsplit(delimiter).map(Into::into).collect()
+ }
+ /// Split the string into at most a specified number of `segments` based on a `delimiter` string,
+ /// returning an array of the segments in _reverse_ order.
+ ///
+ /// If `segments` < 1, only one segment is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foo!";
+ ///
+ /// print(text.split_rev("ll", 2)); // prints ["o, foo!", "hello, world! he"]
+ /// ```
+ #[rhai_fn(name = "split_rev")]
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ pub fn rsplitn(string: &str, delimiter: &str, segments: INT) -> Array {
+ if segments < 1 {
+ return [string.into()].into();
+ }
+ let segments = segments.min(MAX_USIZE_INT) as usize;
+ let pieces: usize = if segments < 1 { 1 } else { segments };
+ string.rsplitn(pieces, delimiter).map(Into::into).collect()
+ }
+ /// Split the string into segments based on a `delimiter` character, returning an array of
+ /// the segments in _reverse_ order.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foo!";
+ ///
+ /// print(text.split_rev('l')); // prints ["o, foo!", "", "d! he", "o, wor", "", "he"]
+ /// ```
+ #[rhai_fn(name = "split_rev")]
+ pub fn rsplit_char(string: &str, delimiter: char) -> Array {
+ string.rsplit(delimiter).map(Into::into).collect()
+ }
+ /// Split the string into at most the specified number of `segments` based on a `delimiter` character,
+ /// returning an array of the segments.
+ ///
+ /// If `segments` < 1, only one segment is returned.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let text = "hello, world! hello, foo!";
+ ///
+ /// print(text.split('l', 3)); // prints ["o, foo!", "", "hello, world! he"
+ /// ```
+ #[rhai_fn(name = "split_rev")]
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ pub fn rsplitn_char(string: &str, delimiter: char, segments: INT) -> Array {
+ if segments < 1 {
+ return [string.into()].into();
+ }
+ let segments = segments.min(MAX_USIZE_INT) as usize;
+ let pieces: usize = if segments < 1 { 1 } else { segments };
+ string.rsplitn(pieces, delimiter).map(Into::into).collect()
+ }
+ }
+}
diff --git a/rhai/src/packages/time_basic.rs b/rhai/src/packages/time_basic.rs
new file mode 100644
index 0000000..cf64bba
--- /dev/null
+++ b/rhai/src/packages/time_basic.rs
@@ -0,0 +1,276 @@
+#![cfg(not(feature = "no_time"))]
+
+use super::arithmetic::make_err as make_arithmetic_err;
+use crate::module::ModuleFlags;
+use crate::plugin::*;
+use crate::{def_package, Dynamic, RhaiResult, RhaiResultOf, INT};
+
+#[cfg(not(feature = "no_float"))]
+use crate::FLOAT;
+
+#[cfg(not(target_family = "wasm"))]
+use std::time::{Duration, Instant};
+
+#[cfg(target_family = "wasm")]
+use instant::{Duration, Instant};
+
+def_package! {
+ /// Package of basic timing utilities.
+ pub BasicTimePackage(lib) {
+ lib.flags |= ModuleFlags::STANDARD_LIB;
+
+ // Register date/time functions
+ combine_with_exported_module!(lib, "time", time_functions);
+ }
+}
+
+#[export_module]
+mod time_functions {
+ /// Create a timestamp containing the current system time.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let now = timestamp();
+ ///
+ /// sleep(10.0); // sleep for 10 seconds
+ ///
+ /// print(now.elapsed); // prints 10.???
+ /// ```
+ pub fn timestamp() -> Instant {
+ Instant::now()
+ }
+
+ /// Return the number of seconds between the current system time and the timestamp.
+ ///
+ /// # Example
+ ///
+ /// ```rhai
+ /// let now = timestamp();
+ ///
+ /// sleep(10.0); // sleep for 10 seconds
+ ///
+ /// print(now.elapsed); // prints 10.???
+ /// ```
+ #[rhai_fn(name = "elapsed", get = "elapsed", return_raw)]
+ pub fn elapsed(timestamp: Instant) -> RhaiResult {
+ #[cfg(not(feature = "no_float"))]
+ if timestamp > Instant::now() {
+ Err(make_arithmetic_err("Time-stamp is later than now"))
+ } else {
+ Ok((timestamp.elapsed().as_secs_f64() as FLOAT).into())
+ }
+
+ #[cfg(feature = "no_float")]
+ {
+ let seconds = timestamp.elapsed().as_secs();
+
+ if cfg!(not(feature = "unchecked")) && seconds > (INT::MAX as u64) {
+ Err(make_arithmetic_err(format!(
+ "Integer overflow for timestamp.elapsed: {seconds}"
+ )))
+ } else if timestamp > Instant::now() {
+ Err(make_arithmetic_err("Time-stamp is later than now"))
+ } else {
+ Ok((seconds as INT).into())
+ }
+ }
+ }
+
+ /// Return the number of seconds between two timestamps.
+ #[rhai_fn(return_raw, name = "-")]
+ pub fn time_diff(timestamp1: Instant, timestamp2: Instant) -> RhaiResult {
+ #[cfg(not(feature = "no_float"))]
+ return Ok(if timestamp2 > timestamp1 {
+ -(timestamp2 - timestamp1).as_secs_f64() as FLOAT
+ } else {
+ (timestamp1 - timestamp2).as_secs_f64() as FLOAT
+ }
+ .into());
+
+ #[cfg(feature = "no_float")]
+ if timestamp2 > timestamp1 {
+ let seconds = (timestamp2 - timestamp1).as_secs();
+
+ if cfg!(not(feature = "unchecked")) && seconds > (INT::MAX as u64) {
+ Err(make_arithmetic_err(format!(
+ "Integer overflow for timestamp duration: -{seconds}"
+ )))
+ } else {
+ Ok((-(seconds as INT)).into())
+ }
+ } else {
+ let seconds = (timestamp1 - timestamp2).as_secs();
+
+ if cfg!(not(feature = "unchecked")) && seconds > (INT::MAX as u64) {
+ Err(make_arithmetic_err(format!(
+ "Integer overflow for timestamp duration: {seconds}"
+ )))
+ } else {
+ Ok((seconds as INT).into())
+ }
+ }
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ pub mod float_functions {
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ fn add_impl(timestamp: Instant, seconds: FLOAT) -> RhaiResultOf<Instant> {
+ if seconds < 0.0 {
+ subtract_impl(timestamp, -seconds)
+ } else if cfg!(not(feature = "unchecked")) {
+ if seconds > (INT::MAX as FLOAT).min(u64::MAX as FLOAT) {
+ Err(make_arithmetic_err(format!(
+ "Integer overflow for timestamp add: {seconds}"
+ )))
+ } else {
+ timestamp
+ .checked_add(Duration::from_millis((seconds * 1000.0) as u64))
+ .ok_or_else(|| {
+ make_arithmetic_err(format!(
+ "Timestamp overflow when adding {seconds} second(s)"
+ ))
+ })
+ }
+ } else {
+ Ok(timestamp + Duration::from_millis((seconds * 1000.0) as u64))
+ }
+ }
+ #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+ fn subtract_impl(timestamp: Instant, seconds: FLOAT) -> RhaiResultOf<Instant> {
+ if seconds < 0.0 {
+ add_impl(timestamp, -seconds)
+ } else if cfg!(not(feature = "unchecked")) {
+ if seconds > (INT::MAX as FLOAT).min(u64::MAX as FLOAT) {
+ Err(make_arithmetic_err(format!(
+ "Integer overflow for timestamp subtract: {seconds}"
+ )))
+ } else {
+ timestamp
+ .checked_sub(Duration::from_millis((seconds * 1000.0) as u64))
+ .ok_or_else(|| {
+ make_arithmetic_err(format!(
+ "Timestamp overflow when subtracting {seconds} second(s)"
+ ))
+ })
+ }
+ } else {
+ Ok(timestamp
+ .checked_sub(Duration::from_millis((seconds * 1000.0) as u64))
+ .unwrap())
+ }
+ }
+
+ /// Add the specified number of `seconds` to the timestamp and return it as a new timestamp.
+ #[rhai_fn(return_raw, name = "+")]
+ pub fn add(timestamp: Instant, seconds: FLOAT) -> RhaiResultOf<Instant> {
+ add_impl(timestamp, seconds)
+ }
+ /// Add the specified number of `seconds` to the timestamp.
+ #[rhai_fn(return_raw, name = "+=")]
+ pub fn add_assign(timestamp: &mut Instant, seconds: FLOAT) -> RhaiResultOf<()> {
+ *timestamp = add_impl(*timestamp, seconds)?;
+ Ok(())
+ }
+ /// Subtract the specified number of `seconds` from the timestamp and return it as a new timestamp.
+ #[rhai_fn(return_raw, name = "-")]
+ pub fn subtract(timestamp: Instant, seconds: FLOAT) -> RhaiResultOf<Instant> {
+ subtract_impl(timestamp, seconds)
+ }
+ /// Subtract the specified number of `seconds` from the timestamp.
+ #[rhai_fn(return_raw, name = "-=")]
+ pub fn subtract_assign(timestamp: &mut Instant, seconds: FLOAT) -> RhaiResultOf<()> {
+ *timestamp = subtract_impl(*timestamp, seconds)?;
+ Ok(())
+ }
+ }
+
+ fn add_impl(timestamp: Instant, seconds: INT) -> RhaiResultOf<Instant> {
+ #[allow(clippy::cast_sign_loss)]
+ if seconds < 0 {
+ subtract_impl(timestamp, -seconds)
+ } else if cfg!(not(feature = "unchecked")) {
+ timestamp
+ .checked_add(Duration::from_secs(seconds as u64))
+ .ok_or_else(|| {
+ make_arithmetic_err(format!(
+ "Timestamp overflow when adding {seconds} second(s)"
+ ))
+ })
+ } else {
+ Ok(timestamp + Duration::from_secs(seconds as u64))
+ }
+ }
+ fn subtract_impl(timestamp: Instant, seconds: INT) -> RhaiResultOf<Instant> {
+ #[allow(clippy::cast_sign_loss)]
+ if seconds < 0 {
+ add_impl(timestamp, -seconds)
+ } else if cfg!(not(feature = "unchecked")) {
+ timestamp
+ .checked_sub(Duration::from_secs(seconds as u64))
+ .ok_or_else(|| {
+ make_arithmetic_err(format!(
+ "Timestamp overflow when subtracting {seconds} second(s)"
+ ))
+ })
+ } else {
+ Ok(timestamp
+ .checked_sub(Duration::from_secs(seconds as u64))
+ .unwrap())
+ }
+ }
+
+ /// Add the specified number of `seconds` to the timestamp and return it as a new timestamp.
+ #[rhai_fn(return_raw, name = "+")]
+ pub fn add(timestamp: Instant, seconds: INT) -> RhaiResultOf<Instant> {
+ add_impl(timestamp, seconds)
+ }
+ /// Add the specified number of `seconds` to the timestamp.
+ #[rhai_fn(return_raw, name = "+=")]
+ pub fn add_assign(timestamp: &mut Instant, seconds: INT) -> RhaiResultOf<()> {
+ *timestamp = add_impl(*timestamp, seconds)?;
+ Ok(())
+ }
+ /// Subtract the specified number of `seconds` from the timestamp and return it as a new timestamp.
+ #[rhai_fn(return_raw, name = "-")]
+ pub fn subtract(timestamp: Instant, seconds: INT) -> RhaiResultOf<Instant> {
+ subtract_impl(timestamp, seconds)
+ }
+ /// Subtract the specified number of `seconds` from the timestamp.
+ #[rhai_fn(return_raw, name = "-=")]
+ pub fn subtract_assign(timestamp: &mut Instant, seconds: INT) -> RhaiResultOf<()> {
+ *timestamp = subtract_impl(*timestamp, seconds)?;
+ Ok(())
+ }
+
+ /// Return `true` if two timestamps are equal.
+ #[rhai_fn(name = "==")]
+ pub fn eq(timestamp1: Instant, timestamp2: Instant) -> bool {
+ timestamp1 == timestamp2
+ }
+ /// Return `true` if two timestamps are not equal.
+ #[rhai_fn(name = "!=")]
+ pub fn ne(timestamp1: Instant, timestamp2: Instant) -> bool {
+ timestamp1 != timestamp2
+ }
+ /// Return `true` if the first timestamp is earlier than the second.
+ #[rhai_fn(name = "<")]
+ pub fn lt(timestamp1: Instant, timestamp2: Instant) -> bool {
+ timestamp1 < timestamp2
+ }
+ /// Return `true` if the first timestamp is earlier than or equals to the second.
+ #[rhai_fn(name = "<=")]
+ pub fn lte(timestamp1: Instant, timestamp2: Instant) -> bool {
+ timestamp1 <= timestamp2
+ }
+ /// Return `true` if the first timestamp is later than the second.
+ #[rhai_fn(name = ">")]
+ pub fn gt(timestamp1: Instant, timestamp2: Instant) -> bool {
+ timestamp1 > timestamp2
+ }
+ /// Return `true` if the first timestamp is later than or equals to the second.
+ #[rhai_fn(name = ">=")]
+ pub fn gte(timestamp1: Instant, timestamp2: Instant) -> bool {
+ timestamp1 >= timestamp2
+ }
+}
diff --git a/rhai/src/parser.rs b/rhai/src/parser.rs
new file mode 100644
index 0000000..4009ad9
--- /dev/null
+++ b/rhai/src/parser.rs
@@ -0,0 +1,4154 @@
+//! Main module defining the lexer and parser.
+
+use crate::api::events::VarDefInfo;
+use crate::api::options::LangOptions;
+use crate::ast::{
+ ASTFlags, BinaryExpr, CaseBlocksList, ConditionalExpr, Expr, FlowControl, FnCallExpr,
+ FnCallHashes, Ident, Namespace, OpAssignment, RangeCase, ScriptFnDef, Stmt, StmtBlock,
+ StmtBlockContainer, SwitchCasesCollection,
+};
+use crate::engine::{Precedence, OP_CONTAINS, OP_NOT};
+use crate::eval::{Caches, GlobalRuntimeState};
+use crate::func::{hashing::get_hasher, StraightHashMap};
+use crate::tokenizer::{
+ is_reserved_keyword_or_symbol, is_valid_function_name, is_valid_identifier, Token, TokenStream,
+ TokenizerControl,
+};
+use crate::types::dynamic::{AccessMode, Union};
+use crate::types::StringsInterner;
+use crate::{
+ calc_fn_hash, Dynamic, Engine, EvalAltResult, EvalContext, ExclusiveRange, FnArgsVec,
+ Identifier, ImmutableString, InclusiveRange, LexError, OptimizationLevel, ParseError, Position,
+ Scope, Shared, SmartString, StaticVec, AST, PERR,
+};
+use bitflags::bitflags;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ convert::TryFrom,
+ fmt,
+ hash::{Hash, Hasher},
+ num::{NonZeroU8, NonZeroUsize},
+};
+
+pub type ParseResult<T> = Result<T, ParseError>;
+
+type FnLib = StraightHashMap<Shared<ScriptFnDef>>;
+
+/// Invalid variable name that acts as a search barrier in a [`Scope`].
+const SCOPE_SEARCH_BARRIER_MARKER: &str = "$ BARRIER $";
+
+/// The message: `TokenStream` never ends
+const NEVER_ENDS: &str = "`Token`";
+
+/// _(internals)_ A type that encapsulates the current state of the parser.
+/// Exported under the `internals` feature only.
+pub struct ParseState<'e, 's> {
+ /// Input stream buffer containing the next character to read.
+ pub tokenizer_control: TokenizerControl,
+ /// Controls whether parsing of an expression should stop given the next token.
+ pub expr_filter: fn(&Token) -> bool,
+ /// Strings interner.
+ pub interned_strings: &'s mut StringsInterner,
+ /// External [scope][Scope] with constants.
+ pub external_constants: Option<&'e Scope<'e>>,
+ /// Global runtime state.
+ pub global: Option<Box<GlobalRuntimeState>>,
+ /// Encapsulates a local stack with variable names to simulate an actual runtime scope.
+ pub stack: Option<Scope<'e>>,
+ /// Size of the local variables stack upon entry of the current block scope.
+ pub block_stack_len: usize,
+ /// Tracks a list of external variables (variables that are not explicitly declared in the scope).
+ #[cfg(not(feature = "no_closure"))]
+ pub external_vars: Option<Box<crate::FnArgsVec<Ident>>>,
+ /// An indicator that, when set to `false`, disables variable capturing into externals one
+ /// single time up until the nearest consumed Identifier token.
+ ///
+ /// If set to `false` the next call to [`access_var`][ParseState::access_var] will not capture
+ /// the variable.
+ ///
+ /// All consequent calls to [`access_var`][ParseState::access_var] will not be affected.
+ pub allow_capture: bool,
+ /// Encapsulates a local stack with imported [module][crate::Module] names.
+ #[cfg(not(feature = "no_module"))]
+ pub imports: Option<Box<StaticVec<ImmutableString>>>,
+ /// List of globally-imported [module][crate::Module] names.
+ #[cfg(not(feature = "no_module"))]
+ pub global_imports: Option<Box<StaticVec<ImmutableString>>>,
+}
+
+impl fmt::Debug for ParseState<'_, '_> {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut f = f.debug_struct("ParseState");
+
+ f.field("tokenizer_control", &self.tokenizer_control)
+ .field("interned_strings", &self.interned_strings)
+ .field("external_constants_scope", &self.external_constants)
+ .field("global", &self.global)
+ .field("stack", &self.stack)
+ .field("block_stack_len", &self.block_stack_len);
+
+ #[cfg(not(feature = "no_closure"))]
+ f.field("external_vars", &self.external_vars)
+ .field("allow_capture", &self.allow_capture);
+
+ #[cfg(not(feature = "no_module"))]
+ f.field("imports", &self.imports)
+ .field("global_imports", &self.global_imports);
+
+ f.finish()
+ }
+}
+
+impl<'e, 's> ParseState<'e, 's> {
+ /// Create a new [`ParseState`].
+ #[inline]
+ #[must_use]
+ pub fn new(
+ external_constants: Option<&'e Scope>,
+ interned_strings: &'s mut StringsInterner,
+ tokenizer_control: TokenizerControl,
+ ) -> Self {
+ Self {
+ tokenizer_control,
+ expr_filter: |_| true,
+ #[cfg(not(feature = "no_closure"))]
+ external_vars: None,
+ allow_capture: true,
+ interned_strings,
+ external_constants,
+ global: None,
+ stack: None,
+ block_stack_len: 0,
+ #[cfg(not(feature = "no_module"))]
+ imports: None,
+ #[cfg(not(feature = "no_module"))]
+ global_imports: None,
+ }
+ }
+
+ /// Find explicitly declared variable by name in the [`ParseState`], searching in reverse order.
+ ///
+ /// The first return value is the offset to be deducted from `ParseState::stack::len()`,
+ /// i.e. the top element of [`ParseState`]'s variables stack is offset 1.
+ ///
+ /// If the variable is not present in the scope, the first return value is zero.
+ ///
+ /// The second return value indicates whether the barrier has been hit before finding the variable.
+ #[must_use]
+ pub fn find_var(&self, name: &str) -> (usize, bool) {
+ let mut hit_barrier = false;
+
+ let index = self
+ .stack
+ .as_ref()
+ .into_iter()
+ .flat_map(Scope::iter_rev_raw)
+ .enumerate()
+ .find(|&(.., (n, ..))| {
+ if n == SCOPE_SEARCH_BARRIER_MARKER {
+ // Do not go beyond the barrier
+ hit_barrier = true;
+ false
+ } else {
+ n == name
+ }
+ })
+ .map_or(0, |(i, ..)| i + 1);
+
+ (index, hit_barrier)
+ }
+
+ /// Find explicitly declared variable by name in the [`ParseState`], searching in reverse order.
+ ///
+ /// If the variable is not present in the scope adds it to the list of external variables.
+ ///
+ /// The return value is the offset to be deducted from `ParseState::stack::len()`,
+ /// i.e. the top element of [`ParseState`]'s variables stack is offset 1.
+ ///
+ /// # Return value: `(index, is_func_name)`
+ ///
+ /// * `index`: [`None`] when the variable name is not found in the `stack`,
+ /// otherwise the index value.
+ ///
+ /// * `is_func_name`: `true` if the variable is actually the name of a function
+ /// (in which case it will be converted into a function pointer).
+ #[must_use]
+ pub fn access_var(
+ &mut self,
+ name: &str,
+ lib: &FnLib,
+ pos: Position,
+ ) -> (Option<NonZeroUsize>, bool) {
+ let _lib = lib;
+ let _pos = pos;
+
+ let (index, hit_barrier) = self.find_var(name);
+
+ #[cfg(not(feature = "no_function"))]
+ let is_func_name = _lib.values().any(|f| f.name == name);
+ #[cfg(feature = "no_function")]
+ let is_func_name = false;
+
+ #[cfg(not(feature = "no_closure"))]
+ if self.allow_capture {
+ if !is_func_name
+ && index == 0
+ && !self
+ .external_vars
+ .as_deref()
+ .into_iter()
+ .flatten()
+ .any(|v| v.name == name)
+ {
+ self.external_vars
+ .get_or_insert_with(Default::default)
+ .push(Ident {
+ name: name.into(),
+ pos: _pos,
+ });
+ }
+ } else {
+ self.allow_capture = true;
+ }
+
+ let index = (!hit_barrier).then(|| NonZeroUsize::new(index)).flatten();
+
+ (index, is_func_name)
+ }
+
+ /// Find a module by name in the [`ParseState`], searching in reverse.
+ ///
+ /// Returns the offset to be deducted from `Stack::len`,
+ /// i.e. the top element of the [`ParseState`] is offset 1.
+ ///
+ /// Returns [`None`] when the variable name is not found in the [`ParseState`].
+ ///
+ /// # Panics
+ ///
+ /// Panics when called under `no_module`.
+ #[cfg(not(feature = "no_module"))]
+ #[must_use]
+ pub fn find_module(&self, name: &str) -> Option<NonZeroUsize> {
+ self.imports
+ .as_deref()
+ .into_iter()
+ .flatten()
+ .rev()
+ .enumerate()
+ .find(|(.., n)| n.as_str() == name)
+ .and_then(|(i, ..)| NonZeroUsize::new(i + 1))
+ }
+
+ /// Get an interned string, creating one if it is not yet interned.
+ #[inline(always)]
+ #[must_use]
+ pub fn get_interned_string(
+ &mut self,
+ text: impl AsRef<str> + Into<ImmutableString>,
+ ) -> ImmutableString {
+ self.interned_strings.get(text)
+ }
+
+ /// Get an interned property getter, creating one if it is not yet interned.
+ #[cfg(not(feature = "no_object"))]
+ #[inline]
+ #[must_use]
+ pub fn get_interned_getter(
+ &mut self,
+ text: impl AsRef<str> + Into<ImmutableString>,
+ ) -> ImmutableString {
+ self.interned_strings.get_with_mapper(
+ b'g',
+ |s| crate::engine::make_getter(s.as_ref()).into(),
+ text,
+ )
+ }
+
+ /// Get an interned property setter, creating one if it is not yet interned.
+ #[cfg(not(feature = "no_object"))]
+ #[inline]
+ #[must_use]
+ pub fn get_interned_setter(
+ &mut self,
+ text: impl AsRef<str> + Into<ImmutableString>,
+ ) -> ImmutableString {
+ self.interned_strings.get_with_mapper(
+ b's',
+ |s| crate::engine::make_setter(s.as_ref()).into(),
+ text,
+ )
+ }
+}
+
+bitflags! {
+ /// Bit-flags containing all status for [`ParseSettings`].
+ pub struct ParseSettingFlags: u8 {
+ /// Is the construct being parsed located at global level?
+ const GLOBAL_LEVEL = 0b0000_0001;
+ /// Is the construct being parsed located inside a function definition?
+ const FN_SCOPE = 0b0000_0010;
+ /// Is the construct being parsed located inside a closure definition?
+ const CLOSURE_SCOPE = 0b0000_0100;
+ /// Is the construct being parsed located inside a breakable loop?
+ const BREAKABLE = 0b0000_1000;
+
+ /// Disallow statements in blocks?
+ const DISALLOW_STATEMENTS_IN_BLOCKS = 0b0001_0000;
+ /// Disallow unquoted map properties?
+ const DISALLOW_UNQUOTED_MAP_PROPERTIES = 0b0010_0000;
+ }
+}
+
+bitflags! {
+ /// Bit-flags containing all status for parsing property/indexing/namespace chains.
+ struct ChainingFlags: u8 {
+ /// Is the construct being parsed a property?
+ const PROPERTY = 0b0000_0001;
+ /// Disallow namespaces?
+ const DISALLOW_NAMESPACES = 0b0000_0010;
+ }
+}
+
+/// A type that encapsulates all the settings for a particular parsing function.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct ParseSettings {
+ /// Flags.
+ pub flags: ParseSettingFlags,
+ /// Language options in effect (overrides Engine options).
+ pub options: LangOptions,
+ /// Current expression nesting level.
+ pub level: usize,
+ /// Current position.
+ pub pos: Position,
+ /// Maximum levels of expression nesting (0 for unlimited).
+ #[cfg(not(feature = "unchecked"))]
+ pub max_expr_depth: usize,
+}
+
+impl ParseSettings {
+ /// Is a particular flag on?
+ #[inline(always)]
+ #[must_use]
+ pub const fn has_flag(&self, flag: ParseSettingFlags) -> bool {
+ self.flags.contains(flag)
+ }
+ /// Is a particular language option on?
+ #[inline(always)]
+ #[must_use]
+ pub const fn has_option(&self, option: LangOptions) -> bool {
+ self.options.contains(option)
+ }
+ /// Create a new `ParseSettings` with one higher expression level.
+ #[inline]
+ pub fn level_up(&self) -> ParseResult<Self> {
+ #[cfg(not(feature = "unchecked"))]
+ if self.max_expr_depth > 0 && self.level >= self.max_expr_depth {
+ return Err(PERR::ExprTooDeep.into_err(self.pos));
+ }
+
+ Ok(Self {
+ level: self.level + 1,
+ ..*self
+ })
+ }
+}
+
+/// Make an anonymous function.
+#[cfg(not(feature = "no_function"))]
+#[inline]
+#[must_use]
+pub fn make_anonymous_fn(hash: u64) -> Identifier {
+ use std::fmt::Write;
+
+ let mut buf = Identifier::new_const();
+ write!(&mut buf, "{}{hash:016x}", crate::engine::FN_ANONYMOUS).unwrap();
+ buf
+}
+
+/// Is this function an anonymous function?
+#[cfg(not(feature = "no_function"))]
+#[inline(always)]
+#[must_use]
+pub fn is_anonymous_fn(fn_name: &str) -> bool {
+ fn_name.starts_with(crate::engine::FN_ANONYMOUS)
+}
+
+impl Expr {
+ /// Convert a [`Variable`][Expr::Variable] into a [`Property`][Expr::Property].
+ /// All other variants are untouched.
+ #[cfg(not(feature = "no_object"))]
+ #[inline]
+ #[must_use]
+ fn into_property(self, state: &mut ParseState) -> Self {
+ match self {
+ #[cfg(not(feature = "no_module"))]
+ Self::Variable(x, ..) if !x.1.is_empty() => unreachable!("qualified property"),
+ Self::Variable(x, .., pos) => {
+ let ident = x.3.clone();
+ let getter = state.get_interned_getter(ident.as_str());
+ let hash_get = calc_fn_hash(None, &getter, 1);
+ let setter = state.get_interned_setter(ident.as_str());
+ let hash_set = calc_fn_hash(None, &setter, 2);
+
+ Self::Property(
+ Box::new(((getter, hash_get), (setter, hash_set), ident)),
+ pos,
+ )
+ }
+ _ => self,
+ }
+ }
+ /// Raise an error if the expression can never yield a boolean value.
+ fn ensure_bool_expr(self) -> ParseResult<Expr> {
+ let type_name = match self {
+ Expr::Unit(..) => "()",
+ Expr::DynamicConstant(ref v, ..) if !v.is_bool() => v.type_name(),
+ Expr::IntegerConstant(..) => "a number",
+ #[cfg(not(feature = "no_float"))]
+ Expr::FloatConstant(..) => "a floating-point number",
+ Expr::CharConstant(..) => "a character",
+ Expr::StringConstant(..) => "a string",
+ Expr::InterpolatedString(..) => "a string",
+ Expr::Array(..) => "an array",
+ Expr::Map(..) => "an object map",
+ _ => return Ok(self),
+ };
+
+ Err(
+ PERR::MismatchedType("a boolean expression".into(), type_name.into())
+ .into_err(self.start_position()),
+ )
+ }
+ /// Raise an error if the expression can never yield an iterable value.
+ fn ensure_iterable(self) -> ParseResult<Expr> {
+ let type_name = match self {
+ Expr::Unit(..) => "()",
+ Expr::BoolConstant(..) => "a boolean",
+ Expr::IntegerConstant(..) => "a number",
+ #[cfg(not(feature = "no_float"))]
+ Expr::FloatConstant(..) => "a floating-point number",
+ Expr::CharConstant(..) => "a character",
+ Expr::Map(..) => "an object map",
+ _ => return Ok(self),
+ };
+
+ Err(
+ PERR::MismatchedType("an iterable value".into(), type_name.into())
+ .into_err(self.start_position()),
+ )
+ }
+}
+
+/// Make sure that the next expression is not a statement expression (i.e. wrapped in `{}`).
+fn ensure_not_statement_expr(
+ input: &mut TokenStream,
+ type_name: &(impl ToString + ?Sized),
+) -> ParseResult<()> {
+ match input.peek().expect(NEVER_ENDS) {
+ (Token::LeftBrace, pos) => Err(PERR::ExprExpected(type_name.to_string()).into_err(*pos)),
+ _ => Ok(()),
+ }
+}
+
+/// Make sure that the next expression is not a mis-typed assignment (i.e. `a = b` instead of `a == b`).
+fn ensure_not_assignment(input: &mut TokenStream) -> ParseResult<()> {
+ match input.peek().expect(NEVER_ENDS) {
+ (token @ Token::Equals, pos) => Err(LexError::ImproperSymbol(
+ token.literal_syntax().into(),
+ "Possibly a typo of '=='?".into(),
+ )
+ .into_err(*pos)),
+ _ => Ok(()),
+ }
+}
+
+/// Consume a particular [token][Token], checking that it is the expected one.
+///
+/// # Panics
+///
+/// Panics if the next token is not the expected one, or either tokens is not a literal symbol.
+fn eat_token(input: &mut TokenStream, expected_token: Token) -> Position {
+ let (t, pos) = input.next().expect(NEVER_ENDS);
+
+ if t != expected_token {
+ unreachable!(
+ "{} expected but gets {} at {}",
+ expected_token.literal_syntax(),
+ t.literal_syntax(),
+ pos
+ );
+ }
+ pos
+}
+
+/// Match a particular [token][Token], consuming it if matched.
+fn match_token(input: &mut TokenStream, token: Token) -> (bool, Position) {
+ let (t, pos) = input.peek().expect(NEVER_ENDS);
+ if *t == token {
+ (true, eat_token(input, token))
+ } else {
+ (false, *pos)
+ }
+}
+
+/// Process a block comment such that it indents properly relative to the start token.
+#[cfg(not(feature = "no_function"))]
+#[cfg(feature = "metadata")]
+#[inline]
+fn unindent_block_comment(comment: String, pos: usize) -> String {
+ if pos == 0 || !comment.contains('\n') {
+ return comment;
+ }
+
+ let offset = comment
+ .split('\n')
+ .skip(1)
+ .map(|s| s.len() - s.trim_start().len())
+ .min()
+ .unwrap_or(pos)
+ .min(pos);
+
+ if offset == 0 {
+ return comment;
+ }
+
+ comment
+ .split('\n')
+ .enumerate()
+ .map(|(i, s)| if i > 0 { &s[offset..] } else { s })
+ .collect::<Vec<_>>()
+ .join("\n")
+}
+
+/// Parse a variable name.
+fn parse_var_name(input: &mut TokenStream) -> ParseResult<(SmartString, Position)> {
+ match input.next().expect(NEVER_ENDS) {
+ // Variable name
+ (Token::Identifier(s), pos) => Ok((*s, pos)),
+ // Reserved keyword
+ (Token::Reserved(s), pos) if is_valid_identifier(s.as_str()) => {
+ Err(PERR::Reserved(s.to_string()).into_err(pos))
+ }
+ // Bad identifier
+ (Token::LexError(err), pos) => Err(err.into_err(pos)),
+ // Not a variable name
+ (.., pos) => Err(PERR::VariableExpected.into_err(pos)),
+ }
+}
+
+/// Optimize the structure of a chained expression where the root expression is another chained expression.
+///
+/// # Panics
+///
+/// Panics if the expression is not a combo chain.
+#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+fn optimize_combo_chain(expr: &mut Expr) {
+ let (mut x, x_options, x_pos, mut root, mut root_options, root_pos, make_sub, make_root): (
+ _,
+ _,
+ _,
+ _,
+ _,
+ _,
+ fn(_, _, _) -> Expr,
+ fn(_, _, _) -> Expr,
+ ) = match expr.take() {
+ #[cfg(not(feature = "no_index"))]
+ Expr::Index(mut x, opt, pos) => match x.lhs.take() {
+ Expr::Index(x2, opt2, pos2) => (x, opt, pos, x2, opt2, pos2, Expr::Index, Expr::Index),
+ #[cfg(not(feature = "no_object"))]
+ Expr::Dot(x2, opt2, pos2) => (x, opt, pos, x2, opt2, pos2, Expr::Index, Expr::Dot),
+ _ => panic!("combo chain expected"),
+ },
+ #[cfg(not(feature = "no_object"))]
+ Expr::Dot(mut x, opt, pos) => match x.lhs.take() {
+ #[cfg(not(feature = "no_index"))]
+ Expr::Index(x2, opt2, pos2) => (x, opt, pos, x2, opt2, pos2, Expr::Dot, Expr::Index),
+ Expr::Dot(x2, opt2, pos2) => (x, opt, pos, x2, opt2, pos2, Expr::Dot, Expr::Index),
+ _ => panic!("combo chain expected"),
+ },
+ _ => panic!("combo chain expected"),
+ };
+
+ // Rewrite the chains like this:
+ //
+ // Source: ( x[y].prop_a )[z].prop_b
+ // ^ ^
+ // parentheses that generated the combo chain
+ //
+ // From: Index( Index( x, Dot(y, prop_a) ), Dot(z, prop_b) )
+ // ^ ^ ^
+ // x root tail
+ //
+ // To: Index( x, Dot(y, Index(prop_a, Dot(z, prop_b) ) ) )
+ //
+ // Equivalent to: x[y].prop_a[z].prop_b
+
+ // Find the end of the root chain.
+ let mut tail = root.as_mut();
+ let mut tail_options = &mut root_options;
+
+ while !tail_options.contains(ASTFlags::BREAK) {
+ match tail.rhs {
+ Expr::Index(ref mut x, ref mut options2, ..) => {
+ tail = x.as_mut();
+ tail_options = options2;
+ }
+ #[cfg(not(feature = "no_object"))]
+ Expr::Dot(ref mut x, ref mut options2, ..) => {
+ tail = x.as_mut();
+ tail_options = options2;
+ }
+ _ => break,
+ }
+ }
+
+ // Since we attach the outer chain to the root chain, we no longer terminate at the end of the
+ // root chain, so remove the ASTFlags::BREAK flag.
+ tail_options.remove(ASTFlags::BREAK);
+
+ x.lhs = tail.rhs.take(); // remove tail and insert it into head of outer chain
+ tail.rhs = make_sub(x, x_options, x_pos); // attach outer chain to tail
+ *expr = make_root(root, root_options, root_pos);
+}
+
+impl Engine {
+ /// Parse a function call.
+ fn parse_fn_call(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ id: ImmutableString,
+ no_args: bool,
+ capture_parent_scope: bool,
+ namespace: Namespace,
+ ) -> ParseResult<Expr> {
+ let (token, token_pos) = if no_args {
+ &(Token::RightParen, Position::NONE)
+ } else {
+ input.peek().expect(NEVER_ENDS)
+ };
+
+ let mut _namespace = namespace;
+ let mut args = FnArgsVec::new_const();
+
+ match token {
+ // id( <EOF>
+ Token::EOF => {
+ return Err(PERR::MissingToken(
+ Token::RightParen.into(),
+ format!("to close the arguments list of this function call '{id}'"),
+ )
+ .into_err(*token_pos))
+ }
+ // id( <error>
+ Token::LexError(err) => return Err(err.clone().into_err(*token_pos)),
+ // id()
+ Token::RightParen => {
+ if !no_args {
+ eat_token(input, Token::RightParen);
+ }
+
+ #[cfg(not(feature = "no_module"))]
+ let hash = if _namespace.is_empty() {
+ calc_fn_hash(None, &id, 0)
+ } else {
+ let root = _namespace.root();
+ let index = state.find_module(root);
+ let is_global = false;
+
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "no_module"))]
+ let is_global = is_global || root == crate::engine::KEYWORD_GLOBAL;
+
+ if settings.has_option(LangOptions::STRICT_VAR)
+ && index.is_none()
+ && !is_global
+ && !state
+ .global_imports
+ .as_deref()
+ .into_iter()
+ .flatten()
+ .any(|m| m.as_str() == root)
+ && !self
+ .global_sub_modules
+ .as_ref()
+ .map_or(false, |m| m.contains_key(root))
+ {
+ return Err(
+ PERR::ModuleUndefined(root.into()).into_err(_namespace.position())
+ );
+ }
+
+ _namespace.set_index(index);
+
+ calc_fn_hash(_namespace.iter().map(Ident::as_str), &id, 0)
+ };
+ #[cfg(feature = "no_module")]
+ let hash = calc_fn_hash(None, &id, 0);
+
+ let hashes = if is_valid_function_name(&id) {
+ FnCallHashes::from_hash(hash)
+ } else {
+ FnCallHashes::from_native_only(hash)
+ };
+
+ args.shrink_to_fit();
+
+ return Ok(FnCallExpr {
+ name: state.get_interned_string(id),
+ capture_parent_scope,
+ op_token: None,
+ namespace: _namespace,
+ hashes,
+ args,
+ }
+ .into_fn_call_expr(settings.pos));
+ }
+ // id...
+ _ => (),
+ }
+
+ let settings = settings.level_up()?;
+
+ loop {
+ match input.peek().expect(NEVER_ENDS) {
+ // id(...args, ) - handle trailing comma
+ (Token::RightParen, ..) => (),
+ _ => args.push(self.parse_expr(input, state, lib, settings)?),
+ }
+
+ match input.peek().expect(NEVER_ENDS) {
+ // id(...args)
+ (Token::RightParen, ..) => {
+ eat_token(input, Token::RightParen);
+
+ #[cfg(not(feature = "no_module"))]
+ let hash = if _namespace.is_empty() {
+ calc_fn_hash(None, &id, args.len())
+ } else {
+ let root = _namespace.root();
+ let index = state.find_module(root);
+
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "no_module"))]
+ let is_global = root == crate::engine::KEYWORD_GLOBAL;
+ #[cfg(any(feature = "no_function", feature = "no_module"))]
+ let is_global = false;
+
+ if settings.has_option(LangOptions::STRICT_VAR)
+ && index.is_none()
+ && !is_global
+ && !state
+ .global_imports
+ .as_deref()
+ .into_iter()
+ .flatten()
+ .any(|m| m.as_str() == root)
+ && !self
+ .global_sub_modules
+ .as_ref()
+ .map_or(false, |m| m.contains_key(root))
+ {
+ return Err(
+ PERR::ModuleUndefined(root.into()).into_err(_namespace.position())
+ );
+ }
+
+ _namespace.set_index(index);
+
+ calc_fn_hash(_namespace.iter().map(Ident::as_str), &id, args.len())
+ };
+ #[cfg(feature = "no_module")]
+ let hash = calc_fn_hash(None, &id, args.len());
+
+ let hashes = if is_valid_function_name(&id) {
+ FnCallHashes::from_hash(hash)
+ } else {
+ FnCallHashes::from_native_only(hash)
+ };
+
+ args.shrink_to_fit();
+
+ return Ok(FnCallExpr {
+ name: state.get_interned_string(id),
+ capture_parent_scope,
+ op_token: None,
+ namespace: _namespace,
+ hashes,
+ args,
+ }
+ .into_fn_call_expr(settings.pos));
+ }
+ // id(...args,
+ (Token::Comma, ..) => {
+ eat_token(input, Token::Comma);
+ }
+ // id(...args <EOF>
+ (Token::EOF, pos) => {
+ return Err(PERR::MissingToken(
+ Token::RightParen.into(),
+ format!("to close the arguments list of this function call '{id}'"),
+ )
+ .into_err(*pos))
+ }
+ // id(...args <error>
+ (Token::LexError(err), pos) => return Err(err.clone().into_err(*pos)),
+ // id(...args ???
+ (.., pos) => {
+ return Err(PERR::MissingToken(
+ Token::Comma.into(),
+ format!("to separate the arguments to function call '{id}'"),
+ )
+ .into_err(*pos))
+ }
+ }
+ }
+ }
+
+ /// Parse an indexing chain.
+ /// Indexing binds to the right, so this call parses all possible levels of indexing following in the input.
+ #[cfg(not(feature = "no_index"))]
+ fn parse_index_chain(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ lhs: Expr,
+ options: ASTFlags,
+ check_index_type: bool,
+ ) -> ParseResult<Expr> {
+ let mut settings = settings;
+
+ let idx_expr = self.parse_expr(input, state, lib, settings.level_up()?)?;
+
+ // Check types of indexing that cannot be overridden
+ // - arrays, maps, strings, bit-fields
+ match lhs {
+ _ if !check_index_type => (),
+
+ Expr::Map(..) => match idx_expr {
+ // lhs[int]
+ Expr::IntegerConstant(..) => {
+ return Err(PERR::MalformedIndexExpr(
+ "Object map expects string index, not a number".into(),
+ )
+ .into_err(idx_expr.start_position()))
+ }
+
+ // lhs[string]
+ Expr::StringConstant(..) | Expr::InterpolatedString(..) => (),
+
+ // lhs[float]
+ #[cfg(not(feature = "no_float"))]
+ Expr::FloatConstant(..) => {
+ return Err(PERR::MalformedIndexExpr(
+ "Object map expects string index, not a float".into(),
+ )
+ .into_err(idx_expr.start_position()))
+ }
+ // lhs[char]
+ Expr::CharConstant(..) => {
+ return Err(PERR::MalformedIndexExpr(
+ "Object map expects string index, not a character".into(),
+ )
+ .into_err(idx_expr.start_position()))
+ }
+ // lhs[()]
+ Expr::Unit(..) => {
+ return Err(PERR::MalformedIndexExpr(
+ "Object map expects string index, not ()".into(),
+ )
+ .into_err(idx_expr.start_position()))
+ }
+ // lhs[??? && ???], lhs[??? || ???], lhs[true], lhs[false]
+ Expr::And(..) | Expr::Or(..) | Expr::BoolConstant(..) => {
+ return Err(PERR::MalformedIndexExpr(
+ "Object map expects string index, not a boolean".into(),
+ )
+ .into_err(idx_expr.start_position()))
+ }
+ _ => (),
+ },
+
+ Expr::IntegerConstant(..)
+ | Expr::Array(..)
+ | Expr::StringConstant(..)
+ | Expr::InterpolatedString(..) => match idx_expr {
+ // lhs[int]
+ Expr::IntegerConstant(..) => (),
+
+ // lhs[string]
+ Expr::StringConstant(..) | Expr::InterpolatedString(..) => {
+ return Err(PERR::MalformedIndexExpr(
+ "Array, string or bit-field expects numeric index, not a string".into(),
+ )
+ .into_err(idx_expr.start_position()))
+ }
+ // lhs[float]
+ #[cfg(not(feature = "no_float"))]
+ Expr::FloatConstant(..) => {
+ return Err(PERR::MalformedIndexExpr(
+ "Array, string or bit-field expects integer index, not a float".into(),
+ )
+ .into_err(idx_expr.start_position()))
+ }
+ // lhs[char]
+ Expr::CharConstant(..) => {
+ return Err(PERR::MalformedIndexExpr(
+ "Array, string or bit-field expects integer index, not a character".into(),
+ )
+ .into_err(idx_expr.start_position()))
+ }
+ // lhs[()]
+ Expr::Unit(..) => {
+ return Err(PERR::MalformedIndexExpr(
+ "Array, string or bit-field expects integer index, not ()".into(),
+ )
+ .into_err(idx_expr.start_position()))
+ }
+ // lhs[??? && ???], lhs[??? || ???], lhs[true], lhs[false]
+ Expr::And(..) | Expr::Or(..) | Expr::BoolConstant(..) => {
+ return Err(PERR::MalformedIndexExpr(
+ "Array, string or bit-field expects integer index, not a boolean".into(),
+ )
+ .into_err(idx_expr.start_position()))
+ }
+ _ => (),
+ },
+ _ => (),
+ }
+
+ // Check if there is a closing bracket
+ match input.peek().expect(NEVER_ENDS) {
+ (Token::RightBracket, ..) => {
+ eat_token(input, Token::RightBracket);
+
+ // Any more indexing following?
+ match input.peek().expect(NEVER_ENDS) {
+ // If another indexing level, right-bind it
+ (Token::LeftBracket | Token::QuestionBracket, ..) => {
+ let (token, pos) = input.next().expect(NEVER_ENDS);
+ let prev_pos = settings.pos;
+ settings.pos = pos;
+ let settings = settings.level_up()?;
+ // Recursively parse the indexing chain, right-binding each
+ let options = match token {
+ Token::LeftBracket => ASTFlags::empty(),
+ Token::QuestionBracket => ASTFlags::NEGATED,
+ _ => unreachable!("`[` or `?[`"),
+ };
+ let idx_expr = self.parse_index_chain(
+ input, state, lib, settings, idx_expr, options, false,
+ )?;
+ // Indexing binds to right
+ Ok(Expr::Index(
+ BinaryExpr { lhs, rhs: idx_expr }.into(),
+ options,
+ prev_pos,
+ ))
+ }
+ // Otherwise terminate the indexing chain
+ _ => Ok(Expr::Index(
+ BinaryExpr { lhs, rhs: idx_expr }.into(),
+ options | ASTFlags::BREAK,
+ settings.pos,
+ )),
+ }
+ }
+ (Token::LexError(err), pos) => Err(err.clone().into_err(*pos)),
+ (.., pos) => Err(PERR::MissingToken(
+ Token::RightBracket.into(),
+ "for a matching [ in this index expression".into(),
+ )
+ .into_err(*pos)),
+ }
+ }
+
+ /// Parse an array literal.
+ #[cfg(not(feature = "no_index"))]
+ fn parse_array_literal(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Expr> {
+ // [ ...
+ let mut settings = settings;
+ settings.pos = eat_token(input, Token::LeftBracket);
+
+ let mut array = FnArgsVec::new_const();
+
+ loop {
+ const MISSING_RBRACKET: &str = "to end this array literal";
+
+ if self.max_array_size() > 0 && array.len() >= self.max_array_size() {
+ return Err(PERR::LiteralTooLarge(
+ "Size of array literal".into(),
+ self.max_array_size(),
+ )
+ .into_err(input.peek().expect(NEVER_ENDS).1));
+ }
+
+ match input.peek().expect(NEVER_ENDS) {
+ (Token::RightBracket, ..) => {
+ eat_token(input, Token::RightBracket);
+ break;
+ }
+ (Token::EOF, pos) => {
+ return Err(PERR::MissingToken(
+ Token::RightBracket.into(),
+ MISSING_RBRACKET.into(),
+ )
+ .into_err(*pos))
+ }
+ _ => array.push(self.parse_expr(input, state, lib, settings.level_up()?)?),
+ }
+
+ match input.peek().expect(NEVER_ENDS) {
+ (Token::Comma, ..) => {
+ eat_token(input, Token::Comma);
+ }
+ (Token::RightBracket, ..) => (),
+ (Token::EOF, pos) => {
+ return Err(PERR::MissingToken(
+ Token::RightBracket.into(),
+ MISSING_RBRACKET.into(),
+ )
+ .into_err(*pos))
+ }
+ (Token::LexError(err), pos) => return Err(err.clone().into_err(*pos)),
+ (.., pos) => {
+ return Err(PERR::MissingToken(
+ Token::Comma.into(),
+ "to separate the items of this array literal".into(),
+ )
+ .into_err(*pos))
+ }
+ };
+ }
+
+ array.shrink_to_fit();
+
+ Ok(Expr::Array(array.into(), settings.pos))
+ }
+
+ /// Parse a map literal.
+ #[cfg(not(feature = "no_object"))]
+ fn parse_map_literal(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Expr> {
+ // #{ ...
+ let mut settings = settings;
+ settings.pos = eat_token(input, Token::MapStart);
+
+ let mut map = StaticVec::<(Ident, Expr)>::new();
+ let mut template = std::collections::BTreeMap::<Identifier, crate::Dynamic>::new();
+
+ loop {
+ const MISSING_RBRACE: &str = "to end this object map literal";
+
+ match input.peek().expect(NEVER_ENDS) {
+ (Token::RightBrace, ..) => {
+ eat_token(input, Token::RightBrace);
+ break;
+ }
+ (Token::EOF, pos) => {
+ return Err(
+ PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into())
+ .into_err(*pos),
+ )
+ }
+ _ => (),
+ }
+
+ let (name, pos) = match input.next().expect(NEVER_ENDS) {
+ (Token::Identifier(..), pos)
+ if settings.has_flag(ParseSettingFlags::DISALLOW_UNQUOTED_MAP_PROPERTIES) =>
+ {
+ return Err(PERR::PropertyExpected.into_err(pos))
+ }
+ (Token::Identifier(s) | Token::StringConstant(s), pos) => {
+ if map.iter().any(|(p, ..)| **p == *s) {
+ return Err(PERR::DuplicatedProperty(s.to_string()).into_err(pos));
+ }
+ (*s, pos)
+ }
+ (Token::InterpolatedString(..), pos) => {
+ return Err(PERR::PropertyExpected.into_err(pos))
+ }
+ (Token::Reserved(s), pos) if is_valid_identifier(s.as_str()) => {
+ return Err(PERR::Reserved(s.to_string()).into_err(pos));
+ }
+ (Token::LexError(err), pos) => return Err(err.into_err(pos)),
+ (Token::EOF, pos) => {
+ return Err(PERR::MissingToken(
+ Token::RightBrace.into(),
+ MISSING_RBRACE.into(),
+ )
+ .into_err(pos));
+ }
+ (.., pos) if map.is_empty() => {
+ return Err(PERR::MissingToken(
+ Token::RightBrace.into(),
+ MISSING_RBRACE.into(),
+ )
+ .into_err(pos));
+ }
+ (.., pos) => return Err(PERR::PropertyExpected.into_err(pos)),
+ };
+
+ match input.next().expect(NEVER_ENDS) {
+ (Token::Colon, ..) => (),
+ (Token::LexError(err), pos) => return Err(err.into_err(pos)),
+ (.., pos) => {
+ return Err(PERR::MissingToken(
+ Token::Colon.into(),
+ format!("to follow the property '{name}' in this object map literal"),
+ )
+ .into_err(pos))
+ }
+ };
+
+ if self.max_map_size() > 0 && map.len() >= self.max_map_size() {
+ return Err(PERR::LiteralTooLarge(
+ "Number of properties in object map literal".into(),
+ self.max_map_size(),
+ )
+ .into_err(input.peek().expect(NEVER_ENDS).1));
+ }
+
+ let expr = self.parse_expr(input, state, lib, settings.level_up()?)?;
+ template.insert(name.clone(), crate::Dynamic::UNIT);
+
+ let name = state.get_interned_string(name);
+ map.push((Ident { name, pos }, expr));
+
+ match input.peek().expect(NEVER_ENDS) {
+ (Token::Comma, ..) => {
+ eat_token(input, Token::Comma);
+ }
+ (Token::RightBrace, ..) => (),
+ (Token::Identifier(..), pos) => {
+ return Err(PERR::MissingToken(
+ Token::Comma.into(),
+ "to separate the items of this object map literal".into(),
+ )
+ .into_err(*pos))
+ }
+ (Token::LexError(err), pos) => return Err(err.clone().into_err(*pos)),
+ (.., pos) => {
+ return Err(
+ PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into())
+ .into_err(*pos),
+ )
+ }
+ }
+ }
+
+ map.shrink_to_fit();
+
+ Ok(Expr::Map((map, template).into(), settings.pos))
+ }
+
+ /// Parse a switch expression.
+ fn parse_switch(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Stmt> {
+ // switch ...
+ let mut settings = settings.level_up()?;
+ settings.pos = eat_token(input, Token::Switch);
+
+ let item = self.parse_expr(input, state, lib, settings)?;
+
+ match input.next().expect(NEVER_ENDS) {
+ (Token::LeftBrace, ..) => (),
+ (Token::LexError(err), pos) => return Err(err.into_err(pos)),
+ (.., pos) => {
+ return Err(PERR::MissingToken(
+ Token::LeftBrace.into(),
+ "to start a switch block".into(),
+ )
+ .into_err(pos))
+ }
+ }
+
+ let mut expressions = StaticVec::<ConditionalExpr>::new();
+ let mut cases = StraightHashMap::<CaseBlocksList>::default();
+ let mut ranges = StaticVec::<RangeCase>::new();
+ let mut def_case = None;
+ let mut def_case_pos = Position::NONE;
+
+ loop {
+ const MISSING_RBRACE: &str = "to end this switch block";
+
+ let (case_expr_list, condition) = match input.peek().expect(NEVER_ENDS) {
+ (Token::RightBrace, ..) => {
+ eat_token(input, Token::RightBrace);
+ break;
+ }
+ (Token::EOF, pos) => {
+ return Err(
+ PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into())
+ .into_err(*pos),
+ )
+ }
+ (Token::Underscore, pos) if def_case.is_none() => {
+ def_case_pos = *pos;
+ eat_token(input, Token::Underscore);
+
+ let (if_clause, if_pos) = match_token(input, Token::If);
+
+ if if_clause {
+ return Err(PERR::WrongSwitchCaseCondition.into_err(if_pos));
+ }
+
+ (
+ StaticVec::default(),
+ Expr::BoolConstant(true, Position::NONE),
+ )
+ }
+ _ if def_case.is_some() => {
+ return Err(PERR::WrongSwitchDefaultCase.into_err(def_case_pos))
+ }
+
+ _ => {
+ let mut case_expr_list = StaticVec::new();
+
+ loop {
+ let filter = state.expr_filter;
+ state.expr_filter = |t| t != &Token::Pipe;
+ let expr = self.parse_expr(input, state, lib, settings);
+ state.expr_filter = filter;
+
+ match expr {
+ Ok(expr) => case_expr_list.push(expr),
+ Err(err) => {
+ return Err(PERR::ExprExpected("literal".into()).into_err(err.1))
+ }
+ }
+
+ if !match_token(input, Token::Pipe).0 {
+ break;
+ }
+ }
+
+ let condition = if match_token(input, Token::If).0 {
+ ensure_not_statement_expr(input, "a boolean")?;
+ let guard = self
+ .parse_expr(input, state, lib, settings)?
+ .ensure_bool_expr()?;
+ ensure_not_assignment(input)?;
+ guard
+ } else {
+ Expr::BoolConstant(true, Position::NONE)
+ };
+ (case_expr_list, condition)
+ }
+ };
+
+ match input.next().expect(NEVER_ENDS) {
+ (Token::DoubleArrow, ..) => (),
+ (Token::LexError(err), pos) => return Err(err.into_err(pos)),
+ (.., pos) => {
+ return Err(PERR::MissingToken(
+ Token::DoubleArrow.into(),
+ "in this switch case".into(),
+ )
+ .into_err(pos))
+ }
+ };
+
+ let (action_expr, need_comma) =
+ if settings.has_flag(ParseSettingFlags::DISALLOW_STATEMENTS_IN_BLOCKS) {
+ (self.parse_expr(input, state, lib, settings)?, true)
+ } else {
+ let stmt = self.parse_stmt(input, state, lib, settings)?;
+ let need_comma = !stmt.is_self_terminated();
+
+ let stmt_block: StmtBlock = stmt.into();
+ (Expr::Stmt(stmt_block.into()), need_comma)
+ };
+
+ expressions.push((condition, action_expr).into());
+ let index = expressions.len() - 1;
+
+ if case_expr_list.is_empty() {
+ def_case = Some(index);
+ } else {
+ for expr in case_expr_list {
+ let value = expr.get_literal_value().ok_or_else(|| {
+ PERR::ExprExpected("a literal".into()).into_err(expr.start_position())
+ })?;
+
+ let mut range_value: Option<RangeCase> = None;
+
+ let guard = value.read_lock::<ExclusiveRange>();
+ if let Some(range) = guard {
+ range_value = Some(range.clone().into());
+ } else if let Some(range) = value.read_lock::<InclusiveRange>() {
+ range_value = Some(range.clone().into());
+ }
+
+ if let Some(mut r) = range_value {
+ if !r.is_empty() {
+ // Other range
+ r.set_index(index);
+ ranges.push(r);
+ }
+ continue;
+ }
+
+ if !ranges.is_empty() {
+ let forbidden = match value {
+ Dynamic(Union::Int(..)) => true,
+ #[cfg(not(feature = "no_float"))]
+ Dynamic(Union::Float(..)) => true,
+ #[cfg(feature = "decimal")]
+ Dynamic(Union::Decimal(..)) => true,
+ _ => false,
+ };
+
+ if forbidden {
+ return Err(
+ PERR::WrongSwitchIntegerCase.into_err(expr.start_position())
+ );
+ }
+ }
+
+ let hasher = &mut get_hasher();
+ value.hash(hasher);
+ let hash = hasher.finish();
+
+ cases
+ .entry(hash)
+ .and_modify(|cases| cases.push(index))
+ .or_insert_with(|| [index].into());
+ }
+ }
+
+ match input.peek().expect(NEVER_ENDS) {
+ (Token::Comma, ..) => {
+ eat_token(input, Token::Comma);
+ }
+ (Token::RightBrace, ..) => (),
+ (Token::EOF, pos) => {
+ return Err(
+ PERR::MissingToken(Token::RightParen.into(), MISSING_RBRACE.into())
+ .into_err(*pos),
+ )
+ }
+ (Token::LexError(err), pos) => return Err(err.clone().into_err(*pos)),
+ (.., pos) if need_comma => {
+ return Err(PERR::MissingToken(
+ Token::Comma.into(),
+ "to separate the items in this switch block".into(),
+ )
+ .into_err(*pos))
+ }
+ _ => (),
+ }
+ }
+
+ expressions.shrink_to_fit();
+ cases.shrink_to_fit();
+ ranges.shrink_to_fit();
+
+ let cases = SwitchCasesCollection {
+ expressions,
+ cases,
+ ranges,
+ def_case,
+ };
+
+ Ok(Stmt::Switch((item, cases).into(), settings.pos))
+ }
+
+ /// Parse a primary expression.
+ fn parse_primary(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ options: ChainingFlags,
+ ) -> ParseResult<Expr> {
+ let (token, token_pos) = input.peek().expect(NEVER_ENDS);
+
+ let mut settings = settings;
+ settings.pos = *token_pos;
+
+ let root_expr = match token {
+ _ if !(state.expr_filter)(token) => {
+ return Err(LexError::UnexpectedInput(token.to_string()).into_err(settings.pos))
+ }
+
+ Token::EOF => return Err(PERR::UnexpectedEOF.into_err(settings.pos)),
+
+ Token::Unit => {
+ input.next();
+ Expr::Unit(settings.pos)
+ }
+
+ Token::IntegerConstant(..)
+ | Token::CharConstant(..)
+ | Token::StringConstant(..)
+ | Token::True
+ | Token::False => match input.next().expect(NEVER_ENDS).0 {
+ Token::IntegerConstant(x) => Expr::IntegerConstant(x, settings.pos),
+ Token::CharConstant(c) => Expr::CharConstant(c, settings.pos),
+ Token::StringConstant(s) => {
+ Expr::StringConstant(state.get_interned_string(*s), settings.pos)
+ }
+ Token::True => Expr::BoolConstant(true, settings.pos),
+ Token::False => Expr::BoolConstant(false, settings.pos),
+ token => unreachable!("token is {:?}", token),
+ },
+ #[cfg(not(feature = "no_float"))]
+ Token::FloatConstant(x) => {
+ let x = *x;
+ input.next();
+ Expr::FloatConstant(x, settings.pos)
+ }
+ #[cfg(feature = "decimal")]
+ Token::DecimalConstant(x) => {
+ let x = (**x).into();
+ input.next();
+ Expr::DynamicConstant(Box::new(x), settings.pos)
+ }
+
+ // { - block statement as expression
+ Token::LeftBrace if settings.has_option(LangOptions::STMT_EXPR) => {
+ match self.parse_block(input, state, lib, settings.level_up()?)? {
+ block @ Stmt::Block(..) => Expr::Stmt(Box::new(block.into())),
+ stmt => unreachable!("Stmt::Block expected but gets {:?}", stmt),
+ }
+ }
+
+ // ( - grouped expression
+ Token::LeftParen => {
+ settings.pos = eat_token(input, Token::LeftParen);
+
+ let expr = self.parse_expr(input, state, lib, settings.level_up()?)?;
+
+ match input.next().expect(NEVER_ENDS) {
+ // ( ... )
+ (Token::RightParen, ..) => expr,
+ // ( <error>
+ (Token::LexError(err), pos) => return Err(err.into_err(pos)),
+ // ( ... ???
+ (.., pos) => {
+ return Err(PERR::MissingToken(
+ Token::RightParen.into(),
+ "for a matching ( in this expression".into(),
+ )
+ .into_err(pos))
+ }
+ }
+ }
+
+ // If statement is allowed to act as expressions
+ Token::If if settings.has_option(LangOptions::IF_EXPR) => Expr::Stmt(Box::new(
+ self.parse_if(input, state, lib, settings.level_up()?)?
+ .into(),
+ )),
+ // Loops are allowed to act as expressions
+ Token::While | Token::Loop
+ if self.allow_looping() && settings.has_option(LangOptions::LOOP_EXPR) =>
+ {
+ Expr::Stmt(Box::new(
+ self.parse_while_loop(input, state, lib, settings.level_up()?)?
+ .into(),
+ ))
+ }
+ Token::Do if self.allow_looping() && settings.has_option(LangOptions::LOOP_EXPR) => {
+ Expr::Stmt(Box::new(
+ self.parse_do(input, state, lib, settings.level_up()?)?
+ .into(),
+ ))
+ }
+ Token::For if self.allow_looping() && settings.has_option(LangOptions::LOOP_EXPR) => {
+ Expr::Stmt(Box::new(
+ self.parse_for(input, state, lib, settings.level_up()?)?
+ .into(),
+ ))
+ }
+ // Switch statement is allowed to act as expressions
+ Token::Switch if settings.has_option(LangOptions::SWITCH_EXPR) => Expr::Stmt(Box::new(
+ self.parse_switch(input, state, lib, settings.level_up()?)?
+ .into(),
+ )),
+
+ // | ...
+ #[cfg(not(feature = "no_function"))]
+ Token::Pipe | Token::Or if settings.has_option(LangOptions::ANON_FN) => {
+ // Build new parse state
+ let new_interner = &mut StringsInterner::new();
+ let new_state = &mut ParseState::new(
+ state.external_constants,
+ new_interner,
+ state.tokenizer_control.clone(),
+ );
+
+ // We move the strings interner to the new parse state object by swapping it...
+ std::mem::swap(state.interned_strings, new_state.interned_strings);
+
+ #[cfg(not(feature = "no_module"))]
+ {
+ // Do not allow storing an index to a globally-imported module
+ // just in case the function is separated from this `AST`.
+ //
+ // Keep them in `global_imports` instead so that strict variables
+ // mode will not complain.
+ new_state.global_imports.clone_from(&state.global_imports);
+ new_state
+ .global_imports
+ .get_or_insert_with(Default::default)
+ .extend(state.imports.as_deref().into_iter().flatten().cloned());
+ }
+
+ // Brand new options
+ #[cfg(not(feature = "no_closure"))]
+ let options = self.options & !LangOptions::STRICT_VAR; // a capturing closure can access variables not defined locally, so turn off Strict Variables mode
+ #[cfg(feature = "no_closure")]
+ let options = self.options | (settings.options & LangOptions::STRICT_VAR);
+
+ // Brand new flags, turn on function scope and closure scope
+ let flags = ParseSettingFlags::FN_SCOPE
+ | ParseSettingFlags::CLOSURE_SCOPE
+ | (settings.flags
+ & (ParseSettingFlags::DISALLOW_UNQUOTED_MAP_PROPERTIES
+ | ParseSettingFlags::DISALLOW_STATEMENTS_IN_BLOCKS));
+
+ let new_settings = ParseSettings {
+ flags,
+ options,
+ ..settings
+ };
+
+ let result =
+ self.parse_anon_fn(input, new_state, lib, new_settings.level_up()?, state);
+
+ // Restore the strings interner by swapping it back
+ std::mem::swap(state.interned_strings, new_state.interned_strings);
+
+ let (expr, fn_def) = result?;
+
+ #[cfg(not(feature = "no_closure"))]
+ for Ident { name, pos } in new_state.external_vars.as_deref().into_iter().flatten()
+ {
+ let (index, is_func) = state.access_var(name, lib, *pos);
+
+ if !is_func
+ && index.is_none()
+ && !settings.has_flag(ParseSettingFlags::CLOSURE_SCOPE)
+ && settings.has_option(LangOptions::STRICT_VAR)
+ && !state
+ .external_constants
+ .map_or(false, |scope| scope.contains(name))
+ {
+ // If the parent scope is not inside another capturing closure
+ // then we can conclude that the captured variable doesn't exist.
+ // Under Strict Variables mode, this is not allowed.
+ return Err(PERR::VariableUndefined(name.to_string()).into_err(*pos));
+ }
+ }
+
+ let hash_script = calc_fn_hash(None, &fn_def.name, fn_def.params.len());
+ lib.insert(hash_script, fn_def);
+
+ expr
+ }
+
+ // Interpolated string
+ Token::InterpolatedString(..) => {
+ let mut segments = FnArgsVec::new_const();
+ let settings = settings.level_up()?;
+
+ match input.next().expect(NEVER_ENDS) {
+ (Token::InterpolatedString(s), ..) if s.is_empty() => (),
+ (Token::InterpolatedString(s), pos) => {
+ segments.push(Expr::StringConstant(state.get_interned_string(*s), pos))
+ }
+ token => {
+ unreachable!("Token::InterpolatedString expected but gets {:?}", token)
+ }
+ }
+
+ loop {
+ let expr = match self.parse_block(input, state, lib, settings)? {
+ block @ Stmt::Block(..) => Expr::Stmt(Box::new(block.into())),
+ stmt => unreachable!("Stmt::Block expected but gets {:?}", stmt),
+ };
+ match expr {
+ Expr::StringConstant(s, ..) if s.is_empty() => (),
+ _ => segments.push(expr),
+ }
+
+ // Make sure to parse the following as text
+ state.tokenizer_control.borrow_mut().is_within_text = true;
+
+ match input.next().expect(NEVER_ENDS) {
+ (Token::StringConstant(s), pos) => {
+ if !s.is_empty() {
+ segments
+ .push(Expr::StringConstant(state.get_interned_string(*s), pos));
+ }
+ // End the interpolated string if it is terminated by a back-tick.
+ break;
+ }
+ (Token::InterpolatedString(s), pos) => {
+ if !s.is_empty() {
+ segments
+ .push(Expr::StringConstant(state.get_interned_string(*s), pos));
+ }
+ }
+ (Token::LexError(err), pos)
+ if matches!(*err, LexError::UnterminatedString) =>
+ {
+ return Err(err.into_err(pos))
+ }
+ (token, ..) => unreachable!(
+ "string within an interpolated string literal expected but gets {:?}",
+ token
+ ),
+ }
+ }
+
+ if segments.is_empty() {
+ Expr::StringConstant(state.get_interned_string(""), settings.pos)
+ } else {
+ segments.shrink_to_fit();
+ Expr::InterpolatedString(segments.into(), settings.pos)
+ }
+ }
+
+ // Array literal
+ #[cfg(not(feature = "no_index"))]
+ Token::LeftBracket => {
+ self.parse_array_literal(input, state, lib, settings.level_up()?)?
+ }
+
+ // Map literal
+ #[cfg(not(feature = "no_object"))]
+ Token::MapStart => self.parse_map_literal(input, state, lib, settings.level_up()?)?,
+
+ // Custom syntax.
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Token::Custom(key) | Token::Reserved(key) | Token::Identifier(key)
+ if self
+ .custom_syntax
+ .as_ref()
+ .map_or(false, |m| m.contains_key(&**key)) =>
+ {
+ let (key, syntax) = self
+ .custom_syntax
+ .as_ref()
+ .and_then(|m| m.get_key_value(&**key))
+ .unwrap();
+ let (.., pos) = input.next().expect(NEVER_ENDS);
+ let settings = settings.level_up()?;
+ self.parse_custom_syntax(input, state, lib, settings, key, syntax, pos)?
+ }
+
+ // Identifier
+ Token::Identifier(..) => {
+ let ns = Namespace::NONE;
+
+ let s = match input.next().expect(NEVER_ENDS) {
+ (Token::Identifier(s), ..) => s,
+ token => unreachable!("Token::Identifier expected but gets {:?}", token),
+ };
+
+ match input.peek().expect(NEVER_ENDS) {
+ // Function call
+ (Token::LeftParen | Token::Bang | Token::Unit, _) => {
+ // Once the identifier consumed we must enable next variables capturing
+ state.allow_capture = true;
+
+ Expr::Variable(
+ (None, ns, 0, state.get_interned_string(*s)).into(),
+ None,
+ settings.pos,
+ )
+ }
+ // Namespace qualification
+ #[cfg(not(feature = "no_module"))]
+ (token @ Token::DoubleColon, pos) => {
+ if options.contains(ChainingFlags::DISALLOW_NAMESPACES) {
+ return Err(LexError::ImproperSymbol(
+ token.literal_syntax().into(),
+ String::new(),
+ )
+ .into_err(*pos));
+ }
+
+ // Once the identifier consumed we must enable next variables capturing
+ state.allow_capture = true;
+
+ let name = state.get_interned_string(*s);
+ Expr::Variable((None, ns, 0, name).into(), None, settings.pos)
+ }
+ // Normal variable access
+ _ => {
+ let (index, is_func) = state.access_var(&s, lib, settings.pos);
+
+ if !options.contains(ChainingFlags::PROPERTY)
+ && !is_func
+ && index.is_none()
+ && settings.has_option(LangOptions::STRICT_VAR)
+ && !state
+ .external_constants
+ .map_or(false, |scope| scope.contains(&s))
+ {
+ return Err(
+ PERR::VariableUndefined(s.to_string()).into_err(settings.pos)
+ );
+ }
+
+ let short_index = index
+ .and_then(|x| u8::try_from(x.get()).ok())
+ .and_then(NonZeroU8::new);
+ let name = state.get_interned_string(*s);
+ Expr::Variable((index, ns, 0, name).into(), short_index, settings.pos)
+ }
+ }
+ }
+
+ // Reserved keyword or symbol
+ Token::Reserved(..) => {
+ let ns = Namespace::NONE;
+
+ let s = match input.next().expect(NEVER_ENDS) {
+ (Token::Reserved(s), ..) => s,
+ token => unreachable!("Token::Reserved expected but gets {:?}", token),
+ };
+
+ match input.peek().expect(NEVER_ENDS).0 {
+ // Function call is allowed to have reserved keyword
+ Token::LeftParen | Token::Bang | Token::Unit
+ if is_reserved_keyword_or_symbol(&s).1 =>
+ {
+ Expr::Variable(
+ (None, ns, 0, state.get_interned_string(*s)).into(),
+ None,
+ settings.pos,
+ )
+ }
+ // Access to `this` as a variable
+ #[cfg(not(feature = "no_function"))]
+ _ if *s == crate::engine::KEYWORD_THIS => {
+ // OK within a function scope
+ if settings.has_flag(ParseSettingFlags::FN_SCOPE) {
+ Expr::ThisPtr(settings.pos)
+ } else {
+ // Cannot access to `this` as a variable not in a function scope
+ let msg = format!("'{s}' can only be used in functions");
+ return Err(
+ LexError::ImproperSymbol(s.to_string(), msg).into_err(settings.pos)
+ );
+ }
+ }
+ _ => return Err(PERR::Reserved(s.to_string()).into_err(settings.pos)),
+ }
+ }
+
+ Token::LexError(..) => match input.next().expect(NEVER_ENDS) {
+ (Token::LexError(err), ..) => return Err(err.into_err(settings.pos)),
+ token => unreachable!("Token::LexError expected but gets {:?}", token),
+ },
+
+ _ => return Err(LexError::UnexpectedInput(token.to_string()).into_err(settings.pos)),
+ };
+
+ if !(state.expr_filter)(&input.peek().expect(NEVER_ENDS).0) {
+ return Ok(root_expr);
+ }
+
+ self.parse_postfix(
+ input,
+ state,
+ lib,
+ settings,
+ root_expr,
+ ChainingFlags::empty(),
+ )
+ }
+
+ /// Tail processing of all possible postfix operators of a primary expression.
+ fn parse_postfix(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ mut lhs: Expr,
+ _options: ChainingFlags,
+ ) -> ParseResult<Expr> {
+ let mut settings = settings;
+
+ // Break just in case `lhs` is `Expr::Dot` or `Expr::Index`
+ let mut parent_options = ASTFlags::BREAK;
+
+ // Tail processing all possible postfix operators
+ loop {
+ let (tail_token, ..) = input.peek().expect(NEVER_ENDS);
+
+ if !lhs.is_valid_postfix(tail_token) {
+ break;
+ }
+
+ let (tail_token, tail_pos) = input.next().expect(NEVER_ENDS);
+ settings.pos = tail_pos;
+
+ lhs = match (lhs, tail_token) {
+ // Qualified function call with !
+ #[cfg(not(feature = "no_module"))]
+ (Expr::Variable(x, ..), Token::Bang) if !x.1.is_empty() => {
+ return match input.peek().expect(NEVER_ENDS) {
+ (Token::LeftParen | Token::Unit, ..) => {
+ Err(LexError::UnexpectedInput(Token::Bang.into()).into_err(tail_pos))
+ }
+ _ => Err(LexError::ImproperSymbol(
+ "!".into(),
+ "'!' cannot be used to call module functions".into(),
+ )
+ .into_err(tail_pos)),
+ };
+ }
+ // Function call with !
+ (Expr::Variable(x, .., pos), Token::Bang) => {
+ match input.peek().expect(NEVER_ENDS) {
+ (Token::LeftParen | Token::Unit, ..) => (),
+ (_, pos) => {
+ return Err(PERR::MissingToken(
+ Token::LeftParen.into(),
+ "to start arguments list of function call".into(),
+ )
+ .into_err(*pos))
+ }
+ }
+
+ let no_args = input.next().expect(NEVER_ENDS).0 == Token::Unit;
+
+ let (.., ns, _, name) = *x;
+ settings.pos = pos;
+
+ self.parse_fn_call(input, state, lib, settings, name, no_args, true, ns)?
+ }
+ // Function call
+ (Expr::Variable(x, .., pos), t @ (Token::LeftParen | Token::Unit)) => {
+ let (.., ns, _, name) = *x;
+ let no_args = t == Token::Unit;
+ settings.pos = pos;
+
+ self.parse_fn_call(input, state, lib, settings, name, no_args, false, ns)?
+ }
+ // Disallowed module separator
+ #[cfg(not(feature = "no_module"))]
+ (_, token @ Token::DoubleColon)
+ if _options.contains(ChainingFlags::DISALLOW_NAMESPACES) =>
+ {
+ return Err(LexError::ImproperSymbol(
+ token.literal_syntax().into(),
+ String::new(),
+ )
+ .into_err(tail_pos))
+ }
+ // module access
+ #[cfg(not(feature = "no_module"))]
+ (Expr::Variable(x, .., pos), Token::DoubleColon) => {
+ let (id2, pos2) = parse_var_name(input)?;
+ let (.., mut namespace, _, name) = *x;
+ let var_name_def = Ident { name, pos };
+
+ namespace.push(var_name_def);
+
+ let var_name = state.get_interned_string(id2);
+
+ Expr::Variable((None, namespace, 0, var_name).into(), None, pos2)
+ }
+ // Indexing
+ #[cfg(not(feature = "no_index"))]
+ (expr, token @ (Token::LeftBracket | Token::QuestionBracket)) => {
+ let opt = match token {
+ Token::LeftBracket => ASTFlags::empty(),
+ Token::QuestionBracket => ASTFlags::NEGATED,
+ _ => unreachable!("`[` or `?[`"),
+ };
+ let settings = settings.level_up()?;
+ self.parse_index_chain(input, state, lib, settings, expr, opt, true)?
+ }
+ // Property access
+ #[cfg(not(feature = "no_object"))]
+ (expr, op @ (Token::Period | Token::Elvis)) => {
+ // Expression after dot must start with an identifier
+ match input.peek().expect(NEVER_ENDS) {
+ (Token::Identifier(..), ..) => {
+ // Prevents capturing of the object properties as vars: xxx.<var>
+ state.allow_capture = false;
+ }
+ (Token::Reserved(s), ..) if is_reserved_keyword_or_symbol(s).2 => (),
+ (Token::Reserved(s), pos) => {
+ return Err(PERR::Reserved(s.to_string()).into_err(*pos))
+ }
+ (.., pos) => return Err(PERR::PropertyExpected.into_err(*pos)),
+ }
+
+ let op_flags = match op {
+ Token::Period => ASTFlags::empty(),
+ Token::Elvis => ASTFlags::NEGATED,
+ _ => unreachable!("`.` or `?.`"),
+ };
+ let options = ChainingFlags::PROPERTY | ChainingFlags::DISALLOW_NAMESPACES;
+ let rhs =
+ self.parse_primary(input, state, lib, settings.level_up()?, options)?;
+
+ Self::make_dot_expr(state, expr, rhs, parent_options, op_flags, tail_pos)?
+ }
+ // Unknown postfix operator
+ (expr, token) => {
+ unreachable!("unknown postfix operator '{}' for {:?}", token, expr)
+ }
+ };
+
+ // The chain is now extended
+ parent_options = ASTFlags::empty();
+ }
+
+ // Optimize chain where the root expression is another chain
+ #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
+ if matches!(lhs, Expr::Index(ref x, ..) | Expr::Dot(ref x, ..) if matches!(x.lhs, Expr::Index(..) | Expr::Dot(..)))
+ {
+ optimize_combo_chain(&mut lhs)
+ }
+
+ // Cache the hash key for namespace-qualified variables
+ #[cfg(not(feature = "no_module"))]
+ let namespaced_variable = match lhs {
+ Expr::Variable(ref mut x, ..) if !x.1.is_empty() => Some(&mut **x),
+ Expr::Index(ref mut x, ..) | Expr::Dot(ref mut x, ..) => match x.lhs {
+ Expr::Variable(ref mut x, ..) if !x.1.is_empty() => Some(&mut **x),
+ _ => None,
+ },
+ _ => None,
+ };
+
+ #[cfg(not(feature = "no_module"))]
+ if let Some((.., namespace, hash, name)) = namespaced_variable {
+ if !namespace.is_empty() {
+ *hash = crate::calc_var_hash(namespace.iter().map(Ident::as_str), name);
+
+ #[cfg(not(feature = "no_module"))]
+ {
+ let root = namespace.root();
+ let index = state.find_module(root);
+ let is_global = false;
+
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "no_module"))]
+ let is_global = is_global || root == crate::engine::KEYWORD_GLOBAL;
+
+ if settings.has_option(LangOptions::STRICT_VAR)
+ && index.is_none()
+ && !is_global
+ && !state
+ .global_imports
+ .as_deref()
+ .into_iter()
+ .flatten()
+ .any(|m| m.as_str() == root)
+ && !self
+ .global_sub_modules
+ .as_ref()
+ .map_or(false, |m| m.contains_key(root))
+ {
+ return Err(
+ PERR::ModuleUndefined(root.into()).into_err(namespace.position())
+ );
+ }
+
+ namespace.set_index(index);
+ }
+ }
+ }
+
+ // Make sure identifiers are valid
+ Ok(lhs)
+ }
+
+ /// Parse a potential unary operator.
+ fn parse_unary(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Expr> {
+ let (token, token_pos) = input.peek().expect(NEVER_ENDS);
+
+ if !(state.expr_filter)(token) {
+ return Err(LexError::UnexpectedInput(token.to_string()).into_err(*token_pos));
+ }
+
+ let mut settings = settings;
+ settings.pos = *token_pos;
+
+ match token {
+ // -expr
+ Token::Minus | Token::UnaryMinus => {
+ let token = token.clone();
+ let pos = eat_token(input, token.clone());
+
+ match self.parse_unary(input, state, lib, settings.level_up()?)? {
+ // Negative integer
+ Expr::IntegerConstant(num, ..) => num
+ .checked_neg()
+ .map(|i| Expr::IntegerConstant(i, pos))
+ .or_else(|| {
+ #[cfg(not(feature = "no_float"))]
+ return Some(Expr::FloatConstant((-(num as crate::FLOAT)).into(), pos));
+ #[cfg(feature = "no_float")]
+ return None;
+ })
+ .ok_or_else(|| LexError::MalformedNumber(format!("-{num}")).into_err(pos)),
+
+ // Negative float
+ #[cfg(not(feature = "no_float"))]
+ Expr::FloatConstant(x, ..) => Ok(Expr::FloatConstant((-(*x)).into(), pos)),
+
+ // Call negative function
+ expr => {
+ let mut args = FnArgsVec::new_const();
+ args.push(expr);
+ args.shrink_to_fit();
+
+ Ok(FnCallExpr {
+ namespace: Namespace::NONE,
+ name: state.get_interned_string("-"),
+ hashes: FnCallHashes::from_native_only(calc_fn_hash(None, "-", 1)),
+ args,
+ op_token: Some(token),
+ capture_parent_scope: false,
+ }
+ .into_fn_call_expr(pos))
+ }
+ }
+ }
+ // +expr
+ Token::Plus | Token::UnaryPlus => {
+ let token = token.clone();
+ let pos = eat_token(input, token.clone());
+
+ match self.parse_unary(input, state, lib, settings.level_up()?)? {
+ expr @ Expr::IntegerConstant(..) => Ok(expr),
+ #[cfg(not(feature = "no_float"))]
+ expr @ Expr::FloatConstant(..) => Ok(expr),
+
+ // Call plus function
+ expr => {
+ let mut args = FnArgsVec::new_const();
+ args.push(expr);
+ args.shrink_to_fit();
+
+ Ok(FnCallExpr {
+ namespace: Namespace::NONE,
+ name: state.get_interned_string("+"),
+ hashes: FnCallHashes::from_native_only(calc_fn_hash(None, "+", 1)),
+ args,
+ op_token: Some(token),
+ capture_parent_scope: false,
+ }
+ .into_fn_call_expr(pos))
+ }
+ }
+ }
+ // !expr
+ Token::Bang => {
+ let token = token.clone();
+ let pos = eat_token(input, Token::Bang);
+
+ let mut args = FnArgsVec::new_const();
+ args.push(self.parse_unary(input, state, lib, settings.level_up()?)?);
+ args.shrink_to_fit();
+
+ Ok(FnCallExpr {
+ namespace: Namespace::NONE,
+ name: state.get_interned_string("!"),
+ hashes: FnCallHashes::from_native_only(calc_fn_hash(None, "!", 1)),
+ args,
+ op_token: Some(token),
+ capture_parent_scope: false,
+ }
+ .into_fn_call_expr(pos))
+ }
+ // <EOF>
+ Token::EOF => Err(PERR::UnexpectedEOF.into_err(settings.pos)),
+ // All other tokens
+ _ => self.parse_primary(input, state, lib, settings, ChainingFlags::empty()),
+ }
+ }
+
+ /// Make an assignment statement.
+ fn make_assignment_stmt(
+ op: Option<Token>,
+ state: &mut ParseState,
+ lhs: Expr,
+ rhs: Expr,
+ op_pos: Position,
+ ) -> ParseResult<Stmt> {
+ #[must_use]
+ fn check_lvalue(expr: &Expr, parent_is_dot: bool) -> Option<Position> {
+ match expr {
+ Expr::Index(x, options, ..) | Expr::Dot(x, options, ..) if parent_is_dot => {
+ match x.lhs {
+ Expr::Property(..) if !options.contains(ASTFlags::BREAK) => {
+ check_lvalue(&x.rhs, matches!(expr, Expr::Dot(..)))
+ }
+ Expr::Property(..) => None,
+ // Anything other than a property after dotting (e.g. a method call) is not an l-value
+ ref e => Some(e.position()),
+ }
+ }
+ Expr::Index(x, options, ..) | Expr::Dot(x, options, ..) => match x.lhs {
+ Expr::Property(..) => unreachable!("unexpected Expr::Property in indexing"),
+ _ if !options.contains(ASTFlags::BREAK) => {
+ check_lvalue(&x.rhs, matches!(expr, Expr::Dot(..)))
+ }
+ _ => None,
+ },
+ Expr::Property(..) if parent_is_dot => None,
+ Expr::Property(..) => unreachable!("unexpected Expr::Property in indexing"),
+ e if parent_is_dot => Some(e.position()),
+ _ => None,
+ }
+ }
+
+ let op_info = if let Some(op) = op {
+ OpAssignment::new_op_assignment_from_token(op, op_pos)
+ } else {
+ OpAssignment::new_assignment(op_pos)
+ };
+
+ match lhs {
+ // this = rhs
+ Expr::ThisPtr(_) => Ok(Stmt::Assignment((op_info, (lhs, rhs).into()).into())),
+ // var (non-indexed) = rhs
+ Expr::Variable(ref x, None, _) if x.0.is_none() => {
+ Ok(Stmt::Assignment((op_info, (lhs, rhs).into()).into()))
+ }
+ // var (indexed) = rhs
+ Expr::Variable(ref x, i, var_pos) => {
+ let stack = state.stack.get_or_insert_with(Default::default);
+ let (index, .., name) = &**x;
+ let index = i.map_or_else(
+ || index.expect("either long or short index is `None`").get(),
+ |n| n.get() as usize,
+ );
+
+ match stack.get_mut_by_index(stack.len() - index).access_mode() {
+ AccessMode::ReadWrite => {
+ Ok(Stmt::Assignment((op_info, (lhs, rhs).into()).into()))
+ }
+ // Constant values cannot be assigned to
+ AccessMode::ReadOnly => {
+ Err(PERR::AssignmentToConstant(name.to_string()).into_err(var_pos))
+ }
+ }
+ }
+ // xxx[???]... = rhs, xxx.prop... = rhs
+ Expr::Index(ref x, options, ..) | Expr::Dot(ref x, options, ..) => {
+ let valid_lvalue = if options.contains(ASTFlags::BREAK) {
+ None
+ } else {
+ check_lvalue(&x.rhs, matches!(lhs, Expr::Dot(..)))
+ };
+
+ match valid_lvalue {
+ None => {
+ match x.lhs {
+ // var[???] = rhs, this[???] = rhs, var.??? = rhs, this.??? = rhs
+ Expr::Variable(..) | Expr::ThisPtr(..) => {
+ Ok(Stmt::Assignment((op_info, (lhs, rhs).into()).into()))
+ }
+ // expr[???] = rhs, expr.??? = rhs
+ ref expr => Err(PERR::AssignmentToInvalidLHS(String::new())
+ .into_err(expr.position())),
+ }
+ }
+ Some(err_pos) => {
+ Err(PERR::AssignmentToInvalidLHS(String::new()).into_err(err_pos))
+ }
+ }
+ }
+ // const_expr = rhs
+ ref expr if expr.is_constant() => {
+ Err(PERR::AssignmentToConstant(String::new()).into_err(lhs.start_position()))
+ }
+ // ??? && ??? = rhs, ??? || ??? = rhs, xxx ?? xxx = rhs
+ Expr::And(..) | Expr::Or(..) | Expr::Coalesce(..) if !op_info.is_op_assignment() => {
+ Err(LexError::ImproperSymbol(
+ Token::Equals.literal_syntax().into(),
+ "Possibly a typo of '=='?".into(),
+ )
+ .into_err(op_pos))
+ }
+ // expr = rhs
+ _ => Err(PERR::AssignmentToInvalidLHS(String::new()).into_err(lhs.position())),
+ }
+ }
+
+ /// Make a dot expression.
+ #[cfg(not(feature = "no_object"))]
+ fn make_dot_expr(
+ state: &mut ParseState,
+ lhs: Expr,
+ rhs: Expr,
+ parent_options: ASTFlags,
+ op_flags: ASTFlags,
+ op_pos: Position,
+ ) -> ParseResult<Expr> {
+ match (lhs, rhs) {
+ // lhs[...][...].rhs
+ (Expr::Index(mut x, options, pos), rhs)
+ if !parent_options.contains(ASTFlags::BREAK) =>
+ {
+ let options = options | parent_options;
+ x.rhs = Self::make_dot_expr(state, x.rhs, rhs, options, op_flags, op_pos)?;
+ Ok(Expr::Index(x, ASTFlags::empty(), pos))
+ }
+ // lhs.module::id - syntax error
+ #[cfg(not(feature = "no_module"))]
+ (.., Expr::Variable(x, ..)) if !x.1.is_empty() => unreachable!("lhs.ns::id"),
+ // lhs.id
+ (lhs, var_expr @ Expr::Variable(..)) => {
+ let rhs = var_expr.into_property(state);
+ Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), op_flags, op_pos))
+ }
+ // lhs.prop
+ (lhs, prop @ Expr::Property(..)) => Ok(Expr::Dot(
+ BinaryExpr { lhs, rhs: prop }.into(),
+ op_flags,
+ op_pos,
+ )),
+ // lhs.nnn::func(...) - syntax error
+ #[cfg(not(feature = "no_module"))]
+ (.., Expr::FnCall(f, ..)) if f.is_qualified() => unreachable!("lhs.ns::func()"),
+ // lhs.Fn() or lhs.eval()
+ (.., Expr::FnCall(f, func_pos))
+ if f.args.is_empty()
+ && [crate::engine::KEYWORD_FN_PTR, crate::engine::KEYWORD_EVAL]
+ .contains(&f.name.as_str()) =>
+ {
+ let err_msg = format!(
+ "'{}' should not be called in method style. Try {}(...);",
+ f.name, f.name
+ );
+ Err(LexError::ImproperSymbol(f.name.to_string(), err_msg).into_err(func_pos))
+ }
+ // lhs.func!(...)
+ (.., Expr::FnCall(f, func_pos)) if f.capture_parent_scope => {
+ Err(PERR::MalformedCapture(
+ "method-call style does not support running within the caller's scope".into(),
+ )
+ .into_err(func_pos))
+ }
+ // lhs.func(...)
+ (lhs, Expr::FnCall(mut f, func_pos)) => {
+ // Recalculate hash
+ let args_len = f.args.len() + 1;
+ f.hashes = if is_valid_function_name(&f.name) {
+ #[cfg(not(feature = "no_function"))]
+ {
+ FnCallHashes::from_script_and_native(
+ calc_fn_hash(None, &f.name, args_len - 1),
+ calc_fn_hash(None, &f.name, args_len),
+ )
+ }
+ #[cfg(feature = "no_function")]
+ {
+ FnCallHashes::from_native_only(calc_fn_hash(None, &f.name, args_len))
+ }
+ } else {
+ FnCallHashes::from_native_only(calc_fn_hash(None, &f.name, args_len))
+ };
+
+ let rhs = Expr::MethodCall(f, func_pos);
+ Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), op_flags, op_pos))
+ }
+ // lhs.dot_lhs.dot_rhs or lhs.dot_lhs[idx_rhs]
+ (lhs, rhs @ (Expr::Dot(..) | Expr::Index(..))) => {
+ let (x, options, pos, is_dot) = match rhs {
+ Expr::Dot(x, options, pos) => (x, options, pos, true),
+ Expr::Index(x, options, pos) => (x, options, pos, false),
+ expr => unreachable!("Expr::Dot or Expr::Index expected but gets {:?}", expr),
+ };
+
+ match x.lhs {
+ // lhs.module::id.dot_rhs or lhs.module::id[idx_rhs] - syntax error
+ #[cfg(not(feature = "no_module"))]
+ Expr::Variable(x, ..) if !x.1.is_empty() => unreachable!("lhs.ns::id..."),
+ // lhs.module::func().dot_rhs or lhs.module::func()[idx_rhs] - syntax error
+ #[cfg(not(feature = "no_module"))]
+ Expr::FnCall(f, ..) if f.is_qualified() => {
+ unreachable!("lhs.ns::func()...")
+ }
+ // lhs.id.dot_rhs or lhs.id[idx_rhs]
+ Expr::Variable(..) | Expr::Property(..) => {
+ let new_binary = BinaryExpr {
+ lhs: x.lhs.into_property(state),
+ rhs: x.rhs,
+ }
+ .into();
+
+ let rhs = if is_dot {
+ Expr::Dot(new_binary, options, pos)
+ } else {
+ Expr::Index(new_binary, options, pos)
+ };
+ Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), op_flags, op_pos))
+ }
+ // lhs.func().dot_rhs or lhs.func()[idx_rhs]
+ Expr::FnCall(mut f, func_pos) => {
+ // Recalculate hash
+ let args_len = f.args.len() + 1;
+ f.hashes = if is_valid_function_name(&f.name) {
+ #[cfg(not(feature = "no_function"))]
+ {
+ FnCallHashes::from_script_and_native(
+ calc_fn_hash(None, &f.name, args_len - 1),
+ calc_fn_hash(None, &f.name, args_len),
+ )
+ }
+ #[cfg(feature = "no_function")]
+ {
+ FnCallHashes::from_native_only(calc_fn_hash(
+ None, &f.name, args_len,
+ ))
+ }
+ } else {
+ FnCallHashes::from_native_only(calc_fn_hash(None, &f.name, args_len))
+ };
+
+ let new_lhs = BinaryExpr {
+ lhs: Expr::MethodCall(f, func_pos),
+ rhs: x.rhs,
+ }
+ .into();
+
+ let rhs = if is_dot {
+ Expr::Dot(new_lhs, options, pos)
+ } else {
+ Expr::Index(new_lhs, options, pos)
+ };
+ Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), op_flags, op_pos))
+ }
+ expr => unreachable!("invalid dot expression: {:?}", expr),
+ }
+ }
+ // lhs.rhs
+ (.., rhs) => Err(PERR::PropertyExpected.into_err(rhs.start_position())),
+ }
+ }
+
+ /// Parse a binary expression (if any).
+ fn parse_binary_op(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ parent_precedence: Option<Precedence>,
+ lhs: Expr,
+ ) -> ParseResult<Expr> {
+ let mut settings = settings;
+ settings.pos = lhs.position();
+
+ let mut root = lhs;
+
+ loop {
+ let (current_op, current_pos) = input.peek().expect(NEVER_ENDS);
+
+ if !(state.expr_filter)(current_op) {
+ return Ok(root);
+ }
+
+ let precedence = match current_op {
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Token::Custom(c) => self
+ .custom_keywords
+ .as_ref()
+ .and_then(|m| m.get(&**c))
+ .copied()
+ .ok_or_else(|| PERR::Reserved(c.to_string()).into_err(*current_pos))?,
+ Token::Reserved(c) if !is_valid_identifier(c) => {
+ return Err(PERR::UnknownOperator(c.to_string()).into_err(*current_pos))
+ }
+ _ => current_op.precedence(),
+ };
+ let bind_right = current_op.is_bind_right();
+
+ // Bind left to the parent lhs expression if precedence is higher
+ // If same precedence, then check if the operator binds right
+ if precedence < parent_precedence || (precedence == parent_precedence && !bind_right) {
+ return Ok(root);
+ }
+
+ let (op_token, pos) = input.next().expect(NEVER_ENDS);
+
+ let rhs = self.parse_unary(input, state, lib, settings)?;
+
+ let (next_op, next_pos) = input.peek().expect(NEVER_ENDS);
+ let next_precedence = match next_op {
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Token::Custom(c) => self
+ .custom_keywords
+ .as_ref()
+ .and_then(|m| m.get(&**c))
+ .copied()
+ .ok_or_else(|| PERR::Reserved(c.to_string()).into_err(*next_pos))?,
+ Token::Reserved(c) if !is_valid_identifier(c) => {
+ return Err(PERR::UnknownOperator(c.to_string()).into_err(*next_pos))
+ }
+ _ => next_op.precedence(),
+ };
+
+ // Bind to right if the next operator has higher precedence
+ // If same precedence, then check if the operator binds right
+ let rhs =
+ if (precedence == next_precedence && bind_right) || precedence < next_precedence {
+ self.parse_binary_op(input, state, lib, settings, precedence, rhs)?
+ } else {
+ // Otherwise bind to left (even if next operator has the same precedence)
+ rhs
+ };
+
+ settings = settings.level_up()?;
+ settings.pos = pos;
+
+ let op = op_token.to_string();
+ let hash = calc_fn_hash(None, &op, 2);
+ let native_only = !is_valid_function_name(&op);
+
+ let mut args = FnArgsVec::new_const();
+ args.push(root);
+ args.push(rhs);
+ args.shrink_to_fit();
+
+ let mut op_base = FnCallExpr {
+ namespace: Namespace::NONE,
+ name: state.get_interned_string(&op),
+ hashes: FnCallHashes::from_native_only(hash),
+ args,
+ op_token: native_only.then(|| op_token.clone()),
+ capture_parent_scope: false,
+ };
+
+ root = match op_token {
+ // '!=' defaults to true when passed invalid operands
+ Token::NotEqualsTo => op_base.into_fn_call_expr(pos),
+
+ // Comparison operators default to false when passed invalid operands
+ Token::EqualsTo
+ | Token::LessThan
+ | Token::LessThanEqualsTo
+ | Token::GreaterThan
+ | Token::GreaterThanEqualsTo => {
+ let pos = op_base.args[0].start_position();
+ op_base.into_fn_call_expr(pos)
+ }
+
+ Token::Or => {
+ let rhs = op_base.args.pop().unwrap().ensure_bool_expr()?;
+ let lhs = op_base.args.pop().unwrap().ensure_bool_expr()?;
+ Expr::Or(BinaryExpr { lhs, rhs }.into(), pos)
+ }
+ Token::And => {
+ let rhs = op_base.args.pop().unwrap().ensure_bool_expr()?;
+ let lhs = op_base.args.pop().unwrap().ensure_bool_expr()?;
+ Expr::And(BinaryExpr { lhs, rhs }.into(), pos)
+ }
+ Token::DoubleQuestion => {
+ let rhs = op_base.args.pop().unwrap();
+ let lhs = op_base.args.pop().unwrap();
+ Expr::Coalesce(BinaryExpr { lhs, rhs }.into(), pos)
+ }
+ Token::In | Token::NotIn => {
+ // Swap the arguments
+ let lhs = op_base.args.remove(0);
+ let pos = lhs.start_position();
+ op_base.args.push(lhs);
+ op_base.args.shrink_to_fit();
+
+ // Convert into a call to `contains`
+ op_base.hashes = FnCallHashes::from_hash(calc_fn_hash(None, OP_CONTAINS, 2));
+ op_base.name = state.get_interned_string(OP_CONTAINS);
+ let fn_call = op_base.into_fn_call_expr(pos);
+
+ if op_token == Token::In {
+ fn_call
+ } else {
+ // Put a `!` call in front
+ let mut args = FnArgsVec::new_const();
+ args.push(fn_call);
+
+ let not_base = FnCallExpr {
+ namespace: Namespace::NONE,
+ name: state.get_interned_string(OP_NOT),
+ hashes: FnCallHashes::from_native_only(calc_fn_hash(None, OP_NOT, 1)),
+ args,
+ op_token: Some(Token::Bang),
+ capture_parent_scope: false,
+ };
+ not_base.into_fn_call_expr(pos)
+ }
+ }
+
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Token::Custom(s) if self.is_custom_keyword(s.as_str()) => {
+ op_base.hashes = if native_only {
+ FnCallHashes::from_native_only(calc_fn_hash(None, &s, 2))
+ } else {
+ FnCallHashes::from_hash(calc_fn_hash(None, &s, 2))
+ };
+ op_base.into_fn_call_expr(pos)
+ }
+
+ _ => {
+ let pos = op_base.args[0].start_position();
+ op_base.into_fn_call_expr(pos)
+ }
+ };
+ }
+ }
+
+ /// Parse a custom syntax.
+ #[cfg(not(feature = "no_custom_syntax"))]
+ fn parse_custom_syntax(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ key: impl Into<ImmutableString>,
+ syntax: &crate::api::custom_syntax::CustomSyntax,
+ pos: Position,
+ ) -> ParseResult<Expr> {
+ #[allow(clippy::wildcard_imports)]
+ use crate::api::custom_syntax::markers::*;
+
+ const KEYWORD_SEMICOLON: &str = Token::SemiColon.literal_syntax();
+ const KEYWORD_CLOSE_BRACE: &str = Token::RightBrace.literal_syntax();
+
+ let mut settings = settings;
+ let mut inputs = StaticVec::new_const();
+ let mut segments = StaticVec::new_const();
+ let mut tokens = StaticVec::new_const();
+
+ // Adjust the variables stack
+ if syntax.scope_may_be_changed {
+ // Add a barrier variable to the stack so earlier variables will not be matched.
+ // Variable searches stop at the first barrier.
+ let marker = state.get_interned_string(SCOPE_SEARCH_BARRIER_MARKER);
+ state
+ .stack
+ .get_or_insert_with(Default::default)
+ .push(marker, ());
+ }
+
+ let mut user_state = Dynamic::UNIT;
+ let parse_func = &*syntax.parse;
+ let mut required_token: ImmutableString = key.into();
+
+ tokens.push(required_token.clone());
+ segments.push(required_token.clone());
+
+ loop {
+ let (fwd_token, fwd_pos) = input.peek().expect(NEVER_ENDS);
+ settings.pos = *fwd_pos;
+ let settings = settings.level_up()?;
+
+ required_token = match parse_func(&segments, &fwd_token.to_string(), &mut user_state) {
+ Ok(Some(seg))
+ if seg.starts_with(CUSTOM_SYNTAX_MARKER_SYNTAX_VARIANT)
+ && seg.len() > CUSTOM_SYNTAX_MARKER_SYNTAX_VARIANT.len() =>
+ {
+ inputs.push(Expr::StringConstant(state.get_interned_string(seg), pos));
+ break;
+ }
+ Ok(Some(seg)) => seg,
+ Ok(None) => break,
+ Err(err) => return Err(err.0.into_err(settings.pos)),
+ };
+
+ match required_token.as_str() {
+ CUSTOM_SYNTAX_MARKER_IDENT => {
+ let (name, pos) = parse_var_name(input)?;
+ let name = state.get_interned_string(name);
+
+ let ns = Namespace::NONE;
+
+ segments.push(name.clone());
+ tokens.push(state.get_interned_string(CUSTOM_SYNTAX_MARKER_IDENT));
+ inputs.push(Expr::Variable((None, ns, 0, name).into(), None, pos));
+ }
+ CUSTOM_SYNTAX_MARKER_SYMBOL => {
+ let (symbol, pos) = match input.next().expect(NEVER_ENDS) {
+ // Standard symbol
+ (token, pos) if token.is_standard_symbol() => {
+ Ok((token.literal_syntax().into(), pos))
+ }
+ // Reserved symbol
+ (Token::Reserved(s), pos) if !is_valid_identifier(s.as_str()) => {
+ Ok((*s, pos))
+ }
+ // Bad symbol
+ (Token::LexError(err), pos) => Err(err.into_err(pos)),
+ // Not a symbol
+ (.., pos) => Err(PERR::MissingSymbol(String::new()).into_err(pos)),
+ }?;
+ let symbol = state.get_interned_string(symbol);
+ segments.push(symbol.clone());
+ tokens.push(state.get_interned_string(CUSTOM_SYNTAX_MARKER_SYMBOL));
+ inputs.push(Expr::StringConstant(symbol, pos));
+ }
+ CUSTOM_SYNTAX_MARKER_EXPR => {
+ inputs.push(self.parse_expr(input, state, lib, settings)?);
+ let keyword = state.get_interned_string(CUSTOM_SYNTAX_MARKER_EXPR);
+ segments.push(keyword.clone());
+ tokens.push(keyword);
+ }
+ CUSTOM_SYNTAX_MARKER_BLOCK => {
+ match self.parse_block(input, state, lib, settings)? {
+ block @ Stmt::Block(..) => {
+ inputs.push(Expr::Stmt(Box::new(block.into())));
+ let keyword = state.get_interned_string(CUSTOM_SYNTAX_MARKER_BLOCK);
+ segments.push(keyword.clone());
+ tokens.push(keyword);
+ }
+ stmt => unreachable!("Stmt::Block expected but gets {:?}", stmt),
+ }
+ }
+ CUSTOM_SYNTAX_MARKER_BOOL => match input.next().expect(NEVER_ENDS) {
+ (b @ (Token::True | Token::False), pos) => {
+ inputs.push(Expr::BoolConstant(b == Token::True, pos));
+ segments.push(state.get_interned_string(b.literal_syntax()));
+ tokens.push(state.get_interned_string(CUSTOM_SYNTAX_MARKER_BOOL));
+ }
+ (.., pos) => {
+ return Err(
+ PERR::MissingSymbol("Expecting 'true' or 'false'".into()).into_err(pos)
+ )
+ }
+ },
+ CUSTOM_SYNTAX_MARKER_INT => match input.next().expect(NEVER_ENDS) {
+ (Token::IntegerConstant(i), pos) => {
+ inputs.push(Expr::IntegerConstant(i, pos));
+ segments.push(i.to_string().into());
+ tokens.push(state.get_interned_string(CUSTOM_SYNTAX_MARKER_INT));
+ }
+ (.., pos) => {
+ return Err(
+ PERR::MissingSymbol("Expecting an integer number".into()).into_err(pos)
+ )
+ }
+ },
+ #[cfg(not(feature = "no_float"))]
+ CUSTOM_SYNTAX_MARKER_FLOAT => match input.next().expect(NEVER_ENDS) {
+ (Token::FloatConstant(f), pos) => {
+ inputs.push(Expr::FloatConstant(f, pos));
+ segments.push(f.to_string().into());
+ tokens.push(state.get_interned_string(CUSTOM_SYNTAX_MARKER_FLOAT));
+ }
+ (.., pos) => {
+ return Err(
+ PERR::MissingSymbol("Expecting a floating-point number".into())
+ .into_err(pos),
+ )
+ }
+ },
+ CUSTOM_SYNTAX_MARKER_STRING => match input.next().expect(NEVER_ENDS) {
+ (Token::StringConstant(s), pos) => {
+ let s = state.get_interned_string(*s);
+ inputs.push(Expr::StringConstant(s.clone(), pos));
+ segments.push(s);
+ tokens.push(state.get_interned_string(CUSTOM_SYNTAX_MARKER_STRING));
+ }
+ (.., pos) => {
+ return Err(PERR::MissingSymbol("Expecting a string".into()).into_err(pos))
+ }
+ },
+ s => match input.next().expect(NEVER_ENDS) {
+ (Token::LexError(err), pos) => return Err(err.into_err(pos)),
+ (Token::Identifier(t) | Token::Reserved(t) | Token::Custom(t), ..)
+ if *t == s =>
+ {
+ segments.push(required_token.clone());
+ tokens.push(required_token.clone());
+ }
+ (t, ..) if t.is_literal() && t.literal_syntax() == s => {
+ segments.push(required_token.clone());
+ tokens.push(required_token.clone());
+ }
+ (.., pos) => {
+ return Err(PERR::MissingToken(
+ s.into(),
+ format!("for '{}' expression", segments[0]),
+ )
+ .into_err(pos))
+ }
+ },
+ }
+ }
+
+ inputs.shrink_to_fit();
+ tokens.shrink_to_fit();
+
+ let self_terminated = matches!(
+ required_token.as_str(),
+ // It is self-terminating if the last symbol is a block
+ CUSTOM_SYNTAX_MARKER_BLOCK |
+ // If the last symbol is `;` or `}`, it is self-terminating
+ KEYWORD_SEMICOLON | KEYWORD_CLOSE_BRACE
+ );
+
+ Ok(Expr::Custom(
+ crate::ast::CustomExpr {
+ inputs,
+ tokens,
+ state: user_state,
+ scope_may_be_changed: syntax.scope_may_be_changed,
+ self_terminated,
+ }
+ .into(),
+ pos,
+ ))
+ }
+
+ /// Parse an expression.
+ fn parse_expr(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Expr> {
+ let mut settings = settings;
+ settings.pos = input.peek().expect(NEVER_ENDS).1;
+
+ // Parse expression normally.
+ let precedence = Precedence::new(1);
+ let settings = settings.level_up()?;
+ let lhs = self.parse_unary(input, state, lib, settings)?;
+ self.parse_binary_op(input, state, lib, settings, precedence, lhs)
+ }
+
+ /// Parse an if statement.
+ fn parse_if(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Stmt> {
+ // if ...
+ let mut settings = settings.level_up()?;
+ settings.pos = eat_token(input, Token::If);
+
+ // if guard { if_body }
+ ensure_not_statement_expr(input, "a boolean")?;
+ let expr = self
+ .parse_expr(input, state, lib, settings)?
+ .ensure_bool_expr()?;
+ ensure_not_assignment(input)?;
+ let body = self.parse_block(input, state, lib, settings)?.into();
+
+ // if guard { if_body } else ...
+ let branch = if match_token(input, Token::Else).0 {
+ if let (Token::If, ..) = input.peek().expect(NEVER_ENDS) {
+ // if guard { if_body } else if ...
+ self.parse_if(input, state, lib, settings)?
+ } else {
+ // if guard { if_body } else { else-body }
+ self.parse_block(input, state, lib, settings)?
+ }
+ } else {
+ Stmt::Noop(Position::NONE)
+ }
+ .into();
+
+ Ok(Stmt::If(
+ FlowControl { expr, body, branch }.into(),
+ settings.pos,
+ ))
+ }
+
+ /// Parse a while loop.
+ fn parse_while_loop(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Stmt> {
+ let mut settings = settings.level_up()?;
+
+ // while|loops ...
+ let (expr, token_pos) = match input.next().expect(NEVER_ENDS) {
+ (Token::While, pos) => {
+ ensure_not_statement_expr(input, "a boolean")?;
+ let expr = self
+ .parse_expr(input, state, lib, settings)?
+ .ensure_bool_expr()?;
+ ensure_not_assignment(input)?;
+ (expr, pos)
+ }
+ (Token::Loop, pos) => (Expr::Unit(Position::NONE), pos),
+ token => unreachable!("Token::While or Token::Loop expected but gets {:?}", token),
+ };
+ settings.pos = token_pos;
+ settings.flags |= ParseSettingFlags::BREAKABLE;
+
+ let body = self.parse_block(input, state, lib, settings)?.into();
+ let branch = StmtBlock::NONE;
+
+ Ok(Stmt::While(
+ FlowControl { expr, body, branch }.into(),
+ settings.pos,
+ ))
+ }
+
+ /// Parse a do loop.
+ fn parse_do(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Stmt> {
+ // do ...
+ let mut settings = settings.level_up()?;
+ let orig_breakable = settings.flags.contains(ParseSettingFlags::BREAKABLE);
+ settings.flags |= ParseSettingFlags::BREAKABLE;
+
+ settings.pos = eat_token(input, Token::Do);
+
+ // do { body } [while|until] guard
+
+ let body = self.parse_block(input, state, lib, settings)?.into();
+
+ let negated = match input.next().expect(NEVER_ENDS) {
+ (Token::While, ..) => ASTFlags::empty(),
+ (Token::Until, ..) => ASTFlags::NEGATED,
+ (.., pos) => {
+ return Err(
+ PERR::MissingToken(Token::While.into(), "for the do statement".into())
+ .into_err(pos),
+ )
+ }
+ };
+
+ if !orig_breakable {
+ settings.flags.remove(ParseSettingFlags::BREAKABLE);
+ }
+
+ ensure_not_statement_expr(input, "a boolean")?;
+ let expr = self
+ .parse_expr(input, state, lib, settings)?
+ .ensure_bool_expr()?;
+ ensure_not_assignment(input)?;
+
+ let branch = StmtBlock::NONE;
+
+ Ok(Stmt::Do(
+ FlowControl { expr, body, branch }.into(),
+ negated,
+ settings.pos,
+ ))
+ }
+
+ /// Parse a for loop.
+ fn parse_for(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Stmt> {
+ // for ...
+ let mut settings = settings.level_up()?;
+ settings.pos = eat_token(input, Token::For);
+
+ // for name ...
+ let (name, name_pos, counter_name, counter_pos) = if match_token(input, Token::LeftParen).0
+ {
+ // ( name, counter )
+ let (name, name_pos) = parse_var_name(input)?;
+ let (has_comma, pos) = match_token(input, Token::Comma);
+ if !has_comma {
+ return Err(PERR::MissingToken(
+ Token::Comma.into(),
+ "after the iteration variable name".into(),
+ )
+ .into_err(pos));
+ }
+ let (counter_name, counter_pos) = parse_var_name(input)?;
+
+ if counter_name == name {
+ return Err(PERR::DuplicatedVariable(counter_name.into()).into_err(counter_pos));
+ }
+
+ let (has_close_paren, pos) = match_token(input, Token::RightParen);
+ if !has_close_paren {
+ return Err(PERR::MissingToken(
+ Token::RightParen.into(),
+ "to close the iteration variable".into(),
+ )
+ .into_err(pos));
+ }
+ (name, name_pos, counter_name, counter_pos)
+ } else {
+ // name
+ let (name, name_pos) = parse_var_name(input)?;
+ (name, name_pos, Identifier::new_const(), Position::NONE)
+ };
+
+ // for name in ...
+ match input.next().expect(NEVER_ENDS) {
+ (Token::In, ..) => (),
+ (Token::LexError(err), pos) => return Err(err.into_err(pos)),
+ (.., pos) => {
+ return Err(PERR::MissingToken(
+ Token::In.into(),
+ "after the iteration variable".into(),
+ )
+ .into_err(pos))
+ }
+ }
+
+ // for name in expr { body }
+ ensure_not_statement_expr(input, "a boolean")?;
+ let expr = self
+ .parse_expr(input, state, lib, settings)?
+ .ensure_iterable()?;
+
+ let counter_var = Ident {
+ name: state.get_interned_string(counter_name),
+ pos: counter_pos,
+ };
+
+ let loop_var = Ident {
+ name: state.get_interned_string(name),
+ pos: name_pos,
+ };
+
+ let prev_stack_len = {
+ let stack = state.stack.get_or_insert_with(Default::default);
+
+ let prev_stack_len = stack.len();
+
+ if !counter_var.name.is_empty() {
+ stack.push(counter_var.name.clone(), ());
+ }
+ stack.push(&loop_var.name, ());
+
+ prev_stack_len
+ };
+
+ settings.flags |= ParseSettingFlags::BREAKABLE;
+ let body = self.parse_block(input, state, lib, settings)?.into();
+
+ state.stack.as_mut().unwrap().rewind(prev_stack_len);
+
+ let branch = StmtBlock::NONE;
+
+ Ok(Stmt::For(
+ Box::new((loop_var, counter_var, FlowControl { expr, body, branch })),
+ settings.pos,
+ ))
+ }
+
+ /// Parse a variable definition statement.
+ fn parse_let(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ access: AccessMode,
+ is_export: bool,
+ ) -> ParseResult<Stmt> {
+ // let/const... (specified in `var_type`)
+ let mut settings = settings;
+ settings.pos = input.next().expect(NEVER_ENDS).1;
+
+ // let name ...
+ let (name, pos) = parse_var_name(input)?;
+
+ {
+ let stack = state.stack.get_or_insert_with(Default::default);
+
+ if !self.allow_shadowing() && stack.iter().any(|(v, ..)| v == name) {
+ return Err(PERR::VariableExists(name.into()).into_err(pos));
+ }
+
+ if let Some(ref filter) = self.def_var_filter {
+ let will_shadow = stack.iter().any(|(v, ..)| v == name);
+
+ let global = state
+ .global
+ .get_or_insert_with(|| GlobalRuntimeState::new(self).into());
+
+ global.level = settings.level;
+ let is_const = access == AccessMode::ReadOnly;
+ let info = VarDefInfo {
+ name: &name,
+ is_const,
+ nesting_level: settings.level,
+ will_shadow,
+ };
+ let caches = &mut Caches::new();
+ let context = EvalContext::new(self, global, caches, stack, None);
+
+ match filter(false, info, context) {
+ Ok(true) => (),
+ Ok(false) => return Err(PERR::ForbiddenVariable(name.into()).into_err(pos)),
+ Err(err) => match *err {
+ EvalAltResult::ErrorParsing(e, pos) => return Err(e.into_err(pos)),
+ _ => return Err(PERR::ForbiddenVariable(name.into()).into_err(pos)),
+ },
+ }
+ }
+ }
+
+ let name = state.get_interned_string(name);
+
+ // let name = ...
+ let expr = if match_token(input, Token::Equals).0 {
+ // let name = expr
+ self.parse_expr(input, state, lib, settings.level_up()?)?
+ } else {
+ Expr::Unit(Position::NONE)
+ };
+
+ let export = if is_export {
+ ASTFlags::EXPORTED
+ } else {
+ ASTFlags::empty()
+ };
+
+ let (existing, hit_barrier) = state.find_var(&name);
+
+ let stack = state.stack.as_mut().unwrap();
+
+ let existing = if !hit_barrier && existing > 0 {
+ match stack.len() - existing {
+ // Variable has been aliased
+ #[cfg(not(feature = "no_module"))]
+ offset if !stack.get_entry_by_index(offset).2.is_empty() => None,
+ // Defined in parent block
+ offset if offset < state.block_stack_len => None,
+ offset => Some(offset),
+ }
+ } else {
+ None
+ };
+
+ let idx = if let Some(n) = existing {
+ stack.get_mut_by_index(n).set_access_mode(access);
+ Some(NonZeroUsize::new(stack.len() - n).unwrap())
+ } else {
+ stack.push_entry(name.as_str(), access, Dynamic::UNIT);
+ None
+ };
+
+ #[cfg(not(feature = "no_module"))]
+ if is_export {
+ stack.add_alias_by_index(stack.len() - 1, name.clone());
+ }
+
+ let var_def = (Ident { name, pos }, expr, idx).into();
+
+ Ok(match access {
+ // let name = expr
+ AccessMode::ReadWrite => Stmt::Var(var_def, export, settings.pos),
+ // const name = { expr:constant }
+ AccessMode::ReadOnly => Stmt::Var(var_def, ASTFlags::CONSTANT | export, settings.pos),
+ })
+ }
+
+ /// Parse an import statement.
+ #[cfg(not(feature = "no_module"))]
+ fn parse_import(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Stmt> {
+ // import ...
+ let mut settings = settings.level_up()?;
+ settings.pos = eat_token(input, Token::Import);
+
+ // import expr ...
+ let expr = self.parse_expr(input, state, lib, settings)?;
+
+ let export = if match_token(input, Token::As).0 {
+ // import expr as name ...
+ let (name, pos) = parse_var_name(input)?;
+ Ident {
+ name: state.get_interned_string(name),
+ pos,
+ }
+ } else {
+ // import expr;
+ Ident {
+ name: state.get_interned_string(""),
+ pos: Position::NONE,
+ }
+ };
+
+ state
+ .imports
+ .get_or_insert_with(Default::default)
+ .push(export.name.clone());
+
+ Ok(Stmt::Import((expr, export).into(), settings.pos))
+ }
+
+ /// Parse an export statement.
+ #[cfg(not(feature = "no_module"))]
+ fn parse_export(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Stmt> {
+ let mut settings = settings;
+ settings.pos = eat_token(input, Token::Export);
+
+ match input.peek().expect(NEVER_ENDS) {
+ (Token::Let, pos) => {
+ let pos = *pos;
+ let settings = settings.level_up()?;
+ let mut stmt =
+ self.parse_let(input, state, lib, settings, AccessMode::ReadWrite, true)?;
+ stmt.set_position(pos);
+ return Ok(stmt);
+ }
+ (Token::Const, pos) => {
+ let pos = *pos;
+ let settings = settings.level_up()?;
+ let mut stmt =
+ self.parse_let(input, state, lib, settings, AccessMode::ReadOnly, true)?;
+ stmt.set_position(pos);
+ return Ok(stmt);
+ }
+ _ => (),
+ }
+
+ let (id, id_pos) = parse_var_name(input)?;
+
+ let (alias, alias_pos) = if match_token(input, Token::As).0 {
+ parse_var_name(input).map(|(name, pos)| (state.get_interned_string(name), pos))?
+ } else {
+ (state.get_interned_string(""), Position::NONE)
+ };
+
+ let (existing, hit_barrier) = state.find_var(&id);
+
+ if !hit_barrier && existing > 0 {
+ let stack = state.stack.as_mut().unwrap();
+ stack.add_alias_by_index(stack.len() - existing, alias.clone());
+ }
+
+ let export = (
+ Ident {
+ name: state.get_interned_string(id),
+ pos: id_pos,
+ },
+ Ident {
+ name: alias,
+ pos: alias_pos,
+ },
+ );
+
+ Ok(Stmt::Export(export.into(), settings.pos))
+ }
+
+ /// Parse a statement block.
+ fn parse_block(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Stmt> {
+ // Must start with {
+ let mut settings = settings.level_up()?;
+ settings.pos = match input.next().expect(NEVER_ENDS) {
+ (Token::LeftBrace, pos) => pos,
+ (Token::LexError(err), pos) => return Err(err.into_err(pos)),
+ (.., pos) => {
+ return Err(PERR::MissingToken(
+ Token::LeftBrace.into(),
+ "to start a statement block".into(),
+ )
+ .into_err(pos))
+ }
+ };
+
+ let mut statements = StaticVec::new_const();
+
+ if settings.has_flag(ParseSettingFlags::DISALLOW_STATEMENTS_IN_BLOCKS) {
+ let stmt = self.parse_expr_stmt(input, state, lib, settings)?;
+ statements.push(stmt);
+
+ // Must end with }
+ return match input.next().expect(NEVER_ENDS) {
+ (Token::RightBrace, pos) => Ok((statements, settings.pos, pos).into()),
+ (Token::LexError(err), pos) => Err(err.into_err(pos)),
+ (.., pos) => Err(PERR::MissingToken(
+ Token::LeftBrace.into(),
+ "to start a statement block".into(),
+ )
+ .into_err(pos)),
+ };
+ }
+
+ let prev_entry_stack_len = state.block_stack_len;
+ state.block_stack_len = state.stack.as_ref().map_or(0, Scope::len);
+
+ #[cfg(not(feature = "no_module"))]
+ let orig_imports_len = state.imports.as_deref().map_or(0, StaticVec::len);
+
+ let end_pos = loop {
+ // Terminated?
+ match input.peek().expect(NEVER_ENDS) {
+ (Token::RightBrace, ..) => break eat_token(input, Token::RightBrace),
+ (Token::EOF, pos) => {
+ return Err(PERR::MissingToken(
+ Token::RightBrace.into(),
+ "to terminate this block".into(),
+ )
+ .into_err(*pos));
+ }
+ _ => (),
+ }
+
+ // Parse statements inside the block
+ settings.flags.remove(ParseSettingFlags::GLOBAL_LEVEL);
+
+ let stmt = self.parse_stmt(input, state, lib, settings)?;
+
+ if stmt.is_noop() {
+ continue;
+ }
+
+ // See if it needs a terminating semicolon
+ let need_semicolon = !stmt.is_self_terminated();
+
+ statements.push(stmt);
+
+ match input.peek().expect(NEVER_ENDS) {
+ // { ... stmt }
+ (Token::RightBrace, ..) => break eat_token(input, Token::RightBrace),
+ // { ... stmt;
+ (Token::SemiColon, ..) if need_semicolon => {
+ eat_token(input, Token::SemiColon);
+ }
+ // { ... { stmt } ;
+ (Token::SemiColon, ..) if !need_semicolon => {
+ eat_token(input, Token::SemiColon);
+ }
+ // { ... { stmt } ???
+ _ if !need_semicolon => (),
+ // { ... stmt <error>
+ (Token::LexError(err), err_pos) => return Err(err.clone().into_err(*err_pos)),
+ // { ... stmt ???
+ (.., pos) => {
+ // Semicolons are not optional between statements
+ return Err(PERR::MissingToken(
+ Token::SemiColon.into(),
+ "to terminate this statement".into(),
+ )
+ .into_err(*pos));
+ }
+ }
+ };
+
+ if let Some(ref mut s) = state.stack {
+ s.rewind(state.block_stack_len);
+ }
+ state.block_stack_len = prev_entry_stack_len;
+
+ #[cfg(not(feature = "no_module"))]
+ if let Some(ref mut imports) = state.imports {
+ imports.truncate(orig_imports_len);
+ }
+
+ Ok((statements, settings.pos, end_pos).into())
+ }
+
+ /// Parse an expression as a statement.
+ fn parse_expr_stmt(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Stmt> {
+ let mut settings = settings;
+ settings.pos = input.peek().expect(NEVER_ENDS).1;
+
+ let expr = self.parse_expr(input, state, lib, settings)?;
+
+ let (op, pos) = match input.peek().expect(NEVER_ENDS) {
+ // var = ...
+ (Token::Equals, ..) => (None, eat_token(input, Token::Equals)),
+ // var op= ...
+ (token, ..) if token.is_op_assignment() => input
+ .next()
+ .map(|(op, pos)| (Some(op), pos))
+ .expect(NEVER_ENDS),
+ // Not op-assignment
+ _ => return Ok(Stmt::Expr(expr.into())),
+ };
+
+ settings.pos = pos;
+
+ let rhs = self.parse_expr(input, state, lib, settings)?;
+
+ Self::make_assignment_stmt(op, state, expr, rhs, pos)
+ }
+
+ /// Parse a single statement.
+ fn parse_stmt(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Stmt> {
+ use AccessMode::{ReadOnly, ReadWrite};
+
+ let mut settings = settings;
+
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(feature = "metadata")]
+ let comments = {
+ let mut comments = StaticVec::<Identifier>::new();
+ let mut comments_pos = Position::NONE;
+ let mut buf = Identifier::new();
+
+ // Handle doc-comments.
+ while let (Token::Comment(ref comment), pos) = input.peek().expect(NEVER_ENDS) {
+ if comments_pos.is_none() {
+ comments_pos = *pos;
+ }
+
+ if !crate::tokenizer::is_doc_comment(comment) {
+ unreachable!("doc-comment expected but gets {:?}", comment);
+ }
+
+ if !settings.has_flag(ParseSettingFlags::GLOBAL_LEVEL) {
+ return Err(PERR::WrongDocComment.into_err(comments_pos));
+ }
+
+ match input.next().expect(NEVER_ENDS) {
+ (Token::Comment(comment), pos) => {
+ if comment.contains('\n') {
+ // Assume block comment
+ if !buf.is_empty() {
+ comments.push(buf.clone());
+ buf.clear();
+ }
+ let c =
+ unindent_block_comment(*comment, pos.position().unwrap_or(1) - 1);
+ comments.push(c.into());
+ } else {
+ if !buf.is_empty() {
+ buf.push('\n');
+ }
+ buf.push_str(&comment);
+ }
+
+ match input.peek().expect(NEVER_ENDS) {
+ (Token::Fn | Token::Private, ..) => break,
+ (Token::Comment(..), ..) => (),
+ _ => return Err(PERR::WrongDocComment.into_err(comments_pos)),
+ }
+ }
+ (token, ..) => unreachable!("Token::Comment expected but gets {:?}", token),
+ }
+ }
+
+ if !buf.is_empty() {
+ comments.push(buf);
+ }
+
+ comments
+ };
+
+ let (token, token_pos) = match input.peek().expect(NEVER_ENDS) {
+ (Token::EOF, pos) => return Ok(Stmt::Noop(*pos)),
+ (x, pos) => (x, *pos),
+ };
+
+ settings.pos = token_pos;
+
+ match token {
+ // ; - empty statement
+ Token::SemiColon => {
+ eat_token(input, Token::SemiColon);
+ Ok(Stmt::Noop(token_pos))
+ }
+
+ // { - statements block
+ Token::LeftBrace => Ok(self.parse_block(input, state, lib, settings.level_up()?)?),
+
+ // fn ...
+ #[cfg(not(feature = "no_function"))]
+ Token::Fn if !settings.has_flag(ParseSettingFlags::GLOBAL_LEVEL) => {
+ Err(PERR::WrongFnDefinition.into_err(token_pos))
+ }
+
+ #[cfg(not(feature = "no_function"))]
+ Token::Fn | Token::Private => {
+ let access = if matches!(token, Token::Private) {
+ eat_token(input, Token::Private);
+ crate::FnAccess::Private
+ } else {
+ crate::FnAccess::Public
+ };
+
+ match input.next().expect(NEVER_ENDS) {
+ (Token::Fn, pos) => {
+ // Build new parse state
+ let new_state = &mut ParseState::new(
+ state.external_constants,
+ state.interned_strings,
+ state.tokenizer_control.clone(),
+ );
+
+ #[cfg(not(feature = "no_module"))]
+ {
+ // Do not allow storing an index to a globally-imported module
+ // just in case the function is separated from this `AST`.
+ //
+ // Keep them in `global_imports` instead so that strict variables
+ // mode will not complain.
+ new_state.global_imports.clone_from(&state.global_imports);
+ new_state
+ .global_imports
+ .get_or_insert_with(Default::default)
+ .extend(state.imports.as_deref().into_iter().flatten().cloned());
+ }
+
+ // Brand new options
+ let options = self.options | (settings.options & LangOptions::STRICT_VAR);
+
+ // Brand new flags, turn on function scope
+ let flags = ParseSettingFlags::FN_SCOPE
+ | (settings.flags
+ & ParseSettingFlags::DISALLOW_UNQUOTED_MAP_PROPERTIES);
+
+ let new_settings = ParseSettings {
+ flags,
+ level: 0,
+ options,
+ pos,
+ #[cfg(not(feature = "unchecked"))]
+ max_expr_depth: self.max_function_expr_depth(),
+ };
+
+ let f = self.parse_fn(
+ input,
+ new_state,
+ lib,
+ new_settings,
+ access,
+ #[cfg(feature = "metadata")]
+ comments,
+ )?;
+
+ let hash = calc_fn_hash(None, &f.name, f.params.len());
+
+ #[cfg(not(feature = "no_object"))]
+ let hash = if let Some(ref this_type) = f.this_type {
+ crate::calc_typed_method_hash(hash, this_type)
+ } else {
+ hash
+ };
+
+ if !lib.is_empty() && lib.contains_key(&hash) {
+ return Err(PERR::FnDuplicatedDefinition(
+ f.name.to_string(),
+ f.params.len(),
+ )
+ .into_err(pos));
+ }
+
+ lib.insert(hash, f.into());
+
+ Ok(Stmt::Noop(pos))
+ }
+
+ (.., pos) => Err(PERR::MissingToken(
+ Token::Fn.into(),
+ format!("following '{}'", Token::Private),
+ )
+ .into_err(pos)),
+ }
+ }
+
+ Token::If => self.parse_if(input, state, lib, settings.level_up()?),
+ Token::Switch => self.parse_switch(input, state, lib, settings.level_up()?),
+ Token::While | Token::Loop if self.allow_looping() => {
+ self.parse_while_loop(input, state, lib, settings.level_up()?)
+ }
+ Token::Do if self.allow_looping() => {
+ self.parse_do(input, state, lib, settings.level_up()?)
+ }
+ Token::For if self.allow_looping() => {
+ self.parse_for(input, state, lib, settings.level_up()?)
+ }
+
+ Token::Continue
+ if self.allow_looping() && settings.has_flag(ParseSettingFlags::BREAKABLE) =>
+ {
+ let pos = eat_token(input, Token::Continue);
+ Ok(Stmt::BreakLoop(None, ASTFlags::empty(), pos))
+ }
+ Token::Break
+ if self.allow_looping() && settings.has_flag(ParseSettingFlags::BREAKABLE) =>
+ {
+ let pos = eat_token(input, Token::Break);
+
+ let expr = match input.peek().expect(NEVER_ENDS) {
+ // `break` at <EOF>
+ (Token::EOF, ..) => None,
+ // `break` at end of block
+ (Token::RightBrace, ..) => None,
+ // `break;`
+ (Token::SemiColon, ..) => None,
+ // `break` with expression
+ _ => Some(
+ self.parse_expr(input, state, lib, settings.level_up()?)?
+ .into(),
+ ),
+ };
+
+ Ok(Stmt::BreakLoop(expr, ASTFlags::BREAK, pos))
+ }
+ Token::Continue | Token::Break if self.allow_looping() => {
+ Err(PERR::LoopBreak.into_err(token_pos))
+ }
+
+ Token::Return | Token::Throw => {
+ let (return_type, token_pos) = input
+ .next()
+ .map(|(token, pos)| {
+ let flags = match token {
+ Token::Return => ASTFlags::empty(),
+ Token::Throw => ASTFlags::BREAK,
+ token => unreachable!(
+ "Token::Return or Token::Throw expected but gets {:?}",
+ token
+ ),
+ };
+ (flags, pos)
+ })
+ .expect(NEVER_ENDS);
+
+ match input.peek().expect(NEVER_ENDS) {
+ // `return`/`throw` at <EOF>
+ (Token::EOF, ..) => Ok(Stmt::Return(None, return_type, token_pos)),
+ // `return`/`throw` at end of block
+ (Token::RightBrace, ..)
+ if !settings.has_flag(ParseSettingFlags::GLOBAL_LEVEL) =>
+ {
+ Ok(Stmt::Return(None, return_type, token_pos))
+ }
+ // `return;` or `throw;`
+ (Token::SemiColon, ..) => Ok(Stmt::Return(None, return_type, token_pos)),
+ // `return` or `throw` with expression
+ _ => {
+ let expr = self.parse_expr(input, state, lib, settings.level_up()?)?;
+ Ok(Stmt::Return(Some(expr.into()), return_type, token_pos))
+ }
+ }
+ }
+
+ Token::Try => self.parse_try_catch(input, state, lib, settings.level_up()?),
+
+ Token::Let => self.parse_let(input, state, lib, settings.level_up()?, ReadWrite, false),
+ Token::Const => {
+ self.parse_let(input, state, lib, settings.level_up()?, ReadOnly, false)
+ }
+
+ #[cfg(not(feature = "no_module"))]
+ Token::Import => self.parse_import(input, state, lib, settings.level_up()?),
+
+ #[cfg(not(feature = "no_module"))]
+ Token::Export if !settings.has_flag(ParseSettingFlags::GLOBAL_LEVEL) => {
+ Err(PERR::WrongExport.into_err(token_pos))
+ }
+
+ #[cfg(not(feature = "no_module"))]
+ Token::Export => self.parse_export(input, state, lib, settings.level_up()?),
+
+ _ => self.parse_expr_stmt(input, state, lib, settings.level_up()?),
+ }
+ }
+
+ /// Parse a try/catch statement.
+ fn parse_try_catch(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ ) -> ParseResult<Stmt> {
+ // try ...
+ let mut settings = settings.level_up()?;
+ settings.pos = eat_token(input, Token::Try);
+
+ // try { try_block }
+ let body = self.parse_block(input, state, lib, settings)?.into();
+
+ // try { try_block } catch
+ let (matched, catch_pos) = match_token(input, Token::Catch);
+
+ if !matched {
+ return Err(
+ PERR::MissingToken(Token::Catch.into(), "for the 'try' statement".into())
+ .into_err(catch_pos),
+ );
+ }
+
+ // try { try_block } catch (
+ let catch_var = if match_token(input, Token::LeftParen).0 {
+ let (name, pos) = parse_var_name(input)?;
+ let (matched, err_pos) = match_token(input, Token::RightParen);
+
+ if !matched {
+ return Err(PERR::MissingToken(
+ Token::RightParen.into(),
+ "to enclose the catch variable".into(),
+ )
+ .into_err(err_pos));
+ }
+
+ let name = state.get_interned_string(name);
+ state
+ .stack
+ .get_or_insert_with(Default::default)
+ .push(name.clone(), ());
+ Ident { name, pos }
+ } else {
+ Ident {
+ name: state.get_interned_string(""),
+ pos: Position::NONE,
+ }
+ };
+
+ // try { try_block } catch ( var ) { catch_block }
+ let branch = self.parse_block(input, state, lib, settings)?.into();
+
+ let expr = if catch_var.is_empty() {
+ Expr::Unit(catch_var.pos)
+ } else {
+ // Remove the error variable from the stack
+ state.stack.as_mut().unwrap().pop();
+
+ Expr::Variable(
+ (None, Namespace::default(), 0, catch_var.name).into(),
+ None,
+ catch_var.pos,
+ )
+ };
+
+ Ok(Stmt::TryCatch(
+ FlowControl { expr, body, branch }.into(),
+ settings.pos,
+ ))
+ }
+
+ /// Parse a function definition.
+ #[cfg(not(feature = "no_function"))]
+ fn parse_fn(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ access: crate::FnAccess,
+ #[cfg(feature = "metadata")] comments: impl IntoIterator<Item = Identifier>,
+ ) -> ParseResult<ScriptFnDef> {
+ let settings = settings.level_up()?;
+
+ let (token, pos) = input.next().expect(NEVER_ENDS);
+
+ // Parse type for `this` pointer
+ #[cfg(not(feature = "no_object"))]
+ let ((token, pos), this_type) = {
+ let (next_token, next_pos) = input.peek().expect(NEVER_ENDS);
+
+ match token {
+ Token::StringConstant(s) if next_token == &Token::Period => {
+ eat_token(input, Token::Period);
+ let s = match s.as_str() {
+ "int" => state.get_interned_string(std::any::type_name::<crate::INT>()),
+ #[cfg(not(feature = "no_float"))]
+ "float" => state.get_interned_string(std::any::type_name::<crate::FLOAT>()),
+ _ => state.get_interned_string(*s),
+ };
+ (input.next().expect(NEVER_ENDS), Some(s))
+ }
+ Token::StringConstant(..) => {
+ return Err(PERR::MissingToken(
+ Token::Period.into(),
+ "after the type name for 'this'".into(),
+ )
+ .into_err(*next_pos))
+ }
+ Token::Identifier(s) if next_token == &Token::Period => {
+ eat_token(input, Token::Period);
+ let s = match s.as_str() {
+ "int" => state.get_interned_string(std::any::type_name::<crate::INT>()),
+ #[cfg(not(feature = "no_float"))]
+ "float" => state.get_interned_string(std::any::type_name::<crate::FLOAT>()),
+ _ => state.get_interned_string(*s),
+ };
+ (input.next().expect(NEVER_ENDS), Some(s))
+ }
+ _ => ((token, pos), None),
+ }
+ };
+
+ let name = match token.into_function_name_for_override() {
+ Ok(r) => r,
+ Err(Token::Reserved(s)) => return Err(PERR::Reserved(s.to_string()).into_err(pos)),
+ Err(_) => return Err(PERR::FnMissingName.into_err(pos)),
+ };
+
+ let no_params = match input.peek().expect(NEVER_ENDS) {
+ (Token::LeftParen, ..) => {
+ eat_token(input, Token::LeftParen);
+ match_token(input, Token::RightParen).0
+ }
+ (Token::Unit, ..) => {
+ eat_token(input, Token::Unit);
+ true
+ }
+ (.., pos) => return Err(PERR::FnMissingParams(name.into()).into_err(*pos)),
+ };
+
+ let mut params = StaticVec::<(ImmutableString, _)>::new_const();
+
+ if !no_params {
+ let sep_err = format!("to separate the parameters of function '{name}'");
+
+ loop {
+ match input.next().expect(NEVER_ENDS) {
+ (Token::RightParen, ..) => break,
+ (Token::Identifier(s), pos) => {
+ if params.iter().any(|(p, _)| p.as_str() == *s) {
+ return Err(
+ PERR::FnDuplicatedParam(name.into(), s.to_string()).into_err(pos)
+ );
+ }
+
+ let s = state.get_interned_string(*s);
+ state
+ .stack
+ .get_or_insert_with(Default::default)
+ .push(s.clone(), ());
+ params.push((s, pos));
+ }
+ (Token::LexError(err), pos) => return Err(err.into_err(pos)),
+ (.., pos) => {
+ return Err(PERR::MissingToken(
+ Token::RightParen.into(),
+ format!("to close the parameters list of function '{name}'"),
+ )
+ .into_err(pos))
+ }
+ }
+
+ match input.next().expect(NEVER_ENDS) {
+ (Token::RightParen, ..) => break,
+ (Token::Comma, ..) => (),
+ (Token::LexError(err), pos) => return Err(err.into_err(pos)),
+ (.., pos) => {
+ return Err(PERR::MissingToken(Token::Comma.into(), sep_err).into_err(pos))
+ }
+ }
+ }
+ }
+
+ // Parse function body
+ let body = match input.peek().expect(NEVER_ENDS) {
+ (Token::LeftBrace, ..) => self.parse_block(input, state, lib, settings)?,
+ (.., pos) => return Err(PERR::FnMissingBody(name.into()).into_err(*pos)),
+ }
+ .into();
+
+ let mut params: crate::FnArgsVec<_> = params.into_iter().map(|(p, ..)| p).collect();
+ params.shrink_to_fit();
+
+ Ok(ScriptFnDef {
+ name: state.get_interned_string(name),
+ access,
+ #[cfg(not(feature = "no_object"))]
+ this_type,
+ params,
+ body,
+ #[cfg(feature = "metadata")]
+ comments: comments.into_iter().collect(),
+ })
+ }
+
+ /// Creates a curried expression from a list of external variables
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "no_closure"))]
+ fn make_curry_from_externals(
+ state: &mut ParseState,
+ parent: &mut ParseState,
+ lib: &FnLib,
+ fn_expr: Expr,
+ externals: crate::FnArgsVec<Ident>,
+ pos: Position,
+ ) -> Expr {
+ // If there are no captured variables, no need to curry
+ if externals.is_empty() {
+ return fn_expr;
+ }
+
+ let num_externals = externals.len();
+ let mut args = FnArgsVec::with_capacity(externals.len() + 1);
+
+ args.push(fn_expr);
+
+ args.extend(externals.iter().cloned().map(|Ident { name, pos }| {
+ let (index, is_func) = parent.access_var(&name, lib, pos);
+ let idx = match index {
+ #[allow(clippy::cast_possible_truncation)]
+ Some(n) if !is_func && n.get() <= u8::MAX as usize => NonZeroU8::new(n.get() as u8),
+ _ => None,
+ };
+ Expr::Variable((index, Namespace::default(), 0, name).into(), idx, pos)
+ }));
+
+ let expr = FnCallExpr {
+ namespace: Namespace::NONE,
+ name: state.get_interned_string(crate::engine::KEYWORD_FN_PTR_CURRY),
+ hashes: FnCallHashes::from_native_only(calc_fn_hash(
+ None,
+ crate::engine::KEYWORD_FN_PTR_CURRY,
+ num_externals + 1,
+ )),
+ args,
+ op_token: None,
+ capture_parent_scope: false,
+ }
+ .into_fn_call_expr(pos);
+
+ // Convert the entire expression into a statement block, then insert the relevant
+ // [`Share`][Stmt::Share] statements.
+ let mut statements = StaticVec::with_capacity(2);
+ statements.push(Stmt::Share(
+ externals
+ .into_iter()
+ .map(|var| {
+ let (index, _) = parent.access_var(&var.name, lib, var.pos);
+ (var, index)
+ })
+ .collect::<crate::FnArgsVec<_>>()
+ .into(),
+ ));
+ statements.push(Stmt::Expr(expr.into()));
+ Expr::Stmt(StmtBlock::new(statements, pos, Position::NONE).into())
+ }
+
+ /// Parse an anonymous function definition.
+ #[cfg(not(feature = "no_function"))]
+ fn parse_anon_fn(
+ &self,
+ input: &mut TokenStream,
+ state: &mut ParseState,
+ lib: &mut FnLib,
+ settings: ParseSettings,
+ _parent: &mut ParseState,
+ ) -> ParseResult<(Expr, Shared<ScriptFnDef>)> {
+ let settings = settings.level_up()?;
+ let mut params_list = StaticVec::<ImmutableString>::new_const();
+
+ if input.next().expect(NEVER_ENDS).0 != Token::Or && !match_token(input, Token::Pipe).0 {
+ loop {
+ match input.next().expect(NEVER_ENDS) {
+ (Token::Pipe, ..) => break,
+ (Token::Identifier(s), pos) => {
+ if params_list.iter().any(|p| p.as_str() == *s) {
+ return Err(
+ PERR::FnDuplicatedParam(String::new(), s.to_string()).into_err(pos)
+ );
+ }
+
+ let s = state.get_interned_string(*s);
+ state
+ .stack
+ .get_or_insert_with(Default::default)
+ .push(s.clone(), ());
+ params_list.push(s);
+ }
+ (Token::LexError(err), pos) => return Err(err.into_err(pos)),
+ (.., pos) => {
+ return Err(PERR::MissingToken(
+ Token::Pipe.into(),
+ "to close the parameters list of anonymous function or closure".into(),
+ )
+ .into_err(pos))
+ }
+ }
+
+ match input.next().expect(NEVER_ENDS) {
+ (Token::Pipe, ..) => break,
+ (Token::Comma, ..) => (),
+ (Token::LexError(err), pos) => return Err(err.into_err(pos)),
+ (.., pos) => {
+ return Err(PERR::MissingToken(
+ Token::Comma.into(),
+ "to separate the parameters of anonymous function".into(),
+ )
+ .into_err(pos))
+ }
+ }
+ }
+ }
+
+ // Parse function body
+ let body = self.parse_stmt(input, state, lib, settings)?;
+
+ // External variables may need to be processed in a consistent order,
+ // so extract them into a list.
+ #[cfg(not(feature = "no_closure"))]
+ let (mut params, externals) = if let Some(ref external_vars) = state.external_vars {
+ let externals: crate::FnArgsVec<_> = external_vars.iter().cloned().collect();
+
+ let mut params = crate::FnArgsVec::with_capacity(params_list.len() + externals.len());
+ params.extend(externals.iter().map(|Ident { name, .. }| name.clone()));
+
+ (params, externals)
+ } else {
+ (
+ crate::FnArgsVec::with_capacity(params_list.len()),
+ crate::FnArgsVec::new_const(),
+ )
+ };
+ #[cfg(feature = "no_closure")]
+ let mut params = crate::FnArgsVec::with_capacity(params_list.len());
+
+ params.append(&mut params_list);
+
+ // Create unique function name by hashing the script body plus the parameters.
+ let hasher = &mut get_hasher();
+ params.iter().for_each(|p| p.hash(hasher));
+ body.hash(hasher);
+ let hash = hasher.finish();
+ let fn_name = state.get_interned_string(make_anonymous_fn(hash));
+
+ // Define the function
+ let script = Shared::new(ScriptFnDef {
+ name: fn_name.clone(),
+ access: crate::FnAccess::Public,
+ #[cfg(not(feature = "no_object"))]
+ this_type: None,
+ params,
+ body: body.into(),
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(feature = "metadata")]
+ comments: Box::default(),
+ });
+
+ let mut fn_ptr = crate::FnPtr::new_unchecked(fn_name, StaticVec::new_const());
+ fn_ptr.set_fn_def(Some(script.clone()));
+ let expr = Expr::DynamicConstant(Box::new(fn_ptr.into()), settings.pos);
+
+ #[cfg(not(feature = "no_closure"))]
+ let expr =
+ Self::make_curry_from_externals(state, _parent, lib, expr, externals, settings.pos);
+
+ Ok((expr, script))
+ }
+
+ /// Parse a global level expression.
+ pub(crate) fn parse_global_expr(
+ &self,
+ mut input: TokenStream,
+ state: &mut ParseState,
+ process_settings: impl FnOnce(&mut ParseSettings),
+ _optimization_level: OptimizationLevel,
+ ) -> ParseResult<AST> {
+ let mut functions = StraightHashMap::default();
+
+ let options = self.options & !LangOptions::STMT_EXPR & !LangOptions::LOOP_EXPR;
+
+ let mut settings = ParseSettings {
+ level: 0,
+ flags: ParseSettingFlags::GLOBAL_LEVEL
+ | ParseSettingFlags::DISALLOW_STATEMENTS_IN_BLOCKS,
+ options,
+ pos: Position::START,
+ #[cfg(not(feature = "unchecked"))]
+ max_expr_depth: self.max_expr_depth(),
+ };
+ process_settings(&mut settings);
+
+ let expr = self.parse_expr(&mut input, state, &mut functions, settings)?;
+
+ #[cfg(feature = "no_function")]
+ debug_assert!(functions.is_empty());
+
+ match input.peek().expect(NEVER_ENDS) {
+ (Token::EOF, ..) => (),
+ // Return error if the expression doesn't end
+ (token, pos) => return Err(LexError::UnexpectedInput(token.to_string()).into_err(*pos)),
+ }
+
+ let mut statements = StmtBlockContainer::new_const();
+ statements.push(Stmt::Expr(expr.into()));
+
+ #[cfg(not(feature = "no_optimize"))]
+ return Ok(self.optimize_into_ast(
+ state.external_constants,
+ statements,
+ #[cfg(not(feature = "no_function"))]
+ functions.into_iter().map(|(.., v)| v).collect(),
+ _optimization_level,
+ ));
+
+ #[cfg(feature = "no_optimize")]
+ return Ok(AST::new(
+ statements,
+ #[cfg(not(feature = "no_function"))]
+ crate::Module::from(functions.into_iter().map(|(.., v)| v)),
+ ));
+ }
+
+ /// Parse the global level statements.
+ fn parse_global_level(
+ &self,
+ mut input: TokenStream,
+ state: &mut ParseState,
+ process_settings: impl FnOnce(&mut ParseSettings),
+ ) -> ParseResult<(StmtBlockContainer, StaticVec<Shared<ScriptFnDef>>)> {
+ let mut statements = StmtBlockContainer::new_const();
+ let mut functions = StraightHashMap::default();
+
+ let mut settings = ParseSettings {
+ level: 0,
+ flags: ParseSettingFlags::GLOBAL_LEVEL,
+ options: self.options,
+ pos: Position::START,
+ #[cfg(not(feature = "unchecked"))]
+ max_expr_depth: self.max_expr_depth(),
+ };
+ process_settings(&mut settings);
+
+ while input.peek().expect(NEVER_ENDS).0 != Token::EOF {
+ let stmt = self.parse_stmt(&mut input, state, &mut functions, settings)?;
+
+ if stmt.is_noop() {
+ continue;
+ }
+
+ let need_semicolon = !stmt.is_self_terminated();
+
+ statements.push(stmt);
+
+ match input.peek().expect(NEVER_ENDS) {
+ // EOF
+ (Token::EOF, ..) => break,
+ // stmt ;
+ (Token::SemiColon, ..) if need_semicolon => {
+ eat_token(&mut input, Token::SemiColon);
+ }
+ // stmt ;
+ (Token::SemiColon, ..) if !need_semicolon => (),
+ // { stmt } ???
+ _ if !need_semicolon => (),
+ // stmt <error>
+ (Token::LexError(err), pos) => return Err(err.clone().into_err(*pos)),
+ // stmt ???
+ (.., pos) => {
+ // Semicolons are not optional between statements
+ return Err(PERR::MissingToken(
+ Token::SemiColon.into(),
+ "to terminate this statement".into(),
+ )
+ .into_err(*pos));
+ }
+ }
+ }
+
+ Ok((statements, functions.into_iter().map(|(.., v)| v).collect()))
+ }
+
+ /// Run the parser on an input stream, returning an AST.
+ #[inline]
+ pub(crate) fn parse(
+ &self,
+ input: TokenStream,
+ state: &mut ParseState,
+ _optimization_level: OptimizationLevel,
+ ) -> ParseResult<AST> {
+ let (statements, _lib) = self.parse_global_level(input, state, |_| {})?;
+
+ #[cfg(not(feature = "no_optimize"))]
+ return Ok(self.optimize_into_ast(
+ state.external_constants,
+ statements,
+ #[cfg(not(feature = "no_function"))]
+ _lib,
+ _optimization_level,
+ ));
+
+ #[cfg(feature = "no_optimize")]
+ #[cfg(not(feature = "no_function"))]
+ {
+ let mut m = crate::Module::new();
+
+ _lib.into_iter().for_each(|fn_def| {
+ m.set_script_fn(fn_def);
+ });
+
+ return Ok(AST::new(statements, m));
+ }
+
+ #[cfg(feature = "no_optimize")]
+ #[cfg(feature = "no_function")]
+ return Ok(AST::new(
+ statements,
+ #[cfg(not(feature = "no_function"))]
+ crate::Module::new(),
+ ));
+ }
+}
diff --git a/rhai/src/reify.rs b/rhai/src/reify.rs
new file mode 100644
index 0000000..4c9506a
--- /dev/null
+++ b/rhai/src/reify.rs
@@ -0,0 +1,57 @@
+/// Macro to cast an identifier or expression to another type with type checks.
+///
+/// Runs _code_ if _variable_ or _expression_ is of type _type_, otherwise run _fallback_.
+///
+/// # Syntax
+///
+/// * `reify! { `_variable_ or _expression_` => |`_temp-variable_`: `_type_`|` _code_`,` `||` _fallback_ `)`
+/// * `reify! { `_variable_ or _expression_` => |`_temp-variable_`: `_type_`|` _code_ `)`
+/// * `reify! { `_variable_ or _expression_ `=>` `Option<`_type_`>` `)`
+/// * `reify! { `_variable_ or _expression_ `=>` _type_ `)`
+///
+/// * `reify! { `_expression_ `=> !!!` _type_ `)` (unsafe, no type checks!)
+macro_rules! reify {
+ ($old:ident => |$new:ident : $t:ty| $code:expr, || $fallback:expr) => {{
+ #[allow(clippy::redundant_else)]
+ if std::any::TypeId::of::<$t>() == std::any::Any::type_id(&$old) {
+ // SAFETY: This is safe because we already checked to make sure the two types
+ // are actually the same.
+ let $new: $t = unsafe { std::mem::transmute_copy(&std::mem::ManuallyDrop::new($old)) };
+ $code
+ } else {
+ $fallback
+ }
+ }};
+ ($old:expr => |$new:ident : $t:ty| $code:expr, || $fallback:expr) => {{
+ let old = $old;
+ reify! { old => |$new: $t| $code, || $fallback }
+ }};
+
+ ($old:ident => |$new:ident : $t:ty| $code:expr) => {
+ reify! { $old => |$new: $t| $code, || () }
+ };
+ ($old:expr => |$new:ident : $t:ty| $code:expr) => {
+ reify! { $old => |$new: $t| $code, || () }
+ };
+
+ ($old:ident => Option<$t:ty>) => {
+ reify! { $old => |v: $t| Some(v), || None }
+ };
+ ($old:expr => Option<$t:ty>) => {
+ reify! { $old => |v: $t| Some(v), || None }
+ };
+
+ ($old:ident => $t:ty) => {
+ reify! { $old => |v: $t| v, || unreachable!() }
+ };
+ ($old:expr => $t:ty) => {
+ reify! { $old => |v: $t| v, || unreachable!() }
+ };
+
+ ($old:expr => !!! $t:ty) => {{
+ let old_value = $old;
+ let new_value: $t =
+ unsafe { std::mem::transmute_copy(&std::mem::ManuallyDrop::new(old_value)) };
+ new_value
+ }};
+}
diff --git a/rhai/src/serde/de.rs b/rhai/src/serde/de.rs
new file mode 100644
index 0000000..681a681
--- /dev/null
+++ b/rhai/src/serde/de.rs
@@ -0,0 +1,622 @@
+//! Implement deserialization support of [`Dynamic`][crate::Dynamic] for [`serde`].
+
+use crate::types::dynamic::Union;
+use crate::{Dynamic, ImmutableString, LexError, Position, RhaiError, RhaiResultOf, ERR};
+use serde::de::{Error, IntoDeserializer, Visitor};
+use serde::{Deserialize, Deserializer};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{any::type_name, fmt};
+
+/// Deserializer for [`Dynamic`][crate::Dynamic].
+pub struct DynamicDeserializer<'de>(&'de Dynamic);
+
+impl<'de> IntoDeserializer<'de, RhaiError> for &'de Dynamic {
+ type Deserializer = DynamicDeserializer<'de>;
+
+ #[inline(always)]
+ #[must_use]
+ fn into_deserializer(self) -> Self::Deserializer {
+ DynamicDeserializer(self)
+ }
+}
+
+impl<'de> DynamicDeserializer<'de> {
+ /// Create a [`DynamicDeserializer`] from a reference to a [`Dynamic`][crate::Dynamic] value.
+ ///
+ /// The reference is necessary because the deserialized type may hold references
+ /// (especially `&str`) to the source [`Dynamic`][crate::Dynamic].
+ #[inline(always)]
+ #[must_use]
+ pub const fn new(value: &'de Dynamic) -> Self {
+ Self(value)
+ }
+ /// Shortcut for a type conversion error.
+ #[cold]
+ #[inline(always)]
+ fn type_error<T>(&self) -> RhaiResultOf<T> {
+ self.type_error_str(type_name::<T>())
+ }
+ /// Shortcut for a type conversion error.
+ #[cold]
+ #[inline(never)]
+ fn type_error_str<T>(&self, error: &str) -> RhaiResultOf<T> {
+ Err(
+ ERR::ErrorMismatchOutputType(error.into(), self.0.type_name().into(), Position::NONE)
+ .into(),
+ )
+ }
+ #[inline(always)]
+ fn deserialize_int<V: Visitor<'de>>(v: crate::INT, visitor: V) -> RhaiResultOf<V::Value> {
+ #[cfg(not(feature = "only_i32"))]
+ return visitor.visit_i64(v);
+ #[cfg(feature = "only_i32")]
+ return visitor.visit_i32(v);
+ }
+}
+
+/// Deserialize a [`Dynamic`][crate::Dynamic] value into a Rust type that implements [`serde::Deserialize`].
+///
+/// # Example
+///
+/// ```
+/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+/// # #[cfg(not(feature = "no_index"))]
+/// # #[cfg(not(feature = "no_object"))]
+/// # {
+/// use rhai::{Dynamic, Array, Map, INT};
+/// use rhai::serde::from_dynamic;
+/// use serde::Deserialize;
+///
+/// #[derive(Debug, Deserialize, PartialEq)]
+/// struct Hello {
+/// a: INT,
+/// b: bool,
+/// }
+///
+/// #[derive(Debug, Deserialize, PartialEq)]
+/// struct Test {
+/// int: u32,
+/// seq: Vec<String>,
+/// obj: Hello,
+/// }
+///
+/// let mut map = Map::new();
+/// map.insert("int".into(), Dynamic::from(42_u32));
+///
+/// let mut map2 = Map::new();
+/// map2.insert("a".into(), (123 as INT).into());
+/// map2.insert("b".into(), true.into());
+///
+/// map.insert("obj".into(), map2.into());
+///
+/// let arr: Array = vec!["foo".into(), "bar".into(), "baz".into()];
+/// map.insert("seq".into(), arr.into());
+///
+/// let value: Test = from_dynamic(&map.into())?;
+///
+/// let expected = Test {
+/// int: 42,
+/// seq: vec!["foo".into(), "bar".into(), "baz".into()],
+/// obj: Hello { a: 123, b: true },
+/// };
+///
+/// assert_eq!(value, expected);
+/// # }
+/// # Ok(())
+/// # }
+/// ```
+pub fn from_dynamic<'de, T: Deserialize<'de>>(value: &'de Dynamic) -> RhaiResultOf<T> {
+ T::deserialize(DynamicDeserializer::new(value))
+}
+
+impl Error for RhaiError {
+ #[cold]
+ #[inline(never)]
+ fn custom<T: fmt::Display>(err: T) -> Self {
+ LexError::ImproperSymbol(String::new(), err.to_string())
+ .into_err(Position::NONE)
+ .into()
+ }
+}
+
+impl<'de> Deserializer<'de> for DynamicDeserializer<'de> {
+ type Error = RhaiError;
+
+ fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ if type_name::<V::Value>() == type_name::<Dynamic>() {
+ return Ok(reify! { self.0.clone() => !!! V::Value });
+ }
+
+ match self.0 .0 {
+ Union::Unit(..) => self.deserialize_unit(visitor),
+ Union::Bool(..) => self.deserialize_bool(visitor),
+ Union::Str(..) => self.deserialize_str(visitor),
+ Union::Char(..) => self.deserialize_char(visitor),
+
+ #[cfg(not(feature = "only_i32"))]
+ Union::Int(..) => self.deserialize_i64(visitor),
+ #[cfg(feature = "only_i32")]
+ Union::Int(..) => self.deserialize_i32(visitor),
+
+ #[cfg(not(feature = "no_float"))]
+ #[cfg(not(feature = "f32_float"))]
+ Union::Float(..) => self.deserialize_f64(visitor),
+ #[cfg(not(feature = "no_float"))]
+ #[cfg(feature = "f32_float")]
+ Union::Float(..) => self.deserialize_f32(visitor),
+
+ #[cfg(feature = "decimal")]
+ #[cfg(not(feature = "f32_float"))]
+ Union::Decimal(..) => self.deserialize_f64(visitor),
+ #[cfg(feature = "decimal")]
+ #[cfg(feature = "f32_float")]
+ Union::Decimal(..) => self.deserialize_f32(visitor),
+
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(..) => self.deserialize_seq(visitor),
+ #[cfg(not(feature = "no_index"))]
+ Union::Blob(..) => self.deserialize_bytes(visitor),
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(..) => self.deserialize_map(visitor),
+ Union::FnPtr(..) => self.type_error(),
+ #[cfg(not(feature = "no_time"))]
+ Union::TimeStamp(..) => self.type_error(),
+
+ Union::Variant(ref value, ..) if value.is::<i8>() => self.deserialize_i8(visitor),
+ Union::Variant(ref value, ..) if value.is::<i16>() => self.deserialize_i16(visitor),
+ Union::Variant(ref value, ..) if value.is::<i32>() => self.deserialize_i32(visitor),
+ Union::Variant(ref value, ..) if value.is::<i64>() => self.deserialize_i64(visitor),
+ Union::Variant(ref value, ..) if value.is::<i128>() => self.deserialize_i128(visitor),
+ Union::Variant(ref value, ..) if value.is::<u8>() => self.deserialize_u8(visitor),
+ Union::Variant(ref value, ..) if value.is::<u16>() => self.deserialize_u16(visitor),
+ Union::Variant(ref value, ..) if value.is::<u32>() => self.deserialize_u32(visitor),
+ Union::Variant(ref value, ..) if value.is::<u64>() => self.deserialize_u64(visitor),
+ Union::Variant(ref value, ..) if value.is::<u128>() => self.deserialize_u128(visitor),
+
+ Union::Variant(..) => self.type_error(),
+
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(..) => self.type_error(),
+ }
+ }
+
+ fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ visitor.visit_bool(self.0.as_bool().or_else(|_| self.type_error())?)
+ }
+
+ fn deserialize_i8<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ if let Ok(v) = self.0.as_int() {
+ Self::deserialize_int(v, visitor)
+ } else {
+ self.0
+ .downcast_ref::<i8>()
+ .map_or_else(|| self.type_error(), |&x| visitor.visit_i8(x))
+ }
+ }
+
+ fn deserialize_i16<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ if let Ok(v) = self.0.as_int() {
+ Self::deserialize_int(v, visitor)
+ } else {
+ self.0
+ .downcast_ref::<i16>()
+ .map_or_else(|| self.type_error(), |&x| visitor.visit_i16(x))
+ }
+ }
+
+ fn deserialize_i32<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ if let Ok(v) = self.0.as_int() {
+ Self::deserialize_int(v, visitor)
+ } else if cfg!(feature = "only_i32") {
+ self.type_error()
+ } else {
+ self.0
+ .downcast_ref::<i32>()
+ .map_or_else(|| self.type_error(), |&x| visitor.visit_i32(x))
+ }
+ }
+
+ fn deserialize_i64<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ if let Ok(v) = self.0.as_int() {
+ Self::deserialize_int(v, visitor)
+ } else if cfg!(not(feature = "only_i32")) {
+ self.type_error()
+ } else {
+ self.0
+ .downcast_ref::<i64>()
+ .map_or_else(|| self.type_error(), |&x| visitor.visit_i64(x))
+ }
+ }
+
+ fn deserialize_i128<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ if let Ok(v) = self.0.as_int() {
+ Self::deserialize_int(v, visitor)
+ } else if cfg!(not(feature = "only_i32")) {
+ self.type_error()
+ } else {
+ self.0
+ .downcast_ref::<i128>()
+ .map_or_else(|| self.type_error(), |&x| visitor.visit_i128(x))
+ }
+ }
+
+ fn deserialize_u8<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ if let Ok(v) = self.0.as_int() {
+ Self::deserialize_int(v, visitor)
+ } else {
+ self.0
+ .downcast_ref::<u8>()
+ .map_or_else(|| self.type_error(), |&x| visitor.visit_u8(x))
+ }
+ }
+
+ fn deserialize_u16<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ if let Ok(v) = self.0.as_int() {
+ Self::deserialize_int(v, visitor)
+ } else {
+ self.0
+ .downcast_ref::<u16>()
+ .map_or_else(|| self.type_error(), |&x| visitor.visit_u16(x))
+ }
+ }
+
+ fn deserialize_u32<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ if let Ok(v) = self.0.as_int() {
+ Self::deserialize_int(v, visitor)
+ } else {
+ self.0
+ .downcast_ref::<u32>()
+ .map_or_else(|| self.type_error(), |&x| visitor.visit_u32(x))
+ }
+ }
+
+ fn deserialize_u64<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ if let Ok(v) = self.0.as_int() {
+ Self::deserialize_int(v, visitor)
+ } else {
+ self.0
+ .downcast_ref::<u64>()
+ .map_or_else(|| self.type_error(), |&x| visitor.visit_u64(x))
+ }
+ }
+
+ fn deserialize_u128<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ if let Ok(v) = self.0.as_int() {
+ Self::deserialize_int(v, visitor)
+ } else {
+ self.0
+ .downcast_ref::<u128>()
+ .map_or_else(|| self.type_error(), |&x| visitor.visit_u128(x))
+ }
+ }
+
+ fn deserialize_f32<V: Visitor<'de>>(self, _visitor: V) -> RhaiResultOf<V::Value> {
+ #[cfg(not(feature = "no_float"))]
+ return self
+ .0
+ .downcast_ref::<f32>()
+ .map_or_else(|| self.type_error(), |&x| _visitor.visit_f32(x));
+
+ #[allow(unreachable_code)]
+ {
+ #[cfg(feature = "decimal")]
+ {
+ use rust_decimal::prelude::ToPrimitive;
+
+ return self
+ .0
+ .downcast_ref::<rust_decimal::Decimal>()
+ .and_then(|&x| x.to_f32())
+ .map_or_else(|| self.type_error(), |v| _visitor.visit_f32(v));
+ }
+
+ self.type_error_str("f32")
+ }
+ }
+
+ fn deserialize_f64<V: Visitor<'de>>(self, _visitor: V) -> RhaiResultOf<V::Value> {
+ #[cfg(not(feature = "no_float"))]
+ return self
+ .0
+ .downcast_ref::<f64>()
+ .map_or_else(|| self.type_error(), |&x| _visitor.visit_f64(x));
+
+ #[allow(unreachable_code)]
+ {
+ #[cfg(feature = "decimal")]
+ {
+ use rust_decimal::prelude::ToPrimitive;
+
+ return self
+ .0
+ .downcast_ref::<rust_decimal::Decimal>()
+ .and_then(|&x| x.to_f64())
+ .map_or_else(|| self.type_error(), |v| _visitor.visit_f64(v));
+ }
+
+ self.type_error_str("f64")
+ }
+ }
+
+ fn deserialize_char<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ self.0
+ .downcast_ref::<char>()
+ .map_or_else(|| self.type_error(), |&x| visitor.visit_char(x))
+ }
+
+ fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ self.0.downcast_ref::<ImmutableString>().map_or_else(
+ || self.type_error(),
+ |x| visitor.visit_borrowed_str(x.as_str()),
+ )
+ }
+
+ fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ self.deserialize_str(visitor)
+ }
+
+ fn deserialize_bytes<V: Visitor<'de>>(self, _visitor: V) -> RhaiResultOf<V::Value> {
+ #[cfg(not(feature = "no_index"))]
+ return self
+ .0
+ .downcast_ref::<crate::Blob>()
+ .map_or_else(|| self.type_error(), |x| _visitor.visit_bytes(x));
+
+ #[cfg(feature = "no_index")]
+ return self.type_error();
+ }
+
+ fn deserialize_byte_buf<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ self.deserialize_bytes(visitor)
+ }
+
+ fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ if self.0.is_unit() {
+ visitor.visit_none()
+ } else {
+ visitor.visit_some(self)
+ }
+ }
+
+ fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ self.0
+ .downcast_ref::<()>()
+ .map_or_else(|| self.type_error(), |_| visitor.visit_unit())
+ }
+
+ fn deserialize_unit_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ visitor: V,
+ ) -> RhaiResultOf<V::Value> {
+ self.deserialize_unit(visitor)
+ }
+
+ fn deserialize_newtype_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ visitor: V,
+ ) -> RhaiResultOf<V::Value> {
+ visitor.visit_newtype_struct(self)
+ }
+
+ fn deserialize_seq<V: Visitor<'de>>(self, _visitor: V) -> RhaiResultOf<V::Value> {
+ #[cfg(not(feature = "no_index"))]
+ return self.0.downcast_ref::<crate::Array>().map_or_else(
+ || self.type_error(),
+ |arr| _visitor.visit_seq(IterateDynamicArray::new(arr.iter())),
+ );
+
+ #[cfg(feature = "no_index")]
+ return self.type_error();
+ }
+
+ fn deserialize_tuple<V: Visitor<'de>>(self, _len: usize, visitor: V) -> RhaiResultOf<V::Value> {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_tuple_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ _len: usize,
+ visitor: V,
+ ) -> RhaiResultOf<V::Value> {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_map<V: Visitor<'de>>(self, _visitor: V) -> RhaiResultOf<V::Value> {
+ #[cfg(not(feature = "no_object"))]
+ return self.0.downcast_ref::<crate::Map>().map_or_else(
+ || self.type_error(),
+ |map| {
+ _visitor.visit_map(IterateMap::new(
+ map.keys().map(crate::SmartString::as_str),
+ map.values(),
+ ))
+ },
+ );
+
+ #[cfg(feature = "no_object")]
+ return self.type_error();
+ }
+
+ fn deserialize_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ _fields: &'static [&'static str],
+ visitor: V,
+ ) -> RhaiResultOf<V::Value> {
+ self.deserialize_map(visitor)
+ }
+
+ fn deserialize_enum<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ _variants: &'static [&'static str],
+ visitor: V,
+ ) -> RhaiResultOf<V::Value> {
+ if let Some(s) = self.0.read_lock::<ImmutableString>() {
+ visitor.visit_enum(s.as_str().into_deserializer())
+ } else {
+ #[cfg(not(feature = "no_object"))]
+ return self.0.downcast_ref::<crate::Map>().map_or_else(
+ || self.type_error(),
+ |map| {
+ let mut iter = map.iter();
+ let first = iter.next();
+ let second = iter.next();
+ if let (Some((key, value)), None) = (first, second) {
+ visitor.visit_enum(EnumDeserializer {
+ tag: key,
+ content: DynamicDeserializer::new(value),
+ })
+ } else {
+ self.type_error()
+ }
+ },
+ );
+ #[cfg(feature = "no_object")]
+ return self.type_error();
+ }
+ }
+
+ #[inline(always)]
+ fn deserialize_identifier<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ self.deserialize_str(visitor)
+ }
+
+ #[inline(always)]
+ fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> RhaiResultOf<V::Value> {
+ self.deserialize_any(visitor)
+ }
+}
+
+/// `SeqAccess` implementation for arrays.
+#[cfg(not(feature = "no_index"))]
+struct IterateDynamicArray<'de, ITER: Iterator<Item = &'de Dynamic>> {
+ /// Iterator for a stream of [`Dynamic`][crate::Dynamic] values.
+ iter: ITER,
+}
+
+#[cfg(not(feature = "no_index"))]
+impl<'de, ITER: Iterator<Item = &'de Dynamic>> IterateDynamicArray<'de, ITER> {
+ #[inline(always)]
+ #[must_use]
+ pub const fn new(iter: ITER) -> Self {
+ Self { iter }
+ }
+}
+
+#[cfg(not(feature = "no_index"))]
+impl<'de, ITER: Iterator<Item = &'de Dynamic>> serde::de::SeqAccess<'de>
+ for IterateDynamicArray<'de, ITER>
+{
+ type Error = RhaiError;
+
+ fn next_element_seed<T: serde::de::DeserializeSeed<'de>>(
+ &mut self,
+ seed: T,
+ ) -> RhaiResultOf<Option<T::Value>> {
+ // Deserialize each item coming out of the iterator.
+ self.iter.next().map_or(Ok(None), |item| {
+ seed.deserialize(item.into_deserializer()).map(Some)
+ })
+ }
+}
+
+/// `MapAccess` implementation for maps.
+#[cfg(not(feature = "no_object"))]
+struct IterateMap<'de, K: Iterator<Item = &'de str>, V: Iterator<Item = &'de Dynamic>> {
+ // Iterator for a stream of [`Dynamic`][crate::Dynamic] keys.
+ keys: K,
+ // Iterator for a stream of [`Dynamic`][crate::Dynamic] values.
+ values: V,
+}
+
+#[cfg(not(feature = "no_object"))]
+impl<'de, K: Iterator<Item = &'de str>, V: Iterator<Item = &'de Dynamic>> IterateMap<'de, K, V> {
+ #[inline(always)]
+ #[must_use]
+ pub const fn new(keys: K, values: V) -> Self {
+ Self { keys, values }
+ }
+}
+
+#[cfg(not(feature = "no_object"))]
+impl<'de, K: Iterator<Item = &'de str>, V: Iterator<Item = &'de Dynamic>> serde::de::MapAccess<'de>
+ for IterateMap<'de, K, V>
+{
+ type Error = RhaiError;
+
+ fn next_key_seed<S: serde::de::DeserializeSeed<'de>>(
+ &mut self,
+ seed: S,
+ ) -> RhaiResultOf<Option<S::Value>> {
+ // Deserialize each `Identifier` key coming out of the keys iterator.
+ self.keys
+ .next()
+ .map(<_>::into_deserializer)
+ .map_or(Ok(None), |d| seed.deserialize(d).map(Some))
+ }
+
+ fn next_value_seed<S: serde::de::DeserializeSeed<'de>>(
+ &mut self,
+ seed: S,
+ ) -> RhaiResultOf<S::Value> {
+ // Deserialize each value item coming out of the iterator.
+ seed.deserialize(self.values.next().unwrap().into_deserializer())
+ }
+}
+
+#[cfg(not(feature = "no_object"))]
+struct EnumDeserializer<'de> {
+ tag: &'de str,
+ content: DynamicDeserializer<'de>,
+}
+
+#[cfg(not(feature = "no_object"))]
+impl<'de> serde::de::EnumAccess<'de> for EnumDeserializer<'de> {
+ type Error = RhaiError;
+ type Variant = Self;
+
+ fn variant_seed<V: serde::de::DeserializeSeed<'de>>(
+ self,
+ seed: V,
+ ) -> RhaiResultOf<(V::Value, Self::Variant)> {
+ seed.deserialize(self.tag.into_deserializer())
+ .map(|v| (v, self))
+ }
+}
+
+#[cfg(not(feature = "no_object"))]
+impl<'de> serde::de::VariantAccess<'de> for EnumDeserializer<'de> {
+ type Error = RhaiError;
+
+ #[inline(always)]
+ fn unit_variant(self) -> RhaiResultOf<()> {
+ Deserialize::deserialize(self.content)
+ }
+
+ #[inline(always)]
+ fn newtype_variant_seed<T: serde::de::DeserializeSeed<'de>>(
+ self,
+ seed: T,
+ ) -> RhaiResultOf<T::Value> {
+ seed.deserialize(self.content)
+ }
+
+ #[inline(always)]
+ fn tuple_variant<V: Visitor<'de>>(self, len: usize, visitor: V) -> RhaiResultOf<V::Value> {
+ self.content.deserialize_tuple(len, visitor)
+ }
+
+ #[inline(always)]
+ fn struct_variant<V: Visitor<'de>>(
+ self,
+ fields: &'static [&'static str],
+ visitor: V,
+ ) -> RhaiResultOf<V::Value> {
+ self.content.deserialize_struct("", fields, visitor)
+ }
+}
diff --git a/rhai/src/serde/deserialize.rs b/rhai/src/serde/deserialize.rs
new file mode 100644
index 0000000..c8f59bd
--- /dev/null
+++ b/rhai/src/serde/deserialize.rs
@@ -0,0 +1,321 @@
+//! Implementations of [`serde::Deserialize`].
+
+use crate::{Dynamic, Identifier, ImmutableString, Scope, INT};
+use serde::{
+ de::{Error, SeqAccess, Visitor},
+ Deserialize, Deserializer,
+};
+use std::fmt;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(feature = "decimal")]
+use num_traits::FromPrimitive;
+
+struct DynamicVisitor;
+
+impl<'de> Visitor<'de> for DynamicVisitor {
+ type Value = Dynamic;
+
+ #[cold]
+ #[inline(never)]
+ fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str("any type that can be converted into a Dynamic")
+ }
+ #[inline(always)]
+ fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> {
+ Ok(v.into())
+ }
+ #[inline(always)]
+ fn visit_i8<E: Error>(self, v: i8) -> Result<Self::Value, E> {
+ Ok(INT::from(v).into())
+ }
+ #[inline(always)]
+ fn visit_i16<E: Error>(self, v: i16) -> Result<Self::Value, E> {
+ Ok(INT::from(v).into())
+ }
+ #[inline(always)]
+ fn visit_i32<E: Error>(self, v: i32) -> Result<Self::Value, E> {
+ Ok(INT::from(v).into())
+ }
+ #[inline]
+ fn visit_i64<E: Error>(self, v: i64) -> Result<Self::Value, E> {
+ #[cfg(not(feature = "only_i32"))]
+ return Ok(v.into());
+
+ #[cfg(feature = "only_i32")]
+ if v <= INT::MAX as i64 {
+ return Ok(Dynamic::from(v as INT));
+ }
+
+ #[allow(unreachable_code)]
+ {
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_i64(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ return Ok(Dynamic::from_float(v as crate::FLOAT));
+
+ Err(Error::custom(format!("integer number too large: {v}")))
+ }
+ }
+ #[inline]
+ fn visit_i128<E: Error>(self, v: i128) -> Result<Self::Value, E> {
+ if v <= i128::from(INT::MAX) {
+ return Ok(Dynamic::from(v as INT));
+ }
+
+ #[allow(unreachable_code)]
+ {
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_i128(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ return Ok(Dynamic::from_float(v as crate::FLOAT));
+
+ Err(Error::custom(format!("integer number too large: {v}")))
+ }
+ }
+ #[inline(always)]
+ fn visit_u8<E: Error>(self, v: u8) -> Result<Self::Value, E> {
+ Ok(INT::from(v).into())
+ }
+ #[inline(always)]
+ fn visit_u16<E: Error>(self, v: u16) -> Result<Self::Value, E> {
+ Ok(INT::from(v).into())
+ }
+ #[inline]
+ fn visit_u32<E: Error>(self, v: u32) -> Result<Self::Value, E> {
+ #[cfg(not(feature = "only_i32"))]
+ return Ok(Dynamic::from(v as INT));
+
+ #[cfg(feature = "only_i32")]
+ if v <= INT::MAX as u32 {
+ return Ok(Dynamic::from(v as INT));
+ }
+
+ #[allow(unreachable_code)]
+ {
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_u32(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ return Ok(Dynamic::from_float(v as crate::FLOAT));
+
+ Err(Error::custom(format!("integer number too large: {v}")))
+ }
+ }
+ #[inline]
+ fn visit_u64<E: Error>(self, v: u64) -> Result<Self::Value, E> {
+ if v <= INT::MAX as u64 {
+ return Ok(Dynamic::from(v as INT));
+ }
+
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_u64(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ return Ok(Dynamic::from_float(v as crate::FLOAT));
+
+ #[allow(unreachable_code)]
+ Err(Error::custom(format!("integer number too large: {v}")))
+ }
+ #[inline]
+ fn visit_u128<E: Error>(self, v: u128) -> Result<Self::Value, E> {
+ if v <= INT::MAX as u128 {
+ return Ok(Dynamic::from(v as INT));
+ }
+
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_u128(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ return Ok(Dynamic::from_float(v as crate::FLOAT));
+
+ #[allow(unreachable_code)]
+ Err(Error::custom(format!("integer number too large: {v}")))
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ #[inline(always)]
+ fn visit_f32<E: Error>(self, v: f32) -> Result<Self::Value, E> {
+ #[cfg(not(feature = "no_float"))]
+ return Ok((v as crate::FLOAT).into());
+
+ #[allow(unreachable_code)]
+ {
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_f32(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ Err(Error::custom(format!(
+ "floating-point number is not supported: {v}"
+ )))
+ }
+ }
+ #[cfg(not(feature = "no_float"))]
+ #[inline(always)]
+ fn visit_f64<E: Error>(self, v: f64) -> Result<Self::Value, E> {
+ #[cfg(not(feature = "no_float"))]
+ return Ok((v as crate::FLOAT).into());
+
+ #[allow(unreachable_code)]
+ {
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_f64(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ Err(Error::custom(format!(
+ "floating-point number is not supported: {v}"
+ )))
+ }
+ }
+
+ #[cfg(feature = "no_float")]
+ #[cfg(feature = "decimal")]
+ #[inline]
+ fn visit_f32<E: Error>(self, v: f32) -> Result<Self::Value, E> {
+ use std::convert::TryFrom;
+
+ rust_decimal::Decimal::try_from(v)
+ .map(|v| v.into())
+ .map_err(Error::custom)
+ }
+ #[cfg(feature = "no_float")]
+ #[cfg(feature = "decimal")]
+ #[inline]
+ fn visit_f64<E: Error>(self, v: f64) -> Result<Self::Value, E> {
+ use std::convert::TryFrom;
+
+ rust_decimal::Decimal::try_from(v)
+ .map(|v| v.into())
+ .map_err(Error::custom)
+ }
+
+ #[inline(always)]
+ fn visit_char<E: Error>(self, v: char) -> Result<Self::Value, E> {
+ Ok(v.into())
+ }
+ #[inline(always)]
+ fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
+ Ok(v.into())
+ }
+ #[inline(always)]
+ fn visit_string<E: Error>(self, v: String) -> Result<Self::Value, E> {
+ Ok(v.into())
+ }
+ #[cfg(not(feature = "no_index"))]
+ #[inline(always)]
+ fn visit_bytes<E: Error>(self, v: &[u8]) -> Result<Self::Value, E> {
+ Ok(Dynamic::from_blob(v.to_vec()))
+ }
+
+ #[inline(always)]
+ fn visit_unit<E: Error>(self) -> Result<Self::Value, E> {
+ Ok(Dynamic::UNIT)
+ }
+
+ #[inline(always)]
+ fn visit_newtype_struct<D: Deserializer<'de>>(self, de: D) -> Result<Self::Value, D::Error> {
+ Deserialize::deserialize(de)
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
+ let mut arr = crate::Array::new();
+
+ while let Some(v) = seq.next_element()? {
+ arr.push(v);
+ }
+
+ Ok(arr.into())
+ }
+
+ #[cfg(not(feature = "no_object"))]
+ fn visit_map<M: serde::de::MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
+ let mut m = crate::Map::new();
+
+ while let Some((k, v)) = map.next_entry()? {
+ m.insert(k, v);
+ }
+
+ Ok(m.into())
+ }
+}
+
+impl<'de> Deserialize<'de> for Dynamic {
+ #[inline(always)]
+ fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ deserializer.deserialize_any(DynamicVisitor)
+ }
+}
+
+impl<'de> Deserialize<'de> for ImmutableString {
+ #[inline(always)]
+ fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ let s: String = Deserialize::deserialize(deserializer)?;
+ Ok(s.into())
+ }
+}
+
+impl<'de> Deserialize<'de> for Scope<'de> {
+ #[inline(always)]
+ fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ #[derive(Debug, Clone, Hash, Deserialize)]
+ struct ScopeEntry {
+ pub name: Identifier,
+ pub value: Dynamic,
+ #[serde(default)]
+ pub is_constant: bool,
+ }
+
+ struct VecVisitor;
+
+ impl<'de> Visitor<'de> for VecVisitor {
+ type Value = Scope<'static>;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a sequence")
+ }
+
+ #[inline]
+ fn visit_seq<A>(self, mut access: A) -> Result<Self::Value, A::Error>
+ where
+ A: SeqAccess<'de>,
+ {
+ let mut scope = access
+ .size_hint()
+ .map_or_else(Scope::new, Scope::with_capacity);
+
+ while let Some(ScopeEntry {
+ name,
+ value,
+ is_constant,
+ }) = access.next_element()?
+ {
+ if is_constant {
+ scope.push_constant_dynamic(name, value);
+ } else {
+ scope.push_dynamic(name, value);
+ }
+ }
+
+ Ok(scope)
+ }
+ }
+
+ deserializer.deserialize_seq(VecVisitor)
+ }
+}
diff --git a/rhai/src/serde/metadata.rs b/rhai/src/serde/metadata.rs
new file mode 100644
index 0000000..ec936f6
--- /dev/null
+++ b/rhai/src/serde/metadata.rs
@@ -0,0 +1,303 @@
+//! Serialization of functions metadata.
+#![cfg(feature = "metadata")]
+
+use crate::api::formatting::format_type;
+use crate::module::{calc_native_fn_hash, FuncInfo, ModuleFlags};
+use crate::{calc_fn_hash, Engine, FnAccess, SmartString, StaticVec, AST};
+use serde::Serialize;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap};
+
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize)]
+#[serde(rename_all = "camelCase")]
+enum FnType {
+ Script,
+ Native,
+}
+
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct FnParam<'a> {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub name: Option<&'a str>,
+ #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
+ pub typ: Option<Cow<'a, str>>,
+}
+
+#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct FnMetadata<'a> {
+ pub base_hash: u64,
+ pub full_hash: u64,
+ #[cfg(not(feature = "no_module"))]
+ pub namespace: crate::FnNamespace,
+ pub access: FnAccess,
+ pub name: &'a str,
+ #[cfg(not(feature = "no_function"))]
+ pub is_anonymous: bool,
+ #[serde(rename = "type")]
+ pub typ: FnType,
+ #[cfg(not(feature = "no_object"))]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub this_type: Option<&'a str>,
+ pub num_params: usize,
+ #[serde(default, skip_serializing_if = "StaticVec::is_empty")]
+ pub params: StaticVec<FnParam<'a>>,
+ // No idea why the following is needed otherwise serde comes back with a lifetime error
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub _dummy: Option<&'a str>,
+ #[serde(default, skip_serializing_if = "str::is_empty")]
+ pub return_type: Cow<'a, str>,
+ pub signature: SmartString,
+ #[serde(default, skip_serializing_if = "StaticVec::is_empty")]
+ pub doc_comments: StaticVec<&'a str>,
+}
+
+impl PartialOrd for FnMetadata<'_> {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for FnMetadata<'_> {
+ fn cmp(&self, other: &Self) -> Ordering {
+ match self.name.cmp(other.name) {
+ Ordering::Equal => self.num_params.cmp(&other.num_params),
+ cmp => cmp,
+ }
+ }
+}
+
+impl<'a> From<&'a FuncInfo> for FnMetadata<'a> {
+ fn from(info: &'a FuncInfo) -> Self {
+ let base_hash = calc_fn_hash(None, &info.metadata.name, info.metadata.num_params);
+ let (typ, full_hash) = if info.func.is_script() {
+ (FnType::Script, base_hash)
+ } else {
+ (
+ FnType::Native,
+ calc_native_fn_hash(None, &info.metadata.name, &info.metadata.param_types),
+ )
+ };
+
+ Self {
+ base_hash,
+ full_hash,
+ #[cfg(not(feature = "no_module"))]
+ namespace: info.metadata.namespace,
+ access: info.metadata.access,
+ name: &info.metadata.name,
+ #[cfg(not(feature = "no_function"))]
+ is_anonymous: crate::parser::is_anonymous_fn(&info.metadata.name),
+ typ,
+ #[cfg(not(feature = "no_object"))]
+ this_type: info.metadata.this_type.as_ref().map(|s| s.as_str()),
+ num_params: info.metadata.num_params,
+ params: info
+ .metadata
+ .params_info
+ .iter()
+ .map(|s| {
+ let mut seg = s.splitn(2, ':');
+ let name = match seg.next().unwrap().trim() {
+ "_" => None,
+ s => Some(s),
+ };
+ let typ = seg.next().map(|s| format_type(s, false));
+ FnParam { name, typ }
+ })
+ .collect(),
+ _dummy: None,
+ return_type: format_type(&info.metadata.return_type, true),
+ signature: info.gen_signature().into(),
+ doc_comments: if info.func.is_script() {
+ #[cfg(feature = "no_function")]
+ unreachable!("script-defined functions should not exist under no_function");
+
+ #[cfg(not(feature = "no_function"))]
+ info.func
+ .get_script_fn_def()
+ .expect("script-defined function")
+ .comments
+ .iter()
+ .map(<_>::as_ref)
+ .collect()
+ } else {
+ info.metadata.comments.iter().map(<_>::as_ref).collect()
+ },
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ModuleMetadata<'a> {
+ #[cfg(feature = "metadata")]
+ #[serde(skip_serializing_if = "str::is_empty")]
+ pub doc: &'a str,
+ #[serde(skip_serializing_if = "BTreeMap::is_empty")]
+ pub modules: BTreeMap<&'a str, Self>,
+ #[serde(skip_serializing_if = "StaticVec::is_empty")]
+ pub functions: StaticVec<FnMetadata<'a>>,
+}
+
+impl ModuleMetadata<'_> {
+ #[inline(always)]
+ pub fn new() -> Self {
+ Self {
+ #[cfg(feature = "metadata")]
+ doc: "",
+ modules: BTreeMap::new(),
+ functions: StaticVec::new_const(),
+ }
+ }
+}
+
+impl<'a> From<&'a crate::Module> for ModuleMetadata<'a> {
+ fn from(module: &'a crate::Module) -> Self {
+ let mut functions = module.iter_fn().map(Into::into).collect::<StaticVec<_>>();
+ functions.sort();
+
+ Self {
+ doc: module.doc(),
+ modules: module
+ .iter_sub_modules()
+ .map(|(name, m)| (name, m.as_ref().into()))
+ .collect(),
+ functions,
+ }
+ }
+}
+
+/// Generate a list of all functions in JSON format.
+pub fn gen_metadata_to_json(
+ engine: &Engine,
+ ast: Option<&AST>,
+ include_standard_packages: bool,
+) -> serde_json::Result<String> {
+ let _ast = ast;
+ #[cfg(feature = "metadata")]
+ let mut global_doc = String::new();
+ let mut global = ModuleMetadata::new();
+
+ #[cfg(not(feature = "no_module"))]
+ for (name, m) in engine.global_sub_modules.as_ref().into_iter().flatten() {
+ global.modules.insert(name, m.as_ref().into());
+ }
+
+ let exclude_flags = if include_standard_packages {
+ ModuleFlags::empty()
+ } else {
+ ModuleFlags::STANDARD_LIB
+ };
+
+ engine
+ .global_modules
+ .iter()
+ .filter(|m| !m.flags.contains(exclude_flags))
+ .flat_map(|m| {
+ #[cfg(feature = "metadata")]
+ if !m.doc().is_empty() {
+ if !global_doc.is_empty() {
+ global_doc.push('\n');
+ }
+ global_doc.push_str(m.doc());
+ }
+ m.iter_fn()
+ })
+ .for_each(|f| {
+ #[allow(unused_mut)]
+ let mut meta: FnMetadata = f.into();
+ #[cfg(not(feature = "no_module"))]
+ {
+ meta.namespace = crate::FnNamespace::Global;
+ }
+ global.functions.push(meta);
+ });
+
+ #[cfg(not(feature = "no_function"))]
+ if let Some(ast) = _ast {
+ for f in ast.shared_lib().iter_fn() {
+ #[allow(unused_mut)]
+ let mut meta: FnMetadata = f.into();
+ #[cfg(not(feature = "no_module"))]
+ {
+ meta.namespace = crate::FnNamespace::Global;
+ }
+ global.functions.push(meta);
+ }
+ }
+
+ global.functions.sort();
+
+ #[cfg(feature = "metadata")]
+ if let Some(ast) = _ast {
+ if !ast.doc().is_empty() {
+ if !global_doc.is_empty() {
+ global_doc.push('\n');
+ }
+ global_doc.push_str(ast.doc());
+ }
+ }
+
+ #[cfg(feature = "metadata")]
+ {
+ global.doc = &global_doc;
+ }
+
+ serde_json::to_string_pretty(&global)
+}
+
+#[cfg(feature = "internals")]
+impl crate::api::definitions::Definitions<'_> {
+ /// Generate a list of all functions in JSON format.
+ ///
+ /// Functions from the following sources are included:
+ /// 1) Functions defined in an [`AST`][crate::AST]
+ /// 2) Functions registered into the global namespace
+ /// 3) Functions in static modules
+ /// 4) Functions in registered global packages
+ /// 5) Functions in standard packages (optional)
+ #[inline(always)]
+ pub fn json(&self) -> serde_json::Result<String> {
+ gen_metadata_to_json(self.engine(), None, self.config().include_standard_packages)
+ }
+}
+
+impl Engine {
+ /// _(metadata)_ Generate a list of all functions (including those defined in an
+ /// [`AST`][crate::AST]) in JSON format.
+ /// Exported under the `metadata` feature only.
+ ///
+ /// Functions from the following sources are included:
+ /// 1) Functions defined in an [`AST`][crate::AST]
+ /// 2) Functions registered into the global namespace
+ /// 3) Functions in static modules
+ /// 4) Functions in registered global packages
+ /// 5) Functions in standard packages (optional)
+ #[inline(always)]
+ pub fn gen_fn_metadata_with_ast_to_json(
+ &self,
+ ast: &AST,
+ include_standard_packages: bool,
+ ) -> serde_json::Result<String> {
+ gen_metadata_to_json(self, Some(ast), include_standard_packages)
+ }
+
+ /// Generate a list of all functions in JSON format.
+ /// Exported under the `metadata` feature only.
+ ///
+ /// Functions from the following sources are included:
+ /// 1) Functions registered into the global namespace
+ /// 2) Functions in static modules
+ /// 3) Functions in registered global packages
+ /// 4) Functions in standard packages (optional)
+ #[inline(always)]
+ pub fn gen_fn_metadata_to_json(
+ &self,
+ include_standard_packages: bool,
+ ) -> serde_json::Result<String> {
+ gen_metadata_to_json(self, None, include_standard_packages)
+ }
+}
diff --git a/rhai/src/serde/mod.rs b/rhai/src/serde/mod.rs
new file mode 100644
index 0000000..4c78a93
--- /dev/null
+++ b/rhai/src/serde/mod.rs
@@ -0,0 +1,11 @@
+//! _(serde)_ Serialization and deserialization support for [`serde`](https://crates.io/crates/serde).
+//! Exported under the `serde` feature only.
+
+mod de;
+mod deserialize;
+mod metadata;
+mod ser;
+mod serialize;
+
+pub use de::{from_dynamic, DynamicDeserializer};
+pub use ser::{to_dynamic, DynamicSerializer};
diff --git a/rhai/src/serde/ser.rs b/rhai/src/serde/ser.rs
new file mode 100644
index 0000000..c4030db
--- /dev/null
+++ b/rhai/src/serde/ser.rs
@@ -0,0 +1,745 @@
+//! Implement serialization support of [`Dynamic`][crate::Dynamic] for [`serde`].
+
+use crate::{Dynamic, Identifier, Position, RhaiError, RhaiResult, RhaiResultOf, ERR, INT};
+use serde::ser::{
+ Error, SerializeMap, SerializeSeq, SerializeStruct, SerializeTuple, SerializeTupleStruct,
+};
+use serde::{Serialize, Serializer};
+use std::fmt;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(feature = "decimal")]
+use num_traits::FromPrimitive;
+
+/// Serializer for [`Dynamic`][crate::Dynamic].
+pub struct DynamicSerializer {
+ /// Buffer to hold a temporary key.
+ _key: Identifier,
+ /// Buffer to hold a temporary value.
+ _value: Dynamic,
+}
+
+impl DynamicSerializer {
+ /// Create a [`DynamicSerializer`] from a [`Dynamic`][crate::Dynamic] value.
+ #[must_use]
+ pub const fn new(value: Dynamic) -> Self {
+ Self {
+ _key: Identifier::new_const(),
+ _value: value,
+ }
+ }
+}
+
+/// Serialize a Rust type that implements [`serde::Serialize`] into a [`Dynamic`][crate::Dynamic].
+///
+/// # Example
+///
+/// ```
+/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+/// # #[cfg(not(feature = "no_index"))]
+/// # #[cfg(not(feature = "no_object"))]
+/// # #[cfg(not(feature = "no_float"))]
+/// # #[cfg(not(feature = "f32_float"))]
+/// # {
+/// use rhai::{Dynamic, Array, Map};
+/// use rhai::serde::to_dynamic;
+/// use serde::Serialize;
+///
+/// #[derive(Debug, serde::Serialize, PartialEq)]
+/// struct Point {
+/// x: f64,
+/// y: f64
+/// }
+///
+/// #[derive(Debug, serde::Serialize, PartialEq)]
+/// struct MyStruct {
+/// a: i64,
+/// b: Vec<String>,
+/// c: bool,
+/// d: Point
+/// }
+///
+/// let x = MyStruct {
+/// a: 42,
+/// b: vec![ "hello".into(), "world".into() ],
+/// c: true,
+/// d: Point { x: 123.456, y: 999.0 }
+/// };
+///
+/// // Convert the 'MyStruct' into a 'Dynamic'
+/// let value = to_dynamic(x)?;
+///
+/// assert!(value.is::<Map>());
+///
+/// let map = value.cast::<Map>();
+/// let point = map["d"].read_lock::<Map>().unwrap();
+/// assert_eq!(*point["x"].read_lock::<f64>().unwrap(), 123.456);
+/// assert_eq!(*point["y"].read_lock::<f64>().unwrap(), 999.0);
+/// # }
+/// # Ok(())
+/// # }
+/// ```
+pub fn to_dynamic<T: Serialize>(value: T) -> RhaiResult {
+ let mut s = DynamicSerializer::new(Dynamic::UNIT);
+ value.serialize(&mut s)
+}
+
+impl Error for RhaiError {
+ fn custom<T: fmt::Display>(err: T) -> Self {
+ ERR::ErrorRuntime(err.to_string().into(), Position::NONE).into()
+ }
+}
+
+impl Serializer for &mut DynamicSerializer {
+ type Ok = Dynamic;
+ type Error = RhaiError;
+ type SerializeSeq = DynamicSerializer;
+ type SerializeTuple = DynamicSerializer;
+ type SerializeTupleStruct = DynamicSerializer;
+ #[cfg(not(feature = "no_object"))]
+ #[cfg(not(feature = "no_index"))]
+ type SerializeTupleVariant = TupleVariantSerializer;
+ #[cfg(any(feature = "no_object", feature = "no_index"))]
+ type SerializeTupleVariant = serde::ser::Impossible<Dynamic, RhaiError>;
+ type SerializeMap = DynamicSerializer;
+ type SerializeStruct = DynamicSerializer;
+ #[cfg(not(feature = "no_object"))]
+ type SerializeStructVariant = StructVariantSerializer;
+ #[cfg(feature = "no_object")]
+ type SerializeStructVariant = serde::ser::Impossible<Dynamic, RhaiError>;
+
+ #[inline(always)]
+ fn serialize_bool(self, v: bool) -> RhaiResultOf<Self::Ok> {
+ Ok(v.into())
+ }
+
+ #[inline(always)]
+ fn serialize_i8(self, v: i8) -> RhaiResultOf<Self::Ok> {
+ Ok(INT::from(v).into())
+ }
+
+ #[inline(always)]
+ fn serialize_i16(self, v: i16) -> RhaiResultOf<Self::Ok> {
+ Ok(INT::from(v).into())
+ }
+
+ #[inline(always)]
+ fn serialize_i32(self, v: i32) -> RhaiResultOf<Self::Ok> {
+ Ok(INT::from(v).into())
+ }
+
+ #[inline]
+ fn serialize_i64(self, v: i64) -> RhaiResultOf<Self::Ok> {
+ #[cfg(not(feature = "only_i32"))]
+ return Ok(v.into());
+
+ #[cfg(feature = "only_i32")]
+ if v <= INT::MAX as i64 {
+ return Ok(Dynamic::from(v as INT));
+ }
+
+ #[allow(unreachable_code)]
+ {
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_i64(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ return Ok(Dynamic::from_float(v as crate::FLOAT));
+
+ Err(Error::custom(format!("integer number too large: {v}")))
+ }
+ }
+
+ #[inline]
+ fn serialize_i128(self, v: i128) -> RhaiResultOf<Self::Ok> {
+ if v <= i128::from(INT::MAX) {
+ return Ok(Dynamic::from(v as INT));
+ }
+
+ #[allow(unreachable_code)]
+ {
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_i128(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ return Ok(Dynamic::from_float(v as crate::FLOAT));
+
+ Err(Error::custom(format!("integer number too large: {v}")))
+ }
+ }
+
+ #[inline(always)]
+ fn serialize_u8(self, v: u8) -> RhaiResultOf<Self::Ok> {
+ Ok(INT::from(v).into())
+ }
+
+ #[inline(always)]
+ fn serialize_u16(self, v: u16) -> RhaiResultOf<Self::Ok> {
+ Ok(INT::from(v).into())
+ }
+
+ #[inline]
+ fn serialize_u32(self, v: u32) -> RhaiResultOf<Self::Ok> {
+ #[cfg(not(feature = "only_i32"))]
+ return Ok(Dynamic::from(v as INT));
+
+ #[cfg(feature = "only_i32")]
+ if v <= INT::MAX as u32 {
+ return Ok(Dynamic::from(v as INT));
+ }
+
+ #[allow(unreachable_code)]
+ {
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_u32(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ return Ok(Dynamic::from_float(v as crate::FLOAT));
+
+ Err(Error::custom(format!("integer number too large: {v}")))
+ }
+ }
+
+ #[inline]
+ fn serialize_u64(self, v: u64) -> RhaiResultOf<Self::Ok> {
+ if v <= INT::MAX as u64 {
+ return Ok(Dynamic::from(v as INT));
+ }
+
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_u64(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ return Ok(Dynamic::from_float(v as crate::FLOAT));
+
+ #[allow(unreachable_code)]
+ Err(Error::custom(format!("integer number too large: {v}")))
+ }
+
+ #[inline]
+ fn serialize_u128(self, v: u128) -> RhaiResultOf<Self::Ok> {
+ if v <= INT::MAX as u128 {
+ return Ok(Dynamic::from(v as INT));
+ }
+
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_u128(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ return Ok(Dynamic::from_float(v as crate::FLOAT));
+
+ #[allow(unreachable_code)]
+ Err(Error::custom(format!("integer number too large: {v}")))
+ }
+
+ #[inline(always)]
+ fn serialize_f32(self, v: f32) -> RhaiResultOf<Self::Ok> {
+ #[cfg(not(feature = "no_float"))]
+ return Ok((v as crate::FLOAT).into());
+
+ #[allow(unreachable_code)]
+ {
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_f32(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ Err(Error::custom(format!(
+ "floating-point number is not supported: {v}"
+ )))
+ }
+ }
+
+ #[inline(always)]
+ fn serialize_f64(self, v: f64) -> RhaiResultOf<Self::Ok> {
+ #[cfg(not(feature = "no_float"))]
+ return Ok((v as crate::FLOAT).into());
+
+ #[allow(unreachable_code)]
+ {
+ #[cfg(feature = "decimal")]
+ if let Some(n) = rust_decimal::Decimal::from_f64(v) {
+ return Ok(Dynamic::from_decimal(n));
+ }
+
+ Err(Error::custom(format!(
+ "floating-point number is not supported: {v}"
+ )))
+ }
+ }
+
+ #[inline(always)]
+ fn serialize_char(self, v: char) -> RhaiResultOf<Self::Ok> {
+ Ok(v.into())
+ }
+
+ #[inline(always)]
+ fn serialize_str(self, v: &str) -> RhaiResultOf<Self::Ok> {
+ Ok(v.into())
+ }
+
+ #[inline]
+ fn serialize_bytes(self, _v: &[u8]) -> RhaiResultOf<Self::Ok> {
+ #[cfg(not(feature = "no_index"))]
+ return Ok(Dynamic::from_blob(_v.to_vec()));
+
+ #[cfg(feature = "no_index")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "BLOB's are not supported under 'no_index'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ #[inline(always)]
+ fn serialize_none(self) -> RhaiResultOf<Self::Ok> {
+ Ok(Dynamic::UNIT)
+ }
+
+ #[inline(always)]
+ fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> RhaiResultOf<Self::Ok> {
+ value.serialize(&mut *self)
+ }
+
+ #[inline(always)]
+ fn serialize_unit(self) -> RhaiResultOf<Self::Ok> {
+ Ok(Dynamic::UNIT)
+ }
+
+ #[inline(always)]
+ fn serialize_unit_struct(self, _name: &'static str) -> RhaiResultOf<Self::Ok> {
+ self.serialize_unit()
+ }
+
+ #[inline(always)]
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ ) -> RhaiResultOf<Self::Ok> {
+ self.serialize_str(variant)
+ }
+
+ #[inline(always)]
+ fn serialize_newtype_struct<T: ?Sized + Serialize>(
+ self,
+ _name: &'static str,
+ value: &T,
+ ) -> RhaiResultOf<Self::Ok> {
+ value.serialize(&mut *self)
+ }
+
+ #[inline]
+ fn serialize_newtype_variant<T: ?Sized + Serialize>(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _value: &T,
+ ) -> RhaiResultOf<Self::Ok> {
+ #[cfg(not(feature = "no_object"))]
+ return Ok(make_variant(_variant, to_dynamic(_value)?));
+ #[cfg(feature = "no_object")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "object maps are not supported under 'no_object'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ #[inline]
+ fn serialize_seq(self, _len: Option<usize>) -> RhaiResultOf<Self::SerializeSeq> {
+ #[cfg(not(feature = "no_index"))]
+ return Ok(DynamicSerializer::new(crate::Array::new().into()));
+ #[cfg(feature = "no_index")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "arrays are not supported under 'no_index'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ #[inline(always)]
+ fn serialize_tuple(self, len: usize) -> RhaiResultOf<Self::SerializeTuple> {
+ self.serialize_seq(Some(len))
+ }
+
+ #[inline(always)]
+ fn serialize_tuple_struct(
+ self,
+ _name: &'static str,
+ len: usize,
+ ) -> RhaiResultOf<Self::SerializeTupleStruct> {
+ self.serialize_seq(Some(len))
+ }
+
+ #[inline]
+ fn serialize_tuple_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> RhaiResultOf<Self::SerializeTupleVariant> {
+ #[cfg(not(feature = "no_object"))]
+ #[cfg(not(feature = "no_index"))]
+ return Ok(TupleVariantSerializer {
+ variant: _variant,
+ array: crate::Array::with_capacity(_len),
+ });
+ #[cfg(any(feature = "no_object", feature = "no_index"))]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "tuples are not supported under 'no_index' or 'no_object'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ #[inline]
+ fn serialize_map(self, _len: Option<usize>) -> RhaiResultOf<Self::SerializeMap> {
+ #[cfg(not(feature = "no_object"))]
+ return Ok(DynamicSerializer::new(crate::Map::new().into()));
+ #[cfg(feature = "no_object")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "object maps are not supported under 'no_object'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ #[inline(always)]
+ fn serialize_struct(
+ self,
+ _name: &'static str,
+ len: usize,
+ ) -> RhaiResultOf<Self::SerializeStruct> {
+ self.serialize_map(Some(len))
+ }
+
+ #[inline]
+ fn serialize_struct_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> RhaiResultOf<Self::SerializeStructVariant> {
+ #[cfg(not(feature = "no_object"))]
+ return Ok(StructVariantSerializer {
+ variant: _variant,
+ map: crate::Map::new(),
+ });
+ #[cfg(feature = "no_object")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "object maps are not supported under 'no_object'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+}
+
+impl SerializeSeq for DynamicSerializer {
+ type Ok = Dynamic;
+ type Error = RhaiError;
+
+ fn serialize_element<T: ?Sized + Serialize>(&mut self, _value: &T) -> RhaiResultOf<()> {
+ #[cfg(not(feature = "no_index"))]
+ {
+ let value = _value.serialize(&mut *self)?;
+ let arr = self._value.downcast_mut::<crate::Array>().unwrap();
+ arr.push(value);
+ Ok(())
+ }
+ #[cfg(feature = "no_index")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "arrays are not supported under 'no_index'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ // Close the sequence.
+ #[inline]
+ fn end(self) -> RhaiResultOf<Self::Ok> {
+ #[cfg(not(feature = "no_index"))]
+ return Ok(self._value);
+ #[cfg(feature = "no_index")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "arrays are not supported under 'no_index'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+}
+
+impl SerializeTuple for DynamicSerializer {
+ type Ok = Dynamic;
+ type Error = RhaiError;
+
+ fn serialize_element<T: ?Sized + Serialize>(&mut self, _value: &T) -> RhaiResultOf<()> {
+ #[cfg(not(feature = "no_index"))]
+ {
+ let value = _value.serialize(&mut *self)?;
+ let arr = self._value.downcast_mut::<crate::Array>().unwrap();
+ arr.push(value);
+ Ok(())
+ }
+ #[cfg(feature = "no_index")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "tuples are not supported under 'no_index'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ #[inline]
+ fn end(self) -> RhaiResultOf<Self::Ok> {
+ #[cfg(not(feature = "no_index"))]
+ return Ok(self._value);
+ #[cfg(feature = "no_index")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "tuples are not supported under 'no_index'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+}
+
+impl SerializeTupleStruct for DynamicSerializer {
+ type Ok = Dynamic;
+ type Error = RhaiError;
+
+ fn serialize_field<T: ?Sized + Serialize>(&mut self, _value: &T) -> RhaiResultOf<()> {
+ #[cfg(not(feature = "no_index"))]
+ {
+ let value = _value.serialize(&mut *self)?;
+ let arr = self._value.downcast_mut::<crate::Array>().unwrap();
+ arr.push(value);
+ Ok(())
+ }
+ #[cfg(feature = "no_index")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "tuples are not supported under 'no_index'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ #[inline]
+ fn end(self) -> RhaiResultOf<Self::Ok> {
+ #[cfg(not(feature = "no_index"))]
+ return Ok(self._value);
+ #[cfg(feature = "no_index")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "tuples are not supported under 'no_index'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+}
+
+impl SerializeMap for DynamicSerializer {
+ type Ok = Dynamic;
+ type Error = RhaiError;
+
+ fn serialize_key<T: ?Sized + Serialize>(&mut self, _key: &T) -> RhaiResultOf<()> {
+ #[cfg(not(feature = "no_object"))]
+ {
+ let key = _key.serialize(&mut *self)?;
+ self._key = key
+ .into_immutable_string()
+ .map_err(|typ| {
+ ERR::ErrorMismatchDataType("string".into(), typ.into(), Position::NONE)
+ })?
+ .into();
+ Ok(())
+ }
+ #[cfg(feature = "no_object")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "object maps are not supported under 'no_object'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ fn serialize_value<T: ?Sized + Serialize>(&mut self, _value: &T) -> RhaiResultOf<()> {
+ #[cfg(not(feature = "no_object"))]
+ {
+ let key = std::mem::take(&mut self._key);
+ let value = _value.serialize(&mut *self)?;
+ let map = self._value.downcast_mut::<crate::Map>().unwrap();
+ map.insert(key, value);
+ Ok(())
+ }
+ #[cfg(feature = "no_object")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "object maps are not supported under 'no_object'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ fn serialize_entry<K: ?Sized + Serialize, T: ?Sized + Serialize>(
+ &mut self,
+ _key: &K,
+ _value: &T,
+ ) -> RhaiResultOf<()> {
+ #[cfg(not(feature = "no_object"))]
+ {
+ let key: Dynamic = _key.serialize(&mut *self)?;
+ let key = key.into_immutable_string().map_err(|typ| {
+ ERR::ErrorMismatchDataType("string".into(), typ.into(), Position::NONE)
+ })?;
+ let value = _value.serialize(&mut *self)?;
+ let map = self._value.downcast_mut::<crate::Map>().unwrap();
+ map.insert(key.into(), value);
+ Ok(())
+ }
+ #[cfg(feature = "no_object")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "object maps are not supported under 'no_object'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ #[inline]
+ fn end(self) -> RhaiResultOf<Self::Ok> {
+ #[cfg(not(feature = "no_object"))]
+ return Ok(self._value);
+ #[cfg(feature = "no_object")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "object maps are not supported under 'no_object'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+}
+
+impl SerializeStruct for DynamicSerializer {
+ type Ok = Dynamic;
+ type Error = RhaiError;
+
+ fn serialize_field<T: ?Sized + Serialize>(
+ &mut self,
+ _key: &'static str,
+ _value: &T,
+ ) -> RhaiResultOf<()> {
+ #[cfg(not(feature = "no_object"))]
+ {
+ let value = _value.serialize(&mut *self)?;
+ let map = self._value.downcast_mut::<crate::Map>().unwrap();
+ map.insert(_key.into(), value);
+ Ok(())
+ }
+ #[cfg(feature = "no_object")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "object maps are not supported under 'no_object'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+
+ #[inline]
+ fn end(self) -> RhaiResultOf<Self::Ok> {
+ #[cfg(not(feature = "no_object"))]
+ return Ok(self._value);
+ #[cfg(feature = "no_object")]
+ return Err(ERR::ErrorMismatchDataType(
+ "".into(),
+ "object maps are not supported under 'no_object'".into(),
+ Position::NONE,
+ )
+ .into());
+ }
+}
+
+#[cfg(not(feature = "no_object"))]
+#[cfg(not(feature = "no_index"))]
+pub struct TupleVariantSerializer {
+ variant: &'static str,
+ array: crate::Array,
+}
+
+#[cfg(not(feature = "no_object"))]
+#[cfg(not(feature = "no_index"))]
+impl serde::ser::SerializeTupleVariant for TupleVariantSerializer {
+ type Ok = Dynamic;
+ type Error = RhaiError;
+
+ fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> RhaiResultOf<()> {
+ let value = to_dynamic(value)?;
+ self.array.push(value);
+ Ok(())
+ }
+
+ #[inline]
+ fn end(self) -> RhaiResultOf<Self::Ok> {
+ Ok(make_variant(self.variant, self.array.into()))
+ }
+}
+
+#[cfg(not(feature = "no_object"))]
+pub struct StructVariantSerializer {
+ variant: &'static str,
+ map: crate::Map,
+}
+
+#[cfg(not(feature = "no_object"))]
+impl serde::ser::SerializeStructVariant for StructVariantSerializer {
+ type Ok = Dynamic;
+ type Error = RhaiError;
+
+ #[inline]
+ fn serialize_field<T: ?Sized + Serialize>(
+ &mut self,
+ key: &'static str,
+ value: &T,
+ ) -> RhaiResultOf<()> {
+ let value = to_dynamic(value)?;
+ self.map.insert(key.into(), value);
+ Ok(())
+ }
+
+ #[inline]
+ fn end(self) -> RhaiResultOf<Self::Ok> {
+ Ok(make_variant(self.variant, self.map.into()))
+ }
+}
+
+#[cfg(not(feature = "no_object"))]
+#[inline]
+fn make_variant(variant: &'static str, value: Dynamic) -> Dynamic {
+ let mut map = crate::Map::new();
+ map.insert(variant.into(), value);
+ map.into()
+}
diff --git a/rhai/src/serde/serialize.rs b/rhai/src/serde/serialize.rs
new file mode 100644
index 0000000..580bb34
--- /dev/null
+++ b/rhai/src/serde/serialize.rs
@@ -0,0 +1,119 @@
+//! Implementations of [`serde::Serialize`].
+
+use crate::types::dynamic::Union;
+use crate::{Dynamic, ImmutableString, Scope};
+use serde::{ser::SerializeSeq, Serialize, Serializer};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+#[cfg(not(feature = "no_object"))]
+use serde::ser::SerializeMap;
+
+#[cfg(not(feature = "no_time"))]
+use crate::types::dynamic::Variant;
+
+impl Serialize for Dynamic {
+ fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
+ match self.0 {
+ Union::Unit(..) => ser.serialize_unit(),
+ Union::Bool(x, ..) => ser.serialize_bool(x),
+ Union::Str(ref s, ..) => ser.serialize_str(s.as_str()),
+ Union::Char(c, ..) => ser.serialize_char(c),
+
+ #[cfg(not(feature = "only_i32"))]
+ Union::Int(x, ..) => ser.serialize_i64(x),
+ #[cfg(feature = "only_i32")]
+ Union::Int(x, ..) => ser.serialize_i32(x),
+
+ #[cfg(not(feature = "no_float"))]
+ #[cfg(not(feature = "f32_float"))]
+ Union::Float(x, ..) => ser.serialize_f64(*x),
+ #[cfg(not(feature = "no_float"))]
+ #[cfg(feature = "f32_float")]
+ Union::Float(x, ..) => ser.serialize_f32(*x),
+
+ #[cfg(feature = "decimal")]
+ #[cfg(not(feature = "f32_float"))]
+ Union::Decimal(ref x, ..) => {
+ use rust_decimal::prelude::ToPrimitive;
+
+ match x.to_f64() {
+ Some(v) => ser.serialize_f64(v),
+ None => ser.serialize_str(&x.to_string()),
+ }
+ }
+ #[cfg(feature = "decimal")]
+ #[cfg(feature = "f32_float")]
+ Union::Decimal(ref x, ..) => {
+ use rust_decimal::prelude::ToPrimitive;
+
+ match x.to_f32() {
+ Some(v) => ser.serialize_f32(v),
+ _ => ser.serialize_str(&x.to_string()),
+ }
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(ref a, ..) => (**a).serialize(ser),
+ #[cfg(not(feature = "no_index"))]
+ Union::Blob(ref a, ..) => ser.serialize_bytes(a),
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(ref m, ..) => {
+ let mut map = ser.serialize_map(Some(m.len()))?;
+ m.iter()
+ .try_for_each(|(k, v)| map.serialize_entry(k.as_str(), v))?;
+ map.end()
+ }
+ Union::FnPtr(ref f, ..) => ser.serialize_str(f.fn_name()),
+ #[cfg(not(feature = "no_time"))]
+ Union::TimeStamp(ref x, ..) => ser.serialize_str(x.as_ref().type_name()),
+
+ Union::Variant(ref v, ..) => ser.serialize_str((***v).type_name()),
+
+ #[cfg(not(feature = "no_closure"))]
+ #[cfg(not(feature = "sync"))]
+ Union::Shared(ref cell, ..) => cell.borrow().serialize(ser),
+ #[cfg(not(feature = "no_closure"))]
+ #[cfg(feature = "sync")]
+ Union::Shared(ref cell, ..) => cell.read().unwrap().serialize(ser),
+ }
+ }
+}
+
+impl Serialize for ImmutableString {
+ #[inline(always)]
+ fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
+ ser.serialize_str(self.as_str())
+ }
+}
+
+impl Serialize for Scope<'_> {
+ #[inline(always)]
+ fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
+ #[derive(Debug, Clone, Hash, Serialize)]
+ struct ScopeEntry<'a> {
+ pub name: &'a str,
+ pub value: &'a Dynamic,
+ #[serde(default, skip_serializing_if = "is_false")]
+ pub is_constant: bool,
+ }
+
+ #[allow(clippy::trivially_copy_pass_by_ref)]
+ fn is_false(value: &bool) -> bool {
+ !value
+ }
+
+ let mut ser = ser.serialize_seq(Some(self.len()))?;
+
+ for (name, is_constant, value) in self.iter_raw() {
+ let entry = ScopeEntry {
+ name,
+ value,
+ is_constant,
+ };
+ ser.serialize_element(&entry)?;
+ }
+
+ ser.end()
+ }
+}
diff --git a/rhai/src/tests.rs b/rhai/src/tests.rs
new file mode 100644
index 0000000..de018e6
--- /dev/null
+++ b/rhai/src/tests.rs
@@ -0,0 +1,72 @@
+//! Module containing unit tests.
+#![cfg(test)]
+
+/// This test is to make sure no code changes increase the sizes of critical data structures.
+#[test]
+fn check_struct_sizes() {
+ use crate::*;
+ use std::mem::size_of;
+
+ const IS_32_BIT: bool = cfg!(target_pointer_width = "32");
+ const PACKED: bool = cfg!(all(
+ target_pointer_width = "32",
+ feature = "only_i32",
+ any(feature = "no_float", feature = "f32_float")
+ ));
+ const WORD_SIZE: usize = size_of::<usize>();
+
+ assert_eq!(size_of::<Dynamic>(), if PACKED { 8 } else { 16 });
+ assert_eq!(size_of::<Option<Dynamic>>(), if PACKED { 8 } else { 16 });
+ assert_eq!(
+ size_of::<Position>(),
+ if cfg!(feature = "no_position") { 0 } else { 4 }
+ );
+ assert_eq!(size_of::<tokenizer::Token>(), 2 * WORD_SIZE);
+ assert_eq!(size_of::<ast::Expr>(), if PACKED { 12 } else { 16 });
+ assert_eq!(size_of::<Option<ast::Expr>>(), if PACKED { 12 } else { 16 });
+ assert_eq!(size_of::<ast::Stmt>(), if IS_32_BIT { 12 } else { 16 });
+ assert_eq!(
+ size_of::<Option<ast::Stmt>>(),
+ if IS_32_BIT { 12 } else { 16 }
+ );
+
+ #[cfg(feature = "internals")]
+ {
+ assert_eq!(size_of::<CallableFunction>(), 3 * WORD_SIZE);
+ assert_eq!(size_of::<module::FuncInfo>(), 4 * WORD_SIZE);
+ }
+
+ // The following only on 64-bit platforms
+
+ if !cfg!(target_pointer_width = "64") {
+ return;
+ }
+
+ assert_eq!(size_of::<Scope>(), 536);
+ assert_eq!(
+ size_of::<FnPtr>(),
+ 80 - if cfg!(feature = "no_function") {
+ WORD_SIZE
+ } else {
+ 0
+ }
+ );
+ assert_eq!(size_of::<LexError>(), 56);
+ assert_eq!(
+ size_of::<ParseError>(),
+ 16 - if cfg!(feature = "no_position") {
+ WORD_SIZE
+ } else {
+ 0
+ }
+ );
+ assert_eq!(size_of::<EvalAltResult>(), 64);
+ assert_eq!(
+ size_of::<NativeCallContext>(),
+ 56 - if cfg!(feature = "no_position") {
+ WORD_SIZE
+ } else {
+ 0
+ }
+ );
+}
diff --git a/rhai/src/tokenizer.rs b/rhai/src/tokenizer.rs
new file mode 100644
index 0000000..66c1d52
--- /dev/null
+++ b/rhai/src/tokenizer.rs
@@ -0,0 +1,2678 @@
+//! Main module defining the lexer and parser.
+
+use crate::engine::Precedence;
+use crate::func::native::OnParseTokenCallback;
+use crate::{Engine, Identifier, LexError, Position, SmartString, StaticVec, INT, UNSIGNED_INT};
+use smallvec::SmallVec;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ cell::RefCell,
+ char, fmt,
+ iter::{FusedIterator, Peekable},
+ num::NonZeroUsize,
+ rc::Rc,
+ str::{Chars, FromStr},
+};
+
+/// _(internals)_ A type containing commands to control the tokenizer.
+#[derive(Debug, Clone, Eq, PartialEq, Default, Hash)]
+pub struct TokenizerControlBlock {
+ /// Is the current tokenizer position within an interpolated text string?
+ ///
+ /// This flag allows switching the tokenizer back to _text_ parsing after an interpolation stream.
+ pub is_within_text: bool,
+ /// Global comments.
+ #[cfg(feature = "metadata")]
+ pub global_comments: String,
+ /// Whitespace-compressed version of the script (if any).
+ ///
+ /// Set to `Some` in order to collect a compressed script.
+ pub compressed: Option<String>,
+}
+
+impl TokenizerControlBlock {
+ /// Create a new `TokenizerControlBlock`.
+ #[inline]
+ #[must_use]
+ pub const fn new() -> Self {
+ Self {
+ is_within_text: false,
+ #[cfg(feature = "metadata")]
+ global_comments: String::new(),
+ compressed: None,
+ }
+ }
+}
+
+/// _(internals)_ A shared object that allows control of the tokenizer from outside.
+pub type TokenizerControl = Rc<RefCell<TokenizerControlBlock>>;
+
+type LERR = LexError;
+
+/// Separator character for numbers.
+const NUMBER_SEPARATOR: char = '_';
+
+/// A stream of tokens.
+pub type TokenStream<'a> = Peekable<TokenIterator<'a>>;
+
+/// _(internals)_ A Rhai language token.
+/// Exported under the `internals` feature only.
+#[derive(Debug, PartialEq, Clone, Hash)]
+#[non_exhaustive]
+pub enum Token {
+ /// An `INT` constant.
+ IntegerConstant(INT),
+ /// A `FLOAT` constant.
+ ///
+ /// Reserved under the `no_float` feature.
+ #[cfg(not(feature = "no_float"))]
+ FloatConstant(crate::types::FloatWrapper<crate::FLOAT>),
+ /// A [`Decimal`][rust_decimal::Decimal] constant.
+ ///
+ /// Requires the `decimal` feature.
+ #[cfg(feature = "decimal")]
+ DecimalConstant(Box<rust_decimal::Decimal>),
+ /// An identifier.
+ Identifier(Box<Identifier>),
+ /// A character constant.
+ CharConstant(char),
+ /// A string constant.
+ StringConstant(Box<SmartString>),
+ /// An interpolated string.
+ InterpolatedString(Box<SmartString>),
+ /// `{`
+ LeftBrace,
+ /// `}`
+ RightBrace,
+ /// `(`
+ LeftParen,
+ /// `)`
+ RightParen,
+ /// `[`
+ LeftBracket,
+ /// `]`
+ RightBracket,
+ /// `()`
+ Unit,
+ /// `+`
+ Plus,
+ /// `+` (unary)
+ UnaryPlus,
+ /// `-`
+ Minus,
+ /// `-` (unary)
+ UnaryMinus,
+ /// `*`
+ Multiply,
+ /// `/`
+ Divide,
+ /// `%`
+ Modulo,
+ /// `**`
+ PowerOf,
+ /// `<<`
+ LeftShift,
+ /// `>>`
+ RightShift,
+ /// `;`
+ SemiColon,
+ /// `:`
+ Colon,
+ /// `::`
+ DoubleColon,
+ /// `=>`
+ DoubleArrow,
+ /// `_`
+ Underscore,
+ /// `,`
+ Comma,
+ /// `.`
+ Period,
+ /// `?.`
+ ///
+ /// Reserved under the `no_object` feature.
+ #[cfg(not(feature = "no_object"))]
+ Elvis,
+ /// `??`
+ DoubleQuestion,
+ /// `?[`
+ ///
+ /// Reserved under the `no_object` feature.
+ #[cfg(not(feature = "no_index"))]
+ QuestionBracket,
+ /// `..`
+ ExclusiveRange,
+ /// `..=`
+ InclusiveRange,
+ /// `#{`
+ MapStart,
+ /// `=`
+ Equals,
+ /// `true`
+ True,
+ /// `false`
+ False,
+ /// `let`
+ Let,
+ /// `const`
+ Const,
+ /// `if`
+ If,
+ /// `else`
+ Else,
+ /// `switch`
+ Switch,
+ /// `do`
+ Do,
+ /// `while`
+ While,
+ /// `until`
+ Until,
+ /// `loop`
+ Loop,
+ /// `for`
+ For,
+ /// `in`
+ In,
+ /// `!in`
+ NotIn,
+ /// `<`
+ LessThan,
+ /// `>`
+ GreaterThan,
+ /// `<=`
+ LessThanEqualsTo,
+ /// `>=`
+ GreaterThanEqualsTo,
+ /// `==`
+ EqualsTo,
+ /// `!=`
+ NotEqualsTo,
+ /// `!`
+ Bang,
+ /// `|`
+ Pipe,
+ /// `||`
+ Or,
+ /// `^`
+ XOr,
+ /// `&`
+ Ampersand,
+ /// `&&`
+ And,
+ /// `fn`
+ ///
+ /// Reserved under the `no_function` feature.
+ #[cfg(not(feature = "no_function"))]
+ Fn,
+ /// `continue`
+ Continue,
+ /// `break`
+ Break,
+ /// `return`
+ Return,
+ /// `throw`
+ Throw,
+ /// `try`
+ Try,
+ /// `catch`
+ Catch,
+ /// `+=`
+ PlusAssign,
+ /// `-=`
+ MinusAssign,
+ /// `*=`
+ MultiplyAssign,
+ /// `/=`
+ DivideAssign,
+ /// `<<=`
+ LeftShiftAssign,
+ /// `>>=`
+ RightShiftAssign,
+ /// `&=`
+ AndAssign,
+ /// `|=`
+ OrAssign,
+ /// `^=`
+ XOrAssign,
+ /// `%=`
+ ModuloAssign,
+ /// `**=`
+ PowerOfAssign,
+ /// `private`
+ ///
+ /// Reserved under the `no_function` feature.
+ #[cfg(not(feature = "no_function"))]
+ Private,
+ /// `import`
+ ///
+ /// Reserved under the `no_module` feature.
+ #[cfg(not(feature = "no_module"))]
+ Import,
+ /// `export`
+ ///
+ /// Reserved under the `no_module` feature.
+ #[cfg(not(feature = "no_module"))]
+ Export,
+ /// `as`
+ ///
+ /// Reserved under the `no_module` feature.
+ #[cfg(not(feature = "no_module"))]
+ As,
+ /// A lexer error.
+ LexError(Box<LexError>),
+ /// A comment block.
+ Comment(Box<String>),
+ /// A reserved symbol.
+ Reserved(Box<SmartString>),
+ /// A custom keyword.
+ ///
+ /// Not available under `no_custom_syntax`.
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Custom(Box<SmartString>),
+ /// End of the input stream.
+ /// Used as a placeholder for the end of input.
+ EOF,
+}
+
+impl fmt::Display for Token {
+ #[inline(always)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ #[allow(clippy::enum_glob_use)]
+ use Token::*;
+
+ match self {
+ IntegerConstant(i) => write!(f, "{i}"),
+ #[cfg(not(feature = "no_float"))]
+ FloatConstant(v) => write!(f, "{v}"),
+ #[cfg(feature = "decimal")]
+ DecimalConstant(d) => write!(f, "{d}"),
+ StringConstant(s) => write!(f, r#""{s}""#),
+ InterpolatedString(..) => f.write_str("string"),
+ CharConstant(c) => write!(f, "{c}"),
+ Identifier(s) => f.write_str(s),
+ Reserved(s) => f.write_str(s),
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Custom(s) => f.write_str(s),
+ LexError(err) => write!(f, "{err}"),
+ Comment(s) => f.write_str(s),
+
+ EOF => f.write_str("{EOF}"),
+
+ token => f.write_str(token.literal_syntax()),
+ }
+ }
+}
+
+// Table-driven keyword recognizer generated by GNU `gperf` on the file `tools/keywords.txt`.
+//
+// When adding new keywords, make sure to update `tools/keywords.txt` and re-generate this.
+
+const MIN_KEYWORD_LEN: usize = 1;
+const MAX_KEYWORD_LEN: usize = 8;
+const MIN_KEYWORD_HASH_VALUE: usize = 1;
+const MAX_KEYWORD_HASH_VALUE: usize = 152;
+
+static KEYWORD_ASSOC_VALUES: [u8; 257] = [
+ 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
+ 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 115, 153, 100, 153, 110,
+ 105, 40, 80, 2, 20, 25, 125, 95, 15, 40, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 55,
+ 35, 10, 5, 0, 30, 110, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
+ 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 120, 105, 100, 85, 90, 153, 125, 5,
+ 0, 125, 35, 10, 100, 153, 20, 0, 153, 10, 0, 45, 55, 0, 153, 50, 55, 5, 0, 153, 0, 0, 35, 153,
+ 45, 50, 30, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
+ 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
+ 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
+ 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
+ 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
+ 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
+ 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
+ 153,
+];
+static KEYWORDS_LIST: [(&str, Token); 153] = [
+ ("", Token::EOF),
+ (">", Token::GreaterThan),
+ (">=", Token::GreaterThanEqualsTo),
+ (")", Token::RightParen),
+ ("", Token::EOF),
+ ("const", Token::Const),
+ ("=", Token::Equals),
+ ("==", Token::EqualsTo),
+ ("continue", Token::Continue),
+ ("", Token::EOF),
+ ("catch", Token::Catch),
+ ("<", Token::LessThan),
+ ("<=", Token::LessThanEqualsTo),
+ ("for", Token::For),
+ ("loop", Token::Loop),
+ ("", Token::EOF),
+ (".", Token::Period),
+ ("<<", Token::LeftShift),
+ ("<<=", Token::LeftShiftAssign),
+ ("", Token::EOF),
+ ("false", Token::False),
+ ("*", Token::Multiply),
+ ("*=", Token::MultiplyAssign),
+ ("let", Token::Let),
+ ("", Token::EOF),
+ ("while", Token::While),
+ ("+", Token::Plus),
+ ("+=", Token::PlusAssign),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("throw", Token::Throw),
+ ("}", Token::RightBrace),
+ (">>", Token::RightShift),
+ (">>=", Token::RightShiftAssign),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ (";", Token::SemiColon),
+ ("=>", Token::DoubleArrow),
+ ("", Token::EOF),
+ ("else", Token::Else),
+ ("", Token::EOF),
+ ("/", Token::Divide),
+ ("/=", Token::DivideAssign),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("{", Token::LeftBrace),
+ ("**", Token::PowerOf),
+ ("**=", Token::PowerOfAssign),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("|", Token::Pipe),
+ ("|=", Token::OrAssign),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ (":", Token::Colon),
+ ("..", Token::ExclusiveRange),
+ ("..=", Token::InclusiveRange),
+ ("", Token::EOF),
+ ("until", Token::Until),
+ ("switch", Token::Switch),
+ #[cfg(not(feature = "no_function"))]
+ ("private", Token::Private),
+ #[cfg(feature = "no_function")]
+ ("", Token::EOF),
+ ("try", Token::Try),
+ ("true", Token::True),
+ ("break", Token::Break),
+ ("return", Token::Return),
+ #[cfg(not(feature = "no_function"))]
+ ("fn", Token::Fn),
+ #[cfg(feature = "no_function")]
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ #[cfg(not(feature = "no_module"))]
+ ("import", Token::Import),
+ #[cfg(feature = "no_module")]
+ ("", Token::EOF),
+ #[cfg(not(feature = "no_object"))]
+ ("?.", Token::Elvis),
+ #[cfg(feature = "no_object")]
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ #[cfg(not(feature = "no_module"))]
+ ("export", Token::Export),
+ #[cfg(feature = "no_module")]
+ ("", Token::EOF),
+ ("in", Token::In),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("(", Token::LeftParen),
+ ("||", Token::Or),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("^", Token::XOr),
+ ("^=", Token::XOrAssign),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("_", Token::Underscore),
+ ("::", Token::DoubleColon),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("-", Token::Minus),
+ ("-=", Token::MinusAssign),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("]", Token::RightBracket),
+ ("()", Token::Unit),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("&", Token::Ampersand),
+ ("&=", Token::AndAssign),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("%", Token::Modulo),
+ ("%=", Token::ModuloAssign),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("!", Token::Bang),
+ ("!=", Token::NotEqualsTo),
+ ("!in", Token::NotIn),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("[", Token::LeftBracket),
+ ("if", Token::If),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ (",", Token::Comma),
+ ("do", Token::Do),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ #[cfg(not(feature = "no_module"))]
+ ("as", Token::As),
+ #[cfg(feature = "no_module")]
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ #[cfg(not(feature = "no_index"))]
+ ("?[", Token::QuestionBracket),
+ #[cfg(feature = "no_index")]
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("??", Token::DoubleQuestion),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("&&", Token::And),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("", Token::EOF),
+ ("#{", Token::MapStart),
+];
+
+// Table-driven reserved symbol recognizer generated by GNU `gperf` on the file `tools/reserved.txt`.
+//
+// When adding new reserved symbols, make sure to update `tools/reserved.txt` and re-generate this.
+
+const MIN_RESERVED_LEN: usize = 1;
+const MAX_RESERVED_LEN: usize = 10;
+const MIN_RESERVED_HASH_VALUE: usize = 1;
+const MAX_RESERVED_HASH_VALUE: usize = 149;
+
+static RESERVED_ASSOC_VALUES: [u8; 256] = [
+ 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150,
+ 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 10, 150, 5, 35, 150, 150,
+ 150, 45, 35, 30, 30, 150, 20, 15, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 35,
+ 30, 15, 5, 25, 0, 25, 150, 150, 150, 150, 150, 65, 150, 150, 150, 150, 150, 150, 150, 150, 150,
+ 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 40, 150, 150, 150, 150, 150, 0, 150, 0,
+ 0, 0, 15, 45, 10, 15, 150, 150, 35, 25, 10, 50, 0, 150, 5, 0, 15, 0, 5, 25, 45, 15, 150, 150,
+ 25, 150, 20, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150,
+ 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150,
+ 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150,
+ 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150,
+ 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150,
+ 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150,
+ 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150,
+];
+static RESERVED_LIST: [(&str, bool, bool, bool); 150] = [
+ ("", false, false, false),
+ ("?", true, false, false),
+ ("as", cfg!(feature = "no_module"), false, false),
+ ("use", true, false, false),
+ ("case", true, false, false),
+ ("async", true, false, false),
+ ("public", true, false, false),
+ ("package", true, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("super", true, false, false),
+ ("#", true, false, false),
+ ("private", cfg!(feature = "no_function"), false, false),
+ ("var", true, false, false),
+ ("protected", true, false, false),
+ ("spawn", true, false, false),
+ ("shared", true, false, false),
+ ("is", true, false, false),
+ ("===", true, false, false),
+ ("sync", true, false, false),
+ ("curry", true, true, true),
+ ("static", true, false, false),
+ ("default", true, false, false),
+ ("!==", true, false, false),
+ ("is_shared", cfg!(not(feature = "no_closure")), true, true),
+ ("print", true, true, false),
+ ("", false, false, false),
+ ("#!", true, false, false),
+ ("", false, false, false),
+ ("this", true, false, false),
+ ("is_def_var", true, true, false),
+ ("thread", true, false, false),
+ ("?.", cfg!(feature = "no_object"), false, false),
+ ("", false, false, false),
+ ("is_def_fn", cfg!(not(feature = "no_function")), true, false),
+ ("yield", true, false, false),
+ ("", false, false, false),
+ ("fn", cfg!(feature = "no_function"), false, false),
+ ("new", true, false, false),
+ ("call", true, true, true),
+ ("match", true, false, false),
+ ("~", true, false, false),
+ ("!.", true, false, false),
+ ("", false, false, false),
+ ("eval", true, true, false),
+ ("await", true, false, false),
+ ("", false, false, false),
+ (":=", true, false, false),
+ ("...", true, false, false),
+ ("null", true, false, false),
+ ("debug", true, true, false),
+ ("@", true, false, false),
+ ("type_of", true, true, true),
+ ("", false, false, false),
+ ("with", true, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("<-", true, false, false),
+ ("", false, false, false),
+ ("void", true, false, false),
+ ("", false, false, false),
+ ("import", cfg!(feature = "no_module"), false, false),
+ ("--", true, false, false),
+ ("nil", true, false, false),
+ ("exit", true, false, false),
+ ("", false, false, false),
+ ("export", cfg!(feature = "no_module"), false, false),
+ ("<|", true, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("$", true, false, false),
+ ("->", true, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("|>", true, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("module", true, false, false),
+ ("?[", cfg!(feature = "no_index"), false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("Fn", true, true, false),
+ ("::<", true, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("++", true, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ (":;", true, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("*)", true, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("(*", true, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("", false, false, false),
+ ("go", true, false, false),
+ ("", false, false, false),
+ ("goto", true, false, false),
+];
+
+impl Token {
+ /// Is the token a literal symbol?
+ #[must_use]
+ pub const fn is_literal(&self) -> bool {
+ #[allow(clippy::enum_glob_use)]
+ use Token::*;
+
+ match self {
+ IntegerConstant(..) => false,
+ #[cfg(not(feature = "no_float"))]
+ FloatConstant(..) => false,
+ #[cfg(feature = "decimal")]
+ DecimalConstant(..) => false,
+ StringConstant(..)
+ | InterpolatedString(..)
+ | CharConstant(..)
+ | Identifier(..)
+ | Reserved(..) => false,
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Custom(..) => false,
+ LexError(..) | Comment(..) => false,
+
+ EOF => false,
+
+ _ => true,
+ }
+ }
+ /// Get the literal syntax of the token.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the token is not a literal symbol.
+ #[must_use]
+ pub const fn literal_syntax(&self) -> &'static str {
+ #[allow(clippy::enum_glob_use)]
+ use Token::*;
+
+ match self {
+ LeftBrace => "{",
+ RightBrace => "}",
+ LeftParen => "(",
+ RightParen => ")",
+ LeftBracket => "[",
+ RightBracket => "]",
+ Unit => "()",
+ Plus => "+",
+ UnaryPlus => "+",
+ Minus => "-",
+ UnaryMinus => "-",
+ Multiply => "*",
+ Divide => "/",
+ SemiColon => ";",
+ Colon => ":",
+ DoubleColon => "::",
+ DoubleArrow => "=>",
+ Underscore => "_",
+ Comma => ",",
+ Period => ".",
+ #[cfg(not(feature = "no_object"))]
+ Elvis => "?.",
+ DoubleQuestion => "??",
+ #[cfg(not(feature = "no_index"))]
+ QuestionBracket => "?[",
+ ExclusiveRange => "..",
+ InclusiveRange => "..=",
+ MapStart => "#{",
+ Equals => "=",
+ True => "true",
+ False => "false",
+ Let => "let",
+ Const => "const",
+ If => "if",
+ Else => "else",
+ Switch => "switch",
+ Do => "do",
+ While => "while",
+ Until => "until",
+ Loop => "loop",
+ For => "for",
+ In => "in",
+ NotIn => "!in",
+ LessThan => "<",
+ GreaterThan => ">",
+ Bang => "!",
+ LessThanEqualsTo => "<=",
+ GreaterThanEqualsTo => ">=",
+ EqualsTo => "==",
+ NotEqualsTo => "!=",
+ Pipe => "|",
+ Or => "||",
+ Ampersand => "&",
+ And => "&&",
+ Continue => "continue",
+ Break => "break",
+ Return => "return",
+ Throw => "throw",
+ Try => "try",
+ Catch => "catch",
+ PlusAssign => "+=",
+ MinusAssign => "-=",
+ MultiplyAssign => "*=",
+ DivideAssign => "/=",
+ LeftShiftAssign => "<<=",
+ RightShiftAssign => ">>=",
+ AndAssign => "&=",
+ OrAssign => "|=",
+ XOrAssign => "^=",
+ LeftShift => "<<",
+ RightShift => ">>",
+ XOr => "^",
+ Modulo => "%",
+ ModuloAssign => "%=",
+ PowerOf => "**",
+ PowerOfAssign => "**=",
+
+ #[cfg(not(feature = "no_function"))]
+ Fn => "fn",
+ #[cfg(not(feature = "no_function"))]
+ Private => "private",
+
+ #[cfg(not(feature = "no_module"))]
+ Import => "import",
+ #[cfg(not(feature = "no_module"))]
+ Export => "export",
+ #[cfg(not(feature = "no_module"))]
+ As => "as",
+
+ _ => panic!("token is not a literal symbol"),
+ }
+ }
+
+ /// Is this token an op-assignment operator?
+ #[inline]
+ #[must_use]
+ pub const fn is_op_assignment(&self) -> bool {
+ #[allow(clippy::enum_glob_use)]
+ use Token::*;
+
+ matches!(
+ self,
+ PlusAssign
+ | MinusAssign
+ | MultiplyAssign
+ | DivideAssign
+ | LeftShiftAssign
+ | RightShiftAssign
+ | ModuloAssign
+ | PowerOfAssign
+ | AndAssign
+ | OrAssign
+ | XOrAssign
+ )
+ }
+
+ /// Get the corresponding operator of the token if it is an op-assignment operator.
+ #[must_use]
+ pub const fn get_base_op_from_assignment(&self) -> Option<Self> {
+ #[allow(clippy::enum_glob_use)]
+ use Token::*;
+
+ Some(match self {
+ PlusAssign => Plus,
+ MinusAssign => Minus,
+ MultiplyAssign => Multiply,
+ DivideAssign => Divide,
+ LeftShiftAssign => LeftShift,
+ RightShiftAssign => RightShift,
+ ModuloAssign => Modulo,
+ PowerOfAssign => PowerOf,
+ AndAssign => Ampersand,
+ OrAssign => Pipe,
+ XOrAssign => XOr,
+ _ => return None,
+ })
+ }
+
+ /// Has this token a corresponding op-assignment operator?
+ #[inline]
+ #[must_use]
+ pub const fn has_op_assignment(&self) -> bool {
+ #[allow(clippy::enum_glob_use)]
+ use Token::*;
+
+ matches!(
+ self,
+ Plus | Minus
+ | Multiply
+ | Divide
+ | LeftShift
+ | RightShift
+ | Modulo
+ | PowerOf
+ | Ampersand
+ | Pipe
+ | XOr
+ )
+ }
+
+ /// Get the corresponding op-assignment operator of the token.
+ #[must_use]
+ pub const fn convert_to_op_assignment(&self) -> Option<Self> {
+ #[allow(clippy::enum_glob_use)]
+ use Token::*;
+
+ Some(match self {
+ Plus => PlusAssign,
+ Minus => MinusAssign,
+ Multiply => MultiplyAssign,
+ Divide => DivideAssign,
+ LeftShift => LeftShiftAssign,
+ RightShift => RightShiftAssign,
+ Modulo => ModuloAssign,
+ PowerOf => PowerOfAssign,
+ Ampersand => AndAssign,
+ Pipe => OrAssign,
+ XOr => XOrAssign,
+ _ => return None,
+ })
+ }
+
+ /// Reverse lookup a symbol token from a piece of syntax.
+ #[inline]
+ #[must_use]
+ pub fn lookup_symbol_from_syntax(syntax: &str) -> Option<Self> {
+ // This implementation is based upon a pre-calculated table generated
+ // by GNU `gperf` on the list of keywords.
+ let utf8 = syntax.as_bytes();
+ let len = utf8.len();
+
+ if !(MIN_KEYWORD_LEN..=MAX_KEYWORD_LEN).contains(&len) {
+ return None;
+ }
+
+ let mut hash_val = len;
+
+ match len {
+ 1 => (),
+ _ => hash_val += KEYWORD_ASSOC_VALUES[(utf8[1] as usize) + 1] as usize,
+ }
+ hash_val += KEYWORD_ASSOC_VALUES[utf8[0] as usize] as usize;
+
+ if !(MIN_KEYWORD_HASH_VALUE..=MAX_KEYWORD_HASH_VALUE).contains(&hash_val) {
+ return None;
+ }
+
+ match KEYWORDS_LIST[hash_val] {
+ (_, Token::EOF) => None,
+ // Fail early to avoid calling memcmp().
+ // Since we are already working with bytes, mind as well check the first one.
+ (s, ref t) if s.len() == len && s.as_bytes()[0] == utf8[0] && s == syntax => {
+ Some(t.clone())
+ }
+ _ => None,
+ }
+ }
+
+ /// If another operator is after these, it's probably a unary operator
+ /// (not sure about `fn` name).
+ #[must_use]
+ pub const fn is_next_unary(&self) -> bool {
+ #[allow(clippy::enum_glob_use)]
+ use Token::*;
+
+ match self {
+ SemiColon | // ; - is unary
+ Colon | // #{ foo: - is unary
+ Comma | // ( ... , -expr ) - is unary
+ //Period |
+ //Elvis |
+ DoubleQuestion | // ?? - is unary
+ ExclusiveRange | // .. - is unary
+ InclusiveRange | // ..= - is unary
+ LeftBrace | // { -expr } - is unary
+ // RightBrace | // { expr } - expr not unary & is closing
+ LeftParen | // ( -expr ) - is unary
+ // RightParen | // ( expr ) - expr not unary & is closing
+ LeftBracket | // [ -expr ] - is unary
+ // RightBracket | // [ expr ] - expr not unary & is closing
+ Plus |
+ PlusAssign |
+ UnaryPlus |
+ Minus |
+ MinusAssign |
+ UnaryMinus |
+ Multiply |
+ MultiplyAssign |
+ Divide |
+ DivideAssign |
+ Modulo |
+ ModuloAssign |
+ PowerOf |
+ PowerOfAssign |
+ LeftShift |
+ LeftShiftAssign |
+ RightShift |
+ RightShiftAssign |
+ Equals |
+ EqualsTo |
+ NotEqualsTo |
+ LessThan |
+ GreaterThan |
+ Bang |
+ LessThanEqualsTo |
+ GreaterThanEqualsTo |
+ Pipe |
+ Ampersand |
+ If |
+ //Do |
+ While |
+ Until |
+ In |
+ NotIn |
+ And |
+ AndAssign |
+ Or |
+ OrAssign |
+ XOr |
+ XOrAssign |
+ Return |
+ Throw => true,
+
+ #[cfg(not(feature = "no_index"))]
+ QuestionBracket => true, // ?[ - is unary
+
+ LexError(..) => true,
+
+ _ => false,
+ }
+ }
+
+ /// Get the precedence number of the token.
+ #[must_use]
+ pub const fn precedence(&self) -> Option<Precedence> {
+ #[allow(clippy::enum_glob_use)]
+ use Token::*;
+
+ Precedence::new(match self {
+ Or | XOr | Pipe => 30,
+
+ And | Ampersand => 60,
+
+ EqualsTo | NotEqualsTo => 90,
+
+ In | NotIn => 110,
+
+ LessThan | LessThanEqualsTo | GreaterThan | GreaterThanEqualsTo => 130,
+
+ DoubleQuestion => 135,
+
+ ExclusiveRange | InclusiveRange => 140,
+
+ Plus | Minus => 150,
+
+ Divide | Multiply | Modulo => 180,
+
+ PowerOf => 190,
+
+ LeftShift | RightShift => 210,
+
+ _ => 0,
+ })
+ }
+
+ /// Does an expression bind to the right (instead of left)?
+ #[must_use]
+ pub const fn is_bind_right(&self) -> bool {
+ #[allow(clippy::enum_glob_use)]
+ use Token::*;
+
+ match self {
+ // Exponentiation binds to the right
+ PowerOf => true,
+
+ _ => false,
+ }
+ }
+
+ /// Is this token a standard symbol used in the language?
+ #[must_use]
+ pub const fn is_standard_symbol(&self) -> bool {
+ #[allow(clippy::enum_glob_use)]
+ use Token::*;
+
+ match self {
+ LeftBrace | RightBrace | LeftParen | RightParen | LeftBracket | RightBracket | Plus
+ | UnaryPlus | Minus | UnaryMinus | Multiply | Divide | Modulo | PowerOf | LeftShift
+ | RightShift | SemiColon | Colon | DoubleColon | Comma | Period | DoubleQuestion
+ | ExclusiveRange | InclusiveRange | MapStart | Equals | LessThan | GreaterThan
+ | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo | Bang | Pipe
+ | Or | XOr | Ampersand | And | PlusAssign | MinusAssign | MultiplyAssign
+ | DivideAssign | LeftShiftAssign | RightShiftAssign | AndAssign | OrAssign
+ | XOrAssign | ModuloAssign | PowerOfAssign => true,
+
+ #[cfg(not(feature = "no_object"))]
+ Elvis => true,
+
+ #[cfg(not(feature = "no_index"))]
+ QuestionBracket => true,
+
+ _ => false,
+ }
+ }
+
+ /// Is this token a standard keyword?
+ #[inline]
+ #[must_use]
+ pub const fn is_standard_keyword(&self) -> bool {
+ #[allow(clippy::enum_glob_use)]
+ use Token::*;
+
+ match self {
+ #[cfg(not(feature = "no_function"))]
+ Fn | Private => true,
+
+ #[cfg(not(feature = "no_module"))]
+ Import | Export | As => true,
+
+ True | False | Let | Const | If | Else | Do | While | Until | Loop | For | In
+ | Continue | Break | Return | Throw | Try | Catch => true,
+
+ _ => false,
+ }
+ }
+
+ /// Is this token a reserved keyword or symbol?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_reserved(&self) -> bool {
+ matches!(self, Self::Reserved(..))
+ }
+
+ /// Convert a token into a function name, if possible.
+ #[cfg(not(feature = "no_function"))]
+ #[inline]
+ pub(crate) fn into_function_name_for_override(self) -> Result<SmartString, Self> {
+ match self {
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Self::Custom(s) if is_valid_function_name(&s) => Ok(*s),
+ Self::Identifier(s) if is_valid_function_name(&s) => Ok(*s),
+ _ => Err(self),
+ }
+ }
+
+ /// Is this token a custom keyword?
+ #[cfg(not(feature = "no_custom_syntax"))]
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_custom(&self) -> bool {
+ matches!(self, Self::Custom(..))
+ }
+}
+
+impl From<Token> for String {
+ #[inline(always)]
+ fn from(token: Token) -> Self {
+ token.to_string()
+ }
+}
+
+/// _(internals)_ State of the tokenizer.
+/// Exported under the `internals` feature only.
+#[derive(Debug, Clone, Eq, PartialEq, Default)]
+pub struct TokenizeState {
+ /// Maximum length of a string.
+ pub max_string_len: Option<NonZeroUsize>,
+ /// Can the next token be a unary operator?
+ pub next_token_cannot_be_unary: bool,
+ /// Shared object to allow controlling the tokenizer externally.
+ pub tokenizer_control: TokenizerControl,
+ /// Is the tokenizer currently inside a block comment?
+ pub comment_level: usize,
+ /// Include comments?
+ pub include_comments: bool,
+ /// Is the current tokenizer position within the text stream of an interpolated string?
+ pub is_within_text_terminated_by: Option<char>,
+ /// Textual syntax of the current token, if any.
+ ///
+ /// Set to `Some` to begin tracking this information.
+ pub last_token: Option<SmartString>,
+}
+
+/// _(internals)_ Trait that encapsulates a peekable character input stream.
+/// Exported under the `internals` feature only.
+pub trait InputStream {
+ /// Un-get a character back into the `InputStream`.
+ /// The next [`get_next`][InputStream::get_next] or [`peek_next`][InputStream::peek_next]
+ /// will return this character instead.
+ fn unget(&mut self, ch: char);
+ /// Get the next character from the `InputStream`.
+ fn get_next(&mut self) -> Option<char>;
+ /// Peek the next character in the `InputStream`.
+ #[must_use]
+ fn peek_next(&mut self) -> Option<char>;
+}
+
+/// Return error if the string is longer than the maximum length.
+#[inline]
+fn ensure_string_len_within_limit(max: Option<NonZeroUsize>, value: &str) -> Result<(), LexError> {
+ if let Some(max) = max {
+ if value.len() > max.get() {
+ return Err(LexError::StringTooLong(max.get()));
+ }
+ }
+
+ Ok(())
+}
+
+/// _(internals)_ Parse a string literal ended by a specified termination character.
+/// Exported under the `internals` feature only.
+///
+/// Returns the parsed string and a boolean indicating whether the string is
+/// terminated by an interpolation `${`.
+///
+/// # Returns
+///
+/// | Type | Return Value |`state.is_within_text_terminated_by`|
+/// |---------------------------------|:--------------------------:|:----------------------------------:|
+/// |`"hello"` |`StringConstant("hello")` |`None` |
+/// |`"hello`_{LF}_ or _{EOF}_ |`LexError` |`None` |
+/// |`"hello\`_{EOF}_ or _{LF}{EOF}_ |`StringConstant("hello")` |`Some('"')` |
+/// |`` `hello``_{EOF}_ |`StringConstant("hello")` |``Some('`')`` |
+/// |`` `hello``_{LF}{EOF}_ |`StringConstant("hello\n")` |``Some('`')`` |
+/// |`` `hello ${`` |`InterpolatedString("hello ")`<br/>next token is `{`|`None` |
+/// |`` } hello` `` |`StringConstant(" hello")` |`None` |
+/// |`} hello`_{EOF}_ |`StringConstant(" hello")` |``Some('`')`` |
+///
+/// This function does not throw a `LexError` for the following conditions:
+///
+/// * Unterminated literal string at _{EOF}_
+///
+/// * Unterminated normal string with continuation at _{EOF}_
+///
+/// This is to facilitate using this function to parse a script line-by-line, where the end of the
+/// line (i.e. _{EOF}_) is not necessarily the end of the script.
+///
+/// Any time a [`StringConstant`][`Token::StringConstant`] is returned with
+/// `state.is_within_text_terminated_by` set to `Some(_)` is one of the above conditions.
+pub fn parse_string_literal(
+ stream: &mut impl InputStream,
+ state: &mut TokenizeState,
+ pos: &mut Position,
+ termination_char: char,
+ verbatim: bool,
+ allow_line_continuation: bool,
+ allow_interpolation: bool,
+) -> Result<(SmartString, bool, Position), (LexError, Position)> {
+ let mut result = SmartString::new_const();
+ let mut escape = SmartString::new_const();
+
+ let start = *pos;
+ let mut first_char = Position::NONE;
+ let mut interpolated = false;
+ #[cfg(not(feature = "no_position"))]
+ let mut skip_whitespace_until = 0;
+
+ state.is_within_text_terminated_by = Some(termination_char);
+ if let Some(ref mut last) = state.last_token {
+ last.clear();
+ last.push(termination_char);
+ }
+
+ loop {
+ debug_assert!(
+ !verbatim || escape.is_empty(),
+ "verbatim strings should not have any escapes"
+ );
+
+ let next_char = match stream.get_next() {
+ Some(ch) => {
+ pos.advance();
+ ch
+ }
+ None if verbatim => {
+ debug_assert_eq!(escape, "", "verbatim strings should not have any escapes");
+ pos.advance();
+ break;
+ }
+ None if allow_line_continuation && !escape.is_empty() => {
+ debug_assert_eq!(escape, "\\", "unexpected escape {} at end of line", escape);
+ pos.advance();
+ break;
+ }
+ None => {
+ pos.advance();
+ state.is_within_text_terminated_by = None;
+ return Err((LERR::UnterminatedString, start));
+ }
+ };
+
+ if let Some(ref mut last) = state.last_token {
+ last.push(next_char);
+ }
+
+ // String interpolation?
+ if allow_interpolation
+ && next_char == '$'
+ && escape.is_empty()
+ && stream.peek_next().map_or(false, |ch| ch == '{')
+ {
+ interpolated = true;
+ state.is_within_text_terminated_by = None;
+ break;
+ }
+
+ ensure_string_len_within_limit(state.max_string_len, &result)
+ .map_err(|err| (err, start))?;
+
+ // Close wrapper
+ if termination_char == next_char && escape.is_empty() {
+ // Double wrapper
+ if stream.peek_next().map_or(false, |c| c == termination_char) {
+ eat_next_and_advance(stream, pos);
+ if let Some(ref mut last) = state.last_token {
+ last.push(termination_char);
+ }
+ } else {
+ state.is_within_text_terminated_by = None;
+ break;
+ }
+ }
+
+ if first_char.is_none() {
+ first_char = *pos;
+ }
+
+ match next_char {
+ // \r - ignore if followed by \n
+ '\r' if stream.peek_next().map_or(false, |ch| ch == '\n') => (),
+ // \...
+ '\\' if !verbatim && escape.is_empty() => {
+ escape.push('\\');
+ }
+ // \\
+ '\\' if !escape.is_empty() => {
+ escape.clear();
+ result.push('\\');
+ }
+ // \t
+ 't' if !escape.is_empty() => {
+ escape.clear();
+ result.push('\t');
+ }
+ // \n
+ 'n' if !escape.is_empty() => {
+ escape.clear();
+ result.push('\n');
+ }
+ // \r
+ 'r' if !escape.is_empty() => {
+ escape.clear();
+ result.push('\r');
+ }
+ // \x??, \u????, \U????????
+ ch @ ('x' | 'u' | 'U') if !escape.is_empty() => {
+ let mut seq = escape.clone();
+ escape.clear();
+ seq.push(ch);
+
+ let mut out_val: u32 = 0;
+ let len = match ch {
+ 'x' => 2,
+ 'u' => 4,
+ 'U' => 8,
+ c => unreachable!("x or u or U expected but gets '{}'", c),
+ };
+
+ for _ in 0..len {
+ let c = stream
+ .get_next()
+ .ok_or_else(|| (LERR::MalformedEscapeSequence(seq.to_string()), *pos))?;
+
+ pos.advance();
+ seq.push(c);
+ if let Some(ref mut last) = state.last_token {
+ last.push(c);
+ }
+
+ out_val *= 16;
+ out_val += c
+ .to_digit(16)
+ .ok_or_else(|| (LERR::MalformedEscapeSequence(seq.to_string()), *pos))?;
+ }
+
+ result.push(
+ char::from_u32(out_val)
+ .ok_or_else(|| (LERR::MalformedEscapeSequence(seq.to_string()), *pos))?,
+ );
+ }
+
+ // \{termination_char} - escaped
+ _ if termination_char == next_char && !escape.is_empty() => {
+ escape.clear();
+ result.push(next_char);
+ }
+
+ // Verbatim
+ '\n' if verbatim => {
+ debug_assert_eq!(escape, "", "verbatim strings should not have any escapes");
+ pos.new_line();
+ result.push(next_char);
+ }
+
+ // Line continuation
+ '\n' if allow_line_continuation && !escape.is_empty() => {
+ debug_assert_eq!(escape, "\\", "unexpected escape {} at end of line", escape);
+ escape.clear();
+ pos.new_line();
+
+ #[cfg(not(feature = "no_position"))]
+ {
+ let start_position = start.position().unwrap();
+ skip_whitespace_until = start_position + 1;
+ }
+ }
+
+ // Unterminated string
+ '\n' => {
+ pos.rewind();
+ state.is_within_text_terminated_by = None;
+ return Err((LERR::UnterminatedString, start));
+ }
+
+ // Unknown escape sequence
+ _ if !escape.is_empty() => {
+ escape.push(next_char);
+
+ return Err((LERR::MalformedEscapeSequence(escape.to_string()), *pos));
+ }
+
+ // Whitespace to skip
+ #[cfg(not(feature = "no_position"))]
+ _ if next_char.is_whitespace() && pos.position().unwrap() < skip_whitespace_until => {}
+
+ // All other characters
+ _ => {
+ escape.clear();
+ result.push(next_char);
+
+ #[cfg(not(feature = "no_position"))]
+ {
+ skip_whitespace_until = 0;
+ }
+ }
+ }
+ }
+
+ ensure_string_len_within_limit(state.max_string_len, &result).map_err(|err| (err, start))?;
+
+ Ok((result, interpolated, first_char))
+}
+
+/// Consume the next character.
+#[inline(always)]
+fn eat_next_and_advance(stream: &mut impl InputStream, pos: &mut Position) -> Option<char> {
+ pos.advance();
+ stream.get_next()
+}
+
+/// Scan for a block comment until the end.
+fn scan_block_comment(
+ stream: &mut impl InputStream,
+ level: usize,
+ pos: &mut Position,
+ comment: Option<&mut String>,
+) -> usize {
+ let mut level = level;
+ let mut comment = comment;
+
+ while let Some(c) = stream.get_next() {
+ pos.advance();
+
+ if let Some(comment) = comment.as_mut() {
+ comment.push(c);
+ }
+
+ match c {
+ '/' => {
+ if let Some(c2) = stream.peek_next().filter(|&c2| c2 == '*') {
+ eat_next_and_advance(stream, pos);
+ if let Some(comment) = comment.as_mut() {
+ comment.push(c2);
+ }
+ level += 1;
+ }
+ }
+ '*' => {
+ if let Some(c2) = stream.peek_next().filter(|&c2| c2 == '/') {
+ eat_next_and_advance(stream, pos);
+ if let Some(comment) = comment.as_mut() {
+ comment.push(c2);
+ }
+ level -= 1;
+ }
+ }
+ '\n' => pos.new_line(),
+ _ => (),
+ }
+
+ if level == 0 {
+ break;
+ }
+ }
+
+ level
+}
+
+/// _(internals)_ Get the next token from the input stream.
+/// Exported under the `internals` feature only.
+#[inline]
+#[must_use]
+pub fn get_next_token(
+ stream: &mut impl InputStream,
+ state: &mut TokenizeState,
+ pos: &mut Position,
+) -> Option<(Token, Position)> {
+ let result = get_next_token_inner(stream, state, pos);
+
+ // Save the last token's state
+ if let Some((ref token, ..)) = result {
+ state.next_token_cannot_be_unary = !token.is_next_unary();
+ }
+
+ result
+}
+
+/// Test if the given character is a hex character.
+#[inline(always)]
+const fn is_hex_digit(c: char) -> bool {
+ matches!(c, 'a'..='f' | 'A'..='F' | '0'..='9')
+}
+
+/// Test if the given character is a numeric digit.
+#[inline(always)]
+const fn is_numeric_digit(c: char) -> bool {
+ c.is_ascii_digit()
+}
+
+/// Test if the comment block is a doc-comment.
+#[cfg(not(feature = "no_function"))]
+#[cfg(feature = "metadata")]
+#[inline]
+#[must_use]
+pub fn is_doc_comment(comment: &str) -> bool {
+ (comment.starts_with("///") && !comment.starts_with("////"))
+ || (comment.starts_with("/**") && !comment.starts_with("/***"))
+}
+
+/// Get the next token.
+#[must_use]
+fn get_next_token_inner(
+ stream: &mut impl InputStream,
+ state: &mut TokenizeState,
+ pos: &mut Position,
+) -> Option<(Token, Position)> {
+ state.last_token.as_mut().map(SmartString::clear);
+
+ // Still inside a comment?
+ if state.comment_level > 0 {
+ let start_pos = *pos;
+ let mut comment = state.include_comments.then(|| String::new());
+
+ state.comment_level =
+ scan_block_comment(stream, state.comment_level, pos, comment.as_mut());
+
+ let return_comment = state.include_comments;
+
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(feature = "metadata")]
+ let return_comment = return_comment || is_doc_comment(comment.as_ref().expect("`Some`"));
+
+ if return_comment {
+ return Some((Token::Comment(comment.expect("`Some`").into()), start_pos));
+ }
+ if state.comment_level > 0 {
+ // Reached EOF without ending comment block
+ return None;
+ }
+ }
+
+ // Within text?
+ if let Some(ch) = state.is_within_text_terminated_by.take() {
+ return parse_string_literal(stream, state, pos, ch, true, false, true).map_or_else(
+ |(err, err_pos)| Some((Token::LexError(err.into()), err_pos)),
+ |(result, interpolated, start_pos)| {
+ if interpolated {
+ Some((Token::InterpolatedString(result.into()), start_pos))
+ } else {
+ Some((Token::StringConstant(result.into()), start_pos))
+ }
+ },
+ );
+ }
+
+ let mut negated: Option<Position> = None;
+
+ while let Some(c) = stream.get_next() {
+ pos.advance();
+
+ let start_pos = *pos;
+ let cc = stream.peek_next().unwrap_or('\0');
+
+ // Identifiers and strings that can have non-ASCII characters
+ match (c, cc) {
+ // \n
+ ('\n', ..) => pos.new_line(),
+
+ // digit ...
+ ('0'..='9', ..) => {
+ let mut result = SmartString::new_const();
+ let mut radix_base: Option<u32> = None;
+ let mut valid: fn(char) -> bool = is_numeric_digit;
+ result.push(c);
+
+ while let Some(next_char) = stream.peek_next() {
+ match next_char {
+ NUMBER_SEPARATOR => {
+ eat_next_and_advance(stream, pos);
+ }
+ ch if valid(ch) => {
+ result.push(next_char);
+ eat_next_and_advance(stream, pos);
+ }
+ #[cfg(any(not(feature = "no_float"), feature = "decimal"))]
+ '.' => {
+ stream.get_next().unwrap();
+
+ // Check if followed by digits or something that cannot start a property name
+ match stream.peek_next().unwrap_or('\0') {
+ // digits after period - accept the period
+ '0'..='9' => {
+ result.push(next_char);
+ pos.advance();
+ }
+ // _ - cannot follow a decimal point
+ NUMBER_SEPARATOR => {
+ stream.unget(next_char);
+ break;
+ }
+ // .. - reserved symbol, not a floating-point number
+ '.' => {
+ stream.unget(next_char);
+ break;
+ }
+ // symbol after period - probably a float
+ ch if !is_id_first_alphabetic(ch) => {
+ result.push(next_char);
+ pos.advance();
+ result.push('0');
+ }
+ // Not a floating-point number
+ _ => {
+ stream.unget(next_char);
+ break;
+ }
+ }
+ }
+ #[cfg(not(feature = "no_float"))]
+ 'e' => {
+ stream.get_next().expect("`e`");
+
+ // Check if followed by digits or +/-
+ match stream.peek_next().unwrap_or('\0') {
+ // digits after e - accept the e
+ '0'..='9' => {
+ result.push(next_char);
+ pos.advance();
+ }
+ // +/- after e - accept the e and the sign
+ '+' | '-' => {
+ result.push(next_char);
+ pos.advance();
+ result.push(stream.get_next().unwrap());
+ pos.advance();
+ }
+ // Not a floating-point number
+ _ => {
+ stream.unget(next_char);
+ break;
+ }
+ }
+ }
+ // 0x????, 0o????, 0b???? at beginning
+ ch @ ('x' | 'o' | 'b' | 'X' | 'O' | 'B')
+ if c == '0' && result.len() <= 1 =>
+ {
+ result.push(next_char);
+ eat_next_and_advance(stream, pos);
+
+ valid = match ch {
+ 'x' | 'X' => is_hex_digit,
+ 'o' | 'O' => is_numeric_digit,
+ 'b' | 'B' => is_numeric_digit,
+ c => unreachable!("x/X or o/O or b/B expected but gets '{}'", c),
+ };
+
+ radix_base = Some(match ch {
+ 'x' | 'X' => 16,
+ 'o' | 'O' => 8,
+ 'b' | 'B' => 2,
+ c => unreachable!("x/X or o/O or b/B expected but gets '{}'", c),
+ });
+ }
+
+ _ => break,
+ }
+ }
+
+ let num_pos = negated.map_or(start_pos, |negated_pos| {
+ result.insert(0, '-');
+ negated_pos
+ });
+
+ if let Some(ref mut last) = state.last_token {
+ *last = result.clone();
+ }
+
+ // Parse number
+ let token = radix_base.map_or_else(
+ || {
+ let num = INT::from_str(&result).map(Token::IntegerConstant);
+
+ // If integer parsing is unnecessary, try float instead
+ #[cfg(not(feature = "no_float"))]
+ let num = num.or_else(|_| {
+ crate::types::FloatWrapper::from_str(&result).map(Token::FloatConstant)
+ });
+
+ // Then try decimal
+ #[cfg(feature = "decimal")]
+ let num = num.or_else(|_| {
+ rust_decimal::Decimal::from_str(&result)
+ .map(Box::new)
+ .map(Token::DecimalConstant)
+ });
+
+ // Then try decimal in scientific notation
+ #[cfg(feature = "decimal")]
+ let num = num.or_else(|_| {
+ rust_decimal::Decimal::from_scientific(&result)
+ .map(Box::new)
+ .map(Token::DecimalConstant)
+ });
+
+ num.unwrap_or_else(|_| {
+ Token::LexError(LERR::MalformedNumber(result.to_string()).into())
+ })
+ },
+ |radix| {
+ let result = &result[2..];
+
+ UNSIGNED_INT::from_str_radix(result, radix)
+ .map(|v| v as INT)
+ .map_or_else(
+ |_| {
+ Token::LexError(
+ LERR::MalformedNumber(result.to_string()).into(),
+ )
+ },
+ Token::IntegerConstant,
+ )
+ },
+ );
+
+ return Some((token, num_pos));
+ }
+
+ // " - string literal
+ ('"', ..) => {
+ return parse_string_literal(stream, state, pos, c, false, true, false)
+ .map_or_else(
+ |(err, err_pos)| Some((Token::LexError(err.into()), err_pos)),
+ |(result, ..)| Some((Token::StringConstant(result.into()), start_pos)),
+ );
+ }
+ // ` - string literal
+ ('`', ..) => {
+ // Start from the next line if at the end of line
+ match stream.peek_next() {
+ // `\r - start from next line
+ Some('\r') => {
+ eat_next_and_advance(stream, pos);
+ // `\r\n
+ if stream.peek_next() == Some('\n') {
+ eat_next_and_advance(stream, pos);
+ }
+ pos.new_line();
+ }
+ // `\n - start from next line
+ Some('\n') => {
+ eat_next_and_advance(stream, pos);
+ pos.new_line();
+ }
+ _ => (),
+ }
+
+ return parse_string_literal(stream, state, pos, c, true, false, true).map_or_else(
+ |(err, err_pos)| Some((Token::LexError(err.into()), err_pos)),
+ |(result, interpolated, ..)| {
+ if interpolated {
+ Some((Token::InterpolatedString(result.into()), start_pos))
+ } else {
+ Some((Token::StringConstant(result.into()), start_pos))
+ }
+ },
+ );
+ }
+
+ // ' - character literal
+ ('\'', '\'') => {
+ return Some((
+ Token::LexError(LERR::MalformedChar(String::new()).into()),
+ start_pos,
+ ))
+ }
+ ('\'', ..) => {
+ return Some(
+ parse_string_literal(stream, state, pos, c, false, false, false).map_or_else(
+ |(err, err_pos)| (Token::LexError(err.into()), err_pos),
+ |(result, ..)| {
+ let mut chars = result.chars();
+ let first = chars.next().unwrap();
+
+ if chars.next().is_some() {
+ (
+ Token::LexError(LERR::MalformedChar(result.to_string()).into()),
+ start_pos,
+ )
+ } else {
+ (Token::CharConstant(first), start_pos)
+ }
+ },
+ ),
+ )
+ }
+
+ // Braces
+ ('{', ..) => return Some((Token::LeftBrace, start_pos)),
+ ('}', ..) => return Some((Token::RightBrace, start_pos)),
+
+ // Unit
+ ('(', ')') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Unit, start_pos));
+ }
+
+ // Parentheses
+ ('(', '*') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new("(*".into())), start_pos));
+ }
+ ('(', ..) => return Some((Token::LeftParen, start_pos)),
+ (')', ..) => return Some((Token::RightParen, start_pos)),
+
+ // Indexing
+ ('[', ..) => return Some((Token::LeftBracket, start_pos)),
+ (']', ..) => return Some((Token::RightBracket, start_pos)),
+
+ // Map literal
+ #[cfg(not(feature = "no_object"))]
+ ('#', '{') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::MapStart, start_pos));
+ }
+ // Shebang
+ ('#', '!') => return Some((Token::Reserved(Box::new("#!".into())), start_pos)),
+
+ ('#', ' ') => {
+ eat_next_and_advance(stream, pos);
+ let token = if stream.peek_next() == Some('{') {
+ eat_next_and_advance(stream, pos);
+ "# {"
+ } else {
+ "#"
+ };
+ return Some((Token::Reserved(Box::new(token.into())), start_pos));
+ }
+
+ ('#', ..) => return Some((Token::Reserved(Box::new("#".into())), start_pos)),
+
+ // Operators
+ ('+', '=') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::PlusAssign, start_pos));
+ }
+ ('+', '+') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new("++".into())), start_pos));
+ }
+ ('+', ..) if !state.next_token_cannot_be_unary => {
+ return Some((Token::UnaryPlus, start_pos))
+ }
+ ('+', ..) => return Some((Token::Plus, start_pos)),
+
+ ('-', '0'..='9') if !state.next_token_cannot_be_unary => negated = Some(start_pos),
+ ('-', '0'..='9') => return Some((Token::Minus, start_pos)),
+ ('-', '=') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::MinusAssign, start_pos));
+ }
+ ('-', '>') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new("->".into())), start_pos));
+ }
+ ('-', '-') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new("--".into())), start_pos));
+ }
+ ('-', ..) if !state.next_token_cannot_be_unary => {
+ return Some((Token::UnaryMinus, start_pos))
+ }
+ ('-', ..) => return Some((Token::Minus, start_pos)),
+
+ ('*', ')') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new("*)".into())), start_pos));
+ }
+ ('*', '=') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::MultiplyAssign, start_pos));
+ }
+ ('*', '*') => {
+ eat_next_and_advance(stream, pos);
+
+ return Some((
+ if stream.peek_next() == Some('=') {
+ eat_next_and_advance(stream, pos);
+ Token::PowerOfAssign
+ } else {
+ Token::PowerOf
+ },
+ start_pos,
+ ));
+ }
+ ('*', ..) => return Some((Token::Multiply, start_pos)),
+
+ // Comments
+ ('/', '/') => {
+ eat_next_and_advance(stream, pos);
+
+ let mut comment: Option<String> = match stream.peek_next() {
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(feature = "metadata")]
+ Some('/') => {
+ eat_next_and_advance(stream, pos);
+
+ // Long streams of `///...` are not doc-comments
+ match stream.peek_next() {
+ Some('/') => None,
+ _ => Some("///".into()),
+ }
+ }
+ #[cfg(feature = "metadata")]
+ Some('!') => {
+ eat_next_and_advance(stream, pos);
+ Some("//!".into())
+ }
+ _ if state.include_comments => Some("//".into()),
+ _ => None,
+ };
+
+ while let Some(c) = stream.get_next() {
+ if c == '\r' {
+ // \r\n
+ if stream.peek_next() == Some('\n') {
+ eat_next_and_advance(stream, pos);
+ }
+ pos.new_line();
+ break;
+ }
+ if c == '\n' {
+ pos.new_line();
+ break;
+ }
+ if let Some(comment) = comment.as_mut() {
+ comment.push(c);
+ }
+ pos.advance();
+ }
+
+ if let Some(comment) = comment {
+ match comment {
+ #[cfg(feature = "metadata")]
+ _ if comment.starts_with("//!") => {
+ let g = &mut state.tokenizer_control.borrow_mut().global_comments;
+ if !g.is_empty() {
+ g.push('\n');
+ }
+ g.push_str(&comment);
+ }
+ _ => return Some((Token::Comment(comment.into()), start_pos)),
+ }
+ }
+ }
+ ('/', '*') => {
+ state.comment_level = 1;
+ eat_next_and_advance(stream, pos);
+
+ let mut comment: Option<String> = match stream.peek_next() {
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(feature = "metadata")]
+ Some('*') => {
+ eat_next_and_advance(stream, pos);
+
+ // Long streams of `/****...` are not doc-comments
+ match stream.peek_next() {
+ Some('*') => None,
+ _ => Some("/**".into()),
+ }
+ }
+ _ if state.include_comments => Some("/*".into()),
+ _ => None,
+ };
+
+ state.comment_level =
+ scan_block_comment(stream, state.comment_level, pos, comment.as_mut());
+
+ if let Some(comment) = comment {
+ return Some((Token::Comment(comment.into()), start_pos));
+ }
+ }
+
+ ('/', '=') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::DivideAssign, start_pos));
+ }
+ ('/', ..) => return Some((Token::Divide, start_pos)),
+
+ (';', ..) => return Some((Token::SemiColon, start_pos)),
+ (',', ..) => return Some((Token::Comma, start_pos)),
+
+ ('.', '.') => {
+ eat_next_and_advance(stream, pos);
+ return Some((
+ match stream.peek_next() {
+ Some('.') => {
+ eat_next_and_advance(stream, pos);
+ Token::Reserved(Box::new("...".into()))
+ }
+ Some('=') => {
+ eat_next_and_advance(stream, pos);
+ Token::InclusiveRange
+ }
+ _ => Token::ExclusiveRange,
+ },
+ start_pos,
+ ));
+ }
+ ('.', ..) => return Some((Token::Period, start_pos)),
+
+ ('=', '=') => {
+ eat_next_and_advance(stream, pos);
+
+ if stream.peek_next() == Some('=') {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new("===".into())), start_pos));
+ }
+
+ return Some((Token::EqualsTo, start_pos));
+ }
+ ('=', '>') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::DoubleArrow, start_pos));
+ }
+ ('=', ..) => return Some((Token::Equals, start_pos)),
+
+ #[cfg(not(feature = "no_module"))]
+ (':', ':') => {
+ eat_next_and_advance(stream, pos);
+
+ if stream.peek_next() == Some('<') {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new("::<".into())), start_pos));
+ }
+
+ return Some((Token::DoubleColon, start_pos));
+ }
+ (':', '=') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new(":=".into())), start_pos));
+ }
+ (':', ';') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new(":;".into())), start_pos));
+ }
+ (':', ..) => return Some((Token::Colon, start_pos)),
+
+ ('<', '=') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::LessThanEqualsTo, start_pos));
+ }
+ ('<', '-') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new("<-".into())), start_pos));
+ }
+ ('<', '<') => {
+ eat_next_and_advance(stream, pos);
+
+ return Some((
+ if stream.peek_next() == Some('=') {
+ eat_next_and_advance(stream, pos);
+ Token::LeftShiftAssign
+ } else {
+ Token::LeftShift
+ },
+ start_pos,
+ ));
+ }
+ ('<', '|') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new("<|".into())), start_pos));
+ }
+ ('<', ..) => return Some((Token::LessThan, start_pos)),
+
+ ('>', '=') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::GreaterThanEqualsTo, start_pos));
+ }
+ ('>', '>') => {
+ eat_next_and_advance(stream, pos);
+
+ return Some((
+ if stream.peek_next() == Some('=') {
+ eat_next_and_advance(stream, pos);
+ Token::RightShiftAssign
+ } else {
+ Token::RightShift
+ },
+ start_pos,
+ ));
+ }
+ ('>', ..) => return Some((Token::GreaterThan, start_pos)),
+
+ ('!', 'i') => {
+ stream.get_next().unwrap();
+ if stream.peek_next() == Some('n') {
+ stream.get_next().unwrap();
+ match stream.peek_next() {
+ Some(c) if is_id_continue(c) => {
+ stream.unget('n');
+ stream.unget('i');
+ return Some((Token::Bang, start_pos));
+ }
+ _ => {
+ pos.advance();
+ pos.advance();
+ return Some((Token::NotIn, start_pos));
+ }
+ }
+ }
+
+ stream.unget('i');
+ return Some((Token::Bang, start_pos));
+ }
+ ('!', '=') => {
+ eat_next_and_advance(stream, pos);
+
+ if stream.peek_next() == Some('=') {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new("!==".into())), start_pos));
+ }
+
+ return Some((Token::NotEqualsTo, start_pos));
+ }
+ ('!', '.') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new("!.".into())), start_pos));
+ }
+ ('!', ..) => return Some((Token::Bang, start_pos)),
+
+ ('|', '|') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Or, start_pos));
+ }
+ ('|', '=') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::OrAssign, start_pos));
+ }
+ ('|', '>') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::Reserved(Box::new("|>".into())), start_pos));
+ }
+ ('|', ..) => return Some((Token::Pipe, start_pos)),
+
+ ('&', '&') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::And, start_pos));
+ }
+ ('&', '=') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::AndAssign, start_pos));
+ }
+ ('&', ..) => return Some((Token::Ampersand, start_pos)),
+
+ ('^', '=') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::XOrAssign, start_pos));
+ }
+ ('^', ..) => return Some((Token::XOr, start_pos)),
+
+ ('~', ..) => return Some((Token::Reserved(Box::new("~".into())), start_pos)),
+
+ ('%', '=') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::ModuloAssign, start_pos));
+ }
+ ('%', ..) => return Some((Token::Modulo, start_pos)),
+
+ ('@', ..) => return Some((Token::Reserved(Box::new("@".into())), start_pos)),
+
+ ('$', ..) => return Some((Token::Reserved(Box::new("$".into())), start_pos)),
+
+ ('?', '.') => {
+ eat_next_and_advance(stream, pos);
+ return Some((
+ #[cfg(not(feature = "no_object"))]
+ Token::Elvis,
+ #[cfg(feature = "no_object")]
+ Token::Reserved(Box::new("?.".into())),
+ start_pos,
+ ));
+ }
+ ('?', '?') => {
+ eat_next_and_advance(stream, pos);
+ return Some((Token::DoubleQuestion, start_pos));
+ }
+ ('?', '[') => {
+ eat_next_and_advance(stream, pos);
+ return Some((
+ #[cfg(not(feature = "no_index"))]
+ Token::QuestionBracket,
+ #[cfg(feature = "no_index")]
+ Token::Reserved(Box::new("?[".into())),
+ start_pos,
+ ));
+ }
+ ('?', ..) => return Some((Token::Reserved(Box::new("?".into())), start_pos)),
+
+ // letter or underscore ...
+ _ if is_id_first_alphabetic(c) || c == '_' => {
+ return Some(parse_identifier_token(stream, state, pos, start_pos, c));
+ }
+
+ _ if c.is_whitespace() => (),
+
+ _ => {
+ return Some((
+ Token::LexError(LERR::UnexpectedInput(c.to_string()).into()),
+ start_pos,
+ ))
+ }
+ }
+ }
+
+ pos.advance();
+
+ Some((Token::EOF, *pos))
+}
+
+/// Get the next token, parsing it as an identifier.
+fn parse_identifier_token(
+ stream: &mut impl InputStream,
+ state: &mut TokenizeState,
+ pos: &mut Position,
+ start_pos: Position,
+ first_char: char,
+) -> (Token, Position) {
+ let mut identifier = SmartString::new_const();
+ identifier.push(first_char);
+ if let Some(ref mut last) = state.last_token {
+ last.clear();
+ last.push(first_char);
+ }
+
+ while let Some(next_char) = stream.peek_next() {
+ match next_char {
+ x if is_id_continue(x) => {
+ eat_next_and_advance(stream, pos);
+ identifier.push(x);
+ if let Some(ref mut last) = state.last_token {
+ last.push(x);
+ }
+ }
+ _ => break,
+ }
+ }
+
+ if let Some(token) = Token::lookup_symbol_from_syntax(&identifier) {
+ return (token, start_pos);
+ }
+
+ if is_reserved_keyword_or_symbol(&identifier).0 {
+ return (Token::Reserved(Box::new(identifier)), start_pos);
+ }
+
+ if !is_valid_identifier(&identifier) {
+ return (
+ Token::LexError(LERR::MalformedIdentifier(identifier.to_string()).into()),
+ start_pos,
+ );
+ }
+
+ (Token::Identifier(identifier.into()), start_pos)
+}
+
+/// _(internals)_ Is a text string a valid identifier?
+/// Exported under the `internals` feature only.
+#[must_use]
+pub fn is_valid_identifier(name: &str) -> bool {
+ let mut first_alphabetic = false;
+
+ for ch in name.chars() {
+ match ch {
+ '_' => (),
+ _ if is_id_first_alphabetic(ch) => first_alphabetic = true,
+ _ if !first_alphabetic => return false,
+ _ if char::is_ascii_alphanumeric(&ch) => (),
+ _ => return false,
+ }
+ }
+
+ first_alphabetic
+}
+
+/// _(internals)_ Is a text string a valid script-defined function name?
+/// Exported under the `internals` feature only.
+#[inline(always)]
+#[must_use]
+pub fn is_valid_function_name(name: &str) -> bool {
+ is_valid_identifier(name)
+ && !is_reserved_keyword_or_symbol(name).0
+ && Token::lookup_symbol_from_syntax(name).is_none()
+}
+
+/// Is a character valid to start an identifier?
+#[inline(always)]
+#[must_use]
+pub fn is_id_first_alphabetic(x: char) -> bool {
+ #[cfg(feature = "unicode-xid-ident")]
+ return unicode_xid::UnicodeXID::is_xid_start(x);
+ #[cfg(not(feature = "unicode-xid-ident"))]
+ return x.is_ascii_alphabetic();
+}
+
+/// Is a character valid for an identifier?
+#[inline(always)]
+#[must_use]
+pub fn is_id_continue(x: char) -> bool {
+ #[cfg(feature = "unicode-xid-ident")]
+ return unicode_xid::UnicodeXID::is_xid_continue(x);
+ #[cfg(not(feature = "unicode-xid-ident"))]
+ return x.is_ascii_alphanumeric() || x == '_';
+}
+
+/// Is a piece of syntax a reserved keyword or reserved symbol?
+///
+/// # Return values
+///
+/// The first `bool` indicates whether it is a reserved keyword or symbol.
+///
+/// The second `bool` indicates whether the keyword can be called normally as a function.
+/// `false` if it is not a reserved keyword.
+///
+/// The third `bool` indicates whether the keyword can be called in method-call style.
+/// `false` if it is not a reserved keyword or it cannot be called as a function.
+#[inline]
+#[must_use]
+pub fn is_reserved_keyword_or_symbol(syntax: &str) -> (bool, bool, bool) {
+ // This implementation is based upon a pre-calculated table generated
+ // by GNU `gperf` on the list of keywords.
+ let utf8 = syntax.as_bytes();
+ let len = utf8.len();
+
+ if !(MIN_RESERVED_LEN..=MAX_RESERVED_LEN).contains(&len) {
+ return (false, false, false);
+ }
+
+ let mut hash_val = len;
+
+ match len {
+ 1 => (),
+ _ => hash_val += RESERVED_ASSOC_VALUES[utf8[1] as usize] as usize,
+ }
+ hash_val += RESERVED_ASSOC_VALUES[utf8[0] as usize] as usize;
+ hash_val += RESERVED_ASSOC_VALUES[utf8[len - 1] as usize] as usize;
+
+ if !(MIN_RESERVED_HASH_VALUE..=MAX_RESERVED_HASH_VALUE).contains(&hash_val) {
+ return (false, false, false);
+ }
+
+ match RESERVED_LIST[hash_val] {
+ ("", ..) => (false, false, false),
+ (s, true, a, b) => {
+ // Fail early to avoid calling memcmp().
+ // Since we are already working with bytes, mind as well check the first one.
+ let is_reserved = s.len() == len && s.as_bytes()[0] == utf8[0] && s == syntax;
+ (is_reserved, is_reserved && a, is_reserved && a && b)
+ }
+ _ => (false, false, false),
+ }
+}
+
+/// _(internals)_ A type that implements the [`InputStream`] trait.
+/// Exported under the `internals` feature only.
+///
+/// Multiple character streams are jointed together to form one single stream.
+pub struct MultiInputsStream<'a> {
+ /// Buffered characters, if any.
+ pub buf: SmallVec<[char; 2]>,
+ /// The current stream index.
+ pub index: usize,
+ /// The input character streams.
+ pub streams: StaticVec<Peekable<Chars<'a>>>,
+}
+
+impl InputStream for MultiInputsStream<'_> {
+ #[inline]
+ fn unget(&mut self, ch: char) {
+ self.buf.push(ch);
+ }
+ fn get_next(&mut self) -> Option<char> {
+ if let ch @ Some(..) = self.buf.pop() {
+ return ch;
+ }
+
+ loop {
+ if self.index >= self.streams.len() {
+ // No more streams
+ return None;
+ }
+ if let Some(ch) = self.streams[self.index].next() {
+ // Next character in current stream
+ return Some(ch);
+ }
+ // Jump to the next stream
+ self.index += 1;
+ }
+ }
+ fn peek_next(&mut self) -> Option<char> {
+ if let ch @ Some(..) = self.buf.last() {
+ return ch.copied();
+ }
+
+ loop {
+ if self.index >= self.streams.len() {
+ // No more streams
+ return None;
+ }
+ if let Some(&ch) = self.streams[self.index].peek() {
+ // Next character in current stream
+ return Some(ch);
+ }
+ // Jump to the next stream
+ self.index += 1;
+ }
+ }
+}
+
+/// _(internals)_ An iterator on a [`Token`] stream.
+/// Exported under the `internals` feature only.
+pub struct TokenIterator<'a> {
+ /// Reference to the scripting `Engine`.
+ pub engine: &'a Engine,
+ /// Current state.
+ pub state: TokenizeState,
+ /// Current position.
+ pub pos: Position,
+ /// Input character stream.
+ pub stream: MultiInputsStream<'a>,
+ /// A processor function that maps a token to another.
+ pub token_mapper: Option<&'a OnParseTokenCallback>,
+}
+
+impl<'a> Iterator for TokenIterator<'a> {
+ type Item = (Token, Position);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let (within_interpolated, compress_script) = {
+ let control = &mut *self.state.tokenizer_control.borrow_mut();
+
+ if control.is_within_text {
+ // Switch to text mode terminated by back-tick
+ self.state.is_within_text_terminated_by = Some('`');
+ // Reset it
+ control.is_within_text = false;
+ }
+
+ (
+ self.state.is_within_text_terminated_by.is_some(),
+ control.compressed.is_some(),
+ )
+ };
+
+ let (token, pos) = match get_next_token(&mut self.stream, &mut self.state, &mut self.pos) {
+ // {EOF}
+ None => return None,
+ // {EOF} after unterminated string.
+ // The only case where `TokenizeState.is_within_text_terminated_by` is set is when
+ // a verbatim string or a string with continuation encounters {EOF}.
+ // This is necessary to handle such cases for line-by-line parsing, but for an entire
+ // script it is a syntax error.
+ Some((Token::StringConstant(..), pos)) if self.state.is_within_text_terminated_by.is_some() => {
+ self.state.is_within_text_terminated_by = None;
+ return Some((Token::LexError(LERR::UnterminatedString.into()), pos));
+ }
+ // Reserved keyword/symbol
+ Some((Token::Reserved(s), pos)) => (match
+ (s.as_str(),
+ #[cfg(not(feature = "no_custom_syntax"))]
+ self.engine.is_custom_keyword(&*s),
+ #[cfg(feature = "no_custom_syntax")]
+ false
+ )
+ {
+ ("===", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(),
+ "'===' is not a valid operator. This is not JavaScript! Should it be '=='?".to_string(),
+ ).into()),
+ ("!==", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(),
+ "'!==' is not a valid operator. This is not JavaScript! Should it be '!='?".to_string(),
+ ).into()),
+ ("->", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(),
+ "'->' is not a valid symbol. This is not C or C++!".to_string()).into()),
+ ("<-", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(),
+ "'<-' is not a valid symbol. This is not Go! Should it be '<='?".to_string(),
+ ).into()),
+ (":=", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(),
+ "':=' is not a valid assignment operator. This is not Go or Pascal! Should it be simply '='?".to_string(),
+ ).into()),
+ (":;", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(),
+ "':;' is not a valid symbol. Should it be '::'?".to_string(),
+ ).into()),
+ ("::<", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(),
+ "'::<>' is not a valid symbol. This is not Rust! Should it be '::'?".to_string(),
+ ).into()),
+ ("(*" | "*)", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(),
+ "'(* .. *)' is not a valid comment format. This is not Pascal! Should it be '/* .. */'?".to_string(),
+ ).into()),
+ ("# {", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(),
+ "'#' is not a valid symbol. Should it be '#{'?".to_string(),
+ ).into()),
+ // Reserved keyword/operator that is custom.
+ #[cfg(not(feature = "no_custom_syntax"))]
+ (.., true) => Token::Custom(s),
+ #[cfg(feature = "no_custom_syntax")]
+ (.., true) => unreachable!("no custom operators"),
+ // Reserved keyword that is not custom and disabled.
+ (token, false) if self.engine.is_symbol_disabled(token) => {
+ let msg = format!("reserved {} '{token}' is disabled", if is_valid_identifier(token) { "keyword"} else {"symbol"});
+ Token::LexError(LERR::ImproperSymbol(s.to_string(), msg).into())
+ },
+ // Reserved keyword/operator that is not custom.
+ (.., false) => Token::Reserved(s),
+ }, pos),
+ // Custom keyword
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Some((Token::Identifier(s), pos)) if self.engine.is_custom_keyword(&*s) => {
+ (Token::Custom(s), pos)
+ }
+ // Custom keyword/symbol - must be disabled
+ #[cfg(not(feature = "no_custom_syntax"))]
+ Some((token, pos)) if token.is_literal() && self.engine.is_custom_keyword(token.literal_syntax()) => {
+ if self.engine.is_symbol_disabled(token.literal_syntax()) {
+ // Disabled standard keyword/symbol
+ (Token::Custom(Box::new(token.literal_syntax().into())), pos)
+ } else {
+ // Active standard keyword - should never be a custom keyword!
+ unreachable!("{:?} is an active keyword", token)
+ }
+ }
+ // Disabled symbol
+ Some((token, pos)) if token.is_literal() && self.engine.is_symbol_disabled(token.literal_syntax()) => {
+ (Token::Reserved(Box::new(token.literal_syntax().into())), pos)
+ }
+ // Normal symbol
+ Some(r) => r,
+ };
+
+ // Run the mapper, if any
+ let token = if let Some(func) = self.token_mapper {
+ func(token, pos, &self.state)
+ } else {
+ token
+ };
+
+ // Collect the compressed script, if needed
+ if compress_script {
+ let control = &mut *self.state.tokenizer_control.borrow_mut();
+
+ if let Some(ref mut compressed) = control.compressed {
+ if !matches!(token, Token::EOF) {
+ use std::fmt::Write;
+
+ let last_token = self.state.last_token.as_ref().unwrap();
+ let mut buf = SmartString::new_const();
+
+ if last_token.is_empty() {
+ write!(buf, "{token}").unwrap();
+ } else if within_interpolated
+ && matches!(
+ token,
+ Token::StringConstant(..) | Token::InterpolatedString(..)
+ )
+ {
+ compressed.push_str(&last_token[1..]);
+ } else {
+ buf = last_token.clone();
+ }
+
+ if !buf.is_empty() && !compressed.is_empty() {
+ let cur = buf.chars().next().unwrap();
+
+ if cur == '_' || is_id_first_alphabetic(cur) || is_id_continue(cur) {
+ let prev = compressed.chars().last().unwrap();
+
+ if prev == '_' || is_id_first_alphabetic(prev) || is_id_continue(prev) {
+ compressed.push(' ');
+ }
+ }
+ }
+
+ compressed.push_str(&buf);
+ }
+ }
+ }
+
+ Some((token, pos))
+ }
+}
+
+impl FusedIterator for TokenIterator<'_> {}
+
+impl Engine {
+ /// _(internals)_ Tokenize an input text stream.
+ /// Exported under the `internals` feature only.
+ #[cfg(feature = "internals")]
+ #[inline(always)]
+ #[must_use]
+ pub fn lex<'a>(
+ &'a self,
+ input: impl IntoIterator<Item = &'a (impl AsRef<str> + 'a)>,
+ ) -> (TokenIterator<'a>, TokenizerControl) {
+ self.lex_raw(input, None)
+ }
+ /// _(internals)_ Tokenize an input text stream with a mapping function.
+ /// Exported under the `internals` feature only.
+ #[cfg(feature = "internals")]
+ #[inline(always)]
+ #[must_use]
+ pub fn lex_with_map<'a>(
+ &'a self,
+ input: impl IntoIterator<Item = &'a (impl AsRef<str> + 'a)>,
+ token_mapper: &'a OnParseTokenCallback,
+ ) -> (TokenIterator<'a>, TokenizerControl) {
+ self.lex_raw(input, Some(token_mapper))
+ }
+ /// Tokenize an input text stream with an optional mapping function.
+ #[inline]
+ #[must_use]
+ pub(crate) fn lex_raw<'a>(
+ &'a self,
+ input: impl IntoIterator<Item = &'a (impl AsRef<str> + 'a)>,
+ token_mapper: Option<&'a OnParseTokenCallback>,
+ ) -> (TokenIterator<'a>, TokenizerControl) {
+ let buffer: TokenizerControl = RefCell::new(TokenizerControlBlock::new()).into();
+ let buffer2 = buffer.clone();
+
+ (
+ TokenIterator {
+ engine: self,
+ state: TokenizeState {
+ max_string_len: NonZeroUsize::new(self.max_string_size()),
+ next_token_cannot_be_unary: false,
+ tokenizer_control: buffer,
+ comment_level: 0,
+ include_comments: false,
+ is_within_text_terminated_by: None,
+ last_token: None,
+ },
+ pos: Position::new(1, 0),
+ stream: MultiInputsStream {
+ buf: SmallVec::new_const(),
+ streams: input
+ .into_iter()
+ .map(|s| s.as_ref().chars().peekable())
+ .collect(),
+ index: 0,
+ },
+ token_mapper,
+ },
+ buffer2,
+ )
+ }
+}
diff --git a/rhai/src/types/bloom_filter.rs b/rhai/src/types/bloom_filter.rs
new file mode 100644
index 0000000..51be6f3
--- /dev/null
+++ b/rhai/src/types/bloom_filter.rs
@@ -0,0 +1,120 @@
+//! A simple bloom filter implementation for `u64` hash values only.
+
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ mem,
+ ops::{Add, AddAssign},
+};
+
+/// Number of bits for a `usize`.
+const USIZE_BITS: usize = mem::size_of::<usize>() * 8;
+
+/// Number of `usize` values required for 256 bits.
+const SIZE: usize = 256 / USIZE_BITS;
+
+/// A simple bloom filter implementation for `u64` hash values only - i.e. all 64 bits are assumed
+/// to be relatively random.
+///
+/// For this reason, the implementation is simplistic - it just looks at the least significant byte
+/// of the `u64` hash value and sets the corresponding bit in a 256-long bit vector.
+///
+/// The rationale of this type is to avoid pulling in another dependent crate.
+#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
+pub struct BloomFilterU64([usize; SIZE]);
+
+impl BloomFilterU64 {
+ /// Get the bit position of a `u64` hash value.
+ #[inline(always)]
+ #[must_use]
+ const fn calc_hash(value: u64) -> (usize, usize) {
+ let hash = (value & 0x00ff) as usize;
+ (hash / USIZE_BITS, 0x01 << (hash % USIZE_BITS))
+ }
+ /// Create a new [`BloomFilterU64`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn new() -> Self {
+ Self([0; SIZE])
+ }
+ /// Is this [`BloomFilterU64`] empty?
+ #[inline(always)]
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ self.0 == [0; SIZE]
+ }
+ /// Clear this [`BloomFilterU64`].
+ #[inline(always)]
+ pub fn clear(&mut self) -> &mut Self {
+ self.0 = [0; SIZE];
+ self
+ }
+ /// Mark a `u64` hash into this [`BloomFilterU64`].
+ #[inline]
+ pub fn mark(&mut self, hash: u64) -> &mut Self {
+ let (offset, mask) = Self::calc_hash(hash);
+ self.0[offset] |= mask;
+ self
+ }
+ /// Is a `u64` hash definitely absent from this [`BloomFilterU64`]?
+ #[inline]
+ #[must_use]
+ pub const fn is_absent(&self, hash: u64) -> bool {
+ let (offset, mask) = Self::calc_hash(hash);
+ (self.0[offset] & mask) == 0
+ }
+ /// If a `u64` hash is absent from this [`BloomFilterU64`], return `true` and then mark it.
+ /// Otherwise return `false`.
+ #[inline]
+ #[must_use]
+ pub fn is_absent_and_set(&mut self, hash: u64) -> bool {
+ let (offset, mask) = Self::calc_hash(hash);
+ let result = (self.0[offset] & mask) == 0;
+ self.0[offset] |= mask;
+ result
+ }
+}
+
+impl Add for &BloomFilterU64 {
+ type Output = BloomFilterU64;
+
+ #[inline]
+ fn add(self, rhs: Self) -> Self::Output {
+ let mut buf = [0; SIZE];
+
+ self.0
+ .iter()
+ .zip(rhs.0.iter())
+ .map(|(&a, &b)| a | b)
+ .zip(buf.iter_mut())
+ .for_each(|(v, x)| *x = v);
+
+ BloomFilterU64(buf)
+ }
+}
+
+impl Add<BloomFilterU64> for &BloomFilterU64 {
+ type Output = BloomFilterU64;
+
+ #[inline(always)]
+ fn add(self, rhs: BloomFilterU64) -> Self::Output {
+ self + &rhs
+ }
+}
+
+impl AddAssign<Self> for BloomFilterU64 {
+ #[inline(always)]
+ fn add_assign(&mut self, rhs: Self) {
+ *self += &rhs;
+ }
+}
+
+impl AddAssign<&Self> for BloomFilterU64 {
+ #[inline]
+ fn add_assign(&mut self, rhs: &Self) {
+ self.0
+ .iter_mut()
+ .zip(rhs.0.iter())
+ .for_each(|(x, &v)| *x |= v);
+ }
+}
diff --git a/rhai/src/types/custom_types.rs b/rhai/src/types/custom_types.rs
new file mode 100644
index 0000000..14157e3
--- /dev/null
+++ b/rhai/src/types/custom_types.rs
@@ -0,0 +1,63 @@
+//! Collection of custom types.
+
+use crate::Identifier;
+use std::{any::type_name, collections::BTreeMap};
+
+/// _(internals)_ Information for a custom type.
+/// Exported under the `internals` feature only.
+#[derive(Debug, Eq, PartialEq, Clone, Hash, Default)]
+pub struct CustomTypeInfo {
+ /// Friendly display name of the custom type.
+ pub display_name: Identifier,
+}
+
+/// _(internals)_ A collection of custom types.
+/// Exported under the `internals` feature only.
+#[derive(Debug, Clone, Hash)]
+pub struct CustomTypesCollection(BTreeMap<Identifier, CustomTypeInfo>);
+
+impl Default for CustomTypesCollection {
+ #[inline(always)]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl CustomTypesCollection {
+ /// Create a new [`CustomTypesCollection`].
+ #[inline(always)]
+ pub fn new() -> Self {
+ Self(BTreeMap::new())
+ }
+ /// Register a custom type.
+ #[inline(always)]
+ pub fn add(&mut self, type_name: impl Into<Identifier>, name: impl Into<Identifier>) {
+ self.add_raw(
+ type_name,
+ CustomTypeInfo {
+ display_name: name.into(),
+ },
+ );
+ }
+ /// Register a custom type.
+ #[inline(always)]
+ pub fn add_type<T>(&mut self, name: &str) {
+ self.add_raw(
+ type_name::<T>(),
+ CustomTypeInfo {
+ display_name: name.into(),
+ },
+ );
+ }
+ /// Register a custom type.
+ #[inline(always)]
+ pub fn add_raw(&mut self, type_name: impl Into<Identifier>, custom_type: CustomTypeInfo) {
+ self.0.insert(type_name.into(), custom_type);
+ }
+ /// Find a custom type.
+ #[inline(always)]
+ #[must_use]
+ pub fn get(&self, key: &str) -> Option<&CustomTypeInfo> {
+ self.0.get(key)
+ }
+}
diff --git a/rhai/src/types/dynamic.rs b/rhai/src/types/dynamic.rs
new file mode 100644
index 0000000..7650ad9
--- /dev/null
+++ b/rhai/src/types/dynamic.rs
@@ -0,0 +1,2314 @@
+//! Helper module which defines the [`Dynamic`] data type.
+
+use crate::{ExclusiveRange, FnPtr, ImmutableString, InclusiveRange, INT};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ any::{type_name, Any, TypeId},
+ fmt,
+ hash::{Hash, Hasher},
+ mem,
+ ops::{Deref, DerefMut},
+ str::FromStr,
+};
+
+pub use super::Variant;
+
+#[cfg(not(feature = "no_time"))]
+#[cfg(not(target_family = "wasm"))]
+pub use std::time::Instant;
+
+#[cfg(not(feature = "no_time"))]
+#[cfg(target_family = "wasm")]
+pub use instant::Instant;
+
+/// The message: data type was checked
+#[allow(dead_code)]
+const CHECKED: &str = "data type was checked";
+
+/// _(internals)_ Modes of access.
+/// Exported under the `internals` feature only.
+#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)]
+#[non_exhaustive]
+pub enum AccessMode {
+ /// Mutable.
+ ReadWrite,
+ /// Immutable.
+ ReadOnly,
+}
+
+/// Arbitrary data attached to a [`Dynamic`] value.
+#[cfg(target_pointer_width = "64")]
+pub type Tag = i32;
+
+/// Arbitrary data attached to a [`Dynamic`] value.
+#[cfg(target_pointer_width = "32")]
+pub type Tag = i16;
+
+/// Default tag value for [`Dynamic`].
+const DEFAULT_TAG_VALUE: Tag = 0;
+
+/// Dynamic type containing any value.
+#[must_use]
+pub struct Dynamic(pub(crate) Union);
+
+/// Internal [`Dynamic`] representation.
+///
+/// Most variants are boxed to reduce the size.
+#[must_use]
+pub enum Union {
+ /// The Unit value - ().
+ Unit((), Tag, AccessMode),
+ /// A boolean value.
+ Bool(bool, Tag, AccessMode),
+ /// An [`ImmutableString`] value.
+ Str(ImmutableString, Tag, AccessMode),
+ /// A character value.
+ Char(char, Tag, AccessMode),
+ /// An integer value.
+ Int(INT, Tag, AccessMode),
+ /// A floating-point value.
+ #[cfg(not(feature = "no_float"))]
+ Float(super::FloatWrapper<crate::FLOAT>, Tag, AccessMode),
+ /// _(decimal)_ A fixed-precision decimal value.
+ /// Exported under the `decimal` feature only.
+ #[cfg(feature = "decimal")]
+ Decimal(Box<rust_decimal::Decimal>, Tag, AccessMode),
+ /// An array value.
+ #[cfg(not(feature = "no_index"))]
+ Array(Box<crate::Array>, Tag, AccessMode),
+ /// An blob (byte array).
+ #[cfg(not(feature = "no_index"))]
+ Blob(Box<crate::Blob>, Tag, AccessMode),
+ /// An object map value.
+ #[cfg(not(feature = "no_object"))]
+ Map(Box<crate::Map>, Tag, AccessMode),
+ /// A function pointer.
+ FnPtr(Box<FnPtr>, Tag, AccessMode),
+ /// A timestamp value.
+ #[cfg(not(feature = "no_time"))]
+ TimeStamp(Box<Instant>, Tag, AccessMode),
+
+ /// Any type as a trait object.
+ ///
+ /// An extra level of redirection is used in order to avoid bloating the size of [`Dynamic`]
+ /// because `Box<dyn Variant>` is a fat pointer.
+ Variant(Box<Box<dyn Variant>>, Tag, AccessMode),
+
+ /// A _shared_ value of any type.
+ #[cfg(not(feature = "no_closure"))]
+ Shared(crate::Shared<crate::Locked<Dynamic>>, Tag, AccessMode),
+}
+
+/// _(internals)_ Lock guard for reading a [`Dynamic`].
+/// Exported under the `internals` feature only.
+///
+/// This type provides transparent interoperability between normal [`Dynamic`] and shared
+/// [`Dynamic`] values.
+#[derive(Debug)]
+#[must_use]
+pub struct DynamicReadLock<'d, T: Clone>(DynamicReadLockInner<'d, T>);
+
+/// Different types of read guards for [`DynamicReadLock`].
+#[derive(Debug)]
+#[must_use]
+enum DynamicReadLockInner<'d, T: Clone> {
+ /// A simple reference to a non-shared value.
+ Reference(&'d T),
+
+ /// A read guard to a shared value.
+ #[cfg(not(feature = "no_closure"))]
+ Guard(crate::func::native::LockGuard<'d, Dynamic>),
+}
+
+impl<'d, T: Any + Clone> Deref for DynamicReadLock<'d, T> {
+ type Target = T;
+
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ match self.0 {
+ DynamicReadLockInner::Reference(reference) => reference,
+ #[cfg(not(feature = "no_closure"))]
+ DynamicReadLockInner::Guard(ref guard) => guard.downcast_ref().expect(CHECKED),
+ }
+ }
+}
+
+/// _(internals)_ Lock guard for writing a [`Dynamic`].
+/// Exported under the `internals` feature only.
+///
+/// This type provides transparent interoperability between normal [`Dynamic`] and shared
+/// [`Dynamic`] values.
+#[derive(Debug)]
+#[must_use]
+pub struct DynamicWriteLock<'d, T: Clone>(DynamicWriteLockInner<'d, T>);
+
+/// Different types of write guards for [`DynamicReadLock`].
+#[derive(Debug)]
+#[must_use]
+enum DynamicWriteLockInner<'d, T: Clone> {
+ /// A simple mutable reference to a non-shared value.
+ Reference(&'d mut T),
+
+ /// A write guard to a shared value.
+ #[cfg(not(feature = "no_closure"))]
+ Guard(crate::func::native::LockGuardMut<'d, Dynamic>),
+}
+
+impl<'d, T: Any + Clone> Deref for DynamicWriteLock<'d, T> {
+ type Target = T;
+
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ match self.0 {
+ DynamicWriteLockInner::Reference(ref reference) => reference,
+ #[cfg(not(feature = "no_closure"))]
+ DynamicWriteLockInner::Guard(ref guard) => guard.downcast_ref().expect(CHECKED),
+ }
+ }
+}
+
+impl<'d, T: Any + Clone> DerefMut for DynamicWriteLock<'d, T> {
+ #[inline]
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ match self.0 {
+ DynamicWriteLockInner::Reference(ref mut reference) => reference,
+ #[cfg(not(feature = "no_closure"))]
+ DynamicWriteLockInner::Guard(ref mut guard) => guard.downcast_mut().expect(CHECKED),
+ }
+ }
+}
+
+impl Dynamic {
+ /// Get the arbitrary data attached to this [`Dynamic`].
+ #[must_use]
+ pub const fn tag(&self) -> Tag {
+ match self.0 {
+ Union::Unit(_, tag, _)
+ | Union::Bool(_, tag, _)
+ | Union::Str(_, tag, _)
+ | Union::Char(_, tag, _)
+ | Union::Int(_, tag, _)
+ | Union::FnPtr(_, tag, _)
+ | Union::Variant(_, tag, _) => tag,
+
+ #[cfg(not(feature = "no_float"))]
+ Union::Float(_, tag, _) => tag,
+ #[cfg(feature = "decimal")]
+ Union::Decimal(_, tag, _) => tag,
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(_, tag, _) | Union::Blob(_, tag, _) => tag,
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(_, tag, _) => tag,
+ #[cfg(not(feature = "no_time"))]
+ Union::TimeStamp(_, tag, _) => tag,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(_, tag, _) => tag,
+ }
+ }
+ /// Attach arbitrary data to this [`Dynamic`].
+ pub fn set_tag(&mut self, value: Tag) -> &mut Self {
+ match self.0 {
+ Union::Unit(_, ref mut tag, _)
+ | Union::Bool(_, ref mut tag, _)
+ | Union::Str(_, ref mut tag, _)
+ | Union::Char(_, ref mut tag, _)
+ | Union::Int(_, ref mut tag, _)
+ | Union::FnPtr(_, ref mut tag, _)
+ | Union::Variant(_, ref mut tag, _) => *tag = value,
+
+ #[cfg(not(feature = "no_float"))]
+ Union::Float(_, ref mut tag, _) => *tag = value,
+ #[cfg(feature = "decimal")]
+ Union::Decimal(_, ref mut tag, _) => *tag = value,
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(_, ref mut tag, _) | Union::Blob(_, ref mut tag, _) => *tag = value,
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(_, ref mut tag, _) => *tag = value,
+ #[cfg(not(feature = "no_time"))]
+ Union::TimeStamp(_, ref mut tag, _) => *tag = value,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(_, ref mut tag, _) => *tag = value,
+ }
+ self
+ }
+ /// Does this [`Dynamic`] hold a variant data type instead of one of the supported system
+ /// primitive types?
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_variant(&self) -> bool {
+ matches!(self.0, Union::Variant(..))
+ }
+ /// Is the value held by this [`Dynamic`] shared?
+ ///
+ /// Not available under `no_closure`.
+ #[cfg(not(feature = "no_closure"))]
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_shared(&self) -> bool {
+ #[cfg(not(feature = "no_closure"))]
+ return matches!(self.0, Union::Shared(..));
+ #[cfg(feature = "no_closure")]
+ return false;
+ }
+ /// Is the value held by this [`Dynamic`] a particular type?
+ ///
+ /// # Panics or Deadlocks When Value is Shared
+ ///
+ /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1).
+ /// Otherwise, this call panics if the data is currently borrowed for write.
+ #[inline]
+ #[must_use]
+ pub fn is<T: Any + Clone>(&self) -> bool {
+ #[cfg(not(feature = "no_closure"))]
+ if self.is_shared() {
+ return TypeId::of::<T>() == self.type_id();
+ }
+
+ if TypeId::of::<T>() == TypeId::of::<()>() {
+ return matches!(self.0, Union::Unit(..));
+ }
+ if TypeId::of::<T>() == TypeId::of::<bool>() {
+ return matches!(self.0, Union::Bool(..));
+ }
+ if TypeId::of::<T>() == TypeId::of::<char>() {
+ return matches!(self.0, Union::Char(..));
+ }
+ if TypeId::of::<T>() == TypeId::of::<INT>() {
+ return matches!(self.0, Union::Int(..));
+ }
+ #[cfg(not(feature = "no_float"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::FLOAT>() {
+ return matches!(self.0, Union::Float(..));
+ }
+ if TypeId::of::<T>() == TypeId::of::<ImmutableString>()
+ || TypeId::of::<T>() == TypeId::of::<String>()
+ {
+ return matches!(self.0, Union::Str(..));
+ }
+ #[cfg(not(feature = "no_index"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Array>() {
+ return matches!(self.0, Union::Array(..));
+ }
+ #[cfg(not(feature = "no_index"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Blob>() {
+ return matches!(self.0, Union::Blob(..));
+ }
+ #[cfg(not(feature = "no_object"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Map>() {
+ return matches!(self.0, Union::Map(..));
+ }
+ #[cfg(feature = "decimal")]
+ if TypeId::of::<T>() == TypeId::of::<rust_decimal::Decimal>() {
+ return matches!(self.0, Union::Decimal(..));
+ }
+ if TypeId::of::<T>() == TypeId::of::<FnPtr>() {
+ return matches!(self.0, Union::FnPtr(..));
+ }
+ #[cfg(not(feature = "no_time"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Instant>() {
+ return matches!(self.0, Union::TimeStamp(..));
+ }
+
+ TypeId::of::<T>() == self.type_id()
+ }
+ /// Get the [`TypeId`] of the value held by this [`Dynamic`].
+ ///
+ /// # Panics or Deadlocks When Value is Shared
+ ///
+ /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1).
+ /// Otherwise, this call panics if the data is currently borrowed for write.
+ #[must_use]
+ pub fn type_id(&self) -> TypeId {
+ match self.0 {
+ Union::Unit(..) => TypeId::of::<()>(),
+ Union::Bool(..) => TypeId::of::<bool>(),
+ Union::Str(..) => TypeId::of::<ImmutableString>(),
+ Union::Char(..) => TypeId::of::<char>(),
+ Union::Int(..) => TypeId::of::<INT>(),
+ #[cfg(not(feature = "no_float"))]
+ Union::Float(..) => TypeId::of::<crate::FLOAT>(),
+ #[cfg(feature = "decimal")]
+ Union::Decimal(..) => TypeId::of::<rust_decimal::Decimal>(),
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(..) => TypeId::of::<crate::Array>(),
+ #[cfg(not(feature = "no_index"))]
+ Union::Blob(..) => TypeId::of::<crate::Blob>(),
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(..) => TypeId::of::<crate::Map>(),
+ Union::FnPtr(..) => TypeId::of::<FnPtr>(),
+ #[cfg(not(feature = "no_time"))]
+ Union::TimeStamp(..) => TypeId::of::<Instant>(),
+
+ Union::Variant(ref v, ..) => (***v).type_id(),
+
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => (*crate::func::locked_read(cell)).type_id(),
+ }
+ }
+ /// Get the name of the type of the value held by this [`Dynamic`].
+ ///
+ /// # Panics or Deadlocks When Value is Shared
+ ///
+ /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1).
+ /// Otherwise, this call panics if the data is currently borrowed for write.
+ #[must_use]
+ pub fn type_name(&self) -> &'static str {
+ match self.0 {
+ Union::Unit(..) => "()",
+ Union::Bool(..) => "bool",
+ Union::Str(..) => "string",
+ Union::Char(..) => "char",
+ Union::Int(..) => type_name::<INT>(),
+ #[cfg(not(feature = "no_float"))]
+ Union::Float(..) => type_name::<crate::FLOAT>(),
+ #[cfg(feature = "decimal")]
+ Union::Decimal(..) => "decimal",
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(..) => "array",
+ #[cfg(not(feature = "no_index"))]
+ Union::Blob(..) => "blob",
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(..) => "map",
+ Union::FnPtr(..) => "Fn",
+ #[cfg(not(feature = "no_time"))]
+ Union::TimeStamp(..) => "timestamp",
+
+ Union::Variant(ref v, ..) => (***v).type_name(),
+
+ #[cfg(not(feature = "no_closure"))]
+ #[cfg(not(feature = "sync"))]
+ Union::Shared(ref cell, ..) => cell
+ .try_borrow()
+ .map(|v| (*v).type_name())
+ .unwrap_or("<shared>"),
+ #[cfg(not(feature = "no_closure"))]
+ #[cfg(feature = "sync")]
+ Union::Shared(ref cell, ..) => (*cell.read().unwrap()).type_name(),
+ }
+ }
+}
+
+impl Hash for Dynamic {
+ /// Hash the [`Dynamic`] value.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the [`Dynamic`] value contains an unrecognized trait object.
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ mem::discriminant(&self.0).hash(state);
+
+ match self.0 {
+ Union::Unit(..) => (),
+ Union::Bool(ref b, ..) => b.hash(state),
+ Union::Str(ref s, ..) => s.hash(state),
+ Union::Char(ref c, ..) => c.hash(state),
+ Union::Int(ref i, ..) => i.hash(state),
+ #[cfg(not(feature = "no_float"))]
+ Union::Float(ref f, ..) => f.hash(state),
+ #[cfg(feature = "decimal")]
+ Union::Decimal(ref d, ..) => d.hash(state),
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(ref a, ..) => a.hash(state),
+ #[cfg(not(feature = "no_index"))]
+ Union::Blob(ref a, ..) => a.hash(state),
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(ref m, ..) => m.hash(state),
+ Union::FnPtr(ref f, ..) => f.hash(state),
+
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => (*crate::func::locked_read(cell)).hash(state),
+
+ Union::Variant(..) => unimplemented!("{} cannot be hashed", self.type_name()),
+
+ #[cfg(not(feature = "no_time"))]
+ Union::TimeStamp(..) => unimplemented!("{} cannot be hashed", self.type_name()),
+ }
+ }
+}
+
+impl fmt::Display for Dynamic {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self.0 {
+ Union::Unit(..) => Ok(()),
+ Union::Bool(ref v, ..) => fmt::Display::fmt(v, f),
+ Union::Str(ref v, ..) => fmt::Display::fmt(v, f),
+ Union::Char(ref v, ..) => fmt::Display::fmt(v, f),
+ Union::Int(ref v, ..) => fmt::Display::fmt(v, f),
+ #[cfg(not(feature = "no_float"))]
+ Union::Float(ref v, ..) => fmt::Display::fmt(v, f),
+ #[cfg(feature = "decimal")]
+ Union::Decimal(ref v, ..) => fmt::Display::fmt(v, f),
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(..) => fmt::Debug::fmt(self, f),
+ #[cfg(not(feature = "no_index"))]
+ Union::Blob(..) => fmt::Debug::fmt(self, f),
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(..) => fmt::Debug::fmt(self, f),
+ Union::FnPtr(ref v, ..) => fmt::Display::fmt(v, f),
+ #[cfg(not(feature = "no_time"))]
+ Union::TimeStamp(..) => f.write_str("<timestamp>"),
+
+ Union::Variant(ref v, ..) => {
+ let _value_any = (***v).as_any();
+ let _type_id = _value_any.type_id();
+
+ #[cfg(not(feature = "only_i32"))]
+ #[cfg(not(feature = "only_i64"))]
+ if let Some(value) = _value_any.downcast_ref::<u8>() {
+ return fmt::Display::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<u16>() {
+ return fmt::Display::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<u32>() {
+ return fmt::Display::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<u64>() {
+ return fmt::Display::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<i8>() {
+ return fmt::Display::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<i16>() {
+ return fmt::Display::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<i32>() {
+ return fmt::Display::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<i64>() {
+ return fmt::Display::fmt(value, f);
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ #[cfg(not(feature = "f32_float"))]
+ if let Some(value) = _value_any.downcast_ref::<f32>() {
+ return fmt::Display::fmt(value, f);
+ }
+ #[cfg(not(feature = "no_float"))]
+ #[cfg(feature = "f32_float")]
+ if let Some(value) = _value_any.downcast_ref::<f64>() {
+ return fmt::Display::fmt(value, f);
+ }
+
+ #[cfg(not(feature = "only_i32"))]
+ #[cfg(not(feature = "only_i64"))]
+ #[cfg(not(target_family = "wasm"))]
+ if let Some(value) = _value_any.downcast_ref::<u128>() {
+ return fmt::Display::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<i128>() {
+ return fmt::Display::fmt(value, f);
+ }
+
+ if let Some(range) = _value_any.downcast_ref::<ExclusiveRange>() {
+ return write!(f, "{}..{}", range.start, range.end);
+ } else if let Some(range) = _value_any.downcast_ref::<InclusiveRange>() {
+ return write!(f, "{}..={}", range.start(), range.end());
+ }
+
+ f.write_str((***v).type_name())
+ }
+
+ #[cfg(not(feature = "no_closure"))]
+ #[cfg(not(feature = "sync"))]
+ Union::Shared(ref cell, ..) => {
+ if let Ok(v) = cell.try_borrow() {
+ fmt::Display::fmt(&*v, f)
+ } else {
+ f.write_str("<shared>")
+ }
+ }
+ #[cfg(not(feature = "no_closure"))]
+ #[cfg(feature = "sync")]
+ Union::Shared(ref cell, ..) => fmt::Display::fmt(&*cell.read().unwrap(), f),
+ }
+ }
+}
+
+impl fmt::Debug for Dynamic {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self.0 {
+ Union::Unit(ref v, ..) => fmt::Debug::fmt(v, f),
+ Union::Bool(ref v, ..) => fmt::Debug::fmt(v, f),
+ Union::Str(ref v, ..) => fmt::Debug::fmt(v, f),
+ Union::Char(ref v, ..) => fmt::Debug::fmt(v, f),
+ Union::Int(ref v, ..) => fmt::Debug::fmt(v, f),
+ #[cfg(not(feature = "no_float"))]
+ Union::Float(ref v, ..) => fmt::Debug::fmt(v, f),
+ #[cfg(feature = "decimal")]
+ Union::Decimal(ref v, ..) => fmt::Debug::fmt(v, f),
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(ref v, ..) => fmt::Debug::fmt(v, f),
+ #[cfg(not(feature = "no_index"))]
+ Union::Blob(ref v, ..) => {
+ f.write_str("[")?;
+ v.iter().enumerate().try_for_each(|(i, v)| {
+ if i > 0 && i % 8 == 0 {
+ f.write_str(" ")?;
+ }
+ write!(f, "{v:02x}")
+ })?;
+ f.write_str("]")
+ }
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(ref v, ..) => {
+ f.write_str("#")?;
+ fmt::Debug::fmt(v, f)
+ }
+ Union::FnPtr(ref v, ..) => fmt::Debug::fmt(v, f),
+ #[cfg(not(feature = "no_time"))]
+ Union::TimeStamp(..) => write!(f, "<timestamp>"),
+
+ Union::Variant(ref v, ..) => {
+ let _value_any = (***v).as_any();
+ let _type_id = _value_any.type_id();
+
+ #[cfg(not(feature = "only_i32"))]
+ #[cfg(not(feature = "only_i64"))]
+ if let Some(value) = _value_any.downcast_ref::<u8>() {
+ return fmt::Debug::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<u16>() {
+ return fmt::Debug::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<u32>() {
+ return fmt::Debug::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<u64>() {
+ return fmt::Debug::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<i8>() {
+ return fmt::Debug::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<i16>() {
+ return fmt::Debug::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<i32>() {
+ return fmt::Debug::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<i64>() {
+ return fmt::Debug::fmt(value, f);
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ #[cfg(not(feature = "f32_float"))]
+ if let Some(value) = _value_any.downcast_ref::<f32>() {
+ return fmt::Debug::fmt(value, f);
+ }
+ #[cfg(not(feature = "no_float"))]
+ #[cfg(feature = "f32_float")]
+ if let Some(value) = _value_any.downcast_ref::<f64>() {
+ return fmt::Debug::fmt(value, f);
+ }
+
+ #[cfg(not(feature = "only_i32"))]
+ #[cfg(not(feature = "only_i64"))]
+ #[cfg(not(target_family = "wasm"))]
+ if let Some(value) = _value_any.downcast_ref::<u128>() {
+ return fmt::Debug::fmt(value, f);
+ } else if let Some(value) = _value_any.downcast_ref::<i128>() {
+ return fmt::Debug::fmt(value, f);
+ }
+
+ if let Some(range) = _value_any.downcast_ref::<ExclusiveRange>() {
+ return write!(f, "{}..{}", range.start, range.end);
+ } else if let Some(range) = _value_any.downcast_ref::<InclusiveRange>() {
+ return write!(f, "{}..={}", range.start(), range.end());
+ }
+
+ f.write_str((***v).type_name())
+ }
+
+ #[cfg(not(feature = "no_closure"))]
+ #[cfg(not(feature = "sync"))]
+ Union::Shared(ref cell, ..) => {
+ if let Ok(v) = cell.try_borrow() {
+ write!(f, "{:?} (shared)", *v)
+ } else {
+ f.write_str("<shared>")
+ }
+ }
+ #[cfg(not(feature = "no_closure"))]
+ #[cfg(feature = "sync")]
+ Union::Shared(ref cell, ..) => fmt::Debug::fmt(&*cell.read().unwrap(), f),
+ }
+ }
+}
+
+#[allow(clippy::enum_glob_use)]
+use AccessMode::*;
+
+impl Clone for Dynamic {
+ /// Clone the [`Dynamic`] value.
+ ///
+ /// # WARNING
+ ///
+ /// The cloned copy is marked read-write even if the original is read-only.
+ fn clone(&self) -> Self {
+ match self.0 {
+ Union::Unit(v, tag, ..) => Self(Union::Unit(v, tag, ReadWrite)),
+ Union::Bool(v, tag, ..) => Self(Union::Bool(v, tag, ReadWrite)),
+ Union::Str(ref v, tag, ..) => Self(Union::Str(v.clone(), tag, ReadWrite)),
+ Union::Char(v, tag, ..) => Self(Union::Char(v, tag, ReadWrite)),
+ Union::Int(v, tag, ..) => Self(Union::Int(v, tag, ReadWrite)),
+ #[cfg(not(feature = "no_float"))]
+ Union::Float(v, tag, ..) => Self(Union::Float(v, tag, ReadWrite)),
+ #[cfg(feature = "decimal")]
+ Union::Decimal(ref v, tag, ..) => Self(Union::Decimal(v.clone(), tag, ReadWrite)),
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(ref v, tag, ..) => Self(Union::Array(v.clone(), tag, ReadWrite)),
+ #[cfg(not(feature = "no_index"))]
+ Union::Blob(ref v, tag, ..) => Self(Union::Blob(v.clone(), tag, ReadWrite)),
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(ref v, tag, ..) => Self(Union::Map(v.clone(), tag, ReadWrite)),
+ Union::FnPtr(ref v, tag, ..) => Self(Union::FnPtr(v.clone(), tag, ReadWrite)),
+ #[cfg(not(feature = "no_time"))]
+ Union::TimeStamp(ref v, tag, ..) => Self(Union::TimeStamp(v.clone(), tag, ReadWrite)),
+
+ Union::Variant(ref v, tag, ..) => Self(Union::Variant(
+ v.as_ref().as_ref().clone_object().into(),
+ tag,
+ ReadWrite,
+ )),
+
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, tag, ..) => Self(Union::Shared(cell.clone(), tag, ReadWrite)),
+ }
+ }
+}
+
+impl Default for Dynamic {
+ #[inline(always)]
+ fn default() -> Self {
+ Self::UNIT
+ }
+}
+
+#[cfg(not(feature = "no_float"))]
+#[cfg(feature = "f32_float")]
+use std::f32::consts as FloatConstants;
+#[cfg(not(feature = "no_float"))]
+#[cfg(not(feature = "f32_float"))]
+use std::f64::consts as FloatConstants;
+
+impl Dynamic {
+ /// A [`Dynamic`] containing a `()`.
+ pub const UNIT: Self = Self(Union::Unit((), DEFAULT_TAG_VALUE, ReadWrite));
+ /// A [`Dynamic`] containing a `true`.
+ pub const TRUE: Self = Self::from_bool(true);
+ /// A [`Dynamic`] containing a [`false`].
+ pub const FALSE: Self = Self::from_bool(false);
+ /// A [`Dynamic`] containing the integer zero.
+ pub const ZERO: Self = Self::from_int(0);
+ /// A [`Dynamic`] containing the integer 1.
+ pub const ONE: Self = Self::from_int(1);
+ /// A [`Dynamic`] containing the integer 2.
+ pub const TWO: Self = Self::from_int(2);
+ /// A [`Dynamic`] containing the integer 3.
+ pub const THREE: Self = Self::from_int(3);
+ /// A [`Dynamic`] containing the integer 10.
+ pub const TEN: Self = Self::from_int(10);
+ /// A [`Dynamic`] containing the integer 100.
+ pub const HUNDRED: Self = Self::from_int(100);
+ /// A [`Dynamic`] containing the integer 1,000.
+ pub const THOUSAND: Self = Self::from_int(1000);
+ /// A [`Dynamic`] containing the integer 1,000,000.
+ pub const MILLION: Self = Self::from_int(1_000_000);
+ /// A [`Dynamic`] containing the integer -1.
+ pub const NEGATIVE_ONE: Self = Self::from_int(-1);
+ /// A [`Dynamic`] containing the integer -2.
+ pub const NEGATIVE_TWO: Self = Self::from_int(-2);
+ /// A [`Dynamic`] containing `0.0`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_ZERO: Self = Self::from_float(0.0);
+ /// A [`Dynamic`] containing `1.0`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_ONE: Self = Self::from_float(1.0);
+ /// A [`Dynamic`] containing `2.0`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_TWO: Self = Self::from_float(2.0);
+ /// A [`Dynamic`] containing `10.0`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_TEN: Self = Self::from_float(10.0);
+ /// A [`Dynamic`] containing `100.0`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_HUNDRED: Self = Self::from_float(100.0);
+ /// A [`Dynamic`] containing `1000.0`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_THOUSAND: Self = Self::from_float(1000.0);
+ /// A [`Dynamic`] containing `1000000.0`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_MILLION: Self = Self::from_float(1_000_000.0);
+ /// A [`Dynamic`] containing `-1.0`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_NEGATIVE_ONE: Self = Self::from_float(-1.0);
+ /// A [`Dynamic`] containing `-2.0`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_NEGATIVE_TWO: Self = Self::from_float(-2.0);
+ /// A [`Dynamic`] containing `0.5`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_HALF: Self = Self::from_float(0.5);
+ /// A [`Dynamic`] containing `0.25`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_QUARTER: Self = Self::from_float(0.25);
+ /// A [`Dynamic`] containing `0.2`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_FIFTH: Self = Self::from_float(0.2);
+ /// A [`Dynamic`] containing `0.1`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_TENTH: Self = Self::from_float(0.1);
+ /// A [`Dynamic`] containing `0.01`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_HUNDREDTH: Self = Self::from_float(0.01);
+ /// A [`Dynamic`] containing `0.001`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_THOUSANDTH: Self = Self::from_float(0.001);
+ /// A [`Dynamic`] containing `0.000001`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_MILLIONTH: Self = Self::from_float(0.000_001);
+ /// A [`Dynamic`] containing π.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_PI: Self = Self::from_float(FloatConstants::PI);
+ /// A [`Dynamic`] containing π/2.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_HALF_PI: Self = Self::from_float(FloatConstants::FRAC_PI_2);
+ /// A [`Dynamic`] containing π/4.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_QUARTER_PI: Self = Self::from_float(FloatConstants::FRAC_PI_4);
+ /// A [`Dynamic`] containing 2π.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_TWO_PI: Self = Self::from_float(FloatConstants::TAU);
+ /// A [`Dynamic`] containing 1/π.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_INVERSE_PI: Self = Self::from_float(FloatConstants::FRAC_1_PI);
+ /// A [`Dynamic`] containing _e_.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_E: Self = Self::from_float(FloatConstants::E);
+ /// A [`Dynamic`] containing `log` _e_.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_LOG_E: Self = Self::from_float(FloatConstants::LOG10_E);
+ /// A [`Dynamic`] containing `ln 10`.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ pub const FLOAT_LN_10: Self = Self::from_float(FloatConstants::LN_10);
+
+ /// Create a new [`Dynamic`] from a [`bool`].
+ #[inline(always)]
+ pub const fn from_bool(value: bool) -> Self {
+ Self(Union::Bool(value, DEFAULT_TAG_VALUE, ReadWrite))
+ }
+ /// Create a new [`Dynamic`] from an [`INT`].
+ #[inline(always)]
+ pub const fn from_int(value: INT) -> Self {
+ Self(Union::Int(value, DEFAULT_TAG_VALUE, ReadWrite))
+ }
+ /// Create a new [`Dynamic`] from a [`char`].
+ #[inline(always)]
+ pub const fn from_char(value: char) -> Self {
+ Self(Union::Char(value, DEFAULT_TAG_VALUE, ReadWrite))
+ }
+ /// Create a new [`Dynamic`] from a [`FLOAT`][crate::FLOAT].
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ #[inline(always)]
+ pub const fn from_float(value: crate::FLOAT) -> Self {
+ Self(Union::Float(
+ super::FloatWrapper::new(value),
+ DEFAULT_TAG_VALUE,
+ ReadWrite,
+ ))
+ }
+ /// Create a new [`Dynamic`] from a [`Decimal`](https://docs.rs/rust_decimal).
+ ///
+ /// Exported under the `decimal` feature only.
+ #[cfg(feature = "decimal")]
+ #[inline(always)]
+ pub fn from_decimal(value: rust_decimal::Decimal) -> Self {
+ Self(Union::Decimal(value.into(), DEFAULT_TAG_VALUE, ReadWrite))
+ }
+ /// Create a [`Dynamic`] from an [`Array`][crate::Array].
+ #[cfg(not(feature = "no_index"))]
+ #[inline(always)]
+ pub fn from_array(array: crate::Array) -> Self {
+ Self(Union::Array(array.into(), DEFAULT_TAG_VALUE, ReadWrite))
+ }
+ /// Create a [`Dynamic`] from a [`Blob`][crate::Blob].
+ #[cfg(not(feature = "no_index"))]
+ #[inline(always)]
+ pub fn from_blob(blob: crate::Blob) -> Self {
+ Self(Union::Blob(blob.into(), DEFAULT_TAG_VALUE, ReadWrite))
+ }
+ /// Create a [`Dynamic`] from a [`Map`][crate::Map].
+ #[cfg(not(feature = "no_object"))]
+ #[inline(always)]
+ pub fn from_map(map: crate::Map) -> Self {
+ Self(Union::Map(map.into(), DEFAULT_TAG_VALUE, ReadWrite))
+ }
+ /// Create a new [`Dynamic`] from an [`Instant`].
+ ///
+ /// Not available under `no-std` or `no_time`.
+ #[cfg(not(feature = "no_time"))]
+ #[inline(always)]
+ pub fn from_timestamp(value: Instant) -> Self {
+ Self(Union::TimeStamp(value.into(), DEFAULT_TAG_VALUE, ReadWrite))
+ }
+
+ /// Get the [`AccessMode`] for this [`Dynamic`].
+ #[must_use]
+ pub(crate) const fn access_mode(&self) -> AccessMode {
+ match self.0 {
+ Union::Unit(.., access)
+ | Union::Bool(.., access)
+ | Union::Str(.., access)
+ | Union::Char(.., access)
+ | Union::Int(.., access)
+ | Union::FnPtr(.., access)
+ | Union::Variant(.., access) => access,
+
+ #[cfg(not(feature = "no_float"))]
+ Union::Float(.., access) => access,
+ #[cfg(feature = "decimal")]
+ Union::Decimal(.., access) => access,
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(.., access) | Union::Blob(.., access) => access,
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(.., access) => access,
+ #[cfg(not(feature = "no_time"))]
+ Union::TimeStamp(.., access) => access,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(.., access) => access,
+ }
+ }
+ /// Set the [`AccessMode`] for this [`Dynamic`].
+ pub(crate) fn set_access_mode(&mut self, typ: AccessMode) -> &mut Self {
+ match self.0 {
+ Union::Unit(.., ref mut access)
+ | Union::Bool(.., ref mut access)
+ | Union::Str(.., ref mut access)
+ | Union::Char(.., ref mut access)
+ | Union::Int(.., ref mut access)
+ | Union::FnPtr(.., ref mut access)
+ | Union::Variant(.., ref mut access) => *access = typ,
+
+ #[cfg(not(feature = "no_float"))]
+ Union::Float(.., ref mut access) => *access = typ,
+ #[cfg(feature = "decimal")]
+ Union::Decimal(.., ref mut access) => *access = typ,
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(ref mut a, _, ref mut access) => {
+ *access = typ;
+ for v in a.as_mut() {
+ v.set_access_mode(typ);
+ }
+ }
+ #[cfg(not(feature = "no_index"))]
+ Union::Blob(.., ref mut access) => *access = typ,
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(ref mut m, _, ref mut access) => {
+ *access = typ;
+ for v in m.values_mut() {
+ v.set_access_mode(typ);
+ }
+ }
+ #[cfg(not(feature = "no_time"))]
+ Union::TimeStamp(.., ref mut access) => *access = typ,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(.., ref mut access) => *access = typ,
+ }
+ self
+ }
+ /// Make this [`Dynamic`] read-only (i.e. a constant).
+ #[inline(always)]
+ pub fn into_read_only(self) -> Self {
+ let mut value = self;
+ value.set_access_mode(AccessMode::ReadOnly);
+ value
+ }
+ /// Is this [`Dynamic`] read-only?
+ ///
+ /// Constant [`Dynamic`] values are read-only.
+ ///
+ /// # Usage
+ ///
+ /// If a [`&mut Dynamic`][Dynamic] to such a constant is passed to a Rust function, the function
+ /// can use this information to return the error
+ /// [`ErrorAssignmentToConstant`][crate::EvalAltResult::ErrorAssignmentToConstant] if its value
+ /// will be modified.
+ ///
+ /// This safe-guards constant values from being modified within Rust functions.
+ ///
+ /// # Shared Values
+ ///
+ /// If a [`Dynamic`] holds a _shared_ value, then it is read-only only if the shared value
+ /// itself is read-only.
+ #[must_use]
+ pub fn is_read_only(&self) -> bool {
+ #[cfg(not(feature = "no_closure"))]
+ if let Union::Shared(ref cell, ..) = self.0 {
+ return match crate::func::locked_read(cell).access_mode() {
+ ReadWrite => false,
+ ReadOnly => true,
+ };
+ }
+
+ match self.access_mode() {
+ ReadWrite => false,
+ ReadOnly => true,
+ }
+ }
+ /// Can this [`Dynamic`] be hashed?
+ #[must_use]
+ pub(crate) fn is_hashable(&self) -> bool {
+ match self.0 {
+ Union::Unit(..)
+ | Union::Bool(..)
+ | Union::Str(..)
+ | Union::Char(..)
+ | Union::Int(..) => true,
+
+ #[cfg(not(feature = "no_float"))]
+ Union::Float(..) => true,
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(..) => true,
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(..) => true,
+
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => crate::func::locked_read(cell).is_hashable(),
+
+ _ => false,
+ }
+ }
+ /// Create a [`Dynamic`] from any type. A [`Dynamic`] value is simply returned as is.
+ ///
+ /// # Arrays
+ ///
+ /// Beware that you need to pass in an [`Array`][crate::Array] type for it to be recognized as
+ /// an [`Array`][crate::Array]. A [`Vec<T>`][Vec] does not get automatically converted to an
+ /// [`Array`][crate::Array], but will be a custom type instead (stored as a trait object).
+ ///
+ /// Use `array.into()` or `array.into_iter()` to convert a [`Vec<T>`][Vec] into a [`Dynamic`] as
+ /// an [`Array`][crate::Array] value. See the examples for details.
+ ///
+ /// # Hash Maps
+ ///
+ /// Similarly, passing in a [`HashMap<String, T>`][std::collections::HashMap] or
+ /// [`BTreeMap<String, T>`][std::collections::BTreeMap] will not get a [`Map`][crate::Map] but a
+ /// custom type.
+ ///
+ /// Again, use `map.into()` to get a [`Dynamic`] with a [`Map`][crate::Map] value.
+ /// See the examples for details.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use rhai::Dynamic;
+ ///
+ /// let result = Dynamic::from(42_i64);
+ /// assert_eq!(result.type_name(), "i64");
+ /// assert_eq!(result.to_string(), "42");
+ ///
+ /// let result = Dynamic::from("hello");
+ /// assert_eq!(result.type_name(), "string");
+ /// assert_eq!(result.to_string(), "hello");
+ ///
+ /// let new_result = Dynamic::from(result);
+ /// assert_eq!(new_result.type_name(), "string");
+ /// assert_eq!(new_result.to_string(), "hello");
+ ///
+ /// # #[cfg(not(feature = "no_index"))]
+ /// # {
+ /// // Arrays - this is a custom object!
+ /// let result = Dynamic::from(vec![1_i64, 2, 3]);
+ /// assert_eq!(result.type_name(), "alloc::vec::Vec<i64>");
+ ///
+ /// // Use '.into()' to convert a Vec<T> into an Array
+ /// let result: Dynamic = vec![1_i64, 2, 3].into();
+ /// assert_eq!(result.type_name(), "array");
+ /// # }
+ ///
+ /// # #[cfg(not(feature = "no_object"))]
+ /// # {
+ /// # use std::collections::HashMap;
+ /// // Hash map
+ /// let mut map = HashMap::new();
+ /// map.insert("a".to_string(), 1_i64);
+ ///
+ /// // This is a custom object!
+ /// let result = Dynamic::from(map.clone());
+ /// assert_eq!(result.type_name(), "std::collections::hash::map::HashMap<alloc::string::String, i64>");
+ ///
+ /// // Use '.into()' to convert a HashMap<String, T> into an object map
+ /// let result: Dynamic = map.into();
+ /// assert_eq!(result.type_name(), "map");
+ /// # }
+ /// ```
+ #[inline]
+ pub fn from<T: Variant + Clone>(value: T) -> Self {
+ // Coded this way in order to maximally leverage potentials for dead-code removal.
+
+ reify! { value => |v: Self| return v }
+ reify! { value => |v: INT| return v.into() }
+
+ #[cfg(not(feature = "no_float"))]
+ reify! { value => |v: crate::FLOAT| return v.into() }
+
+ #[cfg(feature = "decimal")]
+ reify! { value => |v: rust_decimal::Decimal| return v.into() }
+
+ reify! { value => |v: bool| return v.into() }
+ reify! { value => |v: char| return v.into() }
+ reify! { value => |v: ImmutableString| return v.into() }
+ reify! { value => |v: String| return v.into() }
+ reify! { value => |v: &str| return v.into() }
+ reify! { value => |v: ()| return v.into() }
+
+ #[cfg(not(feature = "no_index"))]
+ reify! { value => |v: crate::Array| return v.into() }
+ #[cfg(not(feature = "no_index"))]
+ // don't use blob.into() because it'll be converted into an Array
+ reify! { value => |v: crate::Blob| return Self::from_blob(v) }
+ #[cfg(not(feature = "no_object"))]
+ reify! { value => |v: crate::Map| return v.into() }
+ reify! { value => |v: FnPtr| return v.into() }
+
+ #[cfg(not(feature = "no_time"))]
+ reify! { value => |v: Instant| return v.into() }
+ #[cfg(not(feature = "no_closure"))]
+ reify! { value => |v: crate::Shared<crate::Locked<Self>>| return v.into() }
+
+ Self(Union::Variant(
+ Box::new(Box::new(value)),
+ DEFAULT_TAG_VALUE,
+ ReadWrite,
+ ))
+ }
+ /// Turn the [`Dynamic`] value into a shared [`Dynamic`] value backed by an
+ /// [`Rc<RefCell<Dynamic>>`][std::rc::Rc] or [`Arc<RwLock<Dynamic>>`][std::sync::Arc]
+ /// depending on the `sync` feature.
+ ///
+ /// Not available under `no_closure`.
+ ///
+ /// Shared [`Dynamic`] values are relatively cheap to clone as they simply increment the
+ /// reference counts.
+ ///
+ /// Shared [`Dynamic`] values can be converted seamlessly to and from ordinary [`Dynamic`]
+ /// values.
+ ///
+ /// If the [`Dynamic`] value is already shared, this method returns itself.
+ #[cfg(not(feature = "no_closure"))]
+ #[inline]
+ pub fn into_shared(self) -> Self {
+ let _access = self.access_mode();
+
+ match self.0 {
+ Union::Shared(..) => self,
+ _ => Self(Union::Shared(
+ crate::Locked::new(self).into(),
+ DEFAULT_TAG_VALUE,
+ _access,
+ )),
+ }
+ }
+ /// Return this [`Dynamic`], replacing it with [`Dynamic::UNIT`].
+ #[inline(always)]
+ pub fn take(&mut self) -> Self {
+ mem::take(self)
+ }
+ /// Convert the [`Dynamic`] value into specific type.
+ ///
+ /// Casting to a [`Dynamic`] just returns as is, but if it contains a shared value,
+ /// it is cloned into a [`Dynamic`] with a normal value.
+ ///
+ /// Returns [`None`] if types mismatched.
+ ///
+ /// # Panics or Deadlocks
+ ///
+ /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1).
+ /// Otherwise, this call panics if the data is currently borrowed for write.
+ ///
+ /// These normally shouldn't occur since most operations in Rhai is single-threaded.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Dynamic;
+ ///
+ /// let x = Dynamic::from(42_u32);
+ ///
+ /// assert_eq!(x.try_cast::<u32>().expect("x should be u32"), 42);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ #[allow(unused_mut)]
+ pub fn try_cast<T: Any>(mut self) -> Option<T> {
+ self.try_cast_raw().ok()
+ }
+ /// Convert the [`Dynamic`] value into specific type.
+ ///
+ /// Casting to a [`Dynamic`] just returns as is, but if it contains a shared value,
+ /// it is cloned into a [`Dynamic`] with a normal value.
+ ///
+ /// Returns itself if types mismatched.
+ #[allow(unused_mut)]
+ pub(crate) fn try_cast_raw<T: Any>(mut self) -> Result<T, Self> {
+ // Coded this way in order to maximally leverage potentials for dead-code removal.
+
+ #[cfg(not(feature = "no_closure"))]
+ self.flatten_in_place();
+
+ if TypeId::of::<T>() == TypeId::of::<Self>() {
+ return Ok(reify! { self => !!! T });
+ }
+ if TypeId::of::<T>() == TypeId::of::<()>() {
+ return match self.0 {
+ Union::Unit(..) => Ok(reify! { () => !!! T }),
+ _ => Err(self),
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<INT>() {
+ return match self.0 {
+ Union::Int(n, ..) => Ok(reify! { n => !!! T }),
+ _ => Err(self),
+ };
+ }
+ #[cfg(not(feature = "no_float"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::FLOAT>() {
+ return match self.0 {
+ Union::Float(v, ..) => Ok(reify! { *v => !!! T }),
+ _ => Err(self),
+ };
+ }
+ #[cfg(feature = "decimal")]
+ if TypeId::of::<T>() == TypeId::of::<rust_decimal::Decimal>() {
+ return match self.0 {
+ Union::Decimal(v, ..) => Ok(reify! { *v => !!! T }),
+ _ => Err(self),
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<bool>() {
+ return match self.0 {
+ Union::Bool(b, ..) => Ok(reify! { b => !!! T }),
+ _ => Err(self),
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<ImmutableString>() {
+ return match self.0 {
+ Union::Str(s, ..) => Ok(reify! { s => !!! T }),
+ _ => Err(self),
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<String>() {
+ return match self.0 {
+ Union::Str(s, ..) => Ok(reify! { s.to_string() => !!! T }),
+ _ => Err(self),
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<char>() {
+ return match self.0 {
+ Union::Char(c, ..) => Ok(reify! { c => !!! T }),
+ _ => Err(self),
+ };
+ }
+ #[cfg(not(feature = "no_index"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Array>() {
+ return match self.0 {
+ Union::Array(a, ..) => Ok(reify! { *a => !!! T }),
+ _ => Err(self),
+ };
+ }
+ #[cfg(not(feature = "no_index"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Blob>() {
+ return match self.0 {
+ Union::Blob(b, ..) => Ok(reify! { *b => !!! T }),
+ _ => Err(self),
+ };
+ }
+ #[cfg(not(feature = "no_object"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Map>() {
+ return match self.0 {
+ Union::Map(m, ..) => Ok(reify! { *m => !!! T }),
+ _ => Err(self),
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<FnPtr>() {
+ return match self.0 {
+ Union::FnPtr(f, ..) => Ok(reify! { *f => !!! T }),
+ _ => Err(self),
+ };
+ }
+ #[cfg(not(feature = "no_time"))]
+ if TypeId::of::<T>() == TypeId::of::<Instant>() {
+ return match self.0 {
+ Union::TimeStamp(t, ..) => Ok(reify! { *t => !!! T }),
+ _ => Err(self),
+ };
+ }
+
+ match self.0 {
+ Union::Variant(v, ..) if TypeId::of::<T>() == (**v).type_id() => {
+ Ok((*v).as_boxed_any().downcast().map(|x| *x).unwrap())
+ }
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(..) => unreachable!("Union::Shared case should be already handled"),
+ _ => Err(self),
+ }
+ }
+ /// Convert the [`Dynamic`] value into a specific type.
+ ///
+ /// Casting to a [`Dynamic`] just returns as is, but if it contains a shared value,
+ /// it is cloned into a [`Dynamic`] with a normal value.
+ ///
+ /// # Panics or Deadlocks
+ ///
+ /// Panics if the cast fails (e.g. the type of the actual value is not the same as the specified type).
+ ///
+ /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1).
+ /// Otherwise, this call panics if the data is currently borrowed for write.
+ ///
+ /// These normally shouldn't occur since most operations in Rhai is single-threaded.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Dynamic;
+ ///
+ /// let x = Dynamic::from(42_u32);
+ ///
+ /// assert_eq!(x.cast::<u32>(), 42);
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn cast<T: Any + Clone>(self) -> T {
+ // Bail out early if the return type needs no cast
+ if TypeId::of::<T>() == TypeId::of::<Self>() {
+ return reify! { self => !!! T };
+ }
+
+ #[cfg(not(feature = "no_closure"))]
+ let self_type_name = if self.is_shared() {
+ // Avoid panics/deadlocks with shared values
+ "<shared>"
+ } else {
+ self.type_name()
+ };
+ #[cfg(feature = "no_closure")]
+ let self_type_name = self.type_name();
+
+ self.try_cast::<T>()
+ .unwrap_or_else(|| panic!("cannot cast {} to {}", self_type_name, type_name::<T>()))
+ }
+ /// Clone the [`Dynamic`] value and convert it into a specific type.
+ ///
+ /// Casting to a [`Dynamic`] just returns as is, but if it contains a shared value,
+ /// it is cloned into a [`Dynamic`] with a normal value.
+ ///
+ /// Returns [`None`] if types mismatched.
+ ///
+ /// # Panics or Deadlocks
+ ///
+ /// Panics if the cast fails (e.g. the type of the actual value is not the
+ /// same as the specified type).
+ ///
+ /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1).
+ /// Otherwise, this call panics if the data is currently borrowed for write.
+ ///
+ /// These normally shouldn't occur since most operations in Rhai is single-threaded.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Dynamic;
+ ///
+ /// let x = Dynamic::from(42_u32);
+ /// let y = &x;
+ ///
+ /// assert_eq!(y.clone_cast::<u32>(), 42);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn clone_cast<T: Any + Clone>(&self) -> T {
+ self.flatten_clone().cast::<T>()
+ }
+ /// Flatten the [`Dynamic`] and clone it.
+ ///
+ /// If the [`Dynamic`] is not a shared value, it returns a cloned copy.
+ ///
+ /// If the [`Dynamic`] is a shared value, it returns a cloned copy of the shared value.
+ #[inline]
+ pub fn flatten_clone(&self) -> Self {
+ match self.0 {
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => crate::func::locked_read(cell).clone(),
+ _ => self.clone(),
+ }
+ }
+ /// Flatten the [`Dynamic`].
+ ///
+ /// If the [`Dynamic`] is not a shared value, it returns itself.
+ ///
+ /// If the [`Dynamic`] is a shared value, it returns the shared value if there are no
+ /// outstanding references, or a cloned copy.
+ #[inline]
+ pub fn flatten(self) -> Self {
+ match self.0 {
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(cell, ..) => crate::func::shared_try_take(cell).map_or_else(
+ |ref cell| crate::func::locked_read(cell).clone(),
+ #[cfg(not(feature = "sync"))]
+ |value| value.into_inner(),
+ #[cfg(feature = "sync")]
+ |value| value.into_inner().unwrap(),
+ ),
+ _ => self,
+ }
+ }
+ /// Flatten the [`Dynamic`] in place.
+ ///
+ /// If the [`Dynamic`] is not a shared value, it does nothing.
+ ///
+ /// If the [`Dynamic`] is a shared value, it is set to the shared value if there are no
+ /// outstanding references, or a cloned copy otherwise.
+ #[inline]
+ pub(crate) fn flatten_in_place(&mut self) -> &mut Self {
+ match self.0 {
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref mut cell, ..) => {
+ let cell = mem::take(cell);
+ *self = crate::func::shared_try_take(cell).map_or_else(
+ |ref cell| crate::func::locked_read(cell).clone(),
+ #[cfg(not(feature = "sync"))]
+ |value| value.into_inner(),
+ #[cfg(feature = "sync")]
+ |value| value.into_inner().unwrap(),
+ );
+ }
+ _ => (),
+ }
+ self
+ }
+ /// Is the [`Dynamic`] a shared value that is locked?
+ ///
+ /// Not available under `no_closure`.
+ ///
+ /// ## Note
+ ///
+ /// Under the `sync` feature, shared values use [`RwLock`][std::sync::RwLock] and they are never locked.
+ /// Access just waits until the [`RwLock`][std::sync::RwLock] is released.
+ /// So this method always returns [`false`] under [`Sync`].
+ #[cfg(not(feature = "no_closure"))]
+ #[inline]
+ #[must_use]
+ pub fn is_locked(&self) -> bool {
+ #[cfg(not(feature = "no_closure"))]
+ if let Union::Shared(ref _cell, ..) = self.0 {
+ #[cfg(not(feature = "sync"))]
+ return _cell.try_borrow().is_err();
+ #[cfg(feature = "sync")]
+ return false;
+ }
+
+ false
+ }
+ /// Get a reference of a specific type to the [`Dynamic`].
+ /// Casting to [`Dynamic`] just returns a reference to it.
+ ///
+ /// Returns [`None`] if the cast fails.
+ ///
+ /// # Panics or Deadlocks When Value is Shared
+ ///
+ /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1).
+ /// Otherwise, this call panics if the data is currently borrowed for write.
+ #[inline]
+ pub fn read_lock<T: Any + Clone>(&self) -> Option<DynamicReadLock<T>> {
+ match self.0 {
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ let value = crate::func::locked_read(cell);
+
+ return if (*value).type_id() != TypeId::of::<T>()
+ && TypeId::of::<Self>() != TypeId::of::<T>()
+ {
+ None
+ } else {
+ Some(DynamicReadLock(DynamicReadLockInner::Guard(value)))
+ };
+ }
+ _ => (),
+ }
+
+ self.downcast_ref()
+ .map(DynamicReadLockInner::Reference)
+ .map(DynamicReadLock)
+ }
+ /// Get a mutable reference of a specific type to the [`Dynamic`].
+ /// Casting to [`Dynamic`] just returns a mutable reference to it.
+ ///
+ /// Returns [`None`] if the cast fails.
+ ///
+ /// # Panics or Deadlocks When Value is Shared
+ ///
+ /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1).
+ /// Otherwise, this call panics if the data is currently borrowed for write.
+ #[inline]
+ pub fn write_lock<T: Any + Clone>(&mut self) -> Option<DynamicWriteLock<T>> {
+ match self.0 {
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ let guard = crate::func::locked_write(cell);
+
+ return if (*guard).type_id() != TypeId::of::<T>()
+ && TypeId::of::<Self>() != TypeId::of::<T>()
+ {
+ None
+ } else {
+ Some(DynamicWriteLock(DynamicWriteLockInner::Guard(guard)))
+ };
+ }
+ _ => (),
+ }
+
+ self.downcast_mut()
+ .map(DynamicWriteLockInner::Reference)
+ .map(DynamicWriteLock)
+ }
+ /// Get a reference of a specific type to the [`Dynamic`].
+ /// Casting to [`Dynamic`] just returns a reference to it.
+ ///
+ /// Returns [`None`] if the cast fails, or if the value is shared.
+ #[inline]
+ #[must_use]
+ pub(crate) fn downcast_ref<T: Any + Clone + ?Sized>(&self) -> Option<&T> {
+ // Coded this way in order to maximally leverage potentials for dead-code removal.
+
+ if TypeId::of::<T>() == TypeId::of::<INT>() {
+ return match self.0 {
+ Union::Int(ref v, ..) => v.as_any().downcast_ref::<T>(),
+ _ => None,
+ };
+ }
+ #[cfg(not(feature = "no_float"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::FLOAT>() {
+ return match self.0 {
+ Union::Float(ref v, ..) => v.as_ref().as_any().downcast_ref::<T>(),
+ _ => None,
+ };
+ }
+ #[cfg(feature = "decimal")]
+ if TypeId::of::<T>() == TypeId::of::<rust_decimal::Decimal>() {
+ return match self.0 {
+ Union::Decimal(ref v, ..) => v.as_ref().as_any().downcast_ref::<T>(),
+ _ => None,
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<bool>() {
+ return match self.0 {
+ Union::Bool(ref v, ..) => v.as_any().downcast_ref::<T>(),
+ _ => None,
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<ImmutableString>() {
+ return match self.0 {
+ Union::Str(ref v, ..) => v.as_any().downcast_ref::<T>(),
+ _ => None,
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<char>() {
+ return match self.0 {
+ Union::Char(ref v, ..) => v.as_any().downcast_ref::<T>(),
+ _ => None,
+ };
+ }
+ #[cfg(not(feature = "no_index"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Array>() {
+ return match self.0 {
+ Union::Array(ref v, ..) => v.as_ref().as_any().downcast_ref::<T>(),
+ _ => None,
+ };
+ }
+ #[cfg(not(feature = "no_index"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Blob>() {
+ return match self.0 {
+ Union::Blob(ref v, ..) => v.as_ref().as_any().downcast_ref::<T>(),
+ _ => None,
+ };
+ }
+ #[cfg(not(feature = "no_object"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Map>() {
+ return match self.0 {
+ Union::Map(ref v, ..) => v.as_ref().as_any().downcast_ref::<T>(),
+ _ => None,
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<FnPtr>() {
+ return match self.0 {
+ Union::FnPtr(ref v, ..) => v.as_ref().as_any().downcast_ref::<T>(),
+ _ => None,
+ };
+ }
+ #[cfg(not(feature = "no_time"))]
+ if TypeId::of::<T>() == TypeId::of::<Instant>() {
+ return match self.0 {
+ Union::TimeStamp(ref v, ..) => v.as_ref().as_any().downcast_ref::<T>(),
+ _ => None,
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<()>() {
+ return match self.0 {
+ Union::Unit(ref v, ..) => v.as_any().downcast_ref::<T>(),
+ _ => None,
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<Self>() {
+ return self.as_any().downcast_ref::<T>();
+ }
+
+ match self.0 {
+ Union::Variant(ref v, ..) => (***v).as_any().downcast_ref::<T>(),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(..) => None,
+ _ => None,
+ }
+ }
+ /// Get a mutable reference of a specific type to the [`Dynamic`].
+ /// Casting to [`Dynamic`] just returns a mutable reference to it.
+ ///
+ /// Returns [`None`] if the cast fails, or if the value is shared.
+ #[inline]
+ #[must_use]
+ pub(crate) fn downcast_mut<T: Any + Clone>(&mut self) -> Option<&mut T> {
+ // Coded this way in order to maximally leverage potentials for dead-code removal.
+
+ if TypeId::of::<T>() == TypeId::of::<INT>() {
+ return match self.0 {
+ Union::Int(ref mut v, ..) => v.as_any_mut().downcast_mut::<T>(),
+ _ => None,
+ };
+ }
+ #[cfg(not(feature = "no_float"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::FLOAT>() {
+ return match self.0 {
+ Union::Float(ref mut v, ..) => v.as_mut().as_any_mut().downcast_mut::<T>(),
+ _ => None,
+ };
+ }
+ #[cfg(feature = "decimal")]
+ if TypeId::of::<T>() == TypeId::of::<rust_decimal::Decimal>() {
+ return match self.0 {
+ Union::Decimal(ref mut v, ..) => v.as_mut().as_any_mut().downcast_mut::<T>(),
+ _ => None,
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<bool>() {
+ return match self.0 {
+ Union::Bool(ref mut v, ..) => v.as_any_mut().downcast_mut::<T>(),
+ _ => None,
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<ImmutableString>() {
+ return match self.0 {
+ Union::Str(ref mut v, ..) => v.as_any_mut().downcast_mut::<T>(),
+ _ => None,
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<char>() {
+ return match self.0 {
+ Union::Char(ref mut v, ..) => v.as_any_mut().downcast_mut::<T>(),
+ _ => None,
+ };
+ }
+ #[cfg(not(feature = "no_index"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Array>() {
+ return match self.0 {
+ Union::Array(ref mut v, ..) => v.as_mut().as_any_mut().downcast_mut::<T>(),
+ _ => None,
+ };
+ }
+ #[cfg(not(feature = "no_index"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Blob>() {
+ return match self.0 {
+ Union::Blob(ref mut v, ..) => v.as_mut().as_any_mut().downcast_mut::<T>(),
+ _ => None,
+ };
+ }
+ #[cfg(not(feature = "no_object"))]
+ if TypeId::of::<T>() == TypeId::of::<crate::Map>() {
+ return match self.0 {
+ Union::Map(ref mut v, ..) => v.as_mut().as_any_mut().downcast_mut::<T>(),
+ _ => None,
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<FnPtr>() {
+ return match self.0 {
+ Union::FnPtr(ref mut v, ..) => v.as_mut().as_any_mut().downcast_mut::<T>(),
+ _ => None,
+ };
+ }
+ #[cfg(not(feature = "no_time"))]
+ if TypeId::of::<T>() == TypeId::of::<Instant>() {
+ return match self.0 {
+ Union::TimeStamp(ref mut v, ..) => v.as_mut().as_any_mut().downcast_mut::<T>(),
+ _ => None,
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<()>() {
+ return match self.0 {
+ Union::Unit(ref mut v, ..) => v.as_any_mut().downcast_mut::<T>(),
+ _ => None,
+ };
+ }
+ if TypeId::of::<T>() == TypeId::of::<Self>() {
+ return self.as_any_mut().downcast_mut::<T>();
+ }
+
+ match self.0 {
+ Union::Variant(ref mut v, ..) => (***v).as_any_mut().downcast_mut::<T>(),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(..) => None,
+ _ => None,
+ }
+ }
+
+ /// Return `true` if the [`Dynamic`] holds a `()`.
+ #[inline]
+ #[must_use]
+ pub fn is_unit(&self) -> bool {
+ match self.0 {
+ Union::Unit(..) => true,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ matches!(crate::func::locked_read(cell).0, Union::Unit(..))
+ }
+ _ => false,
+ }
+ }
+ /// Return `true` if the [`Dynamic`] holds the system integer type [`INT`].
+ #[inline]
+ #[must_use]
+ pub fn is_int(&self) -> bool {
+ match self.0 {
+ Union::Int(..) => true,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ matches!(crate::func::locked_read(cell).0, Union::Int(..))
+ }
+ _ => false,
+ }
+ }
+ /// Return `true` if the [`Dynamic`] holds the system floating-point type [`FLOAT`][crate::FLOAT].
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ #[inline]
+ #[must_use]
+ pub fn is_float(&self) -> bool {
+ match self.0 {
+ Union::Float(..) => true,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ matches!(crate::func::locked_read(cell).0, Union::Float(..))
+ }
+ _ => false,
+ }
+ }
+ /// _(decimal)_ Return `true` if the [`Dynamic`] holds a [`Decimal`][rust_decimal::Decimal].
+ ///
+ /// Exported under the `decimal` feature only.
+ #[cfg(feature = "decimal")]
+ #[inline]
+ #[must_use]
+ pub fn is_decimal(&self) -> bool {
+ match self.0 {
+ Union::Decimal(..) => true,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ matches!(crate::func::locked_read(cell).0, Union::Decimal(..))
+ }
+ _ => false,
+ }
+ }
+ /// Return `true` if the [`Dynamic`] holds a [`bool`].
+ #[inline]
+ #[must_use]
+ pub fn is_bool(&self) -> bool {
+ match self.0 {
+ Union::Bool(..) => true,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ matches!(crate::func::locked_read(cell).0, Union::Bool(..))
+ }
+ _ => false,
+ }
+ }
+ /// Return `true` if the [`Dynamic`] holds a [`char`].
+ #[inline]
+ #[must_use]
+ pub fn is_char(&self) -> bool {
+ match self.0 {
+ Union::Char(..) => true,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ matches!(crate::func::locked_read(cell).0, Union::Char(..))
+ }
+ _ => false,
+ }
+ }
+ /// Return `true` if the [`Dynamic`] holds an [`ImmutableString`].
+ #[inline]
+ #[must_use]
+ pub fn is_string(&self) -> bool {
+ match self.0 {
+ Union::Str(..) => true,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ matches!(crate::func::locked_read(cell).0, Union::Str(..))
+ }
+ _ => false,
+ }
+ }
+ /// Return `true` if the [`Dynamic`] holds an [`Array`][crate::Array].
+ ///
+ /// Not available under `no_index`.
+ #[cfg(not(feature = "no_index"))]
+ #[inline]
+ #[must_use]
+ pub fn is_array(&self) -> bool {
+ match self.0 {
+ Union::Array(..) => true,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ matches!(crate::func::locked_read(cell).0, Union::Array(..))
+ }
+ _ => false,
+ }
+ }
+ /// Return `true` if the [`Dynamic`] holds a [`Blob`][crate::Blob].
+ ///
+ /// Not available under `no_index`.
+ #[cfg(not(feature = "no_index"))]
+ #[inline]
+ #[must_use]
+ pub fn is_blob(&self) -> bool {
+ match self.0 {
+ Union::Blob(..) => true,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ matches!(crate::func::locked_read(cell).0, Union::Blob(..))
+ }
+ _ => false,
+ }
+ }
+ /// Return `true` if the [`Dynamic`] holds a [`Map`][crate::Map].
+ ///
+ /// Not available under `no_object`.
+ #[cfg(not(feature = "no_object"))]
+ #[inline]
+ #[must_use]
+ pub fn is_map(&self) -> bool {
+ match self.0 {
+ Union::Map(..) => true,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ matches!(crate::func::locked_read(cell).0, Union::Map(..))
+ }
+ _ => false,
+ }
+ }
+ /// Return `true` if the [`Dynamic`] holds a [`FnPtr`].
+ #[inline]
+ #[must_use]
+ pub(crate) fn is_fnptr(&self) -> bool {
+ match self.0 {
+ Union::FnPtr(..) => true,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ matches!(crate::func::locked_read(cell).0, Union::FnPtr(..))
+ }
+ _ => false,
+ }
+ }
+ /// Return `true` if the [`Dynamic`] holds a [timestamp][Instant].
+ ///
+ /// Not available under `no_time`.
+ #[cfg(not(feature = "no_time"))]
+ #[inline]
+ #[must_use]
+ pub fn is_timestamp(&self) -> bool {
+ match self.0 {
+ Union::TimeStamp(..) => true,
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ matches!(crate::func::locked_read(cell).0, Union::TimeStamp(..))
+ }
+ _ => false,
+ }
+ }
+
+ /// Cast the [`Dynamic`] as a unit `()`.
+ /// Returns the name of the actual type if the cast fails.
+ #[inline]
+ pub fn as_unit(&self) -> Result<(), &'static str> {
+ match self.0 {
+ Union::Unit(..) => Ok(()),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => match crate::func::locked_read(cell).0 {
+ Union::Unit(..) => Ok(()),
+ _ => Err(cell.type_name()),
+ },
+ _ => Err(self.type_name()),
+ }
+ }
+ /// Cast the [`Dynamic`] as the system integer type [`INT`].
+ /// Returns the name of the actual type if the cast fails.
+ #[inline]
+ pub fn as_int(&self) -> Result<INT, &'static str> {
+ match self.0 {
+ Union::Int(n, ..) => Ok(n),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => match crate::func::locked_read(cell).0 {
+ Union::Int(n, ..) => Ok(n),
+ _ => Err(cell.type_name()),
+ },
+ _ => Err(self.type_name()),
+ }
+ }
+ /// Cast the [`Dynamic`] as the system floating-point type [`FLOAT`][crate::FLOAT].
+ /// Returns the name of the actual type if the cast fails.
+ ///
+ /// Not available under `no_float`.
+ #[cfg(not(feature = "no_float"))]
+ #[inline]
+ pub fn as_float(&self) -> Result<crate::FLOAT, &'static str> {
+ match self.0 {
+ Union::Float(n, ..) => Ok(*n),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => match crate::func::locked_read(cell).0 {
+ Union::Float(n, ..) => Ok(*n),
+ _ => Err(cell.type_name()),
+ },
+ _ => Err(self.type_name()),
+ }
+ }
+ /// _(decimal)_ Cast the [`Dynamic`] as a [`Decimal`][rust_decimal::Decimal].
+ /// Returns the name of the actual type if the cast fails.
+ ///
+ /// Exported under the `decimal` feature only.
+ #[cfg(feature = "decimal")]
+ #[inline]
+ pub fn as_decimal(&self) -> Result<rust_decimal::Decimal, &'static str> {
+ match self.0 {
+ Union::Decimal(ref n, ..) => Ok(**n),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => match crate::func::locked_read(cell).0 {
+ Union::Decimal(ref n, ..) => Ok(**n),
+ _ => Err(cell.type_name()),
+ },
+ _ => Err(self.type_name()),
+ }
+ }
+ /// Cast the [`Dynamic`] as a [`bool`].
+ /// Returns the name of the actual type if the cast fails.
+ #[inline]
+ pub fn as_bool(&self) -> Result<bool, &'static str> {
+ match self.0 {
+ Union::Bool(b, ..) => Ok(b),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => match crate::func::locked_read(cell).0 {
+ Union::Bool(b, ..) => Ok(b),
+ _ => Err(cell.type_name()),
+ },
+ _ => Err(self.type_name()),
+ }
+ }
+ /// Cast the [`Dynamic`] as a [`char`].
+ /// Returns the name of the actual type if the cast fails.
+ #[inline]
+ pub fn as_char(&self) -> Result<char, &'static str> {
+ match self.0 {
+ Union::Char(c, ..) => Ok(c),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => match crate::func::locked_read(cell).0 {
+ Union::Char(c, ..) => Ok(c),
+ _ => Err(cell.type_name()),
+ },
+ _ => Err(self.type_name()),
+ }
+ }
+
+ /// Cast the [`Dynamic`] as a string slice.
+ /// Returns the name of the actual type if the cast fails.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the value is shared.
+ #[inline]
+ pub(crate) fn as_str_ref(&self) -> Result<&str, &'static str> {
+ match self.0 {
+ Union::Str(ref s, ..) => Ok(s),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(..) => panic!("as_str_ref() cannot be called on shared values"),
+ _ => Err(self.type_name()),
+ }
+ }
+ /// Convert the [`Dynamic`] into a [`String`].
+ /// If there are other references to the same string, a cloned copy is returned.
+ /// Returns the name of the actual type if the cast fails.
+ #[inline]
+ pub fn into_string(self) -> Result<String, &'static str> {
+ self.into_immutable_string()
+ .map(ImmutableString::into_owned)
+ }
+ /// Convert the [`Dynamic`] into an [`ImmutableString`].
+ /// Returns the name of the actual type if the cast fails.
+ #[inline]
+ pub fn into_immutable_string(self) -> Result<ImmutableString, &'static str> {
+ match self.0 {
+ Union::Str(s, ..) => Ok(s),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => match crate::func::locked_read(cell).0 {
+ Union::Str(ref s, ..) => Ok(s.clone()),
+ _ => Err(cell.type_name()),
+ },
+ _ => Err(self.type_name()),
+ }
+ }
+ /// Convert the [`Dynamic`] into an [`Array`][crate::Array].
+ /// Returns the name of the actual type if the cast fails.
+ ///
+ /// Not available under `no_index`.
+ #[cfg(not(feature = "no_index"))]
+ #[inline(always)]
+ pub fn into_array(self) -> Result<crate::Array, &'static str> {
+ match self.0 {
+ Union::Array(a, ..) => Ok(*a),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => match crate::func::locked_read(cell).0 {
+ Union::Array(ref a, ..) => Ok(a.as_ref().clone()),
+ _ => Err(cell.type_name()),
+ },
+ _ => Err(self.type_name()),
+ }
+ }
+ /// Convert the [`Dynamic`] into a [`Vec`].
+ /// Returns the name of the actual type if any cast fails.
+ ///
+ /// Not available under `no_index`.
+ #[cfg(not(feature = "no_index"))]
+ #[inline(always)]
+ pub fn into_typed_array<T: Variant + Clone>(self) -> Result<Vec<T>, &'static str> {
+ match self.0 {
+ Union::Array(a, ..) => a
+ .into_iter()
+ .map(|v| {
+ #[cfg(not(feature = "no_closure"))]
+ let typ = if v.is_shared() {
+ // Avoid panics/deadlocks with shared values
+ "<shared>"
+ } else {
+ v.type_name()
+ };
+ #[cfg(feature = "no_closure")]
+ let typ = v.type_name();
+
+ v.try_cast::<T>().ok_or(typ)
+ })
+ .collect(),
+ Union::Blob(b, ..) if TypeId::of::<T>() == TypeId::of::<u8>() => {
+ Ok(reify! { *b => !!! Vec<T> })
+ }
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => {
+ match crate::func::locked_read(cell).0 {
+ Union::Array(ref a, ..) => {
+ a.iter()
+ .map(|v| {
+ #[cfg(not(feature = "no_closure"))]
+ let typ = if v.is_shared() {
+ // Avoid panics/deadlocks with shared values
+ "<shared>"
+ } else {
+ v.type_name()
+ };
+ #[cfg(feature = "no_closure")]
+ let typ = v.type_name();
+
+ v.read_lock::<T>().ok_or(typ).map(|v| v.clone())
+ })
+ .collect()
+ }
+ Union::Blob(ref b, ..) if TypeId::of::<T>() == TypeId::of::<u8>() => {
+ Ok(reify! { b.clone() => !!! Vec<T> })
+ }
+ _ => Err(cell.type_name()),
+ }
+ }
+ _ => Err(self.type_name()),
+ }
+ }
+ /// Convert the [`Dynamic`] into a [`Blob`][crate::Blob].
+ /// Returns the name of the actual type if the cast fails.
+ ///
+ /// Not available under `no_index`.
+ #[cfg(not(feature = "no_index"))]
+ #[inline(always)]
+ pub fn into_blob(self) -> Result<crate::Blob, &'static str> {
+ match self.0 {
+ Union::Blob(b, ..) => Ok(*b),
+ #[cfg(not(feature = "no_closure"))]
+ Union::Shared(ref cell, ..) => match crate::func::locked_read(cell).0 {
+ Union::Blob(ref b, ..) => Ok(b.as_ref().clone()),
+ _ => Err(cell.type_name()),
+ },
+ _ => Err(self.type_name()),
+ }
+ }
+
+ /// Recursively scan for [`Dynamic`] values within this [`Dynamic`] (e.g. items in an array or map),
+ /// calling a filter function on each.
+ ///
+ /// Shared values are _NOT_ scanned.
+ #[inline]
+ #[allow(clippy::only_used_in_recursion)]
+ pub fn deep_scan(&mut self, mut filter: impl FnMut(&mut Self)) {
+ fn scan_inner(value: &mut Dynamic, filter: &mut impl FnMut(&mut Dynamic)) {
+ match &mut value.0 {
+ #[cfg(not(feature = "no_index"))]
+ Union::Array(a, ..) => a.iter_mut().for_each(|v| scan_inner(v, filter)),
+ #[cfg(not(feature = "no_object"))]
+ Union::Map(m, ..) => m.values_mut().for_each(|v| scan_inner(v, filter)),
+ Union::FnPtr(f, ..) => f.iter_curry_mut().for_each(|v| scan_inner(v, filter)),
+ _ => (),
+ }
+ }
+
+ filter(self);
+ scan_inner(self, &mut filter);
+ }
+}
+
+impl From<()> for Dynamic {
+ #[inline(always)]
+ fn from(value: ()) -> Self {
+ Self(Union::Unit(value, DEFAULT_TAG_VALUE, ReadWrite))
+ }
+}
+impl From<bool> for Dynamic {
+ #[inline(always)]
+ fn from(value: bool) -> Self {
+ Self(Union::Bool(value, DEFAULT_TAG_VALUE, ReadWrite))
+ }
+}
+impl From<INT> for Dynamic {
+ #[inline(always)]
+ fn from(value: INT) -> Self {
+ Self(Union::Int(value, DEFAULT_TAG_VALUE, ReadWrite))
+ }
+}
+#[cfg(not(feature = "no_float"))]
+impl From<crate::FLOAT> for Dynamic {
+ #[inline(always)]
+ fn from(value: crate::FLOAT) -> Self {
+ Self(Union::Float(value.into(), DEFAULT_TAG_VALUE, ReadWrite))
+ }
+}
+#[cfg(not(feature = "no_float"))]
+impl From<super::FloatWrapper<crate::FLOAT>> for Dynamic {
+ #[inline(always)]
+ fn from(value: super::FloatWrapper<crate::FLOAT>) -> Self {
+ Self(Union::Float(value, DEFAULT_TAG_VALUE, ReadWrite))
+ }
+}
+#[cfg(feature = "decimal")]
+impl From<rust_decimal::Decimal> for Dynamic {
+ #[inline(always)]
+ fn from(value: rust_decimal::Decimal) -> Self {
+ Self(Union::Decimal(value.into(), DEFAULT_TAG_VALUE, ReadWrite))
+ }
+}
+impl From<char> for Dynamic {
+ #[inline(always)]
+ fn from(value: char) -> Self {
+ Self(Union::Char(value, DEFAULT_TAG_VALUE, ReadWrite))
+ }
+}
+impl<S: Into<ImmutableString>> From<S> for Dynamic {
+ #[inline(always)]
+ fn from(value: S) -> Self {
+ Self(Union::Str(value.into(), DEFAULT_TAG_VALUE, ReadWrite))
+ }
+}
+impl From<&ImmutableString> for Dynamic {
+ #[inline(always)]
+ fn from(value: &ImmutableString) -> Self {
+ value.clone().into()
+ }
+}
+impl FromStr for Dynamic {
+ type Err = ();
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ Ok(Self(Union::Str(value.into(), DEFAULT_TAG_VALUE, ReadWrite)))
+ }
+}
+#[cfg(not(feature = "no_index"))]
+impl<T: Variant + Clone> From<Vec<T>> for Dynamic {
+ #[inline]
+ fn from(value: Vec<T>) -> Self {
+ Self(Union::Array(
+ Box::new(value.into_iter().map(Self::from).collect()),
+ DEFAULT_TAG_VALUE,
+ ReadWrite,
+ ))
+ }
+}
+#[cfg(not(feature = "no_index"))]
+impl<T: Variant + Clone> From<&[T]> for Dynamic {
+ #[inline]
+ fn from(value: &[T]) -> Self {
+ Self(Union::Array(
+ Box::new(value.iter().cloned().map(Self::from).collect()),
+ DEFAULT_TAG_VALUE,
+ ReadWrite,
+ ))
+ }
+}
+#[cfg(not(feature = "no_index"))]
+impl<T: Variant + Clone> std::iter::FromIterator<T> for Dynamic {
+ #[inline]
+ fn from_iter<X: IntoIterator<Item = T>>(iter: X) -> Self {
+ Self(Union::Array(
+ Box::new(iter.into_iter().map(Self::from).collect()),
+ DEFAULT_TAG_VALUE,
+ ReadWrite,
+ ))
+ }
+}
+#[cfg(not(feature = "no_object"))]
+#[cfg(not(feature = "no_std"))]
+impl<K: Into<crate::Identifier>, T: Variant + Clone> From<std::collections::HashMap<K, T>>
+ for Dynamic
+{
+ #[inline]
+ fn from(value: std::collections::HashMap<K, T>) -> Self {
+ Self(Union::Map(
+ Box::new(
+ value
+ .into_iter()
+ .map(|(k, v)| (k.into(), Self::from(v)))
+ .collect(),
+ ),
+ DEFAULT_TAG_VALUE,
+ ReadWrite,
+ ))
+ }
+}
+#[cfg(not(feature = "no_object"))]
+#[cfg(not(feature = "no_std"))]
+impl<K: Into<crate::Identifier>> From<std::collections::HashSet<K>> for Dynamic {
+ #[inline]
+ fn from(value: std::collections::HashSet<K>) -> Self {
+ Self(Union::Map(
+ Box::new(value.into_iter().map(|k| (k.into(), Self::UNIT)).collect()),
+ DEFAULT_TAG_VALUE,
+ ReadWrite,
+ ))
+ }
+}
+#[cfg(not(feature = "no_object"))]
+impl<K: Into<crate::Identifier>, T: Variant + Clone> From<std::collections::BTreeMap<K, T>>
+ for Dynamic
+{
+ #[inline]
+ fn from(value: std::collections::BTreeMap<K, T>) -> Self {
+ Self(Union::Map(
+ Box::new(
+ value
+ .into_iter()
+ .map(|(k, v)| (k.into(), Self::from(v)))
+ .collect(),
+ ),
+ DEFAULT_TAG_VALUE,
+ ReadWrite,
+ ))
+ }
+}
+#[cfg(not(feature = "no_object"))]
+impl<K: Into<crate::Identifier>> From<std::collections::BTreeSet<K>> for Dynamic {
+ #[inline]
+ fn from(value: std::collections::BTreeSet<K>) -> Self {
+ Self(Union::Map(
+ Box::new(value.into_iter().map(|k| (k.into(), Self::UNIT)).collect()),
+ DEFAULT_TAG_VALUE,
+ ReadWrite,
+ ))
+ }
+}
+impl From<FnPtr> for Dynamic {
+ #[inline(always)]
+ fn from(value: FnPtr) -> Self {
+ Self(Union::FnPtr(value.into(), DEFAULT_TAG_VALUE, ReadWrite))
+ }
+}
+#[cfg(not(feature = "no_time"))]
+impl From<Instant> for Dynamic {
+ #[inline(always)]
+ fn from(value: Instant) -> Self {
+ Self(Union::TimeStamp(value.into(), DEFAULT_TAG_VALUE, ReadWrite))
+ }
+}
+#[cfg(not(feature = "no_closure"))]
+impl From<crate::Shared<crate::Locked<Self>>> for Dynamic {
+ #[inline(always)]
+ fn from(value: crate::Shared<crate::Locked<Self>>) -> Self {
+ Self(Union::Shared(value, DEFAULT_TAG_VALUE, ReadWrite))
+ }
+}
+
+impl From<ExclusiveRange> for Dynamic {
+ #[inline(always)]
+ fn from(value: ExclusiveRange) -> Self {
+ Self::from(value)
+ }
+}
+impl From<InclusiveRange> for Dynamic {
+ #[inline(always)]
+ fn from(value: InclusiveRange) -> Self {
+ Self::from(value)
+ }
+}
diff --git a/rhai/src/types/error.rs b/rhai/src/types/error.rs
new file mode 100644
index 0000000..c60edce
--- /dev/null
+++ b/rhai/src/types/error.rs
@@ -0,0 +1,568 @@
+//! Module containing error definitions for the evaluation process.
+
+use crate::{Dynamic, ImmutableString, ParseErrorType, Position, INT};
+#[cfg(feature = "no_std")]
+use core_error::Error;
+#[cfg(not(feature = "no_std"))]
+use std::error::Error;
+use std::fmt;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// Evaluation result.
+///
+/// All wrapped [`Position`] values represent the location in the script where the error occurs.
+///
+/// Some errors never appear when certain features are turned on.
+/// They still exist so that the application can turn features on and off without going through
+/// massive code changes to remove/add back enum variants in match statements.
+///
+/// # Thread Safety
+///
+/// Currently, [`EvalAltResult`] is neither [`Send`] nor [`Sync`].
+/// Turn on the `sync` feature to make it [`Send`] `+` [`Sync`].
+#[derive(Debug)]
+#[non_exhaustive]
+#[must_use]
+pub enum EvalAltResult {
+ /// System error. Wrapped values are the error message and the internal error.
+ #[cfg(not(feature = "sync"))]
+ ErrorSystem(String, Box<dyn Error>),
+ /// System error. Wrapped values are the error message and the internal error.
+ #[cfg(feature = "sync")]
+ ErrorSystem(String, Box<dyn Error + Send + Sync>),
+
+ /// Syntax error.
+ ErrorParsing(ParseErrorType, Position),
+
+ /// Shadowing of an existing variable disallowed. Wrapped value is the variable name.
+ ErrorVariableExists(String, Position),
+ /// Forbidden variable name. Wrapped value is the variable name.
+ ErrorForbiddenVariable(String, Position),
+ /// Access of an unknown variable. Wrapped value is the variable name.
+ ErrorVariableNotFound(String, Position),
+ /// Access of an unknown object map property. Wrapped value is the property name.
+ ErrorPropertyNotFound(String, Position),
+ /// Access of an invalid index. Wrapped value is the index name.
+ ErrorIndexNotFound(Dynamic, Position),
+ /// Call to an unknown function. Wrapped value is the function signature.
+ ErrorFunctionNotFound(String, Position),
+ /// Usage of an unknown [module][crate::Module]. Wrapped value is the [module][crate::Module] name.
+ ErrorModuleNotFound(String, Position),
+
+ /// An error has occurred inside a called function.
+ /// Wrapped values are the function name, function source, and the interior error.
+ ErrorInFunctionCall(String, String, Box<Self>, Position),
+ /// An error has occurred while loading a [module][crate::Module].
+ /// Wrapped value are the [module][crate::Module] name and the interior error.
+ ErrorInModule(String, Box<Self>, Position),
+
+ /// Access to `this` that is not bound.
+ ErrorUnboundThis(Position),
+
+ /// Data is not of the required type.
+ /// Wrapped values are the type requested and type of the actual result.
+ ErrorMismatchDataType(String, String, Position),
+ /// Returned type is not the same as the required output type.
+ /// Wrapped values are the type requested and type of the actual result.
+ ErrorMismatchOutputType(String, String, Position),
+ /// Trying to index into a type that has no indexer function defined. Wrapped value is the type name.
+ ErrorIndexingType(String, Position),
+
+ /// Array access out-of-bounds.
+ /// Wrapped values are the current number of elements in the array and the index number.
+ ErrorArrayBounds(usize, INT, Position),
+ /// String indexing out-of-bounds.
+ /// Wrapped values are the current number of characters in the string and the index number.
+ ErrorStringBounds(usize, INT, Position),
+ /// Bit-field indexing out-of-bounds.
+ /// Wrapped values are the current number of bits in the bit-field and the index number.
+ ErrorBitFieldBounds(usize, INT, Position),
+
+ /// The `for` statement encounters a type that is not iterable.
+ ErrorFor(Position),
+
+ /// Data race detected when accessing a variable. Wrapped value is the variable name.
+ ErrorDataRace(String, Position),
+ /// Calling a non-pure method on a constant. Wrapped value is the function name.
+ ErrorNonPureMethodCallOnConstant(String, Position),
+ /// Assignment to a constant variable. Wrapped value is the variable name.
+ ErrorAssignmentToConstant(String, Position),
+ /// Inappropriate property access. Wrapped value is the property name.
+ ErrorDotExpr(String, Position),
+ /// Arithmetic error encountered. Wrapped value is the error message.
+ ErrorArithmetic(String, Position),
+
+ /// Number of operations over maximum limit.
+ ErrorTooManyOperations(Position),
+ /// [Modules][crate::Module] over maximum limit.
+ ErrorTooManyModules(Position),
+ /// Call stack over maximum limit.
+ ErrorStackOverflow(Position),
+ /// Data value over maximum size limit. Wrapped value is the type name.
+ ErrorDataTooLarge(String, Position),
+ /// The script is prematurely terminated. Wrapped value is the termination token.
+ ErrorTerminated(Dynamic, Position),
+
+ /// Error encountered for a custom syntax. Wrapped values are the error message and
+ /// custom syntax symbols stream.
+ ///
+ /// Normally this should never happen, unless an [`AST`][crate::AST] is compiled on one
+ /// [`Engine`][crate::Engine] but evaluated on another unrelated [`Engine`][crate::Engine].
+ ErrorCustomSyntax(String, Vec<String>, Position),
+
+ /// Run-time error encountered. Wrapped value is the error token.
+ ErrorRuntime(Dynamic, Position),
+
+ /// Breaking out of loops - not an error if within a loop.
+ /// The wrapped value, if true, means breaking clean out of the loop (i.e. a `break` statement).
+ /// The wrapped value, if false, means breaking the current context (i.e. a `continue` statement).
+ LoopBreak(bool, Dynamic, Position),
+ /// Not an error: Value returned from a script via the `return` keyword.
+ /// Wrapped value is the result value.
+ Return(Dynamic, Position),
+}
+
+impl Error for EvalAltResult {}
+
+impl fmt::Display for EvalAltResult {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::ErrorSystem(s, err) if s.is_empty() => write!(f, "{err}")?,
+ Self::ErrorSystem(s, err) => write!(f, "{s}: {err}")?,
+
+ Self::ErrorParsing(p, ..) => write!(f, "Syntax error: {p}")?,
+
+ #[cfg(not(feature = "no_function"))]
+ Self::ErrorInFunctionCall(s, src, err, ..) if crate::parser::is_anonymous_fn(s) => {
+ write!(f, "{err}\nin closure call")?;
+ if !src.is_empty() {
+ write!(f, " @ '{src}'")?;
+ }
+ }
+ Self::ErrorInFunctionCall(s, src, err, ..) => {
+ write!(f, "{err}\nin call to function '{s}'")?;
+ if !src.is_empty() {
+ write!(f, " @ '{src}'")?;
+ }
+ }
+
+ Self::ErrorInModule(s, err, ..) if s.is_empty() => write!(f, "{err}\nin module")?,
+ Self::ErrorInModule(s, err, ..) => write!(f, "{err}\nin module '{s}'")?,
+
+ Self::ErrorVariableExists(s, ..) => write!(f, "Variable already defined: {s}")?,
+ Self::ErrorForbiddenVariable(s, ..) => write!(f, "Forbidden variable name: {s}")?,
+ Self::ErrorVariableNotFound(s, ..) => write!(f, "Variable not found: {s}")?,
+ Self::ErrorPropertyNotFound(s, ..) => write!(f, "Property not found: {s}")?,
+ Self::ErrorIndexNotFound(s, ..) => write!(f, "Invalid index: {s}")?,
+ Self::ErrorFunctionNotFound(s, ..) => write!(f, "Function not found: {s}")?,
+ Self::ErrorModuleNotFound(s, ..) => write!(f, "Module not found: {s}")?,
+ Self::ErrorDataRace(s, ..) => write!(f, "Data race detected on variable '{s}'")?,
+
+ Self::ErrorDotExpr(s, ..) if s.is_empty() => f.write_str("Malformed dot expression")?,
+ Self::ErrorDotExpr(s, ..) => f.write_str(s)?,
+
+ Self::ErrorIndexingType(s, ..) => write!(f, "Indexer unavailable: {s}")?,
+ Self::ErrorUnboundThis(..) => f.write_str("'this' not bound")?,
+ Self::ErrorFor(..) => f.write_str("For loop expects iterable type")?,
+ Self::ErrorTooManyOperations(..) => f.write_str("Too many operations")?,
+ Self::ErrorTooManyModules(..) => f.write_str("Too many modules imported")?,
+ Self::ErrorStackOverflow(..) => f.write_str("Stack overflow")?,
+ Self::ErrorTerminated(..) => f.write_str("Script terminated")?,
+
+ Self::ErrorRuntime(d, ..) if d.is_unit() => f.write_str("Runtime error")?,
+ Self::ErrorRuntime(d, ..)
+ if d.read_lock::<ImmutableString>()
+ .map_or(false, |v| v.is_empty()) =>
+ {
+ write!(f, "Runtime error")?
+ }
+ Self::ErrorRuntime(d, ..) => write!(f, "Runtime error: {d}")?,
+
+ #[cfg(not(feature = "no_object"))]
+ Self::ErrorNonPureMethodCallOnConstant(s, ..)
+ if s.starts_with(crate::engine::FN_GET) =>
+ {
+ let prop = &s[crate::engine::FN_GET.len()..];
+ write!(f, "Non-pure property {prop} cannot be accessed on constant")?
+ }
+ #[cfg(not(feature = "no_object"))]
+ Self::ErrorNonPureMethodCallOnConstant(s, ..)
+ if s.starts_with(crate::engine::FN_SET) =>
+ {
+ let prop = &s[crate::engine::FN_SET.len()..];
+ write!(f, "Cannot modify property '{prop}' of constant")?
+ }
+ #[cfg(not(feature = "no_index"))]
+ Self::ErrorNonPureMethodCallOnConstant(s, ..) if s == crate::engine::FN_IDX_GET => {
+ write!(f, "Non-pure indexer cannot be accessed on constant")?
+ }
+ #[cfg(not(feature = "no_index"))]
+ Self::ErrorNonPureMethodCallOnConstant(s, ..) if s == crate::engine::FN_IDX_SET => {
+ write!(f, "Cannot assign to indexer of constant")?
+ }
+ Self::ErrorNonPureMethodCallOnConstant(s, ..) => {
+ write!(f, "Non-pure method '{s}' cannot be called on constant")?
+ }
+
+ Self::ErrorAssignmentToConstant(s, ..) => write!(f, "Cannot modify constant {s}")?,
+ Self::ErrorMismatchOutputType(e, a, ..) => match (a.as_str(), e.as_str()) {
+ ("", e) => write!(f, "Output type incorrect, expecting {e}"),
+ (a, "") => write!(f, "Output type incorrect: {a}"),
+ (a, e) => write!(f, "Output type incorrect: {a} (expecting {e})"),
+ }?,
+ Self::ErrorMismatchDataType(e, a, ..) => match (a.as_str(), e.as_str()) {
+ ("", e) => write!(f, "Data type incorrect, expecting {e}"),
+ (a, "") => write!(f, "Data type incorrect: {a}"),
+ (a, e) => write!(f, "Data type incorrect: {a} (expecting {e})"),
+ }?,
+
+ Self::ErrorArithmetic(s, ..) if s.is_empty() => f.write_str("Arithmetic error")?,
+ Self::ErrorArithmetic(s, ..) => f.write_str(s)?,
+
+ Self::LoopBreak(true, ..) => f.write_str("'break' must be within a loop")?,
+ Self::LoopBreak(false, ..) => f.write_str("'continue' must be within a loop")?,
+
+ Self::Return(..) => f.write_str("NOT AN ERROR - function returns value")?,
+
+ Self::ErrorArrayBounds(max, index, ..) => match max {
+ 0 => write!(f, "Array index {index} out of bounds: array is empty"),
+ 1 => write!(
+ f,
+ "Array index {index} out of bounds: only 1 element in array",
+ ),
+ _ => write!(
+ f,
+ "Array index {index} out of bounds: only {max} elements in array",
+ ),
+ }?,
+ Self::ErrorStringBounds(max, index, ..) => match max {
+ 0 => write!(f, "String index {index} out of bounds: string is empty"),
+ 1 => write!(
+ f,
+ "String index {index} out of bounds: only 1 character in string",
+ ),
+ _ => write!(
+ f,
+ "String index {index} out of bounds: only {max} characters in string",
+ ),
+ }?,
+ Self::ErrorBitFieldBounds(max, index, ..) => write!(
+ f,
+ "Bit-field index {index} out of bounds: only {max} bits in bit-field",
+ )?,
+ Self::ErrorDataTooLarge(typ, ..) => write!(f, "{typ} too large")?,
+
+ Self::ErrorCustomSyntax(s, tokens, ..) => write!(f, "{s}: {}", tokens.join(" "))?,
+ }
+
+ // Do not write any position if None
+ if !self.position().is_none() {
+ write!(f, " ({})", self.position())?;
+ }
+
+ Ok(())
+ }
+}
+
+impl<T: AsRef<str>> From<T> for EvalAltResult {
+ #[cold]
+ #[inline(never)]
+ fn from(err: T) -> Self {
+ Self::ErrorRuntime(err.as_ref().to_string().into(), Position::NONE)
+ }
+}
+
+impl<T: AsRef<str>> From<T> for Box<EvalAltResult> {
+ #[cold]
+ #[inline(always)]
+ fn from(err: T) -> Self {
+ Into::<EvalAltResult>::into(err).into()
+ }
+}
+
+impl EvalAltResult {
+ /// Is this a pseudo error? A pseudo error is one that does not occur naturally.
+ ///
+ /// [`LoopBreak`][EvalAltResult::LoopBreak] and [`Return`][EvalAltResult::Return] are pseudo errors.
+ #[cold]
+ #[inline(never)]
+ #[must_use]
+ pub const fn is_pseudo_error(&self) -> bool {
+ matches!(self, Self::LoopBreak(..) | Self::Return(..))
+ }
+ /// Can this error be caught?
+ #[cold]
+ #[inline(never)]
+ #[must_use]
+ pub const fn is_catchable(&self) -> bool {
+ match self {
+ Self::ErrorSystem(..) => false,
+ Self::ErrorParsing(..) => false,
+
+ Self::ErrorFunctionNotFound(..)
+ | Self::ErrorInFunctionCall(..)
+ | Self::ErrorInModule(..)
+ | Self::ErrorUnboundThis(..)
+ | Self::ErrorMismatchDataType(..)
+ | Self::ErrorArrayBounds(..)
+ | Self::ErrorStringBounds(..)
+ | Self::ErrorBitFieldBounds(..)
+ | Self::ErrorIndexingType(..)
+ | Self::ErrorFor(..)
+ | Self::ErrorVariableExists(..)
+ | Self::ErrorForbiddenVariable(..)
+ | Self::ErrorVariableNotFound(..)
+ | Self::ErrorPropertyNotFound(..)
+ | Self::ErrorIndexNotFound(..)
+ | Self::ErrorModuleNotFound(..)
+ | Self::ErrorDataRace(..)
+ | Self::ErrorNonPureMethodCallOnConstant(..)
+ | Self::ErrorAssignmentToConstant(..)
+ | Self::ErrorMismatchOutputType(..)
+ | Self::ErrorDotExpr(..)
+ | Self::ErrorArithmetic(..)
+ | Self::ErrorRuntime(..) => true,
+
+ // Custom syntax raises errors only when they are compiled by one
+ // [`Engine`][crate::Engine] and run by another, causing a mismatch.
+ //
+ // Therefore, this error should not be catchable.
+ Self::ErrorCustomSyntax(..) => false,
+
+ Self::ErrorTooManyOperations(..)
+ | Self::ErrorTooManyModules(..)
+ | Self::ErrorStackOverflow(..)
+ | Self::ErrorDataTooLarge(..)
+ | Self::ErrorTerminated(..) => false,
+
+ Self::LoopBreak(..) | Self::Return(..) => false,
+ }
+ }
+ /// Is this error a system exception?
+ #[cold]
+ #[inline(never)]
+ #[must_use]
+ pub const fn is_system_exception(&self) -> bool {
+ matches!(
+ self,
+ Self::ErrorSystem(..)
+ | Self::ErrorParsing(..)
+ | Self::ErrorCustomSyntax(..)
+ | Self::ErrorTooManyOperations(..)
+ | Self::ErrorTooManyModules(..)
+ | Self::ErrorStackOverflow(..)
+ | Self::ErrorDataTooLarge(..)
+ | Self::ErrorTerminated(..)
+ )
+ }
+ /// Get the [position][Position] of this error.
+ #[cfg(not(feature = "no_object"))]
+ #[cold]
+ #[inline(never)]
+ pub(crate) fn dump_fields(&self, map: &mut crate::Map) {
+ map.insert(
+ "error".into(),
+ format!("{self:?}")
+ .split('(')
+ .next()
+ .expect("`ErrorXXX(...)`")
+ .into(),
+ );
+
+ match self {
+ Self::LoopBreak(..) | Self::Return(..) => (),
+
+ Self::ErrorSystem(..)
+ | Self::ErrorParsing(..)
+ | Self::ErrorUnboundThis(..)
+ | Self::ErrorFor(..)
+ | Self::ErrorArithmetic(..)
+ | Self::ErrorTooManyOperations(..)
+ | Self::ErrorTooManyModules(..)
+ | Self::ErrorStackOverflow(..)
+ | Self::ErrorRuntime(..) => (),
+
+ Self::ErrorFunctionNotFound(f, ..) | Self::ErrorNonPureMethodCallOnConstant(f, ..) => {
+ map.insert("function".into(), f.into());
+ }
+ Self::ErrorInFunctionCall(f, s, ..) => {
+ map.insert("function".into(), f.into());
+ map.insert("source".into(), s.into());
+ }
+ Self::ErrorMismatchDataType(r, a, ..) | Self::ErrorMismatchOutputType(r, a, ..) => {
+ map.insert("requested".into(), r.into());
+ map.insert("actual".into(), a.into());
+ }
+ Self::ErrorArrayBounds(n, i, ..)
+ | Self::ErrorStringBounds(n, i, ..)
+ | Self::ErrorBitFieldBounds(n, i, ..) => {
+ map.insert("length".into(), (*n as INT).into());
+ map.insert("index".into(), (*i as INT).into());
+ }
+ Self::ErrorVariableExists(v, ..)
+ | Self::ErrorForbiddenVariable(v, ..)
+ | Self::ErrorVariableNotFound(v, ..)
+ | Self::ErrorPropertyNotFound(v, ..)
+ | Self::ErrorDataRace(v, ..)
+ | Self::ErrorAssignmentToConstant(v, ..) => {
+ map.insert("variable".into(), v.into());
+ }
+ Self::ErrorIndexNotFound(v, ..) => {
+ map.insert("index".into(), v.clone());
+ }
+ Self::ErrorInModule(m, ..) | Self::ErrorModuleNotFound(m, ..) => {
+ map.insert("module".into(), m.into());
+ }
+ Self::ErrorDotExpr(p, ..) => {
+ map.insert("property".into(), p.into());
+ }
+
+ Self::ErrorIndexingType(t, ..) | Self::ErrorDataTooLarge(t, ..) => {
+ map.insert("type".into(), t.into());
+ }
+ Self::ErrorTerminated(t, ..) => {
+ map.insert("token".into(), t.clone());
+ }
+ Self::ErrorCustomSyntax(_, tokens, _) => {
+ map.insert(
+ "tokens".into(),
+ #[cfg(not(feature = "no_index"))]
+ Dynamic::from_array(tokens.iter().map(Into::into).collect()),
+ #[cfg(feature = "no_index")]
+ tokens
+ .iter()
+ .map(|s| s.as_str())
+ .collect::<Vec<_>>()
+ .join(" ")
+ .into(),
+ );
+ }
+ };
+ }
+ /// Unwrap this error and get the very base error.
+ #[cold]
+ #[inline(never)]
+ pub fn unwrap_inner(&self) -> &Self {
+ match self {
+ Self::ErrorInFunctionCall(.., err, _) | Self::ErrorInModule(.., err, _) => {
+ err.unwrap_inner()
+ }
+ _ => self,
+ }
+ }
+ /// Get the [position][Position] of this error.
+ #[cold]
+ #[inline(never)]
+ #[must_use]
+ pub const fn position(&self) -> Position {
+ match self {
+ Self::ErrorSystem(..) => Position::NONE,
+
+ Self::ErrorParsing(.., pos)
+ | Self::ErrorFunctionNotFound(.., pos)
+ | Self::ErrorInFunctionCall(.., pos)
+ | Self::ErrorInModule(.., pos)
+ | Self::ErrorUnboundThis(pos)
+ | Self::ErrorMismatchDataType(.., pos)
+ | Self::ErrorArrayBounds(.., pos)
+ | Self::ErrorStringBounds(.., pos)
+ | Self::ErrorBitFieldBounds(.., pos)
+ | Self::ErrorIndexingType(.., pos)
+ | Self::ErrorFor(pos)
+ | Self::ErrorVariableExists(.., pos)
+ | Self::ErrorForbiddenVariable(.., pos)
+ | Self::ErrorVariableNotFound(.., pos)
+ | Self::ErrorPropertyNotFound(.., pos)
+ | Self::ErrorIndexNotFound(.., pos)
+ | Self::ErrorModuleNotFound(.., pos)
+ | Self::ErrorDataRace(.., pos)
+ | Self::ErrorNonPureMethodCallOnConstant(.., pos)
+ | Self::ErrorAssignmentToConstant(.., pos)
+ | Self::ErrorMismatchOutputType(.., pos)
+ | Self::ErrorDotExpr(.., pos)
+ | Self::ErrorArithmetic(.., pos)
+ | Self::ErrorTooManyOperations(pos)
+ | Self::ErrorTooManyModules(pos)
+ | Self::ErrorStackOverflow(pos)
+ | Self::ErrorDataTooLarge(.., pos)
+ | Self::ErrorTerminated(.., pos)
+ | Self::ErrorCustomSyntax(.., pos)
+ | Self::ErrorRuntime(.., pos)
+ | Self::LoopBreak(.., pos)
+ | Self::Return(.., pos) => *pos,
+ }
+ }
+ /// Remove the [position][Position] information from this error.
+ ///
+ /// The [position][Position] of this error is set to [`NONE`][Position::NONE] afterwards.
+ #[cold]
+ #[inline(never)]
+ pub fn clear_position(&mut self) -> &mut Self {
+ self.set_position(Position::NONE)
+ }
+ /// Remove the [position][Position] information from this error and return it.
+ ///
+ /// The [position][Position] of this error is set to [`NONE`][Position::NONE] afterwards.
+ #[cold]
+ #[inline(never)]
+ #[must_use]
+ pub fn take_position(&mut self) -> Position {
+ let pos = self.position();
+ self.set_position(Position::NONE);
+ pos
+ }
+ /// Override the [position][Position] of this error.
+ #[cold]
+ #[inline(never)]
+ pub fn set_position(&mut self, new_position: Position) -> &mut Self {
+ match self {
+ Self::ErrorSystem(..) => (),
+
+ Self::ErrorParsing(.., pos)
+ | Self::ErrorFunctionNotFound(.., pos)
+ | Self::ErrorInFunctionCall(.., pos)
+ | Self::ErrorInModule(.., pos)
+ | Self::ErrorUnboundThis(pos)
+ | Self::ErrorMismatchDataType(.., pos)
+ | Self::ErrorArrayBounds(.., pos)
+ | Self::ErrorStringBounds(.., pos)
+ | Self::ErrorBitFieldBounds(.., pos)
+ | Self::ErrorIndexingType(.., pos)
+ | Self::ErrorFor(pos)
+ | Self::ErrorVariableExists(.., pos)
+ | Self::ErrorForbiddenVariable(.., pos)
+ | Self::ErrorVariableNotFound(.., pos)
+ | Self::ErrorPropertyNotFound(.., pos)
+ | Self::ErrorIndexNotFound(.., pos)
+ | Self::ErrorModuleNotFound(.., pos)
+ | Self::ErrorDataRace(.., pos)
+ | Self::ErrorNonPureMethodCallOnConstant(.., pos)
+ | Self::ErrorAssignmentToConstant(.., pos)
+ | Self::ErrorMismatchOutputType(.., pos)
+ | Self::ErrorDotExpr(.., pos)
+ | Self::ErrorArithmetic(.., pos)
+ | Self::ErrorTooManyOperations(pos)
+ | Self::ErrorTooManyModules(pos)
+ | Self::ErrorStackOverflow(pos)
+ | Self::ErrorDataTooLarge(.., pos)
+ | Self::ErrorTerminated(.., pos)
+ | Self::ErrorCustomSyntax(.., pos)
+ | Self::ErrorRuntime(.., pos)
+ | Self::LoopBreak(.., pos)
+ | Self::Return(.., pos) => *pos = new_position,
+ }
+ self
+ }
+ /// Consume the current [`EvalAltResult`] and return a new one with the specified [`Position`]
+ /// if the current position is [`Position::NONE`].
+ #[cold]
+ #[inline(never)]
+ #[must_use]
+ pub(crate) fn fill_position(mut self: Box<Self>, new_position: Position) -> Box<Self> {
+ if self.position().is_none() {
+ self.set_position(new_position);
+ }
+ self
+ }
+}
diff --git a/rhai/src/types/float.rs b/rhai/src/types/float.rs
new file mode 100644
index 0000000..91af3a8
--- /dev/null
+++ b/rhai/src/types/float.rs
@@ -0,0 +1,115 @@
+#![cfg(not(feature = "no_float"))]
+
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ fmt,
+ hash::{Hash, Hasher},
+ ops::{Deref, DerefMut},
+ str::FromStr,
+};
+
+use num_traits::float::FloatCore as Float;
+
+/// A type that wraps a floating-point number and implements [`Hash`].
+///
+/// Not available under `no_float`.
+#[derive(Clone, Copy, Eq, PartialEq, PartialOrd)]
+#[must_use]
+pub struct FloatWrapper<F>(F);
+
+impl Hash for FloatWrapper<crate::FLOAT> {
+ #[inline]
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.0.to_ne_bytes().hash(state);
+ }
+}
+
+impl<F: Float> AsRef<F> for FloatWrapper<F> {
+ #[inline(always)]
+ #[must_use]
+ fn as_ref(&self) -> &F {
+ &self.0
+ }
+}
+
+impl<F: Float> AsMut<F> for FloatWrapper<F> {
+ #[inline(always)]
+ #[must_use]
+ fn as_mut(&mut self) -> &mut F {
+ &mut self.0
+ }
+}
+
+impl<F: Float> Deref for FloatWrapper<F> {
+ type Target = F;
+
+ #[inline(always)]
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl<F: Float> DerefMut for FloatWrapper<F> {
+ #[inline(always)]
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl<F: Float + fmt::Debug> fmt::Debug for FloatWrapper<F> {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Debug::fmt(&self.0, f)
+ }
+}
+
+impl<F: Float + fmt::Display + fmt::LowerExp + From<f32>> fmt::Display for FloatWrapper<F> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let abs = self.0.abs();
+ if abs.is_zero() {
+ f.write_str("0.0")
+ } else if abs > Self::MAX_NATURAL_FLOAT_FOR_DISPLAY.into()
+ || abs < Self::MIN_NATURAL_FLOAT_FOR_DISPLAY.into()
+ {
+ write!(f, "{:e}", self.0)
+ } else {
+ fmt::Display::fmt(&self.0, f)?;
+ if abs.fract().is_zero() {
+ f.write_str(".0")?;
+ }
+ Ok(())
+ }
+ }
+}
+
+impl<F: Float> From<F> for FloatWrapper<F> {
+ #[inline(always)]
+ fn from(value: F) -> Self {
+ Self::new(value)
+ }
+}
+
+impl<F: Float + FromStr> FromStr for FloatWrapper<F> {
+ type Err = <F as FromStr>::Err;
+
+ #[inline]
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ F::from_str(s).map(Into::into)
+ }
+}
+
+impl<F: Float> FloatWrapper<F> {
+ /// Maximum floating-point number for natural display before switching to scientific notation.
+ pub const MAX_NATURAL_FLOAT_FOR_DISPLAY: f32 = 10_000_000_000_000.0;
+
+ /// Minimum floating-point number for natural display before switching to scientific notation.
+ pub const MIN_NATURAL_FLOAT_FOR_DISPLAY: f32 = 0.000_000_000_000_1;
+
+ /// Create a new [`FloatWrapper`].
+ #[inline(always)]
+ pub const fn new(value: F) -> Self {
+ Self(value)
+ }
+}
diff --git a/rhai/src/types/fn_ptr.rs b/rhai/src/types/fn_ptr.rs
new file mode 100644
index 0000000..ba13ab9
--- /dev/null
+++ b/rhai/src/types/fn_ptr.rs
@@ -0,0 +1,589 @@
+//! The `FnPtr` type.
+
+use crate::eval::GlobalRuntimeState;
+use crate::func::EncapsulatedEnviron;
+use crate::tokenizer::{is_reserved_keyword_or_symbol, is_valid_function_name, Token};
+use crate::types::dynamic::Variant;
+use crate::{
+ Dynamic, Engine, FnArgsVec, FuncArgs, ImmutableString, NativeCallContext, ParseErrorType,
+ Position, RhaiError, RhaiResult, RhaiResultOf, Shared, StaticVec, AST, ERR,
+};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ any::type_name,
+ convert::{TryFrom, TryInto},
+ fmt,
+ hash::{Hash, Hasher},
+ mem,
+ ops::{Index, IndexMut},
+};
+
+/// A general function pointer, which may carry additional (i.e. curried) argument values
+/// to be passed onto a function during a call.
+#[derive(Clone)]
+pub struct FnPtr {
+ name: ImmutableString,
+ curry: StaticVec<Dynamic>,
+ environ: Option<Shared<EncapsulatedEnviron>>,
+ #[cfg(not(feature = "no_function"))]
+ fn_def: Option<Shared<crate::ast::ScriptFnDef>>,
+}
+
+impl Hash for FnPtr {
+ #[inline(always)]
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.name.hash(state);
+ self.curry.hash(state);
+
+ // Hash the shared [`EncapsulatedEnviron`] by hashing its shared pointer.
+ self.environ.as_ref().map(Shared::as_ptr).hash(state);
+
+ // Hash the linked [`ScriptFnDef`][crate::ast::ScriptFnDef] by hashing its shared pointer.
+ #[cfg(not(feature = "no_function"))]
+ self.fn_def.as_ref().map(Shared::as_ptr).hash(state);
+ }
+}
+
+impl fmt::Debug for FnPtr {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let ff = &mut f.debug_tuple("Fn");
+ ff.field(&self.name);
+ self.curry.iter().for_each(|curry| {
+ ff.field(curry);
+ });
+ ff.finish()?;
+
+ #[cfg(not(feature = "no_function"))]
+ if let Some(ref fn_def) = self.fn_def {
+ write!(f, ": {fn_def}")?;
+ }
+
+ Ok(())
+ }
+}
+
+impl FnPtr {
+ /// Create a new function pointer.
+ #[inline(always)]
+ pub fn new(name: impl Into<ImmutableString>) -> RhaiResultOf<Self> {
+ name.into().try_into()
+ }
+ /// Create a new function pointer without checking its parameters.
+ #[inline(always)]
+ #[must_use]
+ #[allow(dead_code)]
+ pub(crate) fn new_unchecked(
+ name: impl Into<ImmutableString>,
+ curry: StaticVec<Dynamic>,
+ ) -> Self {
+ Self {
+ name: name.into(),
+ curry,
+ environ: None,
+ #[cfg(not(feature = "no_function"))]
+ fn_def: None,
+ }
+ }
+ /// Get the name of the function.
+ #[inline(always)]
+ #[must_use]
+ pub fn fn_name(&self) -> &str {
+ self.fn_name_raw().as_str()
+ }
+ /// Get the name of the function.
+ #[inline(always)]
+ #[must_use]
+ pub(crate) const fn fn_name_raw(&self) -> &ImmutableString {
+ &self.name
+ }
+ /// Get the underlying data of the function pointer.
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ #[must_use]
+ pub(crate) fn take_data(
+ self,
+ ) -> (
+ ImmutableString,
+ StaticVec<Dynamic>,
+ Option<Shared<EncapsulatedEnviron>>,
+ Option<Shared<crate::ast::ScriptFnDef>>,
+ ) {
+ (self.name, self.curry, self.environ, self.fn_def)
+ }
+ /// Get the underlying data of the function pointer.
+ #[cfg(feature = "no_function")]
+ #[inline(always)]
+ #[must_use]
+ pub(crate) fn take_data(
+ self,
+ ) -> (
+ ImmutableString,
+ StaticVec<Dynamic>,
+ Option<Shared<EncapsulatedEnviron>>,
+ ) {
+ (self.name, self.curry, self.environ)
+ }
+ /// Get the curried arguments.
+ #[inline(always)]
+ pub fn curry(&self) -> &[Dynamic] {
+ self.curry.as_ref()
+ }
+ /// Iterate the curried arguments.
+ #[inline(always)]
+ pub fn iter_curry(&self) -> impl Iterator<Item = &Dynamic> {
+ self.curry.iter()
+ }
+ /// Mutably-iterate the curried arguments.
+ #[inline(always)]
+ pub fn iter_curry_mut(&mut self) -> impl Iterator<Item = &mut Dynamic> {
+ self.curry.iter_mut()
+ }
+ /// Add a new curried argument.
+ #[inline(always)]
+ pub fn add_curry(&mut self, value: Dynamic) -> &mut Self {
+ self.curry.push(value);
+ self
+ }
+ /// Set curried arguments to the function pointer.
+ #[inline]
+ pub fn set_curry(&mut self, values: impl IntoIterator<Item = Dynamic>) -> &mut Self {
+ self.curry = values.into_iter().collect();
+ self
+ }
+ /// Is the function pointer curried?
+ #[inline(always)]
+ #[must_use]
+ pub fn is_curried(&self) -> bool {
+ !self.curry.is_empty()
+ }
+ /// Does the function pointer refer to an anonymous function?
+ ///
+ /// Not available under `no_function`.
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ #[must_use]
+ pub fn is_anonymous(&self) -> bool {
+ crate::func::is_anonymous_fn(&self.name)
+ }
+ /// Call the function pointer with curried arguments (if any).
+ /// The function may be script-defined (not available under `no_function`) or native Rust.
+ ///
+ /// This method is intended for calling a function pointer directly, possibly on another [`Engine`].
+ /// Therefore, the [`AST`] is _NOT_ evaluated before calling the function.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+ /// # #[cfg(not(feature = "no_function"))]
+ /// # {
+ /// use rhai::{Engine, FnPtr};
+ ///
+ /// let engine = Engine::new();
+ ///
+ /// let ast = engine.compile("fn foo(x, y) { len(x) + y }")?;
+ ///
+ /// let mut fn_ptr = FnPtr::new("foo")?;
+ ///
+ /// // Curry values into the function pointer
+ /// fn_ptr.set_curry(vec!["abc".into()]);
+ ///
+ /// // Values are only needed for non-curried parameters
+ /// let result: i64 = fn_ptr.call(&engine, &ast, ( 39_i64, ) )?;
+ ///
+ /// assert_eq!(result, 42);
+ /// # }
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[inline]
+ pub fn call<T: Variant + Clone>(
+ &self,
+ engine: &Engine,
+ ast: &AST,
+ args: impl FuncArgs,
+ ) -> RhaiResultOf<T> {
+ let _ast = ast;
+ let mut arg_values = StaticVec::new_const();
+ args.parse(&mut arg_values);
+
+ let global = &mut GlobalRuntimeState::new(engine);
+
+ #[cfg(not(feature = "no_function"))]
+ global.lib.push(_ast.shared_lib().clone());
+
+ let ctx = (engine, self.fn_name(), None, &*global, Position::NONE).into();
+
+ self.call_raw(&ctx, None, arg_values).and_then(|result| {
+ result.try_cast_raw().map_err(|r| {
+ let result_type = engine.map_type_name(r.type_name());
+ let cast_type = match type_name::<T>() {
+ typ @ _ if typ.contains("::") => engine.map_type_name(typ),
+ typ @ _ => typ,
+ };
+ ERR::ErrorMismatchOutputType(cast_type.into(), result_type.into(), Position::NONE)
+ .into()
+ })
+ })
+ }
+ /// Call the function pointer with curried arguments (if any).
+ /// The function may be script-defined (not available under `no_function`) or native Rust.
+ ///
+ /// This method is intended for calling a function pointer that is passed into a native Rust
+ /// function as an argument. Therefore, the [`AST`] is _NOT_ evaluated before calling the
+ /// function.
+ #[inline]
+ pub fn call_within_context<T: Variant + Clone>(
+ &self,
+ context: &NativeCallContext,
+ args: impl FuncArgs,
+ ) -> RhaiResultOf<T> {
+ let mut arg_values = StaticVec::new_const();
+ args.parse(&mut arg_values);
+
+ self.call_raw(context, None, arg_values).and_then(|result| {
+ result.try_cast_raw().map_err(|r| {
+ let result_type = context.engine().map_type_name(r.type_name());
+ let cast_type = match type_name::<T>() {
+ typ @ _ if typ.contains("::") => context.engine().map_type_name(typ),
+ typ @ _ => typ,
+ };
+ ERR::ErrorMismatchOutputType(cast_type.into(), result_type.into(), Position::NONE)
+ .into()
+ })
+ })
+ }
+ /// Call the function pointer with curried arguments (if any).
+ /// The function may be script-defined (not available under `no_function`) or native Rust.
+ ///
+ /// This method is intended for calling a function pointer that is passed into a native Rust
+ /// function as an argument. Therefore, the [`AST`] is _NOT_ evaluated before calling the
+ /// function.
+ ///
+ /// # WARNING - Low Level API
+ ///
+ /// This function is very low level.
+ ///
+ /// # Arguments
+ ///
+ /// All the arguments are _consumed_, meaning that they're replaced by `()`.
+ /// This is to avoid unnecessarily cloning the arguments.
+ ///
+ /// Do not use the arguments after this call. If they are needed afterwards,
+ /// clone them _before_ calling this function.
+ #[inline]
+ pub fn call_raw(
+ &self,
+ context: &NativeCallContext,
+ this_ptr: Option<&mut Dynamic>,
+ arg_values: impl AsMut<[Dynamic]>,
+ ) -> RhaiResult {
+ let mut arg_values = arg_values;
+ let mut arg_values = arg_values.as_mut();
+ let mut args_data;
+
+ if self.is_curried() {
+ args_data = FnArgsVec::with_capacity(self.curry().len() + arg_values.len());
+ args_data.extend(self.curry().iter().cloned());
+ args_data.extend(arg_values.iter_mut().map(mem::take));
+ arg_values = &mut *args_data;
+ };
+
+ let args = &mut StaticVec::with_capacity(arg_values.len() + 1);
+ args.extend(arg_values.iter_mut());
+
+ // Linked to scripted function?
+ #[cfg(not(feature = "no_function"))]
+ if let Some(ref fn_def) = self.fn_def {
+ if fn_def.params.len() == args.len() {
+ let global = &mut context.global_runtime_state().clone();
+ global.level += 1;
+
+ let caches = &mut crate::eval::Caches::new();
+
+ return context.engine().call_script_fn(
+ global,
+ caches,
+ &mut crate::Scope::new(),
+ this_ptr,
+ self.encapsulated_environ().map(|r| r.as_ref()),
+ fn_def,
+ args,
+ true,
+ context.position(),
+ );
+ }
+ }
+
+ let is_method = this_ptr.is_some();
+
+ if let Some(obj) = this_ptr {
+ args.insert(0, obj);
+ }
+
+ context.call_fn_raw(self.fn_name(), is_method, is_method, args)
+ }
+ /// Get a reference to the [encapsulated environment][EncapsulatedEnviron].
+ #[inline(always)]
+ #[must_use]
+ #[allow(dead_code)]
+ pub(crate) fn encapsulated_environ(&self) -> Option<&Shared<EncapsulatedEnviron>> {
+ self.environ.as_ref()
+ }
+ /// Set a reference to the [encapsulated environment][EncapsulatedEnviron].
+ #[inline(always)]
+ #[allow(dead_code)]
+ pub(crate) fn set_encapsulated_environ(
+ &mut self,
+ value: Option<impl Into<Shared<EncapsulatedEnviron>>>,
+ ) {
+ self.environ = value.map(Into::into);
+ }
+ /// Get a reference to the linked [`ScriptFnDef`][crate::ast::ScriptFnDef].
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ #[must_use]
+ pub(crate) fn fn_def(&self) -> Option<&Shared<crate::ast::ScriptFnDef>> {
+ self.fn_def.as_ref()
+ }
+ /// Set a reference to the linked [`ScriptFnDef`][crate::ast::ScriptFnDef].
+ #[cfg(not(feature = "no_function"))]
+ #[inline(always)]
+ pub(crate) fn set_fn_def(&mut self, value: Option<impl Into<Shared<crate::ast::ScriptFnDef>>>) {
+ self.fn_def = value.map(Into::into);
+ }
+
+ /// Make a call to a function pointer with either a specified number of arguments, or with extra
+ /// arguments attached.
+ ///
+ /// If `this_ptr` is provided, it is first provided to script-defined functions bound to `this`.
+ ///
+ /// When an appropriate function is not found and `move_this_ptr_to_args` is `Some`, `this_ptr`
+ /// is removed and inserted as the appropriate parameter number.
+ ///
+ /// This is useful for calling predicate closures within an iteration loop where the extra argument
+ /// is the current element's index.
+ ///
+ /// If the function pointer is linked to a scripted function definition, use the appropriate number
+ /// of arguments to call it directly (one version attaches extra arguments).
+ #[cfg(not(feature = "internals"))]
+ #[inline(always)]
+ #[allow(dead_code)]
+ pub(crate) fn call_raw_with_extra_args<const N: usize, const E: usize>(
+ &self,
+ fn_name: &str,
+ ctx: &NativeCallContext,
+ this_ptr: Option<&mut Dynamic>,
+ args: [Dynamic; N],
+ extras: [Dynamic; E],
+ move_this_ptr_to_args: Option<usize>,
+ ) -> RhaiResult {
+ self._call_with_extra_args(fn_name, ctx, this_ptr, args, extras, move_this_ptr_to_args)
+ }
+ /// _(internals)_ Make a call to a function pointer with either a specified number of arguments,
+ /// or with extra arguments attached. Exported under the `internals` feature only.
+ ///
+ /// If `this_ptr` is provided, it is first provided to script-defined functions bound to `this`.
+ ///
+ /// When an appropriate function is not found and `move_this_ptr_to_args` is `Some`, `this_ptr`
+ /// is removed and inserted as the appropriate parameter number.
+ ///
+ /// This is useful for calling predicate closures within an iteration loop where the extra
+ /// argument is the current element's index.
+ ///
+ /// If the function pointer is linked to a scripted function definition, use the appropriate
+ /// number of arguments to call it directly (one version attaches extra arguments).
+ #[cfg(feature = "internals")]
+ #[inline(always)]
+ pub fn call_raw_with_extra_args<const N: usize, const E: usize>(
+ &self,
+ fn_name: &str,
+ ctx: &NativeCallContext,
+ this_ptr: Option<&mut Dynamic>,
+ args: [Dynamic; N],
+ extras: [Dynamic; E],
+ move_this_ptr_to_args: Option<usize>,
+ ) -> RhaiResult {
+ self._call_with_extra_args(fn_name, ctx, this_ptr, args, extras, move_this_ptr_to_args)
+ }
+ /// Make a call to a function pointer with either a specified number of arguments, or with extra
+ /// arguments attached.
+ fn _call_with_extra_args<const N: usize, const E: usize>(
+ &self,
+ fn_name: &str,
+ ctx: &NativeCallContext,
+ mut this_ptr: Option<&mut Dynamic>,
+ args: [Dynamic; N],
+ extras: [Dynamic; E],
+ move_this_ptr_to_args: Option<usize>,
+ ) -> RhaiResult {
+ #[cfg(not(feature = "no_function"))]
+ if let Some(arity) = self.fn_def().map(|f| f.params.len()) {
+ if arity == N + self.curry().len() {
+ return self.call_raw(ctx, this_ptr, args);
+ }
+ if let Some(move_to_args) = move_this_ptr_to_args {
+ if this_ptr.is_some() {
+ if arity == N + 1 + self.curry().len() {
+ let mut args2 = FnArgsVec::with_capacity(args.len() + 1);
+ if move_to_args == 0 {
+ args2.push(this_ptr.as_mut().unwrap().clone());
+ args2.extend(args);
+ } else {
+ args2.extend(args);
+ args2.insert(move_to_args, this_ptr.as_mut().unwrap().clone());
+ }
+ return self.call_raw(ctx, None, args2);
+ }
+ if arity == N + E + 1 + self.curry().len() {
+ let mut args2 = FnArgsVec::with_capacity(args.len() + extras.len() + 1);
+ if move_to_args == 0 {
+ args2.push(this_ptr.as_mut().unwrap().clone());
+ args2.extend(args);
+ args2.extend(extras);
+ } else {
+ args2.extend(args);
+ args2.insert(move_to_args, this_ptr.as_mut().unwrap().clone());
+ args2.extend(extras);
+ }
+ return self.call_raw(ctx, None, args2);
+ }
+ }
+ }
+ if arity == N + E + self.curry().len() {
+ let mut args2 = FnArgsVec::with_capacity(args.len() + extras.len());
+ args2.extend(args);
+ args2.extend(extras);
+ return self.call_raw(ctx, this_ptr, args2);
+ }
+ }
+
+ self.call_raw(ctx, this_ptr.as_deref_mut(), args.clone())
+ .or_else(|err| match *err {
+ ERR::ErrorFunctionNotFound(sig, ..)
+ if move_this_ptr_to_args.is_some()
+ && this_ptr.is_some()
+ && sig.starts_with(self.fn_name()) =>
+ {
+ let mut args2 = FnArgsVec::with_capacity(args.len() + 1);
+ let move_to_args = move_this_ptr_to_args.unwrap();
+ if move_to_args == 0 {
+ args2.push(this_ptr.as_mut().unwrap().clone());
+ args2.extend(args.clone());
+ } else {
+ args2.extend(args.clone());
+ args2.insert(move_to_args, this_ptr.as_mut().unwrap().clone());
+ }
+ self.call_raw(ctx, None, args2)
+ }
+ _ => Err(err),
+ })
+ .or_else(|err| match *err {
+ ERR::ErrorFunctionNotFound(sig, ..) if sig.starts_with(self.fn_name()) => {
+ if let Some(move_to_args) = move_this_ptr_to_args {
+ if let Some(ref mut this_ptr) = this_ptr {
+ let mut args2 = FnArgsVec::with_capacity(args.len() + extras.len() + 1);
+ if move_to_args == 0 {
+ args2.push(this_ptr.clone());
+ args2.extend(args);
+ args2.extend(extras);
+ } else {
+ args2.extend(args);
+ args2.extend(extras);
+ args2.insert(move_to_args, this_ptr.clone());
+ }
+ return self.call_raw(ctx, None, args2);
+ }
+ }
+
+ let mut args2 = FnArgsVec::with_capacity(args.len() + extras.len());
+ args2.extend(args);
+ args2.extend(extras);
+
+ self.call_raw(ctx, this_ptr, args2)
+ }
+ _ => Err(err),
+ })
+ .map_err(|err| {
+ Box::new(ERR::ErrorInFunctionCall(
+ fn_name.to_string(),
+ ctx.source().unwrap_or("").to_string(),
+ err,
+ Position::NONE,
+ ))
+ })
+ }
+}
+
+impl fmt::Display for FnPtr {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "Fn({})", self.fn_name())
+ }
+}
+
+impl TryFrom<ImmutableString> for FnPtr {
+ type Error = RhaiError;
+
+ #[inline(always)]
+ fn try_from(value: ImmutableString) -> RhaiResultOf<Self> {
+ if is_valid_function_name(&value) {
+ Ok(Self {
+ name: value,
+ curry: StaticVec::new_const(),
+ environ: None,
+ #[cfg(not(feature = "no_function"))]
+ fn_def: None,
+ })
+ } else if is_reserved_keyword_or_symbol(&value).0
+ || Token::lookup_symbol_from_syntax(&value).is_some()
+ {
+ Err(
+ ERR::ErrorParsing(ParseErrorType::Reserved(value.to_string()), Position::NONE)
+ .into(),
+ )
+ } else {
+ Err(ERR::ErrorFunctionNotFound(value.to_string(), Position::NONE).into())
+ }
+ }
+}
+
+#[cfg(not(feature = "no_function"))]
+impl<T: Into<Shared<crate::ast::ScriptFnDef>>> From<T> for FnPtr {
+ #[inline(always)]
+ fn from(value: T) -> Self {
+ let fn_def = value.into();
+
+ Self {
+ name: fn_def.name.clone(),
+ curry: StaticVec::new_const(),
+ environ: None,
+ fn_def: Some(fn_def),
+ }
+ }
+}
+
+impl Index<usize> for FnPtr {
+ type Output = Dynamic;
+
+ #[inline(always)]
+ fn index(&self, index: usize) -> &Self::Output {
+ self.curry.index(index)
+ }
+}
+
+impl IndexMut<usize> for FnPtr {
+ #[inline(always)]
+ fn index_mut(&mut self, index: usize) -> &mut Self::Output {
+ self.curry.index_mut(index)
+ }
+}
+
+impl Extend<Dynamic> for FnPtr {
+ #[inline(always)]
+ fn extend<T: IntoIterator<Item = Dynamic>>(&mut self, iter: T) {
+ self.curry.extend(iter)
+ }
+}
diff --git a/rhai/src/types/immutable_string.rs b/rhai/src/types/immutable_string.rs
new file mode 100644
index 0000000..0d25108
--- /dev/null
+++ b/rhai/src/types/immutable_string.rs
@@ -0,0 +1,677 @@
+//! The `ImmutableString` type.
+
+use crate::func::{shared_get_mut, shared_make_mut, shared_take};
+use crate::{Shared, SmartString};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ borrow::Borrow,
+ cmp::Ordering,
+ fmt,
+ hash::Hash,
+ iter::FromIterator,
+ ops::{Add, AddAssign, Deref, Sub, SubAssign},
+ str::FromStr,
+};
+
+/// The system immutable string type.
+///
+/// An [`ImmutableString`] wraps an `Rc<SmartString>` (or `Arc<SmartString>` under the `sync` feature)
+/// so that it can be simply shared and not cloned.
+///
+/// # Example
+///
+/// ```
+/// use rhai::ImmutableString;
+///
+/// let s1: ImmutableString = "hello".into();
+///
+/// // No actual cloning of the string is involved below.
+/// let s2 = s1.clone();
+/// let s3 = s2.clone();
+///
+/// assert_eq!(s1, s2);
+///
+/// // Clones the underlying string (because it is already shared) and extracts it.
+/// let mut s: String = s1.into_owned();
+///
+/// // Changing the clone has no impact on the previously shared version.
+/// s.push_str(", world!");
+///
+/// // The old version still exists.
+/// assert_eq!(s2, s3);
+/// assert_eq!(s2.as_str(), "hello");
+///
+/// // Not equals!
+/// assert_ne!(s2.as_str(), s.as_str());
+/// assert_eq!(s, "hello, world!");
+/// ```
+#[derive(Clone, Eq, Ord, Hash, Default)]
+pub struct ImmutableString(Shared<SmartString>);
+
+impl Deref for ImmutableString {
+ type Target = SmartString;
+
+ #[inline(always)]
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl AsRef<SmartString> for ImmutableString {
+ #[inline(always)]
+ #[must_use]
+ fn as_ref(&self) -> &SmartString {
+ &self.0
+ }
+}
+
+impl AsRef<str> for ImmutableString {
+ #[inline(always)]
+ #[must_use]
+ fn as_ref(&self) -> &str {
+ &self.0
+ }
+}
+
+impl Borrow<SmartString> for ImmutableString {
+ #[inline(always)]
+ #[must_use]
+ fn borrow(&self) -> &SmartString {
+ &self.0
+ }
+}
+
+impl Borrow<str> for ImmutableString {
+ #[inline(always)]
+ #[must_use]
+ fn borrow(&self) -> &str {
+ self.as_str()
+ }
+}
+
+impl From<&str> for ImmutableString {
+ #[inline(always)]
+ fn from(value: &str) -> Self {
+ let value: SmartString = value.into();
+ Self(value.into())
+ }
+}
+impl From<Box<str>> for ImmutableString {
+ #[inline(always)]
+ fn from(value: Box<str>) -> Self {
+ let value: SmartString = value.into();
+ Self(value.into())
+ }
+}
+impl From<&String> for ImmutableString {
+ #[inline(always)]
+ fn from(value: &String) -> Self {
+ let value: SmartString = value.into();
+ Self(value.into())
+ }
+}
+impl From<String> for ImmutableString {
+ #[inline(always)]
+ fn from(value: String) -> Self {
+ let value: SmartString = value.into();
+ Self(value.into())
+ }
+}
+impl From<&SmartString> for ImmutableString {
+ #[inline(always)]
+ fn from(value: &SmartString) -> Self {
+ Self(value.clone().into())
+ }
+}
+impl From<SmartString> for ImmutableString {
+ #[inline(always)]
+ fn from(value: SmartString) -> Self {
+ Self(value.into())
+ }
+}
+impl From<&ImmutableString> for SmartString {
+ #[inline(always)]
+ fn from(value: &ImmutableString) -> Self {
+ value.as_str().into()
+ }
+}
+impl From<ImmutableString> for SmartString {
+ #[inline(always)]
+ fn from(mut value: ImmutableString) -> Self {
+ std::mem::take(shared_make_mut(&mut value.0))
+ }
+}
+
+impl FromStr for ImmutableString {
+ type Err = ();
+
+ #[inline(always)]
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let s: SmartString = s.into();
+ Ok(Self(s.into()))
+ }
+}
+
+impl FromIterator<char> for ImmutableString {
+ #[inline]
+ #[must_use]
+ fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
+ Self(iter.into_iter().collect::<SmartString>().into())
+ }
+}
+
+impl<'a> FromIterator<&'a char> for ImmutableString {
+ #[inline]
+ #[must_use]
+ fn from_iter<T: IntoIterator<Item = &'a char>>(iter: T) -> Self {
+ Self(iter.into_iter().copied().collect::<SmartString>().into())
+ }
+}
+
+impl<'a> FromIterator<&'a str> for ImmutableString {
+ #[inline]
+ #[must_use]
+ fn from_iter<T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
+ Self(iter.into_iter().collect::<SmartString>().into())
+ }
+}
+
+impl FromIterator<String> for ImmutableString {
+ #[inline]
+ #[must_use]
+ fn from_iter<T: IntoIterator<Item = String>>(iter: T) -> Self {
+ Self(iter.into_iter().collect::<SmartString>().into())
+ }
+}
+
+impl FromIterator<SmartString> for ImmutableString {
+ #[inline]
+ #[must_use]
+ fn from_iter<T: IntoIterator<Item = SmartString>>(iter: T) -> Self {
+ Self(iter.into_iter().collect::<SmartString>().into())
+ }
+}
+
+impl fmt::Display for ImmutableString {
+ #[inline(always)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Display::fmt(self.as_str(), f)
+ }
+}
+
+impl fmt::Debug for ImmutableString {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Debug::fmt(self.as_str(), f)
+ }
+}
+
+impl Add for ImmutableString {
+ type Output = Self;
+
+ #[inline]
+ fn add(mut self, rhs: Self) -> Self::Output {
+ if rhs.is_empty() {
+ self
+ } else if self.is_empty() {
+ rhs
+ } else {
+ self.make_mut().push_str(rhs.as_str());
+ self
+ }
+ }
+}
+
+impl Add for &ImmutableString {
+ type Output = ImmutableString;
+
+ #[inline]
+ fn add(self, rhs: Self) -> Self::Output {
+ if rhs.is_empty() {
+ self.clone()
+ } else if self.is_empty() {
+ rhs.clone()
+ } else {
+ let mut s = self.clone();
+ s.make_mut().push_str(rhs.as_str());
+ s
+ }
+ }
+}
+
+impl Add<&Self> for ImmutableString {
+ type Output = Self;
+
+ #[inline]
+ fn add(mut self, rhs: &Self) -> Self::Output {
+ if rhs.is_empty() {
+ self
+ } else if self.is_empty() {
+ rhs.clone()
+ } else {
+ self.make_mut().push_str(rhs.as_str());
+ self
+ }
+ }
+}
+
+impl Add<ImmutableString> for &ImmutableString {
+ type Output = ImmutableString;
+
+ #[inline]
+ fn add(self, rhs: ImmutableString) -> Self::Output {
+ if rhs.is_empty() {
+ self.clone()
+ } else if self.is_empty() {
+ rhs
+ } else {
+ let mut s = self.clone();
+ s.make_mut().push_str(rhs.as_str());
+ s
+ }
+ }
+}
+
+impl AddAssign<&Self> for ImmutableString {
+ #[inline]
+ fn add_assign(&mut self, rhs: &Self) {
+ if !rhs.is_empty() {
+ if self.is_empty() {
+ self.0 = rhs.0.clone();
+ } else {
+ self.make_mut().push_str(rhs.as_str());
+ }
+ }
+ }
+}
+
+impl AddAssign<Self> for ImmutableString {
+ #[inline]
+ fn add_assign(&mut self, rhs: Self) {
+ if !rhs.is_empty() {
+ if self.is_empty() {
+ self.0 = rhs.0;
+ } else {
+ self.make_mut().push_str(rhs.as_str());
+ }
+ }
+ }
+}
+
+impl Add<&str> for ImmutableString {
+ type Output = Self;
+
+ #[inline]
+ fn add(mut self, rhs: &str) -> Self::Output {
+ if !rhs.is_empty() {
+ self.make_mut().push_str(rhs);
+ }
+ self
+ }
+}
+
+impl Add<&str> for &ImmutableString {
+ type Output = ImmutableString;
+
+ #[inline]
+ fn add(self, rhs: &str) -> Self::Output {
+ if rhs.is_empty() {
+ self.clone()
+ } else {
+ let mut s = self.clone();
+ s.make_mut().push_str(rhs);
+ s
+ }
+ }
+}
+
+impl AddAssign<&str> for ImmutableString {
+ #[inline]
+ fn add_assign(&mut self, rhs: &str) {
+ if !rhs.is_empty() {
+ self.make_mut().push_str(rhs);
+ }
+ }
+}
+
+impl Add<String> for ImmutableString {
+ type Output = Self;
+
+ #[inline]
+ fn add(mut self, rhs: String) -> Self::Output {
+ if rhs.is_empty() {
+ self
+ } else if self.is_empty() {
+ rhs.into()
+ } else {
+ self.make_mut().push_str(&rhs);
+ self
+ }
+ }
+}
+
+impl Add<String> for &ImmutableString {
+ type Output = ImmutableString;
+
+ #[inline]
+ fn add(self, rhs: String) -> Self::Output {
+ if rhs.is_empty() {
+ self.clone()
+ } else if self.is_empty() {
+ rhs.into()
+ } else {
+ let mut s = self.clone();
+ s.make_mut().push_str(&rhs);
+ s
+ }
+ }
+}
+
+impl AddAssign<String> for ImmutableString {
+ #[inline]
+ fn add_assign(&mut self, rhs: String) {
+ if !rhs.is_empty() {
+ if self.is_empty() {
+ let rhs: SmartString = rhs.into();
+ self.0 = rhs.into();
+ } else {
+ self.make_mut().push_str(&rhs);
+ }
+ }
+ }
+}
+
+impl Add<char> for ImmutableString {
+ type Output = Self;
+
+ #[inline]
+ fn add(mut self, rhs: char) -> Self::Output {
+ self.make_mut().push(rhs);
+ self
+ }
+}
+
+impl Add<char> for &ImmutableString {
+ type Output = ImmutableString;
+
+ #[inline]
+ fn add(self, rhs: char) -> Self::Output {
+ let mut s = self.clone();
+ s.make_mut().push(rhs);
+ s
+ }
+}
+
+impl AddAssign<char> for ImmutableString {
+ #[inline]
+ fn add_assign(&mut self, rhs: char) {
+ self.make_mut().push(rhs);
+ }
+}
+
+impl Sub for ImmutableString {
+ type Output = Self;
+
+ #[inline]
+ fn sub(self, rhs: Self) -> Self::Output {
+ if rhs.is_empty() {
+ self
+ } else if self.is_empty() {
+ rhs
+ } else {
+ self.replace(rhs.as_str(), "").into()
+ }
+ }
+}
+
+impl Sub for &ImmutableString {
+ type Output = ImmutableString;
+
+ #[inline]
+ fn sub(self, rhs: Self) -> Self::Output {
+ if rhs.is_empty() {
+ self.clone()
+ } else if self.is_empty() {
+ rhs.clone()
+ } else {
+ self.replace(rhs.as_str(), "").into()
+ }
+ }
+}
+
+impl SubAssign<&Self> for ImmutableString {
+ #[inline]
+ fn sub_assign(&mut self, rhs: &Self) {
+ if !rhs.is_empty() {
+ if self.is_empty() {
+ self.0 = rhs.0.clone();
+ } else {
+ let rhs: SmartString = self.replace(rhs.as_str(), "").into();
+ self.0 = rhs.into();
+ }
+ }
+ }
+}
+
+impl SubAssign<Self> for ImmutableString {
+ #[inline]
+ fn sub_assign(&mut self, rhs: Self) {
+ if !rhs.is_empty() {
+ if self.is_empty() {
+ self.0 = rhs.0;
+ } else {
+ let rhs: SmartString = self.replace(rhs.as_str(), "").into();
+ self.0 = rhs.into();
+ }
+ }
+ }
+}
+
+impl Sub<String> for ImmutableString {
+ type Output = Self;
+
+ #[inline]
+ fn sub(self, rhs: String) -> Self::Output {
+ if rhs.is_empty() {
+ self
+ } else if self.is_empty() {
+ rhs.into()
+ } else {
+ self.replace(&rhs, "").into()
+ }
+ }
+}
+
+impl Sub<String> for &ImmutableString {
+ type Output = ImmutableString;
+
+ #[inline]
+ fn sub(self, rhs: String) -> Self::Output {
+ if rhs.is_empty() {
+ self.clone()
+ } else if self.is_empty() {
+ rhs.into()
+ } else {
+ self.replace(&rhs, "").into()
+ }
+ }
+}
+
+impl SubAssign<String> for ImmutableString {
+ #[inline]
+ fn sub_assign(&mut self, rhs: String) {
+ if !rhs.is_empty() {
+ let rhs: SmartString = self.replace(&rhs, "").into();
+ self.0 = rhs.into();
+ }
+ }
+}
+
+impl Sub<&str> for ImmutableString {
+ type Output = Self;
+
+ #[inline]
+ fn sub(self, rhs: &str) -> Self::Output {
+ if rhs.is_empty() {
+ self
+ } else if self.is_empty() {
+ rhs.into()
+ } else {
+ self.replace(rhs, "").into()
+ }
+ }
+}
+
+impl Sub<&str> for &ImmutableString {
+ type Output = ImmutableString;
+
+ #[inline]
+ fn sub(self, rhs: &str) -> Self::Output {
+ if rhs.is_empty() {
+ self.clone()
+ } else if self.is_empty() {
+ rhs.into()
+ } else {
+ self.replace(rhs, "").into()
+ }
+ }
+}
+
+impl SubAssign<&str> for ImmutableString {
+ #[inline]
+ fn sub_assign(&mut self, rhs: &str) {
+ if !rhs.is_empty() {
+ let rhs: SmartString = self.replace(rhs, "").into();
+ self.0 = rhs.into();
+ }
+ }
+}
+
+impl Sub<char> for ImmutableString {
+ type Output = Self;
+
+ #[inline(always)]
+ fn sub(self, rhs: char) -> Self::Output {
+ self.replace(rhs, "").into()
+ }
+}
+
+impl Sub<char> for &ImmutableString {
+ type Output = ImmutableString;
+
+ #[inline(always)]
+ fn sub(self, rhs: char) -> Self::Output {
+ self.replace(rhs, "").into()
+ }
+}
+
+impl SubAssign<char> for ImmutableString {
+ #[inline]
+ fn sub_assign(&mut self, rhs: char) {
+ let rhs: SmartString = self.replace(rhs, "").into();
+ self.0 = rhs.into();
+ }
+}
+
+impl<S: AsRef<str>> PartialEq<S> for ImmutableString {
+ #[inline(always)]
+ fn eq(&self, other: &S) -> bool {
+ self.as_str().eq(other.as_ref())
+ }
+}
+
+impl PartialEq<ImmutableString> for str {
+ #[inline(always)]
+ fn eq(&self, other: &ImmutableString) -> bool {
+ self.eq(other.as_str())
+ }
+}
+
+impl PartialEq<ImmutableString> for String {
+ #[inline(always)]
+ fn eq(&self, other: &ImmutableString) -> bool {
+ self.eq(other.as_str())
+ }
+}
+
+impl<S: AsRef<str>> PartialOrd<S> for ImmutableString {
+ fn partial_cmp(&self, other: &S) -> Option<Ordering> {
+ self.as_str().partial_cmp(other.as_ref())
+ }
+}
+
+impl PartialOrd<ImmutableString> for str {
+ #[inline(always)]
+ fn partial_cmp(&self, other: &ImmutableString) -> Option<Ordering> {
+ self.partial_cmp(other.as_str())
+ }
+}
+
+impl PartialOrd<ImmutableString> for String {
+ #[inline(always)]
+ fn partial_cmp(&self, other: &ImmutableString) -> Option<Ordering> {
+ self.as_str().partial_cmp(other.as_str())
+ }
+}
+
+impl ImmutableString {
+ /// Create a new [`ImmutableString`].
+ #[inline(always)]
+ #[must_use]
+ pub fn new() -> Self {
+ Self(SmartString::new_const().into())
+ }
+ /// Strong count of references to the underlying string.
+ pub(crate) fn strong_count(&self) -> usize {
+ Shared::strong_count(&self.0)
+ }
+ /// Consume the [`ImmutableString`] and convert it into a [`String`].
+ ///
+ /// If there are other references to the same string, a cloned copy is returned.
+ #[inline]
+ #[must_use]
+ pub fn into_owned(mut self) -> String {
+ let _ = self.make_mut(); // Make sure it is unique reference
+ shared_take(self.0).into() // Should succeed
+ }
+ /// Make sure that the [`ImmutableString`] is unique (i.e. no other outstanding references).
+ /// Then return a mutable reference to the [`SmartString`].
+ ///
+ /// If there are other references to the same string, a cloned copy is used.
+ #[inline(always)]
+ #[must_use]
+ pub(crate) fn make_mut(&mut self) -> &mut SmartString {
+ shared_make_mut(&mut self.0)
+ }
+ /// Return a mutable reference to the [`SmartString`] wrapped by the [`ImmutableString`].
+ #[inline(always)]
+ pub(crate) fn get_mut(&mut self) -> Option<&mut SmartString> {
+ shared_get_mut(&mut self.0)
+ }
+ /// Returns `true` if the two [`ImmutableString`]'s point to the same allocation.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::ImmutableString;
+ ///
+ /// let s1: ImmutableString = "hello".into();
+ /// let s2 = s1.clone();
+ /// let s3: ImmutableString = "hello".into();
+ ///
+ /// assert_eq!(s1, s2);
+ /// assert_eq!(s1, s3);
+ /// assert_eq!(s2, s3);
+ ///
+ /// assert!(s1.ptr_eq(&s2));
+ /// assert!(!s1.ptr_eq(&s3));
+ /// assert!(!s2.ptr_eq(&s3));
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn ptr_eq(&self, other: &Self) -> bool {
+ Shared::ptr_eq(&self.0, &other.0)
+ }
+}
diff --git a/rhai/src/types/interner.rs b/rhai/src/types/interner.rs
new file mode 100644
index 0000000..22e06fe
--- /dev/null
+++ b/rhai/src/types/interner.rs
@@ -0,0 +1,171 @@
+//! A strings interner type.
+
+use super::BloomFilterU64;
+use crate::func::{hashing::get_hasher, StraightHashMap};
+use crate::ImmutableString;
+#[cfg(feature = "no_std")]
+use hashbrown::hash_map::Entry;
+#[cfg(not(feature = "no_std"))]
+use std::collections::hash_map::Entry;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ fmt,
+ hash::{Hash, Hasher},
+ ops::AddAssign,
+};
+
+/// Maximum number of strings interned.
+pub const MAX_INTERNED_STRINGS: usize = 1024;
+
+/// Maximum length of strings interned.
+pub const MAX_STRING_LEN: usize = 24;
+
+/// _(internals)_ A cache for interned strings.
+/// Exported under the `internals` feature only.
+#[derive(Clone)]
+pub struct StringsInterner {
+ /// Cached strings.
+ cache: StraightHashMap<ImmutableString>,
+ /// Bloom filter to avoid caching "one-hit wonders".
+ bloom_filter: BloomFilterU64,
+}
+
+impl Default for StringsInterner {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl fmt::Debug for StringsInterner {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_list().entries(self.cache.values()).finish()
+ }
+}
+
+impl StringsInterner {
+ /// Create a new [`StringsInterner`].
+ #[inline(always)]
+ #[must_use]
+ pub fn new() -> Self {
+ Self {
+ cache: StraightHashMap::default(),
+ bloom_filter: BloomFilterU64::new(),
+ }
+ }
+
+ /// Get an identifier from a text string, adding it to the interner if necessary.
+ #[inline(always)]
+ #[must_use]
+ pub fn get<S: AsRef<str> + Into<ImmutableString>>(&mut self, text: S) -> ImmutableString {
+ self.get_with_mapper(0, Into::into, text)
+ }
+
+ /// Get an identifier from a text string, adding it to the interner if necessary.
+ #[inline]
+ #[must_use]
+ pub fn get_with_mapper<S: AsRef<str>>(
+ &mut self,
+ category: u8,
+ mapper: impl FnOnce(S) -> ImmutableString,
+ text: S,
+ ) -> ImmutableString {
+ let key = text.as_ref();
+
+ let hasher = &mut get_hasher();
+ hasher.write_u8(category);
+ key.hash(hasher);
+ let hash = hasher.finish();
+
+ // Do not cache long strings and avoid caching "one-hit wonders".
+ if key.len() > MAX_STRING_LEN || self.bloom_filter.is_absent_and_set(hash) {
+ return mapper(text);
+ }
+
+ if self.cache.is_empty() {
+ self.cache.reserve(MAX_INTERNED_STRINGS);
+ }
+
+ let result = match self.cache.entry(hash) {
+ Entry::Occupied(e) => return e.get().clone(),
+ Entry::Vacant(e) => e.insert(mapper(text)).clone(),
+ };
+
+ // Throttle the cache upon exit
+ self.throttle_cache(hash);
+
+ result
+ }
+
+ /// If the interner is over capacity, remove the longest entry that has the lowest count
+ #[inline]
+ fn throttle_cache(&mut self, skip_hash: u64) {
+ if self.cache.len() <= MAX_INTERNED_STRINGS {
+ return;
+ }
+
+ // Leave some buffer to grow when shrinking the cache.
+ // We leave at least two entries, one for the empty string, and one for the string
+ // that has just been inserted.
+ while self.cache.len() > MAX_INTERNED_STRINGS - 3 {
+ let mut max_len = 0;
+ let mut min_count = usize::MAX;
+ let mut index = 0;
+
+ for (&k, v) in &self.cache {
+ if k != skip_hash
+ && (v.strong_count() < min_count
+ || (v.strong_count() == min_count && v.len() > max_len))
+ {
+ max_len = v.len();
+ min_count = v.strong_count();
+ index = k;
+ }
+ }
+
+ self.cache.remove(&index);
+ }
+ }
+
+ /// Number of strings interned.
+ #[inline(always)]
+ #[must_use]
+ #[allow(dead_code)]
+ pub fn len(&self) -> usize {
+ self.cache.len()
+ }
+
+ /// Returns `true` if there are no interned strings.
+ #[inline(always)]
+ #[must_use]
+ #[allow(dead_code)]
+ pub fn is_empty(&self) -> bool {
+ self.cache.is_empty()
+ }
+
+ /// Clear all interned strings.
+ #[inline(always)]
+ #[allow(dead_code)]
+ pub fn clear(&mut self) {
+ self.cache.clear();
+ }
+}
+
+impl AddAssign<Self> for StringsInterner {
+ #[inline(always)]
+ fn add_assign(&mut self, rhs: Self) {
+ self.cache.extend(rhs.cache.into_iter());
+ }
+}
+
+impl AddAssign<&Self> for StringsInterner {
+ #[inline(always)]
+ fn add_assign(&mut self, rhs: &Self) {
+ self.cache
+ .extend(rhs.cache.iter().map(|(&k, v)| (k, v.clone())));
+ }
+}
diff --git a/rhai/src/types/mod.rs b/rhai/src/types/mod.rs
new file mode 100644
index 0000000..2e1f1ed
--- /dev/null
+++ b/rhai/src/types/mod.rs
@@ -0,0 +1,36 @@
+//! Module defining Rhai data types.
+
+pub mod bloom_filter;
+pub mod custom_types;
+pub mod dynamic;
+pub mod error;
+pub mod float;
+pub mod fn_ptr;
+pub mod immutable_string;
+pub mod interner;
+pub mod parse_error;
+pub mod position;
+pub mod position_none;
+pub mod scope;
+pub mod variant;
+
+pub use bloom_filter::BloomFilterU64;
+pub use custom_types::{CustomTypeInfo, CustomTypesCollection};
+pub use dynamic::Dynamic;
+#[cfg(not(feature = "no_time"))]
+pub use dynamic::Instant;
+pub use error::EvalAltResult;
+#[cfg(not(feature = "no_float"))]
+pub use float::FloatWrapper;
+pub use fn_ptr::FnPtr;
+pub use immutable_string::ImmutableString;
+pub use interner::StringsInterner;
+pub use parse_error::{LexError, ParseError, ParseErrorType};
+
+#[cfg(not(feature = "no_position"))]
+pub use position::{Position, Span};
+#[cfg(feature = "no_position")]
+pub use position_none::{Position, Span};
+
+pub use scope::Scope;
+pub use variant::Variant;
diff --git a/rhai/src/types/parse_error.rs b/rhai/src/types/parse_error.rs
new file mode 100644
index 0000000..e6062d5
--- /dev/null
+++ b/rhai/src/types/parse_error.rs
@@ -0,0 +1,339 @@
+//! Module containing error definitions for the parsing process.
+
+use crate::tokenizer::is_valid_identifier;
+use crate::{Position, RhaiError, ERR};
+#[cfg(feature = "no_std")]
+use core_error::Error;
+#[cfg(not(feature = "no_std"))]
+use std::error::Error;
+use std::fmt;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+/// Error encountered when tokenizing the script text.
+#[derive(Debug, Eq, PartialEq, Clone, Hash)]
+#[non_exhaustive]
+#[must_use]
+pub enum LexError {
+ /// An unexpected symbol is encountered when tokenizing the script text.
+ UnexpectedInput(String),
+ /// A string literal is not terminated before a new-line or EOF.
+ UnterminatedString,
+ /// An identifier or string literal is longer than the maximum allowed length.
+ StringTooLong(usize),
+ /// An string/character/numeric escape sequence is in an invalid format.
+ MalformedEscapeSequence(String),
+ /// An numeric literal is in an invalid format.
+ MalformedNumber(String),
+ /// An character literal is in an invalid format.
+ MalformedChar(String),
+ /// An identifier is in an invalid format.
+ MalformedIdentifier(String),
+ /// Bad symbol encountered when tokenizing the script text.
+ ImproperSymbol(String, String),
+}
+
+impl Error for LexError {}
+
+impl fmt::Display for LexError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::UnexpectedInput(s) => write!(f, "Unexpected '{s}'"),
+ Self::MalformedEscapeSequence(s) => write!(f, "Invalid escape sequence: '{s}'"),
+ Self::MalformedNumber(s) => write!(f, "Invalid number: '{s}'"),
+ Self::MalformedChar(s) => write!(f, "Invalid character: '{s}'"),
+ Self::MalformedIdentifier(s) => write!(f, "Variable name is not proper: '{s}'"),
+ Self::UnterminatedString => f.write_str("Open string is not terminated"),
+ Self::StringTooLong(max) => write!(f, "String is too long (max {max})"),
+ Self::ImproperSymbol(s, d) if d.is_empty() => {
+ write!(f, "Invalid symbol encountered: '{s}'")
+ }
+ Self::ImproperSymbol(.., d) => f.write_str(d),
+ }
+ }
+}
+
+impl LexError {
+ /// Convert a [`LexError`] into a [`ParseError`].
+ #[cold]
+ #[inline(never)]
+ pub fn into_err(self, pos: Position) -> ParseError {
+ ParseError(Box::new(self.into()), pos)
+ }
+}
+
+/// Error encountered when parsing a script.
+///
+/// Some errors never appear when certain features are turned on.
+/// They still exist so that the application can turn features on and off without going through
+/// massive code changes to remove/add back enum variants in match statements.
+#[derive(Debug, Eq, PartialEq, Clone, Hash)]
+#[non_exhaustive]
+#[must_use]
+pub enum ParseErrorType {
+ /// The script ends prematurely.
+ UnexpectedEOF,
+ /// Error in the script text. Wrapped value is the lex error.
+ BadInput(LexError),
+ /// An unknown operator is encountered. Wrapped value is the operator.
+ UnknownOperator(String),
+ /// Expecting a particular token but not finding one. Wrapped values are the token and description.
+ MissingToken(String, String),
+ /// Expecting a particular symbol but not finding one. Wrapped value is the description.
+ MissingSymbol(String),
+ /// An expression in function call arguments `()` has syntax error. Wrapped value is the error
+ /// description (if any).
+ MalformedCallExpr(String),
+ /// An expression in indexing brackets `[]` has syntax error. Wrapped value is the error
+ /// description (if any).
+ MalformedIndexExpr(String),
+ /// An expression in an `in` expression has syntax error. Wrapped value is the error description (if any).
+ MalformedInExpr(String),
+ /// A capturing has syntax error. Wrapped value is the error description (if any).
+ MalformedCapture(String),
+ /// A map definition has duplicated property names. Wrapped value is the property name.
+ DuplicatedProperty(String),
+ /// A `switch` case is duplicated.
+ ///
+ /// # Deprecated
+ ///
+ /// This error variant is deprecated. It never occurs and will be removed in the next major version.
+ #[deprecated(
+ since = "1.9.0",
+ note = "This error variant is deprecated. It never occurs and will be removed in the next major version."
+ )]
+ DuplicatedSwitchCase,
+ /// A variable name is duplicated. Wrapped value is the variable name.
+ DuplicatedVariable(String),
+ /// A numeric case of a `switch` statement is in an appropriate place.
+ WrongSwitchIntegerCase,
+ /// The default case of a `switch` statement is in an appropriate place.
+ WrongSwitchDefaultCase,
+ /// The case condition of a `switch` statement is not appropriate.
+ WrongSwitchCaseCondition,
+ /// Missing a property name for custom types and maps.
+ PropertyExpected,
+ /// Missing a variable name after the `let`, `const`, `for` or `catch` keywords.
+ VariableExpected,
+ /// Forbidden variable name. Wrapped value is the variable name.
+ ForbiddenVariable(String),
+ /// An identifier is a reserved symbol.
+ Reserved(String),
+ /// An expression is of the wrong type.
+ /// Wrapped values are the type requested and type of the actual result.
+ MismatchedType(String, String),
+ /// Missing an expression. Wrapped value is the expression type.
+ ExprExpected(String),
+ /// Defining a doc-comment in an appropriate place (e.g. not at global level).
+ WrongDocComment,
+ /// Defining a function `fn` in an appropriate place (e.g. inside another function).
+ WrongFnDefinition,
+ /// Defining a function with a name that conflicts with an existing function.
+ /// Wrapped values are the function name and number of parameters.
+ FnDuplicatedDefinition(String, usize),
+ /// Missing a function name after the `fn` keyword.
+ FnMissingName,
+ /// A function definition is missing the parameters list. Wrapped value is the function name.
+ FnMissingParams(String),
+ /// A function definition has duplicated parameters. Wrapped values are the function name and
+ /// parameter name.
+ FnDuplicatedParam(String, String),
+ /// A function definition is missing the body. Wrapped value is the function name.
+ FnMissingBody(String),
+ /// Export statement not at global level.
+ WrongExport,
+ /// Assignment to an a constant variable. Wrapped value is the constant variable name.
+ AssignmentToConstant(String),
+ /// Assignment to an inappropriate LHS (left-hand-side) expression.
+ /// Wrapped value is the error message (if any).
+ AssignmentToInvalidLHS(String),
+ /// A variable is already defined.
+ ///
+ /// Only appears when variables shadowing is disabled.
+ VariableExists(String),
+ /// A variable is not found.
+ ///
+ /// Only appears when strict variables mode is enabled.
+ VariableUndefined(String),
+ /// An imported module is not found.
+ ///
+ /// Only appears when strict variables mode is enabled.
+ ModuleUndefined(String),
+ /// Expression exceeding the maximum levels of complexity.
+ ExprTooDeep,
+ /// Literal exceeding the maximum size. Wrapped values are the data type name and the maximum size.
+ LiteralTooLarge(String, usize),
+ /// Break statement not inside a loop.
+ LoopBreak,
+}
+
+impl ParseErrorType {
+ /// Make a [`ParseError`] using the current type and position.
+ #[cold]
+ #[inline(never)]
+ pub(crate) fn into_err(self, pos: Position) -> ParseError {
+ ParseError(self.into(), pos)
+ }
+}
+
+impl fmt::Display for ParseErrorType {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::BadInput(err) => write!(f, "{err}"),
+
+ Self::UnknownOperator(s) => write!(f, "Unknown operator: '{s}'"),
+
+ Self::MalformedCallExpr(s) if s.is_empty() => f.write_str(s),
+ Self::MalformedCallExpr(..) => f.write_str("Invalid expression in function call arguments"),
+
+ Self::MalformedIndexExpr(s) if s.is_empty() => f.write_str("Invalid index in indexing expression"),
+ Self::MalformedIndexExpr(s) => f.write_str(s),
+
+ Self::MalformedInExpr(s) if s.is_empty() => f.write_str("Invalid 'in' expression"),
+ Self::MalformedInExpr(s) => f.write_str(s),
+
+ Self::MalformedCapture(s) if s.is_empty() => f.write_str("Invalid capturing"),
+ Self::MalformedCapture(s) => f.write_str(s),
+
+ Self::FnDuplicatedDefinition(s, n) => {
+ write!(f, "Function {s} with ")?;
+ match n {
+ 0 => f.write_str("no parameters already exists"),
+ 1 => f.write_str("1 parameter already exists"),
+ _ => write!(f, "{n} parameters already exists"),
+ }
+ }
+
+ Self::FnMissingBody(s) if s.is_empty() => f.write_str("Expecting body statement block for anonymous function"),
+ Self::FnMissingBody(s) => write!(f, "Expecting body statement block for function {s}"),
+
+ Self::FnMissingParams(s) => write!(f, "Expecting parameters for function {s}"),
+ Self::FnDuplicatedParam(s, arg) => write!(f, "Duplicated parameter {arg} for function {s}"),
+
+ Self::DuplicatedProperty(s) => write!(f, "Duplicated property for object map literal: {s}"),
+ #[allow(deprecated)]
+ Self::DuplicatedSwitchCase => f.write_str("Duplicated switch case"),
+ Self::DuplicatedVariable(s) => write!(f, "Duplicated variable name: {s}"),
+
+ Self::VariableExists(s) => write!(f, "Variable already defined: {s}"),
+ Self::VariableUndefined(s) => write!(f, "Undefined variable: {s}"),
+ Self::ModuleUndefined(s) => write!(f, "Undefined module: {s}"),
+
+ Self::MismatchedType(r, a) => write!(f, "Expecting {r}, not {a}"),
+ Self::ExprExpected(s) => write!(f, "Expecting {s} expression"),
+ Self::MissingToken(token, s) => write!(f, "Expecting '{token}' {s}"),
+
+ Self::MissingSymbol(s) if s.is_empty() => f.write_str("Expecting a symbol"),
+ Self::MissingSymbol(s) => f.write_str(s),
+
+ Self::AssignmentToConstant(s) if s.is_empty() => f.write_str("Cannot assign to a constant value"),
+ Self::AssignmentToConstant(s) => write!(f, "Cannot assign to constant {s}"),
+
+ Self::AssignmentToInvalidLHS(s) if s.is_empty() => f.write_str("Expression cannot be assigned to"),
+ Self::AssignmentToInvalidLHS(s) => f.write_str(s),
+
+ Self::LiteralTooLarge(typ, max) => write!(f, "{typ} exceeds the maximum limit ({max})"),
+ Self::Reserved(s) if is_valid_identifier(s.as_str()) => write!(f, "'{s}' is a reserved keyword"),
+ Self::Reserved(s) => write!(f, "'{s}' is a reserved symbol"),
+ Self::UnexpectedEOF => f.write_str("Script is incomplete"),
+ Self::WrongSwitchIntegerCase => f.write_str("Numeric switch case cannot follow a range case"),
+ Self::WrongSwitchDefaultCase => f.write_str("Default switch case must be the last"),
+ Self::WrongSwitchCaseCondition => f.write_str("This switch case cannot have a condition"),
+ Self::PropertyExpected => f.write_str("Expecting name of a property"),
+ Self::VariableExpected => f.write_str("Expecting name of a variable"),
+ Self::ForbiddenVariable(s) => write!(f, "Forbidden variable name: {s}"),
+ Self::WrongFnDefinition => f.write_str("Function definitions must be at global level and cannot be inside a block or another function"),
+ Self::FnMissingName => f.write_str("Expecting function name in function declaration"),
+ Self::WrongDocComment => f.write_str("Doc-comment must be followed immediately by a function definition"),
+ Self::WrongExport => f.write_str("Export statement can only appear at global level"),
+ Self::ExprTooDeep => f.write_str("Expression exceeds maximum complexity"),
+ Self::LoopBreak => f.write_str("Break statement should only be used inside a loop"),
+ }
+ }
+}
+
+impl From<LexError> for ParseErrorType {
+ #[cold]
+ #[inline(never)]
+ fn from(err: LexError) -> Self {
+ match err {
+ LexError::StringTooLong(max) => {
+ Self::LiteralTooLarge("Length of string".to_string(), max)
+ }
+ _ => Self::BadInput(err),
+ }
+ }
+}
+
+/// Error when parsing a script.
+#[derive(Debug, Eq, PartialEq, Clone, Hash)]
+#[must_use]
+pub struct ParseError(
+ /// Parse error type.
+ pub Box<ParseErrorType>,
+ /// [Position] of the parse error.
+ pub Position,
+);
+
+impl Error for ParseError {}
+
+impl fmt::Display for ParseError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Display::fmt(&self.0, f)?;
+
+ // Do not write any position if None
+ if !self.1.is_none() {
+ write!(f, " ({})", self.1)?;
+ }
+
+ Ok(())
+ }
+}
+
+impl ParseError {
+ /// Get the [type][ParseErrorType] of this parse error.
+ #[cold]
+ #[inline(never)]
+ pub const fn err_type(&self) -> &ParseErrorType {
+ &self.0
+ }
+ /// Get the [position][Position] of this parse error.
+ #[cold]
+ #[inline(never)]
+ #[must_use]
+ pub const fn position(&self) -> Position {
+ self.1
+ }
+}
+
+impl From<ParseErrorType> for RhaiError {
+ #[cold]
+ #[inline(never)]
+ fn from(err: ParseErrorType) -> Self {
+ Self::new(err.into())
+ }
+}
+
+impl From<ParseErrorType> for ERR {
+ #[cold]
+ #[inline(never)]
+ fn from(err: ParseErrorType) -> Self {
+ Self::ErrorParsing(err, Position::NONE)
+ }
+}
+
+impl From<ParseError> for RhaiError {
+ #[cold]
+ #[inline(never)]
+ fn from(err: ParseError) -> Self {
+ Self::new(err.into())
+ }
+}
+
+impl From<ParseError> for ERR {
+ #[cold]
+ #[inline(never)]
+ fn from(err: ParseError) -> Self {
+ Self::ErrorParsing(*err.0, err.1)
+ }
+}
diff --git a/rhai/src/types/position.rs b/rhai/src/types/position.rs
new file mode 100644
index 0000000..8bdaaab
--- /dev/null
+++ b/rhai/src/types/position.rs
@@ -0,0 +1,282 @@
+//! Script character position type.
+#![cfg(not(feature = "no_position"))]
+
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ fmt,
+ ops::{Add, AddAssign},
+};
+
+/// A location (line number + character position) in the input script.
+///
+/// # Limitations
+///
+/// In order to keep footprint small, both line number and character position have 16-bit resolution,
+/// meaning they go up to a maximum of 65,535 lines and 65,535 characters per line.
+///
+/// Advancing beyond the maximum line length or maximum number of lines is not an error but has no effect.
+#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy)]
+pub struct Position {
+ /// Line number: 0 = none
+ line: u16,
+ /// Character position: 0 = BOL
+ pos: u16,
+}
+
+impl Position {
+ /// A [`Position`] representing no position.
+ pub const NONE: Self = Self { line: 0, pos: 0 };
+ /// A [`Position`] representing the first position.
+ pub const START: Self = Self { line: 1, pos: 0 };
+
+ /// Create a new [`Position`].
+ ///
+ /// `line` must not be zero.
+ ///
+ /// If `position` is zero, then it is at the beginning of a line.
+ ///
+ /// # Panics
+ ///
+ /// Panics if `line` is zero.
+ #[inline]
+ #[must_use]
+ pub const fn new(line: u16, position: u16) -> Self {
+ assert!(line != 0, "line cannot be zero");
+
+ let _pos = position;
+
+ Self { line, pos: _pos }
+ }
+ /// Get the line number (1-based), or [`None`] if there is no position.
+ ///
+ /// Always returns [`None`] under `no_position`.
+ #[inline]
+ #[must_use]
+ pub const fn line(self) -> Option<usize> {
+ if self.is_none() {
+ None
+ } else {
+ Some(self.line as usize)
+ }
+ }
+ /// Get the character position (1-based), or [`None`] if at beginning of a line.
+ ///
+ /// Always returns [`None`] under `no_position`.
+ #[inline]
+ #[must_use]
+ pub const fn position(self) -> Option<usize> {
+ if self.is_none() || self.pos == 0 {
+ None
+ } else {
+ Some(self.pos as usize)
+ }
+ }
+ /// Advance by one character position.
+ #[inline]
+ pub(crate) fn advance(&mut self) {
+ assert!(!self.is_none(), "cannot advance Position::none");
+
+ // Advance up to maximum position
+ self.pos = self.pos.saturating_add(1);
+ }
+ /// Go backwards by one character position.
+ ///
+ /// # Panics
+ ///
+ /// Panics if already at beginning of a line - cannot rewind to a previous line.
+ #[inline]
+ pub(crate) fn rewind(&mut self) {
+ assert!(!self.is_none(), "cannot rewind Position::none");
+ assert!(self.pos > 0, "cannot rewind at position 0");
+ self.pos -= 1;
+ }
+ /// Advance to the next line.
+ #[inline]
+ pub(crate) fn new_line(&mut self) {
+ assert!(!self.is_none(), "cannot advance Position::none");
+
+ // Advance up to maximum position
+ if self.line < u16::MAX {
+ self.line += 1;
+ self.pos = 0;
+ }
+ }
+ /// Is this [`Position`] at the beginning of a line?
+ ///
+ /// Always returns `false` under `no_position`.
+ #[inline]
+ #[must_use]
+ pub const fn is_beginning_of_line(self) -> bool {
+ self.pos == 0 && !self.is_none()
+ }
+ /// Is there no [`Position`]?
+ ///
+ /// Always returns `true` under `no_position`.
+ #[inline]
+ #[must_use]
+ pub const fn is_none(self) -> bool {
+ self.line == 0 && self.pos == 0
+ }
+ /// Returns an fallback [`Position`] if it is [`NONE`][Position::NONE]?
+ ///
+ /// Always returns the fallback under `no_position`.
+ #[inline]
+ #[must_use]
+ pub const fn or_else(self, pos: Self) -> Self {
+ if self.is_none() {
+ pos
+ } else {
+ self
+ }
+ }
+ /// Print this [`Position`] for debug purposes.
+ #[inline]
+ pub(crate) fn debug_print(self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if !self.is_none() {
+ write!(_f, " @ {self:?}")?;
+ }
+ Ok(())
+ }
+}
+
+impl Default for Position {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::START
+ }
+}
+
+impl fmt::Display for Position {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.is_none() {
+ write!(f, "none")
+ } else {
+ write!(f, "line {}, position {}", self.line, self.pos)
+ }
+ }
+}
+
+impl fmt::Debug for Position {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.is_none() {
+ f.write_str("none")
+ } else if self.is_beginning_of_line() {
+ write!(f, "{}", self.line)
+ } else {
+ write!(f, "{}:{}", self.line, self.pos)
+ }
+ }
+}
+
+impl Add for Position {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ if rhs.is_none() {
+ self
+ } else {
+ Self {
+ line: self.line + rhs.line - 1,
+ pos: if rhs.is_beginning_of_line() {
+ self.pos
+ } else {
+ self.pos + rhs.pos - 1
+ },
+ }
+ }
+ }
+}
+
+impl AddAssign for Position {
+ fn add_assign(&mut self, rhs: Self) {
+ *self = *self + rhs;
+ }
+}
+
+/// _(internals)_ A span consisting of a starting and an ending [positions][Position].
+/// Exported under the `internals` feature only.
+#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy)]
+pub struct Span {
+ /// Starting [position][Position].
+ start: Position,
+ /// Ending [position][Position].
+ end: Position,
+}
+
+impl Default for Span {
+ #[inline(always)]
+ #[must_use]
+ fn default() -> Self {
+ Self::NONE
+ }
+}
+
+impl Span {
+ /// Empty [`Span`].
+ pub const NONE: Self = Self::new(Position::NONE, Position::NONE);
+
+ /// Create a new [`Span`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn new(start: Position, end: Position) -> Self {
+ Self { start, end }
+ }
+ /// Is this [`Span`] non-existent?
+ ///
+ /// Always returns `true` under `no_position`.
+ #[inline]
+ #[must_use]
+ pub const fn is_none(&self) -> bool {
+ self.start.is_none() && self.end.is_none()
+ }
+ /// Get the [`Span`]'s starting [position][Position].
+ ///
+ /// Always returns [`Position::NONE`] under `no_position`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn start(&self) -> Position {
+ self.start
+ }
+ /// Get the [`Span`]'s ending [position][Position].
+ ///
+ /// Always returns [`Position::NONE`] under `no_position`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn end(&self) -> Position {
+ self.end
+ }
+}
+
+impl fmt::Display for Span {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let _f = f;
+
+ match (self.start(), self.end()) {
+ (Position::NONE, Position::NONE) => write!(_f, "{:?}", Position::NONE),
+ (Position::NONE, end) => write!(_f, "..{end:?}"),
+ (start, Position::NONE) => write!(_f, "{start:?}"),
+ (start, end) if start.line() != end.line() => {
+ write!(_f, "{start:?}-{end:?}")
+ }
+ (start, end) => write!(
+ _f,
+ "{}:{}-{}",
+ start.line().unwrap(),
+ start.position().unwrap_or(0),
+ end.position().unwrap_or(0)
+ ),
+ }
+ }
+}
+
+impl fmt::Debug for Span {
+ #[cold]
+ #[inline(never)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Display::fmt(self, f)
+ }
+}
diff --git a/rhai/src/types/position_none.rs b/rhai/src/types/position_none.rs
new file mode 100644
index 0000000..e6a65d1
--- /dev/null
+++ b/rhai/src/types/position_none.rs
@@ -0,0 +1,166 @@
+//! Placeholder script character position type.
+#![cfg(feature = "no_position")]
+#![allow(unused_variables)]
+
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ fmt,
+ ops::{Add, AddAssign},
+};
+
+/// A location (line number + character position) in the input script.
+#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, Default)]
+pub struct Position;
+
+impl Position {
+ /// A [`Position`] representing no position.
+ pub const NONE: Self = Self;
+ /// A [`Position`] representing the first position.
+ pub const START: Self = Self;
+
+ /// Create a new [`Position`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn new(line: u16, position: u16) -> Self {
+ Self
+ }
+ /// Get the line number (1-based), or [`None`] if there is no position.
+ ///
+ /// Always returns [`None`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn line(self) -> Option<usize> {
+ None
+ }
+ /// Get the character position (1-based), or [`None`] if at beginning of a line.
+ ///
+ /// Always returns [`None`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn position(self) -> Option<usize> {
+ None
+ }
+ /// Advance by one character position.
+ #[inline(always)]
+ pub(crate) fn advance(&mut self) {}
+ /// Go backwards by one character position.
+ #[inline(always)]
+ pub(crate) fn rewind(&mut self) {}
+ /// Advance to the next line.
+ #[inline(always)]
+ pub(crate) fn new_line(&mut self) {}
+ /// Is this [`Position`] at the beginning of a line?
+ ///
+ /// Always returns `false`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_beginning_of_line(self) -> bool {
+ false
+ }
+ /// Is there no [`Position`]?
+ ///
+ /// Always returns `true`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_none(self) -> bool {
+ true
+ }
+ /// Returns an fallback [`Position`] if it is [`NONE`][Position::NONE]?
+ ///
+ /// Always returns the fallback.
+ #[inline(always)]
+ #[must_use]
+ pub const fn or_else(self, pos: Self) -> Self {
+ pos
+ }
+ /// Print this [`Position`] for debug purposes.
+ #[inline(always)]
+ pub(crate) fn debug_print(self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ Ok(())
+ }
+}
+
+impl fmt::Display for Position {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "none")
+ }
+}
+
+impl fmt::Debug for Position {
+ #[cold]
+ #[inline(always)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str("none")
+ }
+}
+
+impl Add for Position {
+ type Output = Self;
+
+ #[inline(always)]
+ fn add(self, rhs: Self) -> Self::Output {
+ Self
+ }
+}
+
+impl AddAssign for Position {
+ #[inline(always)]
+ fn add_assign(&mut self, rhs: Self) {}
+}
+
+/// _(internals)_ A span consisting of a starting and an ending [positions][Position].
+/// Exported under the `internals` feature only.
+#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, Default)]
+pub struct Span;
+
+impl Span {
+ /// Empty [`Span`].
+ pub const NONE: Self = Self;
+
+ /// Create a new [`Span`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn new(start: Position, end: Position) -> Self {
+ Self
+ }
+ /// Is this [`Span`] non-existent?
+ ///
+ /// Always returns `true`.
+ #[inline(always)]
+ #[must_use]
+ pub const fn is_none(&self) -> bool {
+ true
+ }
+ /// Get the [`Span`]'s starting [position][Position].
+ ///
+ /// Always returns [`Position::NONE`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn start(&self) -> Position {
+ Position::NONE
+ }
+ /// Get the [`Span`]'s ending [position][Position].
+ ///
+ /// Always returns [`Position::NONE`].
+ #[inline(always)]
+ #[must_use]
+ pub const fn end(&self) -> Position {
+ Position::NONE
+ }
+}
+
+impl fmt::Display for Span {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let f = f;
+ write!(f, "{:?}", Position)
+ }
+}
+
+impl fmt::Debug for Span {
+ #[cold]
+ #[inline(always)]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Display::fmt(self, f)
+ }
+}
diff --git a/rhai/src/types/scope.rs b/rhai/src/types/scope.rs
new file mode 100644
index 0000000..8dc2e42
--- /dev/null
+++ b/rhai/src/types/scope.rs
@@ -0,0 +1,912 @@
+//! Module that defines the [`Scope`] type representing a function call-stack scope.
+
+use super::dynamic::{AccessMode, Variant};
+use crate::{Dynamic, Identifier, ImmutableString};
+use smallvec::SmallVec;
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+use std::{
+ fmt,
+ iter::{Extend, FromIterator},
+ marker::PhantomData,
+};
+
+/// Keep a number of entries inline (since [`Dynamic`] is usually small enough).
+pub const SCOPE_ENTRIES_INLINED: usize = 8;
+
+/// Type containing information about the current scope. Useful for keeping state between
+/// [`Engine`][crate::Engine] evaluation runs.
+///
+/// # Lifetime
+///
+/// Currently the lifetime parameter is not used, but it is not guaranteed to remain unused for
+/// future versions. Until then, `'static` can be used.
+///
+/// # Constant Generic Parameter
+///
+/// There is a constant generic parameter that indicates how many entries to keep inline.
+/// As long as the number of entries does not exceed this limit, no allocations occur.
+/// The default is 8.
+///
+/// A larger value makes [`Scope`] larger, but reduces the chance of allocations.
+///
+/// # Thread Safety
+///
+/// Currently, [`Scope`] is neither [`Send`] nor [`Sync`]. Turn on the `sync` feature to make it
+/// [`Send`] `+` [`Sync`].
+///
+/// # Example
+///
+/// ```
+/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
+/// use rhai::{Engine, Scope};
+///
+/// let engine = Engine::new();
+/// let mut my_scope = Scope::new();
+///
+/// my_scope.push("z", 40_i64);
+///
+/// engine.run_with_scope(&mut my_scope, "let x = z + 1; z = 0;")?;
+///
+/// let result: i64 = engine.eval_with_scope(&mut my_scope, "x + 1")?;
+///
+/// assert_eq!(result, 42);
+/// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 41);
+/// assert_eq!(my_scope.get_value::<i64>("z").expect("z should exist"), 0);
+/// # Ok(())
+/// # }
+/// ```
+///
+/// When searching for entries, newly-added entries are found before similarly-named but older
+/// entries, allowing for automatic _shadowing_.
+//
+// # Implementation Notes
+//
+// [`Scope`] is implemented as three arrays of exactly the same length. That's because variable
+// names take up the most space, with [`Identifier`] being three words long, but in the vast
+// majority of cases the name is NOT used to look up a variable. Variable lookup is usually via
+// direct indexing, by-passing the name altogether.
+//
+// [`Dynamic`] is reasonably small so packing it tightly improves cache performance.
+#[derive(Debug, Hash, Default)]
+pub struct Scope<'a, const N: usize = SCOPE_ENTRIES_INLINED> {
+ /// Current value of the entry.
+ values: SmallVec<[Dynamic; SCOPE_ENTRIES_INLINED]>,
+ /// Name of the entry.
+ names: SmallVec<[Identifier; SCOPE_ENTRIES_INLINED]>,
+ /// Aliases of the entry.
+ aliases: SmallVec<[Vec<ImmutableString>; SCOPE_ENTRIES_INLINED]>,
+ /// Phantom to keep the lifetime parameter in order not to break existing code.
+ dummy: PhantomData<&'a ()>,
+}
+
+impl fmt::Display for Scope<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ for (i, (name, constant, value)) in self.iter_raw().enumerate() {
+ #[cfg(not(feature = "no_closure"))]
+ let value_is_shared = if value.is_shared() { " (shared)" } else { "" };
+ #[cfg(feature = "no_closure")]
+ let value_is_shared = "";
+
+ writeln!(
+ f,
+ "[{}] {}{}{} = {:?}",
+ i + 1,
+ if constant { "const " } else { "" },
+ name,
+ value_is_shared,
+ *value.read_lock::<Dynamic>().unwrap(),
+ )?;
+ }
+
+ Ok(())
+ }
+}
+
+impl Clone for Scope<'_> {
+ #[inline]
+ fn clone(&self) -> Self {
+ Self {
+ values: self
+ .values
+ .iter()
+ .map(|v| {
+ // Also copy the value's access mode (otherwise will turn to read-write)
+ let mut v2 = v.clone();
+ v2.set_access_mode(v.access_mode());
+ v2
+ })
+ .collect(),
+ names: self.names.clone(),
+ aliases: self.aliases.clone(),
+ dummy: self.dummy,
+ }
+ }
+}
+
+impl IntoIterator for Scope<'_> {
+ type Item = (String, Dynamic, Vec<ImmutableString>);
+ type IntoIter = Box<dyn Iterator<Item = Self::Item>>;
+
+ #[must_use]
+ fn into_iter(self) -> Self::IntoIter {
+ Box::new(
+ self.values
+ .into_iter()
+ .zip(self.names.into_iter().zip(self.aliases.into_iter()))
+ .map(|(value, (name, alias))| (name.into(), value, alias)),
+ )
+ }
+}
+
+impl<'a> IntoIterator for &'a Scope<'_> {
+ type Item = (&'a Identifier, &'a Dynamic, &'a Vec<ImmutableString>);
+ type IntoIter = Box<dyn Iterator<Item = Self::Item> + 'a>;
+
+ #[must_use]
+ fn into_iter(self) -> Self::IntoIter {
+ Box::new(
+ self.values
+ .iter()
+ .zip(self.names.iter().zip(self.aliases.iter()))
+ .map(|(value, (name, alias))| (name, value, alias)),
+ )
+ }
+}
+
+impl Scope<'_> {
+ /// Create a new [`Scope`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push("x", 42_i64);
+ /// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 42);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub const fn new() -> Self {
+ Self {
+ values: SmallVec::new_const(),
+ names: SmallVec::new_const(),
+ aliases: SmallVec::new_const(),
+ dummy: PhantomData,
+ }
+ }
+ /// Create a new [`Scope`] with a particular capacity.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::with_capacity(10);
+ ///
+ /// my_scope.push("x", 42_i64);
+ /// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 42);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn with_capacity(capacity: usize) -> Self {
+ Self {
+ values: SmallVec::with_capacity(capacity),
+ names: SmallVec::with_capacity(capacity),
+ aliases: SmallVec::with_capacity(capacity),
+ dummy: PhantomData,
+ }
+ }
+ /// Empty the [`Scope`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push("x", 42_i64);
+ /// assert!(my_scope.contains("x"));
+ /// assert_eq!(my_scope.len(), 1);
+ /// assert!(!my_scope.is_empty());
+ ///
+ /// my_scope.clear();
+ /// assert!(!my_scope.contains("x"));
+ /// assert_eq!(my_scope.len(), 0);
+ /// assert!(my_scope.is_empty());
+ /// ```
+ #[inline(always)]
+ pub fn clear(&mut self) -> &mut Self {
+ self.names.clear();
+ self.values.clear();
+ self.aliases.clear();
+ self
+ }
+ /// Get the number of entries inside the [`Scope`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ /// assert_eq!(my_scope.len(), 0);
+ ///
+ /// my_scope.push("x", 42_i64);
+ /// assert_eq!(my_scope.len(), 1);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn len(&self) -> usize {
+ self.values.len()
+ }
+ /// Returns `true` if this [`Scope`] contains no variables.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ /// assert!(my_scope.is_empty());
+ ///
+ /// my_scope.push("x", 42_i64);
+ /// assert!(!my_scope.is_empty());
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ self.values.is_empty()
+ }
+ /// Add (push) a new entry to the [`Scope`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push("x", 42_i64);
+ /// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 42);
+ /// ```
+ #[inline(always)]
+ pub fn push(&mut self, name: impl Into<Identifier>, value: impl Variant + Clone) -> &mut Self {
+ self.push_entry(name, AccessMode::ReadWrite, Dynamic::from(value))
+ }
+ /// Add (push) a new [`Dynamic`] entry to the [`Scope`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::{Dynamic, Scope};
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push_dynamic("x", Dynamic::from(42_i64));
+ /// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 42);
+ /// ```
+ #[inline(always)]
+ pub fn push_dynamic(&mut self, name: impl Into<Identifier>, value: Dynamic) -> &mut Self {
+ self.push_entry(name, value.access_mode(), value)
+ }
+ /// Add (push) a new constant to the [`Scope`].
+ ///
+ /// Constants are immutable and cannot be assigned to. Their values never change.
+ /// Constants propagation is a technique used to optimize an [`AST`][crate::AST].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push_constant("x", 42_i64);
+ /// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 42);
+ /// ```
+ #[inline(always)]
+ pub fn push_constant(
+ &mut self,
+ name: impl Into<Identifier>,
+ value: impl Variant + Clone,
+ ) -> &mut Self {
+ self.push_entry(name, AccessMode::ReadOnly, Dynamic::from(value))
+ }
+ /// Add (push) a new constant with a [`Dynamic`] value to the Scope.
+ ///
+ /// Constants are immutable and cannot be assigned to. Their values never change.
+ /// Constants propagation is a technique used to optimize an [`AST`][crate::AST].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::{Dynamic, Scope};
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push_constant_dynamic("x", Dynamic::from(42_i64));
+ /// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 42);
+ /// ```
+ #[inline(always)]
+ pub fn push_constant_dynamic(
+ &mut self,
+ name: impl Into<Identifier>,
+ value: Dynamic,
+ ) -> &mut Self {
+ self.push_entry(name, AccessMode::ReadOnly, value)
+ }
+ /// Add (push) a new entry with a [`Dynamic`] value to the [`Scope`].
+ #[inline]
+ pub(crate) fn push_entry(
+ &mut self,
+ name: impl Into<Identifier>,
+ access: AccessMode,
+ mut value: Dynamic,
+ ) -> &mut Self {
+ self.names.push(name.into());
+ self.aliases.push(Vec::new());
+ value.set_access_mode(access);
+ self.values.push(value);
+ self
+ }
+ /// Remove the last entry from the [`Scope`].
+ ///
+ /// # Panics
+ ///
+ /// Panics is the [`Scope`] is empty.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push("x", 42_i64);
+ /// my_scope.push("y", 123_i64);
+ /// assert!(my_scope.contains("x"));
+ /// assert!(my_scope.contains("y"));
+ /// assert_eq!(my_scope.len(), 2);
+ ///
+ /// my_scope.pop();
+ /// assert!(my_scope.contains("x"));
+ /// assert!(!my_scope.contains("y"));
+ /// assert_eq!(my_scope.len(), 1);
+ ///
+ /// my_scope.pop();
+ /// assert!(!my_scope.contains("x"));
+ /// assert!(!my_scope.contains("y"));
+ /// assert_eq!(my_scope.len(), 0);
+ /// assert!(my_scope.is_empty());
+ /// ```
+ #[inline(always)]
+ pub fn pop(&mut self) -> &mut Self {
+ self.names.pop().expect("not empty");
+ let _ = self.values.pop().expect("not empty");
+ self.aliases.pop().expect("not empty");
+ self
+ }
+ /// Remove the last entry from the [`Scope`] and return it.
+ #[inline(always)]
+ #[allow(dead_code)]
+ pub(crate) fn pop_entry(&mut self) -> Option<(Identifier, Dynamic, Vec<ImmutableString>)> {
+ self.values.pop().map(|value| {
+ (
+ self.names.pop().expect("not empty"),
+ value,
+ self.aliases.pop().expect("not empty"),
+ )
+ })
+ }
+ /// Truncate (rewind) the [`Scope`] to a previous size.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push("x", 42_i64);
+ /// my_scope.push("y", 123_i64);
+ /// assert!(my_scope.contains("x"));
+ /// assert!(my_scope.contains("y"));
+ /// assert_eq!(my_scope.len(), 2);
+ ///
+ /// my_scope.rewind(1);
+ /// assert!(my_scope.contains("x"));
+ /// assert!(!my_scope.contains("y"));
+ /// assert_eq!(my_scope.len(), 1);
+ ///
+ /// my_scope.rewind(0);
+ /// assert!(!my_scope.contains("x"));
+ /// assert!(!my_scope.contains("y"));
+ /// assert_eq!(my_scope.len(), 0);
+ /// assert!(my_scope.is_empty());
+ /// ```
+ #[inline(always)]
+ pub fn rewind(&mut self, size: usize) -> &mut Self {
+ self.names.truncate(size);
+ self.values.truncate(size);
+ self.aliases.truncate(size);
+ self
+ }
+ /// Does the [`Scope`] contain the entry?
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push("x", 42_i64);
+ /// assert!(my_scope.contains("x"));
+ /// assert!(!my_scope.contains("y"));
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn contains(&self, name: &str) -> bool {
+ self.names.iter().any(|key| name == key)
+ }
+ /// Find an entry in the [`Scope`], starting from the last.
+ #[inline]
+ #[must_use]
+ pub(crate) fn search(&self, name: &str) -> Option<usize> {
+ let len = self.len();
+
+ self.names
+ .iter()
+ .rev() // Always search a Scope in reverse order
+ .enumerate()
+ .find_map(|(i, key)| {
+ if name == key {
+ let index = len - 1 - i;
+ Some(index)
+ } else {
+ None
+ }
+ })
+ }
+ /// Get the value of an entry in the [`Scope`], starting from the last.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push("x", 42_i64);
+ /// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 42);
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn get_value<T: Variant + Clone>(&self, name: &str) -> Option<T> {
+ let len = self.len();
+
+ self.names
+ .iter()
+ .rev()
+ .enumerate()
+ .find(|(.., key)| &name == key)
+ .map(|(index, ..)| self.values[len - 1 - index].flatten_clone())
+ .and_then(Dynamic::try_cast)
+ }
+ /// Check if the named entry in the [`Scope`] is constant.
+ ///
+ /// Search starts backwards from the last, stopping at the first entry matching the specified name.
+ ///
+ /// Returns [`None`] if no entry matching the specified name is found.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push_constant("x", 42_i64);
+ /// assert_eq!(my_scope.is_constant("x"), Some(true));
+ /// assert_eq!(my_scope.is_constant("y"), None);
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn is_constant(&self, name: &str) -> Option<bool> {
+ self.search(name)
+ .map(|n| match self.values[n].access_mode() {
+ AccessMode::ReadWrite => false,
+ AccessMode::ReadOnly => true,
+ })
+ }
+ /// Update the value of the named entry in the [`Scope`] if it already exists and is not constant.
+ /// Push a new entry with the value into the [`Scope`] if the name doesn't exist or if the
+ /// existing entry is constant.
+ ///
+ /// Search starts backwards from the last, and only the first entry matching the specified name is updated.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.set_or_push("x", 42_i64);
+ /// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 42);
+ /// assert_eq!(my_scope.len(), 1);
+ ///
+ /// my_scope.set_or_push("x", 0_i64);
+ /// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 0);
+ /// assert_eq!(my_scope.len(), 1);
+ ///
+ /// my_scope.set_or_push("y", 123_i64);
+ /// assert_eq!(my_scope.get_value::<i64>("y").expect("y should exist"), 123);
+ /// assert_eq!(my_scope.len(), 2);
+ /// ```
+ #[inline]
+ pub fn set_or_push(
+ &mut self,
+ name: impl AsRef<str> + Into<Identifier>,
+ value: impl Variant + Clone,
+ ) -> &mut Self {
+ match self
+ .search(name.as_ref())
+ .map(|n| (n, self.values[n].access_mode()))
+ {
+ None | Some((.., AccessMode::ReadOnly)) => {
+ self.push(name, value);
+ }
+ Some((index, AccessMode::ReadWrite)) => {
+ let value_ref = self.values.get_mut(index).unwrap();
+ *value_ref = Dynamic::from(value);
+ }
+ }
+ self
+ }
+ /// Update the value of the named entry in the [`Scope`].
+ ///
+ /// Search starts backwards from the last, and only the first entry matching the specified name is updated.
+ /// If no entry matching the specified name is found, a new one is added.
+ ///
+ /// # Panics
+ ///
+ /// Panics when trying to update the value of a constant.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push("x", 42_i64);
+ /// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 42);
+ ///
+ /// my_scope.set_value("x", 0_i64);
+ /// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 0);
+ /// ```
+ #[inline]
+ pub fn set_value(
+ &mut self,
+ name: impl AsRef<str> + Into<Identifier>,
+ value: impl Variant + Clone,
+ ) -> &mut Self {
+ match self
+ .search(name.as_ref())
+ .map(|n| (n, self.values[n].access_mode()))
+ {
+ None => {
+ self.push(name, value);
+ }
+ Some((.., AccessMode::ReadOnly)) => panic!("variable {} is constant", name.as_ref()),
+ Some((index, AccessMode::ReadWrite)) => {
+ let value_ref = self.values.get_mut(index).unwrap();
+ *value_ref = Dynamic::from(value);
+ }
+ }
+ self
+ }
+ /// Get a reference to an entry in the [`Scope`].
+ ///
+ /// If the entry by the specified name is not found, [`None`] is returned.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push("x", 42_i64);
+ ///
+ /// let value = my_scope.get("x").expect("x should exist");
+ ///
+ /// assert_eq!(value.as_int().unwrap(), 42);
+ ///
+ /// assert!(my_scope.get("z").is_none());
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn get(&self, name: &str) -> Option<&Dynamic> {
+ self.search(name).map(|index| &self.values[index])
+ }
+ /// Get a reference to an entry in the [`Scope`] based on the index.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the index is out of bounds.
+ #[inline(always)]
+ #[must_use]
+ #[allow(dead_code)]
+ pub(crate) fn get_entry_by_index(
+ &mut self,
+ index: usize,
+ ) -> (&Identifier, &Dynamic, &[ImmutableString]) {
+ (
+ &self.names[index],
+ &self.values[index],
+ &self.aliases[index],
+ )
+ }
+ /// Remove the last entry in the [`Scope`] by the specified name and return its value.
+ ///
+ /// If the entry by the specified name is not found, [`None`] is returned.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push("x", 123_i64); // first 'x'
+ /// my_scope.push("x", 42_i64); // second 'x', shadows first
+ ///
+ /// assert_eq!(my_scope.len(), 2);
+ ///
+ /// let value = my_scope.remove::<i64>("x").expect("x should exist");
+ ///
+ /// assert_eq!(value, 42);
+ ///
+ /// assert_eq!(my_scope.len(), 1);
+ ///
+ /// let value = my_scope.get_value::<i64>("x").expect("x should still exist");
+ ///
+ /// assert_eq!(value, 123);
+ /// ```
+ #[inline(always)]
+ #[must_use]
+ pub fn remove<T: Variant + Clone>(&mut self, name: &str) -> Option<T> {
+ self.search(name).and_then(|index| {
+ self.names.remove(index);
+ self.aliases.remove(index);
+ self.values.remove(index).try_cast()
+ })
+ }
+ /// Get a mutable reference to the value of an entry in the [`Scope`].
+ ///
+ /// If the entry by the specified name is not found, or if it is read-only,
+ /// [`None`] is returned.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::Scope;
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push("x", 42_i64);
+ /// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 42);
+ ///
+ /// let ptr = my_scope.get_mut("x").expect("x should exist");
+ /// *ptr = 123_i64.into();
+ ///
+ /// assert_eq!(my_scope.get_value::<i64>("x").expect("x should exist"), 123);
+ ///
+ /// my_scope.push_constant("z", 1_i64);
+ /// assert!(my_scope.get_mut("z").is_none());
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn get_mut(&mut self, name: &str) -> Option<&mut Dynamic> {
+ self.search(name)
+ .and_then(move |n| match self.values[n].access_mode() {
+ AccessMode::ReadWrite => Some(self.get_mut_by_index(n)),
+ AccessMode::ReadOnly => None,
+ })
+ }
+ /// Get a mutable reference to the value of an entry in the [`Scope`] based on the index.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the index is out of bounds.
+ #[inline(always)]
+ pub(crate) fn get_mut_by_index(&mut self, index: usize) -> &mut Dynamic {
+ &mut self.values[index]
+ }
+ /// Add an alias to an entry in the [`Scope`].
+ ///
+ /// # Panics
+ ///
+ /// Panics if the index is out of bounds.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ pub(crate) fn add_alias_by_index(&mut self, index: usize, alias: ImmutableString) -> &mut Self {
+ let aliases = self.aliases.get_mut(index).unwrap();
+ if aliases.is_empty() || !aliases.contains(&alias) {
+ aliases.push(alias);
+ }
+ self
+ }
+ /// Add an alias to a variable in the [`Scope`] so that it is exported under that name.
+ /// This is an advanced API.
+ ///
+ /// Variable aliases are used, for example, in [`Module::eval_ast_as_new`][crate::Module::eval_ast_as_new]
+ /// to create a new module with exported variables under different names.
+ ///
+ /// If the alias is empty, then the variable is exported under its original name.
+ ///
+ /// Multiple aliases can be added to any variable.
+ ///
+ /// Only the last variable matching the name (and not other shadowed versions) is aliased by this call.
+ #[cfg(not(feature = "no_module"))]
+ #[inline]
+ pub fn set_alias(
+ &mut self,
+ name: impl AsRef<str> + Into<Identifier>,
+ alias: impl Into<ImmutableString>,
+ ) {
+ if let Some(index) = self.search(name.as_ref()) {
+ let alias = match alias.into() {
+ x if x.is_empty() => name.into().into(),
+ x => x,
+ };
+ self.add_alias_by_index(index, alias);
+ }
+ }
+ /// Clone the [`Scope`], keeping only the last instances of each variable name.
+ /// Shadowed variables are omitted in the copy.
+ #[inline]
+ #[must_use]
+ pub fn clone_visible(&self) -> Self {
+ let len = self.len();
+ let mut scope = Self::new();
+
+ self.names.iter().rev().enumerate().for_each(|(i, name)| {
+ if scope.names.contains(name) {
+ return;
+ }
+
+ let v1 = &self.values[len - 1 - i];
+ let alias = &self.aliases[len - 1 - i];
+ let mut v2 = v1.clone();
+ v2.set_access_mode(v1.access_mode());
+
+ scope.names.push(name.clone());
+ scope.values.push(v2);
+ scope.aliases.push(alias.clone());
+ });
+
+ scope
+ }
+ /// Get an iterator to entries in the [`Scope`].
+ #[allow(dead_code)]
+ pub(crate) fn into_iter(
+ self,
+ ) -> impl Iterator<Item = (Identifier, Dynamic, Vec<ImmutableString>)> {
+ self.names
+ .into_iter()
+ .zip(self.values.into_iter().zip(self.aliases.into_iter()))
+ .map(|(name, (value, alias))| (name, value, alias))
+ }
+ /// Get an iterator to entries in the [`Scope`].
+ /// Shared values are flatten-cloned.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use rhai::{Dynamic, Scope};
+ ///
+ /// let mut my_scope = Scope::new();
+ ///
+ /// my_scope.push("x", 42_i64);
+ /// my_scope.push_constant("foo", "hello");
+ ///
+ /// let mut iter = my_scope.iter();
+ ///
+ /// let (name, is_constant, value) = iter.next().expect("value should exist");
+ /// assert_eq!(name, "x");
+ /// assert!(!is_constant);
+ /// assert_eq!(value.cast::<i64>(), 42);
+ ///
+ /// let (name, is_constant, value) = iter.next().expect("value should exist");
+ /// assert_eq!(name, "foo");
+ /// assert!(is_constant);
+ /// assert_eq!(value.cast::<String>(), "hello");
+ /// ```
+ #[inline]
+ pub fn iter(&self) -> impl Iterator<Item = (&str, bool, Dynamic)> {
+ self.iter_raw()
+ .map(|(name, constant, value)| (name, constant, value.flatten_clone()))
+ }
+ /// Get an iterator to entries in the [`Scope`].
+ /// Shared values are not expanded.
+ #[inline]
+ pub fn iter_raw(&self) -> impl Iterator<Item = (&str, bool, &Dynamic)> {
+ self.names
+ .iter()
+ .zip(self.values.iter())
+ .map(|(name, value)| (name.as_str(), value.is_read_only(), value))
+ }
+ /// Get a reverse iterator to entries in the [`Scope`].
+ /// Shared values are not expanded.
+ #[inline]
+ pub(crate) fn iter_rev_raw(&self) -> impl Iterator<Item = (&str, bool, &Dynamic)> {
+ self.names
+ .iter()
+ .rev()
+ .zip(self.values.iter().rev())
+ .map(|(name, value)| (name.as_str(), value.is_read_only(), value))
+ }
+ /// Remove a range of entries within the [`Scope`].
+ ///
+ /// # Panics
+ ///
+ /// Panics if the range is out of bounds.
+ #[inline]
+ #[allow(dead_code)]
+ pub(crate) fn remove_range(&mut self, start: usize, len: usize) {
+ self.values.drain(start..start + len).for_each(|_| {});
+ self.names.drain(start..start + len).for_each(|_| {});
+ self.aliases.drain(start..start + len).for_each(|_| {});
+ }
+}
+
+impl<K: Into<Identifier>> Extend<(K, Dynamic)> for Scope<'_> {
+ #[inline]
+ fn extend<T: IntoIterator<Item = (K, Dynamic)>>(&mut self, iter: T) {
+ for (name, value) in iter {
+ self.push_entry(name, AccessMode::ReadWrite, value);
+ }
+ }
+}
+
+impl<K: Into<Identifier>> FromIterator<(K, Dynamic)> for Scope<'_> {
+ #[inline]
+ fn from_iter<T: IntoIterator<Item = (K, Dynamic)>>(iter: T) -> Self {
+ let mut scope = Self::new();
+ scope.extend(iter);
+ scope
+ }
+}
+
+impl<K: Into<Identifier>> Extend<(K, bool, Dynamic)> for Scope<'_> {
+ #[inline]
+ fn extend<T: IntoIterator<Item = (K, bool, Dynamic)>>(&mut self, iter: T) {
+ for (name, is_constant, value) in iter {
+ self.push_entry(
+ name,
+ if is_constant {
+ AccessMode::ReadOnly
+ } else {
+ AccessMode::ReadWrite
+ },
+ value,
+ );
+ }
+ }
+}
+
+impl<K: Into<Identifier>> FromIterator<(K, bool, Dynamic)> for Scope<'_> {
+ #[inline]
+ fn from_iter<T: IntoIterator<Item = (K, bool, Dynamic)>>(iter: T) -> Self {
+ let mut scope = Self::new();
+ scope.extend(iter);
+ scope
+ }
+}
diff --git a/rhai/src/types/variant.rs b/rhai/src/types/variant.rs
new file mode 100644
index 0000000..0e1d4e6
--- /dev/null
+++ b/rhai/src/types/variant.rs
@@ -0,0 +1,105 @@
+//! [`Variant`] trait to to allow custom type handling.
+
+use crate::func::SendSync;
+use std::any::{type_name, Any, TypeId};
+#[cfg(feature = "no_std")]
+use std::prelude::v1::*;
+
+mod private {
+ use crate::func::SendSync;
+ use std::any::Any;
+
+ /// A sealed trait that prevents other crates from implementing [`Variant`][super::Variant].
+ pub trait Sealed {}
+
+ impl<T: Any + Clone + SendSync> Sealed for T {}
+}
+
+/// _(internals)_ Trait to represent any type.
+/// Exported under the `internals` feature only.
+///
+/// This trait is sealed and cannot be implemented.
+///
+/// Currently, [`Variant`] is not [`Send`] nor [`Sync`], so it can practically be any type.
+/// Turn on the `sync` feature to restrict it to only types that implement [`Send`] `+` [`Sync`].
+#[cfg(not(feature = "sync"))]
+pub trait Variant: Any + private::Sealed {
+ /// Convert this [`Variant`] trait object to [`&dyn Any`][Any].
+ #[must_use]
+ fn as_any(&self) -> &dyn Any;
+
+ /// Convert this [`Variant`] trait object to [`&mut dyn Any`][Any].
+ #[must_use]
+ fn as_any_mut(&mut self) -> &mut dyn Any;
+
+ /// Convert this [`Variant`] trait object to [`Box<dyn Any>`][Any].
+ #[must_use]
+ fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
+
+ /// Get the name of this type.
+ #[must_use]
+ fn type_name(&self) -> &'static str;
+
+ /// Clone this [`Variant`] trait object.
+ #[must_use]
+ fn clone_object(&self) -> Box<dyn Variant>;
+}
+
+/// _(internals)_ Trait to represent any type.
+/// Exported under the `internals` feature only.
+///
+/// This trait is sealed and cannot be implemented.
+#[cfg(feature = "sync")]
+pub trait Variant: Any + Send + Sync + private::Sealed {
+ /// Convert this [`Variant`] trait object to [`&dyn Any`][Any].
+ #[must_use]
+ fn as_any(&self) -> &dyn Any;
+
+ /// Convert this [`Variant`] trait object to [`&mut dyn Any`][Any].
+ #[must_use]
+ fn as_any_mut(&mut self) -> &mut dyn Any;
+
+ /// Convert this [`Variant`] trait object to [`Box<dyn Any>`][Any].
+ #[must_use]
+ fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
+
+ /// Get the name of this type.
+ #[must_use]
+ fn type_name(&self) -> &'static str;
+
+ /// Clone this [`Variant`] trait object.
+ #[must_use]
+ fn clone_object(&self) -> Box<dyn Variant>;
+}
+
+impl<T: Any + Clone + SendSync> Variant for T {
+ #[inline(always)]
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+ #[inline(always)]
+ fn as_any_mut(&mut self) -> &mut dyn Any {
+ self
+ }
+ #[inline(always)]
+ fn as_boxed_any(self: Box<Self>) -> Box<dyn Any> {
+ self
+ }
+ #[inline(always)]
+ fn type_name(&self) -> &'static str {
+ type_name::<T>()
+ }
+ #[inline(always)]
+ fn clone_object(&self) -> Box<dyn Variant> {
+ Box::new(self.clone()) as Box<dyn Variant>
+ }
+}
+
+impl dyn Variant {
+ /// Is this [`Variant`] a specific type?
+ #[inline(always)]
+ #[must_use]
+ pub fn is<T: Any>(&self) -> bool {
+ TypeId::of::<T>() == self.type_id()
+ }
+}
diff --git a/rhai/tests/README.md b/rhai/tests/README.md
new file mode 100644
index 0000000..c5b5e04
--- /dev/null
+++ b/rhai/tests/README.md
@@ -0,0 +1,12 @@
+Tests
+=====
+
+Rhai engine tests.
+
+
+How to Run
+----------
+
+```bash
+cargo test
+```
diff --git a/rhai/tests/arrays.rs b/rhai/tests/arrays.rs
new file mode 100644
index 0000000..d8b24f4
--- /dev/null
+++ b/rhai/tests/arrays.rs
@@ -0,0 +1,561 @@
+#![cfg(not(feature = "no_index"))]
+use rhai::{Array, Dynamic, Engine, EvalAltResult, ParseErrorType, INT};
+use std::iter::FromIterator;
+
+#[test]
+fn test_arrays() -> Result<(), Box<EvalAltResult>> {
+ let a = Array::from_iter([(42 as INT).into()]);
+
+ assert_eq!(a[0].clone_cast::<INT>(), 42);
+
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("let x = [1, 2, 3]; x[1]")?, 2);
+ assert_eq!(engine.eval::<INT>("let x = [1, 2, 3,]; x[1]")?, 2);
+ assert_eq!(engine.eval::<INT>("let y = [1, 2, 3]; y[1] = 5; y[1]")?, 5);
+ assert_eq!(
+ engine.eval::<char>(r#"let y = [1, [ 42, 88, "93" ], 3]; y[1][2][1]"#)?,
+ '3'
+ );
+ assert_eq!(engine.eval::<INT>("let y = [1, 2, 3]; y[0]")?, 1);
+ assert_eq!(engine.eval::<INT>("let y = [1, 2, 3]; y[-1]")?, 3);
+ assert_eq!(engine.eval::<INT>("let y = [1, 2, 3]; y[-3]")?, 1);
+ assert!(engine.eval::<bool>("let y = [1, 2, 3]; 2 in y")?);
+ assert!(engine.eval::<bool>("let y = [1, 2, 3]; 42 !in y")?);
+ assert_eq!(engine.eval::<INT>("let y = [1, 2, 3]; y += 4; y[3]")?, 4);
+ assert_eq!(
+ engine.eval::<INT>("let y = [1, 2, 3]; pad(y, 5, 42); len(y)")?,
+ 5
+ );
+ assert_eq!(
+ engine.eval::<INT>("let y = [1, 2, 3]; pad(y, 5, [42]); len(y)")?,
+ 5
+ );
+ assert_eq!(
+ engine.eval::<INT>("let y = [1, 2, 3]; pad(y, 5, [42, 999, 123]); y[4][0]")?,
+ 42
+ );
+ assert_eq!(
+ engine
+ .eval::<Dynamic>("let y = [1, 2, 3]; y[1] += 4; y")?
+ .into_typed_array::<INT>()?,
+ [1, 6, 3]
+ );
+ assert_eq!(
+ engine
+ .eval::<Dynamic>("let y = [1, 2, 3]; extract(y, 1, 10)")?
+ .into_typed_array::<INT>()?,
+ vec![2, 3]
+ );
+ assert_eq!(
+ engine
+ .eval::<Dynamic>("let y = [1, 2, 3]; extract(y, -3, 1)")?
+ .into_typed_array::<INT>()?,
+ vec![1]
+ );
+ assert_eq!(
+ engine
+ .eval::<Dynamic>("let y = [1, 2, 3]; extract(y, -99, 2)")?
+ .into_typed_array::<INT>()?,
+ vec![1, 2]
+ );
+ assert_eq!(
+ engine
+ .eval::<Dynamic>("let y = [1, 2, 3]; extract(y, 99, 1)")?
+ .into_typed_array::<INT>()?,
+ vec![] as Vec<INT>
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ {
+ assert_eq!(
+ engine
+ .eval::<Dynamic>("let y = [1, 2, 3]; y.push(4); y")?
+ .into_typed_array::<INT>()?,
+ [1, 2, 3, 4]
+ );
+ assert_eq!(
+ engine
+ .eval::<Dynamic>("let y = [1, 2, 3]; y.insert(0, 4); y")?
+ .into_typed_array::<INT>()?,
+ [4, 1, 2, 3]
+ );
+ assert_eq!(
+ engine
+ .eval::<Dynamic>("let y = [1, 2, 3]; y.insert(999, 4); y")?
+ .into_typed_array::<INT>()?,
+ [1, 2, 3, 4]
+ );
+ assert_eq!(
+ engine
+ .eval::<Dynamic>("let y = [1, 2, 3]; y.insert(-2, 4); y")?
+ .into_typed_array::<INT>()?,
+ [1, 4, 2, 3]
+ );
+ assert_eq!(
+ engine
+ .eval::<Dynamic>("let y = [1, 2, 3]; y.insert(-999, 4); y")?
+ .into_typed_array::<INT>()?,
+ [4, 1, 2, 3]
+ );
+ assert_eq!(
+ engine.eval::<INT>("let y = [1, 2, 3]; let z = [42]; y[z.len]")?,
+ 2
+ );
+ assert_eq!(
+ engine.eval::<INT>("let y = [1, 2, [3, 4, 5, 6]]; let z = [42]; y[2][z.len]")?,
+ 4
+ );
+ assert_eq!(
+ engine.eval::<INT>("let y = [1, 2, 3]; let z = [2]; y[z[0]]")?,
+ 3
+ );
+
+ assert_eq!(
+ engine
+ .eval::<Dynamic>(
+ "
+ let x = [2, 9];
+ x.insert(-1, 1);
+ x.insert(999, 3);
+ x.insert(-9, 99);
+
+ let r = x.remove(2);
+
+ let y = [4, 5];
+ x.append(y);
+
+ x
+ "
+ )?
+ .into_typed_array::<INT>()?,
+ [99, 2, 9, 3, 4, 5]
+ );
+ }
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = #{ foo: 42 };
+ let n = 0;
+ let a = [[x]];
+ let i = [n];
+ a[n][i[n]].foo
+ "
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine
+ .eval::<Dynamic>(
+ "
+ let x = [1, 2, 3];
+ x += [4, 5];
+ x
+ "
+ )?
+ .into_typed_array::<INT>()?,
+ [1, 2, 3, 4, 5]
+ );
+ assert_eq!(
+ engine
+ .eval::<Dynamic>(
+ "
+ let x = [1, 2, 3];
+ let y = [4, 5];
+ x + y
+ "
+ )?
+ .into_typed_array::<INT>()?,
+ [1, 2, 3, 4, 5]
+ );
+ #[cfg(not(feature = "no_closure"))]
+ assert!(!engine.eval::<bool>(
+ "
+ let x = 42;
+ let y = [];
+ let f = || x;
+ for n in 0..10 {
+ y += x;
+ }
+ some(y, |x| is_shared(x))
+ "
+ )?);
+
+ let value = vec![
+ String::from("hello"),
+ String::from("world"),
+ String::from("foo"),
+ String::from("bar"),
+ ];
+
+ let array: Dynamic = value.into();
+
+ assert_eq!(array.type_name(), "array");
+
+ let array = array.cast::<Array>();
+
+ assert_eq!(array[0].type_name(), "string");
+ assert_eq!(array.len(), 4);
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_float"))]
+#[cfg(not(feature = "no_object"))]
+#[test]
+fn test_array_chaining() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!(engine.eval::<bool>(
+ "
+ let v = [ PI() ];
+ ( v[0].cos() ).sin() == v[0].cos().sin()
+ "
+ )?);
+
+ Ok(())
+}
+
+#[test]
+fn test_array_index_types() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ engine.compile("[1, 2, 3][0]['x']")?;
+
+ assert!(matches!(
+ engine.compile("[1, 2, 3]['x']").unwrap_err().err_type(),
+ ParseErrorType::MalformedIndexExpr(..)
+ ));
+
+ #[cfg(not(feature = "no_float"))]
+ assert!(matches!(
+ engine.compile("[1, 2, 3][123.456]").unwrap_err().err_type(),
+ ParseErrorType::MalformedIndexExpr(..)
+ ));
+
+ assert!(matches!(
+ engine.compile("[1, 2, 3][()]").unwrap_err().err_type(),
+ ParseErrorType::MalformedIndexExpr(..)
+ ));
+
+ assert!(matches!(
+ engine
+ .compile(r#"[1, 2, 3]["hello"]"#)
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::MalformedIndexExpr(..)
+ ));
+
+ assert!(matches!(
+ engine
+ .compile("[1, 2, 3][true && false]")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::MalformedIndexExpr(..)
+ ));
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_array_with_structs() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Clone)]
+ struct TestStruct {
+ x: INT,
+ }
+
+ impl TestStruct {
+ fn update(&mut self) {
+ self.x += 1000;
+ }
+
+ fn get_x(&mut self) -> INT {
+ self.x
+ }
+
+ fn set_x(&mut self, new_x: INT) {
+ self.x = new_x;
+ }
+
+ fn new() -> Self {
+ Self { x: 1 }
+ }
+ }
+
+ let mut engine = Engine::new();
+
+ engine.register_type::<TestStruct>();
+
+ engine.register_get_set("x", TestStruct::get_x, TestStruct::set_x);
+ engine.register_fn("update", TestStruct::update);
+ engine.register_fn("new_ts", TestStruct::new);
+
+ assert_eq!(engine.eval::<INT>("let a = [new_ts()]; a[0].x")?, 1);
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let a = [new_ts()];
+ a[0].x = 100;
+ a[0].update();
+ a[0].x
+ "
+ )?,
+ 1100
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_object"))]
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_closure"))]
+#[test]
+fn test_arrays_map_reduce() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("[1].map(|x| x + 41)[0]")?, 42);
+ assert_eq!(engine.eval::<INT>("[1].map(|| this + 41)[0]")?, 42);
+ assert_eq!(
+ engine.eval::<INT>("let x = [1, 2, 3]; x.for_each(|| this += 41); x[0]")?,
+ 42
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = [1, 2, 3];
+ let sum = 0;
+ let factor = 2;
+ x.for_each(|| sum += this * factor);
+ sum
+ "
+ )?,
+ 12
+ );
+ assert_eq!(engine.eval::<INT>("([1].map(|x| x + 41))[0]")?, 42);
+ assert_eq!(
+ engine.eval::<INT>("let c = 40; let y = 1; [1].map(|x, i| c + x + y + i)[0]")?,
+ 42
+ );
+ assert_eq!(
+ engine.eval::<INT>("let x = [1, 2, 3]; x.for_each(|i| this += i); x[2]")?,
+ 5
+ );
+
+ assert_eq!(
+ engine
+ .eval::<Dynamic>(
+ "
+ let x = [1, 2, 3];
+ x.filter(|v| v > 2)
+ "
+ )?
+ .into_typed_array::<INT>()?,
+ [3]
+ );
+
+ assert_eq!(
+ engine
+ .eval::<Dynamic>(
+ "
+ let x = [1, 2, 3];
+ x.filter(|| this > 2)
+ "
+ )?
+ .into_typed_array::<INT>()?,
+ [3]
+ );
+
+ assert_eq!(
+ engine
+ .eval::<Dynamic>(
+ "
+ let x = [1, 2, 3];
+ x.filter(|v, i| v > i)
+ "
+ )?
+ .into_typed_array::<INT>()?,
+ [1, 2, 3]
+ );
+
+ assert_eq!(
+ engine
+ .eval::<Dynamic>(
+ "
+ let x = [1, 2, 3];
+ x.map(|v| v * 2)
+ "
+ )?
+ .into_typed_array::<INT>()?,
+ [2, 4, 6]
+ );
+
+ assert_eq!(
+ engine
+ .eval::<Dynamic>(
+ "
+ let x = [1, 2, 3];
+ x.map(|| this * 2)
+ "
+ )?
+ .into_typed_array::<INT>()?,
+ [2, 4, 6]
+ );
+
+ assert_eq!(
+ engine
+ .eval::<Dynamic>(
+ "
+ let x = [1, 2, 3];
+ x.map(|v, i| v * i)
+ "
+ )?
+ .into_typed_array::<INT>()?,
+ [0, 2, 6]
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let x = [1, 2, 3];
+ x.reduce(|sum, v| if sum.type_of() == "()" { v * v } else { sum + v * v })
+ "#
+ )?,
+ 14
+ );
+
+ // assert_eq!(
+ // engine.eval::<INT>(
+ // "
+ // let x = [1, 2, 3];
+ // x.reduce(|sum, v, i| {
+ // if i == 0 { sum = 10 }
+ // sum + v * v
+ // })
+ // "
+ // )?,
+ // 24
+ // );
+
+ // assert_eq!(
+ // engine.eval::<INT>(
+ // "
+ // let x = [1, 2, 3];
+ // x.reduce(|sum, i| {
+ // if i == 0 { sum = 10 }
+ // sum + this * this
+ // })
+ // "
+ // )?,
+ // 24
+ // );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let x = [1, 2, 3];
+ x.reduce_rev(|sum, v| if sum.type_of() == "()" { v * v } else { sum + v * v })
+ "#
+ )?,
+ 14
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = [1, 2, 3];
+ x.reduce_rev(|sum, v, i| { if i == 2 { sum = 10 } sum + v * v })
+ "
+ )?,
+ 24
+ );
+
+ assert!(engine.eval::<bool>(
+ "
+ let x = [1, 2, 3];
+ x.some(|v| v > 1)
+ "
+ )?);
+
+ assert!(engine.eval::<bool>(
+ "
+ let x = [1, 2, 3];
+ x.some(|v, i| v * i == 0)
+ "
+ )?);
+
+ assert!(!engine.eval::<bool>(
+ "
+ let x = [1, 2, 3];
+ x.all(|v| v > 1)
+ "
+ )?);
+
+ assert!(engine.eval::<bool>(
+ "
+ let x = [1, 2, 3];
+ x.all(|v, i| v > i)
+ "
+ )?);
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = [1, 2, 3];
+ x.find(|v| v > 2)
+ "
+ )?,
+ 3
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = [1, 2, 3];
+ x.find(|v, i| v * i == 6)
+ "
+ )?,
+ 3
+ );
+
+ engine.eval::<()>(
+ "
+ let x = [1, 2, 3, 2, 1];
+ x.find(|v| v > 4)
+ ",
+ )?;
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = [#{alice: 1}, #{bob: 2}, #{clara: 3}];
+ x.find_map(|v| v.bob)
+ "
+ )?,
+ 2
+ );
+
+ engine.eval::<()>(
+ "
+ let x = [#{alice: 1}, #{bob: 2}, #{clara: 3}];
+ x.find_map(|v| v.dave)
+ ",
+ )?;
+
+ Ok(())
+}
+
+#[test]
+fn test_arrays_elvis() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ engine.eval::<()>("let x = (); x?[2]")?;
+
+ engine.run("let x = (); x?[2] = 42")?;
+
+ Ok(())
+}
diff --git a/rhai/tests/assignments.rs b/rhai/tests/assignments.rs
new file mode 100644
index 0000000..0884684
--- /dev/null
+++ b/rhai/tests/assignments.rs
@@ -0,0 +1,67 @@
+use rhai::{Engine, EvalAltResult, ParseErrorType, INT};
+
+#[test]
+fn test_assignments() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("let x = 42; x = 123; x")?, 123);
+ assert_eq!(engine.eval::<INT>("let x = 42; x += 123; x")?, 165);
+
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(engine.eval::<INT>("let x = [42]; x[0] += 123; x[0]")?, 165);
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(engine.eval::<INT>("let x = #{a:42}; x.a += 123; x.a")?, 165);
+
+ Ok(())
+}
+
+#[test]
+fn test_assignments_bad_lhs() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ *engine.compile("(x+y) = 42;").unwrap_err().err_type(),
+ ParseErrorType::AssignmentToInvalidLHS(String::new())
+ );
+ assert_eq!(
+ *engine.compile("foo(x) = 42;").unwrap_err().err_type(),
+ ParseErrorType::AssignmentToInvalidLHS(String::new())
+ );
+ assert_eq!(
+ *engine.compile("true = 42;").unwrap_err().err_type(),
+ ParseErrorType::AssignmentToConstant(String::new())
+ );
+ assert_eq!(
+ *engine.compile("123 = 42;").unwrap_err().err_type(),
+ ParseErrorType::AssignmentToConstant(String::new())
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ {
+ assert_eq!(
+ *engine.compile("x.foo() = 42;").unwrap_err().err_type(),
+ ParseErrorType::AssignmentToInvalidLHS(String::new())
+ );
+ assert_eq!(
+ *engine.compile("x.foo().x.y = 42;").unwrap_err().err_type(),
+ ParseErrorType::AssignmentToInvalidLHS(String::new())
+ );
+ assert_eq!(
+ *engine.compile("x.y.z.foo() = 42;").unwrap_err().err_type(),
+ ParseErrorType::AssignmentToInvalidLHS(String::new())
+ );
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ *engine.compile("x.foo()[0] = 42;").unwrap_err().err_type(),
+ ParseErrorType::AssignmentToInvalidLHS(String::new())
+ );
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ *engine.compile("x[y].z.foo() = 42;").unwrap_err().err_type(),
+ ParseErrorType::AssignmentToInvalidLHS(String::new())
+ );
+ }
+
+ Ok(())
+}
diff --git a/rhai/tests/binary_ops.rs b/rhai/tests/binary_ops.rs
new file mode 100644
index 0000000..6d0a9d7
--- /dev/null
+++ b/rhai/tests/binary_ops.rs
@@ -0,0 +1,149 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_binary_ops() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("10 + 4")?, 14);
+ assert_eq!(engine.eval::<INT>("10 - 4")?, 6);
+ assert_eq!(engine.eval::<INT>("10 * 4")?, 40);
+ assert_eq!(engine.eval::<INT>("10 / 4")?, 2);
+ assert_eq!(engine.eval::<INT>("10 % 4")?, 2);
+ assert_eq!(engine.eval::<INT>("10 ** 4")?, 10000);
+ assert_eq!(engine.eval::<INT>("10 << 4")?, 160);
+ assert_eq!(engine.eval::<INT>("10 >> 4")?, 0);
+ assert_eq!(engine.eval::<INT>("10 & 4")?, 0);
+ assert_eq!(engine.eval::<INT>("10 | 4")?, 14);
+ assert_eq!(engine.eval::<INT>("10 ^ 4")?, 14);
+
+ assert!(engine.eval::<bool>("42 == 42")?);
+ assert!(!engine.eval::<bool>("42 != 42")?);
+ assert!(!engine.eval::<bool>("42 > 42")?);
+ assert!(engine.eval::<bool>("42 >= 42")?);
+ assert!(!engine.eval::<bool>("42 < 42")?);
+ assert!(engine.eval::<bool>("42 <= 42")?);
+
+ assert_eq!(engine.eval::<INT>("let x = 10; x += 4; x")?, 14);
+ assert_eq!(engine.eval::<INT>("let x = 10; x -= 4; x")?, 6);
+ assert_eq!(engine.eval::<INT>("let x = 10; x *= 4; x")?, 40);
+ assert_eq!(engine.eval::<INT>("let x = 10; x /= 4; x")?, 2);
+ assert_eq!(engine.eval::<INT>("let x = 10; x %= 4; x")?, 2);
+ assert_eq!(engine.eval::<INT>("let x = 10; x **= 4; x")?, 10000);
+ assert_eq!(engine.eval::<INT>("let x = 10; x <<= 4; x")?, 160);
+ assert_eq!(engine.eval::<INT>("let x = 10; x <<= -1; x")?, 5);
+ assert_eq!(engine.eval::<INT>("let x = 10; x >>= 4; x")?, 0);
+ assert_eq!(engine.eval::<INT>("let x = 10; x >>= -2; x")?, 40);
+ assert_eq!(engine.eval::<INT>("let x = 10; x &= 4; x")?, 0);
+ assert_eq!(engine.eval::<INT>("let x = 10; x |= 4; x")?, 14);
+ assert_eq!(engine.eval::<INT>("let x = 10; x ^= 4; x")?, 14);
+
+ #[cfg(not(feature = "no_float"))]
+ {
+ use rhai::FLOAT;
+
+ assert_eq!(engine.eval::<FLOAT>("10.0 + 4.0")?, 14.0);
+ assert_eq!(engine.eval::<FLOAT>("10.0 - 4.0")?, 6.0);
+ assert_eq!(engine.eval::<FLOAT>("10.0 * 4.0")?, 40.0);
+ assert_eq!(engine.eval::<FLOAT>("10.0 / 4.0")?, 2.5);
+ assert_eq!(engine.eval::<FLOAT>("10.0 % 4.0")?, 2.0);
+ assert_eq!(engine.eval::<FLOAT>("10.0 ** 4.0")?, 10000.0);
+
+ assert_eq!(engine.eval::<FLOAT>("10.0 + 4")?, 14.0);
+ assert_eq!(engine.eval::<FLOAT>("10.0 - 4")?, 6.0);
+ assert_eq!(engine.eval::<FLOAT>("10.0 * 4")?, 40.0);
+ assert_eq!(engine.eval::<FLOAT>("10.0 / 4")?, 2.5);
+ assert_eq!(engine.eval::<FLOAT>("10.0 % 4")?, 2.0);
+ assert_eq!(engine.eval::<FLOAT>("10.0 ** 4")?, 10000.0);
+
+ assert_eq!(engine.eval::<FLOAT>("10 + 4.0")?, 14.0);
+ assert_eq!(engine.eval::<FLOAT>("10 - 4.0")?, 6.0);
+ assert_eq!(engine.eval::<FLOAT>("10 * 4.0")?, 40.0);
+ assert_eq!(engine.eval::<FLOAT>("10 / 4.0")?, 2.5);
+ assert_eq!(engine.eval::<FLOAT>("10 % 4.0")?, 2.0);
+ assert_eq!(engine.eval::<FLOAT>("10 ** 4.0")?, 10000.0);
+
+ assert!(engine.eval::<bool>("42 == 42.0")?);
+ assert!(!engine.eval::<bool>("42 != 42.0")?);
+ assert!(!engine.eval::<bool>("42 > 42.0")?);
+ assert!(engine.eval::<bool>("42 >= 42.0")?);
+ assert!(!engine.eval::<bool>("42 < 42.0")?);
+ assert!(engine.eval::<bool>("42 <= 42.0")?);
+
+ assert!(engine.eval::<bool>("42.0 == 42")?);
+ assert!(!engine.eval::<bool>("42.0 != 42")?);
+ assert!(!engine.eval::<bool>("42.0 > 42")?);
+ assert!(engine.eval::<bool>("42.0 >= 42")?);
+ assert!(!engine.eval::<bool>("42.0 < 42")?);
+ assert!(engine.eval::<bool>("42.0 <= 42")?);
+
+ assert_eq!(engine.eval::<FLOAT>("let x = 10.0; x += 4.0; x")?, 14.0);
+ assert_eq!(engine.eval::<FLOAT>("let x = 10.0; x -= 4.0; x")?, 6.0);
+ assert_eq!(engine.eval::<FLOAT>("let x = 10.0; x *= 4.0; x")?, 40.0);
+ assert_eq!(engine.eval::<FLOAT>("let x = 10.0; x /= 4.0; x")?, 2.5);
+ assert_eq!(engine.eval::<FLOAT>("let x = 10.0; x %= 4.0; x")?, 2.0);
+ assert_eq!(engine.eval::<FLOAT>("let x = 10.0; x **= 4.0; x")?, 10000.0);
+
+ assert_eq!(engine.eval::<FLOAT>("let x = 10.0; x += 4; x")?, 14.0);
+ assert_eq!(engine.eval::<FLOAT>("let x = 10.0; x -= 4; x")?, 6.0);
+ assert_eq!(engine.eval::<FLOAT>("let x = 10.0; x *= 4; x")?, 40.0);
+ assert_eq!(engine.eval::<FLOAT>("let x = 10.0; x /= 4; x")?, 2.5);
+ assert_eq!(engine.eval::<FLOAT>("let x = 10.0; x %= 4; x")?, 2.0);
+ assert_eq!(engine.eval::<FLOAT>("let x = 10.0; x **= 4; x")?, 10000.0);
+ }
+
+ assert_eq!(
+ engine.eval::<String>(r#""hello" + ", world""#)?,
+ "hello, world"
+ );
+ assert_eq!(engine.eval::<String>(r#""hello" + '!'"#)?, "hello!");
+ assert_eq!(engine.eval::<String>(r#""hello" - "el""#)?, "hlo");
+ assert_eq!(engine.eval::<String>(r#""hello" - 'l'"#)?, "heo");
+
+ assert!(!engine.eval::<bool>(r#""a" == "x""#)?);
+ assert!(engine.eval::<bool>(r#""a" != "x""#)?);
+ assert!(!engine.eval::<bool>(r#""a" > "x""#)?);
+ assert!(!engine.eval::<bool>(r#""a" >= "x""#)?);
+ assert!(engine.eval::<bool>(r#""a" < "x""#)?);
+ assert!(engine.eval::<bool>(r#""a" <= "x""#)?);
+
+ assert!(engine.eval::<bool>(r#""x" == 'x'"#)?);
+ assert!(!engine.eval::<bool>(r#""x" != 'x'"#)?);
+ assert!(!engine.eval::<bool>(r#""x" > 'x'"#)?);
+ assert!(engine.eval::<bool>(r#""x" >= 'x'"#)?);
+ assert!(!engine.eval::<bool>(r#""x" < 'x'"#)?);
+ assert!(engine.eval::<bool>(r#""x" <= 'x'"#)?);
+
+ assert!(engine.eval::<bool>(r#"'x' == "x""#)?);
+ assert!(!engine.eval::<bool>(r#"'x' != "x""#)?);
+ assert!(!engine.eval::<bool>(r#"'x' > "x""#)?);
+ assert!(engine.eval::<bool>(r#"'x' >= "x""#)?);
+ assert!(!engine.eval::<bool>(r#"'x' < "x""#)?);
+ assert!(engine.eval::<bool>(r#"'x' <= "x""#)?);
+
+ // Incompatible types compare to false
+ assert!(!engine.eval::<bool>("true == 42")?);
+ assert!(engine.eval::<bool>("true != 42")?);
+ assert!(!engine.eval::<bool>("true > 42")?);
+ assert!(!engine.eval::<bool>("true >= 42")?);
+ assert!(!engine.eval::<bool>("true < 42")?);
+ assert!(!engine.eval::<bool>("true <= 42")?);
+
+ assert!(!engine.eval::<bool>(r#""42" == 42"#)?);
+ assert!(engine.eval::<bool>(r#""42" != 42"#)?);
+ assert!(!engine.eval::<bool>(r#""42" > 42"#)?);
+ assert!(!engine.eval::<bool>(r#""42" >= 42"#)?);
+ assert!(!engine.eval::<bool>(r#""42" < 42"#)?);
+ assert!(!engine.eval::<bool>(r#""42" <= 42"#)?);
+
+ Ok(())
+}
+
+#[test]
+fn test_binary_ops_null_coalesce() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("let x = 42; x ?? 123")?, 42);
+ assert_eq!(engine.eval::<INT>("let x = (); x ?? 123")?, 123);
+
+ Ok(())
+}
diff --git a/rhai/tests/bit_fields.rs b/rhai/tests/bit_fields.rs
new file mode 100644
index 0000000..f2cffb3
--- /dev/null
+++ b/rhai/tests/bit_fields.rs
@@ -0,0 +1,82 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_left_shift() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ assert_eq!(engine.eval::<INT>("4 << 2")?, 16);
+ Ok(())
+}
+
+#[test]
+fn test_right_shift() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ assert_eq!(engine.eval::<INT>("9 >> 1")?, 4);
+ Ok(())
+}
+
+#[cfg(not(feature = "no_index"))]
+#[test]
+fn test_bit_fields() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ assert!(!engine.eval::<bool>("let x = 10; x[0]")?);
+ assert!(engine.eval::<bool>("let x = 10; x[1]")?);
+ assert!(!engine.eval::<bool>("let x = 10; x[-1]")?);
+ assert_eq!(
+ engine.eval::<INT>("let x = 10; x[0] = true; x[1] = false; x")?,
+ 9
+ );
+ assert_eq!(engine.eval::<INT>("let x = 10; get_bits(x, 1, 3)")?, 5);
+ assert_eq!(engine.eval::<INT>("let x = 10; x[1..=3]")?, 5);
+ assert!(engine.eval::<INT>("let x = 10; x[1..99]").is_err());
+ assert!(engine.eval::<INT>("let x = 10; x[-1..3]").is_err());
+ assert_eq!(
+ engine.eval::<INT>("let x = 10; set_bits(x, 1, 3, 7); x")?,
+ 14
+ );
+ #[cfg(target_pointer_width = "64")]
+ #[cfg(not(feature = "only_i32"))]
+ {
+ assert_eq!(engine.eval::<INT>("let x = 255; get_bits(x, -60, 2)")?, 3);
+ assert_eq!(
+ engine.eval::<INT>("let x = 0; set_bits(x, -64, 1, 15); x")?,
+ 1
+ );
+ assert_eq!(
+ engine.eval::<INT>("let x = 0; set_bits(x, -60, 2, 15); x")?,
+ 0b00110000
+ );
+ }
+ assert_eq!(engine.eval::<INT>("let x = 10; x[1..4] = 7; x")?, 14);
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 0b001101101010001;
+ let count = 0;
+
+ for b in bits(x, 2, 10) {
+ if b { count += 1; }
+ }
+
+ count
+ "
+ )?,
+ 5
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 0b001101101010001;
+ let count = 0;
+
+ for b in bits(x, 2..=11) {
+ if b { count += 1; }
+ }
+
+ count
+ "
+ )?,
+ 5
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/blobs.rs b/rhai/tests/blobs.rs
new file mode 100644
index 0000000..acd9f95
--- /dev/null
+++ b/rhai/tests/blobs.rs
@@ -0,0 +1,272 @@
+#![cfg(not(feature = "no_index"))]
+use rhai::{Blob, Engine, EvalAltResult, Scope, INT};
+use std::iter::FromIterator;
+
+#[test]
+fn test_blobs() -> Result<(), Box<EvalAltResult>> {
+ let a = Blob::from_iter([1, 2, 3]);
+
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+ scope.push("x", a);
+
+ assert_eq!(engine.eval_with_scope::<INT>(&mut scope, "x[1]")?, 2);
+ assert_eq!(engine.eval_with_scope::<INT>(&mut scope, "x[0]")?, 1);
+ assert_eq!(engine.eval_with_scope::<INT>(&mut scope, "x[-1]")?, 3);
+ assert_eq!(engine.eval_with_scope::<INT>(&mut scope, "x[-3]")?, 1);
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope.clone(), "x += 4; x[3]")?,
+ 4
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ {
+ assert_eq!(
+ engine.eval_with_scope::<Blob>(&mut scope.clone(), "x.push(4); x")?,
+ [1, 2, 3, 4]
+ );
+ assert_eq!(
+ engine.eval_with_scope::<Blob>(&mut scope.clone(), "x.insert(0, 4); x")?,
+ [4, 1, 2, 3]
+ );
+ assert_eq!(
+ engine.eval_with_scope::<Blob>(&mut scope.clone(), "x.insert(999, 4); x")?,
+ [1, 2, 3, 4]
+ );
+ assert_eq!(
+ engine.eval_with_scope::<Blob>(&mut scope.clone(), "x.insert(-2, 4); x")?,
+ [1, 4, 2, 3]
+ );
+ assert_eq!(
+ engine.eval_with_scope::<Blob>(&mut scope.clone(), "x.insert(-999, 4); x")?,
+ [4, 1, 2, 3]
+ );
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope.clone(), "let z = [42]; x[z.len]")?,
+ 2
+ );
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope.clone(), "let z = [2]; x[z[0]]")?,
+ 3
+ );
+ }
+
+ assert_eq!(
+ engine.eval_with_scope::<Blob>(&mut scope.clone(), "x += x; x")?,
+ [1, 2, 3, 1, 2, 3]
+ );
+ assert_eq!(
+ engine.eval_with_scope::<Blob>(&mut scope.clone(), "x + x")?,
+ [1, 2, 3, 1, 2, 3]
+ );
+ assert_eq!(
+ engine.eval_with_scope::<Blob>(&mut scope.clone(), "x += 999; x")?,
+ [1, 2, 3, 0xe7]
+ );
+ assert_eq!(
+ engine.eval_with_scope::<Blob>(&mut scope.clone(), "x[2] = 999; x")?,
+ [1, 2, 0xe7]
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "only_i32"))]
+#[test]
+fn test_blobs_parse() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine
+ .eval::<INT>("let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_le_int(x,2,0)")?,
+ 0
+ );
+
+ assert_eq!(
+ engine
+ .eval::<INT>("let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_le_int(x,2,9)")?,
+ 0x0908070605040302
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_le_int(x,2..=11)"
+ )?,
+ 0x0908070605040302
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_le_int(x,2..11)"
+ )?,
+ 0x0908070605040302
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_be_int(x,2,10)"
+ )?,
+ 0x0203040506070809
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_be_int(x,2..12)"
+ )?,
+ 0x0203040506070809
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_le_int(x,-5,99)"
+ )?,
+ 0x0f0e0d0c0b
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_le_int(x,-5,2)"
+ )?,
+ 0x0c0b
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_le_int(x,-99,99)"
+ )?,
+ 0x0706050403020100
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } write_be(x, 3, 3, -98765432); parse_be_int(x, 3, 3)"
+ )?,
+ 0xffffff0000000000_u64 as INT
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } write_be(x, 3..=5, -98765432); parse_be_int(x, 3..6)"
+ )?,
+ 0xffffff0000000000_u64 as INT
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } write_le(x, 3, 3, -98765432); parse_le_int(x, 3, 3)"
+ )?,
+ 0x1cf588
+ );
+
+ assert_eq!(
+ engine.eval::<Blob>(
+ "let x = blob(16, 0); write_be(x, 0, 8, 0x1234567890abcdef); write_be(x, 8, 8, 0xabcdef1234567890); x"
+ )?,
+ vec![0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90]
+ );
+
+ Ok(())
+}
+
+#[cfg(feature = "only_i32")]
+#[test]
+fn test_blobs_parse() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine
+ .eval::<INT>("let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_le_int(x,2,0)")?,
+ 0
+ );
+
+ assert_eq!(
+ engine
+ .eval::<INT>("let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_le_int(x,2,9)")?,
+ 0x05040302
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_be_int(x,2,10)"
+ )?,
+ 0x02030405
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_le_int(x,-5,99)"
+ )?,
+ 0x0e0d0c0b
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_le_int(x,-5,2)"
+ )?,
+ 0x0c0b
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } parse_le_int(x,-99,99)"
+ )?,
+ 0x03020100
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } write_be(x, 3, 3, -98765432); parse_be_int(x, 3, 3)"
+ )?,
+ 0xfa1cf500_u32 as INT
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } write_be(x, 3..=5, -98765432); parse_be_int(x, 3..6)"
+ )?,
+ 0xfa1cf500_u32 as INT
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = blob(16, 0); for n in 0..16 { x[n] = n; } write_le(x, 3, 3, -98765432); parse_le_int(x, 3, 3)"
+ )?,
+ 0x1cf588
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_blobs_write_string() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<Blob>(r#"let x = blob(16, 0); write_ascii(x, 0, 14, "hello, world!"); x"#)?,
+ "hello, world!\0\0\0".as_bytes()
+ );
+
+ assert_eq!(
+ engine.eval::<Blob>(r#"let x = blob(10, 0); write_ascii(x, 3..8, "hello, world!"); x"#)?,
+ "\0\0\0hello\0\0".as_bytes()
+ );
+
+ assert_eq!(
+ engine.eval::<Blob>(
+ r#"let x = blob(10, 0); write_ascii(x, 0..9, "❤ hello, ❤ world! ❤❤❤"); x"#
+ )?,
+ " hello, \0".as_bytes()
+ );
+
+ assert_eq!(
+ engine.eval::<Blob>(r#"let x = blob(10, 0); write_utf8(x, 3..9, "❤❤❤❤"); x"#)?,
+ "\0\0\0\u{2764}\u{2764}\0".as_bytes()
+ );
+
+ assert_eq!(
+ engine.eval::<Blob>(r#"let x = blob(10, 0); write_utf8(x, 3..7, "❤❤❤❤"); x"#)?,
+ vec![0, 0, 0, 226, 157, 164, 226, 0, 0, 0]
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/bool_op.rs b/rhai/tests/bool_op.rs
new file mode 100644
index 0000000..247fc33
--- /dev/null
+++ b/rhai/tests/bool_op.rs
@@ -0,0 +1,82 @@
+use rhai::{Engine, EvalAltResult};
+
+#[test]
+fn test_bool_op1() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!(engine.eval::<bool>("true && (false || true)")?);
+ assert!(engine.eval::<bool>("true & (false | true)")?);
+
+ Ok(())
+}
+
+#[test]
+fn test_bool_op2() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!(!engine.eval::<bool>("false && (false || true)")?);
+ assert!(!engine.eval::<bool>("false & (false | true)")?);
+
+ Ok(())
+}
+
+#[test]
+fn test_bool_op3() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!(engine.eval::<bool>("true && (false || 123)").is_err());
+ assert!(engine.eval::<bool>("true && (true || { throw })")?);
+ assert!(engine.eval::<bool>("123 && (false || true)").is_err());
+ assert!(!engine.eval::<bool>("false && (true || { throw })")?);
+
+ Ok(())
+}
+
+#[test]
+fn test_bool_op_short_circuit() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!(engine.eval::<bool>(
+ "
+ let x = true;
+ x || { throw; };
+ "
+ )?);
+
+ assert!(!engine.eval::<bool>(
+ "
+ let x = false;
+ x && { throw; };
+ "
+ )?);
+
+ Ok(())
+}
+
+#[test]
+fn test_bool_op_no_short_circuit1() {
+ let engine = Engine::new();
+
+ let _ = engine
+ .eval::<bool>(
+ "
+ let x = true;
+ x | { throw; }
+ ",
+ )
+ .unwrap_err();
+}
+
+#[test]
+fn test_bool_op_no_short_circuit2() {
+ let engine = Engine::new();
+
+ assert!(engine
+ .eval::<bool>(
+ "
+ let x = false;
+ x & { throw; }
+ "
+ )
+ .is_err());
+}
diff --git a/rhai/tests/build_type.rs b/rhai/tests/build_type.rs
new file mode 100644
index 0000000..4844a3b
--- /dev/null
+++ b/rhai/tests/build_type.rs
@@ -0,0 +1,149 @@
+#![cfg(not(feature = "no_object"))]
+use rhai::{CustomType, Engine, EvalAltResult, Position, TypeBuilder, INT};
+
+#[test]
+fn build_type() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, Clone, PartialEq, Eq)]
+ struct Vec3 {
+ x: INT,
+ y: INT,
+ z: INT,
+ }
+
+ impl Vec3 {
+ fn new(x: INT, y: INT, z: INT) -> Self {
+ Self { x, y, z }
+ }
+ fn get_x(&mut self) -> INT {
+ self.x
+ }
+ fn set_x(&mut self, x: INT) {
+ self.x = x
+ }
+ fn get_y(&mut self) -> INT {
+ self.y
+ }
+ fn set_y(&mut self, y: INT) {
+ self.y = y
+ }
+ fn get_z(&mut self) -> INT {
+ self.z
+ }
+ fn set_z(&mut self, z: INT) {
+ self.z = z
+ }
+ fn get_component(&mut self, idx: INT) -> Result<INT, Box<EvalAltResult>> {
+ match idx {
+ 0 => Ok(self.x),
+ 1 => Ok(self.y),
+ 2 => Ok(self.z),
+ _ => Err(Box::new(EvalAltResult::ErrorIndexNotFound(
+ idx.into(),
+ Position::NONE,
+ ))),
+ }
+ }
+ }
+
+ impl IntoIterator for Vec3 {
+ type Item = INT;
+ type IntoIter = std::vec::IntoIter<Self::Item>;
+
+ #[inline]
+ #[must_use]
+ fn into_iter(self) -> Self::IntoIter {
+ vec![self.x, self.y, self.z].into_iter()
+ }
+ }
+
+ impl CustomType for Vec3 {
+ fn build(mut builder: TypeBuilder<Self>) {
+ builder
+ .with_name("Vec3")
+ .is_iterable()
+ .with_fn("vec3", Self::new)
+ .with_fn("==", |x: &mut Vec3, y: Vec3| *x == y)
+ .with_fn("!=", |x: &mut Vec3, y: Vec3| *x != y)
+ .with_get_set("x", Self::get_x, Self::set_x)
+ .with_get_set("y", Self::get_y, Self::set_y)
+ .with_get_set("z", Self::get_z, Self::set_z);
+
+ #[cfg(not(feature = "no_index"))]
+ builder.with_indexer_get(Self::get_component);
+ }
+ }
+
+ let mut engine = Engine::new();
+ engine.build_type::<Vec3>();
+
+ assert_eq!(
+ engine.eval::<Vec3>(
+ "
+ let v = vec3(1, 2, 3);
+ v
+ ",
+ )?,
+ Vec3::new(1, 2, 3),
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let v = vec3(1, 2, 3);
+ v.x
+ ",
+ )?,
+ 1,
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let v = vec3(1, 2, 3);
+ v.y
+ ",
+ )?,
+ 2,
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let v = vec3(1, 2, 3);
+ v.z
+ ",
+ )?,
+ 3,
+ );
+ #[cfg(not(feature = "no_index"))]
+ assert!(engine.eval::<bool>(
+ "
+ let v = vec3(1, 2, 3);
+ v.x == v[0] && v.y == v[1] && v.z == v[2]
+ ",
+ )?);
+ assert_eq!(
+ engine.eval::<Vec3>(
+ "
+ let v = vec3(1, 2, 3);
+ v.x = 5;
+ v.y = 6;
+ v.z = 7;
+ v
+ ",
+ )?,
+ Vec3::new(5, 6, 7),
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let sum = 0;
+ let v = vec3(1, 2, 3);
+ for i in v {
+ sum += i;
+ }
+ sum
+ ",
+ )?,
+ 6,
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/call_fn.rs b/rhai/tests/call_fn.rs
new file mode 100644
index 0000000..1547363
--- /dev/null
+++ b/rhai/tests/call_fn.rs
@@ -0,0 +1,360 @@
+#![cfg(not(feature = "no_function"))]
+use rhai::{CallFnOptions, Dynamic, Engine, EvalAltResult, FnPtr, Func, FuncArgs, Scope, AST, INT};
+use std::any::TypeId;
+
+#[test]
+fn test_call_fn() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+
+ scope.push("foo", 42 as INT);
+
+ let ast = engine.compile(
+ "
+ fn hello(x, y) {
+ x + y
+ }
+ fn hello(x) {
+ x *= foo;
+ foo = 1;
+ x
+ }
+ fn hello() {
+ 41 + foo
+ }
+ fn define_var(scale) {
+ let bar = 21;
+ bar * scale
+ }
+ ",
+ )?;
+
+ let r = engine.call_fn::<INT>(&mut scope, &ast, "hello", (42 as INT, 123 as INT))?;
+ assert_eq!(r, 165);
+
+ let r = engine.call_fn::<INT>(&mut scope, &ast, "hello", (123 as INT,))?;
+ assert_eq!(r, 5166);
+
+ let r = engine.call_fn::<INT>(&mut scope, &ast, "hello", ())?;
+ assert_eq!(r, 42);
+
+ assert_eq!(
+ scope
+ .get_value::<INT>("foo")
+ .expect("variable foo should exist"),
+ 1
+ );
+
+ let r = engine.call_fn::<INT>(&mut scope, &ast, "define_var", (2 as INT,))?;
+ assert_eq!(r, 42);
+
+ assert!(!scope.contains("bar"));
+
+ let options = CallFnOptions::new().eval_ast(false).rewind_scope(false);
+
+ let r =
+ engine.call_fn_with_options::<INT>(options, &mut scope, &ast, "define_var", (2 as INT,))?;
+ assert_eq!(r, 42);
+
+ assert_eq!(
+ scope
+ .get_value::<INT>("bar")
+ .expect("variable bar should exist"),
+ 21
+ );
+
+ assert!(!scope.contains("scale"));
+
+ Ok(())
+}
+
+#[test]
+fn test_call_fn_scope() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+
+ let ast = engine.compile(
+ "
+ fn foo(x) {
+ let hello = 42;
+ bar + hello + x
+ }
+
+ let bar = 123;
+ ",
+ )?;
+
+ for _ in 0..50 {
+ assert_eq!(
+ engine.call_fn_with_options::<INT>(
+ CallFnOptions::new().rewind_scope(false),
+ &mut scope,
+ &ast,
+ "foo",
+ [Dynamic::THREE],
+ )?,
+ 168
+ );
+ }
+
+ assert_eq!(scope.len(), 100);
+
+ Ok(())
+}
+
+struct Options {
+ pub foo: bool,
+ pub bar: String,
+ pub baz: INT,
+}
+
+impl FuncArgs for Options {
+ fn parse<C: Extend<Dynamic>>(self, container: &mut C) {
+ container.extend(Some(self.foo.into()));
+ container.extend(Some(self.bar.into()));
+ container.extend(Some(self.baz.into()));
+ }
+}
+
+#[test]
+fn test_call_fn_args() -> Result<(), Box<EvalAltResult>> {
+ let options = Options {
+ foo: false,
+ bar: "world".to_string(),
+ baz: 42,
+ };
+
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+
+ let ast = engine.compile(
+ "
+ fn hello(x, y, z) {
+ if x { `hello ${y}` } else { y + z }
+ }
+ ",
+ )?;
+
+ let result = engine.call_fn::<String>(&mut scope, &ast, "hello", options)?;
+
+ assert_eq!(result, "world42");
+
+ Ok(())
+}
+
+#[test]
+fn test_call_fn_private() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+
+ let ast = engine.compile("fn add(x, n) { x + n }")?;
+
+ let r = engine.call_fn::<INT>(&mut scope, &ast, "add", (40 as INT, 2 as INT))?;
+ assert_eq!(r, 42);
+
+ let ast = engine.compile("private fn add(x, n, ) { x + n }")?;
+
+ let r = engine.call_fn::<INT>(&mut scope, &ast, "add", (40 as INT, 2 as INT))?;
+ assert_eq!(r, 42);
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_fn_ptr_raw() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine
+ .register_fn("mul", |x: &mut INT, y: INT| *x *= y)
+ .register_raw_fn(
+ "bar",
+ [
+ TypeId::of::<INT>(),
+ TypeId::of::<FnPtr>(),
+ TypeId::of::<INT>(),
+ ],
+ move |context, args| {
+ let fp = args[1].take().cast::<FnPtr>();
+ let value = args[2].clone();
+ let this_ptr = args.get_mut(0).unwrap();
+
+ fp.call_raw(&context, Some(this_ptr), [value])
+ },
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ fn foo(x) { this += x; }
+
+ let x = 41;
+ x.bar(foo, 1);
+ x
+ "#
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ fn foo(x, y) { this += x + y; }
+
+ let x = 40;
+ let v = 1;
+ x.bar(Fn("foo").curry(v), 1);
+ x
+ "#
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ private fn foo(x) { this += x; }
+
+ let x = 41;
+ x.bar(Fn("foo"), 1);
+ x
+ "#
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let x = 21;
+ x.bar(Fn("mul"), 2);
+ x
+ "#
+ )?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_anonymous_fn() -> Result<(), Box<EvalAltResult>> {
+ let calc_func = Func::<(INT, INT, INT), INT>::create_from_script(
+ Engine::new(),
+ "fn calc(x, y, z,) { (x + y) * z }",
+ "calc",
+ )?;
+
+ assert_eq!(calc_func(42, 123, 9)?, 1485);
+
+ let calc_func = Func::<(INT, String, INT), INT>::create_from_script(
+ Engine::new(),
+ "fn calc(x, y, z) { (x + len(y)) * z }",
+ "calc",
+ )?;
+
+ assert_eq!(calc_func(42, "hello".to_string(), 9)?, 423);
+
+ let calc_func = Func::<(INT, String, INT), INT>::create_from_script(
+ Engine::new(),
+ "private fn calc(x, y, z) { (x + len(y)) * z }",
+ "calc",
+ )?;
+
+ assert_eq!(calc_func(42, "hello".to_string(), 9)?, 423);
+
+ let calc_func = Func::<(INT, &str, INT), INT>::create_from_script(
+ Engine::new(),
+ "fn calc(x, y, z) { (x + len(y)) * z }",
+ "calc",
+ )?;
+
+ assert_eq!(calc_func(42, "hello", 9)?, 423);
+
+ Ok(())
+}
+
+#[test]
+fn test_call_fn_events() -> Result<(), Box<EvalAltResult>> {
+ // Event handler
+ struct Handler {
+ // Scripting engine
+ pub engine: Engine,
+ // Use a custom 'Scope' to keep stored state
+ pub scope: Scope<'static>,
+ // Program script
+ pub ast: AST,
+ }
+
+ const SCRIPT: &str = r#"
+ fn start(data) { 42 + data }
+ fn end(data) { 0 }
+ "#;
+
+ impl Handler {
+ pub fn new() -> Self {
+ let engine = Engine::new();
+
+ // Create a custom 'Scope' to hold state
+ let mut scope = Scope::new();
+
+ // Add initialized state into the custom 'Scope'
+ scope.push("state", false);
+
+ // Compile the handler script.
+ let ast = engine.compile(SCRIPT).unwrap();
+
+ // Evaluate the script to initialize it and other state variables.
+ // In a real application you'd again be handling errors...
+ engine.run_ast_with_scope(&mut scope, &ast).unwrap();
+
+ // The event handler is essentially these three items:
+ Handler { engine, scope, ast }
+ }
+
+ // Say there are three events: 'start', 'end', 'update'.
+ // In a real application you'd be handling errors...
+ pub fn on_event(&mut self, event_name: &str, event_data: INT) -> Dynamic {
+ let engine = &self.engine;
+ let scope = &mut self.scope;
+ let ast = &self.ast;
+
+ match event_name {
+ // The 'start' event maps to function 'start'.
+ // In a real application you'd be handling errors...
+ "start" => engine.call_fn(scope, ast, "start", (event_data,)).unwrap(),
+
+ // The 'end' event maps to function 'end'.
+ // In a real application you'd be handling errors...
+ "end" => engine.call_fn(scope, ast, "end", (event_data,)).unwrap(),
+
+ // The 'update' event maps to function 'update'.
+ // This event provides a default implementation when the script-defined function is not found.
+ "update" => engine
+ .call_fn(scope, ast, "update", (event_data,))
+ .or_else(|err| match *err {
+ EvalAltResult::ErrorFunctionNotFound(fn_name, ..)
+ if fn_name.starts_with("update") =>
+ {
+ // Default implementation of 'update' event handler
+ self.scope.set_value("state", true);
+ // Turn function-not-found into a success
+ Ok(Dynamic::UNIT)
+ }
+ _ => Err(err),
+ })
+ .unwrap(),
+ // In a real application you'd be handling unknown events...
+ _ => panic!("unknown event: {}", event_name),
+ }
+ }
+ }
+
+ let mut handler = Handler::new();
+ assert!(!handler.scope.get_value::<bool>("state").unwrap());
+ let _ = handler.on_event("update", 999);
+ assert!(handler.scope.get_value::<bool>("state").unwrap());
+ assert_eq!(handler.on_event("start", 999).as_int().unwrap(), 1041);
+
+ Ok(())
+}
diff --git a/rhai/tests/chars.rs b/rhai/tests/chars.rs
new file mode 100644
index 0000000..1e6d0db
--- /dev/null
+++ b/rhai/tests/chars.rs
@@ -0,0 +1,25 @@
+use rhai::{Engine, EvalAltResult};
+
+#[test]
+fn test_chars() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<char>("'y'")?, 'y');
+ assert_eq!(engine.eval::<char>(r"'\''")?, '\'');
+ assert_eq!(engine.eval::<char>(r#"'"'"#)?, '"');
+ assert_eq!(engine.eval::<char>(r"'\u2764'")?, '❤');
+
+ #[cfg(not(feature = "no_index"))]
+ {
+ assert_eq!(engine.eval::<char>(r#"let x="hello"; x[2]"#)?, 'l');
+ assert_eq!(
+ engine.eval::<String>(r#"let y="hello"; y[2]='$'; y"#)?,
+ "he$lo"
+ );
+ }
+
+ assert!(engine.eval::<char>(r"'\uhello'").is_err());
+ assert!(engine.eval::<char>("''").is_err());
+
+ Ok(())
+}
diff --git a/rhai/tests/closures.rs b/rhai/tests/closures.rs
new file mode 100644
index 0000000..f596c75
--- /dev/null
+++ b/rhai/tests/closures.rs
@@ -0,0 +1,431 @@
+#![cfg(not(feature = "no_function"))]
+use rhai::{Dynamic, Engine, EvalAltResult, FnPtr, ParseErrorType, Scope, INT};
+use std::any::TypeId;
+use std::cell::RefCell;
+use std::mem::take;
+use std::rc::Rc;
+
+#[cfg(not(feature = "no_object"))]
+use rhai::Map;
+
+#[test]
+fn test_fn_ptr_curry_call() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_raw_fn(
+ "call_with_arg",
+ [TypeId::of::<FnPtr>(), TypeId::of::<INT>()],
+ |context, args| {
+ let fn_ptr = args[0].take().cast::<FnPtr>();
+ fn_ptr.call_raw(&context, None, [args[1].take()])
+ },
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let addition = |x, y| { x + y };
+ let curried = addition.curry(2);
+
+ call_with_arg(curried, 40)
+ "
+ )?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_closure"))]
+#[cfg(not(feature = "no_object"))]
+fn test_closures() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let mut scope = Scope::new();
+
+ scope.push("x", 42 as INT);
+
+ assert!(matches!(
+ engine.compile_expression("|x| {}").unwrap_err().err_type(),
+ ParseErrorType::BadInput(..)
+ ));
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "
+ let f = || { x };
+ f.call()
+ ",
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let foo = #{ x: 42 };
+ let f = || { this.x };
+ foo.call(f)
+ ",
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 8;
+
+ let res = |y, z| {
+ let w = 12;
+
+ return (|| x + y + z + w).call();
+ }.curry(15).call(2);
+
+ res + (|| x - 3).call()
+ "
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let a = 41;
+ let foo = |x| { a += x };
+ foo.call(1);
+ a
+ "
+ )?,
+ 42
+ );
+
+ assert!(engine.eval::<bool>(
+ "
+ let a = 41;
+ let foo = |x| { a += x };
+ a.is_shared()
+ "
+ )?);
+
+ assert!(engine.eval::<bool>(
+ "
+ let a = 41;
+ let foo = |x| { a += x };
+ is_shared(a)
+ "
+ )?);
+
+ engine.register_fn("plus_one", |x: INT| x + 1);
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let a = 41;
+ let f = || plus_one(a);
+ f.call()
+ "
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let a = 40;
+ let f = |x| {
+ let f = |x| {
+ let f = |x| plus_one(a) + x;
+ f.call(x)
+ };
+ f.call(x)
+ };
+ f.call(1)
+ "
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let a = 21;
+ let f = |x| a += x;
+ f.call(a);
+ a
+ "
+ )?,
+ 42
+ );
+
+ engine.register_raw_fn(
+ "custom_call",
+ [TypeId::of::<INT>(), TypeId::of::<FnPtr>()],
+ |context, args| {
+ let func = take(args[1]).cast::<FnPtr>();
+
+ func.call_raw(&context, None, [])
+ },
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let a = 41;
+ let b = 0;
+ let f = || b.custom_call(|| a + 1);
+
+ f.call()
+ "
+ )?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_closure"))]
+fn test_closures_sharing() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_fn("foo", |x: INT, s: &str| s.len() as INT + x);
+ engine.register_fn("bar", |x: INT, s: String| s.len() as INT + x);
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let s = "hello";
+ let f = || s;
+ foo(1, s)
+ "#
+ )?,
+ 6
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"
+ let s = "hello";
+ let f = || s;
+ let n = foo(1, s);
+ s
+ "#
+ )?,
+ "hello"
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let s = "hello";
+ let f = || s;
+ bar(1, s)
+ "#
+ )?,
+ 6
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ {
+ let mut m = Map::new();
+ m.insert("hello".into(), "world".into());
+ let m = Dynamic::from(m).into_shared();
+
+ engine.register_fn("baz", move || m.clone());
+
+ assert!(!engine.eval::<bool>(
+ "
+ let m = baz();
+ m.is_shared()
+ "
+ )?);
+
+ assert_eq!(
+ engine.eval::<String>(
+ "
+ let m = baz();
+ m.hello
+ "
+ )?,
+ "world"
+ );
+
+ assert_eq!(engine.eval::<String>("baz().hello")?, "world");
+ }
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_closure"))]
+#[cfg(not(feature = "no_object"))]
+#[cfg(not(feature = "sync"))]
+fn test_closures_data_race() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let a = 1;
+ let b = 40;
+ let foo = |x| { this += a + x };
+ b.call(foo, 1);
+ b
+ "
+ )?,
+ 42
+ );
+
+ assert!(matches!(
+ *engine
+ .eval::<INT>(
+ "
+ let a = 20;
+ let foo = |x| { this += a + x };
+ a.call(foo, 1);
+ a
+ "
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataRace(..)
+ ));
+
+ Ok(())
+}
+
+type TestStruct = Rc<RefCell<INT>>;
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+#[cfg(not(feature = "sync"))]
+fn test_closures_shared_obj() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ // Register API on TestStruct
+ engine
+ .register_type_with_name::<TestStruct>("TestStruct")
+ .register_get_set(
+ "data",
+ |p: &mut TestStruct| *p.borrow(),
+ |p: &mut TestStruct, value: INT| *p.borrow_mut() = value,
+ )
+ .register_fn("+=", |p1: &mut TestStruct, p2: TestStruct| {
+ *p1.borrow_mut() += *p2.borrow()
+ })
+ .register_fn("-=", |p1: &mut TestStruct, p2: TestStruct| {
+ *p1.borrow_mut() -= *p2.borrow()
+ });
+
+ let engine = engine; // Make engine immutable
+
+ let code = r#"
+ #{
+ name: "A",
+ description: "B",
+ cost: 1,
+ health_added: 0,
+ action: |p1, p2| { p1 += p2 }
+ }
+ "#;
+
+ let ast = engine.compile(code)?;
+ let res = engine.eval_ast::<Map>(&ast)?;
+
+ // Make closure
+ let f = move |p1: TestStruct, p2: TestStruct| {
+ let action_ptr = res["action"].clone_cast::<FnPtr>();
+ let name = action_ptr.fn_name();
+ engine.call_fn(&mut Scope::new(), &ast, name, (p1, p2))
+ };
+
+ // Test closure
+ let p1 = Rc::new(RefCell::new(41));
+ let p2 = Rc::new(RefCell::new(1));
+
+ f(p1.clone(), p2)?;
+
+ assert_eq!(*p1.borrow(), 42);
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_closure"))]
+fn test_closures_external() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ let ast = engine.compile(
+ r#"
+ let test = "hello";
+ |x| test + x
+ "#,
+ )?;
+
+ let fn_ptr = engine.eval_ast::<FnPtr>(&ast)?;
+
+ let f = move |x: INT| fn_ptr.call::<String>(&engine, &ast, (x,));
+
+ assert_eq!(f(42)?, "hello42");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_closure"))]
+#[cfg(not(feature = "sync"))]
+fn test_closures_callback() -> Result<(), Box<EvalAltResult>> {
+ type SingleNode = Rc<dyn Node>;
+
+ trait Node {
+ fn run(&self, x: INT) -> Result<INT, Box<EvalAltResult>>;
+ }
+
+ struct PhaserNode {
+ func: Box<dyn Fn(INT) -> Result<INT, Box<EvalAltResult>>>,
+ }
+
+ impl Node for PhaserNode {
+ fn run(&self, x: INT) -> Result<INT, Box<EvalAltResult>> {
+ (self.func)(x)
+ }
+ }
+
+ fn phaser(callback: impl Fn(INT) -> Result<INT, Box<EvalAltResult>> + 'static) -> impl Node {
+ PhaserNode {
+ func: Box::new(callback),
+ }
+ }
+
+ let mut engine = Engine::new();
+
+ let ast = Rc::new(engine.compile(
+ "
+ const FACTOR = 2;
+ phaser(|x| x * FACTOR)
+ ",
+ )?);
+
+ let shared_engine = Rc::new(RefCell::new(Engine::new_raw()));
+ let engine2 = shared_engine.clone();
+ let ast2 = ast.clone();
+
+ engine.register_fn("phaser", move |fp: FnPtr| {
+ let engine = engine2.clone();
+ let ast = ast2.clone();
+
+ let callback = Box::new(move |x: INT| fp.call(&engine.borrow(), &ast, (x,)));
+
+ Rc::new(phaser(callback)) as SingleNode
+ });
+
+ *shared_engine.borrow_mut() = engine;
+
+ let cb = shared_engine.borrow().eval_ast::<SingleNode>(&ast)?;
+
+ assert_eq!(cb.run(21)?, 42);
+
+ Ok(())
+}
diff --git a/rhai/tests/comments.rs b/rhai/tests/comments.rs
new file mode 100644
index 0000000..9d981c5
--- /dev/null
+++ b/rhai/tests/comments.rs
@@ -0,0 +1,99 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_comments() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>("let x = 42; x // I am a single line comment, yay!")?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let /* I am a
+ multi-line
+ comment, yay!
+ */ x = 42; x
+ "
+ )?,
+ 42
+ );
+
+ engine.run("/* Hello world */")?;
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_function"))]
+#[cfg(feature = "metadata")]
+#[test]
+fn test_comments_doc() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ let ast = engine.compile(
+ "
+ /// Hello world
+
+
+ fn foo() {}
+ ",
+ )?;
+
+ assert_eq!(
+ ast.iter_functions().next().unwrap().comments[0],
+ "/// Hello world"
+ );
+
+ assert!(engine
+ .compile(
+ "
+ /// Hello world
+ let x = 42;
+ "
+ )
+ .is_err());
+
+ engine.compile(
+ "
+ ///////////////
+ let x = 42;
+
+ /***************/
+ let x = 42;
+ ",
+ )?;
+
+ let ast = engine.compile(
+ "
+ /** Hello world
+ ** how are you?
+ **/
+
+ fn foo() {}
+ ",
+ )?;
+
+ #[cfg(not(feature = "no_position"))]
+ assert_eq!(
+ ast.iter_functions().next().unwrap().comments[0],
+ "/** Hello world\n** how are you?\n**/"
+ );
+ #[cfg(feature = "no_position")]
+ assert_eq!(
+ ast.iter_functions().next().unwrap().comments[0],
+ "/** Hello world\n ** how are you?\n **/",
+ );
+
+ assert!(engine
+ .compile(
+ "
+ /** Hello world */
+ let x = 42;
+ "
+ )
+ .is_err());
+
+ Ok(())
+}
diff --git a/rhai/tests/compound_equality.rs b/rhai/tests/compound_equality.rs
new file mode 100644
index 0000000..bd93f2c
--- /dev/null
+++ b/rhai/tests/compound_equality.rs
@@ -0,0 +1,66 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_or_equals() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("let x = 16; x |= 74; x")?, 90);
+ assert!(engine.eval::<bool>("let x = true; x |= false; x")?);
+ assert!(engine.eval::<bool>("let x = false; x |= true; x")?);
+
+ Ok(())
+}
+
+#[test]
+fn test_and_equals() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("let x = 16; x &= 31; x")?, 16);
+ assert!(!engine.eval::<bool>("let x = true; x &= false; x")?);
+ assert!(!engine.eval::<bool>("let x = false; x &= true; x")?);
+ assert!(engine.eval::<bool>("let x = true; x &= true; x")?);
+
+ Ok(())
+}
+
+#[test]
+fn test_xor_equals() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ assert_eq!(engine.eval::<INT>("let x = 90; x ^= 12; x")?, 86);
+ Ok(())
+}
+
+#[test]
+fn test_multiply_equals() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ assert_eq!(engine.eval::<INT>("let x = 2; x *= 3; x")?, 6);
+ Ok(())
+}
+
+#[test]
+fn test_divide_equals() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ assert_eq!(engine.eval::<INT>("let x = 6; x /= 2; x")?, 3);
+ Ok(())
+}
+
+#[test]
+fn test_right_shift_equals() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ assert_eq!(engine.eval::<INT>("let x = 9; x >>=1; x")?, 4);
+ Ok(())
+}
+
+#[test]
+fn test_left_shift_equals() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ assert_eq!(engine.eval::<INT>("let x = 4; x <<= 2; x")?, 16);
+ Ok(())
+}
+
+#[test]
+fn test_modulo_equals() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ assert_eq!(engine.eval::<INT>("let x = 10; x %= 4; x")?, 2);
+ Ok(())
+}
diff --git a/rhai/tests/constants.rs b/rhai/tests/constants.rs
new file mode 100644
index 0000000..943b518
--- /dev/null
+++ b/rhai/tests/constants.rs
@@ -0,0 +1,127 @@
+use rhai::{Engine, EvalAltResult, ParseErrorType, Scope, INT};
+
+#[test]
+fn test_constant() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("const x = 123; x")?, 123);
+
+ assert!(matches!(
+ *engine
+ .eval::<INT>("const x = 123; x = 42;")
+ .expect_err("expects error"),
+ EvalAltResult::ErrorParsing(ParseErrorType::AssignmentToConstant(x), ..) if x == "x"
+ ));
+
+ #[cfg(not(feature = "no_index"))]
+ assert!(matches!(
+ *engine.run("const x = [1, 2, 3, 4, 5]; x[2] = 42;").expect_err("expects error"),
+ EvalAltResult::ErrorAssignmentToConstant(x, ..) if x == "x"
+ ));
+
+ Ok(())
+}
+
+#[test]
+fn test_constant_scope() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ let mut scope = Scope::new();
+ scope.push_constant("x", 42 as INT);
+
+ assert!(matches!(
+ *engine.run_with_scope(&mut scope, "x = 1").expect_err("expects error"),
+ EvalAltResult::ErrorAssignmentToConstant(x, ..) if x == "x"
+ ));
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_object"))]
+#[test]
+fn test_constant_mut() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, Clone)]
+ struct TestStruct(INT); // custom type
+
+ let mut engine = Engine::new();
+
+ fn set_value(obj: &mut TestStruct, value: INT) {
+ obj.0 = value;
+ }
+
+ engine
+ .register_type_with_name::<TestStruct>("TestStruct")
+ .register_fn("new_ts", || TestStruct(123))
+ .register_get("value", |obj: &mut TestStruct| obj.0)
+ .register_set("value", set_value)
+ .register_fn("update_value", set_value);
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ const MY_NUMBER = new_ts();
+ MY_NUMBER.update_value(42);
+ MY_NUMBER.value
+ ",
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ const MY_NUMBER = new_ts();
+ update_value(MY_NUMBER, 42);
+ MY_NUMBER.value
+ ",
+ )?,
+ 123
+ );
+
+ assert!(matches!(
+ *engine
+ .run(
+ "
+ const MY_NUMBER = new_ts();
+ MY_NUMBER.value = 42;
+ "
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorNonPureMethodCallOnConstant(..)
+ ));
+
+ let mut scope = Scope::new();
+
+ scope.push_constant("MY_NUMBER", TestStruct(123));
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "
+ update_value(MY_NUMBER, 42);
+ MY_NUMBER.value
+ ",
+ )?,
+ 123
+ );
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "
+ MY_NUMBER.update_value(42);
+ MY_NUMBER.value
+ ",
+ )?,
+ 42
+ );
+
+ assert!(matches!(
+ *engine
+ .run_with_scope(&mut scope, "MY_NUMBER.value = 42;")
+ .unwrap_err(),
+ EvalAltResult::ErrorNonPureMethodCallOnConstant(..)
+ ));
+
+ Ok(())
+}
diff --git a/rhai/tests/custom_syntax.rs b/rhai/tests/custom_syntax.rs
new file mode 100644
index 0000000..ede7078
--- /dev/null
+++ b/rhai/tests/custom_syntax.rs
@@ -0,0 +1,466 @@
+#![cfg(not(feature = "no_custom_syntax"))]
+
+use rhai::{
+ Dynamic, Engine, EvalAltResult, ImmutableString, LexError, ParseErrorType, Position, Scope, INT,
+};
+
+#[test]
+fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.run("while false {}")?;
+
+ // Disable 'while' and make sure it still works with custom syntax
+ engine.disable_symbol("while");
+ assert!(matches!(
+ engine.compile("while false {}").unwrap_err().err_type(),
+ ParseErrorType::Reserved(err) if err == "while"
+ ));
+ assert!(matches!(
+ engine.compile("let while = 0").unwrap_err().err_type(),
+ ParseErrorType::Reserved(err) if err == "while"
+ ));
+
+ // Implement ternary operator
+ engine.register_custom_syntax(
+ ["iff", "$expr$", "?", "$expr$", ":", "$expr$"],
+ false,
+ |context, inputs| match context.eval_expression_tree(&inputs[0])?.as_bool() {
+ Ok(true) => context.eval_expression_tree(&inputs[1]),
+ Ok(false) => context.eval_expression_tree(&inputs[2]),
+ Err(typ) => Err(Box::new(EvalAltResult::ErrorMismatchDataType(
+ "bool".to_string(),
+ typ.to_string(),
+ inputs[0].position(),
+ ))),
+ },
+ )?;
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 42;
+ let y = iff x > 40 ? 0 : 123;
+ y
+ "
+ )?,
+ 0
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 42;
+ let y = iff x == 0 ? 0 : 123;
+ y
+ "
+ )?,
+ 123
+ );
+
+ // Custom syntax
+ engine.register_custom_syntax(
+ [
+ "exec", "[", "$ident$", "$symbol$", "$int$", "]", "->", "$block$", "while", "$expr$",
+ ],
+ true,
+ |context, inputs| {
+ let var_name = inputs[0].get_string_value().unwrap();
+ let op = inputs[1].get_literal_value::<ImmutableString>().unwrap();
+ let max = inputs[2].get_literal_value::<INT>().unwrap();
+ let stmt = &inputs[3];
+ let condition = &inputs[4];
+
+ context.scope_mut().push(var_name.to_string(), 0 as INT);
+
+ let mut count: INT = 0;
+
+ loop {
+ let done = match op.as_str() {
+ "<" => count >= max,
+ "<=" => count > max,
+ ">" => count <= max,
+ ">=" => count < max,
+ "==" => count != max,
+ "!=" => count == max,
+ _ => return Err(format!("Unsupported operator: {op}").into()),
+ };
+
+ if done {
+ break;
+ }
+
+ // Do not rewind if the variable is upper-case
+ let _: Dynamic = if var_name.to_uppercase() == var_name {
+ #[allow(deprecated)] // not deprecated but unstable
+ context.eval_expression_tree_raw(stmt, false)
+ } else {
+ context.eval_expression_tree(stmt)
+ }?;
+
+ count += 1;
+
+ context
+ .scope_mut()
+ .push(format!("{var_name}{count}"), count);
+
+ let stop = !context
+ .eval_expression_tree(condition)?
+ .as_bool()
+ .map_err(|err| {
+ Box::new(EvalAltResult::ErrorMismatchDataType(
+ "bool".to_string(),
+ err.to_string(),
+ condition.position(),
+ ))
+ })?;
+
+ if stop {
+ break;
+ }
+ }
+
+ Ok(count.into())
+ },
+ )?;
+
+ assert!(matches!(
+ *engine
+ .run("let foo = (exec [x<<15] -> { x += 2 } while x < 42) * 10;")
+ .unwrap_err(),
+ EvalAltResult::ErrorRuntime(..)
+ ));
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 0;
+ let foo = (exec [x<15] -> { x += 2 } while x < 42) * 10;
+ foo
+ "
+ )?,
+ 150
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 0;
+ exec [x<100] -> { x += 1 } while x < 42;
+ x
+ "
+ )?,
+ 42
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ exec [x<100] -> { x += 1 } while x < 42;
+ x
+ "
+ )?,
+ 42
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let foo = 123;
+ exec [x<15] -> { x += 1 } while x < 42;
+ foo + x + x1 + x2 + x3
+ "
+ )?,
+ 144
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let foo = 123;
+ exec [x<15] -> { let foo = x; x += 1; } while x < 42;
+ foo
+ "
+ )?,
+ 123
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let foo = 123;
+ exec [ABC<15] -> { let foo = ABC; ABC += 1; } while ABC < 42;
+ foo
+ "
+ )?,
+ 14
+ );
+
+ // The first symbol must be an identifier
+ assert_eq!(
+ *engine
+ .register_custom_syntax(["!"], false, |_, _| Ok(Dynamic::UNIT))
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::BadInput(LexError::ImproperSymbol(
+ "!".to_string(),
+ "Improper symbol for custom syntax at position #1: '!'".to_string()
+ ))
+ );
+
+ // Check self-termination
+ engine
+ .register_custom_syntax(["test1", "$block$"], true, |_, _| Ok(Dynamic::UNIT))?
+ .register_custom_syntax(["test2", "}"], true, |_, _| Ok(Dynamic::UNIT))?
+ .register_custom_syntax(["test3", ";"], true, |_, _| Ok(Dynamic::UNIT))?;
+
+ assert_eq!(engine.eval::<INT>("test1 { x = y + z; } 42")?, 42);
+ assert_eq!(engine.eval::<INT>("test2 } 42")?, 42);
+ assert_eq!(engine.eval::<INT>("test3; 42")?, 42);
+
+ // Register the custom syntax: var x = ???
+ engine.register_custom_syntax(
+ ["var", "$ident$", "=", "$expr$"],
+ true,
+ |context, inputs| {
+ let var_name = inputs[0].get_string_value().unwrap();
+ let expr = &inputs[1];
+
+ // Evaluate the expression
+ let value = context.eval_expression_tree(expr)?;
+
+ if !context.scope().is_constant(var_name).unwrap_or(false) {
+ context.scope_mut().set_value(var_name.to_string(), value);
+ Ok(Dynamic::UNIT)
+ } else {
+ Err(format!("variable {var_name} is constant").into())
+ }
+ },
+ )?;
+
+ let mut scope = Scope::new();
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope, "var foo = 42; foo")?,
+ 42
+ );
+ assert_eq!(scope.get_value::<INT>("foo"), Some(42));
+ assert_eq!(scope.len(), 1);
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope, "var foo = 123; foo")?,
+ 123
+ );
+ assert_eq!(scope.get_value::<INT>("foo"), Some(123));
+ assert_eq!(scope.len(), 1);
+
+ Ok(())
+}
+
+#[test]
+fn test_custom_syntax_scope() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_custom_syntax(
+ [
+ "with", "offset", "(", "$expr$", ",", "$expr$", ")", "$block$",
+ ],
+ true,
+ |context, inputs| {
+ let x = context
+ .eval_expression_tree(&inputs[0])?
+ .as_int()
+ .map_err(|typ| {
+ Box::new(EvalAltResult::ErrorMismatchDataType(
+ "integer".to_string(),
+ typ.to_string(),
+ inputs[0].position(),
+ ))
+ })?;
+
+ let y = context
+ .eval_expression_tree(&inputs[1])?
+ .as_int()
+ .map_err(|typ| {
+ Box::new(EvalAltResult::ErrorMismatchDataType(
+ "integer".to_string(),
+ typ.to_string(),
+ inputs[1].position(),
+ ))
+ })?;
+
+ let orig_len = context.scope().len();
+
+ context.scope_mut().push_constant("x", x);
+ context.scope_mut().push_constant("y", y);
+
+ let result = context.eval_expression_tree(&inputs[2]);
+
+ context.scope_mut().rewind(orig_len);
+
+ result
+ },
+ )?;
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let y = 1;
+ let x = 0;
+ with offset(44, 2) { x - y }
+ "
+ )?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_custom_syntax_matrix() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.disable_symbol("|");
+
+ engine.register_custom_syntax(
+ [
+ "@", //
+ "|", "$expr$", "$expr$", "$expr$", "|", //
+ "|", "$expr$", "$expr$", "$expr$", "|", //
+ "|", "$expr$", "$expr$", "$expr$", "|",
+ ],
+ false,
+ |context, inputs| {
+ let mut values = [[0; 3]; 3];
+
+ for y in 0..3 {
+ for x in 0..3 {
+ let offset = y * 3 + x;
+
+ match context.eval_expression_tree(&inputs[offset])?.as_int() {
+ Ok(v) => values[y][x] = v,
+ Err(typ) => {
+ return Err(Box::new(EvalAltResult::ErrorMismatchDataType(
+ "integer".to_string(),
+ typ.to_string(),
+ inputs[offset].position(),
+ )))
+ }
+ }
+ }
+ }
+
+ Ok(Dynamic::from(values))
+ },
+ )?;
+
+ let r = engine.eval::<[[INT; 3]; 3]>(
+ "
+ let a = 42;
+ let b = 123;
+ let c = 1;
+ let d = 99;
+
+ @| a b 0 |
+ | -b a 0 |
+ | 0 0 c*d |
+ ",
+ )?;
+
+ assert_eq!(r, [[42, 123, 0], [-123, 42, 0], [0, 0, 99]]);
+
+ Ok(())
+}
+
+#[test]
+fn test_custom_syntax_raw() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_custom_syntax_with_state_raw(
+ "hello",
+ |stream, look_ahead, state| match stream.len() {
+ 0 => unreachable!(),
+ 1 if look_ahead == "\"world\"" => {
+ *state = Dynamic::TRUE;
+ Ok(Some("$string$".into()))
+ }
+ 1 => {
+ *state = Dynamic::FALSE;
+ Ok(Some("$ident$".into()))
+ }
+ 2 => {
+ match stream[1].as_str() {
+ "world" if state.as_bool().unwrap_or(false) => Ok(Some("$$world".into())),
+ "world" => Ok(Some("$$hello".into())),
+ "kitty" => {
+ *state = (42 as INT).into();
+ Ok(None)
+ }
+ s => Err(LexError::ImproperSymbol(s.to_string(), String::new())
+ .into_err(Position::NONE)),
+ }
+ }
+ _ => unreachable!(),
+ },
+ true,
+ |context, inputs, state| {
+ context.scope_mut().push("foo", 999 as INT);
+
+ Ok(match inputs[0].get_string_value().unwrap() {
+ "world" => match inputs.last().unwrap().get_string_value().unwrap_or("") {
+ "$$hello" => 0 as INT,
+ "$$world" => 123456 as INT,
+ _ => 123 as INT,
+ },
+ "kitty" if inputs.len() > 1 => 999 as INT,
+ "kitty" => state.as_int().unwrap(),
+ _ => unreachable!(),
+ }
+ .into())
+ },
+ );
+
+ assert_eq!(engine.eval::<INT>(r#"hello "world""#)?, 123456);
+ assert_eq!(engine.eval::<INT>("hello world")?, 0);
+ assert_eq!(engine.eval::<INT>("hello kitty")?, 42);
+ assert_eq!(
+ engine.eval::<INT>("let foo = 0; (hello kitty) + foo")?,
+ 1041
+ );
+ assert_eq!(engine.eval::<INT>("(hello kitty) + foo")?, 1041);
+ assert_eq!(
+ *engine.compile("hello hey").unwrap_err().err_type(),
+ ParseErrorType::BadInput(LexError::ImproperSymbol("hey".to_string(), String::new()))
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_custom_syntax_raw2() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_custom_syntax_with_state_raw(
+ "#",
+ |symbols, lookahead, _| match symbols.len() {
+ 1 if lookahead == "-" => Ok(Some("$symbol$".into())),
+ 1 => Ok(Some("$int$".into())),
+ 2 if symbols[1] == "-" => Ok(Some("$int$".into())),
+ 2 => Ok(None),
+ 3 => Ok(None),
+ _ => unreachable!(),
+ },
+ false,
+ move |_, inputs, _| {
+ let id = if inputs.len() == 2 {
+ -inputs[1].get_literal_value::<INT>().unwrap()
+ } else {
+ inputs[0].get_literal_value::<INT>().unwrap()
+ };
+ Ok(id.into())
+ },
+ );
+
+ assert_eq!(engine.eval::<INT>("#-1")?, -1);
+ assert_eq!(engine.eval::<INT>("let x = 41; x + #1")?, 42);
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(engine.eval::<INT>("#-42.abs()")?, 42);
+ assert_eq!(engine.eval::<INT>("#42/2")?, 21);
+ assert_eq!(engine.eval::<INT>("sign(#1)")?, 1);
+
+ Ok(())
+}
diff --git a/rhai/tests/data_size.rs b/rhai/tests/data_size.rs
new file mode 100644
index 0000000..feb4419
--- /dev/null
+++ b/rhai/tests/data_size.rs
@@ -0,0 +1,354 @@
+#![cfg(not(feature = "unchecked"))]
+use rhai::{Engine, EvalAltResult, ParseErrorType, INT};
+
+#[cfg(not(feature = "no_index"))]
+use rhai::Array;
+
+#[cfg(not(feature = "no_object"))]
+use rhai::Map;
+
+#[test]
+fn test_max_string_size() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ engine.set_max_string_size(10);
+
+ assert_eq!(
+ *engine
+ .compile(r#"let x = "hello, world!";"#)
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::LiteralTooLarge("Length of string".to_string(), 10)
+ );
+
+ assert_eq!(
+ *engine
+ .compile(r#"let x = "朝に紅顔、暮に白骨";"#)
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::LiteralTooLarge("Length of string".to_string(), 10)
+ );
+
+ assert!(matches!(
+ *engine
+ .run(
+ r#"
+ let x = "hello, ";
+ let y = "world!";
+ x + y
+ "#
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataTooLarge(..)
+ ));
+
+ #[cfg(not(feature = "no_object"))]
+ assert!(matches!(
+ *engine
+ .run(
+ r#"
+ let x = "hello";
+ x.pad(100, '!');
+ x
+ "#
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataTooLarge(..)
+ ));
+
+ engine.set_max_string_size(0);
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"
+ let x = "hello, ";
+ let y = "world!";
+ x + y
+ "#
+ )?,
+ "hello, world!"
+ );
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_index"))]
+fn test_max_array_size() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ engine.set_max_array_size(10);
+
+ #[cfg(not(feature = "no_object"))]
+ engine.set_max_map_size(10);
+
+ assert_eq!(
+ *engine
+ .compile("let x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::LiteralTooLarge("Size of array literal".to_string(), 10)
+ );
+
+ assert!(matches!(
+ *engine
+ .run(
+ "
+ let x = [1,2,3,4,5,6];
+ let y = [7,8,9,10,11,12];
+ x + y
+ "
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataTooLarge(..)
+ ));
+
+ #[cfg(not(feature = "no_closure"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 42;
+ let y = [];
+ let f = || x;
+ for n in 0..10 {
+ y += x;
+ }
+ len(y)
+ "
+ )?,
+ 10
+ );
+
+ #[cfg(not(feature = "no_closure"))]
+ assert!(matches!(
+ *engine
+ .run(
+ "
+ let x = 42;
+ let y = [];
+ let f = || x;
+ for n in 0..11 {
+ y += x;
+ }
+ "
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataTooLarge(..)
+ ));
+
+ assert!(matches!(
+ *engine
+ .run(
+ "
+ let x = [ 42 ];
+ loop { x[0] = x; }
+ "
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataTooLarge(..)
+ ));
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = [1,2,3,4,5,6];
+ x.pad(10, 42);
+ len(x)
+ "
+ )?,
+ 10
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ assert!(matches!(
+ *engine
+ .run(
+ "
+ let x = [1,2,3,4,5,6];
+ x.pad(11, 42);
+ x
+ "
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataTooLarge(..)
+ ));
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = [1,2];
+ len([x, x, x])
+ "
+ )?,
+ 3
+ );
+
+ assert!(matches!(
+ *engine
+ .run(
+ "
+ let x = [1,2,3];
+ [x, x, x, x]
+ "
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataTooLarge(..)
+ ));
+
+ #[cfg(not(feature = "no_object"))]
+ assert!(matches!(
+ *engine
+ .run(
+ "
+ let x = #{a:1, b:2, c:3};
+ [x, x, x, x]
+ "
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataTooLarge(..)
+ ));
+
+ assert!(matches!(
+ *engine
+ .run(
+ "
+ let x = [1];
+ let y = [x, x];
+ let z = [y, y];
+ [z, z, z]
+ "
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataTooLarge(..)
+ ));
+
+ engine.set_max_array_size(0);
+
+ assert_eq!(
+ engine
+ .eval::<Array>(
+ "
+ let x = [1,2,3,4,5,6];
+ let y = [7,8,9,10,11,12];
+ x + y
+ "
+ )?
+ .len(),
+ 12
+ );
+
+ assert_eq!(
+ engine
+ .eval::<Array>(
+ "
+ let x = [1,2,3];
+ [x, x, x, x]
+ "
+ )?
+ .len(),
+ 4
+ );
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_max_map_size() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ engine.set_max_map_size(10);
+
+ #[cfg(not(feature = "no_index"))]
+ engine.set_max_array_size(10);
+
+ assert_eq!(
+ *engine
+ .compile(
+ "let x = #{a:1,b:2,c:3,d:4,e:5,f:6,g:7,h:8,i:9,j:10,k:11,l:12,m:13,n:14,o:15};"
+ )
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::LiteralTooLarge(
+ "Number of properties in object map literal".to_string(),
+ 10
+ )
+ );
+
+ assert!(matches!(
+ *engine
+ .run(
+ "
+ let x = #{};
+ loop { x.a = x; }
+ "
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataTooLarge(..)
+ ));
+
+ assert!(matches!(
+ *engine
+ .run(
+ "
+ let x = #{a:1,b:2,c:3,d:4,e:5,f:6};
+ let y = #{g:7,h:8,i:9,j:10,k:11,l:12};
+ x + y
+ "
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataTooLarge(..)
+ ));
+
+ assert!(matches!(
+ *engine
+ .run(
+ "
+ let x = #{a:1,b:2,c:3};
+ #{u:x, v:x, w:x, z:x}
+ "
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataTooLarge(..)
+ ));
+
+ #[cfg(not(feature = "no_index"))]
+ assert!(matches!(
+ *engine
+ .run(
+ "
+ let x = [1, 2, 3];
+ #{u:x, v:x, w:x, z:x}
+ "
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorDataTooLarge(..)
+ ));
+
+ engine.set_max_map_size(0);
+
+ assert_eq!(
+ engine
+ .eval::<Map>(
+ "
+ let x = #{a:1,b:2,c:3,d:4,e:5,f:6};
+ let y = #{g:7,h:8,i:9,j:10,k:11,l:12};
+ x + y
+ "
+ )?
+ .len(),
+ 12
+ );
+
+ assert_eq!(
+ engine
+ .eval::<Map>(
+ "
+ let x = #{a:1,b:2,c:3};
+ #{u:x, v:x, w:x, z:x}
+ "
+ )?
+ .len(),
+ 4
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/debugging.rs b/rhai/tests/debugging.rs
new file mode 100644
index 0000000..46b419c
--- /dev/null
+++ b/rhai/tests/debugging.rs
@@ -0,0 +1,86 @@
+#![cfg(feature = "debugging")]
+use rhai::{Dynamic, Engine, EvalAltResult, INT};
+
+#[cfg(not(feature = "no_index"))]
+use rhai::Array;
+
+#[cfg(not(feature = "no_object"))]
+use rhai::Map;
+
+#[test]
+fn test_debugging() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_debugger(
+ |_, dbg| dbg,
+ |_, _, _, _, _| Ok(rhai::debugger::DebuggerCommand::Continue),
+ );
+
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "no_index"))]
+ {
+ let r = engine.eval::<Array>(
+ "
+ fn foo(x) {
+ if x >= 5 {
+ back_trace()
+ } else {
+ foo(x+1)
+ }
+ }
+
+ foo(0)
+ ",
+ )?;
+
+ assert_eq!(r.len(), 6);
+
+ assert_eq!(engine.eval::<INT>("len(back_trace())")?, 0);
+ }
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_debugger_state() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_debugger(
+ |_, mut debugger| {
+ // Say, use an object map for the debugger state
+ let mut state = Map::new();
+ // Initialize properties
+ state.insert("hello".into(), (42 as INT).into());
+ state.insert("foo".into(), false.into());
+ debugger.set_state(state);
+ debugger
+ },
+ |mut context, _, _, _, _| {
+ // Print debugger state - which is an object map
+ println!(
+ "Current state = {}",
+ context.global_runtime_state().debugger().state()
+ );
+
+ // Modify state
+ let mut state = context
+ .global_runtime_state_mut()
+ .debugger_mut()
+ .state_mut()
+ .write_lock::<Map>()
+ .unwrap();
+ let hello = state.get("hello").unwrap().as_int().unwrap();
+ state.insert("hello".into(), (hello + 1).into());
+ state.insert("foo".into(), true.into());
+ state.insert("something_new".into(), "hello, world!".into());
+
+ // Continue with debugging
+ Ok(rhai::debugger::DebuggerCommand::StepInto)
+ },
+ );
+
+ engine.run("let x = 42;")?;
+
+ Ok(())
+}
diff --git a/rhai/tests/decrement.rs b/rhai/tests/decrement.rs
new file mode 100644
index 0000000..25a80b2
--- /dev/null
+++ b/rhai/tests/decrement.rs
@@ -0,0 +1,15 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_decrement() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("let x = 10; x -= 7; x")?, 3);
+
+ assert_eq!(
+ engine.eval::<String>(r#"let s = "test"; s -= 's'; s"#)?,
+ "tet"
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/eval.rs b/rhai/tests/eval.rs
new file mode 100644
index 0000000..bd55e72
--- /dev/null
+++ b/rhai/tests/eval.rs
@@ -0,0 +1,178 @@
+use rhai::{Engine, EvalAltResult, LexError, ParseErrorType, Scope, INT};
+
+#[test]
+fn test_eval() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>(r#"eval("40 + 2")"#)?, 42);
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let foo = 42;
+
+ eval("let foo = 123");
+ eval("let xyz = 10");
+
+ foo + xyz
+ "#
+ )?,
+ 133
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_eval_blocks() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let x = 999;
+
+ eval("let x = x - 1000");
+
+ let y = if x < 0 {
+ eval("let x = 42");
+ x
+ } else {
+ 0
+ };
+
+ x + y
+ "#
+ )?,
+ 41
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let foo = 42;
+
+ eval("{ let foo = 123; }");
+
+ foo
+ "#
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let foo = 42;
+ { { {
+ eval("let foo = 123");
+ } } }
+ foo
+ "#
+ )?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_module"))]
+#[test]
+fn test_eval_globals() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ const XYZ = 123;
+
+ fn foo() { global::XYZ }
+ {
+ eval("const XYZ = 42;");
+ }
+
+ foo()
+ "#
+ )?,
+ 123
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ const XYZ = 123;
+
+ fn foo() { global::XYZ }
+
+ eval("const XYZ = 42;");
+
+ foo()
+ "#
+ )?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_function"))]
+fn test_eval_function() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ r#"
+ let x = 10;
+
+ fn foo(x) { x += 12; x }
+
+ let script = "let y = x;"; // build a script
+ script += "y += foo(y);";
+ script += "x + y";
+
+ eval(script) + x + y
+ "#
+ )?,
+ 84
+ );
+
+ assert_eq!(
+ scope
+ .get_value::<INT>("x")
+ .expect("variable x should exist"),
+ 10
+ );
+
+ assert_eq!(
+ scope
+ .get_value::<INT>("y")
+ .expect("variable y should exist"),
+ 32
+ );
+
+ assert!(scope.contains("script"));
+ assert_eq!(scope.len(), 3);
+
+ Ok(())
+}
+
+#[test]
+fn test_eval_disabled() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.disable_symbol("eval");
+
+ assert!(matches!(
+ engine
+ .compile(r#"eval("40 + 2")"#)
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::BadInput(LexError::ImproperSymbol(err, ..)) if err == "eval"
+ ));
+
+ Ok(())
+}
diff --git a/rhai/tests/expressions.rs b/rhai/tests/expressions.rs
new file mode 100644
index 0000000..e15008e
--- /dev/null
+++ b/rhai/tests/expressions.rs
@@ -0,0 +1,136 @@
+use rhai::{Engine, EvalAltResult, Scope, INT};
+
+#[test]
+fn test_expressions() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+
+ scope.push("x", 10 as INT);
+
+ assert_eq!(engine.eval_expression::<INT>("2 + (10 + 10) * 2")?, 42);
+ assert_eq!(
+ engine.eval_expression_with_scope::<INT>(&mut scope, "2 + (x + 10) * 2")?,
+ 42
+ );
+ assert_eq!(
+ engine.eval_expression_with_scope::<INT>(&mut scope, "if x > 0 { 42 } else { 123 }")?,
+ 42
+ );
+ #[cfg(not(feature = "no_index"))]
+ #[cfg(not(feature = "no_object"))]
+ #[cfg(not(feature = "no_function"))]
+ {
+ assert_eq!(
+ engine.eval_expression_with_scope::<INT>(
+ &mut scope,
+ "[1, 2, 3, 4].map(|x| x * x).reduce(|a, v| a + v, 0)"
+ )?,
+ 30
+ );
+ assert!(engine
+ .eval_expression_with_scope::<INT>(
+ &mut scope,
+ "[1, 2, 3, 4].map(|x| { let r = 2; x * r }).reduce(|a, v| a + v, 0)"
+ )
+ .is_err());
+ }
+ assert!(engine
+ .eval_expression_with_scope::<INT>(&mut scope, "if x > 0 { let y = 42; y } else { 123 }")
+ .is_err());
+ assert!(engine
+ .eval_expression_with_scope::<INT>(&mut scope, "if x > 0 { 42 } else { let y = 123; y }")
+ .is_err());
+ assert!(engine
+ .eval_expression_with_scope::<INT>(&mut scope, "if x > 0 { 42 } else {}")
+ .is_err());
+
+ assert_eq!(
+ engine.eval_expression_with_scope::<INT>(
+ &mut scope,
+ "
+ switch x {
+ 0 => 1,
+ 10 => 42,
+ 1..10 => 123,
+ }
+ "
+ )?,
+ 42
+ );
+ assert!(engine
+ .eval_expression_with_scope::<INT>(
+ &mut scope,
+ "
+ switch x {
+ 0 => 1,
+ 10 => 42,
+ 1..10 => {
+ let y = 123;
+ y
+ }
+ }
+ "
+ )
+ .is_err());
+
+ assert!(engine.compile_expression("40 + 2;").is_err());
+ assert!(engine.compile_expression("40 + { 2 }").is_err());
+ assert!(engine.compile_expression("x = 42").is_err());
+ assert!(engine.compile_expression("let x = 42").is_err());
+ assert!(engine
+ .compile_expression("do { break 42; } while true")
+ .is_err());
+
+ engine.compile("40 + { let x = 2; x }")?;
+
+ Ok(())
+}
+
+/// This example taken from https://github.com/rhaiscript/rhai/issues/115
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_expressions_eval() -> Result<(), Box<EvalAltResult>> {
+ #[allow(clippy::upper_case_acronyms)]
+ #[derive(Debug, Clone)]
+ struct AGENT {
+ pub gender: String,
+ pub age: INT,
+ }
+
+ impl AGENT {
+ pub fn get_gender(&mut self) -> String {
+ self.gender.clone()
+ }
+ pub fn get_age(&mut self) -> INT {
+ self.age
+ }
+ }
+
+ // This is your agent
+ let my_agent = AGENT {
+ gender: "male".into(),
+ age: 42,
+ };
+
+ // Create the engine
+ let mut engine = Engine::new();
+
+ // Register your AGENT type
+ engine.register_type_with_name::<AGENT>("AGENT");
+ engine.register_get("gender", AGENT::get_gender);
+ engine.register_get("age", AGENT::get_age);
+
+ // Create your scope, add the agent as a constant
+ let mut scope = Scope::new();
+ scope.push_constant("agent", my_agent);
+
+ // Evaluate the expression
+ assert!(engine.eval_expression_with_scope(
+ &mut scope,
+ r#"
+ agent.age > 10 && agent.gender == "male"
+ "#,
+ )?);
+
+ Ok(())
+}
diff --git a/rhai/tests/float.rs b/rhai/tests/float.rs
new file mode 100644
index 0000000..a44243e
--- /dev/null
+++ b/rhai/tests/float.rs
@@ -0,0 +1,94 @@
+#![cfg(not(feature = "no_float"))]
+use rhai::{Engine, EvalAltResult, FLOAT};
+
+const EPSILON: FLOAT = 0.000_000_000_1;
+
+#[test]
+fn test_float() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!(engine.eval::<bool>("let x = 0.0; let y = 1.0; x < y")?);
+ assert!(!engine.eval::<bool>("let x = 0.0; let y = 1.0; x > y")?);
+ assert!(!engine.eval::<bool>("let x = 0.; let y = 1.; x > y")?);
+ assert!((engine.eval::<FLOAT>("let x = 9.9999; x")? - 9.9999 as FLOAT).abs() < EPSILON);
+
+ Ok(())
+}
+
+#[test]
+fn test_float_scientific() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!(engine.eval::<bool>("123.456 == 1.23456e2")?);
+ assert!(engine.eval::<bool>("123.456 == 1.23456e+2")?);
+ assert!(engine.eval::<bool>("123.456 == 123456e-3")?);
+
+ Ok(())
+}
+
+#[test]
+fn test_float_parse() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!((engine.eval::<FLOAT>(r#"parse_float("9.9999")"#)? - 9.9999 as FLOAT).abs() < EPSILON);
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_struct_with_float() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Clone)]
+ struct TestStruct {
+ x: FLOAT,
+ }
+
+ impl TestStruct {
+ fn update(&mut self) {
+ self.x += 5.789;
+ }
+
+ fn get_x(&mut self) -> FLOAT {
+ self.x
+ }
+
+ fn set_x(&mut self, new_x: FLOAT) {
+ self.x = new_x;
+ }
+
+ fn new() -> Self {
+ Self { x: 1.0 }
+ }
+ }
+
+ let mut engine = Engine::new();
+
+ engine.register_type::<TestStruct>();
+
+ engine.register_get_set("x", TestStruct::get_x, TestStruct::set_x);
+ engine.register_fn("update", TestStruct::update);
+ engine.register_fn("new_ts", TestStruct::new);
+
+ assert!(
+ (engine.eval::<FLOAT>("let ts = new_ts(); ts.update(); ts.x")? - 6.789).abs() < EPSILON
+ );
+ assert!(
+ (engine.eval::<FLOAT>("let ts = new_ts(); ts.x = 10.1001; ts.x")? - 10.1001).abs()
+ < EPSILON
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_float_func() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_fn("sum", |x: FLOAT, y: FLOAT, z: FLOAT, w: FLOAT| {
+ x + y + z + w
+ });
+
+ assert_eq!(engine.eval::<FLOAT>("sum(1.0, 2.0, 3.0, 4.0)")?, 10.0);
+
+ Ok(())
+}
diff --git a/rhai/tests/fn_ptr.rs b/rhai/tests/fn_ptr.rs
new file mode 100644
index 0000000..1d587e2
--- /dev/null
+++ b/rhai/tests/fn_ptr.rs
@@ -0,0 +1,167 @@
+use rhai::{Engine, EvalAltResult, FnPtr, INT};
+
+#[test]
+fn test_fn_ptr() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_fn("bar", |x: &mut INT, y: INT| *x += y);
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let f = Fn("bar");
+ let x = 40;
+ f.call(x, 2);
+ x
+ "#
+ )?,
+ 40
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let f = Fn("bar");
+ let x = 40;
+ x.call(f, 2);
+ x
+ "#
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let f = Fn("bar");
+ let x = 40;
+ call(f, x, 2);
+ x
+ "#
+ )?,
+ 42
+ );
+
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ fn foo(x) { this += x; }
+
+ let f = Fn("foo");
+ let x = 40;
+ x.call(f, 2);
+ x
+ "#
+ )?,
+ 42
+ );
+
+ #[cfg(not(feature = "no_function"))]
+ assert!(matches!(
+ *engine
+ .eval::<INT>(
+ r#"
+ fn foo(x) { this += x; }
+
+ let f = Fn("foo");
+ call(f, 2);
+ x
+ "#
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorInFunctionCall(fn_name, _, err, ..)
+ if fn_name == "foo" && matches!(*err, EvalAltResult::ErrorUnboundThis(..))
+ ));
+
+ #[cfg(not(feature = "no_function"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ fn foo(x) { x + 1 }
+ let f = foo;
+ let g = 42;
+ g = foo;
+ call(f, 39) + call(g, 1)
+ "#
+ )?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_fn_ptr_curry() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_fn("foo", |x: &mut INT, y: INT| *x + y);
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let f = Fn("foo");
+ let f2 = f.curry(40);
+ f2.call(2)
+ "#
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let f = Fn("foo");
+ let f2 = curry(f, 40);
+ call(f2, 2)
+ "#
+ )?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_function"))]
+fn test_fn_ptr_call() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ let ast = engine.compile("private fn foo(x, y) { len(x) + y }")?;
+
+ let mut fn_ptr = FnPtr::new("foo")?;
+ fn_ptr.set_curry(vec!["abc".into()]);
+ let result: INT = fn_ptr.call(&engine, &ast, (39 as INT,))?;
+
+ assert_eq!(result, 42);
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_closure"))]
+fn test_fn_ptr_make_closure() -> Result<(), Box<EvalAltResult>> {
+ let f = {
+ let engine = Engine::new();
+
+ let ast = engine.compile(
+ r#"
+ let test = "hello";
+ |x| test + x // this creates a closure
+ "#,
+ )?;
+
+ let fn_ptr = engine.eval_ast::<FnPtr>(&ast)?;
+
+ move |x: INT| -> Result<String, _> { fn_ptr.call(&engine, &ast, (x,)) }
+ };
+
+ // 'f' captures: the Engine, the AST, and the closure
+ assert_eq!(f(42)?, "hello42");
+
+ Ok(())
+}
diff --git a/rhai/tests/for.rs b/rhai/tests/for.rs
new file mode 100644
index 0000000..48139e7
--- /dev/null
+++ b/rhai/tests/for.rs
@@ -0,0 +1,423 @@
+use rhai::{Engine, EvalAltResult, Module, INT};
+
+#[cfg(not(feature = "no_float"))]
+use rhai::FLOAT;
+
+#[cfg(feature = "decimal")]
+#[cfg(not(feature = "no_float"))]
+use rust_decimal::Decimal;
+
+#[test]
+fn test_for_loop() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let sum1 = 0;
+ let sum2 = 0;
+ let inputs = [1, 2, 3, 4, 5];
+
+ for x in inputs {
+ sum1 += x;
+ }
+
+ for x in range(1, 6) {
+ sum2 += x;
+ }
+
+ for x in range(1, 6, 3) {
+ sum2 += x;
+ }
+
+ sum1 + sum2
+ "
+ )?,
+ 35
+ );
+
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let sum = 0;
+ let inputs = [1, 2, 3, 4, 5];
+
+ for (x, i) in inputs {
+ sum += x * (i + 1);
+ }
+ sum
+ "
+ )?,
+ 55
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let sum = 0;
+ for x in range(1, 10) { sum += x; }
+ sum
+ "
+ )?,
+ 45
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let sum = 0;
+ for x in 1..10 { sum += x; }
+ sum
+ "
+ )?,
+ 45
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let sum = 0;
+ for x in 1..=10 { sum += x; }
+ sum
+ "
+ )?,
+ 55
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let sum = 0;
+ for x in range(1, 10, 2) { sum += x; }
+ sum
+ "
+ )?,
+ 25
+ );
+
+ #[cfg(not(feature = "unchecked"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let sum = 0;
+ for x in range(10, 1, 2) { sum += x; }
+ sum
+ "
+ )?,
+ 0
+ );
+
+ #[cfg(not(feature = "unchecked"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let sum = 0;
+ for x in range(1, 10, -2) { sum += x; }
+ sum
+ "
+ )?,
+ 0
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let sum = 0;
+ for x in range(10, 1, -2) { sum += x; }
+ sum
+ "
+ )?,
+ 30
+ );
+
+ #[cfg(not(feature = "no_float"))]
+ {
+ assert_eq!(
+ engine.eval::<FLOAT>(
+ "
+ let sum = 0.0;
+ for x in range(1.0, 10.0, 2.0) { sum += x; }
+ sum
+ "
+ )?,
+ 25.0
+ );
+
+ #[cfg(not(feature = "unchecked"))]
+ assert_eq!(
+ engine.eval::<FLOAT>(
+ "
+ let sum = 0.0;
+ for x in range(10.0, 1.0, 2.0) { sum += x; }
+ sum
+ "
+ )?,
+ 0.0
+ );
+
+ #[cfg(not(feature = "unchecked"))]
+ assert_eq!(
+ engine.eval::<FLOAT>(
+ "
+ let sum = 0.0;
+ for x in range(1.0, 10.0, -2.0) { sum += x; }
+ sum
+ "
+ )?,
+ 0.0
+ );
+
+ assert_eq!(
+ engine.eval::<FLOAT>(
+ "
+ let sum = 0.0;
+ for x in range(10.0, 1.0, -2.0) { sum += x; }
+ sum
+ "
+ )?,
+ 30.0
+ );
+ }
+
+ #[cfg(not(feature = "no_float"))]
+ #[cfg(feature = "decimal")]
+ {
+ assert_eq!(
+ engine.eval::<Decimal>(
+ "
+ let sum = to_decimal(0);
+ for x in range(to_decimal(1), to_decimal(10), to_decimal(2)) { sum += x; }
+ sum
+ "
+ )?,
+ Decimal::from(25)
+ );
+
+ #[cfg(not(feature = "unchecked"))]
+ assert_eq!(
+ engine.eval::<Decimal>(
+ "
+ let sum = to_decimal(0);
+ for x in range(to_decimal(10), to_decimal(1), to_decimal(2)) { sum += x; }
+ sum
+ "
+ )?,
+ Decimal::from(0)
+ );
+
+ #[cfg(not(feature = "unchecked"))]
+ assert_eq!(
+ engine.eval::<Decimal>(
+ "
+ let sum = to_decimal(0);
+ for x in range(to_decimal(1), to_decimal(10), to_decimal(-2)) { sum += x; }
+ sum
+ "
+ )?,
+ Decimal::from(0)
+ );
+
+ assert_eq!(
+ engine.eval::<Decimal>(
+ "
+ let sum = to_decimal(0);
+ for x in range(to_decimal(10), to_decimal(1), to_decimal(-2)) { sum += x; }
+ sum
+ "
+ )?,
+ Decimal::from(30)
+ );
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ #[cfg(not(feature = "no_object"))]
+ #[cfg(not(feature = "no_float"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let a = [123, 999, 42, 0, true, "hello", "world!", 987.654];
+
+ for (item, count) in a {
+ switch item.type_of() {
+ "i64" | "i32" if item.is_even => break count,
+ "f64" | "f32" if item.to_int().is_even => break count,
+ }
+ }
+ "#
+ )?,
+ 2
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "unchecked"))]
+#[test]
+fn test_for_overflow() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ #[cfg(not(feature = "only_i32"))]
+ let script = "
+ let sum = 0;
+
+ for x in range(9223372036854775807, 0, 9223372036854775807) {
+ sum += 1;
+ }
+
+ sum
+ ";
+ #[cfg(feature = "only_i32")]
+ let script = "
+ let sum = 0;
+
+ for x in range(2147483647 , 0, 2147483647 ) {
+ sum += 1;
+ }
+
+ sum
+ ";
+
+ assert_eq!(engine.eval::<INT>(script)?, 0);
+
+ Ok(())
+}
+
+#[test]
+fn test_for_string() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let s = "hello";
+ let sum = 0;
+
+ for ch in chars(s) {
+ sum += to_int(ch);
+ }
+
+ sum
+ "#
+ )?,
+ 532
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let s = "hello";
+ let sum = 0;
+
+ for ch in chars(s, 2..=3) {
+ sum += to_int(ch);
+ }
+
+ sum
+ "#
+ )?,
+ 216
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_object"))]
+#[cfg(not(feature = "no_index"))]
+#[test]
+fn test_for_object() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ let script = r#"
+ let sum = 0;
+ let keys = "";
+ let map = #{a: 1, b: 2, c: 3};
+
+ for key in map.keys() {
+ keys += key;
+ }
+ for value in map.values() {
+ sum += value;
+ }
+
+ keys.len + sum
+ "#;
+
+ assert_eq!(engine.eval::<INT>(script)?, 9);
+
+ Ok(())
+}
+
+#[derive(Debug, Clone)]
+struct MyIterableType(String);
+
+impl IntoIterator for MyIterableType {
+ type Item = char;
+ type IntoIter = std::vec::IntoIter<Self::Item>;
+
+ #[inline]
+ #[must_use]
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.chars().collect::<Vec<_>>().into_iter()
+ }
+}
+
+#[cfg(not(feature = "no_module"))]
+#[test]
+fn test_for_module_iterator() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ // Set a type iterator deep inside a nested module chain
+ let mut sub_module = Module::new();
+ sub_module.set_iterable::<MyIterableType>();
+ sub_module.set_native_fn("new_ts", || Ok(MyIterableType("hello".to_string())));
+
+ let mut module = Module::new();
+ module.set_sub_module("inner", sub_module);
+
+ engine.register_static_module("testing", module.into());
+
+ let script = r#"
+ let item = testing::inner::new_ts();
+ let result = "";
+
+ for x in item {
+ result += x;
+ }
+ result
+ "#;
+
+ assert_eq!(engine.eval::<String>(script)?, "hello");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_index"))]
+#[cfg(not(feature = "no_closure"))]
+fn test_for_capture() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let a = [];
+
+ for (x, i) in 100..110 {
+ a += || i + x;
+ }
+
+ let sum = 0;
+
+ for fp in a {
+ sum += call(fp);
+ }
+
+ sum
+ "
+ )?,
+ 1180
+ );
+ Ok(())
+}
diff --git a/rhai/tests/functions.rs b/rhai/tests/functions.rs
new file mode 100644
index 0000000..ef873ab
--- /dev/null
+++ b/rhai/tests/functions.rs
@@ -0,0 +1,241 @@
+#![cfg(not(feature = "no_function"))]
+use rhai::{Engine, EvalAltResult, FnNamespace, Module, NativeCallContext, Shared, INT};
+
+#[cfg(not(feature = "no_object"))]
+#[test]
+fn test_functions_trait_object() -> Result<(), Box<EvalAltResult>> {
+ trait TestTrait {
+ fn greet(&self) -> INT;
+ }
+
+ #[allow(clippy::upper_case_acronyms)]
+ #[derive(Debug, Clone)]
+ struct ABC(INT);
+
+ impl TestTrait for ABC {
+ fn greet(&self) -> INT {
+ self.0
+ }
+ }
+
+ #[cfg(not(feature = "sync"))]
+ type MySharedTestTrait = Shared<dyn TestTrait>;
+
+ #[cfg(feature = "sync")]
+ type MySharedTestTrait = Shared<dyn TestTrait + Send + Sync>;
+
+ let mut engine = Engine::new();
+
+ engine
+ .register_type_with_name::<MySharedTestTrait>("MySharedTestTrait")
+ .register_fn("new_ts", || Shared::new(ABC(42)) as MySharedTestTrait)
+ .register_fn("greet", |x: MySharedTestTrait| x.greet());
+
+ assert_eq!(
+ engine.eval::<String>("type_of(new_ts())")?,
+ "MySharedTestTrait"
+ );
+ assert_eq!(engine.eval::<INT>("let x = new_ts(); greet(x)")?, 42);
+
+ Ok(())
+}
+
+#[test]
+fn test_functions_namespaces() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ #[cfg(not(feature = "no_module"))]
+ {
+ let mut m = Module::new();
+ let hash = m.set_native_fn("test", || Ok(999 as INT));
+ m.update_fn_namespace(hash, FnNamespace::Global);
+
+ engine.register_static_module("hello", m.into());
+
+ let mut m = Module::new();
+ m.set_var("ANSWER", 123 as INT);
+
+ assert_eq!(engine.eval::<INT>("test()")?, 999);
+
+ assert_eq!(engine.eval::<INT>("fn test() { 123 } test()")?, 123);
+ }
+
+ engine.register_fn("test", || 42 as INT);
+
+ assert_eq!(engine.eval::<INT>("fn test() { 123 } test()")?, 123);
+
+ assert_eq!(engine.eval::<INT>("test()")?, 42);
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_module"))]
+#[test]
+fn test_functions_global_module() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ const ANSWER = 42;
+ fn foo() { global::ANSWER }
+ foo()
+ "
+ )?,
+ 42
+ );
+
+ assert!(matches!(*engine.run(
+ "
+ fn foo() { global::ANSWER }
+
+ {
+ const ANSWER = 42;
+ foo()
+ }
+ ").unwrap_err(),
+ EvalAltResult::ErrorInFunctionCall(.., err, _)
+ if matches!(&*err, EvalAltResult::ErrorVariableNotFound(v, ..) if v == "global::ANSWER")
+ ));
+
+ engine.register_fn(
+ "do_stuff",
+ |context: NativeCallContext, callback: rhai::FnPtr| -> Result<INT, _> {
+ callback.call_within_context(&context, ())
+ },
+ );
+
+ #[cfg(not(feature = "no_closure"))]
+ assert!(matches!(*engine.run(
+ "
+ do_stuff(|| {
+ const LOCAL_VALUE = 42;
+ global::LOCAL_VALUE
+ });
+ ").unwrap_err(),
+ EvalAltResult::ErrorInFunctionCall(.., err, _)
+ if matches!(&*err, EvalAltResult::ErrorVariableNotFound(v, ..) if v == "global::LOCAL_VALUE")
+ ));
+
+ #[cfg(not(feature = "no_closure"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ const GLOBAL_VALUE = 42;
+ do_stuff(|| global::GLOBAL_VALUE);
+ "
+ )?,
+ 42
+ );
+
+ // Override global
+ let mut module = Module::new();
+ module.set_var("ANSWER", 123 as INT);
+ engine.register_static_module("global", module.into());
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ const ANSWER = 42;
+ fn foo() { global::ANSWER }
+ foo()
+ "
+ )?,
+ 123
+ );
+
+ // Other globals
+ let mut module = Module::new();
+ module.set_var("ANSWER", 123 as INT);
+ engine.register_global_module(module.into());
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ fn foo() { global::ANSWER }
+ foo()
+ "
+ )?,
+ 123
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_functions_bang() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ fn foo() {
+ hello + bar
+ }
+
+ let hello = 42;
+ let bar = 123;
+
+ foo!()
+ ",
+ )?,
+ 165
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ fn foo() {
+ hello = 0;
+ hello + bar
+ }
+
+ let hello = 42;
+ let bar = 123;
+
+ foo!()
+ ",
+ )?,
+ 123
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ fn foo() {
+ let hello = bar + 42;
+ }
+
+ let bar = 999;
+ let hello = 123;
+
+ foo!();
+
+ hello
+ ",
+ )?,
+ 123
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ fn foo(x) {
+ let hello = bar + 42 + x;
+ }
+
+ let bar = 999;
+ let hello = 123;
+
+ let f = Fn("foo");
+
+ call!(f, 1);
+
+ hello
+ "#,
+ )?,
+ 123
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/get_set.rs b/rhai/tests/get_set.rs
new file mode 100644
index 0000000..62e5780
--- /dev/null
+++ b/rhai/tests/get_set.rs
@@ -0,0 +1,408 @@
+#![cfg(not(feature = "no_object"))]
+
+use rhai::{Engine, EvalAltResult, NativeCallContext, Scope, INT};
+
+#[test]
+fn test_get_set() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Clone)]
+ struct TestStruct {
+ x: INT,
+ y: INT,
+ array: Vec<INT>,
+ }
+
+ impl TestStruct {
+ fn get_x(&mut self) -> INT {
+ self.x
+ }
+
+ fn set_x(&mut self, new_x: INT) {
+ self.x = new_x;
+ }
+
+ fn get_y(&mut self) -> INT {
+ self.y
+ }
+
+ fn new() -> Self {
+ Self {
+ x: 1,
+ y: 0,
+ array: vec![1, 2, 3, 4, 5],
+ }
+ }
+ }
+
+ let mut engine = Engine::new();
+
+ engine.register_type::<TestStruct>();
+
+ engine.register_get_set("x", TestStruct::get_x, TestStruct::set_x);
+ engine.register_get("y", TestStruct::get_y);
+ engine.register_fn("add", |value: &mut INT| *value += 41);
+ engine.register_fn("new_ts", TestStruct::new);
+
+ assert_eq!(engine.eval::<INT>("let a = new_ts(); a.x = 500; a.x")?, 500);
+ assert_eq!(engine.eval::<INT>("let a = new_ts(); a.x.add(); a.x")?, 42);
+ assert_eq!(engine.eval::<INT>("let a = new_ts(); a.y.add(); a.y")?, 0);
+
+ engine.register_indexer_get_set(
+ |value: &mut TestStruct, index: &str| value.array[index.len()],
+ |value: &mut TestStruct, index: &str, new_val: INT| value.array[index.len()] = new_val,
+ );
+
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(engine.eval::<INT>(r#"let a = new_ts(); a["abc"]"#)?, 4);
+
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ engine.eval::<INT>(r#"let a = new_ts(); a["abc"] = 42; a["abc"]"#)?,
+ 42
+ );
+
+ assert_eq!(engine.eval::<INT>(r"let a = new_ts(); a.abc")?, 4);
+ assert_eq!(
+ engine.eval::<INT>(r"let a = new_ts(); a.abc = 42; a.abc")?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_get_set_chain_with_write_back() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Clone)]
+ struct TestChild {
+ x: INT,
+ }
+
+ impl TestChild {
+ fn get_x(&mut self) -> INT {
+ self.x
+ }
+
+ fn set_x(&mut self, new_x: INT) {
+ self.x = new_x;
+ }
+
+ fn new() -> TestChild {
+ TestChild { x: 1 }
+ }
+ }
+
+ #[derive(Clone)]
+ struct TestParent {
+ child: TestChild,
+ }
+
+ impl TestParent {
+ fn get_child(&mut self) -> TestChild {
+ self.child.clone()
+ }
+
+ fn set_child(&mut self, new_child: TestChild) {
+ self.child = new_child;
+ }
+
+ fn new() -> TestParent {
+ TestParent {
+ child: TestChild::new(),
+ }
+ }
+ }
+
+ let mut engine = Engine::new();
+
+ engine.register_type::<TestChild>();
+ engine.register_type_with_name::<TestParent>("TestParent");
+
+ engine.register_get_set("x", TestChild::get_x, TestChild::set_x);
+ engine.register_get_set("child", TestParent::get_child, TestParent::set_child);
+
+ #[cfg(not(feature = "no_index"))]
+ engine.register_indexer_get_set(
+ |parent: &mut TestParent, _: INT| parent.child.clone(),
+ |parent: &mut TestParent, n: INT, mut new_child: TestChild| {
+ new_child.x *= n;
+ parent.child = new_child;
+ },
+ );
+
+ engine.register_fn("new_tp", TestParent::new);
+ engine.register_fn("new_tc", TestChild::new);
+
+ assert_eq!(engine.eval::<INT>("let a = new_tp(); a.child.x")?, 1);
+ assert_eq!(
+ engine.eval::<INT>("let a = new_tp(); a.child.x = 42; a.child.x")?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<String>("let a = new_tp(); type_of(a)")?,
+ "TestParent"
+ );
+
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ engine.eval::<INT>("let a = new_tp(); let c = new_tc(); c.x = 123; a[2] = c; a.child.x")?,
+ 246
+ );
+
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ engine.eval::<INT>("let a = new_tp(); a[2].x = 42; a.child.x")?,
+ 84
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_get_set_op_assignment() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Clone, Debug, Eq, PartialEq)]
+ struct Num(INT);
+
+ impl Num {
+ fn get(&mut self) -> INT {
+ self.0
+ }
+ fn set(&mut self, x: INT) {
+ self.0 = x;
+ }
+ }
+
+ let mut engine = Engine::new();
+
+ engine
+ .register_type::<Num>()
+ .register_fn("new_ts", || Num(40))
+ .register_get_set("v", Num::get, Num::set);
+
+ assert_eq!(
+ engine.eval::<Num>("let a = new_ts(); a.v = a.v + 2; a")?,
+ Num(42)
+ );
+ assert_eq!(
+ engine.eval::<Num>("let a = new_ts(); a.v += 2; a")?,
+ Num(42)
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_get_set_chain_without_write_back() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, Clone)]
+ struct Outer {
+ pub inner: Inner,
+ }
+
+ #[derive(Debug, Clone)]
+ struct Inner {
+ pub value: INT,
+ }
+
+ let mut engine = Engine::new();
+ let mut scope = Scope::new();
+
+ scope.push(
+ "outer",
+ Outer {
+ inner: Inner { value: 42 },
+ },
+ );
+
+ engine
+ .register_type::<Inner>()
+ .register_get_set(
+ "value",
+ |t: &mut Inner| t.value,
+ |_: NativeCallContext, _: &mut Inner, new: INT| {
+ panic!("Inner::value setter called with {}", new)
+ },
+ )
+ .register_type::<Outer>()
+ .register_get_set(
+ "inner",
+ |_: NativeCallContext, t: &mut Outer| t.inner.clone(),
+ |_: &mut Outer, new: Inner| panic!("Outer::inner setter called with {:?}", new),
+ );
+
+ #[cfg(not(feature = "no_index"))]
+ engine.register_indexer_get_set(
+ |t: &mut Outer, n: INT| Inner {
+ value: t.inner.value * n,
+ },
+ |_: &mut Outer, n: INT, new: Inner| {
+ panic!("Outer::inner index setter called with {} and {:?}", n, new)
+ },
+ );
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope, "outer.inner.value")?,
+ 42
+ );
+
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope, "outer[2].value")?,
+ 84
+ );
+
+ engine.run_with_scope(&mut scope, "print(outer.inner.value)")?;
+
+ #[cfg(not(feature = "no_index"))]
+ engine.run_with_scope(&mut scope, "print(outer[0].value)")?;
+
+ Ok(())
+}
+
+#[test]
+fn test_get_set_collection() -> Result<(), Box<EvalAltResult>> {
+ type MyItem = INT;
+ type MyBag = std::collections::BTreeSet<MyItem>;
+
+ let mut engine = Engine::new();
+
+ engine
+ .register_type_with_name::<MyBag>("MyBag")
+ .register_iterator::<MyBag>()
+ .register_fn("new_bag", MyBag::new)
+ .register_fn("len", |col: &mut MyBag| col.len() as INT)
+ .register_get("len", |col: &mut MyBag| col.len() as INT)
+ .register_fn("clear", |col: &mut MyBag| col.clear())
+ .register_fn("contains", |col: &mut MyBag, item: INT| col.contains(&item))
+ .register_fn("add", |col: &mut MyBag, item: MyItem| col.insert(item))
+ .register_fn("+=", |col: &mut MyBag, item: MyItem| col.insert(item))
+ .register_fn("remove", |col: &mut MyBag, item: MyItem| col.remove(&item))
+ .register_fn("-=", |col: &mut MyBag, item: MyItem| col.remove(&item))
+ .register_fn("+", |mut col1: MyBag, col2: MyBag| {
+ col1.extend(col2.into_iter());
+ col1
+ });
+
+ let result = engine.eval::<INT>(
+ "
+ let bag = new_bag();
+
+ bag += 1;
+ bag += 2;
+ bag += 39;
+ bag -= 2;
+
+ if !bag.contains(2) {
+ let sum = 0;
+ for n in bag { sum += n; }
+ sum + bag.len
+ } else {
+ -1
+ }
+ ",
+ )?;
+
+ assert_eq!(result, 42);
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_index"))]
+#[test]
+fn test_get_set_indexer() -> Result<(), Box<EvalAltResult>> {
+ type MyMap = std::collections::BTreeMap<String, INT>;
+
+ let mut engine = Engine::new();
+
+ engine
+ .register_type_with_name::<MyMap>("MyMap")
+ .register_fn("new_map", MyMap::new)
+ .register_indexer_get(
+ |map: &mut MyMap, index: &str| -> Result<_, Box<EvalAltResult>> {
+ map.get(index).cloned().ok_or_else(|| {
+ EvalAltResult::ErrorIndexNotFound(index.into(), rhai::Position::NONE).into()
+ })
+ },
+ )
+ .register_indexer_set(|map: &mut MyMap, index: &str, value: INT| {
+ map.insert(index.to_string(), value);
+ });
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let my_map = new_map();
+ my_map["eggs"] = 42;
+ my_map["eggs"]
+ "#,
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let my_map = new_map();
+ my_map["eggs"] = 41;
+ my_map["eggs"] = my_map["eggs"] + 1;
+ my_map["eggs"]
+ "#,
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let my_map = new_map();
+ my_map["eggs"] = 41;
+ my_map["eggs"] += 1;
+ my_map["eggs"]
+ "#,
+ )?,
+ 42
+ );
+
+ assert!(engine
+ .eval::<INT>(
+ r#"
+ let my_map = new_map();
+ my_map["eggs"] = 42;
+ my_map["not_found"]
+ "#,
+ )
+ .is_err());
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let my_map = new_map();
+ my_map["eggs"] = 42;
+
+ try {
+ let eggs = my_map["eggs"];
+ let eggs = my_map["not found"];
+ }
+ catch(x)
+ {
+ print("Not found!");
+ }
+
+ my_map["eggs"]
+ "#,
+ )?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_get_set_elvis() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ engine.eval::<()>("let x = (); x?.foo.bar.baz")?;
+ engine.eval::<()>("let x = (); x?.foo(1,2,3)")?;
+ engine.eval::<()>("let x = #{a:()}; x.a?.foo.bar.baz")?;
+ assert_eq!(engine.eval::<String>("let x = 'x'; x?.type_of()")?, "char");
+
+ Ok(())
+}
diff --git a/rhai/tests/if_block.rs b/rhai/tests/if_block.rs
new file mode 100644
index 0000000..ca9eaa2
--- /dev/null
+++ b/rhai/tests/if_block.rs
@@ -0,0 +1,47 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_if() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("if true { 55 }")?, 55);
+ assert_eq!(engine.eval::<INT>("if false { 55 } else { 44 }")?, 44);
+ assert_eq!(engine.eval::<INT>("if true { 55 } else { 44 }")?, 55);
+ assert_eq!(
+ engine.eval::<INT>("if false { 55 } else if true { 33 } else { 44 }")?,
+ 33
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ if false { 55 }
+ else if false { 33 }
+ else if false { 66 }
+ else if false { 77 }
+ else if false { 88 }
+ else { 44 }
+ "
+ )?,
+ 44
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_if_expr() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 42;
+ let y = 1 + if x > 40 { 100 } else { 0 } / x;
+ y
+ "
+ )?,
+ 3
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/increment.rs b/rhai/tests/increment.rs
new file mode 100644
index 0000000..4ab5911
--- /dev/null
+++ b/rhai/tests/increment.rs
@@ -0,0 +1,15 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_increment() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("let x = 1; x += 2; x")?, 3);
+
+ assert_eq!(
+ engine.eval::<String>(r#"let s = "test"; s += "ing"; s"#)?,
+ "testing"
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/internal_fn.rs b/rhai/tests/internal_fn.rs
new file mode 100644
index 0000000..38c0674
--- /dev/null
+++ b/rhai/tests/internal_fn.rs
@@ -0,0 +1,324 @@
+#![cfg(not(feature = "no_function"))]
+
+use rhai::{Engine, EvalAltResult, ParseErrorType, INT};
+
+#[test]
+fn test_internal_fn() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>("fn add_me(a, b) { a+b } add_me(3, 4)")?,
+ 7
+ );
+
+ assert_eq!(
+ engine.eval::<INT>("fn add_me(a, b,) { a+b } add_me(3, 4,)")?,
+ 7
+ );
+
+ assert_eq!(engine.eval::<INT>("fn bob() { return 4; 5 } bob()")?, 4);
+
+ assert_eq!(engine.eval::<INT>("fn add(x, n) { x + n } add(40, 2)")?, 42);
+
+ assert_eq!(
+ engine.eval::<INT>("fn add(x, n,) { x + n } add(40, 2,)")?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>("fn add(x, n) { x + n } let a = 40; add(a, 2); a")?,
+ 40
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>("fn add(n) { this + n } let x = 40; x.add(2)")?,
+ 42
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>("fn add(n) { this += n; } let x = 40; x.add(2); x")?,
+ 42
+ );
+
+ assert_eq!(engine.eval::<INT>("fn mul2(x) { x * 2 } mul2(21)")?, 42);
+
+ assert_eq!(
+ engine.eval::<INT>("fn mul2(x) { x *= 2 } let a = 21; mul2(a); a")?,
+ 21
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>("fn mul2() { this * 2 } let x = 21; x.mul2()")?,
+ 42
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>("fn mul2() { this *= 2; } let x = 21; x.mul2(); x")?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Default)]
+struct TestStruct(INT);
+
+impl Clone for TestStruct {
+ fn clone(&self) -> Self {
+ Self(self.0 + 1)
+ }
+}
+
+#[test]
+fn test_internal_fn_take() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine
+ .register_type_with_name::<TestStruct>("TestStruct")
+ .register_fn("new_ts", |x: INT| TestStruct(x));
+
+ assert_eq!(
+ engine.eval::<TestStruct>(
+ "
+ let x = new_ts(0);
+ for n in 0..41 { x = x }
+ x
+ ",
+ )?,
+ TestStruct(42)
+ );
+
+ assert_eq!(
+ engine.eval::<TestStruct>(
+ "
+ let x = new_ts(0);
+ for n in 0..41 { x = take(x) }
+ take(x)
+ ",
+ )?,
+ TestStruct(0)
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_internal_fn_big() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ fn math_me(a, b, c, d, e, f) {
+ a - b * c + d * e - f
+ }
+ math_me(100, 5, 2, 9, 6, 32)
+ ",
+ )?,
+ 112
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_internal_fn_overloading() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ fn abc(x,y,z) { 2*x + 3*y + 4*z + 888 }
+ fn abc(x,y) { x + 2*y + 88 }
+ fn abc() { 42 }
+ fn abc(x) { x - 42 }
+
+ abc() + abc(1) + abc(1,2) + abc(1,2,3)
+ "
+ )?,
+ 1002
+ );
+
+ assert_eq!(
+ *engine
+ .compile(
+ "
+ fn abc(x) { x + 42 }
+ fn abc(x) { x - 42 }
+ "
+ )
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::FnDuplicatedDefinition("abc".to_string(), 1)
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_internal_fn_params() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ // Expect duplicated parameters error
+ assert_eq!(
+ *engine
+ .compile("fn hello(x, x) { x }")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::FnDuplicatedParam("hello".to_string(), "x".to_string())
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_function_pointers() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<String>(r#"type_of(Fn("abc"))"#)?, "Fn");
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ fn foo(x) { 40 + x }
+
+ let f = Fn("foo");
+ call(f, 2)
+ "#
+ )?,
+ 42
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ fn foo(x) { 40 + x }
+
+ let fn_name = "f";
+ fn_name += "oo";
+
+ let f = Fn(fn_name);
+ f.call(2)
+ "#
+ )?,
+ 42
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ assert!(matches!(
+ *engine.eval::<INT>(r#"let f = Fn("abc"); f.call(0)"#).unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(f, ..) if f.starts_with("abc (")
+ ));
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ fn foo(x) { 40 + x }
+
+ let x = #{ action: Fn("foo") };
+ x.action.call(2)
+ "#
+ )?,
+ 42
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ fn foo(x) { this.data += x; }
+
+ let x = #{ data: 40, action: Fn("foo") };
+ x.action(2);
+ x.data
+ "#
+ )?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_internal_fn_bang() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ fn foo(y) { x += y; x }
+
+ let x = 41;
+ let y = 999;
+
+ foo!(1) + x
+ "
+ )?,
+ 84
+ );
+
+ assert!(engine
+ .eval::<INT>(
+ "
+ fn foo(y) { x += y; x }
+
+ let x = 41;
+ let y = 999;
+
+ foo(1) + x
+ "
+ )
+ .is_err());
+
+ #[cfg(not(feature = "no_object"))]
+ assert!(matches!(
+ engine
+ .compile(
+ "
+ fn foo() { this += x; }
+
+ let x = 41;
+ let y = 999;
+
+ y.foo!();
+ "
+ )
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::MalformedCapture(..)
+ ));
+
+ Ok(())
+}
+
+#[test]
+fn test_internal_fn_is_def() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!(engine.eval::<bool>(
+ r#"
+ fn foo(x) { x + 1 }
+ is_def_fn("foo", 1)
+ "#
+ )?);
+ assert!(!engine.eval::<bool>(
+ r#"
+ fn foo(x) { x + 1 }
+ is_def_fn("bar", 1)
+ "#
+ )?);
+ assert!(!engine.eval::<bool>(
+ r#"
+ fn foo(x) { x + 1 }
+ is_def_fn("foo", 0)
+ "#
+ )?);
+
+ Ok(())
+}
diff --git a/rhai/tests/looping.rs b/rhai/tests/looping.rs
new file mode 100644
index 0000000..191b7e8
--- /dev/null
+++ b/rhai/tests/looping.rs
@@ -0,0 +1,92 @@
+use rhai::{Engine, EvalAltResult, ParseErrorType, INT};
+
+#[test]
+fn test_loop() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 0;
+ let i = 0;
+
+ loop {
+ if i < 10 {
+ i += 1;
+ if x > 20 { continue; }
+ x += i;
+ } else {
+ break;
+ }
+ }
+
+ x
+ "
+ )?,
+ 21
+ );
+
+ assert_eq!(
+ *engine.compile("let x = 0; break;").unwrap_err().err_type(),
+ ParseErrorType::LoopBreak
+ );
+
+ #[cfg(not(feature = "no_function"))]
+ assert_eq!(
+ *engine
+ .compile("loop { let f = || { break; } }")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::LoopBreak
+ );
+
+ assert_eq!(
+ *engine
+ .compile("let x = 0; if x > 0 { continue; }")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::LoopBreak
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_loop_expression() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 0;
+
+ let value = while x < 10 {
+ if x % 5 == 0 { break 42; }
+ x += 1;
+ };
+
+ value
+ "
+ )?,
+ 42
+ );
+
+ engine.set_allow_loop_expressions(false);
+
+ assert!(engine
+ .eval::<INT>(
+ "
+ let x = 0;
+
+ let value = while x < 10 {
+ if x % 5 == 0 { break 42; }
+ x += 1;
+ };
+
+ value
+ "
+ )
+ .is_err());
+
+ Ok(())
+}
diff --git a/rhai/tests/maps.rs b/rhai/tests/maps.rs
new file mode 100644
index 0000000..d72b635
--- /dev/null
+++ b/rhai/tests/maps.rs
@@ -0,0 +1,327 @@
+#![cfg(not(feature = "no_object"))]
+
+use rhai::{Engine, EvalAltResult, Map, ParseErrorType, Scope, INT};
+
+#[test]
+fn test_map_indexing() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ #[cfg(not(feature = "no_index"))]
+ {
+ assert_eq!(
+ engine.eval::<INT>(r#"let x = #{a: 1, b: 2, c: 3}; x["b"]"#)?,
+ 2
+ );
+ assert_eq!(
+ engine.eval::<INT>(r#"let x = #{a: 1, b: 2, c: 3,}; x["b"]"#)?,
+ 2
+ );
+ assert_eq!(
+ engine.eval::<char>(
+ r#"
+ let y = #{d: 1, "e": #{a: 42, b: 88, "": "hello"}, " 123 xyz": 9};
+ y.e[""][4]
+ "#
+ )?,
+ 'o'
+ );
+ assert_eq!(
+ engine.eval::<String>(r#"let a = [#{s:"hello"}]; a[0].s[2] = 'X'; a[0].s"#)?,
+ "heXlo"
+ );
+ }
+
+ assert_eq!(
+ engine.eval::<INT>("let y = #{a: 1, b: 2, c: 3}; y.a = 5; y.a")?,
+ 5
+ );
+
+ engine.run("let y = #{a: 1, b: 2, c: 3}; y.z")?;
+
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let y = #{`a
+b`: 1}; y["a\nb"]
+ "#
+ )?,
+ 1
+ );
+
+ assert!(matches!(
+ *engine
+ .eval::<INT>("let y = #{`a${1}`: 1}; y.a1")
+ .unwrap_err(),
+ EvalAltResult::ErrorParsing(ParseErrorType::PropertyExpected, ..)
+ ));
+
+ assert!(engine.eval::<bool>(r#"let y = #{a: 1, b: 2, c: 3}; "c" in y"#)?);
+ assert!(engine.eval::<bool>(r#"let y = #{a: 1, b: 2, c: 3}; "b" in y"#)?);
+ assert!(!engine.eval::<bool>(r#"let y = #{a: 1, b: 2, c: 3}; "z" in y"#)?);
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let x = #{a: 1, b: 2, c: 3};
+ let c = x.remove("c");
+ x.len() + c
+ "#
+ )?,
+ 5
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = #{a: 1, b: 2, c: 3};
+ let y = #{b: 42, d: 9};
+ x.mixin(y);
+ x.len() + x.b
+ "
+ )?,
+ 46
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = #{a: 1, b: 2, c: 3};
+ x += #{b: 42, d: 9};
+ x.len() + x.b
+ "
+ )?,
+ 46
+ );
+ assert_eq!(
+ engine
+ .eval::<Map>(
+ "
+ let x = #{a: 1, b: 2, c: 3};
+ let y = #{b: 42, d: 9};
+ x + y
+ "
+ )?
+ .len(),
+ 4
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_map_prop() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.eval::<()>("let x = #{a: 42}; x.b").unwrap();
+
+ engine.set_fail_on_invalid_map_property(true);
+
+ assert!(
+ matches!(*engine.eval::<()>("let x = #{a: 42}; x.b").unwrap_err(),
+ EvalAltResult::ErrorPropertyNotFound(prop, _) if prop == "b"
+ )
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_index"))]
+#[test]
+fn test_map_index_types() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ engine.compile(r#"#{a:1, b:2, c:3}["a"]['x']"#)?;
+
+ assert!(matches!(
+ engine
+ .compile("#{a:1, b:2, c:3}['x']")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::MalformedIndexExpr(..)
+ ));
+
+ assert!(matches!(
+ engine
+ .compile("#{a:1, b:2, c:3}[1]")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::MalformedIndexExpr(..)
+ ));
+
+ #[cfg(not(feature = "no_float"))]
+ assert!(matches!(
+ engine
+ .compile("#{a:1, b:2, c:3}[123.456]")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::MalformedIndexExpr(..)
+ ));
+
+ assert!(matches!(
+ engine
+ .compile("#{a:1, b:2, c:3}[()]")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::MalformedIndexExpr(..)
+ ));
+
+ assert!(matches!(
+ engine
+ .compile("#{a:1, b:2, c:3}[true && false]")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::MalformedIndexExpr(..)
+ ));
+
+ Ok(())
+}
+
+#[test]
+fn test_map_assign() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ let x = engine.eval::<Map>(r#"let x = #{a: 1, b: true, "c$": "hello"}; x"#)?;
+
+ assert_eq!(x["a"].as_int().unwrap(), 1);
+ assert!(x["b"].as_bool().unwrap());
+ assert_eq!(x["c$"].clone_cast::<String>(), "hello");
+
+ Ok(())
+}
+
+#[test]
+fn test_map_return() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ let x = engine.eval::<Map>(r#"#{a: 1, b: true, "c$": "hello"}"#)?;
+
+ assert_eq!(x["a"].as_int().unwrap(), 1);
+ assert!(x["b"].as_bool().unwrap());
+ assert_eq!(x["c$"].clone_cast::<String>(), "hello");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_index"))]
+fn test_map_for() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine
+ .eval::<String>(
+ r#"
+ let map = #{a: 1, b_x: true, "$c d e!": "hello"};
+ let s = "";
+
+ for key in keys(map) {
+ s += key;
+ }
+
+ s
+ "#
+ )?
+ .len(),
+ 11
+ );
+
+ Ok(())
+}
+
+#[test]
+/// Because a Rhai object map literal is almost the same as JSON,
+/// it is possible to convert from JSON into a Rhai object map.
+fn test_map_json() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ let json = r#"{"a":1, "b":true, "c":41+1, "$d e f!":"hello", "z":null}"#;
+
+ let map = engine.parse_json(json, true)?;
+
+ assert!(!map.contains_key("x"));
+
+ assert_eq!(map["a"].as_int().unwrap(), 1);
+ assert!(map["b"].as_bool().unwrap());
+ assert_eq!(map["c"].as_int().unwrap(), 42);
+ assert_eq!(map["$d e f!"].clone_cast::<String>(), "hello");
+ let _: () = map["z"].as_unit().unwrap();
+
+ #[cfg(not(feature = "no_index"))]
+ {
+ let mut scope = Scope::new();
+ scope.push_constant("map", map);
+
+ assert_eq!(
+ engine
+ .eval_with_scope::<String>(
+ &mut scope,
+ r#"
+ let s = "";
+
+ for key in keys(map) {
+ s += key;
+ }
+
+ s
+ "#
+ )?
+ .len(),
+ 11
+ );
+ }
+
+ engine.parse_json(json, true)?;
+
+ assert!(matches!(
+ *engine.parse_json("123", true).unwrap_err(),
+ EvalAltResult::ErrorMismatchOutputType(..)
+ ));
+
+ assert!(matches!(
+ *engine.parse_json("{a:42}", true).unwrap_err(),
+ EvalAltResult::ErrorParsing(..)
+ ));
+
+ assert!(matches!(
+ *engine.parse_json("#{a:123}", true).unwrap_err(),
+ EvalAltResult::ErrorParsing(..)
+ ));
+
+ assert!(matches!(
+ *engine.parse_json("{a:()}", true).unwrap_err(),
+ EvalAltResult::ErrorParsing(..)
+ ));
+
+ assert!(matches!(
+ *engine.parse_json("#{a:123+456}", true).unwrap_err(),
+ EvalAltResult::ErrorParsing(..)
+ ));
+
+ assert!(matches!(
+ *engine.parse_json("{a:`hello${world}`}", true).unwrap_err(),
+ EvalAltResult::ErrorParsing(..)
+ ));
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_function"))]
+fn test_map_oop() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let obj = #{ data: 40, action: Fn("abc") };
+
+ fn abc(x) { this.data += x; }
+
+ obj.action(2);
+ obj.data
+ "#,
+ )?,
+ 42
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/math.rs b/rhai/tests/math.rs
new file mode 100644
index 0000000..6453c39
--- /dev/null
+++ b/rhai/tests/math.rs
@@ -0,0 +1,121 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[cfg(not(feature = "no_float"))]
+use rhai::FLOAT;
+
+#[test]
+fn test_math() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("1 + 2")?, 3);
+ assert_eq!(engine.eval::<INT>("1 - 2")?, -1);
+ assert_eq!(engine.eval::<INT>("2 * 3")?, 6);
+ assert_eq!(engine.eval::<INT>("1 / 2")?, 0);
+ assert_eq!(engine.eval::<INT>("3 % 2")?, 1);
+
+ #[cfg(not(feature = "no_float"))]
+ assert!((engine.eval::<FLOAT>("sin(PI()/6.0)")? - 0.5).abs() < 0.001);
+
+ #[cfg(not(feature = "no_float"))]
+ assert!(engine.eval::<FLOAT>("cos(PI()/2.0)")?.abs() < 0.001);
+
+ #[cfg(not(feature = "only_i32"))]
+ assert_eq!(
+ engine.eval::<INT>("abs(-9223372036854775807)")?,
+ 9_223_372_036_854_775_807
+ );
+
+ #[cfg(feature = "only_i32")]
+ assert_eq!(engine.eval::<INT>("abs(-2147483647)")?, 2147483647);
+
+ // Overflow/underflow/division-by-zero errors
+ #[cfg(not(feature = "unchecked"))]
+ {
+ #[cfg(not(feature = "only_i32"))]
+ {
+ assert!(matches!(
+ *engine
+ .eval::<INT>("abs(-9223372036854775808)")
+ .expect_err("expects negation overflow"),
+ EvalAltResult::ErrorArithmetic(..)
+ ));
+ assert!(matches!(
+ *engine
+ .eval::<INT>("9223372036854775807 + 1")
+ .expect_err("expects overflow"),
+ EvalAltResult::ErrorArithmetic(..)
+ ));
+ assert!(matches!(
+ *engine
+ .eval::<INT>("-9223372036854775808 - 1")
+ .expect_err("expects underflow"),
+ EvalAltResult::ErrorArithmetic(..)
+ ));
+ assert!(matches!(
+ *engine
+ .eval::<INT>("9223372036854775807 * 9223372036854775807")
+ .expect_err("expects overflow"),
+ EvalAltResult::ErrorArithmetic(..)
+ ));
+ assert!(matches!(
+ *engine
+ .eval::<INT>("9223372036854775807 / 0")
+ .expect_err("expects division by zero"),
+ EvalAltResult::ErrorArithmetic(..)
+ ));
+ assert!(matches!(
+ *engine
+ .eval::<INT>("9223372036854775807 % 0")
+ .expect_err("expects division by zero"),
+ EvalAltResult::ErrorArithmetic(..)
+ ));
+ }
+
+ #[cfg(feature = "only_i32")]
+ {
+ assert!(matches!(
+ *engine
+ .eval::<INT>("2147483647 + 1")
+ .expect_err("expects overflow"),
+ EvalAltResult::ErrorArithmetic(..)
+ ));
+ assert!(matches!(
+ *engine
+ .eval::<INT>("-2147483648 - 1")
+ .expect_err("expects underflow"),
+ EvalAltResult::ErrorArithmetic(..)
+ ));
+ assert!(matches!(
+ *engine
+ .eval::<INT>("2147483647 * 2147483647")
+ .expect_err("expects overflow"),
+ EvalAltResult::ErrorArithmetic(..)
+ ));
+ assert!(matches!(
+ *engine
+ .eval::<INT>("2147483647 / 0")
+ .expect_err("expects division by zero"),
+ EvalAltResult::ErrorArithmetic(..)
+ ));
+ assert!(matches!(
+ *engine
+ .eval::<INT>("2147483647 % 0")
+ .expect_err("expects division by zero"),
+ EvalAltResult::ErrorArithmetic(..)
+ ));
+ }
+ }
+
+ Ok(())
+}
+
+#[test]
+fn test_math_parse() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>(r#"parse_int("42")"#)?, 42);
+ assert_eq!(engine.eval::<INT>(r#"parse_int("42", 16)"#)?, 0x42);
+ assert_eq!(engine.eval::<INT>(r#"parse_int("abcdef", 16)"#)?, 0xabcdef);
+
+ Ok(())
+}
diff --git a/rhai/tests/method_call.rs b/rhai/tests/method_call.rs
new file mode 100644
index 0000000..75c1979
--- /dev/null
+++ b/rhai/tests/method_call.rs
@@ -0,0 +1,150 @@
+#![cfg(not(feature = "no_object"))]
+
+use rhai::{Engine, EvalAltResult, INT};
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+struct TestStruct {
+ x: INT,
+}
+
+impl TestStruct {
+ fn update(&mut self, n: INT) {
+ self.x += n;
+ }
+
+ fn new() -> Self {
+ Self { x: 1 }
+ }
+}
+
+#[test]
+fn test_method_call() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine
+ .register_type::<TestStruct>()
+ .register_fn("update", TestStruct::update)
+ .register_fn("new_ts", TestStruct::new);
+
+ assert_eq!(
+ engine.eval::<TestStruct>("let x = new_ts(); x.update(1000); x")?,
+ TestStruct { x: 1001 }
+ );
+
+ assert_eq!(
+ engine.eval::<TestStruct>("let x = new_ts(); update(x, 1000); x")?,
+ TestStruct { x: 1001 }
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_method_call_style() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("let x = -123; x.abs(); x")?, -123);
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_optimize"))]
+#[test]
+fn test_method_call_with_full_optimization() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.set_optimization_level(rhai::OptimizationLevel::Full);
+
+ engine
+ .register_fn("new_ts", TestStruct::new)
+ .register_fn("ymd", |_: INT, _: INT, _: INT| 42 as INT)
+ .register_fn("range", |_: &mut TestStruct, _: INT, _: INT| {
+ TestStruct::new()
+ });
+
+ assert_eq!(
+ engine.eval::<TestStruct>(
+ "
+ let xs = new_ts();
+ let ys = xs.range(ymd(2022, 2, 1), ymd(2022, 2, 2));
+ ys
+ "
+ )?,
+ TestStruct::new()
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_function"))]
+#[test]
+fn test_method_call_typed() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine
+ .register_type_with_name::<TestStruct>("Test-Struct#ABC")
+ .register_fn("update", TestStruct::update)
+ .register_fn("new_ts", TestStruct::new);
+
+ assert_eq!(
+ engine.eval::<TestStruct>(
+ r#"
+ fn "Test-Struct#ABC".foo(x) {
+ this.update(x);
+ }
+ fn foo(x) {
+ this += x;
+ }
+
+ let z = 1000;
+ z.foo(1);
+
+ let x = new_ts();
+ x.foo(z);
+
+ x
+ "#
+ )?,
+ TestStruct { x: 1002 }
+ );
+
+ assert!(engine.eval::<bool>(
+ r#"
+ fn "Test-Struct#ABC".foo(x) {
+ this.update(x);
+ }
+ is_def_fn("Test-Struct#ABC", "foo", 1)
+ "#
+ )?);
+
+ assert!(matches!(
+ *engine
+ .run(
+ r#"
+ fn "Test-Struct#ABC".foo(x) {
+ this.update(x);
+ }
+ foo(1000);
+ "#
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(f, ..) if f.starts_with("foo")
+ ));
+
+ assert!(matches!(
+ *engine
+ .run(
+ r#"
+ fn "Test-Struct#ABC".foo(x) {
+ this.update(x);
+ }
+ let x = 42;
+ x.foo(1000);
+ "#
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(f, ..) if f.starts_with("foo")
+ ));
+
+ Ok(())
+}
diff --git a/rhai/tests/mismatched_op.rs b/rhai/tests/mismatched_op.rs
new file mode 100644
index 0000000..ca7b5d9
--- /dev/null
+++ b/rhai/tests/mismatched_op.rs
@@ -0,0 +1,74 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_mismatched_op() {
+ let engine = Engine::new();
+
+ assert!(matches!(
+ *engine.eval::<INT>(r#""hello, " + "world!""#).expect_err("expects error"),
+ EvalAltResult::ErrorMismatchOutputType(need, actual, ..) if need == std::any::type_name::<INT>() && actual == "string"
+ ));
+}
+
+#[test]
+fn test_mismatched_op_name() {
+ let engine = Engine::new();
+
+ assert!(matches!(
+ *engine.eval::<String>("true").expect_err("expects error"),
+ EvalAltResult::ErrorMismatchOutputType(need, actual, ..) if need == "string" && actual == "bool"
+ ));
+
+ assert!(matches!(
+ *engine.eval::<&str>("true").expect_err("expects error"),
+ EvalAltResult::ErrorMismatchOutputType(need, actual, ..) if need == "&str" && actual == "bool"
+ ));
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_mismatched_op_custom_type() -> Result<(), Box<EvalAltResult>> {
+ #[allow(dead_code)] // used inside `register_type_with_name`
+ #[derive(Debug, Clone)]
+ struct TestStruct {
+ x: INT,
+ }
+
+ impl TestStruct {
+ fn new() -> Self {
+ Self { x: 1 }
+ }
+ }
+
+ let mut engine = Engine::new();
+
+ engine
+ .register_type_with_name::<TestStruct>("TestStruct")
+ .register_fn("new_ts", TestStruct::new);
+
+ assert!(matches!(*engine.eval::<bool>(
+ "
+ let x = new_ts();
+ let y = new_ts();
+ x == y
+ ").unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(f, ..) if f == "== (TestStruct, TestStruct)"));
+
+ assert!(
+ matches!(*engine.eval::<bool>("new_ts() == 42").unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(f, ..) if f.starts_with("== (TestStruct, "))
+ );
+
+ assert!(matches!(
+ *engine.eval::<INT>("60 + new_ts()").unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(f, ..) if f == format!("+ ({}, TestStruct)", std::any::type_name::<INT>())
+ ));
+
+ assert!(matches!(
+ *engine.eval::<TestStruct>("42").unwrap_err(),
+ EvalAltResult::ErrorMismatchOutputType(need, actual, ..)
+ if need == "TestStruct" && actual == std::any::type_name::<INT>()
+ ));
+
+ Ok(())
+}
diff --git a/rhai/tests/modules.rs b/rhai/tests/modules.rs
new file mode 100644
index 0000000..f296d78
--- /dev/null
+++ b/rhai/tests/modules.rs
@@ -0,0 +1,659 @@
+#![cfg(not(feature = "no_module"))]
+use rhai::{
+ module_resolvers::{DummyModuleResolver, StaticModuleResolver},
+ Dynamic, Engine, EvalAltResult, FnNamespace, ImmutableString, Module, ParseError,
+ ParseErrorType, Scope, INT,
+};
+//
+#[cfg(all(not(feature = "no_function"), feature = "internals"))]
+use rhai::{FnPtr, NativeCallContext};
+
+#[test]
+fn test_module() {
+ let mut module = Module::new();
+ module.set_var("answer", 42 as INT);
+
+ assert!(module.contains_var("answer"));
+ assert_eq!(module.get_var_value::<INT>("answer").unwrap(), 42);
+}
+
+#[test]
+fn test_module_syntax() {
+ let engine = Engine::new();
+ assert!(engine.compile("abc.def::xyz").is_err());
+ assert!(engine.compile("abc.def::xyz()").is_err());
+}
+
+#[test]
+fn test_module_sub_module() -> Result<(), Box<EvalAltResult>> {
+ let mut module = Module::new();
+
+ let mut sub_module = Module::new();
+
+ let mut sub_module2 = Module::new();
+ sub_module2.set_var("answer", 41 as INT);
+
+ let hash_inc = sub_module2.set_native_fn("inc", |x: &mut INT| Ok(*x + 1));
+ sub_module2.build_index();
+ assert!(!sub_module2.contains_indexed_global_functions());
+
+ let super_hash = sub_module2.set_native_fn("super_inc", |x: &mut INT| Ok(*x + 1));
+ sub_module2.update_fn_namespace(super_hash, FnNamespace::Global);
+ sub_module2.build_index();
+ assert!(sub_module2.contains_indexed_global_functions());
+
+ #[cfg(not(feature = "no_object"))]
+ sub_module2.set_getter_fn("doubled", |x: &mut INT| Ok(*x * 2));
+
+ sub_module.set_sub_module("universe", sub_module2);
+ module.set_sub_module("life", sub_module);
+ module.set_var("MYSTIC_NUMBER", Dynamic::from(42 as INT));
+ module.build_index();
+
+ assert!(module.contains_indexed_global_functions());
+
+ assert!(module.contains_sub_module("life"));
+ let m = module.get_sub_module("life").unwrap();
+
+ assert!(m.contains_sub_module("universe"));
+ let m2 = m.get_sub_module("universe").unwrap();
+
+ assert!(m2.contains_var("answer"));
+ assert!(m2.contains_fn(hash_inc));
+
+ assert_eq!(m2.get_var_value::<INT>("answer").unwrap(), 41);
+
+ module.set_custom_type::<()>("Don't Panic");
+
+ let mut engine = Engine::new();
+ engine.register_static_module("question", module.into());
+
+ assert_eq!(engine.eval::<String>("type_of(())")?, "Don't Panic");
+
+ assert_eq!(engine.eval::<INT>("question::MYSTIC_NUMBER")?, 42);
+ assert!(engine.eval::<INT>("MYSTIC_NUMBER").is_err());
+ assert_eq!(engine.eval::<INT>("question::life::universe::answer")?, 41);
+ assert_eq!(
+ engine.eval::<INT>("question::life::universe::answer + 1")?,
+ 42
+ );
+ assert_eq!(
+ engine.eval::<INT>("question::life::universe::inc(question::life::universe::answer)")?,
+ 42
+ );
+ assert!(engine
+ .eval::<INT>("inc(question::life::universe::answer)")
+ .is_err());
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(engine.eval::<INT>("question::MYSTIC_NUMBER.doubled")?, 84);
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>("question::life::universe::answer.doubled")?,
+ 82
+ );
+ assert_eq!(
+ engine.eval::<INT>("super_inc(question::life::universe::answer)")?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_module_resolver() -> Result<(), Box<EvalAltResult>> {
+ let mut resolver = StaticModuleResolver::new();
+
+ let mut module = Module::new();
+
+ module.set_var("answer", 42 as INT);
+ module.set_native_fn("sum", |x: INT, y: INT, z: INT, w: INT| Ok(x + y + z + w));
+ let double_hash = module.set_native_fn("double", |x: &mut INT| {
+ *x *= 2;
+ Ok(())
+ });
+ module.update_fn_namespace(double_hash, FnNamespace::Global);
+
+ #[cfg(not(feature = "no_float"))]
+ module.set_native_fn(
+ "sum_of_three_args",
+ |target: &mut INT, a: INT, b: INT, c: rhai::FLOAT| {
+ *target = a + b + c as INT;
+ Ok(())
+ },
+ );
+
+ resolver.insert("hello", module);
+
+ let mut engine = Engine::new();
+ engine.set_module_resolver(resolver);
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ import "hello" as h1;
+ import "hello" as h2;
+ h1::sum(h2::answer, -10, 3, 7)
+ "#
+ )?,
+ 42
+ );
+
+ assert!(engine
+ .eval::<INT>(
+ r#"
+ import "hello" as h;
+ sum(h::answer, -10, 3, 7)
+ "#
+ )
+ .is_err());
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ import "hello" as h1;
+ import "hello" as h2;
+ let x = 42;
+ h1::sum(x, -10, 3, 7)
+ "#
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ import "hello" as h1;
+ import "hello" as h2;
+ let x = 42;
+ h1::sum(x, 0, 0, 0);
+ x
+ "#
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ import "hello" as h;
+ let x = 21;
+ h::double(x);
+ x
+ "#
+ )?,
+ 42
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ import "hello" as h;
+ let x = 21;
+ double(x);
+ x
+ "#
+ )?,
+ 42
+ );
+ #[cfg(not(feature = "no_float"))]
+ {
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ import "hello" as h;
+ let x = 21;
+ h::sum_of_three_args(x, 14, 26, 2.0);
+ x
+ "#
+ )?,
+ 42
+ );
+ }
+
+ #[cfg(not(feature = "unchecked"))]
+ {
+ engine.set_max_modules(5);
+
+ assert!(matches!(
+ *engine
+ .eval::<INT>(
+ r#"
+ let sum = 0;
+
+ for x in 0..10 {
+ import "hello" as h;
+ sum += h::answer;
+ }
+
+ sum
+ "#
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorTooManyModules(..)
+ ));
+
+ #[cfg(not(feature = "no_function"))]
+ assert!(matches!(
+ *engine
+ .eval::<INT>(
+ r#"
+ let sum = 0;
+
+ fn foo() {
+ import "hello" as h;
+ sum += h::answer;
+ }
+
+ for x in 0..10 {
+ foo();
+ }
+
+ sum
+ "#
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorInFunctionCall(fn_name, ..) if fn_name == "foo"
+ ));
+
+ engine.set_max_modules(1000);
+
+ #[cfg(not(feature = "no_function"))]
+ engine.run(
+ r#"
+ fn foo() {
+ import "hello" as h;
+ }
+
+ for x in 0..10 {
+ foo();
+ }
+ "#,
+ )?;
+ }
+
+ #[cfg(not(feature = "no_function"))]
+ {
+ let script = r#"
+ fn foo(x) {
+ import "hello" as h;
+ h::answer * x
+ }
+ foo(1) + { import "hello" as h; h::answer }
+ "#;
+ let scope = Scope::new();
+
+ let ast = engine.compile_into_self_contained(&scope, script)?;
+
+ engine.set_module_resolver(DummyModuleResolver::new());
+
+ assert_eq!(engine.eval_ast::<INT>(&ast)?, 84);
+
+ assert!(engine.eval::<INT>(script).is_err());
+
+ let result = engine.call_fn::<INT>(&mut Scope::new(), &ast, "foo", (2 as INT,))?;
+
+ assert_eq!(result, 84);
+
+ let mut ast2 = engine.compile("fn foo(x) { 42 }")?;
+
+ #[cfg(feature = "internals")]
+ let len = ast.resolver().unwrap().len();
+
+ ast2 += ast;
+
+ #[cfg(feature = "internals")]
+ {
+ assert!(ast2.resolver().is_some());
+ assert_eq!(ast2.resolver().unwrap().len(), len);
+ }
+
+ let result = engine.call_fn::<INT>(&mut Scope::new(), &ast2, "foo", (2 as INT,))?;
+
+ assert_eq!(result, 84);
+ }
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_function"))]
+fn test_module_from_ast() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ let mut resolver1 = StaticModuleResolver::new();
+ let mut sub_module = Module::new();
+ sub_module.set_var("foo", true);
+ resolver1.insert("another module", sub_module);
+
+ let ast = engine.compile(
+ r#"
+ // Functions become module functions
+ fn calc(x) {
+ x + 1
+ }
+ fn add_len(x, y) {
+ x + len(y)
+ }
+ fn cross_call(x) {
+ calc(x)
+ }
+ private fn hidden() {
+ throw "you shouldn't see me!";
+ }
+
+ // Imported modules become sub-modules
+ import "another module" as extra;
+
+ // Variables defined at global level become module variables
+ export const x = 123;
+ let foo = 41;
+ let hello;
+
+ // Final variable values become constant module variable values
+ foo = calc(foo);
+ hello = `hello, ${foo} worlds!`;
+
+ export x as abc;
+ export x as xxx;
+ export foo;
+ export hello;
+ "#,
+ )?;
+
+ engine.set_module_resolver(resolver1);
+
+ let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
+
+ let mut resolver2 = StaticModuleResolver::new();
+ resolver2.insert("testing", module);
+ engine.set_module_resolver(resolver2);
+
+ assert_eq!(
+ engine.eval::<INT>(r#"import "testing" as ttt; ttt::abc"#)?,
+ 123
+ );
+ assert_eq!(
+ engine.eval::<INT>(r#"import "testing" as ttt; ttt::x"#)?,
+ 123
+ );
+ assert_eq!(
+ engine.eval::<INT>(r#"import "testing" as ttt; ttt::xxx"#)?,
+ 123
+ );
+ assert_eq!(
+ engine.eval::<INT>(r#"import "testing" as ttt; ttt::foo"#)?,
+ 42
+ );
+ assert!(engine.eval::<bool>(r#"import "testing" as ttt; ttt::extra::foo"#)?);
+ assert_eq!(
+ engine.eval::<String>(r#"import "testing" as ttt; ttt::hello"#)?,
+ "hello, 42 worlds!"
+ );
+ assert_eq!(
+ engine.eval::<INT>(r#"import "testing" as ttt; ttt::calc(999)"#)?,
+ 1000
+ );
+ assert_eq!(
+ engine.eval::<INT>(r#"import "testing" as ttt; ttt::cross_call(999)"#)?,
+ 1000
+ );
+ assert_eq!(
+ engine.eval::<INT>(r#"import "testing" as ttt; ttt::add_len(ttt::foo, ttt::hello)"#)?,
+ 59
+ );
+ assert!(matches!(
+ *engine
+ .run(r#"import "testing" as ttt; ttt::hidden()"#)
+ .unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(fn_name, ..) if fn_name == "ttt::hidden ()"
+ ));
+
+ Ok(())
+}
+
+#[test]
+fn test_module_export() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!(matches!(
+ engine.compile("let x = 10; { export x; }").unwrap_err(),
+ ParseError(x, ..) if *x == ParseErrorType::WrongExport
+ ));
+
+ #[cfg(not(feature = "no_function"))]
+ assert!(matches!(
+ engine.compile("fn abc(x) { export x; }").unwrap_err(),
+ ParseError(x, ..) if *x == ParseErrorType::WrongExport
+ ));
+
+ Ok(())
+}
+
+#[test]
+fn test_module_str() -> Result<(), Box<EvalAltResult>> {
+ fn test_fn(input: ImmutableString) -> Result<INT, Box<EvalAltResult>> {
+ Ok(input.len() as INT)
+ }
+ fn test_fn2(input: &str) -> Result<INT, Box<EvalAltResult>> {
+ Ok(input.len() as INT)
+ }
+ fn test_fn3(input: String) -> Result<INT, Box<EvalAltResult>> {
+ Ok(input.len() as INT)
+ }
+
+ let mut engine = rhai::Engine::new();
+ let mut module = Module::new();
+ module.set_native_fn("test", test_fn);
+ module.set_native_fn("test2", test_fn2);
+ module.set_native_fn("test3", test_fn3);
+
+ let mut static_modules = rhai::module_resolvers::StaticModuleResolver::new();
+ static_modules.insert("test", module);
+ engine.set_module_resolver(static_modules);
+
+ assert_eq!(
+ engine.eval::<INT>(r#"import "test" as test; test::test("test");"#)?,
+ 4
+ );
+ assert_eq!(
+ engine.eval::<INT>(r#"import "test" as test; test::test2("test");"#)?,
+ 4
+ );
+ assert_eq!(
+ engine.eval::<INT>(r#"import "test" as test; test::test3("test");"#)?,
+ 4
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_function"))]
+#[test]
+fn test_module_ast_namespace() -> Result<(), Box<EvalAltResult>> {
+ let script = "
+ fn foo(x) { x + 1 }
+ fn bar(x) { foo(x) }
+ ";
+
+ let mut engine = Engine::new();
+
+ let ast = engine.compile(script)?;
+
+ let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
+
+ let mut resolver = StaticModuleResolver::new();
+ resolver.insert("testing", module);
+ engine.set_module_resolver(resolver);
+
+ assert_eq!(
+ engine.eval::<INT>(r#"import "testing" as t; t::foo(41)"#)?,
+ 42
+ );
+ assert_eq!(
+ engine.eval::<INT>(r#"import "testing" as t; t::bar(41)"#)?,
+ 42
+ );
+ assert_eq!(
+ engine.eval::<INT>(r#"fn foo(x) { x - 1 } import "testing" as t; t::foo(41)"#)?,
+ 42
+ );
+ assert_eq!(
+ engine.eval::<INT>(r#"fn foo(x) { x - 1 } import "testing" as t; t::bar(41)"#)?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_function"))]
+#[test]
+fn test_module_ast_namespace2() -> Result<(), Box<EvalAltResult>> {
+ use rhai::{Engine, Module, Scope};
+
+ const MODULE_TEXT: &str = "
+ fn run_function(function) {
+ call(function)
+ }
+ ";
+
+ const SCRIPT: &str = r#"
+ import "test_module" as test;
+
+ fn foo() {
+ print("foo");
+ }
+
+ test::run_function(Fn("foo"));
+ "#;
+
+ let mut engine = Engine::new();
+ let module_ast = engine.compile(MODULE_TEXT)?;
+ let module = Module::eval_ast_as_new(Scope::new(), &module_ast, &engine)?;
+ let mut static_modules = rhai::module_resolvers::StaticModuleResolver::new();
+ static_modules.insert("test_module", module);
+ engine.set_module_resolver(static_modules);
+
+ engine.run(SCRIPT)?;
+
+ Ok(())
+}
+
+#[cfg(all(not(feature = "no_function"), feature = "internals"))]
+#[test]
+fn test_module_context() -> Result<(), Box<EvalAltResult>> {
+ let script = "fn bar() { calc(|x| x + 1) }";
+
+ let mut engine = Engine::new();
+
+ let ast = engine.compile(script)?;
+
+ let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
+
+ let mut resolver = StaticModuleResolver::new();
+ resolver.insert("testing", module);
+ engine.set_module_resolver(resolver);
+
+ engine.register_fn(
+ "calc",
+ |context: NativeCallContext, fp: FnPtr| -> Result<INT, Box<EvalAltResult>> {
+ let engine = context.engine();
+
+ // Store context for later use - requires the 'internals' feature
+ let context_data = context.store_data();
+
+ // Recreate the 'NativeCallContext'
+ let new_context = context_data.create_context(engine);
+
+ fp.call_within_context(&new_context, (41 as INT,))
+ },
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(r#"import "testing" as t; t::bar()"#)?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_module_file() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let ast = engine.compile(
+ r#"
+ import "scripts/module";
+ print("top");
+ "#,
+ )?;
+ Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
+ Ok(())
+}
+
+#[cfg(not(feature = "no_function"))]
+#[test]
+fn test_module_environ() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ let ast = engine.compile(
+ r#"
+ const SECRET = 42;
+
+ fn foo(x) {
+ print(global::SECRET);
+ global::SECRET + x
+ }
+ "#,
+ )?;
+
+ let mut m = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
+
+ m.set_id("test");
+ m.build_index();
+
+ engine.register_static_module("test", m.into());
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"
+ const SECRET = "hello";
+
+ fn foo(x) {
+ print(global::SECRET);
+ global::SECRET + x
+ }
+
+ let t = test::foo(0);
+
+ foo(t)
+ "#
+ )?,
+ "hello42"
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_module_dynamic() -> Result<(), Box<EvalAltResult>> {
+ fn test_fn(input: Dynamic, x: INT) -> Result<INT, Box<EvalAltResult>> {
+ let s = input.into_string().unwrap();
+ Ok(s.len() as INT + x)
+ }
+
+ let mut engine = rhai::Engine::new();
+ let mut module = Module::new();
+ module.set_native_fn("test", test_fn);
+
+ let mut static_modules = rhai::module_resolvers::StaticModuleResolver::new();
+ static_modules.insert("test", module);
+ engine.set_module_resolver(static_modules);
+ engine.register_fn("test2", test_fn);
+
+ assert_eq!(engine.eval::<INT>(r#"test2("test", 38);"#)?, 42);
+
+ assert_eq!(
+ engine.eval::<INT>(r#"import "test" as test; test::test("test", 38);"#)?,
+ 42
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/native.rs b/rhai/tests/native.rs
new file mode 100644
index 0000000..0c339cb
--- /dev/null
+++ b/rhai/tests/native.rs
@@ -0,0 +1,99 @@
+use rhai::{Dynamic, Engine, EvalAltResult, ImmutableString, NativeCallContext, INT};
+use std::any::TypeId;
+
+#[cfg(not(feature = "no_module"))]
+#[cfg(not(feature = "unchecked"))]
+#[test]
+fn test_native_context() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.set_max_modules(40);
+ engine.register_fn("test", |context: NativeCallContext, x: INT| {
+ context.engine().max_modules() as INT + x
+ });
+
+ assert_eq!(engine.eval::<INT>("test(2)")?, 42);
+
+ Ok(())
+}
+
+#[test]
+fn test_native_context_fn_name() -> Result<(), Box<EvalAltResult>> {
+ fn add_double(
+ context: NativeCallContext,
+ args: &mut [&mut Dynamic],
+ ) -> Result<Dynamic, Box<EvalAltResult>> {
+ let x = args[0].as_int().unwrap();
+ let y = args[1].as_int().unwrap();
+ Ok(format!("{}_{}", context.fn_name(), x + 2 * y).into())
+ }
+
+ let mut engine = Engine::new();
+
+ engine
+ .register_raw_fn(
+ "add_double",
+ [TypeId::of::<INT>(), TypeId::of::<INT>()],
+ add_double,
+ )
+ .register_raw_fn(
+ "append_x2",
+ [TypeId::of::<INT>(), TypeId::of::<INT>()],
+ add_double,
+ );
+
+ assert_eq!(engine.eval::<String>("add_double(40, 1)")?, "add_double_42");
+
+ assert_eq!(engine.eval::<String>("append_x2(40, 1)")?, "append_x2_42");
+
+ Ok(())
+}
+
+#[test]
+fn test_native_overload() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<String>(r#"let x = "hello, "; let y = "world"; x + y"#)?,
+ "hello, world"
+ );
+ assert_eq!(
+ engine.eval::<String>(r#"let x = "hello"; let y = (); x + y"#)?,
+ "hello"
+ );
+
+ // Overload the `+` operator for strings
+
+ engine
+ .register_fn(
+ "+",
+ |s1: ImmutableString, s2: ImmutableString| -> ImmutableString {
+ format!("{s1}***{s2}").into()
+ },
+ )
+ .register_fn("+", |s1: ImmutableString, _: ()| -> ImmutableString {
+ format!("{s1} Foo!").into()
+ });
+
+ assert_eq!(
+ engine.eval::<String>(r#"let x = "hello"; let y = "world"; x + y"#)?,
+ "helloworld"
+ );
+ assert_eq!(
+ engine.eval::<String>(r#"let x = "hello"; let y = (); x + y"#)?,
+ "hello"
+ );
+
+ engine.set_fast_operators(false);
+
+ assert_eq!(
+ engine.eval::<String>(r#"let x = "hello"; let y = "world"; x + y"#)?,
+ "hello***world"
+ );
+ assert_eq!(
+ engine.eval::<String>(r#"let x = "hello"; let y = (); x + y"#)?,
+ "hello Foo!"
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/not.rs b/rhai/tests/not.rs
new file mode 100644
index 0000000..7440ec4
--- /dev/null
+++ b/rhai/tests/not.rs
@@ -0,0 +1,15 @@
+use rhai::{Engine, EvalAltResult};
+
+#[test]
+fn test_not() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!(!engine.eval::<bool>("let not_true = !true; not_true")?);
+
+ #[cfg(not(feature = "no_function"))]
+ assert!(engine.eval::<bool>("fn not(x) { !x } not(false)")?);
+
+ assert!(engine.eval::<bool>("!!!!true")?);
+
+ Ok(())
+}
diff --git a/rhai/tests/number_literals.rs b/rhai/tests/number_literals.rs
new file mode 100644
index 0000000..daade83
--- /dev/null
+++ b/rhai/tests/number_literals.rs
@@ -0,0 +1,73 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_number_literal() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("42")?, 42);
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<String>("42.type_of()")?,
+ if cfg!(feature = "only_i32") {
+ "i32"
+ } else {
+ "i64"
+ }
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_hex_literal() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("let x = 0xf; x")?, 15);
+ assert_eq!(engine.eval::<INT>("let x = 0Xf; x")?, 15);
+ assert_eq!(engine.eval::<INT>("let x = 0xff; x")?, 255);
+
+ #[cfg(not(feature = "only_i32"))]
+ assert_eq!(engine.eval::<INT>("let x = 0xffffffffffffffff; x")?, -1);
+ #[cfg(feature = "only_i32")]
+ assert_eq!(engine.eval::<INT>("let x = 0xffffffff; x")?, -1);
+
+ Ok(())
+}
+
+#[test]
+fn test_octal_literal() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("let x = 0o77; x")?, 63);
+ assert_eq!(engine.eval::<INT>("let x = 0O77; x")?, 63);
+ assert_eq!(engine.eval::<INT>("let x = 0o1234; x")?, 668);
+
+ Ok(())
+}
+
+#[test]
+fn test_binary_literal() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("let x = 0b1111; x")?, 15);
+ assert_eq!(engine.eval::<INT>("let x = 0B1111; x")?, 15);
+ assert_eq!(
+ engine.eval::<INT>("let x = 0b0011_1100_1010_0101; x")?,
+ 15525
+ );
+ #[cfg(not(feature = "only_i32"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ "let x = 0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111; x"
+ )?,
+ -1
+ );
+ #[cfg(feature = "only_i32")]
+ assert_eq!(
+ engine.eval::<INT>("let x = 0b11111111_11111111_11111111_11111111; x")?,
+ -1
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/operations.rs b/rhai/tests/operations.rs
new file mode 100644
index 0000000..b179e3e
--- /dev/null
+++ b/rhai/tests/operations.rs
@@ -0,0 +1,168 @@
+#![cfg(not(feature = "unchecked"))]
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_max_operations() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ #[cfg(not(feature = "no_optimize"))]
+ engine.set_optimization_level(rhai::OptimizationLevel::None);
+ engine.set_max_operations(500);
+
+ engine.on_progress(|count| {
+ if count % 100 == 0 {
+ println!("{count}");
+ }
+ None
+ });
+
+ engine.run("let x = 0; while x < 20 { x += 1; }")?;
+
+ assert!(matches!(
+ *engine.run("for x in 0..500 {}").unwrap_err(),
+ EvalAltResult::ErrorTooManyOperations(..)
+ ));
+
+ engine.set_max_operations(0);
+
+ engine.run("for x in 0..10000 {}")?;
+
+ Ok(())
+}
+
+#[test]
+fn test_max_operations_literal() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ #[cfg(not(feature = "no_optimize"))]
+ engine.set_optimization_level(rhai::OptimizationLevel::None);
+ engine.set_max_operations(10);
+
+ #[cfg(not(feature = "no_index"))]
+ engine.run("[1, 2, 3, 4, 5, 6, 7]")?;
+
+ #[cfg(not(feature = "no_index"))]
+ assert!(matches!(
+ *engine.run("[1, 2, 3, 4, 5, 6, 7, 8, 9]").unwrap_err(),
+ EvalAltResult::ErrorTooManyOperations(..)
+ ));
+
+ #[cfg(not(feature = "no_object"))]
+ engine.run("#{a:1, b:2, c:3, d:4, e:5, f:6, g:7}")?;
+
+ #[cfg(not(feature = "no_object"))]
+ assert!(matches!(
+ *engine
+ .run("#{a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8, i:9}")
+ .unwrap_err(),
+ EvalAltResult::ErrorTooManyOperations(..)
+ ));
+
+ Ok(())
+}
+
+#[test]
+fn test_max_operations_functions() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ engine.set_max_operations(500);
+
+ engine.on_progress(|count| {
+ if count % 100 == 0 {
+ println!("{count}");
+ }
+ None
+ });
+
+ engine.run(
+ r#"
+ print("Test1");
+ let x = 0;
+
+ while x < 28 {
+ print(x);
+ x += 1;
+ }
+ "#,
+ )?;
+
+ #[cfg(not(feature = "no_function"))]
+ engine.run(
+ r#"
+ print("Test2");
+ fn inc(x) { x + 1 }
+ let x = 0;
+ while x < 20 { x = inc(x); }
+ "#,
+ )?;
+
+ #[cfg(not(feature = "no_function"))]
+ assert!(matches!(
+ *engine
+ .run(
+ r#"
+ print("Test3");
+ fn inc(x) { x + 1 }
+ let x = 0;
+
+ while x < 36 {
+ print(x);
+ x = inc(x);
+ }
+ "#,
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorTooManyOperations(..)
+ ));
+
+ Ok(())
+}
+
+#[test]
+fn test_max_operations_eval() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ engine.set_max_operations(500);
+
+ engine.on_progress(|count| {
+ if count % 100 == 0 {
+ println!("{count}");
+ }
+ None
+ });
+
+ assert!(matches!(
+ *engine
+ .run(
+ r#"
+ let script = "for x in 0..500 {}";
+ eval(script);
+ "#
+ )
+ .unwrap_err(),
+ EvalAltResult::ErrorInFunctionCall(.., err, _) if matches!(*err, EvalAltResult::ErrorTooManyOperations(..))
+ ));
+
+ Ok(())
+}
+
+#[test]
+fn test_max_operations_progress() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ #[cfg(not(feature = "no_optimize"))]
+ engine.set_optimization_level(rhai::OptimizationLevel::None);
+ engine.set_max_operations(500);
+
+ engine.on_progress(|count| {
+ if count < 100 {
+ None
+ } else {
+ Some((42 as INT).into())
+ }
+ });
+
+ assert!(matches!(
+ *engine
+ .run("for x in 0..500 {}")
+ .unwrap_err(),
+ EvalAltResult::ErrorTerminated(x, ..) if x.as_int()? == 42
+ ));
+
+ Ok(())
+}
diff --git a/rhai/tests/ops.rs b/rhai/tests/ops.rs
new file mode 100644
index 0000000..7c8e007
--- /dev/null
+++ b/rhai/tests/ops.rs
@@ -0,0 +1,90 @@
+use rhai::{Engine, EvalAltResult, Scope, INT};
+
+#[test]
+fn test_ops() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("60 + 5")?, 65);
+ assert_eq!(engine.eval::<INT>("(1 + 2) * (6 - 4) / 2")?, 3);
+ assert_eq!(engine.eval::<INT>("let x = 41; x = x + 1; x")?, 42);
+ assert_eq!(
+ engine.eval::<String>(r#"let s = "hello"; s = s + 42; s"#)?,
+ "hello42"
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "only_i32"))]
+#[cfg(not(feature = "only_i64"))]
+#[test]
+fn test_ops_other_number_types() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ let mut scope = Scope::new();
+
+ scope.push("x", 42_u16);
+
+ assert!(matches!(
+ *engine.eval_with_scope::<bool>(&mut scope, "x == 42").unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(f, ..) if f.starts_with("== (u16,")
+ ));
+ #[cfg(not(feature = "no_float"))]
+ assert!(matches!(
+ *engine.eval_with_scope::<bool>(&mut scope, "x == 42.0").unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(f, ..) if f.starts_with("== (u16,")
+ ));
+
+ assert!(
+ matches!(*engine.eval_with_scope::<bool>(&mut scope, r#"x == "hello""#).unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(f, ..) if f.starts_with("== (u16,")
+ )
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_ops_strings() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!(engine.eval::<bool>(r#""hello" > 'c'"#)?);
+ assert!(engine.eval::<bool>(r#""" < 'c'"#)?);
+ assert!(engine.eval::<bool>(r#"'x' > "hello""#)?);
+ assert!(engine.eval::<bool>(r#""hello" > "foo""#)?);
+
+ Ok(())
+}
+
+#[test]
+fn test_ops_precedence() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>("let x = 0; if x == 10 || true { x = 1} x")?,
+ 1
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_ops_custom_types() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, Clone, PartialEq, Eq)]
+ struct Test1;
+ #[derive(Debug, Clone, PartialEq, Eq)]
+ struct Test2;
+
+ let mut engine = Engine::new();
+
+ engine
+ .register_type_with_name::<Test1>("Test1")
+ .register_type_with_name::<Test2>("Test2")
+ .register_fn("new_ts1", || Test1)
+ .register_fn("new_ts2", || Test2)
+ .register_fn("==", |x: Test1, y: Test2| true);
+
+ assert!(engine.eval::<bool>("let x = new_ts1(); let y = new_ts2(); x == y")?);
+
+ Ok(())
+}
diff --git a/rhai/tests/optimizer.rs b/rhai/tests/optimizer.rs
new file mode 100644
index 0000000..ab733e1
--- /dev/null
+++ b/rhai/tests/optimizer.rs
@@ -0,0 +1,236 @@
+#![cfg(not(feature = "no_optimize"))]
+
+use rhai::{Engine, EvalAltResult, Module, OptimizationLevel, Scope, INT};
+
+#[test]
+fn test_optimizer() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ engine.set_optimization_level(OptimizationLevel::Simple);
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ const X = 0;
+ const X = 40 + 2 - 1 + 1;
+ X
+ "
+ )?,
+ 42
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_optimizer_run() -> Result<(), Box<EvalAltResult>> {
+ fn run_test(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
+ assert_eq!(engine.eval::<INT>("if true { 42 } else { 123 }")?, 42);
+ assert_eq!(
+ engine.eval::<INT>("if 1 == 1 || 2 > 3 { 42 } else { 123 }")?,
+ 42
+ );
+ assert_eq!(
+ engine.eval::<INT>(r#"const abc = "hello"; if abc < "foo" { 42 } else { 123 }"#)?,
+ 123
+ );
+ Ok(())
+ }
+
+ let mut engine = Engine::new();
+
+ engine.set_optimization_level(OptimizationLevel::None);
+ run_test(&mut engine)?;
+
+ engine.set_optimization_level(OptimizationLevel::Simple);
+ run_test(&mut engine)?;
+
+ engine.set_optimization_level(OptimizationLevel::Full);
+ run_test(&mut engine)?;
+
+ // Override == operator
+ engine.register_fn("==", |_x: INT, _y: INT| false);
+
+ engine.set_optimization_level(OptimizationLevel::Simple);
+
+ assert_eq!(
+ engine.eval::<INT>("if 1 == 1 || 2 > 3 { 42 } else { 123 }")?,
+ 42
+ );
+
+ engine.set_fast_operators(false);
+
+ assert_eq!(
+ engine.eval::<INT>("if 1 == 1 || 2 > 3 { 42 } else { 123 }")?,
+ 123
+ );
+
+ engine.set_optimization_level(OptimizationLevel::Full);
+
+ assert_eq!(
+ engine.eval::<INT>("if 1 == 1 || 2 > 3 { 42 } else { 123 }")?,
+ 123
+ );
+
+ Ok(())
+}
+
+#[cfg(feature = "metadata")]
+#[cfg(not(feature = "no_module"))]
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_position"))]
+#[test]
+fn test_optimizer_parse() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.set_optimization_level(OptimizationLevel::Simple);
+
+ let ast = engine.compile("{ const DECISION = false; if DECISION { 42 } else { 123 } }")?;
+
+ assert_eq!(
+ format!("{ast:?}"),
+ r#"AST { source: None, doc: None, resolver: None, body: [Expr(123 @ 1:53)] }"#
+ );
+
+ let ast = engine.compile("const DECISION = false; if DECISION { 42 } else { 123 }")?;
+
+ assert_eq!(
+ format!("{ast:?}"),
+ r#"AST { source: None, doc: None, resolver: None, body: [Var(("DECISION" @ 1:7, false @ 1:18, None), CONSTANT, 1:1), Expr(123 @ 1:51)] }"#
+ );
+
+ let ast = engine.compile("if 1 == 2 { 42 }")?;
+
+ assert_eq!(
+ format!("{ast:?}"),
+ r#"AST { source: None, doc: None, resolver: None, body: [] }"#
+ );
+
+ engine.set_optimization_level(OptimizationLevel::Full);
+
+ let ast = engine.compile("abs(-42)")?;
+
+ assert_eq!(
+ format!("{ast:?}"),
+ r#"AST { source: None, doc: None, resolver: None, body: [Expr(42 @ 1:1)] }"#
+ );
+
+ let ast = engine.compile("NUMBER")?;
+
+ assert_eq!(
+ format!("{ast:?}"),
+ r#"AST { source: None, doc: None, resolver: None, body: [Expr(Variable(NUMBER) @ 1:1)] }"#
+ );
+
+ let mut module = Module::new();
+ module.set_var("NUMBER", 42 as INT);
+
+ engine.register_global_module(module.into());
+
+ let ast = engine.compile("NUMBER")?;
+
+ assert_eq!(
+ format!("{ast:?}"),
+ r#"AST { source: None, doc: None, resolver: None, body: [Expr(42 @ 1:1)] }"#
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_function"))]
+#[test]
+fn test_optimizer_scope() -> Result<(), Box<EvalAltResult>> {
+ const SCRIPT: &str = "
+ fn foo() { FOO }
+ foo()
+ ";
+
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+
+ scope.push_constant("FOO", 42 as INT);
+
+ let ast = engine.compile_with_scope(&scope, SCRIPT)?;
+
+ scope.push("FOO", 123 as INT);
+
+ assert_eq!(engine.eval_ast::<INT>(&ast)?, 42);
+ assert_eq!(engine.eval_ast_with_scope::<INT>(&mut scope, &ast)?, 42);
+
+ let ast = engine.compile_with_scope(&scope, SCRIPT)?;
+
+ assert!(engine.eval_ast_with_scope::<INT>(&mut scope, &ast).is_err());
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_closure"))]
+#[test]
+fn test_optimizer_reoptimize() -> Result<(), Box<EvalAltResult>> {
+ const SCRIPT: &str = "
+ const FOO = 42;
+ fn foo() {
+ let f = || FOO * 2;
+ call(f)
+ }
+ foo()
+ ";
+
+ let engine = Engine::new();
+ let ast = engine.compile(SCRIPT)?;
+ let scope: Scope = ast.iter_literal_variables(true, false).collect();
+ let ast = engine.optimize_ast(&scope, ast, OptimizationLevel::Simple);
+
+ assert_eq!(engine.eval_ast::<INT>(&ast)?, 84);
+
+ Ok(())
+}
+
+#[test]
+fn test_optimizer_full() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, Clone)]
+ struct TestStruct(INT);
+
+ const SCRIPT: &str = "
+ const FOO = ts(40) + ts(2);
+ value(FOO)
+ ";
+
+ let mut engine = Engine::new();
+ let mut scope = Scope::new();
+
+ engine.set_optimization_level(OptimizationLevel::Full);
+
+ #[cfg(not(feature = "no_function"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ fn foo(x) { print(x); return; }
+ fn foo2(x) { if x > 0 {} return; }
+ 42
+ "
+ )?,
+ 42
+ );
+
+ engine
+ .register_type_with_name::<TestStruct>("TestStruct")
+ .register_fn("ts", |n: INT| TestStruct(n))
+ .register_fn("value", |ts: &mut TestStruct| ts.0)
+ .register_fn("+", |ts1: &mut TestStruct, ts2: TestStruct| {
+ TestStruct(ts1.0 + ts2.0)
+ });
+
+ let ast = engine.compile(SCRIPT)?;
+
+ #[cfg(feature = "internals")]
+ assert_eq!(ast.statements().len(), 2);
+
+ assert_eq!(engine.eval_ast_with_scope::<INT>(&mut scope, &ast)?, 42);
+
+ assert_eq!(scope.len(), 1);
+
+ assert_eq!(scope.get_value::<TestStruct>("FOO").unwrap().0, 42);
+
+ Ok(())
+}
diff --git a/rhai/tests/options.rs b/rhai/tests/options.rs
new file mode 100644
index 0000000..2ff30cb
--- /dev/null
+++ b/rhai/tests/options.rs
@@ -0,0 +1,130 @@
+use rhai::{Engine, EvalAltResult, Scope, INT};
+
+#[test]
+fn test_options_allow() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.compile("let x = if y { z } else { w };")?;
+
+ engine.set_allow_if_expression(false);
+
+ assert!(engine.compile("let x = if y { z } else { w };").is_err());
+
+ engine.compile("let x = { let z = 0; z + 1 };")?;
+
+ engine.set_allow_statement_expression(false);
+
+ assert!(engine.compile("let x = { let z = 0; z + 1 };").is_err());
+
+ #[cfg(not(feature = "no_function"))]
+ {
+ engine.compile("let x = || 42;")?;
+
+ engine.set_allow_anonymous_fn(false);
+
+ assert!(engine.compile("let x = || 42;").is_err());
+ }
+
+ let ast = engine.compile("let x = 0; while x < 10 { x += 1; }")?;
+
+ engine.set_allow_looping(false);
+
+ engine.run_ast(&ast)?;
+
+ assert!(engine
+ .compile("let x = 0; while x < 10 { x += 1; }")
+ .is_err());
+
+ engine.compile("let x = 42; let x = 123;")?;
+
+ engine.set_allow_shadowing(false);
+
+ assert!(engine.compile("let x = 42; let x = 123;").is_err());
+ assert!(engine.compile("const x = 42; let x = 123;").is_err());
+ assert!(engine.compile("let x = 42; const x = 123;").is_err());
+ assert!(engine.compile("const x = 42; const x = 123;").is_err());
+
+ let mut scope = Scope::new();
+ scope.push("x", 42 as INT);
+
+ assert!(engine.run_with_scope(&mut scope, "let x = 42;").is_err());
+
+ Ok(())
+}
+
+#[test]
+fn test_options_strict_var() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.compile("let x = if y { z } else { w };")?;
+
+ #[cfg(not(feature = "no_function"))]
+ engine.compile("fn foo(x) { x + y }")?;
+
+ #[cfg(not(feature = "no_module"))]
+ engine.compile("print(h::y::z);")?;
+
+ #[cfg(not(feature = "no_module"))]
+ engine.compile("let x = h::y::foo();")?;
+
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "no_module"))]
+ engine.compile("fn foo() { h::y::foo() }")?;
+
+ #[cfg(not(feature = "no_function"))]
+ engine.compile("let f = |y| x * y;")?;
+
+ let mut scope = Scope::new();
+ scope.push("x", 42 as INT);
+ scope.push_constant("y", 0 as INT);
+
+ engine.set_strict_variables(true);
+
+ assert!(engine.compile("let x = if y { z } else { w };").is_err());
+
+ #[cfg(not(feature = "no_object"))]
+ engine.compile_with_scope(&scope, "if x.abs() { y } else { x + y.len };")?;
+
+ engine.compile("let y = 42; let x = y;")?;
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope, "{ let y = 42; x * y }")?,
+ 42 * 42
+ );
+
+ #[cfg(not(feature = "no_function"))]
+ assert!(engine.compile("fn foo(x) { x + y }").is_err());
+
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "no_module"))]
+ {
+ assert!(engine.compile("print(h::y::z);").is_err());
+ assert!(engine.compile("fn foo() { h::y::z }").is_err());
+ assert!(engine.compile("fn foo() { h::y::foo() }").is_err());
+ engine.compile(r#"import "hello" as h; fn foo() { h::a::b::c } print(h::y::z);"#)?;
+ assert!(engine.compile("let x = h::y::foo();").is_err());
+ engine.compile(r#"import "hello" as h; fn foo() { h::a::b::c() } let x = h::y::foo();"#)?;
+ }
+
+ #[cfg(not(feature = "no_function"))]
+ {
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope, "fn foo(z) { z } let f = foo; call(f, x)")?,
+ 42
+ );
+ assert!(engine.compile("let f = |y| x * y;").is_err());
+ #[cfg(not(feature = "no_closure"))]
+ {
+ engine.compile("let x = 42; let f = |y| x * y;")?;
+ engine.compile("let x = 42; let f = |y| { || x + y };")?;
+ assert!(engine.compile("fn foo() { |y| { || x + y } }").is_err());
+ }
+ #[cfg(not(feature = "no_optimize"))]
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope, "fn foo(z) { y + z } foo(x)")?,
+ 42
+ );
+ }
+
+ Ok(())
+}
diff --git a/rhai/tests/packages.rs b/rhai/tests/packages.rs
new file mode 100644
index 0000000..49e9f27
--- /dev/null
+++ b/rhai/tests/packages.rs
@@ -0,0 +1,57 @@
+use rhai::packages::{Package, StandardPackage as SSS};
+use rhai::{def_package, Engine, EvalAltResult, Module, Scope, INT};
+
+#[cfg(not(feature = "no_module"))]
+#[cfg(not(feature = "no_custom_syntax"))]
+#[test]
+fn test_packages() -> Result<(), Box<EvalAltResult>> {
+ def_package! {
+ /// My custom package.
+ MyPackage(m) : SSS {
+ m.set_native_fn("hello", |x: INT| Ok(x + 1));
+ m.set_native_fn("@", |x: INT, y: INT| Ok(x * x + y * y));
+ } |> |engine| {
+ engine.register_custom_operator("@", 160).unwrap();
+ }
+ }
+
+ let pkg = MyPackage::new();
+
+ let make_call = |x: INT| -> Result<INT, Box<EvalAltResult>> {
+ // Create a raw Engine - extremely cheap.
+ let mut engine = Engine::new_raw();
+
+ // Register packages - cheap.
+ pkg.register_into_engine(&mut engine);
+ pkg.register_into_engine_as(&mut engine, "foo");
+
+ // Create custom scope - cheap.
+ let mut scope = Scope::new();
+
+ // Push variable into scope - relatively cheap.
+ scope.push("x", x);
+
+ // Evaluate script.
+
+ engine.eval_with_scope::<INT>(&mut scope, "hello(x) @ foo::hello(x)")
+ };
+
+ assert_eq!(make_call(42)?, 3698);
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_module"))]
+#[test]
+fn test_packages_with_script() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let ast = engine.compile("fn foo(x) { x + 1 } fn bar(x) { foo(x) + 1 }")?;
+
+ let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
+ engine.register_global_module(module.into());
+ assert_eq!(engine.eval::<INT>("foo(41)")?, 42);
+ assert_eq!(engine.eval::<INT>("bar(40)")?, 42);
+
+ Ok(())
+}
diff --git a/rhai/tests/plugins.rs b/rhai/tests/plugins.rs
new file mode 100644
index 0000000..7b1214c
--- /dev/null
+++ b/rhai/tests/plugins.rs
@@ -0,0 +1,226 @@
+#![cfg(not(feature = "no_index"))]
+#![cfg(not(feature = "no_module"))]
+
+use rhai::plugin::*;
+use rhai::{Engine, EvalAltResult, Scope, INT};
+
+mod test {
+ use super::*;
+
+ #[export_module]
+ pub mod special_array_package {
+ use rhai::{Array, INT};
+
+ pub const MYSTIC_NUMBER: INT = 42;
+
+ #[cfg(not(feature = "no_object"))]
+ pub mod feature {
+ use rhai::{Array, Dynamic, EvalAltResult};
+
+ #[rhai_fn(get = "foo", return_raw)]
+ #[inline(always)]
+ pub fn foo(array: &mut Array) -> Result<Dynamic, Box<EvalAltResult>> {
+ Ok(array[0].clone())
+ }
+ }
+
+ pub fn hash(_text: String) -> INT {
+ 42
+ }
+ pub fn hash2(_text: &str) -> INT {
+ 42
+ }
+
+ #[rhai_fn(name = "test", name = "hi")]
+ pub fn len(array: &mut Array, mul: INT) -> INT {
+ (array.len() as INT) * mul
+ }
+ #[rhai_fn(name = "+")]
+ pub fn funky_add(x: INT, y: INT) -> INT {
+ x / 2 + y * 2
+ }
+ #[rhai_fn(name = "no_effect", set = "no_effect", pure)]
+ pub fn no_effect(array: &mut Array, value: INT) {
+ // array is not modified
+ println!("Array = {array:?}, Value = {value}");
+ }
+ }
+}
+
+macro_rules! gen_unary_functions {
+ ($op_name:ident = $op_fn:ident ( $($arg_type:ident),+ ) -> $return_type:ident) => {
+ mod $op_name { $(
+ #[allow(non_snake_case)]
+ pub mod $arg_type {
+ use super::super::*;
+
+ #[export_fn(name="test")]
+ pub fn single(x: $arg_type) -> $return_type {
+ super::super::$op_fn(x)
+ }
+ }
+ )* }
+ }
+}
+
+macro_rules! reg_functions {
+ ($mod_name:ident += $op_name:ident :: $func:ident ( $($arg_type:ident),+ )) => {
+ $(register_exported_fn!($mod_name, stringify!($op_name), $op_name::$arg_type::$func);)*
+ }
+}
+
+fn make_greeting(n: impl std::fmt::Display) -> String {
+ format!("{n} kitties")
+}
+
+gen_unary_functions!(greet = make_greeting(INT, bool, char) -> String);
+
+macro_rules! expand_enum {
+ ($module:ident : $typ:ty => $($variant:ident),+) => {
+ #[export_module]
+ pub mod $module {
+ $(
+ #[allow(non_upper_case_globals)]
+ pub const $variant: $typ = <$typ>::$variant;
+ )*
+ }
+ };
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum MyEnum {
+ Foo,
+ Bar,
+ Baz,
+ Hello,
+ World,
+}
+
+expand_enum! { my_enum_module: MyEnum => Foo, Bar, Baz, Hello, World }
+
+#[test]
+fn test_plugins_package() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ let mut m = Module::new();
+ combine_with_exported_module!(&mut m, "test", test::special_array_package);
+ combine_with_exported_module!(&mut m, "enum", my_enum_module);
+ engine.register_global_module(m.into());
+
+ reg_functions!(engine += greet::single(INT, bool, char));
+
+ assert_eq!(engine.eval::<INT>("MYSTIC_NUMBER")?, 42);
+
+ #[cfg(not(feature = "no_object"))]
+ {
+ assert_eq!(engine.eval::<INT>("let a = [1, 2, 3]; a.foo")?, 1);
+ engine.run("const A = [1, 2, 3]; A.no_effect(42);")?;
+ engine.run("const A = [1, 2, 3]; A.no_effect = 42;")?;
+
+ assert!(
+ matches!(*engine.run("const A = [1, 2, 3]; A.test(42);").unwrap_err(),
+ EvalAltResult::ErrorNonPureMethodCallOnConstant(x, ..) if x == "test")
+ )
+ }
+
+ assert_eq!(engine.eval::<INT>(r#"hash("hello")"#)?, 42);
+ assert_eq!(engine.eval::<INT>(r#"hash2("hello")"#)?, 42);
+ assert_eq!(engine.eval::<INT>("let a = [1, 2, 3]; test(a, 2)")?, 6);
+ assert_eq!(engine.eval::<INT>("let a = [1, 2, 3]; hi(a, 2)")?, 6);
+ assert_eq!(engine.eval::<INT>("let a = [1, 2, 3]; test(a, 2)")?, 6);
+ assert_eq!(
+ engine.eval::<String>("let a = [1, 2, 3]; greet(test(a, 2))")?,
+ "6 kitties"
+ );
+ assert_eq!(engine.eval::<INT>("2 + 2")?, 4);
+
+ engine.set_fast_operators(false);
+ assert_eq!(engine.eval::<INT>("2 + 2")?, 5);
+
+ engine.register_static_module("test", exported_module!(test::special_array_package).into());
+
+ assert_eq!(engine.eval::<INT>("test::MYSTIC_NUMBER")?, 42);
+
+ Ok(())
+}
+
+#[test]
+fn test_plugins_parameters() -> Result<(), Box<EvalAltResult>> {
+ #[export_module]
+ mod rhai_std {
+ pub fn noop(_: &str) {}
+ }
+
+ let mut engine = Engine::new();
+
+ let std = exported_module!(rhai_std);
+
+ engine.register_static_module("std", std.into());
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"
+ let s = "hello";
+ std::noop(s);
+ s
+ "#
+ )?,
+ "hello"
+ );
+
+ Ok(())
+}
+
+#[cfg(target_pointer_width = "64")]
+mod handle {
+ use super::*;
+
+ #[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
+ pub struct WorldHandle(usize);
+ pub type World = Vec<i64>;
+
+ impl From<&mut World> for WorldHandle {
+ fn from(world: &mut World) -> Self {
+ Self::new(world)
+ }
+ }
+
+ impl AsMut<World> for WorldHandle {
+ fn as_mut(&mut self) -> &mut World {
+ unsafe { std::mem::transmute(self.0) }
+ }
+ }
+
+ impl WorldHandle {
+ pub fn new(world: &mut World) -> Self {
+ Self(unsafe { std::mem::transmute(world) })
+ }
+ }
+
+ #[export_module]
+ pub mod handle_module {
+ pub type Handle = WorldHandle;
+
+ #[rhai_fn(get = "len")]
+ pub fn len(world: &mut Handle) -> INT {
+ world.as_mut().len() as INT
+ }
+ }
+
+ #[test]
+ fn test_module_handle() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_global_module(exported_module!(handle_module).into());
+
+ let mut scope = Scope::new();
+
+ let world: &mut World = &mut vec![42];
+ scope.push("world", WorldHandle::from(world));
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(engine.eval_with_scope::<INT>(&mut scope, "world.len")?, 1);
+
+ Ok(())
+ }
+}
diff --git a/rhai/tests/plugins_register.rs b/rhai/tests/plugins_register.rs
new file mode 100644
index 0000000..525704c
--- /dev/null
+++ b/rhai/tests/plugins_register.rs
@@ -0,0 +1,16 @@
+use rhai::plugin::*;
+use rhai::{Engine, EvalAltResult, INT};
+
+#[export_fn]
+pub fn add_together(x: INT, y: INT) -> INT {
+ x + y
+}
+
+#[test]
+fn test_exported_fn_register() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ register_exported_fn!(engine, "add_two", add_together);
+ assert_eq!(engine.eval::<INT>("let a = 1; add_two(a, 41)")?, 42);
+
+ Ok(())
+}
diff --git a/rhai/tests/plugins_unroll.rs b/rhai/tests/plugins_unroll.rs
new file mode 100644
index 0000000..366f6dc
--- /dev/null
+++ b/rhai/tests/plugins_unroll.rs
@@ -0,0 +1,69 @@
+#![cfg(not(feature = "no_index"))]
+#![cfg(not(feature = "no_module"))]
+
+use rhai::plugin::*;
+use rhai::{Engine, EvalAltResult, Module, INT};
+
+pub fn add_generic<T: std::ops::Add<Output = T>>(x: T, y: T) -> T {
+ x + y
+}
+
+pub fn mul_generic<T: std::ops::Mul<Output = T>>(x: T, y: T) -> T {
+ x * y
+}
+
+macro_rules! generate_ops {
+ ($op_name:ident, $op_fn:ident, $($type_names:ident),+) => {
+ pub mod $op_name {
+ $(
+ pub mod $type_names {
+ use rhai::plugin::*;
+ use super::super::$op_fn;
+ #[export_fn]
+ pub fn op(x: $type_names, y: $type_names) -> $type_names {
+ $op_fn(x, y)
+ }
+ }
+ )*
+ }
+ }
+}
+
+macro_rules! register_in_bulk {
+ ($mod_name:ident, $op_name:ident, $($type_names:ident),+) => {
+ $(
+ {
+ let type_str = stringify!($type_names);
+ set_exported_fn!($mod_name,
+ &format!(concat!(stringify!($op_name), "_{}"), type_str),
+ crate::$op_name::$type_names::op);
+ }
+ )*
+ }
+}
+
+generate_ops!(add, add_generic, i8, i16, i32, i64);
+generate_ops!(mul, mul_generic, i8, i16, i32, i64);
+
+#[test]
+fn test_generated_ops() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ let mut m = Module::new();
+ register_in_bulk!(m, add, i8, i16, i32, i64);
+ register_in_bulk!(m, mul, i8, i16, i32, i64);
+
+ engine.register_global_module(m.into());
+
+ #[cfg(feature = "only_i32")]
+ assert_eq!(engine.eval::<INT>("let a = 0; add_i32(a, 1)")?, 1);
+ #[cfg(not(feature = "only_i32"))]
+ assert_eq!(engine.eval::<INT>("let a = 0; add_i64(a, 1)")?, 1);
+
+ #[cfg(feature = "only_i32")]
+ assert_eq!(engine.eval::<INT>("let a = 1; mul_i32(a, 2)")?, 2);
+ #[cfg(not(feature = "only_i32"))]
+ assert_eq!(engine.eval::<INT>("let a = 1; mul_i64(a, 2)")?, 2);
+
+ Ok(())
+}
diff --git a/rhai/tests/power_of.rs b/rhai/tests/power_of.rs
new file mode 100644
index 0000000..fb03064
--- /dev/null
+++ b/rhai/tests/power_of.rs
@@ -0,0 +1,59 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[cfg(not(feature = "no_float"))]
+use rhai::FLOAT;
+
+#[cfg(not(feature = "no_float"))]
+const EPSILON: FLOAT = 0.000_001;
+
+#[test]
+fn test_power_of() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("2 ** 3")?, 8);
+ assert_eq!(engine.eval::<INT>("(-2 ** 3)")?, -8);
+ assert_eq!(engine.eval::<INT>("2 ** 3 ** 2")?, 512);
+
+ #[cfg(not(feature = "no_float"))]
+ {
+ assert!(
+ (engine.eval::<FLOAT>("2.2 ** 3.3")? - 13.489_468_760_533_386 as FLOAT).abs()
+ <= EPSILON
+ );
+ assert!((engine.eval::<FLOAT>("2.0**-2.0")? - 0.25 as FLOAT).abs() < EPSILON);
+ assert!((engine.eval::<FLOAT>("(-2.0**-2.0)")? - 0.25 as FLOAT).abs() < EPSILON);
+ assert!((engine.eval::<FLOAT>("(-2.0**-2)")? - 0.25 as FLOAT).abs() < EPSILON);
+ assert_eq!(engine.eval::<INT>("4**3")?, 64);
+ }
+
+ Ok(())
+}
+
+#[test]
+fn test_power_of_equals() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("let x = 2; x **= 3; x")?, 8);
+ assert_eq!(engine.eval::<INT>("let x = -2; x **= 3; x")?, -8);
+
+ #[cfg(not(feature = "no_float"))]
+ {
+ assert!(
+ (engine.eval::<FLOAT>("let x = 2.2; x **= 3.3; x")? - 13.489_468_760_533_386 as FLOAT)
+ .abs()
+ <= EPSILON
+ );
+ assert!(
+ (engine.eval::<FLOAT>("let x = 2.0; x **= -2.0; x")? - 0.25 as FLOAT).abs() < EPSILON
+ );
+ assert!(
+ (engine.eval::<FLOAT>("let x = -2.0; x **= -2.0; x")? - 0.25 as FLOAT).abs() < EPSILON
+ );
+ assert!(
+ (engine.eval::<FLOAT>("let x = -2.0; x **= -2; x")? - 0.25 as FLOAT).abs() < EPSILON
+ );
+ assert_eq!(engine.eval::<INT>("let x =4; x **= 3; x")?, 64);
+ }
+
+ Ok(())
+}
diff --git a/rhai/tests/print.rs b/rhai/tests/print.rs
new file mode 100644
index 0000000..982b2ff
--- /dev/null
+++ b/rhai/tests/print.rs
@@ -0,0 +1,119 @@
+use rhai::{Engine, EvalAltResult, Scope, INT};
+use std::sync::{Arc, RwLock};
+
+#[cfg(not(feature = "only_i32"))]
+#[cfg(not(feature = "only_i64"))]
+#[test]
+fn test_to_string() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ let mut scope = Scope::new();
+ scope.push("x", 42_u8);
+ scope.push("y", 42_i32);
+ scope.push("z", 42_i16);
+
+ assert_eq!(
+ engine.eval_with_scope::<String>(&mut scope, "to_string(x)")?,
+ "42"
+ );
+ assert_eq!(
+ engine.eval_with_scope::<String>(&mut scope, "to_string(x)")?,
+ "42"
+ );
+ assert_eq!(
+ engine.eval_with_scope::<String>(&mut scope, "to_string(x)")?,
+ "42"
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_print_debug() -> Result<(), Box<EvalAltResult>> {
+ let logbook = Arc::new(RwLock::new(Vec::<String>::new()));
+
+ // Redirect print/debug output to 'log'
+ let log1 = logbook.clone();
+ let log2 = logbook.clone();
+
+ let mut engine = Engine::new();
+
+ engine
+ .on_print(move |s| log1.write().unwrap().push(format!("entry: {s}")))
+ .on_debug(move |s, src, pos| {
+ let src = src.unwrap_or("unknown");
+ log2.write()
+ .unwrap()
+ .push(format!("DEBUG of {src} at {pos:?}: {s}"))
+ });
+
+ // Evaluate script
+ engine.run("print(40 + 2)")?;
+ let mut ast = engine.compile(r#"let x = "hello!"; debug(x)"#)?;
+ ast.set_source("world");
+ engine.run_ast(&ast)?;
+
+ // 'logbook' captures all the 'print' and 'debug' output
+ assert_eq!(logbook.read().unwrap().len(), 2);
+ assert_eq!(logbook.read().unwrap()[0], "entry: 42");
+ assert_eq!(
+ logbook.read().unwrap()[1],
+ if cfg!(not(feature = "no_position")) {
+ r#"DEBUG of world at 1:19: "hello!""#
+ } else {
+ r#"DEBUG of world at none: "hello!""#
+ }
+ );
+
+ for entry in logbook.read().unwrap().iter() {
+ println!("{entry}");
+ }
+
+ Ok(())
+}
+
+#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
+struct MyStruct {
+ field: INT,
+}
+
+impl std::fmt::Display for MyStruct {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "hello: {}", self.field)
+ }
+}
+
+#[cfg(not(feature = "no_object"))]
+#[test]
+fn test_print_custom_type() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine
+ .register_type_with_name::<MyStruct>("MyStruct")
+ .register_fn("to_debug", |x: &mut MyStruct| x.to_string())
+ .register_fn("debug", |x: &mut MyStruct| x.to_string())
+ .register_fn("new_ts", || MyStruct { field: 42 });
+
+ engine.run("let x = new_ts(); debug(x);")?;
+
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ engine.eval::<String>(
+ r#"
+ let x = [ 123, true, (), "world", new_ts() ];
+ x.to_string()
+ "#
+ )?,
+ r#"[123, true, (), "world", hello: 42]"#
+ );
+
+ assert!(engine
+ .eval::<String>(
+ r#"
+ let x = #{ a:123, b:true, c:(), d:"world", e:new_ts() };
+ x.to_string()
+ "#
+ )?
+ .contains(r#""e": hello: 42"#));
+ Ok(())
+}
diff --git a/rhai/tests/serde.rs b/rhai/tests/serde.rs
new file mode 100644
index 0000000..c8309ce
--- /dev/null
+++ b/rhai/tests/serde.rs
@@ -0,0 +1,928 @@
+#![cfg(feature = "serde")]
+
+use rhai::{
+ serde::{from_dynamic, to_dynamic},
+ Dynamic, Engine, EvalAltResult, ImmutableString, Scope, INT,
+};
+use serde::{Deserialize, Deserializer, Serialize};
+use serde_json::json;
+use std::sync::Arc;
+
+#[cfg(not(feature = "no_index"))]
+use rhai::Array;
+#[cfg(not(feature = "no_object"))]
+use rhai::Map;
+#[cfg(not(feature = "no_float"))]
+use rhai::FLOAT;
+#[cfg(feature = "decimal")]
+use rust_decimal::Decimal;
+
+#[test]
+fn test_serde_ser_primary_types() -> Result<(), Box<EvalAltResult>> {
+ assert!(to_dynamic(42_u64)?.is_int());
+ assert!(to_dynamic(42 as INT)?.is_int());
+ assert!(to_dynamic(true)?.is_bool());
+ assert!(to_dynamic(())?.is_unit());
+
+ #[cfg(not(feature = "no_float"))]
+ {
+ assert!(to_dynamic(123.456_f64)?.is::<FLOAT>());
+ assert!(to_dynamic(123.456_f32)?.is::<FLOAT>());
+ }
+
+ #[cfg(feature = "no_float")]
+ #[cfg(feature = "decimal")]
+ {
+ assert!(to_dynamic(123.456_f64)?.is::<Decimal>());
+ assert!(to_dynamic(123.456_f32)?.is::<Decimal>());
+ }
+
+ assert!(to_dynamic("hello".to_string())?.is::<String>());
+
+ Ok(())
+}
+
+#[test]
+fn test_serde_ser_integer_types() -> Result<(), Box<EvalAltResult>> {
+ assert!(to_dynamic(42_i8)?.is_int());
+ assert!(to_dynamic(42_i16)?.is_int());
+ assert!(to_dynamic(42_i32)?.is_int());
+ assert!(to_dynamic(42_i64)?.is_int());
+ assert!(to_dynamic(42_u8)?.is_int());
+ assert!(to_dynamic(42_u16)?.is_int());
+ assert!(to_dynamic(42_u32)?.is_int());
+ assert!(to_dynamic(42_u64)?.is_int());
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_index"))]
+fn test_serde_ser_array() -> Result<(), Box<EvalAltResult>> {
+ let arr: Vec<INT> = vec![123, 456, 42, 999];
+
+ let d = to_dynamic(arr)?;
+ assert!(d.is_array());
+ assert_eq!(4, d.cast::<Array>().len());
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_index"))]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_ser_struct() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, Serialize, PartialEq)]
+ struct Hello {
+ a: INT,
+ b: bool,
+ }
+
+ #[derive(Debug, Serialize, PartialEq)]
+ struct Test {
+ int: u32,
+ seq: Vec<String>,
+ obj: Hello,
+ }
+
+ let x = Test {
+ int: 42,
+ seq: vec!["hello".into(), "kitty".into(), "world".into()],
+ obj: Hello { a: 123, b: true },
+ };
+
+ let d = to_dynamic(x)?;
+
+ assert!(d.is_map());
+
+ let mut map = d.cast::<Map>();
+ let obj = map.remove("obj").unwrap().cast::<Map>();
+ let mut seq = map.remove("seq").unwrap().cast::<Array>();
+
+ assert_eq!(Ok(123), obj["a"].as_int());
+ assert!(obj["b"].as_bool().unwrap());
+ assert_eq!(Ok(42), map["int"].as_int());
+ assert_eq!(seq.len(), 3);
+ assert_eq!("kitty", seq.remove(1).into_string().unwrap());
+
+ Ok(())
+}
+
+#[test]
+fn test_serde_ser_unit_enum() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Serialize)]
+ enum MyEnum {
+ VariantFoo,
+ VariantBar,
+ }
+
+ let d = to_dynamic(MyEnum::VariantFoo)?;
+ assert_eq!("VariantFoo", d.into_string().unwrap());
+
+ let d = to_dynamic(MyEnum::VariantBar)?;
+ assert_eq!("VariantBar", d.into_string().unwrap());
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_ser_externally_tagged_enum() -> Result<(), Box<EvalAltResult>> {
+ #[allow(clippy::enum_variant_names)]
+ #[derive(Serialize)]
+ enum MyEnum {
+ VariantUnit,
+ #[cfg(not(feature = "no_index"))]
+ VariantUnitTuple(),
+ VariantNewtype(i32),
+ #[cfg(not(feature = "no_index"))]
+ VariantTuple(i32, i32),
+ VariantEmptyStruct {},
+ VariantStruct {
+ a: i32,
+ },
+ }
+
+ {
+ assert_eq!(
+ "VariantUnit",
+ to_dynamic(MyEnum::VariantUnit)?
+ .into_immutable_string()
+ .unwrap()
+ .as_str()
+ );
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ {
+ let mut map = to_dynamic(MyEnum::VariantUnitTuple())?.cast::<Map>();
+ let content = map.remove("VariantUnitTuple").unwrap().cast::<Array>();
+ assert!(map.is_empty());
+ assert!(content.is_empty());
+ }
+
+ let mut map = to_dynamic(MyEnum::VariantNewtype(123))?.cast::<Map>();
+ let content = map.remove("VariantNewtype").unwrap();
+ assert!(map.is_empty());
+ assert_eq!(Ok(123), content.as_int());
+
+ #[cfg(not(feature = "no_index"))]
+ {
+ let mut map = to_dynamic(MyEnum::VariantTuple(123, 456))?.cast::<Map>();
+ let content = map.remove("VariantTuple").unwrap().cast::<Array>();
+ assert!(map.is_empty());
+ assert_eq!(2, content.len());
+ assert_eq!(Ok(123), content[0].as_int());
+ assert_eq!(Ok(456), content[1].as_int());
+ }
+
+ let mut map = to_dynamic(MyEnum::VariantEmptyStruct {})?.cast::<Map>();
+ let map_inner = map.remove("VariantEmptyStruct").unwrap().cast::<Map>();
+ assert!(map.is_empty());
+ assert!(map_inner.is_empty());
+
+ let mut map = to_dynamic(MyEnum::VariantStruct { a: 123 })?.cast::<Map>();
+ let mut map_inner = map.remove("VariantStruct").unwrap().cast::<Map>();
+ assert!(map.is_empty());
+ assert_eq!(Ok(123), map_inner.remove("a").unwrap().as_int());
+ assert!(map_inner.is_empty());
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_ser_internally_tagged_enum() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Serialize)]
+ #[serde(tag = "tag")]
+ enum MyEnum {
+ VariantEmptyStruct {},
+ VariantStruct { a: i32 },
+ }
+
+ let mut map = to_dynamic(MyEnum::VariantEmptyStruct {})?.cast::<Map>();
+ assert_eq!(
+ "VariantEmptyStruct",
+ map.remove("tag")
+ .unwrap()
+ .into_immutable_string()
+ .unwrap()
+ .as_str()
+ );
+ assert!(map.is_empty());
+
+ let mut map = to_dynamic(MyEnum::VariantStruct { a: 123 })?.cast::<Map>();
+ assert_eq!(
+ "VariantStruct",
+ map.remove("tag")
+ .unwrap()
+ .into_immutable_string()
+ .unwrap()
+ .as_str()
+ );
+ assert_eq!(Ok(123), map.remove("a").unwrap().as_int());
+ assert!(map.is_empty());
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_ser_adjacently_tagged_enum() -> Result<(), Box<EvalAltResult>> {
+ #[allow(clippy::enum_variant_names)]
+ #[derive(Serialize)]
+ #[serde(tag = "tag", content = "content")]
+ enum MyEnum {
+ VariantUnit,
+ #[cfg(not(feature = "no_index"))]
+ VariantUnitTuple(),
+ VariantNewtype(i32),
+ #[cfg(not(feature = "no_index"))]
+ VariantTuple(i32, i32),
+ VariantEmptyStruct {},
+ VariantStruct {
+ a: i32,
+ },
+ }
+
+ let mut map = to_dynamic(MyEnum::VariantUnit)?.cast::<Map>();
+ assert_eq!(
+ "VariantUnit",
+ map.remove("tag")
+ .unwrap()
+ .into_immutable_string()
+ .unwrap()
+ .as_str()
+ );
+ assert!(map.is_empty());
+
+ #[cfg(not(feature = "no_index"))]
+ {
+ let mut map = to_dynamic(MyEnum::VariantUnitTuple())?.cast::<Map>();
+ assert_eq!(
+ "VariantUnitTuple",
+ map.remove("tag")
+ .unwrap()
+ .into_immutable_string()
+ .unwrap()
+ .as_str()
+ );
+ let content = map.remove("content").unwrap().cast::<Array>();
+ assert!(map.is_empty());
+ assert!(content.is_empty());
+ }
+
+ let mut map = to_dynamic(MyEnum::VariantNewtype(123))?.cast::<Map>();
+ assert_eq!(
+ "VariantNewtype",
+ map.remove("tag")
+ .unwrap()
+ .into_immutable_string()
+ .unwrap()
+ .as_str()
+ );
+ let content = map.remove("content").unwrap();
+ assert!(map.is_empty());
+ assert_eq!(Ok(123), content.as_int());
+
+ #[cfg(not(feature = "no_index"))]
+ {
+ let mut map = to_dynamic(MyEnum::VariantTuple(123, 456))?.cast::<Map>();
+ assert_eq!(
+ "VariantTuple",
+ map.remove("tag")
+ .unwrap()
+ .into_immutable_string()
+ .unwrap()
+ .as_str()
+ );
+ let content = map.remove("content").unwrap().cast::<Array>();
+ assert!(map.is_empty());
+ assert_eq!(2, content.len());
+ assert_eq!(Ok(123), content[0].as_int());
+ assert_eq!(Ok(456), content[1].as_int());
+ }
+
+ let mut map = to_dynamic(MyEnum::VariantEmptyStruct {})?.cast::<Map>();
+ assert_eq!(
+ "VariantEmptyStruct",
+ map.remove("tag")
+ .unwrap()
+ .into_immutable_string()
+ .unwrap()
+ .as_str()
+ );
+ let map_inner = map.remove("content").unwrap().cast::<Map>();
+ assert!(map.is_empty());
+ assert!(map_inner.is_empty());
+
+ let mut map = to_dynamic(MyEnum::VariantStruct { a: 123 })?.cast::<Map>();
+ assert_eq!(
+ "VariantStruct",
+ map.remove("tag").unwrap().into_string().unwrap()
+ );
+ let mut map_inner = map.remove("content").unwrap().cast::<Map>();
+ assert!(map.is_empty());
+ assert_eq!(Ok(123), map_inner.remove("a").unwrap().as_int());
+ assert!(map_inner.is_empty());
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_ser_untagged_enum() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Serialize)]
+ #[serde(untagged)]
+ enum MyEnum {
+ VariantEmptyStruct {},
+ VariantStruct1 { a: i32 },
+ VariantStruct2 { b: i32 },
+ }
+
+ let map = to_dynamic(MyEnum::VariantEmptyStruct {})?.cast::<Map>();
+ assert!(map.is_empty());
+
+ let mut map = to_dynamic(MyEnum::VariantStruct1 { a: 123 })?.cast::<Map>();
+ assert_eq!(Ok(123), map.remove("a").unwrap().as_int());
+ assert!(map.is_empty());
+
+ let mut map = to_dynamic(MyEnum::VariantStruct2 { b: 123 })?.cast::<Map>();
+ assert_eq!(Ok(123), map.remove("b").unwrap().as_int());
+ assert!(map.is_empty());
+
+ Ok(())
+}
+
+#[test]
+fn test_serde_de_primary_types() -> Result<(), Box<EvalAltResult>> {
+ assert_eq!(42, from_dynamic::<u16>(&Dynamic::from(42_u16))?);
+ assert_eq!(42, from_dynamic::<INT>(&(42 as INT).into())?);
+ assert!(from_dynamic::<bool>(&true.into())?);
+ let _: () = from_dynamic::<()>(&().into()).unwrap();
+
+ #[cfg(not(feature = "no_float"))]
+ {
+ assert_eq!(123.456, from_dynamic::<FLOAT>(&123.456.into())?);
+ assert_eq!(123.456, from_dynamic::<f32>(&Dynamic::from(123.456_f32))?);
+ }
+
+ #[cfg(feature = "no_float")]
+ #[cfg(feature = "decimal")]
+ {
+ let d: Dynamic = Decimal::from_str("123.456").unwrap().into();
+
+ assert_eq!(123.456, from_dynamic::<f64>(&d)?);
+ assert_eq!(123.456, from_dynamic::<f32>(&d)?);
+ }
+
+ assert_eq!(
+ "hello",
+ from_dynamic::<String>(&"hello".to_string().into())?
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_object"))]
+#[test]
+fn test_serde_de_variants() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug)]
+ struct Foo;
+
+ #[derive(Debug, Deserialize)]
+ struct Bar {
+ #[serde(deserialize_with = "deserialize_foo")]
+ value: Arc<Foo>,
+ }
+
+ fn deserialize_foo<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Arc<Foo>, D::Error> {
+ let value = <Dynamic as Deserialize>::deserialize(deserializer)?;
+
+ value
+ .try_cast::<Arc<Foo>>()
+ .ok_or_else(|| serde::de::Error::custom("type error"))
+ }
+
+ let value = Arc::new(Foo);
+ let mut map = Map::new();
+ map.insert("value".into(), Dynamic::from(value.clone()));
+ let x = Dynamic::from(map);
+ let bar = from_dynamic::<Bar>(&x)?;
+
+ assert!(Arc::ptr_eq(&bar.value, &value));
+
+ Ok(())
+}
+
+#[test]
+fn test_serde_de_integer_types() -> Result<(), Box<EvalAltResult>> {
+ assert_eq!(42, from_dynamic::<i8>(&Dynamic::from(42 as INT))?);
+ assert_eq!(42, from_dynamic::<i16>(&Dynamic::from(42 as INT))?);
+ assert_eq!(42, from_dynamic::<i32>(&Dynamic::from(42 as INT))?);
+ assert_eq!(42, from_dynamic::<i64>(&Dynamic::from(42 as INT))?);
+ assert_eq!(42, from_dynamic::<u8>(&Dynamic::from(42 as INT))?);
+ assert_eq!(42, from_dynamic::<u16>(&Dynamic::from(42 as INT))?);
+ assert_eq!(42, from_dynamic::<u32>(&Dynamic::from(42 as INT))?);
+ assert_eq!(42, from_dynamic::<u64>(&Dynamic::from(42 as INT))?);
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_index"))]
+fn test_serde_de_array() -> Result<(), Box<EvalAltResult>> {
+ let arr: Vec<INT> = vec![123, 456, 42, 999];
+ assert_eq!(arr, from_dynamic::<Vec<INT>>(&arr.clone().into())?);
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_index"))]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_de_struct() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, Deserialize, PartialEq)]
+ struct Hello {
+ a: INT,
+ b: bool,
+ }
+
+ #[derive(Debug, Deserialize, PartialEq)]
+ struct Test {
+ int: u32,
+ seq: Vec<String>,
+ obj: Hello,
+ }
+
+ let mut map = Map::new();
+ map.insert("int".into(), Dynamic::from(42_u32));
+
+ let mut map2 = Map::new();
+ map2.insert("a".into(), (123 as INT).into());
+ map2.insert("b".into(), true.into());
+
+ map.insert("obj".into(), map2.into());
+
+ let arr: Array = vec!["hello".into(), "kitty".into(), "world".into()];
+ map.insert("seq".into(), arr.into());
+
+ let expected = Test {
+ int: 42,
+ seq: vec!["hello".into(), "kitty".into(), "world".into()],
+ obj: Hello { a: 123, b: true },
+ };
+ assert_eq!(expected, from_dynamic(&map.into())?);
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_index"))]
+#[cfg(not(feature = "no_object"))]
+#[cfg(not(feature = "no_float"))]
+fn test_serde_de_script() -> Result<(), Box<EvalAltResult>> {
+ #[allow(dead_code)]
+ #[derive(Debug, Deserialize)]
+ struct Point {
+ x: FLOAT,
+ y: FLOAT,
+ }
+
+ #[allow(dead_code)]
+ #[derive(Debug, Deserialize)]
+ struct MyStruct {
+ a: i64,
+ b: Vec<String>,
+ c: bool,
+ d: Point,
+ }
+
+ let engine = Engine::new();
+
+ let result: Dynamic = engine.eval(
+ r#"
+ #{
+ a: 42,
+ b: [ "hello", "world" ],
+ c: true,
+ d: #{ x: 123.456, y: 999.0 }
+ }
+ "#,
+ )?;
+
+ // Convert the 'Dynamic' object map into 'MyStruct'
+ let _: MyStruct = from_dynamic(&result)?;
+
+ Ok(())
+}
+
+#[test]
+fn test_serde_de_unit_enum() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, PartialEq, Deserialize)]
+ enum MyEnum {
+ VariantFoo,
+ VariantBar,
+ }
+
+ let d = Dynamic::from("VariantFoo".to_string());
+ assert_eq!(MyEnum::VariantFoo, from_dynamic(&d)?);
+
+ let d = Dynamic::from("VariantBar".to_string());
+ assert_eq!(MyEnum::VariantBar, from_dynamic(&d)?);
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_de_externally_tagged_enum() -> Result<(), Box<EvalAltResult>> {
+ #[allow(clippy::enum_variant_names)]
+ #[derive(Debug, PartialEq, Deserialize)]
+ #[serde(deny_unknown_fields)]
+ enum MyEnum {
+ VariantUnit,
+ #[cfg(not(feature = "no_index"))]
+ VariantUnitTuple(),
+ VariantNewtype(i32),
+ #[cfg(not(feature = "no_index"))]
+ VariantTuple(i32, i32),
+ VariantEmptyStruct {},
+ VariantStruct {
+ a: i32,
+ },
+ }
+
+ let d = Dynamic::from("VariantUnit".to_string());
+ assert_eq!(MyEnum::VariantUnit, from_dynamic(&d).unwrap());
+
+ #[cfg(not(feature = "no_index"))]
+ {
+ let array: Array = vec![];
+ let mut map_outer = Map::new();
+ map_outer.insert("VariantUnitTuple".into(), array.into());
+ assert_eq!(
+ MyEnum::VariantUnitTuple(),
+ from_dynamic(&map_outer.into()).unwrap()
+ );
+ }
+
+ let mut map_outer = Map::new();
+ map_outer.insert("VariantNewtype".into(), (123 as INT).into());
+ assert_eq!(
+ MyEnum::VariantNewtype(123),
+ from_dynamic(&map_outer.into()).unwrap()
+ );
+
+ #[cfg(not(feature = "no_index"))]
+ {
+ let array: Array = vec![(123 as INT).into(), (456 as INT).into()];
+ let mut map_outer = Map::new();
+ map_outer.insert("VariantTuple".into(), array.into());
+ assert_eq!(
+ MyEnum::VariantTuple(123, 456),
+ from_dynamic(&map_outer.into()).unwrap()
+ );
+ }
+
+ let map_inner = Map::new();
+ let mut map_outer = Map::new();
+ map_outer.insert("VariantEmptyStruct".into(), map_inner.into());
+ assert_eq!(
+ MyEnum::VariantEmptyStruct {},
+ from_dynamic(&map_outer.into()).unwrap()
+ );
+
+ let mut map_inner = Map::new();
+ map_inner.insert("a".into(), (123 as INT).into());
+ let mut map_outer = Map::new();
+ map_outer.insert("VariantStruct".into(), map_inner.into());
+ assert_eq!(
+ MyEnum::VariantStruct { a: 123 },
+ from_dynamic(&map_outer.into()).unwrap()
+ );
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_de_internally_tagged_enum() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, PartialEq, Deserialize)]
+ #[serde(tag = "tag", deny_unknown_fields)]
+ enum MyEnum {
+ VariantEmptyStruct {},
+ VariantStruct { a: i32 },
+ }
+
+ let mut map = Map::new();
+ map.insert("tag".into(), "VariantStruct".into());
+ map.insert("a".into(), (123 as INT).into());
+ assert_eq!(
+ MyEnum::VariantStruct { a: 123 },
+ from_dynamic(&map.into()).unwrap()
+ );
+
+ let mut map = Map::new();
+ map.insert("tag".into(), "VariantEmptyStruct".into());
+ assert_eq!(
+ MyEnum::VariantEmptyStruct {},
+ from_dynamic(&map.into()).unwrap()
+ );
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_de_adjacently_tagged_enum() -> Result<(), Box<EvalAltResult>> {
+ #[allow(clippy::enum_variant_names)]
+ #[derive(Debug, PartialEq, Deserialize)]
+ #[serde(tag = "tag", content = "content", deny_unknown_fields)]
+ enum MyEnum {
+ VariantUnit,
+ #[cfg(not(feature = "no_index"))]
+ VariantUnitTuple(),
+ VariantNewtype(i32),
+ #[cfg(not(feature = "no_index"))]
+ VariantTuple(i32, i32),
+ VariantEmptyStruct {},
+ VariantStruct {
+ a: i32,
+ },
+ }
+
+ let mut map_outer = Map::new();
+ map_outer.insert("tag".into(), "VariantUnit".into());
+ assert_eq!(
+ MyEnum::VariantUnit,
+ from_dynamic(&map_outer.into()).unwrap()
+ );
+
+ #[cfg(not(feature = "no_index"))]
+ {
+ let array: Array = vec![];
+ let mut map_outer = Map::new();
+ map_outer.insert("tag".into(), "VariantUnitTuple".into());
+ map_outer.insert("content".into(), array.into());
+ assert_eq!(
+ MyEnum::VariantUnitTuple(),
+ from_dynamic(&map_outer.into()).unwrap()
+ );
+ }
+
+ let mut map_outer = Map::new();
+ map_outer.insert("tag".into(), "VariantNewtype".into());
+ map_outer.insert("content".into(), (123 as INT).into());
+ assert_eq!(
+ MyEnum::VariantNewtype(123),
+ from_dynamic(&map_outer.into()).unwrap()
+ );
+
+ #[cfg(not(feature = "no_index"))]
+ {
+ let array: Array = vec![(123 as INT).into(), (456 as INT).into()];
+ let mut map_outer = Map::new();
+ map_outer.insert("tag".into(), "VariantTuple".into());
+ map_outer.insert("content".into(), array.into());
+ assert_eq!(
+ MyEnum::VariantTuple(123, 456),
+ from_dynamic(&map_outer.into()).unwrap()
+ );
+ }
+
+ let map_inner = Map::new();
+ let mut map_outer = Map::new();
+ map_outer.insert("tag".into(), "VariantEmptyStruct".into());
+ map_outer.insert("content".into(), map_inner.into());
+ assert_eq!(
+ MyEnum::VariantEmptyStruct {},
+ from_dynamic(&map_outer.into()).unwrap()
+ );
+
+ let mut map_inner = Map::new();
+ map_inner.insert("a".into(), (123 as INT).into());
+ let mut map_outer = Map::new();
+ map_outer.insert("tag".into(), "VariantStruct".into());
+ map_outer.insert("content".into(), map_inner.into());
+ assert_eq!(
+ MyEnum::VariantStruct { a: 123 },
+ from_dynamic(&map_outer.into()).unwrap()
+ );
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_de_untagged_enum() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, PartialEq, Deserialize)]
+ #[serde(untagged, deny_unknown_fields)]
+ enum MyEnum {
+ VariantEmptyStruct {},
+ VariantStruct1 { a: i32 },
+ VariantStruct2 { b: i32 },
+ }
+
+ let map = Map::new();
+ assert_eq!(
+ MyEnum::VariantEmptyStruct {},
+ from_dynamic(&map.into()).unwrap()
+ );
+
+ let mut map = Map::new();
+ map.insert("a".into(), (123 as INT).into());
+ assert_eq!(
+ MyEnum::VariantStruct1 { a: 123 },
+ from_dynamic(&map.into()).unwrap()
+ );
+
+ let mut map = Map::new();
+ map.insert("b".into(), (123 as INT).into());
+ assert_eq!(
+ MyEnum::VariantStruct2 { b: 123 },
+ from_dynamic(&map.into()).unwrap()
+ );
+
+ Ok(())
+}
+
+#[test]
+#[cfg(feature = "metadata")]
+#[cfg(not(feature = "no_object"))]
+#[cfg(not(feature = "no_index"))]
+fn test_serde_json() -> serde_json::Result<()> {
+ let s: ImmutableString = "hello".into();
+ assert_eq!(serde_json::to_string(&s)?, r#""hello""#);
+
+ let mut map = Map::new();
+ map.insert("a".into(), (123 as INT).into());
+
+ let arr: Array = vec![(1 as INT).into(), (2 as INT).into(), (3 as INT).into()];
+ map.insert("b".into(), arr.into());
+ map.insert("c".into(), true.into());
+ let d: Dynamic = map.into();
+
+ let json = serde_json::to_string(&d)?;
+
+ assert!(json.contains("\"a\":123"));
+ assert!(json.contains("\"b\":[1,2,3]"));
+ assert!(json.contains("\"c\":true"));
+
+ let d2: Dynamic = serde_json::from_str(&json)?;
+
+ assert!(d2.is_map());
+
+ let mut m = d2.cast::<Map>();
+
+ assert_eq!(m["a"].as_int().unwrap(), 123);
+ assert!(m["c"].as_bool().unwrap());
+
+ let a = m.remove("b").unwrap().cast::<Array>();
+
+ assert_eq!(a.len(), 3);
+ assert_eq!(format!("{a:?}"), "[1, 2, 3]");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(feature = "metadata")]
+#[cfg(feature = "decimal")]
+#[cfg(not(feature = "no_float"))]
+fn test_serde_json_numbers() -> serde_json::Result<()> {
+ use std::str::FromStr;
+
+ let d: Dynamic = serde_json::from_str("100000000000")?;
+ assert!(d.is::<INT>());
+ assert_eq!(d.as_int().unwrap(), 100000000000);
+
+ let d: Dynamic = serde_json::from_str("10000000000000000000")?;
+ assert!(d.is::<Decimal>());
+ assert_eq!(
+ d.as_decimal().unwrap(),
+ Decimal::from_str("10000000000000000000").unwrap()
+ );
+
+ let d: Dynamic = serde_json::from_str("10000000000000000000000000")?;
+ assert!(d.is::<FLOAT>());
+ assert_eq!(d.as_float().unwrap(), 10000000000000000000000000.0);
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_optional() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+ struct TestStruct {
+ foo: Option<char>,
+ }
+
+ let mut engine = Engine::new();
+ engine.register_type_with_name::<TestStruct>("TestStruct");
+
+ let r = engine.eval::<Dynamic>("#{ foo: 'a' }")?;
+
+ assert_eq!(
+ from_dynamic::<TestStruct>(&r)?,
+ TestStruct { foo: Some('a') }
+ );
+
+ let r = engine.eval::<Dynamic>("#{ foo: () }")?;
+
+ assert_eq!(from_dynamic::<TestStruct>(&r)?, TestStruct { foo: None });
+
+ let r = engine.eval::<Dynamic>("#{ }")?;
+
+ assert_eq!(from_dynamic::<TestStruct>(&r)?, TestStruct { foo: None });
+
+ let ts = TestStruct { foo: Some('a') };
+
+ let r = to_dynamic(&ts)?;
+
+ let map = r.cast::<Map>();
+
+ assert_eq!(map.len(), 1);
+ assert_eq!(map.get("foo").unwrap().as_char().unwrap(), 'a');
+
+ let ts = TestStruct { foo: None };
+
+ let r = to_dynamic(&ts)?;
+
+ let map = r.cast::<Map>();
+
+ assert_eq!(map.len(), 1);
+ let _: () = map.get("foo").unwrap().as_unit().unwrap();
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_index"))]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_blob() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ let r = engine.eval::<Dynamic>(
+ "
+ let x = blob(10);
+ for i in 0..10 { x[i] = i; }
+ #{ x: x }
+ ",
+ )?;
+
+ let data = format!("{r:?}");
+
+ let encoded = rmp_serde::to_vec(&r).unwrap();
+ let decoded: Dynamic = rmp_serde::from_slice(&encoded).unwrap();
+
+ assert_eq!(format!("{decoded:?}"), data);
+
+ Ok(())
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_json_borrowed_string() {
+ let value = json!({ "a": "b" });
+ println!("value: {value:?}");
+
+ let result: Dynamic = serde_json::from_value(value.clone()).unwrap();
+ println!("result: {result:?}");
+
+ let value2 = serde_json::to_value(&result).unwrap();
+ println!("value2: {value2:?}");
+
+ assert_eq!(value, value2);
+}
+
+#[test]
+#[cfg(not(feature = "no_object"))]
+fn test_serde_scope() {
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+ struct TestStruct {
+ foo: Option<char>,
+ }
+
+ let mut scope = Scope::new();
+ scope.push("x", 42 as INT);
+ scope.push_constant("y", true);
+ scope.push("z", TestStruct { foo: None });
+
+ let json = serde_json::to_string(&scope).unwrap();
+
+ assert_eq!(
+ json,
+ r#"[{"name":"x","value":42},{"name":"y","value":true,"is_constant":true},{"name":"z","value":"serde::test_serde_scope::TestStruct"}]"#
+ );
+
+ scope = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(scope.len(), 3);
+ assert_eq!(scope.get_value::<INT>("x").unwrap(), 42);
+ assert!(scope.get_value::<bool>("y").unwrap());
+ assert_eq!(
+ scope.get_value::<String>("z").unwrap(),
+ "serde::test_serde_scope::TestStruct"
+ );
+}
diff --git a/rhai/tests/side_effects.rs b/rhai/tests/side_effects.rs
new file mode 100644
index 0000000..72afc71
--- /dev/null
+++ b/rhai/tests/side_effects.rs
@@ -0,0 +1,82 @@
+///! This test simulates an external command object that is driven by a script.
+use rhai::{Engine, EvalAltResult, Scope, INT};
+use std::sync::{Arc, Mutex, RwLock};
+
+/// Simulate a command object.
+struct Command {
+ /// Simulate an external state.
+ state: INT,
+}
+
+impl Command {
+ /// Do some action.
+ pub fn action(&mut self, val: INT) {
+ self.state = val;
+ }
+ /// Get current value.
+ pub fn get(&self) -> INT {
+ self.state
+ }
+}
+
+#[allow(clippy::upper_case_acronyms)]
+type API = Arc<Mutex<Command>>;
+
+#[cfg(not(feature = "no_object"))]
+#[test]
+fn test_side_effects_command() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+ let mut scope = Scope::new();
+
+ // Create the command object with initial state, handled by an `Arc`.
+ let command = Arc::new(Mutex::new(Command { state: 12 }));
+ assert_eq!(command.lock().unwrap().get(), 12);
+
+ // Create the API object.
+ let api = command.clone(); // Notice this clones the `Arc` only
+
+ // Make the API object a singleton in the script environment.
+ scope.push_constant("Command", api);
+
+ // Register type.
+ engine.register_type_with_name::<API>("CommandType");
+ engine.register_fn("action", |api: &mut API, x: INT| {
+ let mut command = api.lock().unwrap();
+ let val = command.get();
+ command.action(val + x);
+ });
+ engine.register_get("value", |command: &mut API| command.lock().unwrap().get());
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "
+ // Drive the command object via the wrapper
+ Command.action(30);
+ Command.value
+ "
+ )?,
+ 42
+ );
+
+ // Make sure the actions are properly performed
+ assert_eq!(command.lock().unwrap().get(), 42);
+
+ Ok(())
+}
+
+#[test]
+fn test_side_effects_print() -> Result<(), Box<EvalAltResult>> {
+ let result = Arc::new(RwLock::new(String::new()));
+
+ let mut engine = Engine::new();
+
+ // Override action of 'print' function
+ let logger = result.clone();
+ engine.on_print(move |s| logger.write().unwrap().push_str(s));
+
+ engine.run("print(40 + 2);")?;
+
+ assert_eq!(*result.read().unwrap(), "42");
+ Ok(())
+}
diff --git a/rhai/tests/stack.rs b/rhai/tests/stack.rs
new file mode 100644
index 0000000..49e2822
--- /dev/null
+++ b/rhai/tests/stack.rs
@@ -0,0 +1,125 @@
+#![cfg(not(feature = "unchecked"))]
+use rhai::{Engine, EvalAltResult, ParseErrorType, INT};
+
+#[test]
+#[cfg(not(feature = "no_function"))]
+fn test_stack_overflow_fn_calls() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ fn foo(n) { if n <= 1 { 0 } else { n + foo(n-1) } }
+ foo(6)
+ ",
+ )?,
+ 20
+ );
+
+ let max = engine.max_call_levels();
+
+ #[cfg(not(feature = "unchecked"))]
+ assert!(matches!(
+ *engine
+ .run(&format!(
+ "
+ fn foo(n) {{ if n == 0 {{ 0 }} else {{ n + foo(n-1) }} }}
+ foo({})
+ ",
+ max + 1
+ ))
+ .unwrap_err(),
+ EvalAltResult::ErrorStackOverflow(..)
+ ));
+
+ Ok(())
+}
+
+#[test]
+fn test_stack_overflow_parsing() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ assert_eq!(
+ *engine.compile(
+ "
+ let a = (1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+1))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
+ "
+ ).unwrap_err().0,
+ ParseErrorType::ExprTooDeep
+ );
+
+ engine.compile("1 + 2")?;
+
+ #[cfg(debug_assertions)]
+ engine.compile(
+ "
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1
+ ",
+ )?;
+
+ #[cfg(debug_assertions)]
+ assert_eq!(
+ *engine
+ .compile(
+ "
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2
+ "
+ )
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::ExprTooDeep
+ );
+
+ engine.set_max_expr_depths(
+ 100,
+ #[cfg(not(feature = "no_function"))]
+ 6,
+ );
+
+ engine.compile(
+ "
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 0 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9
+ ",
+ )?;
+
+ assert_eq!(
+ *engine
+ .compile(
+ "
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0
+ "
+ )
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::ExprTooDeep
+ );
+
+ #[cfg(not(feature = "no_function"))]
+ engine.compile("fn abc(x) { x + 1 }")?;
+
+ Ok(())
+}
diff --git a/rhai/tests/string.rs b/rhai/tests/string.rs
new file mode 100644
index 0000000..6b150b9
--- /dev/null
+++ b/rhai/tests/string.rs
@@ -0,0 +1,419 @@
+use rhai::{Engine, EvalAltResult, ImmutableString, Scope, INT};
+
+#[test]
+fn test_string() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<String>(r#""Test string: \u2764""#)?,
+ "Test string: ❤"
+ );
+ assert_eq!(
+ engine.eval::<String>(r#""Test string: ""\u2764""""#)?,
+ r#"Test string: "❤""#
+ );
+ assert_eq!(
+ engine.eval::<String>("\"Test\rstring: \\u2764\"")?,
+ "Test\rstring: ❤"
+ );
+ assert_eq!(
+ engine.eval::<String>(" \"Test string: \\u2764\\\n hello, world!\"")?,
+ if cfg!(not(feature = "no_position")) {
+ "Test string: ❤ hello, world!"
+ } else {
+ "Test string: ❤ hello, world!"
+ }
+ );
+ assert_eq!(
+ engine.eval::<String>(" `Test string: \\u2764\nhello,\\nworld!`")?,
+ "Test string: \\u2764\nhello,\\nworld!"
+ );
+ assert_eq!(
+ engine.eval::<String>(r#" `Test string: \\u2764\n``hello``,\\n"world"!`"#)?,
+ r#"Test string: \\u2764\n`hello`,\\n"world"!"#
+ );
+ assert_eq!(
+ engine.eval::<String>(" `\nTest string: \\u2764\nhello,\\nworld!`")?,
+ "Test string: \\u2764\nhello,\\nworld!"
+ );
+ assert_eq!(
+ engine.eval::<String>(" `\r\nTest string: \\u2764\nhello,\\nworld!`")?,
+ "Test string: \\u2764\nhello,\\nworld!"
+ );
+ assert_eq!(
+ engine.eval::<String>(r#""Test string: \x58""#)?,
+ "Test string: X"
+ );
+ assert_eq!(engine.eval::<String>(r#""\"hello\"""#)?, r#""hello""#);
+
+ assert_eq!(engine.eval::<String>(r#""foo" + "bar""#)?, "foobar");
+
+ assert!(engine.eval::<bool>(r#"let y = "hello, world!"; "world" in y"#)?);
+ assert!(engine.eval::<bool>(r#"let y = "hello, world!"; 'w' in y"#)?);
+ assert!(!engine.eval::<bool>(r#"let y = "hello, world!"; "hey" in y"#)?);
+
+ assert_eq!(engine.eval::<String>(r#""foo" + 123"#)?, "foo123");
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(engine.eval::<String>("to_string(42)")?, "42");
+
+ #[cfg(not(feature = "no_index"))]
+ {
+ assert_eq!(engine.eval::<char>(r#"let y = "hello"; y[1]"#)?, 'e');
+ assert_eq!(engine.eval::<char>(r#"let y = "hello"; y[-1]"#)?, 'o');
+ assert_eq!(engine.eval::<char>(r#"let y = "hello"; y[-4]"#)?, 'e');
+ }
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(engine.eval::<INT>(r#"let y = "hello"; y.len"#)?, 5);
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<INT>(r#"let y = "hello"; y.clear(); y.len"#)?,
+ 0
+ );
+
+ assert_eq!(engine.eval::<INT>(r#"let y = "hello"; len(y)"#)?, 5);
+
+ #[cfg(not(feature = "no_object"))]
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(engine.eval::<char>(r#"let y = "hello"; y[y.len-1]"#)?, 'o');
+
+ #[cfg(not(feature = "no_float"))]
+ assert_eq!(engine.eval::<String>(r#""foo" + 123.4556"#)?, "foo123.4556");
+
+ Ok(())
+}
+
+#[test]
+fn test_string_dynamic() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+ scope.push("x", "foo");
+ scope.push("y", "foo");
+ scope.push("z", "foo");
+
+ assert!(engine.eval_with_scope::<bool>(&mut scope, r#"x == "foo""#)?);
+ assert!(engine.eval_with_scope::<bool>(&mut scope, r#"y == "foo""#)?);
+ assert!(engine.eval_with_scope::<bool>(&mut scope, r#"z == "foo""#)?);
+
+ Ok(())
+}
+
+#[test]
+fn test_string_mut() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_fn("foo", |s: &str| s.len() as INT);
+ engine.register_fn("bar", |s: String| s.len() as INT);
+ engine.register_fn("baz", |s: &mut String| s.len());
+
+ assert_eq!(engine.eval::<char>(r#"pop("hello")"#)?, 'o');
+ assert_eq!(engine.eval::<String>(r#"pop("hello", 3)"#)?, "llo");
+ assert_eq!(engine.eval::<String>(r#"pop("hello", 10)"#)?, "hello");
+ assert_eq!(engine.eval::<String>(r#"pop("hello", -42)"#)?, "");
+
+ assert_eq!(engine.eval::<INT>(r#"foo("hello")"#)?, 5);
+ assert_eq!(engine.eval::<INT>(r#"bar("hello")"#)?, 5);
+ assert!(
+ matches!(*engine.eval::<INT>(r#"baz("hello")"#).unwrap_err(),
+ EvalAltResult::ErrorFunctionNotFound(f, ..) if f == "baz (&str | ImmutableString | String)"
+ )
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_object"))]
+#[test]
+fn test_string_substring() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<String>(r#"let x = "hello! \u2764\u2764\u2764"; x.sub_string(-2, 2)"#)?,
+ "❤❤"
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.sub_string(1, 5)"#
+ )?,
+ "❤❤ he"
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.sub_string(1)"#
+ )?,
+ "❤❤ hello! ❤❤❤"
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.sub_string(99)"#
+ )?,
+ ""
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.sub_string(1, -1)"#
+ )?,
+ ""
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.sub_string(1, 999)"#
+ )?,
+ "❤❤ hello! ❤❤❤"
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.crop(1, -1); x"#
+ )?,
+ ""
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.crop(4, 6); x"#
+ )?,
+ "hello!"
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.crop(1, 999); x"#
+ )?,
+ "❤❤ hello! ❤❤❤"
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x -= 'l'; x"#
+ )?,
+ "❤❤❤ heo! ❤❤❤"
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x -= "\u2764\u2764"; x"#
+ )?,
+ "❤ hello! ❤"
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.index_of('\u2764')"#
+ )?,
+ 0
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.index_of('\u2764', 5)"#
+ )?,
+ 11
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.index_of('\u2764', -6)"#
+ )?,
+ 11
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.index_of('\u2764', 999)"#
+ )?,
+ -1
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.index_of('x')"#
+ )?,
+ -1
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_object"))]
+#[test]
+fn test_string_format() -> Result<(), Box<EvalAltResult>> {
+ #[derive(Debug, Clone)]
+ struct TestStruct {
+ field: i64,
+ }
+
+ let mut engine = Engine::new();
+
+ engine
+ .register_type_with_name::<TestStruct>("TestStruct")
+ .register_fn("new_ts", || TestStruct { field: 42 })
+ .register_fn("to_string", |ts: TestStruct| format!("TS={}", ts.field))
+ .register_fn("to_debug", |ts: TestStruct| {
+ format!("!!!TS={}!!!", ts.field)
+ });
+
+ assert_eq!(
+ engine.eval::<String>(r#"let x = new_ts(); "foo" + x"#)?,
+ "fooTS=42"
+ );
+ assert_eq!(
+ engine.eval::<String>(r#"let x = new_ts(); x + "foo""#)?,
+ "TS=42foo"
+ );
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ engine.eval::<String>(r#"let x = [new_ts()]; "foo" + x"#)?,
+ "foo[!!!TS=42!!!]"
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_string_fn() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.register_fn("set_to_x", |ch: &mut char| *ch = 'X');
+
+ #[cfg(not(feature = "no_index"))]
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<String>(r#"let x="foo"; x[0].set_to_x(); x"#)?,
+ "Xoo"
+ );
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ engine.eval::<String>(r#"let x="foo"; set_to_x(x[0]); x"#)?,
+ "foo"
+ );
+
+ engine
+ .register_fn("foo1", |s: &str| s.len() as INT)
+ .register_fn("foo2", |s: ImmutableString| s.len() as INT)
+ .register_fn("foo3", |s: String| s.len() as INT)
+ .register_fn("foo4", |s: &mut ImmutableString| s.len() as INT);
+
+ assert_eq!(engine.eval::<INT>(r#"foo1("hello")"#)?, 5);
+ assert_eq!(engine.eval::<INT>(r#"foo2("hello")"#)?, 5);
+ assert_eq!(engine.eval::<INT>(r#"foo3("hello")"#)?, 5);
+ assert_eq!(engine.eval::<INT>(r#"foo4("hello")"#)?, 5);
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_object"))]
+#[cfg(not(feature = "no_index"))]
+#[test]
+fn test_string_split() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.split(' ').len"#
+ )?,
+ 3
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.split("hello").len"#
+ )?,
+ 2
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_string_interpolated() -> Result<(), Box<EvalAltResult>> {
+ // Make sure strings interpolation works even under raw
+ let engine = Engine::new_raw();
+
+ assert_eq!(engine.eval::<String>("`${}`")?, "");
+
+ assert_eq!(
+ engine.eval::<String>(
+ "
+ let x = 40;
+ `hello ${x+2} worlds!`
+ "
+ )?,
+ "hello 42 worlds!"
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"
+ let x = 40;
+ "hello ${x+2} worlds!"
+ "#
+ )?,
+ "hello ${x+2} worlds!"
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ "
+ const x = 42;
+ `hello ${x} worlds!`
+ "
+ )?,
+ "hello 42 worlds!"
+ );
+
+ assert_eq!(engine.eval::<String>("`hello ${}world!`")?, "hello world!");
+
+ assert_eq!(
+ engine.eval::<String>(
+ "
+ const x = 42;
+ `${x} worlds!`
+ "
+ )?,
+ "42 worlds!"
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ "
+ const x = 42;
+ `hello ${x}`
+ "
+ )?,
+ "hello 42"
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ "
+ const x = 20;
+ `hello ${let y = x + 1; `${y * 2}`} worlds!`
+ "
+ )?,
+ "hello 42 worlds!"
+ );
+
+ assert_eq!(
+ engine.eval::<String>(
+ r#"
+ let x = 42;
+ let y = 123;
+
+ `
+Undeniable logic:
+1) Hello, ${let w = `${x} world`; if x > 1 { w += "s" } w}!
+2) If ${y} > ${x} then it is ${y > x}!
+`
+ "#
+ )?,
+ "Undeniable logic:\n1) Hello, 42 worlds!\n2) If 123 > 42 then it is true!\n",
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/switch.rs b/rhai/tests/switch.rs
new file mode 100644
index 0000000..67569b5
--- /dev/null
+++ b/rhai/tests/switch.rs
@@ -0,0 +1,354 @@
+use rhai::{Engine, EvalAltResult, ParseErrorType, Scope, INT};
+
+#[test]
+fn test_switch() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+ scope.push("x", 42 as INT);
+
+ assert_eq!(
+ engine.eval::<char>("switch 2 { 1 => (), 2 => 'a', 42 => true }")?,
+ 'a'
+ );
+ engine.run("switch 3 { 1 => (), 2 => 'a', 42 => true }")?;
+ assert_eq!(
+ engine.eval::<INT>("switch 3 { 1 => (), 2 => 'a', 42 => true, _ => 123 }")?,
+ 123
+ );
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "switch 2 { 1 => (), 2 if x < 40 => 'a', 42 => true, _ => 123 }"
+ )?,
+ 123
+ );
+ assert_eq!(
+ engine.eval_with_scope::<char>(
+ &mut scope,
+ "switch 2 { 1 => (), 2 if x > 40 => 'a', 42 => true, _ => 123 }"
+ )?,
+ 'a'
+ );
+ assert!(
+ engine.eval_with_scope::<bool>(&mut scope, "switch x { 1 => (), 2 => 'a', 42 => true }")?
+ );
+ assert!(
+ engine.eval_with_scope::<bool>(&mut scope, "switch x { 1 => (), 2 => 'a', _ => true }")?
+ );
+ let _: () = engine.eval_with_scope::<()>(&mut scope, "switch x { 1 => 123, 2 => 'a' }")?;
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "switch x { 1 | 2 | 3 | 5..50 | 'x' | true => 123, 'z' => 'a' }"
+ )?,
+ 123
+ );
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope, "switch x { 424242 => 123, _ => 42 }")?,
+ 42
+ );
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "switch x { 1 => 123, 42 => { x / 2 }, _ => 999 }"
+ )?,
+ 21
+ );
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "
+ let y = [1, 2, 3];
+
+ switch y {
+ 42 => 1,
+ true => 2,
+ [1, 2, 3] => 3,
+ _ => 9
+ }
+ "
+ )?,
+ 3
+ );
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "
+ let y = #{a:1, b:true, c:'x'};
+
+ switch y {
+ 42 => 1,
+ true => 2,
+ #{b:true, c:'x', a:1} => 3,
+ _ => 9
+ }
+ "
+ )?,
+ 3
+ );
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope, "switch 42 { 42 => 123, 42 => 999 }")?,
+ 123
+ );
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope, "switch x { 42 => 123, 42 => 999 }")?,
+ 123
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_switch_errors() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!(matches!(
+ engine
+ .compile("switch x { _ => 123, 1 => 42 }")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::WrongSwitchDefaultCase
+ ));
+
+ Ok(())
+}
+
+#[test]
+fn test_switch_condition() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+ scope.push("x", 42 as INT);
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "
+ switch x / 2 {
+ 21 if x > 40 => 1,
+ 0 if x < 100 => 2,
+ 1 => 3,
+ _ => 9
+ }
+ "
+ )?,
+ 1
+ );
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "
+ switch x / 2 {
+ 21 if x < 40 => 1,
+ 0 if x < 100 => 2,
+ 1 => 3,
+ _ => 9
+ }
+ "
+ )?,
+ 9
+ );
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "
+ switch x {
+ 42 if x < 40 => 1,
+ 42 if x > 40 => 7,
+ 0 if x < 100 => 2,
+ 1 => 3,
+ 42 if x == 10 => 10,
+ _ => 9
+ }
+ "
+ )?,
+ 7
+ );
+
+ assert!(matches!(
+ engine
+ .compile("switch x { 1 => 123, _ if true => 42 }")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::WrongSwitchCaseCondition
+ ));
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_index"))]
+#[cfg(not(feature = "no_object"))]
+mod test_switch_enum {
+ use super::*;
+ use rhai::Array;
+ #[derive(Debug, Clone)]
+ #[allow(dead_code)]
+ enum MyEnum {
+ Foo,
+ Bar(INT),
+ Baz(String, bool),
+ }
+
+ impl MyEnum {
+ fn get_enum_data(&mut self) -> Array {
+ match self {
+ Self::Foo => vec!["Foo".into()] as Array,
+ Self::Bar(num) => vec!["Bar".into(), (*num).into()] as Array,
+ Self::Baz(name, option) => {
+ vec!["Baz".into(), name.clone().into(), (*option).into()] as Array
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn test_switch_enum() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine
+ .register_type_with_name::<MyEnum>("MyEnum")
+ .register_get("get_data", MyEnum::get_enum_data);
+
+ let mut scope = Scope::new();
+ scope.push("x", MyEnum::Baz("hello".to_string(), true));
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ r#"
+ switch x.get_data {
+ ["Foo"] => 1,
+ ["Bar", 42] => 2,
+ ["Bar", 123] => 3,
+ ["Baz", "hello", false] => 4,
+ ["Baz", "hello", true] => 5,
+ _ => 9
+ }
+ "#
+ )?,
+ 5
+ );
+
+ Ok(())
+ }
+}
+
+#[test]
+fn test_switch_ranges() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+ scope.push("x", 42 as INT);
+
+ assert_eq!(
+ engine.eval_with_scope::<char>(
+ &mut scope,
+ "switch x { 10..20 => (), 20..=42 => 'a', 25..45 => 'z', 30..100 => true }"
+ )?,
+ 'a'
+ );
+ assert_eq!(
+ engine.eval_with_scope::<char>(
+ &mut scope,
+ "switch x { 10..20 => (), 20..=42 if x < 40 => 'a', 25..45 => 'z', 30..100 => true }"
+ )?,
+ 'z'
+ );
+ assert_eq!(
+ engine.eval_with_scope::<char>(
+ &mut scope,
+ "switch x { 42 => 'x', 10..20 => (), 20..=42 => 'a', 25..45 => 'z', 30..100 => true, 'w' => true }"
+ )?,
+ 'x'
+ );
+ assert!(matches!(
+ engine.compile(
+ "switch x { 10..20 => (), 20..=42 => 'a', 25..45 => 'z', 42 => 'x', 30..100 => true }"
+ ).unwrap_err().err_type(),
+ ParseErrorType::WrongSwitchIntegerCase
+ ));
+ #[cfg(not(feature = "no_float"))]
+ assert!(matches!(
+ engine.compile(
+ "switch x { 10..20 => (), 20..=42 => 'a', 25..45 => 'z', 42.0 => 'x', 30..100 => true }"
+ ).unwrap_err().err_type(),
+ ParseErrorType::WrongSwitchIntegerCase
+ ));
+ assert_eq!(
+ engine.eval_with_scope::<char>(
+ &mut scope,
+ "
+ switch 5 {
+ 'a' => true,
+ 0..10 if x+2==1+2 => print(40+2),
+ _ => 'x'
+ }
+ "
+ )?,
+ 'x'
+ );
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "
+ switch 5 {
+ 'a' => true,
+ 0..10 => 123,
+ 2..12 => 'z',
+ _ => 'x'
+ }
+ "
+ )?,
+ 123
+ );
+ assert_eq!(
+ engine.eval_with_scope::<INT>(
+ &mut scope,
+ "
+ switch 5 {
+ 'a' => true,
+ 4 | 5 | 6 => 42,
+ 0..10 => 123,
+ 2..12 => 'z',
+ _ => 'x'
+ }
+ "
+ )?,
+ 42
+ );
+ assert_eq!(
+ engine.eval_with_scope::<char>(
+ &mut scope,
+ "
+ switch 5 {
+ 'a' => true,
+ 2..12 => 'z',
+ 0..10 if x+2==1+2 => print(40+2),
+ _ => 'x'
+ }
+ "
+ )?,
+ 'z'
+ );
+ assert_eq!(
+ engine.eval_with_scope::<char>(
+ &mut scope,
+ "
+ switch 5 {
+ 'a' => true,
+ 0..10 if x+2==1+2 => print(40+2),
+ 2..12 => 'z',
+ _ => 'x'
+ }
+ "
+ )?,
+ 'z'
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/throw.rs b/rhai/tests/throw.rs
new file mode 100644
index 0000000..b83dbce
--- /dev/null
+++ b/rhai/tests/throw.rs
@@ -0,0 +1,103 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_throw() {
+ let engine = Engine::new();
+
+ assert!(matches!(
+ *engine.run("if true { throw 42 }").expect_err("expects error"),
+ EvalAltResult::ErrorRuntime(s, ..) if s.as_int().unwrap() == 42
+ ));
+
+ assert!(matches!(
+ *engine.run(r#"throw"#).expect_err("expects error"),
+ EvalAltResult::ErrorRuntime(s, ..) if s.is_unit()
+ ));
+}
+
+#[test]
+fn test_try_catch() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>("try { throw 42; } catch (x) { return x; }")?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>("try { throw 42; } catch { return 123; }")?,
+ 123
+ );
+
+ #[cfg(not(feature = "unchecked"))]
+ assert_eq!(
+ engine.eval::<INT>("let x = 42; try { let y = 123; print(x/0); } catch { x = 0 } x")?,
+ 0
+ );
+
+ #[cfg(not(feature = "no_function"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ fn foo(x) { try { throw 42; } catch (x) { return x; } }
+ foo(0)
+ "
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let err = 123;
+ let x = 0;
+ try { throw 42; } catch(err) { return err; }
+ "
+ )?,
+ 42
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let err = 123;
+ let x = 0;
+ try { throw 42; } catch(err) { print(err); }
+ err
+ "
+ )?,
+ 123
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let foo = 123;
+ let x = 0;
+ try { throw 42; } catch(err) { return foo; }
+ "
+ )?,
+ 123
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let foo = 123;
+ let x = 0;
+ try { throw 42; } catch(err) { return err; }
+ "
+ )?,
+ 42
+ );
+
+ #[cfg(not(feature = "unchecked"))]
+ assert!(matches!(
+ *engine
+ .run("try { 42/0; } catch { throw; }")
+ .expect_err("expects error"),
+ EvalAltResult::ErrorArithmetic(..)
+ ));
+
+ Ok(())
+}
diff --git a/rhai/tests/time.rs b/rhai/tests/time.rs
new file mode 100644
index 0000000..5434faa
--- /dev/null
+++ b/rhai/tests/time.rs
@@ -0,0 +1,108 @@
+#![cfg(not(feature = "no_time"))]
+
+use rhai::{Engine, EvalAltResult};
+
+#[cfg(not(feature = "no_float"))]
+use rhai::FLOAT;
+
+#[cfg(feature = "no_float")]
+use rhai::INT;
+
+#[test]
+fn test_timestamp() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<String>("type_of(timestamp())")?, "timestamp");
+
+ #[cfg(not(feature = "no_float"))]
+ assert!(
+ engine.eval::<FLOAT>(
+ r#"
+ let time = timestamp();
+ let x = 10_000;
+ while x > 0 { x -= 1; }
+ elapsed(time)
+ "#
+ )? < 10.0
+ );
+
+ #[cfg(feature = "no_float")]
+ assert!(
+ engine.eval::<INT>(
+ r#"
+ let time = timestamp();
+ let x = 10_000;
+ while x > 0 { x -= 1; }
+ elapsed(time)
+ "#
+ )? < 10
+ );
+
+ assert!(engine.eval::<bool>(
+ "
+ let time1 = timestamp();
+ for x in 0..10000 {}
+ let time2 = timestamp();
+ time1 <= time2
+ "
+ )?);
+
+ Ok(())
+}
+
+#[test]
+fn test_timestamp_op() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ #[cfg(not(feature = "no_float"))]
+ assert!(
+ (engine.eval::<FLOAT>(
+ r#"
+ let time1 = timestamp();
+ let time2 = time1 + 123.45;
+ time2 - time1
+ "#
+ )? - 123.45)
+ .abs()
+ < 0.001
+ );
+
+ #[cfg(not(feature = "no_float"))]
+ assert!(
+ (engine.eval::<FLOAT>(
+ r#"
+ let time1 = timestamp();
+ let time2 = time1 - 123.45;
+ time1 - time2
+ "#
+ )? - 123.45)
+ .abs()
+ < 0.001
+ );
+
+ #[cfg(feature = "no_float")]
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let time1 = timestamp();
+ let time2 = time1 + 42;
+ time2 - time1
+ "#
+ )?,
+ 42
+ );
+
+ #[cfg(feature = "no_float")]
+ assert_eq!(
+ engine.eval::<INT>(
+ r#"
+ let time1 = timestamp();
+ let time2 = time1 - 42;
+ time1 - time2
+ "#
+ )?,
+ 42
+ );
+
+ Ok(())
+}
diff --git a/rhai/tests/tokens.rs b/rhai/tests/tokens.rs
new file mode 100644
index 0000000..9a7b7d8
--- /dev/null
+++ b/rhai/tests/tokens.rs
@@ -0,0 +1,112 @@
+use rhai::{Engine, EvalAltResult, ParseErrorType, INT};
+
+#[test]
+fn test_tokens_disabled() {
+ let mut engine = Engine::new();
+
+ engine.disable_symbol("if"); // disable the 'if' keyword
+
+ assert!(matches!(
+ engine
+ .compile("let x = if true { 42 } else { 0 };")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::Reserved(err) if err == "if"
+ ));
+
+ engine.disable_symbol("+="); // disable the '+=' operator
+
+ assert_eq!(
+ *engine
+ .compile("let x = 40 + 2; x += 1;")
+ .unwrap_err()
+ .err_type(),
+ ParseErrorType::UnknownOperator("+=".to_string())
+ );
+
+ assert!(matches!(
+ engine.compile("let x = += 0;").unwrap_err().err_type(),
+ ParseErrorType::Reserved(err) if err == "+="
+ ));
+}
+
+#[cfg(not(feature = "no_custom_syntax"))]
+#[test]
+fn test_tokens_custom_operator_identifiers() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ // Register a custom operator called `foo` and give it
+ // a precedence of 160 (i.e. between +|- and *|/).
+ engine.register_custom_operator("foo", 160).unwrap();
+
+ // Register a binary function named `foo`
+ engine.register_fn("foo", |x: INT, y: INT| (x * y) - (x + y));
+
+ assert_eq!(
+ engine.eval_expression::<INT>("1 + 2 * 3 foo 4 - 5 / 6")?,
+ 15
+ );
+
+ #[cfg(not(feature = "no_function"))]
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ fn foo(x, y) { y - x }
+ 1 + 2 * 3 foo 4 - 5 / 6
+ "
+ )?,
+ -1
+ );
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_custom_syntax"))]
+#[test]
+fn test_tokens_custom_operator_symbol() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ // Register a custom operator `#` and give it
+ // a precedence of 160 (i.e. between +|- and *|/).
+ engine.register_custom_operator("#", 160).unwrap();
+
+ // Register a binary function named `#`
+ engine.register_fn("#", |x: INT, y: INT| (x * y) - (x + y));
+
+ assert_eq!(engine.eval_expression::<INT>("1 + 2 * 3 # 4 - 5 / 6")?, 15);
+
+ // Register a custom operator named `=>`
+ assert!(engine.register_custom_operator("=>", 160).is_err());
+ engine.disable_symbol("=>");
+ engine.register_custom_operator("=>", 160).unwrap();
+ engine.register_fn("=>", |x: INT, y: INT| (x * y) - (x + y));
+ assert_eq!(engine.eval_expression::<INT>("1 + 2 * 3 => 4 - 5 / 6")?, 15);
+
+ Ok(())
+}
+
+#[test]
+fn test_tokens_unicode_xid_ident() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let result = engine.eval::<INT>(
+ "
+ fn すべての答え() { 42 }
+ すべての答え()
+ ",
+ );
+ #[cfg(feature = "unicode-xid-ident")]
+ assert_eq!(result?, 42);
+
+ #[cfg(not(feature = "unicode-xid-ident"))]
+ assert!(result.is_err());
+
+ let result = engine.eval::<INT>(
+ "
+ fn _1() { 1 }
+ _1()
+ ",
+ );
+ assert!(result.is_err());
+
+ Ok(())
+}
diff --git a/rhai/tests/types.rs b/rhai/tests/types.rs
new file mode 100644
index 0000000..76bbfe9
--- /dev/null
+++ b/rhai/tests/types.rs
@@ -0,0 +1,60 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_type_of() -> Result<(), Box<EvalAltResult>> {
+ #[allow(dead_code)]
+ #[derive(Clone)]
+ struct TestStruct {
+ x: INT,
+ }
+
+ let mut engine = Engine::new();
+
+ #[cfg(not(feature = "only_i32"))]
+ assert_eq!(engine.eval::<String>("type_of(60 + 5)")?, "i64");
+
+ #[cfg(feature = "only_i32")]
+ assert_eq!(engine.eval::<String>("type_of(60 + 5)")?, "i32");
+
+ #[cfg(not(feature = "no_float"))]
+ {
+ #[cfg(not(feature = "f32_float"))]
+ assert_eq!(engine.eval::<String>("type_of(1.0 + 2.0)")?, "f64");
+
+ #[cfg(feature = "f32_float")]
+ assert_eq!(engine.eval::<String>("type_of(1.0 + 2.0)")?, "f32");
+ }
+
+ #[cfg(not(feature = "no_index"))]
+ assert_eq!(
+ engine.eval::<String>(r#"type_of([true, 2, "hello"])"#)?,
+ "array"
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(
+ engine.eval::<String>(r#"type_of(#{a:true, "":2, "z":"hello"})"#)?,
+ "map"
+ );
+
+ #[cfg(not(feature = "no_object"))]
+ {
+ engine.register_type_with_name::<TestStruct>("Hello");
+ engine.register_fn("new_ts", || TestStruct { x: 1 });
+
+ assert_eq!(engine.eval::<String>("type_of(new_ts())")?, "Hello");
+ }
+
+ assert_eq!(engine.eval::<String>(r#"type_of("hello")"#)?, "string");
+
+ #[cfg(not(feature = "no_object"))]
+ assert_eq!(engine.eval::<String>(r#""hello".type_of()"#)?, "string");
+
+ #[cfg(not(feature = "only_i32"))]
+ assert_eq!(engine.eval::<String>("let x = 123; type_of(x)")?, "i64");
+
+ #[cfg(feature = "only_i32")]
+ assert_eq!(engine.eval::<String>("let x = 123; type_of(x)")?, "i32");
+
+ Ok(())
+}
diff --git a/rhai/tests/unary_after_binary.rs b/rhai/tests/unary_after_binary.rs
new file mode 100644
index 0000000..d2366be
--- /dev/null
+++ b/rhai/tests/unary_after_binary.rs
@@ -0,0 +1,17 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+// TODO also add test case for unary after compound
+// Hah, turns out unary + has a good use after all!
+fn test_unary_after_binary() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("10 % +4")?, 2);
+ assert_eq!(engine.eval::<INT>("10 << +4")?, 160);
+ assert_eq!(engine.eval::<INT>("10 >> +4")?, 0);
+ assert_eq!(engine.eval::<INT>("10 & +4")?, 0);
+ assert_eq!(engine.eval::<INT>("10 | +4")?, 14);
+ assert_eq!(engine.eval::<INT>("10 ^ +4")?, 14);
+
+ Ok(())
+}
diff --git a/rhai/tests/unary_minus.rs b/rhai/tests/unary_minus.rs
new file mode 100644
index 0000000..5aebe8d
--- /dev/null
+++ b/rhai/tests/unary_minus.rs
@@ -0,0 +1,15 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_unary_minus() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(engine.eval::<INT>("let x = -5; x")?, -5);
+
+ #[cfg(not(feature = "no_function"))]
+ assert_eq!(engine.eval::<INT>("fn neg(x) { -x } neg(5)")?, -5);
+
+ assert_eq!(engine.eval::<INT>("5 - -+ + + - -+-5")?, 0);
+
+ Ok(())
+}
diff --git a/rhai/tests/unit.rs b/rhai/tests/unit.rs
new file mode 100644
index 0000000..04c078b
--- /dev/null
+++ b/rhai/tests/unit.rs
@@ -0,0 +1,22 @@
+use rhai::{Engine, EvalAltResult};
+
+#[test]
+fn test_unit() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ engine.run("let x = (); x")?;
+ Ok(())
+}
+
+#[test]
+fn test_unit_eq() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ assert!(engine.eval::<bool>("let x = (); let y = (); x == y")?);
+ Ok(())
+}
+
+#[test]
+fn test_unit_with_spaces() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let _ = engine.run("let x = ( ); x").unwrap_err();
+ Ok(())
+}
diff --git a/rhai/tests/var_scope.rs b/rhai/tests/var_scope.rs
new file mode 100644
index 0000000..776bf21
--- /dev/null
+++ b/rhai/tests/var_scope.rs
@@ -0,0 +1,286 @@
+use rhai::{Dynamic, Engine, EvalAltResult, Module, ParseErrorType, Position, Scope, INT};
+
+#[test]
+fn test_var_scope() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+
+ engine.run_with_scope(&mut scope, "let x = 4 + 5")?;
+ assert_eq!(engine.eval_with_scope::<INT>(&mut scope, "x")?, 9);
+ engine.run_with_scope(&mut scope, "x += 1; x += 2;")?;
+ assert_eq!(engine.eval_with_scope::<INT>(&mut scope, "x")?, 12);
+
+ scope.set_value("x", 42 as INT);
+ assert_eq!(engine.eval_with_scope::<INT>(&mut scope, "x")?, 42);
+
+ engine.run_with_scope(&mut scope, "{ let x = 3 }")?;
+ assert_eq!(engine.eval_with_scope::<INT>(&mut scope, "x")?, 42);
+
+ #[cfg(not(feature = "no_optimize"))]
+ if engine.optimization_level() != rhai::OptimizationLevel::None {
+ scope.clear();
+ engine.run_with_scope(&mut scope, "let x = 3; let x = 42; let x = 123;")?;
+ assert_eq!(scope.len(), 1);
+ assert_eq!(scope.get_value::<INT>("x").unwrap(), 123);
+
+ scope.clear();
+ engine.run_with_scope(
+ &mut scope,
+ "let x = 3; let y = 0; let x = 42; let y = 999; let x = 123;",
+ )?;
+ assert_eq!(scope.len(), 2);
+ assert_eq!(scope.get_value::<INT>("x").unwrap(), 123);
+ assert_eq!(scope.get_value::<INT>("y").unwrap(), 999);
+
+ scope.clear();
+ engine.run_with_scope(
+ &mut scope,
+ "const x = 3; let y = 0; let x = 42; let y = 999;",
+ )?;
+ assert_eq!(scope.len(), 2);
+ assert_eq!(scope.get_value::<INT>("x").unwrap(), 42);
+ assert_eq!(scope.get_value::<INT>("y").unwrap(), 999);
+ assert!(!scope.is_constant("x").unwrap());
+ assert!(!scope.is_constant("y").unwrap());
+
+ scope.clear();
+ engine.run_with_scope(
+ &mut scope,
+ "const x = 3; let y = 0; let x = 42; let y = 999; const x = 123;",
+ )?;
+ assert_eq!(scope.len(), 2);
+ assert_eq!(scope.get_value::<INT>("x").unwrap(), 123);
+ assert_eq!(scope.get_value::<INT>("y").unwrap(), 999);
+ assert!(scope.is_constant("x").unwrap());
+ assert!(!scope.is_constant("y").unwrap());
+
+ scope.clear();
+ engine.run_with_scope(
+ &mut scope,
+ "let x = 3; let y = 0; { let x = 42; let y = 999; } let x = 123;",
+ )?;
+
+ assert_eq!(scope.len(), 2);
+ assert_eq!(scope.get_value::<INT>("x").unwrap(), 123);
+ assert_eq!(scope.get_value::<INT>("y").unwrap(), 0);
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let sum = 0;
+ for x in 0..10 {
+ let x = 42;
+ sum += x;
+ }
+ sum
+ ",
+ )?,
+ 420
+ );
+ }
+
+ scope.clear();
+
+ scope.push("x", 42 as INT);
+ scope.push_constant("x", 42 as INT);
+
+ let scope2 = scope.clone();
+ let scope3 = scope.clone_visible();
+
+ assert_eq!(scope2.is_constant("x"), Some(true));
+ assert_eq!(scope3.is_constant("x"), Some(true));
+
+ Ok(())
+}
+
+#[cfg(not(feature = "no_module"))]
+#[test]
+fn test_var_scope_alias() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+ let mut scope = Scope::new();
+
+ scope.push("x", 42 as INT);
+ scope.set_alias("x", "a");
+ scope.set_alias("x", "b");
+ scope.set_alias("x", "y");
+ scope.push("x", 123 as INT);
+ scope.set_alias("x", "b");
+ scope.set_alias("x", "c");
+
+ let ast = engine.compile(
+ "
+ let x = 999;
+ export x as a;
+ export x as c;
+ let x = 0;
+ export x as z;
+ ",
+ )?;
+
+ let m = Module::eval_ast_as_new(scope, &ast, &engine)?;
+
+ assert_eq!(m.get_var_value::<INT>("a").unwrap(), 999);
+ assert_eq!(m.get_var_value::<INT>("b").unwrap(), 123);
+ assert_eq!(m.get_var_value::<INT>("c").unwrap(), 999);
+ assert_eq!(m.get_var_value::<INT>("y").unwrap(), 42);
+ assert_eq!(m.get_var_value::<INT>("z").unwrap(), 0);
+
+ Ok(())
+}
+
+#[test]
+fn test_var_is_def() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert!(engine.eval::<bool>(
+ r#"
+ let x = 42;
+ is_def_var("x")
+ "#
+ )?);
+ assert!(!engine.eval::<bool>(
+ r#"
+ let x = 42;
+ is_def_var("y")
+ "#
+ )?);
+ assert!(engine.eval::<bool>(
+ r#"
+ const x = 42;
+ is_def_var("x")
+ "#
+ )?);
+
+ Ok(())
+}
+
+#[test]
+fn test_scope_eval() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ // First create the state
+ let mut scope = Scope::new();
+
+ // Then push some initialized variables into the state
+ // NOTE: Remember the default numbers used by Rhai are INT and f64.
+ // Better stick to them or it gets hard to work with other variables in the script.
+ scope.push("y", 42 as INT);
+ scope.push("z", 999 as INT);
+
+ // First invocation
+ engine
+ .run_with_scope(&mut scope, " let x = 4 + 5 - y + z; y = 1;")
+ .expect("variables y and z should exist");
+
+ // Second invocation using the same state
+ let result = engine.eval_with_scope::<INT>(&mut scope, "x")?;
+
+ println!("result: {result}"); // should print 966
+
+ // Variable y is changed in the script
+ assert_eq!(
+ scope
+ .get_value::<INT>("y")
+ .expect("variable y should exist"),
+ 1
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_var_resolver() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ let mut scope = Scope::new();
+ scope.push("innocent", 1 as INT);
+ scope.push("chameleon", 123 as INT);
+ scope.push("DO_NOT_USE", 999 as INT);
+
+ #[cfg(not(feature = "no_closure"))]
+ let mut base = Dynamic::ONE.into_shared();
+ #[cfg(not(feature = "no_closure"))]
+ let shared = base.clone();
+
+ #[allow(deprecated)] // not deprecated but unstable
+ engine.on_var(move |name, _, context| {
+ match name {
+ "MYSTIC_NUMBER" => Ok(Some((42 as INT).into())),
+ #[cfg(not(feature = "no_closure"))]
+ "HELLO" => Ok(Some(shared.clone())),
+ // Override a variable - make it not found even if it exists!
+ "DO_NOT_USE" => {
+ Err(EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::NONE).into())
+ }
+ // Silently maps 'chameleon' into 'innocent'.
+ "chameleon" => context
+ .scope()
+ .get_value("innocent")
+ .map(Some)
+ .ok_or_else(|| {
+ EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::NONE).into()
+ }),
+ // Return Ok(None) to continue with the normal variable resolution process.
+ _ => Ok(None),
+ }
+ });
+
+ assert_eq!(
+ engine.eval_with_scope::<INT>(&mut scope, "MYSTIC_NUMBER")?,
+ 42
+ );
+
+ #[cfg(not(feature = "no_closure"))]
+ {
+ assert_eq!(engine.eval::<INT>("HELLO")?, 1);
+ *base.write_lock::<INT>().unwrap() = 42;
+ assert_eq!(engine.eval::<INT>("HELLO")?, 42);
+ engine.run("HELLO = 123")?;
+ assert_eq!(base.as_int().unwrap(), 123);
+ assert_eq!(engine.eval::<INT>("HELLO = HELLO + 1; HELLO")?, 124);
+ assert_eq!(engine.eval::<INT>("HELLO = HELLO * 2; HELLO")?, 248);
+ assert_eq!(base.as_int().unwrap(), 248);
+ }
+
+ assert_eq!(engine.eval_with_scope::<INT>(&mut scope, "chameleon")?, 1);
+ assert!(
+ matches!(*engine.eval_with_scope::<INT>(&mut scope, "DO_NOT_USE").unwrap_err(),
+ EvalAltResult::ErrorVariableNotFound(n, ..) if n == "DO_NOT_USE")
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_var_def_filter() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ let ast = engine.compile("let x = 42;")?;
+ engine.run_ast(&ast)?;
+
+ #[allow(deprecated)] // not deprecated but unstable
+ engine.on_def_var(|_, info, _| match (info.name, info.nesting_level) {
+ ("x", 0 | 1) => Ok(false),
+ _ => Ok(true),
+ });
+
+ assert_eq!(
+ engine.eval::<INT>("let y = 42; let y = 123; let z = y + 1; z")?,
+ 124
+ );
+
+ assert!(matches!(
+ engine.compile("let x = 42;").unwrap_err().err_type(),
+ ParseErrorType::ForbiddenVariable(s) if s == "x"
+ ));
+ assert!(matches!(
+ *engine.run_ast(&ast).expect_err("should err"),
+ EvalAltResult::ErrorForbiddenVariable(s, _) if s == "x"
+ ));
+ assert!(engine.run("const x = 42;").is_err());
+ assert!(engine.run("let y = 42; { let x = y + 1; }").is_err());
+ assert!(engine.run("let y = 42; { let x = y + 1; }").is_err());
+ engine.run("let y = 42; { let z = y + 1; { let x = z + 1; } }")?;
+
+ Ok(())
+}
diff --git a/rhai/tests/while_loop.rs b/rhai/tests/while_loop.rs
new file mode 100644
index 0000000..ebcdb8a
--- /dev/null
+++ b/rhai/tests/while_loop.rs
@@ -0,0 +1,100 @@
+use rhai::{Engine, EvalAltResult, INT};
+
+#[test]
+fn test_while() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 0;
+
+ while x < 10 {
+ x += 1;
+ if x > 5 { break; }
+ if x > 3 { continue; }
+ x += 3;
+ }
+
+ x
+ ",
+ )?,
+ 6
+ );
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 0;
+
+ while x < 10 {
+ x += 1;
+ if x > 5 { break x * 2; }
+ if x > 3 { continue; }
+ x += 3;
+ }
+ ",
+ )?,
+ 12
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_do() -> Result<(), Box<EvalAltResult>> {
+ let engine = Engine::new();
+
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 0;
+
+ do {
+ x += 1;
+ if x > 5 { break; }
+ if x > 3 { continue; }
+ x += 3;
+ } while x < 10;
+
+ x
+ ",
+ )?,
+ 6
+ );
+ assert_eq!(
+ engine.eval::<INT>(
+ "
+ let x = 0;
+
+ do {
+ x += 1;
+ if x > 5 { break x * 2; }
+ if x > 3 { continue; }
+ x += 3;
+ } while x < 10;
+ ",
+ )?,
+ 12
+ );
+
+ engine.run("do {} while false")?;
+
+ assert_eq!(engine.eval::<INT>("do { break 42; } while false")?, 42);
+
+ Ok(())
+}
+
+#[cfg(not(feature = "unchecked"))]
+#[test]
+fn test_infinite_loops() -> Result<(), Box<EvalAltResult>> {
+ let mut engine = Engine::new();
+
+ engine.set_max_operations(1024);
+
+ assert!(engine.run("loop {}").is_err());
+ assert!(engine.run("while true {}").is_err());
+ assert!(engine.run("do {} while true").is_err());
+
+ Ok(())
+}
diff --git a/rhai/tools/README.md b/rhai/tools/README.md
new file mode 100644
index 0000000..9b96f6c
--- /dev/null
+++ b/rhai/tools/README.md
@@ -0,0 +1,9 @@
+Build Tools
+===========
+
+This directory contains input files for various build tools required for building Rhai.
+
+| File | Build tool | Description |
+| -------------- | :---------: | ------------------------------------------------------------------ |
+| `keywords.txt` | GNU `gperf` | Input file for the tokenizer – keywords recognition. |
+| `reserved.txt` | GNU `gperf` | Input file for the tokenizer – reserved symbols recognition. |
diff --git a/rhai/tools/keywords.txt b/rhai/tools/keywords.txt
new file mode 100644
index 0000000..cc98a10
--- /dev/null
+++ b/rhai/tools/keywords.txt
@@ -0,0 +1,105 @@
+// This file holds a list of keywords/symbols for the Rhai language, with mapping to
+// an appropriate `Token` variant.
+//
+// Generate the output table via:
+// ```bash
+// gperf keywords.txt
+// ```
+//
+// Since GNU gperf does not produce Rust output, the ANSI-C output must be hand-edited and
+// manually spliced into `tokenizer.rs`.
+//
+// This includes:
+// * Rewrite the C hashing program (especially since it uses a `switch` statement with fall-through)
+// into equivalent Rust as the function `lookup_symbol_from_syntax`.
+// * Update the values for the `???_KEYWORD_???` constants.
+// * Copy the `asso_values` array into `KEYWORD_ASSOC_VALUES`.
+// * Copy the `wordlist` array into `KEYWORDS_LIST` with the following modifications:
+// - Remove the `#line` comments
+// - Change the entry wrapping `{ .. }` into tuples `( .. )`
+// - Put feature flags on the appropriate lines, and duplicating lines that maps to `Token::EOF`
+// for the opposite feature flags
+//
+%global-table
+%struct-type
+%omit-struct-type
+%define initializer-suffix ,Token::EOF
+struct keyword;
+%%
+{, Token::LeftBrace
+}, Token::RightBrace
+(, Token::LeftParen
+), Token::RightParen
+[, Token::LeftBracket
+], Token::RightBracket
+(), Token::Unit
++, Token::Plus
+-, Token::Minus
+*, Token::Multiply
+/, Token::Divide
+;, Token::SemiColon
+:, Token::Colon
+::, Token::DoubleColon
+=>, Token::DoubleArrow
+_, Token::Underscore
+",", Token::Comma
+., Token::Period
+?., Token::Elvis
+??, Token::DoubleQuestion
+?[, Token::QuestionBracket
+.., Token::ExclusiveRange
+..=, Token::InclusiveRange
+"#{", Token::MapStart
+=, Token::Equals
+true, Token::True
+false, Token::False
+let, Token::Let
+const, Token::Const
+if, Token::If
+else, Token::Else
+switch, Token::Switch
+do, Token::Do
+while, Token::While
+until, Token::Until
+loop, Token::Loop
+for, Token::For
+in, Token::In
+!in, Token::NotIn
+<, Token::LessThan
+>, Token::GreaterThan
+<=, Token::LessThanEqualsTo
+>=, Token::GreaterThanEqualsTo
+==, Token::EqualsTo
+!=, Token::NotEqualsTo
+!, Token::Bang
+|, Token::Pipe
+||, Token::Or
+&, Token::Ampersand
+&&, Token::And
+continue, Token::Continue
+break, Token::Break
+return, Token::Return
+throw, Token::Throw
+try, Token::Try
+catch, Token::Catch
++=, Token::PlusAssign
+-=, Token::MinusAssign
+*=, Token::MultiplyAssign
+/=, Token::DivideAssign
+<<=, Token::LeftShiftAssign
+>>=, Token::RightShiftAssign
+&=, Token::AndAssign
+|=, Token::OrAssign
+^=, Token::XOrAssign
+<<, Token::LeftShift
+>>, Token::RightShift
+^, Token::XOr
+"%", Token::Modulo
+"%=", Token::ModuloAssign
+**, Token::PowerOf
+**=, Token::PowerOfAssign
+fn, Token::Fn
+private, Token::Private
+import, Token::Import
+export, Token::Export
+as, Token::As
diff --git a/rhai/tools/reserved.txt b/rhai/tools/reserved.txt
new file mode 100644
index 0000000..4ae6d8d
--- /dev/null
+++ b/rhai/tools/reserved.txt
@@ -0,0 +1,108 @@
+// This file holds a list of reserved symbols for the Rhai language.
+//
+// The mapped attributes are:
+// - is this a reserved symbol? (bool)
+// - can this keyword be called normally as a function? (bool)
+// - can this keyword be called in method-call style? (bool)
+//
+// Generate the output table via:
+// ```bash
+// gperf reserved.txt
+// ```
+//
+// Since GNU gperf does not produce Rust output, the ANSI-C output must be hand-edited and
+// manually spliced into `tokenizer.rs`.
+//
+// This includes:
+// * Rewrite the C hashing program (especially since it uses a `switch` statement with fall-through)
+// into equivalent Rust as the function `is_reserved_keyword_or_symbol`.
+// * Update the values for the `???_RESERVED_???` constants.
+// * Copy the `asso_values` array into `RESERVED_ASSOC_VALUES`.
+// * Copy the `wordlist` array into `RESERVED_LIST` with the following modifications:
+// - Remove the `#line` comments
+// - Change the entry wrapping `{ .. }` into tuples `( .. )`
+// - Feature flags can be incorporated directly into the output via the `cfg!` macro
+//
+%global-table
+%struct-type
+%omit-struct-type
+%define initializer-suffix ,false,false,false
+struct reserved;
+%%
+# reserved under certain flags
+#
+?., cfg!(feature = no_object), false, false
+?[, cfg!(feature = no_index), false, false
+fn, cfg!(feature = no_function), false, false
+private, cfg!(feature = no_function), false, false
+import, cfg!(feature = no_module), false, false
+export, cfg!(feature = no_module), false, false
+as, cfg!(feature = no_module), false, false
+#
+# reserved symbols
+#
+===, true, false, false
+!==, true, false, false
+->, true, false, false
+<-, true, false, false
+?, true, false, false
+:=, true, false, false
+:;, true, false, false
+~, true, false, false
+!., true, false, false
+::<, true, false, false
+(*, true, false, false
+*), true, false, false
+"#", true, false, false
+"#!", true, false, false
+@, true, false, false
+$, true, false, false
+++, true, false, false
+--, true, false, false
+..., true, false, false
+<|, true, false, false
+|>, true, false, false
+#
+# reserved keywords
+#
+public, true, false, false
+protected, true, false, false
+super, true, false, false
+new, true, false, false
+use, true, false, false
+module, true, false, false
+package, true, false, false
+var, true, false, false
+static, true, false, false
+shared, true, false, false
+with, true, false, false
+is, true, false, false
+goto, true, false, false
+exit, true, false, false
+match, true, false, false
+case, true, false, false
+default, true, false, false
+void, true, false, false
+null, true, false, false
+nil, true, false, false
+spawn, true, false, false
+thread, true, false, false
+go, true, false, false
+sync, true, false, false
+async, true, false, false
+await, true, false, false
+yield, true, false, false
+#
+# keyword functions
+#
+print, true, true, false
+debug, true, true, false
+type_of, true, true, true
+eval, true, true, false
+Fn, true, true, false
+call, true, true, true
+curry, true, true, true
+this, true, false, false
+is_def_var, true, true, false
+is_def_fn, cfg!(not(feature = no_function)), true, false
+is_shared, cfg!(not(feature = no_closure)), true, true