├── .devcontainer └── devcontainer.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci-checks.yaml │ └── deploy.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── build.rs ├── build.sh ├── clippy.toml ├── enum-field-getter ├── .gitignore ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── hq-test.project.json ├── js ├── cast │ ├── float2string.ts │ ├── int2string.ts │ └── string2float.ts ├── looks │ ├── say_debug_float.ts │ ├── say_debug_int.ts │ ├── say_debug_string.ts │ ├── say_float.ts │ ├── say_int.ts │ ├── say_string.ts │ ├── think_debug_float.ts │ ├── think_debug_int.ts │ ├── think_debug_string.ts │ ├── think_float.ts │ ├── think_int.ts │ └── think_string.ts ├── operator │ ├── contains.ts │ ├── eq_string.ts │ ├── gt_string.ts │ └── lt_string.ts ├── sensing │ └── dayssince2000.ts ├── shared.ts └── wasm-js-string │ ├── concat.ts │ ├── length.ts │ └── substring.ts ├── opcodes.mjs ├── package.json ├── playground ├── App.vue ├── assets │ ├── base.css │ ├── logo.svg │ ├── main.css │ └── renderer.js ├── components │ ├── Loading.vue │ ├── ProjectFileInput.vue │ ├── ProjectIdPlayer.vue │ ├── ProjectInput.vue │ └── ProjectPlayer.vue ├── index.html ├── lib │ ├── imports.js │ ├── project-loader.js │ ├── project-runner.js │ └── settings.js ├── main.js ├── public │ ├── favicon.ico │ └── logo.png ├── router │ └── index.js ├── stores │ ├── debug.js │ └── projectfile.js └── views │ ├── 404.vue │ ├── AboutView.vue │ ├── HomeView.vue │ ├── ProjectFileView.vue │ ├── ProjectIdView.vue │ ├── Settings.vue │ └── TestProject.vue ├── rust-toolchain.toml ├── src ├── alloc.rs ├── error.rs ├── instructions.rs ├── instructions │ ├── control.rs │ ├── control │ │ ├── if_else.rs │ │ └── loop.rs │ ├── data.rs │ ├── data │ │ ├── setvariableto.rs │ │ ├── teevariable.rs │ │ └── variable.rs │ ├── hq.rs │ ├── hq │ │ ├── cast.rs │ │ ├── drop.rs │ │ ├── float.rs │ │ ├── integer.rs │ │ ├── text.rs │ │ └── yield.rs │ ├── input_switcher.rs │ ├── looks.rs │ ├── looks │ │ ├── say.rs │ │ └── think.rs │ ├── operator.rs │ ├── operator │ │ ├── add.rs │ │ ├── and.rs │ │ ├── contains.rs │ │ ├── divide.rs │ │ ├── equals.rs │ │ ├── gt.rs │ │ ├── join.rs │ │ ├── length.rs │ │ ├── letter_of.rs │ │ ├── lt.rs │ │ ├── multiply.rs │ │ ├── not.rs │ │ ├── or.rs │ │ └── subtract.rs │ ├── procedures.rs │ ├── procedures │ │ ├── argument.rs │ │ └── call_warp.rs │ ├── sensing.rs │ ├── sensing │ │ └── dayssince2000.rs │ └── tests.rs ├── ir.rs ├── ir │ ├── blocks.rs │ ├── context.rs │ ├── event.rs │ ├── proc.rs │ ├── project.rs │ ├── step.rs │ ├── target.rs │ ├── thread.rs │ ├── types.rs │ └── variable.rs ├── lib.rs ├── optimisation.rs ├── optimisation │ └── var_types.rs ├── registry.rs ├── sb3.rs ├── wasm.rs └── wasm │ ├── external.rs │ ├── flags.rs │ ├── func.rs │ ├── globals.rs │ ├── project.rs │ ├── registries.rs │ ├── strings.rs │ ├── tables.rs │ ├── type_registry.rs │ └── variable.rs ├── vite.config.js └── wasm-gen ├── .gitignore ├── Cargo.toml ├── README.md └── src └── lib.rs /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/universal:2", 3 | "features": { 4 | "ghcr.io/devcontainers/features/rust:1": {}, 5 | "devwasm.azurecr.io/dev-wasm/dev-wasm-feature/rust-wasi:0": {} 6 | }, 7 | "ghcr.io/devcontainers/features/rust:1": {} 8 | } 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behaviour: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Example project that this occurs on:** 21 | A link to or `sb3` file of a project which exhibits this unexpected behaviour. 22 | 23 | **Expected behaviour** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Device information (please complete the following information):** 30 | - OS: [e.g. windows 11] 31 | - Browser [e.g. chrome, firefox] 32 | - Browser version [e.g. 122] 33 | - Hyperquark version (e.g. the one on the website, a specific commit or some local version) 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci-checks.yaml: -------------------------------------------------------------------------------- 1 | on: [push, workflow_dispatch, pull_request] 2 | 3 | name: CI checks 4 | 5 | jobs: 6 | build: 7 | name: Build check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v2 12 | 13 | - name: Install nightly toolchain 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: nightly 18 | override: true 19 | target: wasm32-unknown-unknown 20 | 21 | - name: Run cargo check 22 | uses: actions-rs/cargo@v1 23 | with: 24 | command: check 25 | args: --target=wasm32-unknown-unknown 26 | 27 | clippy: 28 | name: Lint (clippy) 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout sources 32 | uses: actions/checkout@v4 33 | 34 | - name: Install nightly toolchain with clippy available 35 | uses: actions-rs/toolchain@v1 36 | with: 37 | profile: minimal 38 | toolchain: nightly 39 | override: true 40 | components: clippy 41 | 42 | - name: Run cargo clippy 43 | uses: actions-rs/cargo@v1 44 | with: 45 | command: clippy 46 | args: -- -D warnings 47 | 48 | rustfmt: 49 | name: Format 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout sources 53 | uses: actions/checkout@v4 54 | 55 | - name: Install nightly toolchain with rustfmt available 56 | uses: actions-rs/toolchain@v1 57 | with: 58 | profile: minimal 59 | toolchain: nightly 60 | override: true 61 | components: rustfmt 62 | 63 | - name: Run cargo fmt 64 | uses: actions-rs/cargo@v1 65 | with: 66 | command: fmt 67 | args: --all -- --check 68 | 69 | test: 70 | name: Run unit tests 71 | runs-on: ubuntu-latest 72 | steps: 73 | - name: Checkout sources 74 | uses: actions/checkout@v2 75 | 76 | - name: Install nightly toolchain 77 | uses: actions-rs/toolchain@v1 78 | with: 79 | profile: minimal 80 | toolchain: nightly 81 | override: true 82 | 83 | - name: Run cargo test 84 | uses: actions-rs/cargo@v1 85 | with: 86 | command: test 87 | args: >- 88 | -- 89 | --skip cast::float::js_functions_match_declared_types 90 | --skip cast::int::js_functions_match_declared_types 91 | --skip cast::string::js_functions_match_declared_types 92 | --skip join::tests::js_functions_match_declared_types 93 | --skip lt::tests::js_functions_match_declared_types 94 | --skip gt::tests::js_functions_match_declared_types 95 | --skip equals::tests::js_functions_match_declared_types 96 | --skip length::tests::js_functions_match_declared_types 97 | --skip letter_of::tests::js_functions_match_declared_types 98 | --skip contains::tests::js_functions_match_declared_types 99 | --skip dayssince2000::tests::js_functions_match_declared_types 100 | --skip looks::say::tests_debug::js_functions_match_declared_types 101 | --skip looks::say::tests_non_debug::js_functions_match_declared_types 102 | --skip looks::think::tests_debug::js_functions_match_declared_types 103 | --skip looks::think::tests_non_debug::js_functions_match_declared_types 104 | 105 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | on: [push, workflow_dispatch] 2 | 3 | name: Deploy 4 | 5 | jobs: 6 | deploy: 7 | name: Build WASM & website 8 | runs-on: ubuntu-latest 9 | env: 10 | BRANCH_NAME: ${{ github.head_ref || github.ref_name }} 11 | permissions: 12 | contents: write 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v4 16 | 17 | - name: Install nightly toolchain 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: nightly 22 | override: true 23 | target: wasm32-unknown-unknown 24 | 25 | - name: Install wasm-bindgen 26 | uses: actions-rs/cargo@v1 27 | with: 28 | command: install 29 | args: -f wasm-bindgen-cli 30 | 31 | - name: Install cargo-outdir 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: install 35 | args: cargo-outdir 36 | 37 | - name: Install binaryen 38 | run: sudo apt-get install binaryen 39 | 40 | - name: Install node 41 | uses: actions/setup-node@v3 42 | with: 43 | node-version: "20.x" 44 | 45 | - name: Run npm install 46 | run: | 47 | npm install 48 | npm i -g vite 49 | npm i -g binaryen@nightly 50 | 51 | - name: Build 52 | env: 53 | VITE_HASH_HISTORY: true 54 | run: | 55 | chmod +x build.sh && ./build.sh -Wpz 56 | vite build --base=/hyperquark/$BRANCH_NAME/ 57 | 58 | - name: Move files to tmp 59 | run: mv ./playground/dist /tmp/hq-dist 60 | 61 | - name: checkout gh-pages 62 | uses: actions/checkout@v4 63 | with: 64 | ref: gh-pages 65 | 66 | - name: move file to gh-pages 67 | run: | 68 | rm -rf ./$BRANCH_NAME 69 | mv /tmp/hq-dist ./$BRANCH_NAME 70 | #mv ./main/* ./ 71 | 72 | - name: Commit changes 73 | uses: stefanzweifel/git-auto-commit-action@v5 74 | with: 75 | branch: gh-pages 76 | push_options: '--force-with-lease' 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /Cargo.lock 3 | /bad.wasm 4 | /bad.mjs 5 | /bad.wat 6 | /wabt* 7 | /js/compiler 8 | /js/no-compiler 9 | /js/imports.ts 10 | /js/opcodes.js 11 | /node_modules/ 12 | /package-lock.json 13 | /playground/dist/ 14 | /.vite/ 15 | /.vscode/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to HyperQuark 2 | 3 | ## I found a bug 4 | You should check first if there is already an issue for the bug. If there is, feel free to add any supporting information that you feel would be helpful in a comment. If there isn't, you should [create an issue](https://github.com/hyperquark/hyperquark), including as much detail as possible, including a description of the expected and observed behaviour, which projects you see this behaviour in, and whether this was observed on the hyperquark website or a local build. 5 | 6 | ## There's a feature I'd like 7 | First think: is it reasonable? If the answer is yes, check if there are any issues for this feature (remember to check for closed issues), and if there aren't, you can create one yourself. 8 | 9 | ## I'd like to contribute to the codebase 10 | Does the thing you want to code have an open, triaged issue for it? If not, it probably won't be accepted (unless it's a trivial, obviously beneficial change), so you should consider opening an issue first to discuss it. If there is an open issue, it's probably fine to work on it, however, if there is an assignee on the issue then you should ask them first before starting work on it. 11 | 12 | ## First time contributions 13 | If you're looking for a good place to get started, "easier" issues have the ["good first issue" label](https://github.com/HyperQuark/hyperquark/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hyperquark" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } 9 | serde_json = { version = "1.0", default-features = false, features = ["alloc"] } 10 | enum-field-getter = { path = "enum-field-getter" } 11 | wasm-encoder = "0.226.0" 12 | indexmap = { version = "2.0.0", default-features = false } 13 | hashers = "1.0.1" 14 | uuid = { version = "1.4.1", default-features = false, features = ["v4", "js"] } 15 | regex = "1.10.5" 16 | lazy-regex = "3.2.0" 17 | bitmask-enum = "2.2.5" 18 | itertools = { version = "0.13.0", default-features = false, features = ["use_alloc"] } 19 | split_exact = "1.1.0" 20 | wasm-bindgen = "0.2.92" 21 | serde-wasm-bindgen = "0.6.5" 22 | wasm-gen = { path = "wasm-gen" } 23 | 24 | [dev-dependencies] 25 | wasmparser = "0.226.0" 26 | wasmprinter = "0.226.0" 27 | #reqwest = { version = "0.11", features = ["blocking"] } 28 | 29 | [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] 30 | ezno-checker = { git = "https://github.com/kaleidawave/ezno.git", rev = "96d5058bdbb0cde924be008ca1e5a67fe39f46b9" } 31 | 32 | [lib] 33 | crate-type = ["cdylib", "rlib"] 34 | 35 | [profile.release] 36 | lto = true 37 | opt-level = "z" 38 | 39 | [build-dependencies] 40 | convert_case = "0.6.0" 41 | 42 | [features] 43 | compiler = [] # if we only want to access flags, we don't want to additionally have all the compiler machinery 44 | default = ["compiler"] 45 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Gregor Hutchison 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | # build script for hyperquark 2 | # a lot of code here was adapted from https://www.shellscript.sh/examples/getopts/ 3 | 4 | trap "err" ERR # exit if any command returns a non-zero exit code 5 | 6 | 7 | err() 8 | { 9 | echo; 10 | echo Exiting early since previous build step failed!; 11 | exit 1; 12 | } 13 | usage() 14 | { 15 | echo "Usage: $0 [options]" 16 | echo "Options:" 17 | echo " -h -? show this help screen" 18 | echo " -d build for development" 19 | echo " -p build for production" 20 | echo " -V build the website with vite" 21 | echo " -W build wasm" 22 | echo " -o do not run wasm-opt" 23 | echo " -s run wasm-opt with -Os" 24 | echo " -z run wasm-opt with -Oz" 25 | echo " -v verbose output" 26 | exit 1 27 | } 28 | 29 | set_variable() 30 | { 31 | local varname=$1 32 | shift 33 | if [ -z "${!varname}" ]; then 34 | eval "$varname=\"$@\"" 35 | else 36 | echo "Error: $varname already set. This probably means that you've used two conflicting flags." 37 | echo 38 | usage 39 | fi 40 | } 41 | 42 | unset VITE WASM PROD; 43 | QUIET=1; 44 | while getopts 'dpVWoszvh' c 45 | do 46 | case $c in 47 | d) set_variable PROD 0 ;; 48 | p) set_variable PROD 1 ;; 49 | V) set_variable VITE 1 ;; 50 | W) set_variable WASM 1 ;; 51 | o) set_variable WOPT 0 ;; 52 | s) set_variable WOPT 1 ;; 53 | z) set_variable WOPT 2 ;; 54 | v) unset QUIET ;; 55 | h|?) usage ;; 56 | esac 57 | done 58 | 59 | [ -z $WASM ] && set_variable WASM 0; 60 | [ -z $VITE ] && set_variable VITE 0; 61 | 62 | [ -z $PROD ] && usage; 63 | 64 | if [ -z $WOPT ]; then 65 | if [ $PROD = "1" ]; then 66 | set_variable WOPT 2; 67 | else 68 | set_variable WOPT 0; 69 | fi 70 | fi 71 | [ $VITE = "0" ] && [ $WASM = "0" ] && [ $WOPT = "0" ] && echo "exiting (nothing to build)" && exit 0 72 | 73 | if [ $WASM = "1" ]; then 74 | if [ $PROD = "1" ]; then 75 | echo "building hyperquark (compiler) for production..." 76 | cargo build --target=wasm32-unknown-unknown --release ${QUIET:+--quiet} 77 | echo running wasm-bindgen... 78 | wasm-bindgen target/wasm32-unknown-unknown/release/hyperquark.wasm --out-dir=js/compiler 79 | echo "building hyperquark (no compiler) for production..." 80 | cargo build --target=wasm32-unknown-unknown --release ${QUIET:+--quiet} --no-default-features 81 | echo running wasm-bindgen... 82 | wasm-bindgen target/wasm32-unknown-unknown/release/hyperquark.wasm --out-dir=js/no-compiler 83 | else 84 | echo "building hyperquark (compiler) for development..." 85 | cargo build --target=wasm32-unknown-unknown ${QUIET:+--quiet} 86 | echo running wasm-bindgen... 87 | wasm-bindgen target/wasm32-unknown-unknown/debug/hyperquark.wasm --out-dir=js/compiler 88 | echo "building hyperquark (no compiler) for development..." 89 | cargo build --target=wasm32-unknown-unknown ${QUIET:+--quiet} --no-default-features 90 | echo running wasm-bindgen... 91 | wasm-bindgen target/wasm32-unknown-unknown/debug/hyperquark.wasm --out-dir=js/no-compiler 92 | fi 93 | mv $(cargo outdir --no-names --quiet)/imports.ts js/imports.ts 94 | node opcodes.mjs 95 | fi 96 | if [ $WOPT = "1" ]; then 97 | echo running wasm-opt -Os... 98 | wasm-opt -Os -g js/compiler/hyperquark_bg.wasm -o js/compiler/hyperquark_bg.wasm 99 | wasm-opt -Os -g js/no-compiler/hyperquark_bg.wasm -o js/no-compiler/hyperquark_bg.wasm 100 | fi 101 | if [ $WOPT = "2" ]; then 102 | echo running wasm-opt -Oz... 103 | wasm-opt -Oz -g js/compiler/hyperquark_bg.wasm -o js/compiler/hyperquark_bg.wasm 104 | wasm-opt -Oz -g js/no-compiler/hyperquark_bg.wasm -o js/no-compiler/hyperquark_bg.wasm 105 | fi 106 | if [ $VITE = "1" ]; then 107 | echo running npm build... 108 | npm run build 109 | fi 110 | echo finished! -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | allowed-duplicate-crates = ["syn"] 2 | too-many-lines-threshold = 150 3 | allow-panic-in-tests = true 4 | allow-unwrap-in-tests = true -------------------------------------------------------------------------------- /enum-field-getter/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock -------------------------------------------------------------------------------- /enum-field-getter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "enum-field-getter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | repository = "https://github.com/HyperQuark/hyperquark/tree/main/enum-field-getter" 7 | authors = ["pufferfish101007"] 8 | description = "A derive macro to create mutable and immutable getters for tuple/struct members of enum variants" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | syn = { version = "1.0", features = ["full"] } 15 | quote = "1.0" 16 | proc-macro2 = "1.0" 17 | proc-macro-error = "1.0" -------------------------------------------------------------------------------- /enum-field-getter/README.md: -------------------------------------------------------------------------------- 1 | # `enum-field-getter` 2 | 3 | A simple derive macro used to implement methods to access fields which are of the same type across every tuple/struct enum variant. 4 | 5 | ## Usage 6 | 7 | Derive `EnumFieldGetter`. For tuple enum variants, produces `get_n(&self) -> Option<&_>` and `get_n_mut(&mut self) -> Option<&mut _>` methods for each tuple member (starting from 0); for struct variants, getters are of the form `prop(&self) -> Option<&_>` and `prop_mut(&mut self) -> Option<&mut _>`. Getters are only produced so long as that member is the same type across all enum variants - if they are of different types, no getter will be produced for that member. These methods are produced even if that member doesn't exist for all enum variants - in the case that it doesn't exist, the getter will return `None`. 8 | 9 | ## Examples 10 | 11 | ```rust 12 | use enum_field_getter::EnumFieldGetter; 13 | 14 | #[derive(EnumFieldGetter)] 15 | enum Foo { 16 | Bar(u32), 17 | Baz(u32, u32), 18 | } 19 | 20 | let foo = Foo::Bar(16); 21 | let foo0 = foo.get_0(); 22 | assert_eq!(foo0, Some(&16)); 23 | let foo1 = foo.get_1(); 24 | assert!(foo1.is_none()); 25 | ``` 26 | 27 | ```rust 28 | use enum_field_getter::EnumFieldGetter; 29 | 30 | #[derive(EnumFieldGetter)] 31 | enum Boop { 32 | Moo { 33 | a: i32, 34 | b: i32, 35 | }, 36 | Baa { 37 | a: i32, 38 | b: i32, 39 | c: i32, 40 | } 41 | } 42 | 43 | let mut boop = Boop::Baa { a: 0, b: 42, c: 180 }; 44 | let boop_a = boop.a(); 45 | assert_eq!(boop_a, Some(&0)); 46 | let boop_c = boop.c(); 47 | assert_eq!(boop_c, Some(&180)); 48 | *boop.b_mut().unwrap() = 43; 49 | assert_eq!(boop.b(), Some(&43)); 50 | ``` -------------------------------------------------------------------------------- /enum-field-getter/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | use proc_macro::TokenStream; 4 | use proc_macro_error::{abort_call_site, emit_warning, proc_macro_error}; 5 | use quote::{format_ident, quote}; 6 | use syn::{parse_macro_input, Data, DeriveInput, Fields, Type}; 7 | 8 | use std::collections::{HashMap, HashSet}; 9 | 10 | /// See top-level crate documentation. 11 | #[proc_macro_error] 12 | #[proc_macro_derive(EnumFieldGetter)] 13 | pub fn enum_field_getter(stream: TokenStream) -> TokenStream { 14 | let info = parse_macro_input!(stream as DeriveInput); 15 | if let Data::Enum(enum_data) = info.data { 16 | let variants = enum_data.variants.iter(); 17 | let name = info.ident; 18 | let mut field_info: HashMap)> = HashMap::new(); 19 | let mut tuple_field_info: HashMap)> = HashMap::new(); 20 | let mut incompatible = HashSet::::new(); 21 | let mut tuple_incompatible = HashSet::::new(); 22 | for variant in variants { 23 | if let Fields::Named(_) = variant.fields { 24 | for field in &variant.fields { 25 | let ident = field.ident.clone().unwrap().to_string(); 26 | let field_ty = field.ty.clone(); 27 | let df = (field_ty.clone(), vec![variant.ident.to_string()]); 28 | field_info.entry(ident.clone()).and_modify(|info| { 29 | let (ty, used_variants) = info; 30 | if quote!{#field_ty}.to_string() != quote!{#ty}.to_string() { 31 | emit_warning!(field, "fields must be the same type across all variants - no getter will be emitted for this field"); 32 | incompatible.insert(ident.clone()); 33 | } else { 34 | used_variants.push(variant.ident.to_string()); 35 | } 36 | }).or_insert(df); 37 | } 38 | } else if let Fields::Unnamed(_) = variant.fields { 39 | for (i, field) in variant.fields.iter().enumerate() { 40 | let field_ty = field.ty.clone(); 41 | let df = (field_ty.clone(), vec![variant.ident.to_string()]); 42 | tuple_field_info.entry(i).and_modify(|info| { 43 | let (ty, used_variants) = info; 44 | if quote!{#field_ty}.to_string() != quote!{#ty}.to_string() { 45 | emit_warning!(field, "Fields must be the same type across all variants - no getter will be emitted for this field.\nExpected type {}, got {}.", quote!{#ty}.to_string(), quote!{#field_ty}.to_string()); 46 | tuple_incompatible.insert(i); 47 | } else { 48 | used_variants.push(variant.ident.to_string()); 49 | } 50 | }).or_insert(df); 51 | } 52 | } 53 | } 54 | for removeable in incompatible { 55 | field_info.remove(&removeable); 56 | } 57 | for tuple_removeable in tuple_incompatible { 58 | tuple_field_info.remove(&tuple_removeable); 59 | } 60 | let getters = field_info.keys().map(|k| format_ident!("{}", k)); 61 | let getters_mut = field_info.keys().map(|k| format_ident!("{}_mut", k)); 62 | let types = field_info.values().map(|v| v.0.clone()); 63 | let types_mut = types.clone(); 64 | let field_info_vec = field_info.iter().collect::>(); 65 | let matches = field_info_vec.iter().map(|(k, v)| { 66 | let variants = 67 | v.1.clone() 68 | .iter() 69 | .map(|v| format_ident!("{}", v)) 70 | .collect::>(); 71 | let field = vec![format_ident!("{}", k); variants.len()]; 72 | quote! { 73 | match self { 74 | #( 75 | Self::#variants { #field, .. } => Some(#field), 76 | )* 77 | _ => None, 78 | } 79 | } 80 | }); 81 | let matches_mut = matches.clone(); 82 | let tuple_getters = tuple_field_info.keys().map(|k| format_ident!("get_{}", k)); 83 | let tuple_getters_mut = tuple_field_info 84 | .keys() 85 | .map(|k| format_ident!("get_{}_mut", k)); 86 | let tuple_types = tuple_field_info.values().map(|v| v.0.clone()); 87 | let tuple_types_mut = tuple_types.clone(); 88 | let tuple_field_info_vec = tuple_field_info.iter().collect::>(); 89 | let tuple_matches = tuple_field_info_vec.iter().map(|(k, v)| { 90 | let variants = 91 | v.1.clone() 92 | .iter() 93 | .map(|v| format_ident!("{}", v)) 94 | .collect::>(); 95 | let preceding = vec![format_ident!("_"); **k]; 96 | let preceding_quote = vec![quote! { #(#preceding,)* }; variants.len()]; 97 | let field = vec![format_ident!("val_{}", k); variants.len()]; 98 | quote! { 99 | match self { 100 | #( 101 | Self::#variants(#preceding_quote #field, .. ) => Some(#field), 102 | )* 103 | _ => None, 104 | } 105 | } 106 | }); 107 | let tuple_matches_mut = tuple_matches.clone(); 108 | quote! { 109 | impl #name { 110 | #( 111 | pub fn #getters (&self) -> Option<&#types> { 112 | #matches 113 | } 114 | )* 115 | #( 116 | pub fn #tuple_getters (&self) -> Option<&#tuple_types> { 117 | #tuple_matches 118 | } 119 | )* 120 | #( 121 | pub fn #getters_mut (&mut self) -> Option<&mut #types_mut> { 122 | #matches_mut 123 | } 124 | )* 125 | #( 126 | pub fn #tuple_getters_mut (&mut self) -> Option<&mut #tuple_types_mut> { 127 | #tuple_matches_mut 128 | } 129 | )* 130 | } 131 | } 132 | .into() 133 | } else { 134 | abort_call_site!("macro can only be used on enums"); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /js/cast/float2string.ts: -------------------------------------------------------------------------------- 1 | export function float2string(s: number): string { 2 | return s.toString(); 3 | } -------------------------------------------------------------------------------- /js/cast/int2string.ts: -------------------------------------------------------------------------------- 1 | export function int2string(s: number): string { 2 | return s.toString(); 3 | } -------------------------------------------------------------------------------- /js/cast/string2float.ts: -------------------------------------------------------------------------------- 1 | export function string2float(s: string): number { 2 | return parseFloat(s); 3 | } -------------------------------------------------------------------------------- /js/looks/say_debug_float.ts: -------------------------------------------------------------------------------- 1 | import { target_names } from "../shared"; 2 | 3 | export function say_debug_float(data: number, target_idx: number): void { 4 | console.log('%s says: %d', target_names()[target_idx], data); 5 | } -------------------------------------------------------------------------------- /js/looks/say_debug_int.ts: -------------------------------------------------------------------------------- 1 | import { target_names } from "../shared"; 2 | 3 | export function say_debug_int(data: number, target_idx: number): void { 4 | console.log('%s says: %d', target_names()[target_idx], data); 5 | } -------------------------------------------------------------------------------- /js/looks/say_debug_string.ts: -------------------------------------------------------------------------------- 1 | import { target_names } from "../shared"; 2 | 3 | export function say_debug_string(data: string, target_idx: number): void { 4 | console.log('%s says: %s', target_names()[target_idx], data); 5 | } -------------------------------------------------------------------------------- /js/looks/say_float.ts: -------------------------------------------------------------------------------- 1 | import { update_bubble } from "../shared"; 2 | 3 | export function say_float(data: number, target_idx: number): void { 4 | update_bubble(target_idx, "say", data.toString()); 5 | } -------------------------------------------------------------------------------- /js/looks/say_int.ts: -------------------------------------------------------------------------------- 1 | import { update_bubble } from "../shared"; 2 | 3 | export function say_int(data: number, target_idx: number): void { 4 | update_bubble(target_idx, "say", data.toString()); 5 | } -------------------------------------------------------------------------------- /js/looks/say_string.ts: -------------------------------------------------------------------------------- 1 | import { update_bubble } from "../shared"; 2 | 3 | export function say_string(data: string, target_idx: number): void { 4 | update_bubble(target_idx, "say", data); 5 | } -------------------------------------------------------------------------------- /js/looks/think_debug_float.ts: -------------------------------------------------------------------------------- 1 | import { target_names } from "../shared"; 2 | 3 | export function think_debug_float(data: number, target_idx: number): void { 4 | console.log('%s thinks: %d', target_names()[target_idx], data); 5 | } -------------------------------------------------------------------------------- /js/looks/think_debug_int.ts: -------------------------------------------------------------------------------- 1 | import { target_names } from "../shared"; 2 | 3 | export function think_debug_int(data: number, target_idx: number): void { 4 | console.log('%s thinks: %d', target_names()[target_idx], data); 5 | } -------------------------------------------------------------------------------- /js/looks/think_debug_string.ts: -------------------------------------------------------------------------------- 1 | import { target_names } from "../shared"; 2 | 3 | export function think_debug_string(data: string, target_idx: number): void { 4 | console.log('%s thinks: %s', target_names()[target_idx], data); 5 | } -------------------------------------------------------------------------------- /js/looks/think_float.ts: -------------------------------------------------------------------------------- 1 | import { update_bubble } from "../shared"; 2 | 3 | export function think_float(data: number, target_idx: number): void { 4 | update_bubble(target_idx, "think", data.toString()); 5 | } -------------------------------------------------------------------------------- /js/looks/think_int.ts: -------------------------------------------------------------------------------- 1 | import { update_bubble } from "../shared"; 2 | 3 | export function think_int(data: number, target_idx: number): void { 4 | update_bubble(target_idx, "think", data.toString()); 5 | } -------------------------------------------------------------------------------- /js/looks/think_string.ts: -------------------------------------------------------------------------------- 1 | import { update_bubble } from "../shared"; 2 | 3 | export function think_string(data: string, target_idx: number): void { 4 | update_bubble(target_idx, "think", data); 5 | } -------------------------------------------------------------------------------- /js/operator/contains.ts: -------------------------------------------------------------------------------- 1 | export function contains(left: string, right: string): boolean { 2 | return left.toLowerCase().includes(right.toLowerCase()); 3 | } -------------------------------------------------------------------------------- /js/operator/eq_string.ts: -------------------------------------------------------------------------------- 1 | export function eq_string(left: string, right: string): boolean { 2 | return left.toLowerCase() === right.toLowerCase(); 3 | } -------------------------------------------------------------------------------- /js/operator/gt_string.ts: -------------------------------------------------------------------------------- 1 | export function gt_string(left: string, right: string): boolean { 2 | return left.toLowerCase() > right.toLowerCase(); 3 | } -------------------------------------------------------------------------------- /js/operator/lt_string.ts: -------------------------------------------------------------------------------- 1 | export function lt_string(left: string, right: string): boolean { 2 | return left.toLowerCase() < right.toLowerCase(); 3 | } -------------------------------------------------------------------------------- /js/sensing/dayssince2000.ts: -------------------------------------------------------------------------------- 1 | export function dayssince2000(): number { 2 | // https://github.com/scratchfoundation/scratch-vm/blob/f10ab17bf351939153d9d0a17c577b5ba7b3c908/src/blocks/scratch3_sensing.js#L252 3 | const msPerDay = 24 * 60 * 60 * 1000; 4 | const start = new Date(2000, 0, 1); // Months are 0-indexed. 5 | const today = new Date(); 6 | const dstAdjust = today.getTimezoneOffset() - start.getTimezoneOffset(); 7 | let mSecsSinceStart = today.valueOf() - start.valueOf(); 8 | mSecsSinceStart += ((today.getTimezoneOffset() - dstAdjust) * 60 * 1000); 9 | return mSecsSinceStart / msPerDay; 10 | } -------------------------------------------------------------------------------- /js/shared.ts: -------------------------------------------------------------------------------- 1 | let _target_names: Array = []; 2 | let _setup = false; 3 | let _target_bubbles; 4 | let _renderer; 5 | 6 | export function setup(new_target_names: Array, renderer: object) { 7 | _target_names = new_target_names; 8 | _target_bubbles = _target_names.map(_ => null); 9 | console.log(_target_names, _target_bubbles) 10 | _renderer = renderer; 11 | _setup = true; 12 | } 13 | 14 | export function is_setup(): boolean { 15 | return _setup; 16 | } 17 | 18 | function check_setup() { 19 | if (!_setup) { 20 | throw new Error("shared state must be set up before use!") 21 | } 22 | } 23 | 24 | export function target_names(): Array { 25 | check_setup(); 26 | return _target_names 27 | } 28 | 29 | export function update_bubble(target_index: number, verb: "say" | "think", text: string) { 30 | check_setup(); 31 | if (_target_bubbles[target_index] === null) { 32 | _target_bubbles[target_index] = _renderer.createSkin( 33 | "text", 34 | "sprite", 35 | verb, 36 | text, 37 | false 38 | ); 39 | } else { 40 | _renderer.updateTextSkin(_target_bubbles[target_index][0], verb, text, false); 41 | } 42 | } -------------------------------------------------------------------------------- /js/wasm-js-string/concat.ts: -------------------------------------------------------------------------------- 1 | export function concat(left: string, right: string): string { 2 | return left.concat(right); 3 | } -------------------------------------------------------------------------------- /js/wasm-js-string/length.ts: -------------------------------------------------------------------------------- 1 | export function length(string: string): number { 2 | return string.length; 3 | } -------------------------------------------------------------------------------- /js/wasm-js-string/substring.ts: -------------------------------------------------------------------------------- 1 | export function substring(string: string, start: number, end: number): string { 2 | return string.substring(start, end); 3 | } -------------------------------------------------------------------------------- /opcodes.mjs: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'node:fs/promises'; 2 | 3 | let blocksRs = (await readFile('./src/ir/blocks.rs', 'utf8')); 4 | const opcodes = [...new Set(blocksRs.match(/BlockOpcode::[a-z_0-9]+? (?==>)/g).map(op => op.replace('BlockOpcode::', '').trim()))].sort() 5 | await writeFile('./js/opcodes.js', `export const opcodes = ${JSON.stringify(opcodes)};`); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperquark", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "binaryen": "github:pufferfish101007/binaryen.js#non-nullable-table", 12 | "pinia": "^2.1.3", 13 | "scratch-parser": "^5.1.1", 14 | "scratch-sb1-converter": "^0.2.7", 15 | "vite-plugin-wasm": "^3.2.2", 16 | "vue": "^3.3.4", 17 | "vue-router": "^4.2.2", 18 | "wasm-feature-detect": "^1.8.0" 19 | }, 20 | "devDependencies": { 21 | "@vitejs/plugin-vue": "^4.2.3", 22 | "@vitejs/plugin-vue-jsx": "^3.0.1", 23 | "vite": "^4.3.9", 24 | "vite-plugin-node-polyfills": "^0.9.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /playground/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 41 | 42 | 89 | -------------------------------------------------------------------------------- /playground/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | :root { 54 | --transition-time: 0.4s; 55 | } 56 | 57 | *, 58 | *::before, 59 | *::after { 60 | box-sizing: border-box; 61 | margin: 0; 62 | font-weight: normal; 63 | } 64 | 65 | body { 66 | min-height: 100vh; 67 | color: var(--color-text); 68 | background: var(--color-background); 69 | transition: color 0.5s, background-color 0.5s; 70 | line-height: 1.6; 71 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 72 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 73 | font-size: 15px; 74 | text-rendering: optimizeLegibility; 75 | -webkit-font-smoothing: antialiased; 76 | -moz-osx-font-smoothing: grayscale; 77 | } -------------------------------------------------------------------------------- /playground/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 1rem; 7 | font-weight: normal; 8 | } 9 | 10 | div, p { 11 | margin: 1rem auto; 12 | } 13 | 14 | a, 15 | .green { 16 | text-decoration: none; 17 | color: hsl(39.3,100%,37%); 18 | transition: var(--transition-time); 19 | } 20 | 21 | @media (hover: hover) { 22 | a:hover { 23 | background-color: hsla(33.1,100%,41%,0.2); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /playground/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /playground/components/ProjectFileInput.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /playground/components/ProjectIdPlayer.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 38 | 39 | -------------------------------------------------------------------------------- /playground/components/ProjectInput.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 34 | 35 | -------------------------------------------------------------------------------- /playground/components/ProjectPlayer.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 160 | 161 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | HyperQuark 10 | 11 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /playground/lib/imports.js: -------------------------------------------------------------------------------- 1 | export { imports } from '../../js/imports.ts'; -------------------------------------------------------------------------------- /playground/lib/project-loader.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import validate from 'scratch-parser'; 3 | import { SB1File, ValidationError } from 'scratch-sb1-converter'; 4 | 5 | // adapted from https://github.com/scratchfoundation/scratch-vm/blob/6cfea59e7aaff880a1c4e709b2adc14d0113ecab/src/virtual-machine.js#L320 6 | /** 7 | * @param {ArrayBuffer|String} input 8 | */ 9 | export const unpackProject = (input) => { 10 | if (typeof input !== 'string') { 11 | input = Buffer.from(input); 12 | } 13 | return new Promise((resolve, reject) => { 14 | // The second argument of false below indicates to the validator that the 15 | // input should be parsed/validated as an entire project (and not a single sprite) 16 | validate(input, false, (error, res) => { 17 | if (error) return reject(error); 18 | resolve(res); 19 | }); 20 | }).catch(error => { 21 | try { 22 | const sb1 = new SB1File(input); 23 | const json = sb1.json; 24 | json.projectVersion = 2; 25 | return Promise.resolve([json, sb1.zip]); 26 | } catch (sb1Error) { 27 | if (sb1Error instanceof ValidationError) { 28 | // The input does not validate as a Scratch 1 file. 29 | } else { 30 | // The project appears to be a Scratch 1 file but it 31 | // could not be successfully translated into a Scratch 2 32 | // project. 33 | return Promise.reject(sb1Error); 34 | } 35 | } 36 | // Throw original error since the input does not appear to be 37 | // an SB1File. 38 | return Promise.reject(error); 39 | }); 40 | }; -------------------------------------------------------------------------------- /playground/lib/project-runner.js: -------------------------------------------------------------------------------- 1 | import { getSettings } from './settings.js'; 2 | import { imports } from './imports.js'; 3 | import { useDebugModeStore } from '../stores/debug.js'; 4 | import { setup as sharedSetup } from '../../js/shared.ts' 5 | await import('../assets/renderer.js'); 6 | 7 | const debugModeStore = useDebugModeStore(); 8 | 9 | function createSkin(renderer, type, layer, ...params) { 10 | let drawableId = renderer.createDrawable(layer.toString()); 11 | const realType = { 12 | pen: 'Pen', 13 | text: 'Text', 14 | svg: 'SVG' 15 | }[type.toLowerCase()]; 16 | let skin = renderer[`create${realType}Skin`](...params); 17 | renderer.updateDrawableSkinId(drawableId, skin); 18 | return [skin, drawableId]; 19 | } 20 | 21 | const spriteInfoLen = 80; 22 | let _setup = false; 23 | 24 | function setup(renderer, project_json, assets, target_names) { 25 | if (_setup) return; 26 | _setup = true; 27 | renderer.getDrawable = id => renderer._allDrawables[id]; 28 | renderer.getSkin = id => renderer._allSkins[id]; 29 | renderer.createSkin = (type, layer, ...params) => createSkin(renderer, type, layer, ...params); 30 | 31 | const costumes = project_json.targets.map( 32 | (target, index) => target.costumes.map( 33 | ({ md5ext }) => assets[md5ext] 34 | ) 35 | ); 36 | 37 | const costumeNameMap = project_json.targets.map( 38 | target => Object.fromEntries(target.costumes.map( 39 | ({ name }, index) => [name, index] 40 | )) 41 | ); 42 | 43 | // @ts-ignore 44 | window.renderer = renderer; 45 | renderer.setLayerGroupOrdering(["background", "video", "pen", "sprite"]); 46 | //window.open(URL.createObjectURL(new Blob([wasm_bytes], { type: "octet/stream" }))); 47 | const pen_skin = createSkin(renderer, "pen", "pen")[0]; 48 | 49 | const target_skins = project_json.targets.map((target, index) => { 50 | const realCostume = target.costumes[target.currentCostume]; 51 | const costume = costumes[index][target.currentCostume]; 52 | const [skin, drawableId] = createSkin(renderer, costume[0], 'sprite', costume[1], [realCostume.rotationCenterX, realCostume.rotationCenterY]); 53 | const drawable = renderer.getDrawable(drawableId); 54 | if (!target.is_stage) { 55 | drawable.updateVisible(target.visible); 56 | drawable.updatePosition([target.x, target.y]); 57 | drawable.updateDirection(target.rotation); 58 | drawable.updateScale([target.size, target.size]); 59 | } 60 | return [skin, drawableId]; 61 | }); 62 | console.log(target_skins) 63 | 64 | sharedSetup(target_names, renderer); 65 | } 66 | 67 | // @ts-ignore 68 | export default async ( 69 | { framerate = 30, turbo, renderer, wasm_bytes, target_names, string_consts, project_json, assets } = { 70 | framerate: 30, turbo: false, 71 | } 72 | ) => { 73 | if (debugModeStore.debug) window.open(URL.createObjectURL(new Blob([wasm_bytes], { type: 'application/wasm' }))); 74 | const framerate_wait = Math.round(1000 / framerate); 75 | let assert; 76 | let exit; 77 | let browser = false; 78 | let output_div; 79 | let text_div; 80 | 81 | setup(renderer, project_json, assets, target_names); 82 | 83 | console.log('green flag setup complete') 84 | 85 | let strings_tbl; 86 | 87 | let updatePenColor; 88 | let start_time = 0; 89 | let sprite_info_offset = 0; 90 | 91 | const settings = getSettings(); 92 | const builtins = [...(settings['js-string-builtins'] ? ['js-string'] : [])] 93 | 94 | try { 95 | if (!WebAssembly.validate(wasm_bytes, { 96 | builtins 97 | })) { 98 | throw Error(); 99 | } 100 | } catch { 101 | try { 102 | new WebAssembly.Module(wasm_bytes); 103 | throw new Error("invalid WASM module"); 104 | } catch (e) { 105 | throw new Error("invalid WASM module: " + e.message); 106 | } 107 | } 108 | function sleep(ms) { 109 | return new Promise((resolve) => { 110 | setTimeout(resolve, ms); 111 | }); 112 | } 113 | function waitAnimationFrame() { 114 | return new Promise((resolve) => { 115 | requestAnimationFrame(resolve); 116 | }); 117 | } 118 | WebAssembly.instantiate(wasm_bytes, imports, { 119 | builtins 120 | }) 121 | .then(async ({ instance }) => { 122 | const { 123 | flag_clicked, 124 | tick, 125 | memory, 126 | strings, 127 | step_funcs, 128 | vars_num, 129 | threads_count, 130 | requests_refresh, 131 | upc, 132 | threads, 133 | noop, 134 | unreachable_dbg 135 | } = instance.exports; 136 | strings.grow(Object.entries(string_consts).length); 137 | for (const [i, str] of Object.entries(string_consts || {})) { 138 | // @ts-ignore 139 | strings.set(i, str); 140 | } 141 | updatePenColor = (i) => null;//upc(i - 1); 142 | strings_tbl = strings; 143 | window.memory = memory; 144 | window.flag_clicked = flag_clicked; 145 | window.tick = tick; 146 | window.stop = () => { 147 | if (typeof threads == "undefined") { 148 | let memArr = new Uint32Array(memory.buffer); 149 | for (let i = 0; i < threads_count.value; i++) { 150 | memArr[i] = 0; 151 | } 152 | } else { 153 | for (let i = 0; i < threads.length; i++) { 154 | threads.set(i, noop); 155 | } 156 | } 157 | threads_count.value = 0; 158 | }; 159 | // @ts-ignore 160 | //sprite_info_offset = vars_num.value * 16 + thn_offset + 4; 161 | const dv = new DataView(memory.buffer); 162 | /*for (let i = 0; i < target_names.length - 1; i++) { 163 | dv.setFloat32( 164 | sprite_info_offset + i * spriteInfoLen + 16, 165 | 66.66, 166 | true 167 | ); 168 | dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 20, 100, true); 169 | dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 24, 100, true); 170 | dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 28, 0, true); 171 | dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 40, 1, true); 172 | dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 44, 1, true); 173 | dv.setFloat64(sprite_info_offset + i * spriteInfoLen + 48, 1, true); 174 | }*/ 175 | try { 176 | // expose the module to devtools 177 | unreachable_dbg(); 178 | } catch (error) { 179 | console.info('synthetic error to expose wasm modulee to devtools:', error) 180 | } 181 | flag_clicked(); 182 | start_time = Date.now(); 183 | console.log("green_flag()"); 184 | $outertickloop: while (true) { 185 | renderer.draw(); 186 | const thisTickStartTime = Date.now(); 187 | // @ts-ignore 188 | $innertickloop: do {//for (const _ of [1]) { 189 | // @ts-ignore 190 | tick(); 191 | // @ts-ignore 192 | if (threads_count.value === 0) { 193 | break $outertickloop; 194 | } 195 | } while ( 196 | (Date.now() - thisTickStartTime) < (framerate_wait * 0.8) && 197 | (!turbo && requests_refresh.value === 0) 198 | ) 199 | // @ts-ignore 200 | requests_refresh.value = 0; 201 | if (framerate_wait > 0) { 202 | await sleep( 203 | Math.max(0, framerate_wait - (Date.now() - thisTickStartTime)) 204 | ); 205 | } else { 206 | await waitAnimationFrame(); 207 | } 208 | } 209 | }) 210 | .catch((e) => { 211 | throw new Error("error when instantiating module:\n" + e.stack); 212 | /*exit(1);*/ 213 | }); 214 | }; 215 | -------------------------------------------------------------------------------- /playground/lib/settings.js: -------------------------------------------------------------------------------- 1 | import * as hyperquarkExports from '../../js/no-compiler/hyperquark.js'; 2 | import { WasmFlags, WasmFeature, all_wasm_features, wasm_feature_detect_name } from '../../js/no-compiler/hyperquark.js'; 3 | import * as WasmFeatureDetect from 'wasm-feature-detect'; 4 | export { WasmFlags }; 5 | 6 | console.log(WasmFeature) 7 | window.hyperquarkExports = hyperquarkExports; 8 | 9 | export const supportedWasmFeatures = await getSupportedWasmFeatures(); 10 | export const defaultSettings = new WasmFlags(Array.from(supportedWasmFeatures, (feat) => WasmFeature[feat])); 11 | const defaultSettingsObj = defaultSettings.to_js(); 12 | 13 | window.defaultSettings = defaultSettings; 14 | 15 | function settingsInfoFromType(type) { 16 | if (type === "boolean") { 17 | return { 18 | type: "checkbox" 19 | } 20 | } else if (type in hyperquarkExports) { 21 | return { 22 | type: "radio", 23 | options: Object.keys(hyperquarkExports[type]).filter(key => typeof key === 'string' && !/\d+/.test(key)), 24 | enum_obj: hyperquarkExports[type], 25 | } 26 | } else { 27 | return null; 28 | } 29 | } 30 | 31 | export const settingsInfo = Object.fromEntries(Object.entries(Object.getOwnPropertyDescriptors(WasmFlags.prototype)) 32 | .filter(([_, descriptor]) => typeof descriptor.get === 'function') 33 | .map(([key, _]) => key) 34 | .map(key => { 35 | let flag_info = WasmFlags.flag_info(key); 36 | return [key, { 37 | flag_info, 38 | ...settingsInfoFromType(flag_info.ty) 39 | }] 40 | })); 41 | 42 | /** 43 | * @returns {WasmFlags} 44 | */ 45 | export function getSettings() { 46 | let store = localStorage["settings"]; 47 | try { 48 | return WasmFlags.from_js({ ...defaultSettingsObj, ...JSON.parse(store) }); 49 | } catch { 50 | return defaultSettings; 51 | } 52 | } 53 | 54 | /** 55 | * @param {WasmFlags} settings 56 | */ 57 | export function saveSettings(settings) { 58 | console.log(settings.to_js()) 59 | localStorage['settings'] = JSON.stringify(settings.to_js()); 60 | } 61 | 62 | /** 63 | * @returns {Set} 64 | */ 65 | export async function getSupportedWasmFeatures() { 66 | const featureSet = new Set(); 67 | for (const feature of all_wasm_features()) { 68 | if (await WasmFeatureDetect[wasm_feature_detect_name(feature)]()) { 69 | featureSet.add(WasmFeature[feature]); 70 | } 71 | } 72 | return featureSet; 73 | } 74 | 75 | console.log(await getSupportedWasmFeatures()) 76 | 77 | /** 78 | * @returns {Set} 79 | */ 80 | export function getUsedWasmFeatures() { 81 | const settings = getSettings().to_js(); 82 | console.log(settings) 83 | const featureSet = new Set(); 84 | for (const [id, info] of Object.entries(settingsInfo)) { 85 | const theseFeatures = info.flag_info.wasm_features(settings[id])?.map?.((num) => WasmFeature[num]); 86 | for (const feature of theseFeatures || []) { 87 | featureSet.add(feature); 88 | } 89 | } 90 | return featureSet; 91 | } -------------------------------------------------------------------------------- /playground/main.js: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | 3 | import { createApp } from 'vue' 4 | import { createPinia } from 'pinia' 5 | 6 | import App from './App.vue' 7 | import router from './router' 8 | 9 | const app = createApp(App) 10 | 11 | app.use(createPinia()) 12 | app.use(router) 13 | 14 | app.mount('#app') 15 | 16 | -------------------------------------------------------------------------------- /playground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperQuark/hyperquark/21060f097791796f6a83fb7a710b55026532797a/playground/public/favicon.ico -------------------------------------------------------------------------------- /playground/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperQuark/hyperquark/21060f097791796f6a83fb7a710b55026532797a/playground/public/logo.png -------------------------------------------------------------------------------- /playground/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'; 2 | import { h, ref, onMounted } from 'vue'; 3 | import Loading from '../components/Loading.vue'; 4 | 5 | let componentCache = Object.setPrototypeOf({}, null); 6 | 7 | 8 | const view = (name) => ({ 9 | setup() { 10 | let component = componentCache[name]; 11 | const loading = ref(!Boolean(component)); 12 | if (loading.value) { 13 | import(`../views/${name}.vue`).then((c) => { 14 | loading.value = false; 15 | component = c.default; 16 | componentCache[name] = component; 17 | }); 18 | } 19 | return () => loading.value ? h(Loading) : h(component); 20 | } 21 | }); 22 | 23 | const router = createRouter({ 24 | history: (import.meta.env.VITE_HASH_HISTORY ? createWebHashHistory : createWebHistory)(import.meta.env.BASE_URL), 25 | routes: [ 26 | { 27 | path: '/', 28 | name: 'home', 29 | component: view('HomeView'), 30 | }, 31 | { 32 | path: '/projects/:id(\\d+)', 33 | name: 'projectIdPlayer', 34 | component: view('ProjectIdView'), 35 | props: true, 36 | }, 37 | { 38 | path: '/projects/file', 39 | name: 'projectFilePlayer', 40 | component: view('ProjectFileView'), 41 | }, 42 | { 43 | path: '/projects/test', 44 | name: 'testProjectPlayer', 45 | component: view('TestProject'), 46 | }, 47 | { 48 | path: '/about', 49 | name: 'about', 50 | component: view('AboutView'), 51 | }, 52 | { 53 | path: '/settings', 54 | name: 'settings', 55 | component: view('Settings'), 56 | }, 57 | { 58 | path: '/:_(.*)*', 59 | name: '404', 60 | component: view('404'), 61 | } 62 | ] 63 | }); 64 | 65 | router.afterEach((to, from) => { 66 | document.getElementById('canonical-rel').setAttribute('href', `https://hyperquark.edgecompute.app${to.path}`); 67 | }) 68 | 69 | export default router; 70 | -------------------------------------------------------------------------------- /playground/stores/debug.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | export const useDebugModeStore = defineStore('debugMode', () => { 5 | const debug = ref(typeof new URLSearchParams(window.location.search).get('debug') === 'string'); 6 | const toggleDebug = () => { 7 | debug.value = !debug.value; 8 | if (!erudaEnabled && debug.value) { 9 | eruda.init(); 10 | } 11 | } 12 | let erudaEnabled = false; 13 | if (debug.value) { 14 | eruda.init(); 15 | erudaEnabled = true; 16 | } 17 | return { debug, toggleDebug }; 18 | }) 19 | -------------------------------------------------------------------------------- /playground/stores/projectfile.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | export const useProjectFileStore = defineStore('projectFile', () => { 5 | const json = ref(null); 6 | const assets = ref([]); 7 | const title = ref('untitled'); 8 | const author = ref('unknown'); 9 | 10 | return { json, assets, title, author }; 11 | }) 12 | -------------------------------------------------------------------------------- /playground/views/404.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /playground/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | -------------------------------------------------------------------------------- /playground/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | 33 | 61 | 62 | -------------------------------------------------------------------------------- /playground/views/ProjectFileView.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /playground/views/ProjectIdView.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /playground/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 77 | 78 | -------------------------------------------------------------------------------- /playground/views/TestProject.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" -------------------------------------------------------------------------------- /src/alloc.rs: -------------------------------------------------------------------------------- 1 | use buddy_alloc::{BuddyAllocParam, FastAllocParam, NonThreadsafeAlloc}; 2 | 3 | // These values can be tuned 4 | const FAST_HEAP_SIZE: usize = 4 * 1024; // 4 KB 5 | const HEAP_SIZE: usize = 16 * 1024; // 16 KB 6 | const LEAF_SIZE: usize = 16; 7 | 8 | static mut FAST_HEAP: [u8; FAST_HEAP_SIZE] = [0u8; FAST_HEAP_SIZE]; 9 | static mut HEAP: [u8; HEAP_SIZE] = [0u8; HEAP_SIZE]; 10 | 11 | #[global_allocator] 12 | static ALLOC: NonThreadsafeAlloc = unsafe { 13 | let fast_param = FastAllocParam::new(FAST_HEAP.as_ptr(), FAST_HEAP_SIZE); 14 | let buddy_param = BuddyAllocParam::new(HEAP.as_ptr(), HEAP_SIZE, LEAF_SIZE); 15 | NonThreadsafeAlloc::new(fast_param, buddy_param) 16 | }; 17 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use core::cell::{BorrowError, BorrowMutError}; 2 | 3 | use alloc::boxed::Box; 4 | use wasm_bindgen::JsValue; 5 | 6 | pub type HQResult = Result; 7 | 8 | #[derive(Clone, Debug)] // todo: get rid of this once all expects are gone 9 | pub struct HQError { 10 | pub err_type: HQErrorType, 11 | pub msg: Box, 12 | pub file: Box, 13 | pub line: u32, 14 | pub column: u32, 15 | } 16 | #[derive(Clone, Debug, PartialEq, Eq)] // todo: get rid of this once all expects are gone 17 | pub enum HQErrorType { 18 | MalformedProject, 19 | InternalError, 20 | Unimplemented, 21 | } 22 | 23 | impl From for JsValue { 24 | fn from(val: HQError) -> Self { 25 | Self::from_str(match val.err_type { 26 | HQErrorType::Unimplemented => format!("todo: {}
at {}:{}:{}
this is a bug or missing feature that is known and will be fixed or implemented in a future update", val.msg, val.file, val.line, val.column), 27 | HQErrorType::InternalError => format!("error: {}
at {}:{}:{}
this is probably a bug with HyperQuark itself. Please report this bug, with this error message, at https://github.com/hyperquark/hyperquark/issues/new", val.msg, val.file, val.line, val.column), 28 | HQErrorType::MalformedProject => format!("error: {}
at {}:{}:{}
this is probably a problem with the project itself, but if it works in vanilla scratch then this is a bug; please report it, by creating an issue at https://github.com/hyperquark/hyperquark/issues/new, including this error message", val.msg, val.file, val.line, val.column), 29 | }.as_str()) 30 | } 31 | } 32 | 33 | impl From for HQError { 34 | fn from(_e: BorrowError) -> Self { 35 | Self { 36 | err_type: HQErrorType::InternalError, 37 | msg: "couldn't borrow cell".into(), 38 | file: file!().into(), 39 | line: line!(), 40 | column: column!(), 41 | } 42 | } 43 | } 44 | 45 | impl From for HQError { 46 | fn from(_e: BorrowMutError) -> Self { 47 | Self { 48 | err_type: HQErrorType::InternalError, 49 | msg: "couldn't mutably borrow cell".into(), 50 | file: file!().into(), 51 | line: line!(), 52 | column: column!(), 53 | } 54 | } 55 | } 56 | 57 | #[macro_export] 58 | #[clippy::format_args] 59 | macro_rules! hq_todo { 60 | () => {{ 61 | return Err($crate::HQError { 62 | err_type: $crate::HQErrorType::Unimplemented, 63 | msg: "todo".into(), 64 | file: file!().into(), 65 | line: line!(), 66 | column: column!() 67 | }); 68 | }}; 69 | ($($args:tt)+) => {{ 70 | return Err($crate::HQError { 71 | err_type: $crate::HQErrorType::Unimplemented, 72 | msg: format!("{}", format_args!($($args)*)).into(), 73 | file: file!().into(), 74 | line: line!(), 75 | column: column!() 76 | }); 77 | }}; 78 | } 79 | 80 | #[macro_export] 81 | #[clippy::format_args] 82 | macro_rules! hq_bug { 83 | ($($args:tt)+) => {{ 84 | return Err($crate::HQError { 85 | err_type: $crate::HQErrorType::InternalError, 86 | msg: format!("{}", format_args!($($args)*)).into(), 87 | file: file!().into(), 88 | line: line!(), 89 | column: column!() 90 | }); 91 | }}; 92 | } 93 | 94 | #[macro_export] 95 | #[clippy::format_args] 96 | macro_rules! hq_assert { 97 | ($expr:expr) => {{ 98 | if !($expr) { 99 | return Err($crate::HQError { 100 | err_type: $crate::HQErrorType::InternalError, 101 | msg: format!("Assertion failed: {}", stringify!($expr)).into(), 102 | file: file!().into(), 103 | line: line!(), 104 | column: column!() 105 | }); 106 | }; 107 | assert!($expr); 108 | }}; 109 | ($expr:expr, $($args:tt)+) => {{ 110 | if !($expr) { 111 | return Err($crate::HQError { 112 | err_type: $crate::HQErrorType::InternalError, 113 | msg: format!("Assertion failed: {}\nMessage: {}", stringify!($expr), format_args!($($args)*)).into(), 114 | file: file!().into(), 115 | line: line!(), 116 | column: column!() 117 | }); 118 | }; 119 | assert!($expr); 120 | }}; 121 | } 122 | 123 | #[macro_export] 124 | #[clippy::format_args] 125 | macro_rules! hq_assert_eq { 126 | ($l:expr, $r:expr) => {{ 127 | if $l != $r { 128 | return Err($crate::HQError { 129 | err_type: $crate::HQErrorType::InternalError, 130 | msg: format!("Assertion failed: {} == {}\nLeft: {}\nRight: {}", stringify!($l), stringify!($r), $l, $r).into(), 131 | file: file!().into(), 132 | line: line!(), 133 | column: column!() 134 | }); 135 | }; 136 | assert_eq!($l, $r); 137 | }}; 138 | ($l:expr, $r:expr, $($args:tt)+) => {{ 139 | if $l != $r { 140 | return Err($crate::HQError { 141 | err_type: $crate::HQErrorType::InternalError, 142 | msg: format!("Assertion failed: {}\nLeft: {}\nRight: {}\nMessage: {}", stringify!($l), stringify!($r), $l, $r, format_args!($($args)*)).into(), 143 | file: file!().into(), 144 | line: line!(), 145 | column: column!() 146 | }); 147 | }; 148 | assert_eq!($l, $r); 149 | }}; 150 | } 151 | 152 | #[macro_export] 153 | #[clippy::format_args] 154 | macro_rules! hq_bad_proj { 155 | ($($args:tt)+) => {{ 156 | return Err($crate::HQError { 157 | err_type: $crate::HQErrorType::MalformedProject, 158 | msg: format!("{}", format_args!($($args)*)).into(), 159 | file: file!().into(), 160 | line: line!(), 161 | column: column!() 162 | }); 163 | }}; 164 | } 165 | 166 | #[macro_export] 167 | #[clippy::format_args] 168 | macro_rules! make_hq_todo { 169 | ($($args:tt)+) => {{ 170 | use $crate::alloc::Box::ToBox; 171 | $crate::HQError { 172 | err_type: $crate::HQErrorType::Unimplemented, 173 | msg: format!("{}", format_args!($($args)*)).into(), 174 | file: file!().into(), 175 | line: line!(), 176 | column: column!() 177 | } 178 | }}; 179 | } 180 | 181 | #[macro_export] 182 | #[clippy::format_args] 183 | macro_rules! make_hq_bug { 184 | ($($args:tt)+) => {{ 185 | $crate::HQError { 186 | err_type: $crate::HQErrorType::InternalError, 187 | msg: format!("{}", format_args!($($args)*)).into(), 188 | file: file!().into(), 189 | line: line!(), 190 | column: column!() 191 | } 192 | }}; 193 | } 194 | 195 | #[macro_export] 196 | #[clippy::format_args] 197 | macro_rules! make_hq_bad_proj { 198 | ($($args:tt)+) => {{ 199 | $crate::HQError { 200 | err_type: $crate::HQErrorType::MalformedProject, 201 | msg: format!("{}", format_args!($($args)*)).into(), 202 | file: file!().into(), 203 | line: line!(), 204 | column: column!() 205 | } 206 | }}; 207 | } 208 | -------------------------------------------------------------------------------- /src/instructions.rs: -------------------------------------------------------------------------------- 1 | //! Contains information about instructions (roughly anaologus to blocks), 2 | //! including input type validation, output type mapping, and WASM generation. 3 | 4 | #![allow( 5 | clippy::unnecessary_wraps, 6 | reason = "many functions here needlessly return `Result`s in order to keep type signatures consistent" 7 | )] 8 | #![allow( 9 | clippy::needless_pass_by_value, 10 | reason = "there are so many `Rc`s here which I don't want to change" 11 | )] 12 | 13 | use crate::prelude::*; 14 | 15 | mod control; 16 | mod data; 17 | mod hq; 18 | mod looks; 19 | mod operator; 20 | mod procedures; 21 | mod sensing; 22 | 23 | #[macro_use] 24 | mod tests; 25 | 26 | include!(concat!(env!("OUT_DIR"), "/ir-opcodes.rs")); 27 | 28 | mod input_switcher; 29 | pub use input_switcher::wrap_instruction; 30 | 31 | pub use hq::r#yield::YieldMode; 32 | 33 | mod prelude { 34 | pub use crate::ir::Type as IrType; 35 | pub use crate::prelude::*; 36 | pub use crate::wasm::{InternalInstruction, StepFunc}; 37 | pub use wasm_encoder::{RefType, ValType}; 38 | pub use wasm_gen::wasm; 39 | 40 | /// Canonical NaN + bit 33, + string pointer in bits 1-32 41 | pub const BOXED_STRING_PATTERN: i64 = 0x7FF8_0001 << 32; 42 | /// Canonical NaN + bit 33, + i32 in bits 1-32 43 | pub const BOXED_INT_PATTERN: i64 = 0x7ff8_0002 << 32; 44 | } 45 | -------------------------------------------------------------------------------- /src/instructions/control.rs: -------------------------------------------------------------------------------- 1 | pub mod if_else; 2 | pub mod r#loop; 3 | -------------------------------------------------------------------------------- /src/instructions/control/if_else.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::ir::Step; 3 | use wasm_encoder::BlockType; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct Fields { 7 | pub branch_if: Rc, 8 | pub branch_else: Rc, 9 | } 10 | 11 | pub fn wasm( 12 | func: &StepFunc, 13 | _inputs: Rc<[IrType]>, 14 | Fields { 15 | branch_if, 16 | branch_else, 17 | }: &Fields, 18 | ) -> HQResult> { 19 | let if_instructions = func.compile_inner_step(branch_if)?; 20 | let else_instructions = func.compile_inner_step(branch_else)?; 21 | let block_type = func 22 | .registries() 23 | .types() 24 | .register_default((vec![ValType::I32], vec![]))?; 25 | Ok(wasm![ 26 | Block(BlockType::FunctionType(block_type)), 27 | Block(BlockType::FunctionType(block_type)), 28 | I32Eqz, 29 | BrIf(0) 30 | ] 31 | .into_iter() 32 | .chain(if_instructions) 33 | .chain(wasm![Br(1), End,]) 34 | .chain(else_instructions) 35 | .chain(wasm![End]) 36 | .collect()) 37 | } 38 | 39 | pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { 40 | Rc::new([IrType::Boolean]) 41 | } 42 | 43 | pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { 44 | Ok(None) 45 | } 46 | 47 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 48 | 49 | // crate::instructions_test! {none; hq__if; @ super::Fields(None)} 50 | -------------------------------------------------------------------------------- /src/instructions/control/loop.rs: -------------------------------------------------------------------------------- 1 | // for use in warped contexts only. 2 | 3 | use super::super::prelude::*; 4 | use crate::ir::Step; 5 | use wasm_encoder::BlockType; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct Fields { 9 | pub first_condition: Option>, 10 | pub condition: Rc, 11 | pub body: Rc, 12 | pub flip_if: bool, 13 | } 14 | 15 | pub fn wasm( 16 | func: &StepFunc, 17 | _inputs: Rc<[IrType]>, 18 | Fields { 19 | first_condition, 20 | condition, 21 | body, 22 | flip_if, 23 | }: &Fields, 24 | ) -> HQResult> { 25 | let inner_instructions = func.compile_inner_step(body)?; 26 | let first_condition_instructions = func.compile_inner_step( 27 | &first_condition 28 | .clone() 29 | .unwrap_or_else(|| Rc::clone(condition)), 30 | )?; 31 | let condition_instructions = func.compile_inner_step(condition)?; 32 | Ok(wasm![Block(BlockType::Empty),] 33 | .into_iter() 34 | .chain(first_condition_instructions) 35 | .chain(if *flip_if { 36 | wasm![BrIf(0), Loop(BlockType::Empty)] 37 | } else { 38 | wasm![I32Eqz, BrIf(0), Loop(BlockType::Empty)] 39 | }) 40 | .chain(inner_instructions) 41 | .chain(condition_instructions) 42 | .chain(if *flip_if { 43 | wasm![I32Eqz, BrIf(0), End, End] 44 | } else { 45 | wasm![BrIf(0), End, End] 46 | }) 47 | .collect()) 48 | } 49 | 50 | pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { 51 | Rc::new([]) 52 | } 53 | 54 | pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { 55 | Ok(None) 56 | } 57 | 58 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 59 | 60 | // crate::instructions_test! {none; hq_repeat; @ super::Fields(None)} 61 | -------------------------------------------------------------------------------- /src/instructions/data.rs: -------------------------------------------------------------------------------- 1 | pub mod setvariableto; 2 | pub mod teevariable; 3 | pub mod variable; 4 | -------------------------------------------------------------------------------- /src/instructions/data/setvariableto.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::ir::RcVar; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct Fields(pub RcVar); 6 | 7 | pub fn wasm( 8 | func: &StepFunc, 9 | inputs: Rc<[IrType]>, 10 | Fields(variable): &Fields, 11 | ) -> HQResult> { 12 | let t1 = inputs[0]; 13 | if variable.0.local() { 14 | let local_index: u32 = func.local_variable(variable)?; 15 | if variable.0.possible_types().is_base_type() { 16 | Ok(wasm![LocalSet(local_index)]) 17 | } else { 18 | Ok(wasm![ 19 | @boxed(t1), 20 | LocalSet(local_index) 21 | ]) 22 | } 23 | } else { 24 | let global_index: u32 = func.registries().variables().register(variable)?; 25 | if variable.0.possible_types().is_base_type() { 26 | Ok(wasm![GlobalSet(global_index)]) 27 | } else { 28 | Ok(wasm![ 29 | @boxed(t1), 30 | GlobalSet(global_index), 31 | ]) 32 | } 33 | } 34 | } 35 | 36 | pub fn acceptable_inputs(Fields(rcvar): &Fields) -> Rc<[IrType]> { 37 | Rc::new([if rcvar.0.possible_types().is_none() { 38 | IrType::Any 39 | } else { 40 | *rcvar.0.possible_types() 41 | }]) 42 | } 43 | 44 | pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { 45 | Ok(None) 46 | } 47 | 48 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 49 | 50 | crate::instructions_test!( 51 | any_global; 52 | data_setvariableto; 53 | t 54 | @ super::Fields( 55 | super::RcVar( 56 | Rc::new( 57 | crate::ir::Variable::new( 58 | IrType::Any, 59 | crate::sb3::VarVal::Float(0.0), 60 | false 61 | ) 62 | ) 63 | ) 64 | ) 65 | ); 66 | 67 | crate::instructions_test!( 68 | float_global; 69 | data_setvariableto; 70 | t 71 | @ super::Fields( 72 | super::RcVar( 73 | Rc::new( 74 | crate::ir::Variable::new( 75 | IrType::Float, 76 | crate::sb3::VarVal::Float(0.0), 77 | false 78 | ) 79 | ) 80 | ) 81 | ) 82 | ); 83 | 84 | crate::instructions_test!( 85 | string_global; 86 | data_setvariableto; 87 | t 88 | @ super::Fields( 89 | super::RcVar( 90 | Rc::new( 91 | crate::ir::Variable::new( 92 | IrType::String, 93 | crate::sb3::VarVal::String("".into()), 94 | false 95 | ) 96 | ) 97 | ) 98 | ) 99 | ); 100 | 101 | crate::instructions_test!( 102 | int_global; 103 | data_setvariableto; 104 | t 105 | @ super::Fields( 106 | super::RcVar( 107 | Rc::new( 108 | crate::ir::Variable::new( 109 | IrType::QuasiInt, 110 | crate::sb3::VarVal::Bool(true), 111 | false 112 | ) 113 | ) 114 | ) 115 | ) 116 | ); 117 | 118 | crate::instructions_test!( 119 | any_local; 120 | data_setvariableto; 121 | t 122 | @ super::Fields( 123 | super::RcVar( 124 | Rc::new( 125 | crate::ir::Variable::new( 126 | IrType::Any, 127 | crate::sb3::VarVal::Float(0.0), 128 | true 129 | ) 130 | ) 131 | ) 132 | ) 133 | ); 134 | 135 | crate::instructions_test!( 136 | float_local; 137 | data_setvariableto; 138 | t 139 | @ super::Fields( 140 | super::RcVar( 141 | Rc::new( 142 | crate::ir::Variable::new( 143 | IrType::Float, 144 | crate::sb3::VarVal::Float(0.0), 145 | true 146 | ) 147 | ) 148 | ) 149 | ) 150 | ); 151 | 152 | crate::instructions_test!( 153 | string_local; 154 | data_setvariableto; 155 | t 156 | @ super::Fields( 157 | super::RcVar( 158 | Rc::new( 159 | crate::ir::Variable::new( 160 | IrType::String, 161 | crate::sb3::VarVal::String("".into()), 162 | true 163 | ) 164 | ) 165 | ) 166 | ) 167 | ); 168 | 169 | crate::instructions_test!( 170 | int_local; 171 | data_setvariableto; 172 | t 173 | @ super::Fields( 174 | super::RcVar( 175 | Rc::new( 176 | crate::ir::Variable::new( 177 | IrType::QuasiInt, 178 | crate::sb3::VarVal::Bool(true), 179 | true 180 | ) 181 | ) 182 | ) 183 | ) 184 | ); 185 | -------------------------------------------------------------------------------- /src/instructions/data/teevariable.rs: -------------------------------------------------------------------------------- 1 | /// this is currently just a convenience block. if we get thread-scoped variables 2 | /// (i.e. locals) then this can actually use the wasm tee instruction. 3 | use super::super::prelude::*; 4 | use crate::ir::RcVar; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct Fields(pub RcVar); 8 | 9 | pub fn wasm( 10 | func: &StepFunc, 11 | inputs: Rc<[IrType]>, 12 | Fields(variable): &Fields, 13 | ) -> HQResult> { 14 | let t1 = inputs[0]; 15 | if variable.0.local() { 16 | let local_index: u32 = func.local_variable(variable)?; 17 | if variable.0.possible_types().is_base_type() { 18 | Ok(wasm![LocalTee(local_index)]) 19 | } else { 20 | Ok(wasm![ 21 | @boxed(t1), 22 | LocalTee(local_index) 23 | ]) 24 | } 25 | } else { 26 | let global_index: u32 = func.registries().variables().register(variable)?; 27 | if variable.0.possible_types().is_base_type() { 28 | Ok(wasm![GlobalSet(global_index), GlobalGet(global_index)]) 29 | } else { 30 | Ok(wasm![ 31 | @boxed(t1), 32 | GlobalSet(global_index), 33 | GlobalGet(global_index) 34 | ]) 35 | } 36 | } 37 | } 38 | 39 | pub fn acceptable_inputs(Fields(rcvar): &Fields) -> Rc<[IrType]> { 40 | Rc::new([if rcvar.0.possible_types().is_none() { 41 | IrType::Any 42 | } else { 43 | *rcvar.0.possible_types() 44 | }]) 45 | } 46 | 47 | pub fn output_type(_inputs: Rc<[IrType]>, Fields(rcvar): &Fields) -> HQResult> { 48 | Ok(Some(*rcvar.0.possible_types())) 49 | } 50 | 51 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 52 | 53 | crate::instructions_test!( 54 | any_global; 55 | data_teevariable; 56 | t 57 | @ super::Fields( 58 | super::RcVar( 59 | Rc::new( 60 | crate::ir::Variable::new( 61 | IrType::Any, 62 | crate::sb3::VarVal::Float(0.0), 63 | false 64 | ) 65 | ) 66 | ) 67 | ) 68 | ); 69 | 70 | crate::instructions_test!( 71 | float_global; 72 | data_teevariable; 73 | t 74 | @ super::Fields( 75 | super::RcVar( 76 | Rc::new( 77 | crate::ir::Variable::new( 78 | IrType::Float, 79 | crate::sb3::VarVal::Float(0.0), 80 | false 81 | ) 82 | ) 83 | ) 84 | ) 85 | ); 86 | 87 | crate::instructions_test!( 88 | string_global; 89 | data_teevariable; 90 | t 91 | @ super::Fields( 92 | super::RcVar( 93 | Rc::new( 94 | crate::ir::Variable::new( 95 | IrType::String, 96 | crate::sb3::VarVal::String("".into()), 97 | false 98 | ) 99 | ) 100 | ) 101 | ) 102 | ); 103 | 104 | crate::instructions_test!( 105 | int_global; 106 | data_teevariable; 107 | t 108 | @ super::Fields( 109 | super::RcVar( 110 | Rc::new( 111 | crate::ir::Variable::new( 112 | IrType::QuasiInt, 113 | crate::sb3::VarVal::Bool(true), 114 | false 115 | ) 116 | ) 117 | ) 118 | ) 119 | ); 120 | 121 | crate::instructions_test!( 122 | any_local; 123 | data_teevariable; 124 | t 125 | @ super::Fields( 126 | super::RcVar( 127 | Rc::new( 128 | crate::ir::Variable::new( 129 | IrType::Any, 130 | crate::sb3::VarVal::Float(0.0), 131 | true 132 | ) 133 | ) 134 | ) 135 | ) 136 | ); 137 | 138 | crate::instructions_test!( 139 | float_local; 140 | data_teevariable; 141 | t 142 | @ super::Fields( 143 | super::RcVar( 144 | Rc::new( 145 | crate::ir::Variable::new( 146 | IrType::Float, 147 | crate::sb3::VarVal::Float(0.0), 148 | true 149 | ) 150 | ) 151 | ) 152 | ) 153 | ); 154 | 155 | crate::instructions_test!( 156 | string_local; 157 | data_teevariable; 158 | t 159 | @ super::Fields( 160 | super::RcVar( 161 | Rc::new( 162 | crate::ir::Variable::new( 163 | IrType::String, 164 | crate::sb3::VarVal::String("".into()), 165 | true 166 | ) 167 | ) 168 | ) 169 | ) 170 | ); 171 | 172 | crate::instructions_test!( 173 | int_local; 174 | data_teevariable; 175 | t 176 | @ super::Fields( 177 | super::RcVar( 178 | Rc::new( 179 | crate::ir::Variable::new( 180 | IrType::QuasiInt, 181 | crate::sb3::VarVal::Bool(true), 182 | true 183 | ) 184 | ) 185 | ) 186 | ) 187 | ); 188 | -------------------------------------------------------------------------------- /src/instructions/data/variable.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::ir::RcVar; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct Fields(pub RcVar); 6 | 7 | pub fn wasm( 8 | func: &StepFunc, 9 | _inputs: Rc<[IrType]>, 10 | Fields(variable): &Fields, 11 | ) -> HQResult> { 12 | if variable.0.local() { 13 | let local_index: u32 = func.local_variable(variable)?; 14 | Ok(wasm![LocalGet(local_index)]) 15 | } else { 16 | let global_index: u32 = func.registries().variables().register(variable)?; 17 | Ok(wasm![GlobalGet(global_index)]) 18 | } 19 | } 20 | 21 | pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { 22 | Rc::new([]) 23 | } 24 | 25 | pub fn output_type(_inputs: Rc<[IrType]>, Fields(rcvar): &Fields) -> HQResult> { 26 | Ok(Some(if rcvar.0.possible_types().is_none() { 27 | IrType::Any 28 | } else { 29 | *rcvar.0.possible_types() 30 | })) 31 | } 32 | 33 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 34 | 35 | crate::instructions_test!( 36 | any_global; 37 | data_variable; 38 | @ super::Fields( 39 | super::RcVar( 40 | Rc::new( 41 | crate::ir::Variable::new( 42 | IrType::Any, 43 | crate::sb3::VarVal::Float(0.0), 44 | false, 45 | ) 46 | ) 47 | ) 48 | ) 49 | ); 50 | 51 | crate::instructions_test!( 52 | float_global; 53 | data_variable; 54 | @ super::Fields( 55 | super::RcVar( 56 | Rc::new( 57 | crate::ir::Variable::new( 58 | IrType::Float, 59 | crate::sb3::VarVal::Float(0.0), 60 | false, 61 | ) 62 | ) 63 | ) 64 | ) 65 | ); 66 | 67 | crate::instructions_test!( 68 | string_global; 69 | data_variable; 70 | @ super::Fields( 71 | super::RcVar( 72 | Rc::new( 73 | crate::ir::Variable::new( 74 | IrType::String, 75 | crate::sb3::VarVal::String("".into()), 76 | false, 77 | ) 78 | ) 79 | ) 80 | ) 81 | ); 82 | 83 | crate::instructions_test!( 84 | int_global; 85 | data_variable; 86 | @ super::Fields( 87 | super::RcVar( 88 | Rc::new( 89 | crate::ir::Variable::new( 90 | IrType::QuasiInt, 91 | crate::sb3::VarVal::Bool(true), 92 | false, 93 | ) 94 | ) 95 | ) 96 | ) 97 | ); 98 | 99 | crate::instructions_test!( 100 | any_local; 101 | data_variable; 102 | @ super::Fields( 103 | super::RcVar( 104 | Rc::new( 105 | crate::ir::Variable::new( 106 | IrType::Any, 107 | crate::sb3::VarVal::Float(0.0), 108 | true, 109 | ) 110 | ) 111 | ) 112 | ) 113 | ); 114 | 115 | crate::instructions_test!( 116 | float_local; 117 | data_variable; 118 | @ super::Fields( 119 | super::RcVar( 120 | Rc::new( 121 | crate::ir::Variable::new( 122 | IrType::Float, 123 | crate::sb3::VarVal::Float(0.0), 124 | true, 125 | ) 126 | ) 127 | ) 128 | ) 129 | ); 130 | 131 | crate::instructions_test!( 132 | string_local; 133 | data_variable; 134 | @ super::Fields( 135 | super::RcVar( 136 | Rc::new( 137 | crate::ir::Variable::new( 138 | IrType::String, 139 | crate::sb3::VarVal::String("".into()), 140 | true, 141 | ) 142 | ) 143 | ) 144 | ) 145 | ); 146 | 147 | crate::instructions_test!( 148 | int_local; 149 | data_variable; 150 | @ super::Fields( 151 | super::RcVar( 152 | Rc::new( 153 | crate::ir::Variable::new( 154 | IrType::QuasiInt, 155 | crate::sb3::VarVal::Bool(true), 156 | true, 157 | ) 158 | ) 159 | ) 160 | ) 161 | ); 162 | -------------------------------------------------------------------------------- /src/instructions/hq.rs: -------------------------------------------------------------------------------- 1 | pub mod cast; 2 | pub mod drop; 3 | pub mod float; 4 | pub mod integer; 5 | pub mod text; 6 | pub mod r#yield; 7 | -------------------------------------------------------------------------------- /src/instructions/hq/cast.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Fields(pub IrType); 5 | 6 | fn best_cast_candidate(from: IrType, to: IrType) -> HQResult { 7 | let to_base_types = to.base_types().collect::>(); 8 | hq_assert!(!to_base_types.is_empty()); 9 | let Some(from_base) = from.base_type() else { 10 | hq_bug!("from type has no base type") 11 | }; 12 | Ok(if to_base_types.contains(&from_base) { 13 | from_base 14 | } else { 15 | let mut candidates = vec![]; 16 | for preference in match from_base { 17 | IrType::QuasiInt => &[IrType::Float, IrType::String] as &[IrType], 18 | IrType::Float => &[IrType::String, IrType::QuasiInt] as &[IrType], 19 | IrType::String => &[IrType::Float, IrType::QuasiInt] as &[IrType], 20 | _ => unreachable!(), 21 | } { 22 | if to_base_types.contains(preference) { 23 | candidates.push(preference); 24 | } 25 | } 26 | hq_assert!(!candidates.is_empty()); 27 | *candidates[0] 28 | }) 29 | } 30 | 31 | pub fn wasm( 32 | func: &StepFunc, 33 | inputs: Rc<[IrType]>, 34 | &Fields(to): &Fields, 35 | ) -> HQResult> { 36 | let from = inputs[0]; 37 | 38 | let target = best_cast_candidate(from, to)?; 39 | 40 | let Some(from_base) = from.base_type() else { 41 | hq_bug!("from type has no base type") 42 | }; 43 | 44 | Ok(match target { 45 | IrType::Float => match from_base { 46 | IrType::Float => wasm![], 47 | IrType::QuasiInt => wasm![F64ConvertI32S], 48 | IrType::String => { 49 | let func_index = func.registries().external_functions().register( 50 | ("cast", "string2float".into()), 51 | (vec![ValType::EXTERNREF], vec![ValType::F64]), 52 | )?; 53 | wasm![Call(func_index)] 54 | } 55 | _ => hq_todo!("bad cast: {:?} -> float", from_base), 56 | }, 57 | IrType::String => match from_base { 58 | IrType::Float => { 59 | let func_index = func.registries().external_functions().register( 60 | ("cast", "float2string".into()), 61 | (vec![ValType::F64], vec![ValType::EXTERNREF]), 62 | )?; 63 | wasm![Call(func_index)] 64 | } 65 | IrType::QuasiInt => { 66 | let func_index = func.registries().external_functions().register( 67 | ("cast", "int2string".into()), 68 | (vec![ValType::I32], vec![ValType::EXTERNREF]), 69 | )?; 70 | wasm![Call(func_index)] 71 | } 72 | IrType::String => vec![], 73 | _ => hq_todo!("bad cast: {:?} -> string", from_base), 74 | }, 75 | IrType::QuasiInt => match from_base { 76 | IrType::Float => wasm![I32TruncSatF64S], 77 | IrType::String => { 78 | let func_index = func.registries().external_functions().register( 79 | ("cast", "string2float".into()), 80 | (vec![ValType::EXTERNREF], vec![ValType::F64]), 81 | )?; 82 | wasm![Call(func_index), I32TruncSatF64S] 83 | } 84 | IrType::QuasiInt => vec![], 85 | _ => hq_todo!("unimplemented cast: {:?} -> Int", from_base), 86 | }, 87 | _ => hq_todo!("unimplemented cast: {:?} -> {:?}", from_base, target), 88 | }) 89 | } 90 | 91 | pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { 92 | Rc::new([IrType::Number.or(IrType::String).or(IrType::Boolean)]) 93 | } 94 | 95 | pub fn output_type(inputs: Rc<[IrType]>, &Fields(to): &Fields) -> HQResult> { 96 | Ok(Some( 97 | inputs[0] 98 | .base_types() 99 | .map(|from| best_cast_candidate(from, to)) 100 | .collect::>>()? 101 | .into_iter() 102 | .reduce(IrType::or) 103 | .ok_or_else(|| make_hq_bug!("input was empty"))?, 104 | )) 105 | } 106 | 107 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 108 | 109 | crate::instructions_test! {float; hq_cast; t @ super::Fields(IrType::Float)} 110 | crate::instructions_test! {string; hq_cast; t @ super::Fields(IrType::String)} 111 | crate::instructions_test! {int; hq_cast; t @ super::Fields(IrType::QuasiInt)} 112 | -------------------------------------------------------------------------------- /src/instructions/hq/drop.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 4 | Ok(wasm![Drop]) 5 | } 6 | 7 | pub fn acceptable_inputs() -> Rc<[IrType]> { 8 | Rc::new([IrType::Number.or(IrType::String)]) 9 | } 10 | 11 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { 12 | Ok(None) 13 | } 14 | 15 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 16 | 17 | crate::instructions_test! {tests; hq_drop; t} 18 | -------------------------------------------------------------------------------- /src/instructions/hq/float.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | clippy::trivially_copy_pass_by_ref, 3 | reason = "Fields should be passed by reference for type signature consistency" 4 | )] 5 | 6 | use super::super::prelude::*; 7 | 8 | #[derive(Clone, Copy, Debug)] 9 | pub struct Fields(pub f64); 10 | 11 | pub fn wasm( 12 | _func: &StepFunc, 13 | _inputs: Rc<[IrType]>, 14 | fields: &Fields, 15 | ) -> HQResult> { 16 | Ok(wasm![F64Const(fields.0)]) 17 | } 18 | 19 | pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { 20 | Rc::new([]) 21 | } 22 | 23 | pub fn output_type(_inputs: Rc<[IrType]>, &Fields(val): &Fields) -> HQResult> { 24 | Ok(Some(match val { 25 | 0.0 => IrType::FloatZero, 26 | f64::INFINITY => IrType::FloatPosInf, 27 | f64::NEG_INFINITY => IrType::FloatNegInf, 28 | nan if f64::is_nan(nan) => IrType::FloatNan, 29 | int if int % 1.0 == 0.0 && int > 0.0 => IrType::FloatPosInt, 30 | int if int % 1.0 == 0.0 && int < 0.0 => IrType::FloatNegInt, 31 | frac if frac > 0.0 => IrType::FloatPosFrac, 32 | frac if frac < 0.0 => IrType::FloatNegFrac, 33 | _ => unreachable!(), 34 | })) 35 | } 36 | 37 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 38 | 39 | crate::instructions_test! {tests; hq_float; @ super::Fields(0.0)} 40 | -------------------------------------------------------------------------------- /src/instructions/hq/integer.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | clippy::trivially_copy_pass_by_ref, 3 | reason = "Fields should be passed by reference for type signature consistency" 4 | )] 5 | 6 | use super::super::prelude::*; 7 | 8 | #[derive(Clone, Copy, Debug)] 9 | pub struct Fields(pub i32); 10 | 11 | pub fn wasm( 12 | _func: &StepFunc, 13 | _inputs: Rc<[IrType]>, 14 | fields: &Fields, 15 | ) -> HQResult> { 16 | Ok(wasm![I32Const(fields.0)]) 17 | } 18 | 19 | pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { 20 | Rc::new([]) 21 | } 22 | 23 | pub fn output_type(_inputs: Rc<[IrType]>, &Fields(val): &Fields) -> HQResult> { 24 | Ok(Some(match val { 25 | 0 => IrType::IntZero, 26 | pos if pos > 0 => IrType::IntPos, 27 | neg if neg < 0 => IrType::IntNeg, 28 | _ => unreachable!(), 29 | })) 30 | } 31 | 32 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 33 | 34 | crate::instructions_test! {tests; hq_integer; @ super::Fields(0)} 35 | -------------------------------------------------------------------------------- /src/instructions/hq/text.rs: -------------------------------------------------------------------------------- 1 | use crate::wasm::TableOptions; 2 | 3 | use super::super::prelude::*; 4 | use wasm_encoder::RefType; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct Fields(pub Box); 8 | 9 | pub fn wasm( 10 | func: &StepFunc, 11 | _inputs: Rc<[IrType]>, 12 | fields: &Fields, 13 | ) -> HQResult> { 14 | let string_idx = func 15 | .registries() 16 | .strings() 17 | .register_default(fields.0.clone())?; 18 | Ok(wasm![ 19 | I32Const(string_idx), 20 | TableGet(func.registries().tables().register( 21 | "strings".into(), 22 | TableOptions { 23 | element_type: RefType::EXTERNREF, 24 | min: 0, 25 | max: None, 26 | // this default gets fixed up in src/wasm/tables.rs 27 | init: None, 28 | } 29 | )?,), 30 | ]) 31 | } 32 | 33 | pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { 34 | Rc::new([]) 35 | } 36 | 37 | pub fn output_type(_inputs: Rc<[IrType]>, Fields(val): &Fields) -> HQResult> { 38 | Ok(Some(match &**val { 39 | bool if bool.to_lowercase() == "true" || bool.to_lowercase() == "false" => { 40 | IrType::StringBoolean 41 | } 42 | num if let Ok(float) = num.parse::() 43 | && !float.is_nan() => 44 | { 45 | IrType::StringNumber 46 | } 47 | _ => IrType::StringNan, 48 | })) 49 | } 50 | 51 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 52 | 53 | crate::instructions_test! {tests; hq_text; @ super::Fields("hello, world!".into())} 54 | -------------------------------------------------------------------------------- /src/instructions/hq/yield.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::ir::Step; 3 | use crate::wasm::TableOptions; 4 | use crate::wasm::{flags::Scheduler, GlobalExportable, GlobalMutable, StepFunc}; 5 | use wasm_encoder::{ConstExpr, HeapType, MemArg}; 6 | 7 | #[derive(Clone, Debug)] 8 | pub enum YieldMode { 9 | Tail(Rc), 10 | Inline(Rc), 11 | Schedule(Weak), 12 | None, 13 | } 14 | 15 | #[derive(Clone, Debug)] 16 | pub struct Fields { 17 | pub mode: YieldMode, 18 | } 19 | 20 | pub fn wasm( 21 | func: &StepFunc, 22 | _inputs: Rc<[IrType]>, 23 | Fields { mode: yield_mode }: &Fields, 24 | ) -> HQResult> { 25 | let noop_global = func.registries().globals().register( 26 | "noop_func".into(), 27 | ( 28 | ValType::Ref(RefType { 29 | nullable: false, 30 | heap_type: HeapType::Concrete( 31 | func.registries() 32 | .types() 33 | .register_default((vec![ValType::I32], vec![]))?, 34 | ), 35 | }), 36 | ConstExpr::ref_func(0), // this is a placeholder. 37 | GlobalMutable(false), 38 | GlobalExportable(false), 39 | ), 40 | )?; 41 | 42 | let step_func_ty = func 43 | .registries() 44 | .types() 45 | .register_default((vec![ValType::I32], vec![]))?; 46 | 47 | let threads_count = func.registries().globals().register( 48 | "threads_count".into(), 49 | ( 50 | ValType::I32, 51 | ConstExpr::i32_const(0), 52 | GlobalMutable(true), 53 | GlobalExportable(true), 54 | ), 55 | )?; 56 | 57 | Ok(match yield_mode { 58 | YieldMode::None => match func.flags().scheduler { 59 | Scheduler::CallIndirect => { 60 | // Write a special value (e.g. 0 for noop) to linear memory for this thread 61 | wasm![ 62 | LocalGet(0), // thread index 63 | I32Const(4), 64 | I32Mul, 65 | I32Const(0), // 0 = noop step index 66 | // store at address (thread_index * 4) 67 | I32Store(MemArg { 68 | offset: 0, 69 | align: 2, 70 | memory_index: 0, 71 | }), 72 | // GlobalGet(threads_count), 73 | // I32Const(1), 74 | // I32Sub, 75 | // GlobalSet(threads_count), 76 | Return 77 | ] 78 | } 79 | Scheduler::TypedFuncRef => { 80 | let threads_table = func.registries().tables().register( 81 | "threads".into(), 82 | TableOptions { 83 | element_type: RefType { 84 | nullable: false, 85 | heap_type: HeapType::Concrete(step_func_ty), 86 | }, 87 | min: 0, 88 | max: None, 89 | // this default gets fixed up in src/wasm/tables.rs 90 | init: None, 91 | }, 92 | )?; 93 | wasm![ 94 | LocalGet(0), 95 | GlobalGet(noop_global), 96 | TableSet(threads_table), 97 | GlobalGet(threads_count), 98 | I32Const(1), 99 | I32Sub, 100 | GlobalSet(threads_count), 101 | Return 102 | ] 103 | } 104 | }, 105 | YieldMode::Inline(step) => func.compile_inner_step(step)?, 106 | YieldMode::Schedule(weak_step) => { 107 | let step = Weak::upgrade(weak_step) 108 | .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; 109 | step.make_used_non_inline()?; 110 | match func.flags().scheduler { 111 | Scheduler::CallIndirect => { 112 | wasm![ 113 | LocalGet(0), // thread index 114 | I32Const(4), 115 | I32Mul, 116 | #LazyStepIndex(Weak::clone(weak_step)), 117 | I32Store(MemArg { 118 | offset: 0, 119 | align: 2, 120 | memory_index: 0, 121 | }), 122 | Return 123 | ] 124 | } 125 | Scheduler::TypedFuncRef => { 126 | let threads_table = func.registries().tables().register( 127 | "threads".into(), 128 | TableOptions { 129 | element_type: RefType { 130 | nullable: false, 131 | heap_type: HeapType::Concrete(step_func_ty), 132 | }, 133 | min: 0, 134 | max: None, 135 | // this default gets fixed up in src/wasm/tables.rs 136 | init: None, 137 | }, 138 | )?; 139 | wasm![ 140 | LocalGet(0), 141 | #LazyStepRef(Weak::clone(weak_step)), 142 | TableSet(threads_table), 143 | Return 144 | ] 145 | } 146 | } 147 | } 148 | YieldMode::Tail(_) => hq_todo!(), 149 | }) 150 | } 151 | 152 | pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { 153 | Rc::new([]) 154 | } 155 | 156 | pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { 157 | Ok(None) 158 | } 159 | 160 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 161 | 162 | crate::instructions_test! {none; hq_yield; @ super::Fields { mode: super::YieldMode::None }} 163 | -------------------------------------------------------------------------------- /src/instructions/looks.rs: -------------------------------------------------------------------------------- 1 | pub mod say; 2 | pub mod think; 3 | -------------------------------------------------------------------------------- /src/instructions/looks/say.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Fields { 5 | pub debug: bool, 6 | pub target_idx: u32, 7 | } 8 | 9 | pub fn wasm( 10 | func: &StepFunc, 11 | inputs: Rc<[IrType]>, 12 | &Fields { debug, target_idx }: &Fields, 13 | ) -> HQResult> { 14 | let prefix = String::from(if debug { "say_debug" } else { "say" }); 15 | let itarget_idx: i32 = target_idx 16 | .try_into() 17 | .map_err(|_| make_hq_bug!("target index out of bounds"))?; 18 | Ok(if IrType::QuasiInt.contains(inputs[0]) { 19 | let func_index = func.registries().external_functions().register( 20 | ("looks", format!("{prefix}_int").into_boxed_str()), 21 | (vec![ValType::I32, ValType::I32], vec![]), 22 | )?; 23 | wasm![I32Const(itarget_idx), Call(func_index)] 24 | } else if IrType::Float.contains(inputs[0]) { 25 | let func_index = func.registries().external_functions().register( 26 | ("looks", format!("{prefix}_float").into_boxed_str()), 27 | (vec![ValType::F64, ValType::I32], vec![]), 28 | )?; 29 | wasm![I32Const(itarget_idx), Call(func_index)] 30 | } else if IrType::String.contains(inputs[0]) { 31 | let func_index = func.registries().external_functions().register( 32 | ("looks", format!("{prefix}_string").into_boxed_str()), 33 | (vec![ValType::EXTERNREF, ValType::I32], vec![]), 34 | )?; 35 | wasm![I32Const(itarget_idx), Call(func_index)] 36 | } else { 37 | hq_bug!("bad input") 38 | }) 39 | } 40 | 41 | pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { 42 | Rc::new([IrType::String.or(IrType::Number)]) 43 | } 44 | 45 | pub fn output_type(inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { 46 | if !(IrType::Number.or(IrType::String).contains(inputs[0])) { 47 | hq_todo!("unimplemented input type: {:?}", inputs) 48 | } 49 | Ok(None) 50 | } 51 | 52 | pub const REQUESTS_SCREEN_REFRESH: bool = true; 53 | 54 | crate::instructions_test! {tests_debug; looks_say; t @ super::Fields { debug: true, target_idx: 0 }} 55 | crate::instructions_test! {tests_non_debug; looks_say; t @ super::Fields { debug: false, target_idx: 0, }} 56 | -------------------------------------------------------------------------------- /src/instructions/looks/think.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Fields { 5 | pub debug: bool, 6 | pub target_idx: u32, 7 | } 8 | 9 | pub fn wasm( 10 | func: &StepFunc, 11 | inputs: Rc<[IrType]>, 12 | &Fields { debug, target_idx }: &Fields, 13 | ) -> HQResult> { 14 | let prefix = String::from(if debug { "think_debug" } else { "think" }); 15 | let itarget_idx: i32 = target_idx 16 | .try_into() 17 | .map_err(|_| make_hq_bug!("target index out of bounds"))?; 18 | Ok(if IrType::QuasiInt.contains(inputs[0]) { 19 | let func_index = func.registries().external_functions().register( 20 | ("looks", format!("{prefix}_int").into_boxed_str()), 21 | (vec![ValType::I32, ValType::I32], vec![]), 22 | )?; 23 | wasm![I32Const(itarget_idx), Call(func_index)] 24 | } else if IrType::Float.contains(inputs[0]) { 25 | let func_index = func.registries().external_functions().register( 26 | ("looks", format!("{prefix}_float").into_boxed_str()), 27 | (vec![ValType::F64, ValType::I32], vec![]), 28 | )?; 29 | wasm![I32Const(itarget_idx), Call(func_index)] 30 | } else if IrType::String.contains(inputs[0]) { 31 | let func_index = func.registries().external_functions().register( 32 | ("looks", format!("{prefix}_string").into_boxed_str()), 33 | (vec![ValType::EXTERNREF, ValType::I32], vec![]), 34 | )?; 35 | wasm![I32Const(itarget_idx), Call(func_index)] 36 | } else { 37 | hq_bug!("bad input") 38 | }) 39 | } 40 | 41 | pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { 42 | Rc::new([IrType::String.or(IrType::Number)]) 43 | } 44 | 45 | pub fn output_type(inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { 46 | if !(IrType::Number.or(IrType::String).contains(inputs[0])) { 47 | hq_todo!("unimplemented input type: {:?}", inputs) 48 | } 49 | Ok(None) 50 | } 51 | 52 | pub const REQUESTS_SCREEN_REFRESH: bool = true; 53 | 54 | crate::instructions_test! {tests_debug; looks_think; t @ super::Fields { debug: true, target_idx: 0 }} 55 | crate::instructions_test! {tests_non_debug; looks_think; t @ super::Fields { debug: false, target_idx: 0, }} 56 | -------------------------------------------------------------------------------- /src/instructions/operator.rs: -------------------------------------------------------------------------------- 1 | pub mod add; 2 | pub mod and; 3 | pub mod contains; 4 | pub mod divide; 5 | pub mod equals; 6 | pub mod gt; 7 | pub mod join; 8 | pub mod length; 9 | pub mod letter_of; 10 | pub mod lt; 11 | pub mod multiply; 12 | pub mod not; 13 | pub mod or; 14 | pub mod subtract; 15 | -------------------------------------------------------------------------------- /src/instructions/operator/add.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 2); 5 | let t1 = inputs[0]; 6 | let t2 = inputs[1]; 7 | Ok(if IrType::QuasiInt.contains(t1) { 8 | if IrType::QuasiInt.contains(t2) { 9 | wasm![I32Add] 10 | } else if IrType::Float.contains(t2) { 11 | let f64_local = func.local(ValType::F64)?; 12 | wasm![ 13 | LocalSet(f64_local), 14 | F64ConvertI32S, 15 | LocalGet(f64_local), 16 | @nanreduce(t2), 17 | F64Add, 18 | ] 19 | } else { 20 | hq_bug!("bad input") 21 | } 22 | } else if IrType::Float.contains(t1) { 23 | if IrType::Float.contains(t2) { 24 | let f64_local = func.local(ValType::F64)?; 25 | wasm![ 26 | @nanreduce(t2), 27 | LocalSet(f64_local), 28 | @nanreduce(t1), 29 | LocalGet(f64_local), 30 | F64Add 31 | ] 32 | } else if IrType::QuasiInt.contains(t2) { 33 | let i32_local = func.local(ValType::I32)?; 34 | wasm![ 35 | LocalSet(i32_local), 36 | @nanreduce(t1), 37 | LocalGet(i32_local), 38 | F64ConvertI32S, 39 | F64Add 40 | ] 41 | } else { 42 | hq_bug!("bad input") 43 | } 44 | } else { 45 | hq_bug!("bad input: {:?}", inputs) 46 | }) 47 | } 48 | 49 | pub fn acceptable_inputs() -> Rc<[IrType]> { 50 | Rc::new([IrType::Number, IrType::Number]) 51 | } 52 | 53 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { 54 | hq_assert_eq!(inputs.len(), 2); 55 | let t1 = inputs[0]; 56 | let t2 = inputs[1]; 57 | let maybe_positive = t1.maybe_positive() || t2.maybe_positive(); 58 | let maybe_negative = t1.maybe_negative() || t2.maybe_negative(); 59 | let maybe_zero = (t1.maybe_zero() || t1.maybe_nan()) && (t2.maybe_zero() || t2.maybe_nan()); 60 | let maybe_nan = (IrType::FloatNegInf.intersects(t1) && IrType::FloatPosInf.intersects(t2)) 61 | || (IrType::FloatNegInf.intersects(t2) && IrType::FloatPosInf.intersects(t1)); 62 | Ok(Some(if IrType::QuasiInt.contains(t1.or(t2)) { 63 | IrType::none_if_false(maybe_positive, IrType::IntPos) 64 | .or(IrType::none_if_false(maybe_negative, IrType::IntNeg)) 65 | .or(IrType::none_if_false(maybe_zero, IrType::IntZero)) 66 | } else if (IrType::QuasiInt.contains(t1) && IrType::Float.contains(t2)) 67 | || (IrType::QuasiInt.contains(t2) && IrType::Float.contains(t1)) 68 | || IrType::Float.contains(t1.or(t2)) 69 | { 70 | IrType::none_if_false(maybe_positive, IrType::FloatPos) 71 | .or(IrType::none_if_false(maybe_negative, IrType::FloatNeg)) 72 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) 73 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) 74 | } else { 75 | // there is a boxed type somewhere 76 | // TODO: can these bounds be tightened? e.g. it may only be a positive int or negative float? 77 | // i have no idea if that would ever work, but it would be useful for considering when 78 | // addition/subtraction may give NaN (since inf-inf=nan but inf+inf=inf) 79 | IrType::none_if_false(maybe_positive, IrType::FloatPos.or(IrType::IntPos)) 80 | .or(IrType::none_if_false( 81 | maybe_negative, 82 | IrType::FloatNeg.or(IrType::IntNeg), 83 | )) 84 | .or(IrType::none_if_false( 85 | maybe_zero, 86 | IrType::FloatZero.or(IrType::IntZero), 87 | )) 88 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) 89 | })) 90 | } 91 | 92 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 93 | 94 | crate::instructions_test! {tests; operator_add; t1, t2 ;} 95 | -------------------------------------------------------------------------------- /src/instructions/operator/and.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 4 | Ok(wasm![I32And]) 5 | } 6 | 7 | pub fn acceptable_inputs() -> Rc<[IrType]> { 8 | Rc::new([IrType::Boolean, IrType::Boolean]) 9 | } 10 | 11 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { 12 | Ok(Some(IrType::Boolean)) 13 | } 14 | 15 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 16 | 17 | crate::instructions_test! {tests; operator_and; t1, t2 ;} 18 | -------------------------------------------------------------------------------- /src/instructions/operator/contains.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 2); 5 | let func_index = func.registries().external_functions().register( 6 | ("operator", "contains".into()), 7 | ( 8 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 9 | vec![ValType::I32], 10 | ), 11 | )?; 12 | Ok(wasm![Call(func_index)]) 13 | } 14 | 15 | pub fn acceptable_inputs() -> Rc<[IrType]> { 16 | Rc::new([IrType::String, IrType::String]) 17 | } 18 | 19 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { 20 | Ok(Some(IrType::Boolean)) 21 | } 22 | 23 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 24 | 25 | crate::instructions_test! {tests; operator_contains; t1, t2 ;} 26 | -------------------------------------------------------------------------------- /src/instructions/operator/divide.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 2); 5 | let t1 = inputs[0]; 6 | let t2 = inputs[1]; 7 | let f64_local = func.local(ValType::F64)?; 8 | Ok(wasm![ 9 | LocalSet(f64_local), 10 | @nanreduce(t1), 11 | LocalGet(f64_local), 12 | @nanreduce(t2), 13 | F64Div 14 | ]) 15 | } 16 | 17 | // TODO: is integer division acceptable if we can prove that it will give an integer result (or if it is floored?) 18 | pub fn acceptable_inputs() -> Rc<[IrType]> { 19 | Rc::new([IrType::Float, IrType::Float]) 20 | } 21 | 22 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { 23 | hq_assert_eq!(inputs.len(), 2); 24 | let t1 = inputs[0]; 25 | let t2 = inputs[1]; 26 | let maybe_positive = (t1.maybe_positive() && t2.maybe_positive()) 27 | || (t1.maybe_negative() && t2.maybe_negative()); 28 | let maybe_negative = (t1.maybe_positive() && t2.maybe_negative()) 29 | || (t1.maybe_negative() && t2.maybe_positive()); 30 | let maybe_zero = t1.maybe_zero() || t1.maybe_nan(); // TODO: can this be narrowed to +/-0? 31 | let maybe_infinity = t2.maybe_zero() || t2.maybe_nan(); // TODO: can this be narrowed to +/-infinity? 32 | let maybe_nan = t1.maybe_zero() && t2.maybe_zero(); 33 | Ok(Some( 34 | IrType::none_if_false(maybe_positive, IrType::FloatPos) 35 | .or(IrType::none_if_false(maybe_negative, IrType::FloatNeg)) 36 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) 37 | .or(IrType::none_if_false(maybe_infinity, IrType::FloatInf)) 38 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)), 39 | )) 40 | } 41 | 42 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 43 | 44 | crate::instructions_test! {tests; operator_divide; t1, t2 ;} 45 | -------------------------------------------------------------------------------- /src/instructions/operator/gt.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::BlockType; 2 | 3 | use super::super::prelude::*; 4 | 5 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 6 | hq_assert_eq!(inputs.len(), 2); 7 | let t1 = inputs[0]; 8 | let t2 = inputs[1]; 9 | let block_type = BlockType::Result(ValType::I32); 10 | Ok(if IrType::QuasiInt.contains(t1) { 11 | if IrType::QuasiInt.contains(t2) { 12 | wasm![I32GtS] 13 | } else if IrType::Float.contains(t2) { 14 | let local1 = func.local(ValType::F64)?; 15 | let local2 = func.local(ValType::F64)?; 16 | wasm![ 17 | LocalSet(local2), 18 | F64ConvertI32S, 19 | LocalSet(local1), 20 | LocalGet(local2), 21 | @isnan(t2), 22 | If(block_type), 23 | I32Const(0), // (number) < NaN always false 24 | Else, 25 | LocalGet(local1), 26 | LocalGet(local2), 27 | F64Gt, 28 | End, 29 | ] 30 | } else if IrType::String.contains(t2) { 31 | // TODO: try converting string to number first (if applicable) 32 | let int2string = func.registries().external_functions().register( 33 | ("cast", "int2string".into()), 34 | (vec![ValType::I32], vec![ValType::EXTERNREF]), 35 | )?; 36 | let string_gt = func.registries().external_functions().register( 37 | ("operator", "gt_string".into()), 38 | ( 39 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 40 | vec![ValType::I32], 41 | ), 42 | )?; 43 | let extern_local = func.local(ValType::EXTERNREF)?; 44 | wasm![ 45 | LocalSet(extern_local), 46 | Call(int2string), 47 | LocalGet(extern_local), 48 | Call(string_gt) 49 | ] 50 | } else { 51 | hq_bug!("bad input") 52 | } 53 | } else if IrType::Float.contains(t1) { 54 | if IrType::Float.contains(t2) { 55 | let local1 = func.local(ValType::F64)?; 56 | let local2 = func.local(ValType::F64)?; 57 | wasm![ 58 | LocalSet(local2), 59 | LocalTee(local1), 60 | @isnan(t1), 61 | If(block_type), 62 | LocalGet(local2), 63 | @isnan(t2), 64 | If(block_type), 65 | I32Const(0), // NaN is not greater than NaN 66 | Else, 67 | I32Const(1), // NaN > number always true 68 | End, 69 | Else, 70 | LocalGet(local1), 71 | LocalGet(local2), 72 | F64Gt, 73 | End, 74 | ] 75 | } else if IrType::QuasiInt.contains(t2) { 76 | let local1 = func.local(ValType::F64)?; 77 | let local2 = func.local(ValType::F64)?; 78 | wasm![ 79 | F64ConvertI32S, 80 | LocalSet(local2), 81 | LocalTee(local1), 82 | @isnan(t1), 83 | If(block_type), 84 | I32Const(1), // NaN > number is always true 85 | Else, 86 | LocalGet(local1), 87 | LocalGet(local2), 88 | F64Gt, 89 | End, 90 | ] 91 | } else if IrType::String.contains(t2) { 92 | let float2string = func.registries().external_functions().register( 93 | ("cast", "float2string".into()), 94 | (vec![ValType::F64], vec![ValType::EXTERNREF]), 95 | )?; 96 | let string_gt = func.registries().external_functions().register( 97 | ("operator", "gt_string".into()), 98 | ( 99 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 100 | vec![ValType::I32], 101 | ), 102 | )?; 103 | let extern_local = func.local(ValType::EXTERNREF)?; 104 | wasm![ 105 | LocalSet(extern_local), 106 | Call(float2string), 107 | LocalGet(extern_local), 108 | Call(string_gt) 109 | ] 110 | } else { 111 | hq_bug!("bad input") 112 | } 113 | } else if IrType::String.contains(t1) { 114 | if IrType::QuasiInt.contains(t2) { 115 | // TODO: try converting string to number first 116 | let int2string = func.registries().external_functions().register( 117 | ("cast", "int2string".into()), 118 | (vec![ValType::I32], vec![ValType::EXTERNREF]), 119 | )?; 120 | let string_gt = func.registries().external_functions().register( 121 | ("operator", "gt_string".into()), 122 | ( 123 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 124 | vec![ValType::I32], 125 | ), 126 | )?; 127 | wasm![Call(int2string), Call(string_gt)] 128 | } else if IrType::Float.contains(t2) { 129 | // TODO: try converting string to number first 130 | let float2string = func.registries().external_functions().register( 131 | ("cast", "float2string".into()), 132 | (vec![ValType::F64], vec![ValType::EXTERNREF]), 133 | )?; 134 | let string_gt = func.registries().external_functions().register( 135 | ("operator", "gt_string".into()), 136 | ( 137 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 138 | vec![ValType::I32], 139 | ), 140 | )?; 141 | wasm![Call(float2string), Call(string_gt)] 142 | } else if IrType::String.contains(t2) { 143 | let string_gt = func.registries().external_functions().register( 144 | ("operator", "gt_string".into()), 145 | ( 146 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 147 | vec![ValType::I32], 148 | ), 149 | )?; 150 | wasm![Call(string_gt)] 151 | } else { 152 | hq_bug!("bad input") 153 | } 154 | } else { 155 | hq_bug!("bad input") 156 | }) 157 | } 158 | 159 | pub fn acceptable_inputs() -> Rc<[IrType]> { 160 | Rc::new([IrType::Any, IrType::Any]) 161 | } 162 | 163 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { 164 | Ok(Some(IrType::Boolean)) 165 | } 166 | 167 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 168 | 169 | crate::instructions_test! {tests; operator_lt; t1, t2 ;} 170 | -------------------------------------------------------------------------------- /src/instructions/operator/join.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::HeapType; 2 | 3 | use super::super::prelude::*; 4 | 5 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 6 | hq_assert_eq!(inputs.len(), 2); 7 | let func_index = func.registries().external_functions().register( 8 | ("wasm:js-string", "concat".into()), 9 | ( 10 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 11 | vec![ValType::Ref(RefType { 12 | nullable: false, 13 | heap_type: HeapType::EXTERN, 14 | })], 15 | ), 16 | )?; 17 | Ok(wasm![Call(func_index)]) 18 | } 19 | 20 | pub fn acceptable_inputs() -> Rc<[IrType]> { 21 | Rc::new([IrType::String, IrType::String]) 22 | } 23 | 24 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { 25 | Ok(Some(IrType::String)) 26 | } 27 | 28 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 29 | 30 | crate::instructions_test! {tests; operator_join; t1, t2 ;} 31 | -------------------------------------------------------------------------------- /src/instructions/operator/length.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 1); 5 | let func_index = func.registries().external_functions().register( 6 | ("wasm:js-string", "length".into()), 7 | (vec![ValType::EXTERNREF], vec![ValType::I32]), 8 | )?; 9 | Ok(wasm![Call(func_index)]) 10 | } 11 | 12 | pub fn acceptable_inputs() -> Rc<[IrType]> { 13 | Rc::new([IrType::String]) 14 | } 15 | 16 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { 17 | Ok(Some(IrType::IntPos)) 18 | } 19 | 20 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 21 | 22 | crate::instructions_test! {tests; operator_length; t ;} 23 | -------------------------------------------------------------------------------- /src/instructions/operator/letter_of.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::HeapType; 2 | 3 | use super::super::prelude::*; 4 | 5 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 6 | hq_assert_eq!(inputs.len(), 2); 7 | let func_index = func.registries().external_functions().register( 8 | ("wasm:js-string", "substring".into()), 9 | ( 10 | vec![ValType::EXTERNREF, ValType::I32, ValType::I32], 11 | vec![ValType::Ref(RefType { 12 | nullable: false, 13 | heap_type: HeapType::EXTERN, 14 | })], 15 | ), 16 | )?; 17 | let i32_local = func.local(ValType::I32)?; 18 | let ef_local = func.local(ValType::EXTERNREF)?; 19 | Ok(wasm![ 20 | LocalSet(ef_local), 21 | LocalSet(i32_local), 22 | LocalGet(ef_local), 23 | LocalGet(i32_local), 24 | I32Const(1), 25 | I32Sub, 26 | LocalGet(i32_local), 27 | Call(func_index), 28 | ]) 29 | } 30 | 31 | pub fn acceptable_inputs() -> Rc<[IrType]> { 32 | Rc::new([IrType::Int, IrType::String]) 33 | } 34 | 35 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { 36 | Ok(Some(IrType::String)) 37 | } 38 | 39 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 40 | 41 | crate::instructions_test! {tests; operator_letter_of; t1, t2 ;} 42 | -------------------------------------------------------------------------------- /src/instructions/operator/lt.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::BlockType; 2 | 3 | use super::super::prelude::*; 4 | 5 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 6 | hq_assert_eq!(inputs.len(), 2); 7 | let t1 = inputs[0]; 8 | let t2 = inputs[1]; 9 | let block_type = BlockType::Result(ValType::I32); 10 | Ok(if IrType::QuasiInt.contains(t1) { 11 | if IrType::QuasiInt.contains(t2) { 12 | wasm![I32LtS] 13 | } else if IrType::Float.contains(t2) { 14 | let local1 = func.local(ValType::F64)?; 15 | let local2 = func.local(ValType::F64)?; 16 | wasm![ 17 | LocalSet(local2), 18 | F64ConvertI32S, 19 | LocalSet(local1), 20 | LocalGet(local2), 21 | @isnan(t2), 22 | If(block_type), 23 | I32Const(1), // (number) < NaN always true 24 | Else, 25 | LocalGet(local1), 26 | LocalGet(local2), 27 | F64Lt, 28 | End, 29 | ] 30 | } else if IrType::String.contains(t2) { 31 | // TODO: try converting string to number first (if applicable) 32 | let int2string = func.registries().external_functions().register( 33 | ("cast", "int2string".into()), 34 | (vec![ValType::I32], vec![ValType::EXTERNREF]), 35 | )?; 36 | let string_lt = func.registries().external_functions().register( 37 | ("operator", "lt_string".into()), 38 | ( 39 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 40 | vec![ValType::I32], 41 | ), 42 | )?; 43 | let extern_local = func.local(ValType::EXTERNREF)?; 44 | wasm![ 45 | LocalSet(extern_local), 46 | Call(int2string), 47 | LocalGet(extern_local), 48 | Call(string_lt) 49 | ] 50 | } else { 51 | hq_bug!("bad input") 52 | } 53 | } else if IrType::Float.contains(t1) { 54 | if IrType::Float.contains(t2) { 55 | let local1 = func.local(ValType::F64)?; 56 | let local2 = func.local(ValType::F64)?; 57 | wasm![ 58 | LocalSet(local2), 59 | LocalTee(local1), 60 | @isnan(t1), 61 | If(block_type), 62 | I32Const(0), // NaN < number always false 63 | Else, 64 | LocalGet(local2), 65 | @isnan(t2), 66 | If(block_type), 67 | I32Const(1), // number < NaN always true 68 | Else, 69 | LocalGet(local1), 70 | LocalGet(local2), 71 | F64Lt, 72 | End, 73 | End, 74 | ] 75 | } else if IrType::QuasiInt.contains(t2) { 76 | let local1 = func.local(ValType::F64)?; 77 | let local2 = func.local(ValType::F64)?; 78 | wasm![ 79 | F64ConvertI32S, 80 | LocalSet(local2), 81 | LocalTee(local1), 82 | @isnan(t1), 83 | If(block_type), 84 | // if t2 is NaN, NaN < NaN is false 85 | // if t2 is a number, NaN < (number) is false, since scratch converts to 86 | // a string if one or both inputs are NaN, and compares the strings; 87 | // numbers (and -) come before N. 88 | I32Const(0), 89 | Else, 90 | LocalGet(local1), 91 | LocalGet(local2), 92 | F64Lt, 93 | End, 94 | ] 95 | } else if IrType::String.contains(t2) { 96 | let float2string = func.registries().external_functions().register( 97 | ("cast", "float2string".into()), 98 | (vec![ValType::F64], vec![ValType::EXTERNREF]), 99 | )?; 100 | let string_lt = func.registries().external_functions().register( 101 | ("operator", "lt_string".into()), 102 | ( 103 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 104 | vec![ValType::I32], 105 | ), 106 | )?; 107 | let extern_local = func.local(ValType::EXTERNREF)?; 108 | wasm![ 109 | LocalSet(extern_local), 110 | Call(float2string), 111 | LocalGet(extern_local), 112 | Call(string_lt) 113 | ] 114 | } else { 115 | hq_bug!("bad input") 116 | } 117 | } else if IrType::String.contains(t1) { 118 | if IrType::QuasiInt.contains(t2) { 119 | // TODO: try converting string to number first 120 | let int2string = func.registries().external_functions().register( 121 | ("cast", "int2string".into()), 122 | (vec![ValType::I32], vec![ValType::EXTERNREF]), 123 | )?; 124 | let string_lt = func.registries().external_functions().register( 125 | ("operator", "lt_string".into()), 126 | ( 127 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 128 | vec![ValType::I32], 129 | ), 130 | )?; 131 | wasm![Call(int2string), Call(string_lt)] 132 | } else if IrType::Float.contains(t2) { 133 | // TODO: try converting string to number first 134 | let float2string = func.registries().external_functions().register( 135 | ("cast", "float2string".into()), 136 | (vec![ValType::F64], vec![ValType::EXTERNREF]), 137 | )?; 138 | let string_lt = func.registries().external_functions().register( 139 | ("operator", "lt_string".into()), 140 | ( 141 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 142 | vec![ValType::I32], 143 | ), 144 | )?; 145 | wasm![Call(float2string), Call(string_lt)] 146 | } else if IrType::String.contains(t2) { 147 | let string_lt = func.registries().external_functions().register( 148 | ("operator", "lt_string".into()), 149 | ( 150 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 151 | vec![ValType::I32], 152 | ), 153 | )?; 154 | wasm![Call(string_lt)] 155 | } else { 156 | hq_bug!("bad input") 157 | } 158 | } else { 159 | hq_bug!("bad input") 160 | }) 161 | } 162 | 163 | pub fn acceptable_inputs() -> Rc<[IrType]> { 164 | Rc::new([IrType::Any, IrType::Any]) 165 | } 166 | 167 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { 168 | Ok(Some(IrType::Boolean)) 169 | } 170 | 171 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 172 | 173 | crate::instructions_test! {tests; operator_lt; t1, t2 ;} 174 | -------------------------------------------------------------------------------- /src/instructions/operator/multiply.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 2); 5 | let t1 = inputs[0]; 6 | let t2 = inputs[1]; 7 | Ok(if IrType::QuasiInt.contains(t1) { 8 | if IrType::QuasiInt.contains(t2) { 9 | wasm![I32Mul] 10 | } else if IrType::Float.contains(t2) { 11 | let f64_local = func.local(ValType::F64)?; 12 | wasm![ 13 | LocalSet(f64_local), 14 | F64ConvertI32S, 15 | LocalGet(f64_local), 16 | @nanreduce(t2), 17 | F64Mul, 18 | ] 19 | } else { 20 | hq_bug!("bad input") 21 | } 22 | } else if IrType::Float.contains(t1) { 23 | if IrType::Float.contains(t2) { 24 | let f64_local = func.local(ValType::F64)?; 25 | wasm![ 26 | @nanreduce(t2), 27 | LocalSet(f64_local), 28 | @nanreduce(t1), 29 | LocalGet(f64_local), 30 | F64Mul 31 | ] 32 | } else if IrType::QuasiInt.contains(t2) { 33 | let i32_local = func.local(ValType::I32)?; 34 | wasm![ 35 | LocalSet(i32_local), 36 | @nanreduce(t1), 37 | LocalGet(i32_local), 38 | F64ConvertI32S, 39 | F64Mul 40 | ] 41 | } else { 42 | hq_bug!("bad input") 43 | } 44 | } else { 45 | hq_bug!("bad input") 46 | }) 47 | } 48 | 49 | pub fn acceptable_inputs() -> Rc<[IrType]> { 50 | Rc::new([IrType::Number, IrType::Number]) 51 | } 52 | 53 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { 54 | hq_assert_eq!(inputs.len(), 2); 55 | let t1 = inputs[0]; 56 | let t2 = inputs[1]; 57 | let maybe_positive = (t1.maybe_positive() && t2.maybe_positive()) 58 | || (t2.maybe_negative() && t1.maybe_negative()); 59 | let maybe_negative = (t1.maybe_negative() && t2.maybe_positive()) 60 | || (t2.maybe_negative() && t1.maybe_positive()); 61 | let maybe_zero = t1.maybe_zero() || t1.maybe_nan() || t2.maybe_zero() || t2.maybe_nan(); 62 | let maybe_nan = (IrType::FloatInf.intersects(t1) && (t2.maybe_zero() || t2.maybe_nan())) 63 | || (IrType::FloatInf.intersects(t2) && (t1.maybe_zero() || t1.maybe_nan())); 64 | let maybe_inf = IrType::FloatInf.intersects(t1.or(t2)); 65 | Ok(Some(if IrType::QuasiInt.contains(t1.or(t2)) { 66 | IrType::none_if_false(maybe_positive, IrType::IntPos) 67 | .or(IrType::none_if_false(maybe_negative, IrType::IntNeg)) 68 | .or(IrType::none_if_false(maybe_zero, IrType::IntZero)) 69 | } else if (IrType::QuasiInt.contains(t1) && IrType::Float.contains(t2)) 70 | || (IrType::QuasiInt.contains(t2) && IrType::Float.contains(t1)) 71 | || IrType::Float.contains(t1.or(t2)) 72 | { 73 | IrType::none_if_false(maybe_positive, IrType::FloatPos) 74 | .or(IrType::none_if_false(maybe_negative, IrType::FloatNeg)) 75 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) 76 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) 77 | .or(IrType::none_if_false(maybe_inf, IrType::FloatInf)) 78 | } else { 79 | // there is a boxed type somewhere 80 | // TODO: can these bounds be tightened? e.g. it may only be a positive int or negative float? 81 | IrType::none_if_false(maybe_positive, IrType::FloatPos.or(IrType::IntPos)) 82 | .or(IrType::none_if_false( 83 | maybe_negative, 84 | IrType::FloatNeg.or(IrType::IntNeg), 85 | )) 86 | .or(IrType::none_if_false( 87 | maybe_zero, 88 | IrType::FloatZero.or(IrType::IntZero), 89 | )) 90 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) 91 | .or(IrType::none_if_false(maybe_inf, IrType::FloatInf)) 92 | })) 93 | } 94 | 95 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 96 | 97 | crate::instructions_test! {tests; operator_multiply; t1, t2 ;} 98 | -------------------------------------------------------------------------------- /src/instructions/operator/not.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 4 | Ok(wasm![I32Eqz]) 5 | } 6 | 7 | pub fn acceptable_inputs() -> Rc<[IrType]> { 8 | Rc::new([IrType::Boolean]) 9 | } 10 | 11 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { 12 | Ok(Some(IrType::Boolean)) 13 | } 14 | 15 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 16 | 17 | crate::instructions_test! {tests; operator_not; t ;} 18 | -------------------------------------------------------------------------------- /src/instructions/operator/or.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 4 | Ok(wasm![I32Or]) 5 | } 6 | 7 | pub fn acceptable_inputs() -> Rc<[IrType]> { 8 | Rc::new([IrType::Boolean, IrType::Boolean]) 9 | } 10 | 11 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { 12 | Ok(Some(IrType::Boolean)) 13 | } 14 | 15 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 16 | 17 | crate::instructions_test! {tests; operator_or; t1, t2 ;} 18 | -------------------------------------------------------------------------------- /src/instructions/operator/subtract.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 2); 5 | let t1 = inputs[0]; 6 | let t2 = inputs[1]; 7 | Ok(if IrType::QuasiInt.contains(t1) { 8 | if IrType::QuasiInt.contains(t2) { 9 | wasm![I32Sub] 10 | } else if IrType::Float.contains(t2) { 11 | let f64_local = func.local(ValType::F64)?; 12 | wasm![ 13 | LocalSet(f64_local), 14 | F64ConvertI32S, 15 | LocalGet(f64_local), 16 | @nanreduce(t2), 17 | F64Sub, 18 | ] 19 | } else { 20 | hq_bug!("bad input") 21 | } 22 | } else if IrType::Float.contains(t1) { 23 | if IrType::Float.contains(t2) { 24 | let f64_local = func.local(ValType::F64)?; 25 | wasm![ 26 | @nanreduce(t2), 27 | LocalSet(f64_local), 28 | @nanreduce(t1), 29 | LocalGet(f64_local), 30 | F64Sub 31 | ] 32 | } else if IrType::QuasiInt.contains(t2) { 33 | let i32_local = func.local(ValType::I32)?; 34 | wasm![ 35 | LocalSet(i32_local), 36 | @nanreduce(t1), 37 | LocalGet(i32_local), 38 | F64ConvertI32S, 39 | F64Sub 40 | ] 41 | } else { 42 | hq_bug!("bad input") 43 | } 44 | } else { 45 | hq_bug!("bad input") 46 | }) 47 | } 48 | 49 | pub fn acceptable_inputs() -> Rc<[IrType]> { 50 | Rc::new([IrType::Number, IrType::Number]) 51 | } 52 | 53 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { 54 | hq_assert!(inputs.len() == 2); 55 | let t1 = inputs[0]; 56 | let t2 = inputs[1]; 57 | let maybe_positive = t1.maybe_positive() || t2.maybe_negative(); 58 | let maybe_negative = t1.maybe_negative() || t2.maybe_positive(); 59 | let maybe_zero = ((t1.maybe_zero() || t1.maybe_nan()) && (t2.maybe_zero() || t2.maybe_nan())) 60 | || (t1.maybe_positive() && t2.maybe_positive()) 61 | || (t1.maybe_negative() && t2.maybe_negative()); 62 | let maybe_nan = 63 | IrType::FloatPosInf.intersects(t1.and(t2)) || IrType::FloatNegInf.intersects(t1.and(t2)); 64 | Ok(Some(if IrType::QuasiInt.contains(t1.or(t2)) { 65 | IrType::none_if_false(maybe_positive, IrType::IntPos) 66 | .or(IrType::none_if_false(maybe_negative, IrType::IntNeg)) 67 | .or(IrType::none_if_false(maybe_zero, IrType::IntZero)) 68 | } else if (IrType::QuasiInt.contains(t1) && IrType::Float.contains(t2)) 69 | || (IrType::QuasiInt.contains(t2) && IrType::Float.contains(t1)) 70 | || IrType::Float.contains(t1.or(t2)) 71 | { 72 | IrType::none_if_false(maybe_positive, IrType::FloatPos) 73 | .or(IrType::none_if_false(maybe_negative, IrType::FloatNeg)) 74 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) 75 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) 76 | } else { 77 | // there is a boxed type somewhere 78 | // TODO: can these bounds be tightened? e.g. it may only be a positive int or negative float? 79 | IrType::none_if_false(maybe_positive, IrType::FloatPos.or(IrType::IntPos)) 80 | .or(IrType::none_if_false( 81 | maybe_negative, 82 | IrType::FloatNeg.or(IrType::IntNeg), 83 | )) 84 | .or(IrType::none_if_false( 85 | maybe_zero, 86 | IrType::FloatZero.or(IrType::IntZero), 87 | )) 88 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) 89 | })) 90 | } 91 | 92 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 93 | 94 | crate::instructions_test! {tests; operator_subtract; t1, t2 ;} 95 | -------------------------------------------------------------------------------- /src/instructions/procedures.rs: -------------------------------------------------------------------------------- 1 | pub mod argument; 2 | pub mod call_warp; 3 | -------------------------------------------------------------------------------- /src/instructions/procedures/argument.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::wasm::{StepFunc, WasmProject}; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct Fields(pub usize, pub IrType); 6 | 7 | pub fn wasm( 8 | func: &StepFunc, 9 | _inputs: Rc<[IrType]>, 10 | Fields(index, ty): &Fields, 11 | ) -> HQResult> { 12 | hq_assert!( 13 | WasmProject::ir_type_to_wasm(*ty)? 14 | == *func.params().get(*index).ok_or_else(|| make_hq_bug!( 15 | "proc argument index was out of bounds for func params" 16 | ))?, 17 | "proc argument type didn't match that of the corresponding function param" 18 | ); 19 | Ok(wasm![LocalGet((*index).try_into().map_err( 20 | |_| make_hq_bug!("argument index out of bounds") 21 | )?)]) 22 | } 23 | 24 | pub fn acceptable_inputs(_: &Fields) -> Rc<[IrType]> { 25 | Rc::from([]) 26 | } 27 | 28 | pub fn output_type(_inputs: Rc<[IrType]>, &Fields(_, ty): &Fields) -> HQResult> { 29 | Ok(Some(ty)) 30 | } 31 | 32 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 33 | -------------------------------------------------------------------------------- /src/instructions/procedures/call_warp.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::ir::Proc; 3 | use crate::wasm::StepFunc; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct Fields { 7 | pub proc: Rc, 8 | } 9 | 10 | pub fn wasm( 11 | func: &StepFunc, 12 | _inputs: Rc<[IrType]>, 13 | Fields { proc }: &Fields, 14 | ) -> HQResult> { 15 | Ok(wasm![ 16 | LocalGet((func.params().len() - 1).try_into().map_err(|_| make_hq_bug!("local index out of bounds"))?), 17 | #LazyWarpedProcCall(Rc::clone(proc)) 18 | ]) 19 | } 20 | 21 | pub fn acceptable_inputs(Fields { proc }: &Fields) -> Rc<[IrType]> { 22 | Rc::from(proc.context().arg_types()) 23 | } 24 | 25 | pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { 26 | Ok(None) 27 | } 28 | 29 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 30 | -------------------------------------------------------------------------------- /src/instructions/sensing.rs: -------------------------------------------------------------------------------- 1 | pub mod dayssince2000; 2 | -------------------------------------------------------------------------------- /src/instructions/sensing/dayssince2000.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 0); 5 | let func_index = func.registries().external_functions().register( 6 | ("sensing", "dayssince2000".into()), 7 | (vec![], vec![ValType::F64]), 8 | )?; 9 | Ok(wasm![Call(func_index)]) 10 | } 11 | 12 | pub fn acceptable_inputs() -> Rc<[IrType]> { 13 | Rc::new([]) 14 | } 15 | 16 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { 17 | Ok(Some(IrType::Float)) 18 | } 19 | 20 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 21 | 22 | crate::instructions_test! {tests; sensing_dayssince2000; ;} 23 | -------------------------------------------------------------------------------- /src/ir.rs: -------------------------------------------------------------------------------- 1 | mod blocks; 2 | mod context; 3 | mod event; 4 | mod proc; 5 | mod project; 6 | mod step; 7 | mod target; 8 | mod thread; 9 | mod types; 10 | mod variable; 11 | 12 | use context::StepContext; 13 | pub use event::Event; 14 | pub use proc::{PartialStep, Proc, ProcContext}; 15 | pub use project::IrProject; 16 | pub use step::Step; 17 | use target::Target; 18 | use thread::Thread; 19 | pub use types::Type; 20 | pub use variable::{RcVar, Variable}; 21 | -------------------------------------------------------------------------------- /src/ir/context.rs: -------------------------------------------------------------------------------- 1 | use super::{IrProject, ProcContext, Target}; 2 | use crate::prelude::*; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct StepContext { 6 | pub target: Weak, 7 | /// whether or not the current thread is warped. this may be because the current 8 | /// procedure is warped, or because a procedure higher up the call stack was warped. 9 | pub warp: bool, 10 | pub proc_context: Option, 11 | /// enables certain behaviours such as `console.log` say/think rather than 12 | /// displaying in bubbles 13 | pub debug: bool, 14 | } 15 | 16 | impl StepContext { 17 | pub fn project(&self) -> HQResult> { 18 | Ok(self 19 | .target 20 | .upgrade() 21 | .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? 22 | .project()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ir/event.rs: -------------------------------------------------------------------------------- 1 | // Ord is required to be used in a BTreeMap; Ord requires PartialOrd, Eq and PartialEq 2 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 3 | pub enum Event { 4 | FlagCLicked, 5 | } 6 | -------------------------------------------------------------------------------- /src/ir/project.rs: -------------------------------------------------------------------------------- 1 | use super::proc::{procs_from_target, ProcMap}; 2 | use super::variable::variables_from_target; 3 | use super::{Step, Target, Thread, Variable}; 4 | use crate::prelude::*; 5 | use crate::sb3::Sb3Project; 6 | 7 | pub type StepSet = IndexSet>; 8 | 9 | #[derive(Clone, Debug)] 10 | pub struct IrProject { 11 | threads: RefCell>, 12 | steps: RefCell, 13 | global_variables: BTreeMap, Rc>, 14 | targets: RefCell, Rc>>, 15 | } 16 | 17 | impl IrProject { 18 | pub const fn threads(&self) -> &RefCell> { 19 | &self.threads 20 | } 21 | 22 | pub const fn steps(&self) -> &RefCell { 23 | &self.steps 24 | } 25 | 26 | pub const fn targets(&self) -> &RefCell, Rc>> { 27 | &self.targets 28 | } 29 | 30 | pub const fn global_variables(&self) -> &BTreeMap, Rc> { 31 | &self.global_variables 32 | } 33 | 34 | pub fn new(global_variables: BTreeMap, Rc>) -> Self { 35 | Self { 36 | threads: RefCell::new(Box::new([])), 37 | steps: RefCell::new(IndexSet::default()), 38 | global_variables, 39 | targets: RefCell::new(IndexMap::default()), 40 | } 41 | } 42 | 43 | pub fn register_step(&self, step: Rc) -> HQResult<()> { 44 | self.steps() 45 | .try_borrow_mut() 46 | .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? 47 | .insert(step); 48 | Ok(()) 49 | } 50 | } 51 | 52 | impl TryFrom for Rc { 53 | type Error = HQError; 54 | 55 | fn try_from(sb3: Sb3Project) -> HQResult { 56 | let global_variables = variables_from_target( 57 | sb3.targets 58 | .iter() 59 | .find(|target| target.is_stage) 60 | .ok_or_else(|| make_hq_bad_proj!("missing stage target"))?, 61 | ); 62 | 63 | let project = Self::new(IrProject::new(global_variables)); 64 | 65 | let (threads_vec, targets): (Vec<_>, Vec<_>) = sb3 66 | .targets 67 | .iter() 68 | .enumerate() 69 | .map(|(index, target)| { 70 | let variables = variables_from_target(target); 71 | let procedures = RefCell::new(ProcMap::new()); 72 | let ir_target = Rc::new(Target::new( 73 | target.is_stage, 74 | variables, 75 | Self::downgrade(&project), 76 | procedures, 77 | index 78 | .try_into() 79 | .map_err(|_| make_hq_bug!("target index out of bounds"))?, 80 | )); 81 | procs_from_target(target, &ir_target)?; 82 | let blocks = &target.blocks; 83 | let threads = blocks 84 | .iter() 85 | .filter_map(|(id, block)| { 86 | let thread = Thread::try_from_top_block( 87 | block, 88 | blocks, 89 | Rc::downgrade(&ir_target), 90 | &Self::downgrade(&project), 91 | target.comments.clone().iter().any(|(_id, comment)| { 92 | matches!(comment.block_id.clone(), Some(d) if &d == id) 93 | && *comment.text.clone() == *"hq-dbg" 94 | }), 95 | ) 96 | .transpose()?; 97 | Some(thread) 98 | }) 99 | .collect::>>()?; 100 | Ok((threads, (target.name.clone(), ir_target))) 101 | }) 102 | .collect::>>()? 103 | .iter() 104 | .cloned() 105 | .unzip(); 106 | let threads = threads_vec.into_iter().flatten().collect::>(); 107 | *project 108 | .threads 109 | .try_borrow_mut() 110 | .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? = threads; 111 | *project 112 | .targets 113 | .try_borrow_mut() 114 | .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? = 115 | targets.into_iter().collect(); 116 | Ok(project) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/ir/step.rs: -------------------------------------------------------------------------------- 1 | use super::blocks::{self, NextBlocks}; 2 | use super::{IrProject, StepContext}; 3 | use crate::instructions::{HqYieldFields, IrOpcode, YieldMode}; 4 | use crate::prelude::*; 5 | use crate::sb3::{Block, BlockMap}; 6 | use uuid::Uuid; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct Step { 10 | context: StepContext, 11 | opcodes: RefCell>, 12 | used_non_inline: RefCell, 13 | inline: RefCell, 14 | /// used for `Hash`. Should be obtained from a block in the `Step` where possible. 15 | id: Box, 16 | project: Weak, 17 | } 18 | 19 | impl PartialEq for Step { 20 | fn eq(&self, other: &Self) -> bool { 21 | self.id == other.id 22 | } 23 | } 24 | 25 | impl Eq for Step {} 26 | 27 | impl Step { 28 | pub const fn context(&self) -> &StepContext { 29 | &self.context 30 | } 31 | 32 | pub const fn opcodes(&self) -> &RefCell> { 33 | &self.opcodes 34 | } 35 | 36 | pub const fn used_non_inline(&self) -> &RefCell { 37 | &self.used_non_inline 38 | } 39 | 40 | pub const fn inline(&self) -> &RefCell { 41 | &self.inline 42 | } 43 | 44 | pub fn project(&self) -> Weak { 45 | Weak::clone(&self.project) 46 | } 47 | 48 | pub fn new( 49 | id: Option>, 50 | context: StepContext, 51 | opcodes: Vec, 52 | project: Weak, 53 | ) -> Self { 54 | Self { 55 | id: id.unwrap_or_else(|| Uuid::new_v4().to_string().into()), 56 | context, 57 | opcodes: RefCell::new(opcodes), 58 | used_non_inline: RefCell::new(false), 59 | inline: RefCell::new(false), 60 | project, 61 | } 62 | } 63 | 64 | pub fn new_rc( 65 | id: Option>, 66 | context: StepContext, 67 | opcodes: Vec, 68 | project: &Weak, 69 | ) -> HQResult> { 70 | let step = Rc::new(Self::new(id, context, opcodes, Weak::clone(project))); 71 | project 72 | .upgrade() 73 | .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? 74 | .steps() 75 | .try_borrow_mut() 76 | .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? 77 | .insert(Rc::clone(&step)); 78 | Ok(step) 79 | } 80 | 81 | pub fn new_empty() -> Self { 82 | Self { 83 | id: "".into(), 84 | context: StepContext { 85 | target: Weak::new(), 86 | proc_context: None, 87 | warp: false, // this is a fairly arbitrary choice and doesn't matter at all 88 | debug: false, 89 | }, 90 | opcodes: RefCell::new(vec![]), 91 | used_non_inline: RefCell::new(false), 92 | inline: RefCell::new(false), 93 | project: Weak::new(), 94 | } 95 | } 96 | 97 | pub fn new_terminating(context: StepContext, project: &Weak) -> HQResult> { 98 | const ID: &str = "__terminating_step_hopefully_this_id_wont_cause_any_clashes"; 99 | let step = Rc::new(Self { 100 | id: ID.into(), 101 | context, 102 | opcodes: RefCell::new(vec![IrOpcode::hq_yield(HqYieldFields { 103 | mode: YieldMode::None, 104 | })]), 105 | used_non_inline: RefCell::new(false), 106 | inline: RefCell::new(false), 107 | project: Weak::clone(project), 108 | }); 109 | project 110 | .upgrade() 111 | .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? 112 | .steps() 113 | .try_borrow_mut() 114 | .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? 115 | .insert(Rc::clone(&step)); 116 | Ok(step) 117 | } 118 | 119 | pub fn opcodes_mut(&self) -> HQResult>> { 120 | self.opcodes 121 | .try_borrow_mut() 122 | .map_err(|_| make_hq_bug!("couldn't mutably borrow cell")) 123 | } 124 | 125 | pub fn from_block( 126 | block: &Block, 127 | block_id: Box, 128 | blocks: &BlockMap, 129 | context: &StepContext, 130 | project: &Weak, 131 | final_next_blocks: NextBlocks, 132 | ) -> HQResult> { 133 | if let Some(existing_step) = project 134 | .upgrade() 135 | .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? 136 | .steps() 137 | .try_borrow() 138 | .map_err(|_| make_hq_bug!("couldn't immutably borrow cell"))? 139 | .iter() 140 | .find(|step| step.id == block_id) 141 | { 142 | crate::log("step from_block already exists!"); 143 | return Ok(Rc::clone(existing_step)); 144 | } 145 | let step = Rc::new(Self::new( 146 | Some(block_id), 147 | context.clone(), 148 | blocks::from_block(block, blocks, context, project, final_next_blocks)?, 149 | Weak::clone(project), 150 | )); 151 | project 152 | .upgrade() 153 | .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? 154 | .steps() 155 | .try_borrow_mut() 156 | .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? 157 | .insert(Rc::clone(&step)); 158 | 159 | Ok(step) 160 | } 161 | 162 | pub fn make_used_non_inline(&self) -> HQResult<()> { 163 | if *self.used_non_inline.try_borrow()? { 164 | return Ok(()); 165 | } 166 | *self 167 | .used_non_inline 168 | .try_borrow_mut() 169 | .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? = true; 170 | Ok(()) 171 | } 172 | 173 | pub fn make_inlined(&self) -> HQResult<()> { 174 | if *self.inline.try_borrow()? { 175 | return Ok(()); 176 | } 177 | *self 178 | .inline 179 | .try_borrow_mut() 180 | .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? = true; 181 | Ok(()) 182 | } 183 | } 184 | 185 | impl core::hash::Hash for Step { 186 | fn hash(&self, state: &mut H) { 187 | self.id.hash(state); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/ir/target.rs: -------------------------------------------------------------------------------- 1 | use super::{proc::Proc, IrProject, Variable}; 2 | use crate::prelude::*; 3 | use core::cell::{Ref, RefMut}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Target { 7 | is_stage: bool, 8 | variables: BTreeMap, Rc>, 9 | project: Weak, 10 | procedures: RefCell, Rc>>, 11 | index: u32, 12 | } 13 | 14 | impl Target { 15 | pub const fn is_stage(&self) -> bool { 16 | self.is_stage 17 | } 18 | 19 | pub const fn variables(&self) -> &BTreeMap, Rc> { 20 | &self.variables 21 | } 22 | 23 | pub fn project(&self) -> Weak { 24 | Weak::clone(&self.project) 25 | } 26 | 27 | pub fn procedures(&self) -> HQResult, Rc>>> { 28 | Ok(self.procedures.try_borrow()?) 29 | } 30 | 31 | pub fn procedures_mut(&self) -> HQResult, Rc>>> { 32 | Ok(self.procedures.try_borrow_mut()?) 33 | } 34 | 35 | pub const fn index(&self) -> u32 { 36 | self.index 37 | } 38 | 39 | pub const fn new( 40 | is_stage: bool, 41 | variables: BTreeMap, Rc>, 42 | project: Weak, 43 | procedures: RefCell, Rc>>, 44 | index: u32, 45 | ) -> Self { 46 | Self { 47 | is_stage, 48 | variables, 49 | project, 50 | procedures, 51 | index, 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ir/thread.rs: -------------------------------------------------------------------------------- 1 | use super::blocks::NextBlocks; 2 | use super::{Event, IrProject, Step, StepContext, Target}; 3 | use crate::prelude::*; 4 | use crate::sb3::{Block, BlockMap, BlockOpcode}; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct Thread { 8 | event: Event, 9 | first_step: Rc, 10 | target: Weak, 11 | } 12 | 13 | impl Thread { 14 | pub const fn event(&self) -> Event { 15 | self.event 16 | } 17 | 18 | pub const fn first_step(&self) -> &Rc { 19 | &self.first_step 20 | } 21 | 22 | pub fn target(&self) -> Weak { 23 | Weak::clone(&self.target) 24 | } 25 | 26 | /// tries to construct a thread from a top-level block. 27 | /// Returns Ok(None) if the top-level block is not a valid event or if there is no next block. 28 | pub fn try_from_top_block( 29 | block: &Block, 30 | blocks: &BlockMap, 31 | target: Weak, 32 | project: &Weak, 33 | debug: bool, 34 | ) -> HQResult> { 35 | let Some(block_info) = block.block_info() else { 36 | return Ok(None); 37 | }; 38 | #[expect(clippy::wildcard_enum_match_arm, reason = "too many variants to match")] 39 | let event = match block_info.opcode { 40 | BlockOpcode::event_whenflagclicked => Event::FlagCLicked, 41 | BlockOpcode::event_whenbackdropswitchesto 42 | | BlockOpcode::event_whenbroadcastreceived 43 | | BlockOpcode::event_whengreaterthan 44 | | BlockOpcode::event_whenkeypressed 45 | | BlockOpcode::event_whenstageclicked 46 | | BlockOpcode::event_whenthisspriteclicked 47 | | BlockOpcode::event_whentouchingobject => { 48 | hq_todo!("unimplemented event {:?}", block_info.opcode) 49 | } 50 | _ => return Ok(None), 51 | }; 52 | let Some(next_id) = &block_info.next else { 53 | return Ok(None); 54 | }; 55 | let next = blocks 56 | .get(next_id) 57 | .ok_or_else(|| make_hq_bug!("block not found in BlockMap"))?; 58 | let first_step = Step::from_block( 59 | next, 60 | next_id.clone(), 61 | blocks, 62 | &StepContext { 63 | target: Weak::clone(&target), 64 | proc_context: None, 65 | warp: false, // steps from top blocks are never warped 66 | debug, 67 | }, 68 | project, 69 | NextBlocks::new(true), 70 | )?; 71 | Ok(Some(Self { 72 | event, 73 | first_step, 74 | target, 75 | })) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/ir/types.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use bitmask_enum::bitmask; 3 | 4 | /// a bitmask of possible IR types 5 | #[bitmask(u32)] 6 | #[bitmask_config(vec_debug, flags_iter)] 7 | pub enum Type { 8 | IntZero, 9 | IntPos, 10 | IntNeg, 11 | IntNonZero = Self::IntPos.or(Self::IntNeg).bits, 12 | Int = Self::IntNonZero.or(Self::IntZero).bits, 13 | 14 | FloatPosZero, 15 | FloatNegZero, 16 | FloatZero = Self::FloatPosZero.or(Self::FloatNegZero).bits, 17 | 18 | FloatPosInt, 19 | FloatPosFrac, 20 | FloatPosReal = Self::FloatPosInt.or(Self::FloatPosFrac).bits, 21 | 22 | FloatNegInt, 23 | FloatNegFrac, 24 | FloatNegReal = Self::FloatNegInt.or(Self::FloatNegFrac).bits, 25 | 26 | FloatPosInf, 27 | FloatNegInf, 28 | FloatInf = Self::FloatPosInf.or(Self::FloatNegInf).bits, 29 | 30 | FloatNan, 31 | 32 | FloatPos = Self::FloatPosReal.or(Self::FloatPosInf).bits, 33 | FloatNeg = Self::FloatNegReal.or(Self::FloatNegInf).bits, 34 | 35 | FloatPosWhole = Self::FloatPosInt.or(Self::FloatPosZero).bits, 36 | FloatNegWhole = Self::FloatNegInt.or(Self::FloatNegZero).bits, 37 | 38 | FloatInt = Self::FloatPosWhole.or(Self::FloatNegWhole).bits, 39 | FloatFrac = Self::FloatPosFrac.or(Self::FloatNegFrac).bits, 40 | FloatReal = Self::FloatInt.or(Self::FloatFrac).bits, 41 | FloatNotNan = Self::FloatReal.or(Self::FloatInf).bits, 42 | Float = Self::FloatNotNan.or(Self::FloatNan).bits, 43 | 44 | BooleanTrue, 45 | BooleanFalse, 46 | Boolean = Self::BooleanTrue.or(Self::BooleanFalse).bits, 47 | 48 | QuasiInt = Self::Int.or(Self::Boolean).bits, 49 | 50 | Number = Self::QuasiInt.or(Self::Float).bits, 51 | 52 | StringNumber, // a string which can be interpreted as a non-nan number 53 | StringBoolean, // "true" or "false" 54 | StringNan, // some other string which can only be interpreted as NaN 55 | String = Self::StringNumber 56 | .or(Self::StringBoolean) 57 | .or(Self::StringNan) 58 | .bits, 59 | 60 | QuasiBoolean = Self::Boolean.or(Self::StringBoolean).bits, 61 | QuasiNumber = Self::Number.or(Self::StringNumber).bits, 62 | 63 | Any = Self::String.or(Self::Number).bits, 64 | 65 | Color, 66 | } 67 | impl Type { 68 | pub const BASE_TYPES: [Self; 3] = [Self::String, Self::QuasiInt, Self::Float]; 69 | 70 | pub fn is_base_type(self) -> bool { 71 | (!self.is_none()) && Self::BASE_TYPES.iter().any(|ty| ty.contains(self)) 72 | } 73 | 74 | pub fn base_type(self) -> Option { 75 | if !self.is_base_type() { 76 | return None; 77 | } 78 | Self::BASE_TYPES 79 | .iter() 80 | .copied() 81 | .find(|&ty| ty.contains(self)) 82 | } 83 | 84 | pub fn base_types(self) -> Box> { 85 | if self.is_none() { 86 | return Box::new(core::iter::empty()); 87 | } 88 | Box::new( 89 | Self::BASE_TYPES 90 | .iter() 91 | .filter(move |ty| ty.intersects(self)) 92 | .copied(), 93 | ) 94 | } 95 | 96 | pub const fn maybe_positive(self) -> bool { 97 | self.contains(Self::IntPos) 98 | || self.intersects(Self::FloatPos) 99 | || self.contains(Self::BooleanTrue) 100 | } 101 | 102 | pub const fn maybe_negative(self) -> bool { 103 | self.contains(Self::IntNeg) || self.intersects(Self::FloatNeg) 104 | } 105 | 106 | pub const fn maybe_zero(self) -> bool { 107 | self.contains(Self::IntZero) 108 | || self.contains(Self::BooleanFalse) 109 | || self.intersects(Self::FloatZero) 110 | } 111 | 112 | pub const fn maybe_nan(self) -> bool { 113 | self.intersects(Self::FloatNan) || self.contains(Self::StringNan) 114 | } 115 | 116 | pub const fn none_if_false(condition: bool, if_true: Self) -> Self { 117 | if condition { 118 | if_true 119 | } else { 120 | Self::none() 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/ir/variable.rs: -------------------------------------------------------------------------------- 1 | use super::Type; 2 | use crate::{ 3 | prelude::*, 4 | sb3::{Target as Sb3Target, VarVal}, 5 | }; 6 | use core::cell::Ref; 7 | use core::hash::{Hash, Hasher}; 8 | 9 | #[derive(Debug, Clone, PartialEq)] 10 | pub struct Variable { 11 | possible_types: RefCell, 12 | initial_value: VarVal, 13 | local: bool, 14 | } 15 | 16 | impl Variable { 17 | pub const fn new(ty: Type, initial_value: VarVal, local: bool) -> Self { 18 | Self { 19 | possible_types: RefCell::new(ty), 20 | initial_value, 21 | local, 22 | } 23 | } 24 | 25 | pub fn add_type(&self, ty: Type) { 26 | let current = *self.possible_types.borrow(); 27 | *self.possible_types.borrow_mut() = current.or(ty); 28 | } 29 | 30 | pub fn possible_types(&self) -> Ref { 31 | self.possible_types.borrow() 32 | } 33 | 34 | pub const fn initial_value(&self) -> &VarVal { 35 | &self.initial_value 36 | } 37 | 38 | pub const fn local(&self) -> bool { 39 | self.local 40 | } 41 | } 42 | 43 | #[derive(Clone, Debug)] 44 | pub struct RcVar(pub Rc); 45 | 46 | impl PartialEq for RcVar { 47 | fn eq(&self, other: &Self) -> bool { 48 | Rc::ptr_eq(&self.0, &other.0) 49 | } 50 | } 51 | 52 | impl Eq for RcVar {} 53 | 54 | impl Hash for RcVar { 55 | fn hash(&self, state: &mut H) { 56 | core::ptr::hash(Rc::as_ptr(&self.0), state); 57 | } 58 | } 59 | 60 | pub fn variables_from_target(target: &Sb3Target) -> BTreeMap, Rc> { 61 | target 62 | .variables 63 | .iter() 64 | .map(|(id, var_info)| { 65 | ( 66 | id.clone(), 67 | Rc::new(Variable::new( 68 | Type::none(), 69 | #[expect(clippy::unwrap_used, reason = "this field exists on all variants")] 70 | var_info.get_1().unwrap().clone(), 71 | false, 72 | )), 73 | ) 74 | }) 75 | .collect() 76 | } 77 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(stmt_expr_attributes)] 2 | #![feature(if_let_guard)] 3 | #![doc(html_logo_url = "https://hyperquark.github.io/hyperquark/logo.png")] 4 | #![doc(html_favicon_url = "https://hyperquark.github.io/hyperquark/favicon.ico")] 5 | #![warn(clippy::cargo, clippy::nursery, clippy::pedantic)] 6 | #![allow( 7 | clippy::non_std_lazy_statics, 8 | reason = "bug in clippy (https://github.com/rust-lang/rust-clippy/issues/14729)" 9 | )] 10 | #![allow( 11 | clippy::missing_errors_doc, 12 | reason = "Too many Results everywhere to document every possible error case. Errors should be self-descriptive and user readable anyway." 13 | )] 14 | #![deny(clippy::allow_attributes, clippy::allow_attributes_without_reason)] 15 | #![warn( 16 | clippy::alloc_instead_of_core, 17 | clippy::clone_on_ref_ptr, 18 | clippy::dbg_macro, 19 | clippy::expect_used, 20 | clippy::get_unwrap, 21 | clippy::missing_asserts_for_indexing, 22 | clippy::panic, 23 | clippy::rc_buffer, 24 | clippy::redundant_type_annotations, 25 | clippy::shadow_reuse, 26 | clippy::std_instead_of_alloc, 27 | clippy::std_instead_of_core, 28 | clippy::string_to_string, 29 | clippy::unwrap_used, 30 | clippy::wildcard_enum_match_arm 31 | )] 32 | 33 | #[macro_use] 34 | extern crate alloc; 35 | extern crate enum_field_getter; 36 | 37 | use wasm_bindgen::prelude::*; 38 | 39 | #[macro_use] 40 | mod error; 41 | mod ir; 42 | mod optimisation; 43 | // pub mod ir_opt; 44 | mod sb3; 45 | mod wasm; 46 | #[macro_use] 47 | mod instructions; 48 | 49 | #[doc(inline)] 50 | pub use error::{HQError, HQErrorType, HQResult}; 51 | 52 | mod registry; 53 | 54 | /// commonly used _things_ which would be nice not to have to type out every time 55 | pub mod prelude { 56 | pub use crate::registry::{Registry, RegistryDefault}; 57 | pub use crate::{HQError, HQResult}; 58 | pub use alloc::borrow::Cow; 59 | pub use alloc::boxed::Box; 60 | pub use alloc::collections::{BTreeMap, BTreeSet}; 61 | pub use alloc::rc::{Rc, Weak}; 62 | pub use alloc::string::{String, ToString}; 63 | pub use alloc::vec::Vec; 64 | pub use core::borrow::Borrow; 65 | pub use core::cell::RefCell; 66 | pub use core::fmt; 67 | 68 | use core::hash::BuildHasherDefault; 69 | use hashers::fnv::FNV1aHasher64; 70 | use indexmap; 71 | pub type IndexMap = indexmap::IndexMap>; 72 | pub type IndexSet = indexmap::IndexSet>; 73 | 74 | pub use itertools::Itertools; 75 | } 76 | 77 | #[cfg(target_family = "wasm")] 78 | #[wasm_bindgen(js_namespace=console)] 79 | extern "C" { 80 | pub fn log(s: &str); 81 | } 82 | 83 | #[cfg(not(target_family = "wasm"))] 84 | pub fn log(s: &str) { 85 | println!("{s}"); 86 | } 87 | 88 | #[cfg(feature = "compiler")] 89 | #[wasm_bindgen] 90 | pub fn sb3_to_wasm(proj: &str, flags: wasm::WasmFlags) -> HQResult { 91 | let sb3_proj = sb3::Sb3Project::try_from(proj)?; 92 | let ir_proj = sb3_proj.try_into()?; 93 | optimisation::ir_optimise(&ir_proj)?; 94 | wasm::WasmProject::from_ir(&ir_proj, flags)?.finish() 95 | } 96 | -------------------------------------------------------------------------------- /src/optimisation.rs: -------------------------------------------------------------------------------- 1 | use crate::ir::IrProject; 2 | use crate::prelude::*; 3 | 4 | mod var_types; 5 | 6 | pub fn ir_optimise(ir: &Rc) -> HQResult<()> { 7 | var_types::optimise_var_types(ir)?; 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /src/optimisation/var_types.rs: -------------------------------------------------------------------------------- 1 | use crate::instructions::{DataSetvariabletoFields, IrOpcode}; 2 | use crate::ir::{IrProject, Type as IrType}; 3 | use crate::prelude::*; 4 | 5 | pub fn optimise_var_types(project: &Rc) -> HQResult<()> { 6 | crate::log("optimise vars"); 7 | for step in project.steps().borrow().iter() { 8 | let mut type_stack: Vec = vec![]; // a vector of types, and where they came from 9 | for block in &*step.opcodes().try_borrow()?.clone() { 10 | let expected_inputs = block.acceptable_inputs(); 11 | if type_stack.len() < expected_inputs.len() { 12 | hq_bug!("didn't have enough inputs on the type stack") 13 | } 14 | let inputs = type_stack 15 | .splice((type_stack.len() - expected_inputs.len()).., []) 16 | .collect::>(); 17 | if let IrOpcode::data_setvariableto(DataSetvariabletoFields(var)) = block { 18 | var.0.add_type(inputs[0]); 19 | } 20 | if let Some(output) = block.output_type(expected_inputs)? { 21 | type_stack.push(output); 22 | } 23 | } 24 | } 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /src/registry.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use core::hash::Hash; 3 | 4 | #[derive(Clone)] 5 | pub struct MapRegistry(RefCell>) 6 | where 7 | K: Hash + Eq + Clone; 8 | 9 | // deriving Default doesn't work if V: !Default, so we implement it manually 10 | impl Default for MapRegistry 11 | where 12 | K: Hash + Eq + Clone, 13 | { 14 | fn default() -> Self { 15 | Self(RefCell::new(IndexMap::default())) 16 | } 17 | } 18 | 19 | impl Registry for MapRegistry 20 | where 21 | K: Hash + Eq + Clone, 22 | { 23 | type Key = K; 24 | type Value = V; 25 | 26 | fn registry(&self) -> &RefCell> { 27 | &self.0 28 | } 29 | } 30 | 31 | #[derive(Clone)] 32 | pub struct SetRegistry(RefCell>) 33 | where 34 | K: Hash + Eq + Clone; 35 | 36 | impl Default for SetRegistry 37 | where 38 | K: Hash + Eq + Clone, 39 | { 40 | fn default() -> Self { 41 | Self(RefCell::new(IndexMap::default())) 42 | } 43 | } 44 | 45 | impl Registry for SetRegistry 46 | where 47 | K: Hash + Eq + Clone, 48 | { 49 | type Key = K; 50 | type Value = (); 51 | 52 | fn registry(&self) -> &RefCell> { 53 | &self.0 54 | } 55 | } 56 | 57 | pub trait Registry: Sized { 58 | const IS_SET: bool = false; 59 | 60 | type Key: Hash + Clone + Eq; 61 | type Value; 62 | 63 | fn registry(&self) -> &RefCell>; 64 | 65 | /// get the index of the specified item, inserting it if it doesn't exist in the map already. 66 | /// Doesn't check if the provided value matches what's already there. This is generic because 67 | /// various different numeric types are needed in different places, so it's easiest to encapsulate 68 | /// the casting logic in here. 69 | fn register(&self, key: Self::Key, value: Self::Value) -> HQResult 70 | where 71 | N: TryFrom, 72 | >::Error: core::fmt::Debug, 73 | { 74 | self.registry() 75 | .try_borrow_mut() 76 | .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? 77 | .entry(key.clone()) 78 | .or_insert(value); 79 | N::try_from( 80 | self.registry() 81 | .try_borrow()? 82 | .get_index_of(&key) 83 | .ok_or_else(|| make_hq_bug!("couldn't find entry in Registry"))?, 84 | ) 85 | .map_err(|_| make_hq_bug!("registry item index out of bounds")) 86 | } 87 | 88 | fn register_override(&self, key: Self::Key, value: Self::Value) -> HQResult 89 | where 90 | N: TryFrom, 91 | >::Error: core::fmt::Debug, 92 | { 93 | self.registry() 94 | .try_borrow_mut() 95 | .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? 96 | .entry(key.clone()) 97 | .insert_entry(value); 98 | N::try_from( 99 | self.registry() 100 | .try_borrow()? 101 | .get_index_of(&key) 102 | .ok_or_else(|| make_hq_bug!("couldn't find entry in Registry"))?, 103 | ) 104 | .map_err(|_| make_hq_bug!("registry item index out of bounds")) 105 | } 106 | } 107 | 108 | pub trait RegistryDefault: Registry { 109 | fn register_default(&self, key: Self::Key) -> HQResult 110 | where 111 | N: TryFrom, 112 | >::Error: core::fmt::Debug, 113 | { 114 | self.register(key, Self::Value::default()) 115 | } 116 | } 117 | 118 | impl RegistryDefault for R where R: Registry {} 119 | -------------------------------------------------------------------------------- /src/wasm.rs: -------------------------------------------------------------------------------- 1 | mod external; 2 | pub mod flags; 3 | mod func; 4 | mod globals; 5 | mod project; 6 | mod registries; 7 | mod strings; 8 | mod tables; 9 | mod type_registry; 10 | mod variable; 11 | 12 | pub use external::{ExternalEnvironment, ExternalFunctionRegistry}; 13 | pub use flags::WasmFlags; 14 | pub use func::{Instruction as InternalInstruction, StepFunc}; 15 | pub use globals::{Exportable as GlobalExportable, GlobalRegistry, Mutable as GlobalMutable}; 16 | pub use project::{FinishedWasm, WasmProject}; 17 | pub use registries::Registries; 18 | pub use strings::StringRegistry; 19 | pub use tables::{TableOptions, TableRegistry}; 20 | pub use type_registry::TypeRegistry; 21 | pub use variable::VariableRegistry; 22 | -------------------------------------------------------------------------------- /src/wasm/external.rs: -------------------------------------------------------------------------------- 1 | use super::TypeRegistry; 2 | use crate::prelude::*; 3 | use crate::registry::MapRegistry; 4 | use wasm_encoder::{EntityType, ImportSection, ValType}; 5 | 6 | pub type ExternalFunctionRegistry = 7 | MapRegistry<(&'static str, Box), (Vec, Vec)>; 8 | 9 | impl ExternalFunctionRegistry { 10 | pub fn finish(self, imports: &mut ImportSection, type_registry: &TypeRegistry) -> HQResult<()> { 11 | for ((module, name), (params, results)) in self.registry().take() { 12 | let type_index = type_registry.register_default((params, results))?; 13 | imports.import(module, &name, EntityType::Function(type_index)); 14 | } 15 | Ok(()) 16 | } 17 | } 18 | 19 | #[derive(Debug, Copy, Clone)] 20 | #[non_exhaustive] 21 | pub enum ExternalEnvironment { 22 | WebBrowser, 23 | } 24 | -------------------------------------------------------------------------------- /src/wasm/flags.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use serde::{Deserialize, Serialize}; 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[derive(Copy, Clone, Serialize, Deserialize)] 6 | #[wasm_bindgen] 7 | pub enum WasmStringType { 8 | ExternRef, 9 | JsStringBuiltins, 10 | //Manual, 11 | } 12 | 13 | #[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] 14 | #[wasm_bindgen] 15 | pub enum WasmOpt { 16 | On, 17 | Off, 18 | } 19 | 20 | #[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] 21 | #[wasm_bindgen] 22 | pub enum Scheduler { 23 | TypedFuncRef, 24 | CallIndirect, 25 | } 26 | 27 | #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] 28 | #[wasm_bindgen] 29 | pub enum WasmFeature { 30 | ReferenceTypes, 31 | TypedFunctionReferences, 32 | JSStringBuiltins, 33 | } 34 | 35 | #[wasm_bindgen] 36 | pub fn all_wasm_features() -> Vec { 37 | use WasmFeature::{JSStringBuiltins, ReferenceTypes, TypedFunctionReferences}; 38 | vec![ReferenceTypes, TypedFunctionReferences, JSStringBuiltins] 39 | } 40 | 41 | // no &self because wasm_bidgen doesn't like it 42 | #[wasm_bindgen] 43 | pub fn wasm_feature_detect_name(feat: WasmFeature) -> String { 44 | use WasmFeature::{JSStringBuiltins, ReferenceTypes, TypedFunctionReferences}; 45 | match feat { 46 | ReferenceTypes => "referenceTypes", 47 | TypedFunctionReferences => "typedFunctionReferences", 48 | JSStringBuiltins => "jsStringBuiltins", 49 | } 50 | .into() 51 | } 52 | 53 | #[derive(Clone, Serialize, Deserialize)] 54 | #[wasm_bindgen(getter_with_clone)] 55 | #[expect( 56 | clippy::unsafe_derive_deserialize, 57 | reason = "wasm-bindgen introduces unsafe methods" 58 | )] 59 | pub struct FlagInfo { 60 | /// a human-readable name for the flag 61 | pub name: String, 62 | pub description: String, 63 | pub ty: String, 64 | /// which WASM features does this flag rely on? 65 | wasm_features: BTreeMap>, 66 | } 67 | 68 | #[wasm_bindgen] 69 | impl FlagInfo { 70 | fn new() -> Self { 71 | Self { 72 | name: String::new(), 73 | description: String::new(), 74 | ty: String::new(), 75 | wasm_features: BTreeMap::default(), 76 | } 77 | } 78 | 79 | fn with_name(mut self, name: &str) -> Self { 80 | self.name = name.to_string(); 81 | self 82 | } 83 | 84 | fn with_description(mut self, description: &str) -> Self { 85 | self.description = description.to_string(); 86 | self 87 | } 88 | 89 | fn with_ty(mut self, ty: &str) -> Self { 90 | self.ty = ty.to_string(); 91 | self 92 | } 93 | 94 | fn with_wasm_features(mut self, wasm_features: BTreeMap>) -> Self { 95 | self.wasm_features = wasm_features; 96 | self 97 | } 98 | 99 | #[wasm_bindgen] 100 | pub fn wasm_features(&self, flag: &str) -> Option> { 101 | self.wasm_features.get(flag).cloned() 102 | } 103 | 104 | #[wasm_bindgen] 105 | pub fn to_js(&self) -> HQResult { 106 | serde_wasm_bindgen::to_value(&self) 107 | .map_err(|_| make_hq_bug!("couldn't convert FlagInfo to JsValue")) 108 | } 109 | } 110 | 111 | macro_rules! stringmap { 112 | ($($k:ident : $v:expr),+ $(,)?) => {{ 113 | BTreeMap::from([$((String::from(stringify!($k)), $v),)+]) 114 | }} 115 | } 116 | 117 | /// stringifies the name of a type whilst ensuring that the type is valid 118 | macro_rules! ty_str { 119 | ($ty:ty) => {{ 120 | let _ = core::any::TypeId::of::<$ty>(); // forces the type to be valid 121 | stringify!($ty) 122 | }}; 123 | } 124 | 125 | /// compilation flags 126 | #[derive(Copy, Clone, Serialize, Deserialize)] 127 | #[wasm_bindgen] 128 | #[expect( 129 | clippy::unsafe_derive_deserialize, 130 | reason = "wasm-bindgen introduces unsafe methods" 131 | )] 132 | pub struct WasmFlags { 133 | pub string_type: WasmStringType, 134 | pub wasm_opt: WasmOpt, 135 | pub scheduler: Scheduler, 136 | } 137 | 138 | #[wasm_bindgen] 139 | impl WasmFlags { 140 | // these attributes should be at the item level, but they don't seem to work there. 141 | #![expect( 142 | clippy::wrong_self_convention, 143 | clippy::trivially_copy_pass_by_ref, 144 | reason = "to_js taking `self` causes weird errors when wasm-fied" 145 | )] 146 | #![expect( 147 | clippy::needless_pass_by_value, 148 | reason = "wasm-bindgen does not support &[T]" 149 | )] 150 | 151 | #[wasm_bindgen] 152 | pub fn from_js(js: JsValue) -> HQResult { 153 | serde_wasm_bindgen::from_value(js) 154 | .map_err(|_| make_hq_bug!("couldn't convert JsValue to WasmFlags")) 155 | } 156 | 157 | #[wasm_bindgen] 158 | pub fn to_js(&self) -> HQResult { 159 | serde_wasm_bindgen::to_value(&self) 160 | .map_err(|_| make_hq_bug!("couldn't convert WasmFlags to JsValue")) 161 | } 162 | 163 | #[wasm_bindgen(constructor)] 164 | pub fn new(wasm_features: Vec) -> Self { 165 | crate::log(format!("{wasm_features:?}").as_str()); 166 | Self { 167 | wasm_opt: WasmOpt::On, 168 | string_type: if wasm_features.contains(&WasmFeature::JSStringBuiltins) { 169 | WasmStringType::JsStringBuiltins 170 | } else { 171 | WasmStringType::ExternRef 172 | }, 173 | scheduler: Scheduler::CallIndirect, 174 | } 175 | } 176 | 177 | #[wasm_bindgen] 178 | pub fn flag_info(flag: &str) -> FlagInfo { 179 | match flag { 180 | "string_type" => FlagInfo::new() 181 | .with_name("Internal string representation") 182 | .with_description( 183 | "ExternRef - uses JavaScript strings.\ 184 |
\ 185 | JsStringBuiltins (recommended) - uses JavaScript strings with the JS String Builtins proposal", 186 | ) 187 | .with_ty(ty_str!(WasmStringType)) 188 | .with_wasm_features(stringmap! { 189 | ExternRef : vec![WasmFeature::ReferenceTypes], 190 | JsStringBuiltins : vec![WasmFeature::ReferenceTypes, WasmFeature::JSStringBuiltins], 191 | }), 192 | "wasm_opt" => FlagInfo::new() 193 | .with_name("WASM optimisation") 194 | .with_description("Should we try to optimise generated WASM modules using wasm-opt?") 195 | .with_ty(ty_str!(WasmOpt)), 196 | "scheduler" => FlagInfo::new() 197 | .with_name("Scheduler") 198 | .with_description("TypedFuncRef - uses typed function references to eliminate runtime checks.\ 199 |
\ 200 | CallIndirect - stores function indices, then uses CallIndirect to call them.") 201 | .with_ty(ty_str!(Scheduler)) 202 | .with_wasm_features(stringmap! { 203 | TypedFuncRef : vec![WasmFeature::TypedFunctionReferences] 204 | }), 205 | _ => FlagInfo::new().with_name(format!("unknown setting '{flag}'").as_str()) 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/wasm/globals.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::registry::MapRegistry; 3 | use core::ops::Deref; 4 | use wasm_encoder::{ 5 | ConstExpr, ExportKind, ExportSection, GlobalSection, GlobalType, ImportSection, ValType, 6 | }; 7 | 8 | #[derive(Copy, Clone, Debug)] 9 | pub struct Mutable(pub bool); 10 | 11 | impl Deref for Mutable { 12 | type Target = bool; 13 | fn deref(&self) -> &bool { 14 | &self.0 15 | } 16 | } 17 | 18 | #[derive(Copy, Clone, Debug)] 19 | pub struct Exportable(pub bool); 20 | 21 | impl Deref for Exportable { 22 | type Target = bool; 23 | fn deref(&self) -> &bool { 24 | &self.0 25 | } 26 | } 27 | 28 | pub type GlobalRegistry = MapRegistry, (ValType, ConstExpr, Mutable, Exportable)>; 29 | 30 | impl GlobalRegistry { 31 | pub fn finish( 32 | self, 33 | imports: &ImportSection, 34 | globals: &mut GlobalSection, 35 | exports: &mut ExportSection, 36 | ) { 37 | for (key, (ty, suggested_initial, mutable, export)) in self.registry().take() { 38 | if *export { 39 | exports.export(&key, ExportKind::Global, globals.len()); 40 | } 41 | let actual_initial = match &*key { 42 | "noop_func" => ConstExpr::ref_func(imports.len()), 43 | _ => suggested_initial, 44 | }; 45 | globals.global( 46 | GlobalType { 47 | val_type: ty, 48 | mutable: *mutable, 49 | shared: false, 50 | }, 51 | &actual_initial, 52 | ); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/wasm/registries.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | ExternalFunctionRegistry, GlobalRegistry, StringRegistry, TableRegistry, TypeRegistry, 3 | VariableRegistry, 4 | }; 5 | use crate::prelude::*; 6 | 7 | pub struct Registries { 8 | strings: StringRegistry, 9 | external_functions: ExternalFunctionRegistry, 10 | types: TypeRegistry, 11 | tables: TableRegistry, 12 | globals: Rc, 13 | variables: VariableRegistry, 14 | } 15 | 16 | impl Default for Registries { 17 | fn default() -> Self { 18 | let globals = Rc::new(GlobalRegistry::default()); 19 | let variables = VariableRegistry::new(&globals); 20 | Self { 21 | globals, 22 | variables, 23 | strings: StringRegistry::default(), 24 | external_functions: ExternalFunctionRegistry::default(), 25 | tables: TableRegistry::default(), 26 | types: TypeRegistry::default(), 27 | } 28 | } 29 | } 30 | 31 | impl Registries { 32 | pub const fn strings(&self) -> &StringRegistry { 33 | &self.strings 34 | } 35 | 36 | pub const fn external_functions(&self) -> &ExternalFunctionRegistry { 37 | &self.external_functions 38 | } 39 | 40 | pub const fn types(&self) -> &TypeRegistry { 41 | &self.types 42 | } 43 | 44 | pub const fn tables(&self) -> &TableRegistry { 45 | &self.tables 46 | } 47 | 48 | pub fn globals(&self) -> &GlobalRegistry { 49 | &self.globals 50 | } 51 | 52 | pub const fn variables(&self) -> &VariableRegistry { 53 | &self.variables 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/wasm/strings.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::registry::SetRegistry; 3 | 4 | pub type StringRegistry = SetRegistry>; 5 | 6 | impl StringRegistry { 7 | pub fn finish(self) -> Vec { 8 | self.registry() 9 | .take() 10 | .keys() 11 | .cloned() 12 | .map(str::into_string) 13 | .collect() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/wasm/tables.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::registry::MapRegistry; 3 | use wasm_encoder::{ 4 | ConstExpr, ExportKind, ExportSection, ImportSection, RefType, TableSection, TableType, 5 | }; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct TableOptions { 9 | pub element_type: RefType, 10 | pub min: u64, 11 | pub max: Option, 12 | pub init: Option, 13 | } 14 | 15 | pub type TableRegistry = MapRegistry, TableOptions>; 16 | 17 | impl TableRegistry { 18 | pub fn finish( 19 | self, 20 | imports: &ImportSection, 21 | tables: &mut TableSection, 22 | exports: &mut ExportSection, 23 | ) { 24 | for ( 25 | key, 26 | TableOptions { 27 | element_type, 28 | min, 29 | max, 30 | init, 31 | }, 32 | ) in self.registry().take() 33 | { 34 | // TODO: allow choosing whether to export a table or not? 35 | exports.export(&key, ExportKind::Table, tables.len()); 36 | let maybe_init = match &*key { 37 | "threads" => Some(ConstExpr::ref_func(imports.len())), 38 | _ => init, 39 | }; 40 | if let Some(init) = maybe_init { 41 | tables.table_with_init( 42 | TableType { 43 | element_type, 44 | minimum: min, 45 | maximum: max, 46 | table64: false, 47 | shared: false, 48 | }, 49 | &init, 50 | ); 51 | } else { 52 | tables.table(TableType { 53 | element_type, 54 | minimum: min, 55 | maximum: max, 56 | table64: false, 57 | shared: false, 58 | }); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/wasm/type_registry.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::registry::SetRegistry; 3 | use wasm_encoder::{TypeSection, ValType}; 4 | 5 | pub type TypeRegistry = SetRegistry<(Vec, Vec)>; 6 | 7 | impl TypeRegistry { 8 | pub fn finish(self, types: &mut TypeSection) { 9 | for (params, results) in self.registry().take().keys().cloned() { 10 | types.ty().function(params, results); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/wasm/variable.rs: -------------------------------------------------------------------------------- 1 | use crate::ir::{RcVar, Type as IrType}; 2 | use crate::prelude::*; 3 | use wasm_encoder::{ConstExpr, HeapType}; 4 | 5 | use super::{GlobalExportable, GlobalMutable, GlobalRegistry, WasmProject}; 6 | 7 | pub struct VariableRegistry(Rc); 8 | 9 | impl VariableRegistry { 10 | const fn globals(&self) -> &Rc { 11 | &self.0 12 | } 13 | 14 | pub fn new(globals: &Rc) -> Self { 15 | Self(Rc::clone(globals)) 16 | } 17 | 18 | pub fn register(&self, var: &RcVar) -> HQResult { 19 | self.globals().register( 20 | format!("__rcvar_{:p}", Rc::as_ptr(&var.0)).into(), 21 | ( 22 | WasmProject::ir_type_to_wasm(*var.0.possible_types())?, 23 | match var.0.possible_types().base_type() { 24 | Some(IrType::Float) => ConstExpr::f64_const(0.0), 25 | Some(IrType::QuasiInt) => ConstExpr::i32_const(0), 26 | Some(IrType::String) => ConstExpr::ref_null(HeapType::EXTERN), 27 | _ => ConstExpr::i64_const(0), // TODO: use the variable's initial value 28 | }, 29 | GlobalMutable(true), 30 | GlobalExportable(false), 31 | ), 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import wasm from "vite-plugin-wasm"; 5 | import vue from '@vitejs/plugin-vue' 6 | import vueJsx from '@vitejs/plugin-vue-jsx' 7 | import { nodePolyfills } from 'vite-plugin-node-polyfills' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | root: './playground', 12 | base: '/hyperquark/', 13 | plugins: [ 14 | wasm(), 15 | vue(), 16 | nodePolyfills({ 17 | globals: { 18 | Buffer: true, 19 | }, 20 | }), 21 | ], 22 | resolve: { 23 | alias: { 24 | '@': fileURLToPath(new URL('./playground', import.meta.url)) 25 | } 26 | }, 27 | build: { 28 | target: 'esnext', 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /wasm-gen/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock -------------------------------------------------------------------------------- /wasm-gen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-gen" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | syn = { version = "1.0", features = ["full"] } 12 | quote = "1.0" 13 | proc-macro2 = "1.0" 14 | proc-macro-error = "1.0" -------------------------------------------------------------------------------- /wasm-gen/README.md: -------------------------------------------------------------------------------- 1 | # wasm-gen 2 | 3 | This is an internal crate used to generate WebAssembly instructions for blocks. It relies upon HyperQuark's internal types so cannot be used outside of HyperQuark. 4 | 5 | ## Usage 6 | 7 | The `wasm![]` macro produces a `Vec>`. Inputs to the macro are (non-namespaced) wasm_encoder [`Instruction`](https://docs.rs/wasm-encoder/latest/wasm_encoder/enum.Instruction.html)s, or a 'special' instruction. Special instructions are currently: 8 | - `@nanreduce(input_name)` - checks the top value on the stack for NaN-ness (and replaces it with a zero if it is), only if `input_name` (must be an in-scope [`IrType`](../src/ir/types.rs)) could possibly be `NaN`. Assumes (rightly so) that the top item on the stack is an `f64`. 9 | - `@box(input_name)` - boxes the top value on the stack if `input_name` is a base type 10 | - `@isnan(input_name)` - checks if the top value on the stack for NaN-ness, only if `input_name` could possibly be NaN. Assumes the top item on the stack is an `f64`. --------------------------------------------------------------------------------