diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci-checks.yaml similarity index 55% rename from .github/workflows/ci.yaml rename to .github/workflows/ci-checks.yaml index db6e9fcf..7989b1ad 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci-checks.yaml @@ -1,6 +1,6 @@ -on: [push, workflow_dispatch] +on: [push, workflow_dispatch, pull_request] -name: CI +name: CI checks jobs: build: @@ -16,18 +16,20 @@ jobs: profile: minimal toolchain: nightly override: true + target: wasm32-unknown-unknown - name: Run cargo check uses: actions-rs/cargo@v1 with: command: check + args: --target=wasm32-unknown-unknown clippy: name: Lint (clippy) runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install nightly toolchain with clippy available uses: actions-rs/toolchain@v1 @@ -48,7 +50,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install nightly toolchain with rustfmt available uses: actions-rs/toolchain@v1 @@ -60,14 +62,12 @@ jobs: - name: Run cargo fmt uses: actions-rs/cargo@v1 - continue-on-error: true # WARNING: only for this example, remove it! with: command: fmt args: --all -- --check - - buildwasm: - name: Build WASM & website - if: github.ref_name == 'main' + + test: + name: Run unit tests runs-on: ubuntu-latest steps: - name: Checkout sources @@ -79,34 +79,26 @@ jobs: profile: minimal toolchain: nightly override: true - target: wasm32-unknown-unknown - - - name: Install wasm-bindgen + + - name: Run cargo test uses: actions-rs/cargo@v1 with: - command: install - args: -f wasm-bindgen-cli - - - name: Install binaryen - run: sudo apt-get install binaryen - - - name: Install node - uses: actions/setup-node@v3 - with: - node-version: "20.x" - - - name: Run npm install - run: npm install - - - name: Build - run: chmod +x build.sh && ./build.sh -pWV - - - name: Move files for gh pages - run: mv ./playground/dist docs && cp docs/index.html docs/404.html - - - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v4 - with: - branch: gh-pages - create_branch: true - push_options: '--force' + command: test + args: >- + -- + --skip cast::float::js_functions_match_declared_types + --skip cast::int::js_functions_match_declared_types + --skip cast::string::js_functions_match_declared_types + --skip join::tests::js_functions_match_declared_types + --skip lt::tests::js_functions_match_declared_types + --skip gt::tests::js_functions_match_declared_types + --skip equals::tests::js_functions_match_declared_types + --skip length::tests::js_functions_match_declared_types + --skip letter_of::tests::js_functions_match_declared_types + --skip contains::tests::js_functions_match_declared_types + --skip dayssince2000::tests::js_functions_match_declared_types + --skip looks::say::tests_debug::js_functions_match_declared_types + --skip looks::say::tests_non_debug::js_functions_match_declared_types + --skip looks::think::tests_debug::js_functions_match_declared_types + --skip looks::think::tests_non_debug::js_functions_match_declared_types + diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..ff2306ea --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,76 @@ +on: [push, workflow_dispatch] + +name: Deploy + +jobs: + deploy: + name: Build WASM & website + runs-on: ubuntu-latest + env: + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + permissions: + contents: write + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Install nightly toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + target: wasm32-unknown-unknown + + - name: Install wasm-bindgen + uses: actions-rs/cargo@v1 + with: + command: install + args: -f wasm-bindgen-cli + + - name: Install cargo-outdir + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-outdir + + - name: Install binaryen + run: sudo apt-get install binaryen + + - name: Install node + uses: actions/setup-node@v3 + with: + node-version: "20.x" + + - name: Run npm install + run: | + npm install + npm i -g vite + npm i -g binaryen@nightly + + - name: Build + env: + VITE_HASH_HISTORY: true + run: | + chmod +x build.sh && ./build.sh -Wpz + vite build --base=/hyperquark/$BRANCH_NAME/ + + - name: Move files to tmp + run: mv ./playground/dist /tmp/hq-dist + + - name: checkout gh-pages + uses: actions/checkout@v4 + with: + ref: gh-pages + + - name: move file to gh-pages + run: | + rm -rf ./$BRANCH_NAME + mv /tmp/hq-dist ./$BRANCH_NAME + #mv ./main/* ./ + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + branch: gh-pages + push_options: '--force-with-lease' diff --git a/.gitignore b/.gitignore index a34cb852..e64f1a39 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,12 @@ /bad.mjs /bad.wat /wabt* -/js/ +/js/compiler +/js/no-compiler +/js/imports.ts +/js/opcodes.js /node_modules/ /package-lock.json /playground/dist/ -/.vite/ \ No newline at end of file +/.vite/ +/.vscode/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 78826379..97b4fb13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,20 +8,37 @@ publish = false serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } enum-field-getter = { path = "enum-field-getter" } -wasm-encoder = "0.214.0" -wasm-bindgen = "0.2.92" +wasm-encoder = "0.226.0" indexmap = { version = "2.0.0", default-features = false } hashers = "1.0.1" uuid = { version = "1.4.1", default-features = false, features = ["v4", "js"] } regex = "1.10.5" lazy-regex = "3.2.0" +bitmask-enum = "2.2.5" +itertools = { version = "0.13.0", default-features = false, features = ["use_alloc"] } +split_exact = "1.1.0" +wasm-bindgen = "0.2.92" +serde-wasm-bindgen = "0.6.5" +wasm-gen = { path = "wasm-gen" } -#[dev-dependencies] +[dev-dependencies] +wasmparser = "0.226.0" +wasmprinter = "0.226.0" #reqwest = { version = "0.11", features = ["blocking"] } +[target.'cfg(not(target_family = "wasm"))'.dev-dependencies] +ezno-checker = { git = "https://github.com/kaleidawave/ezno.git", rev = "96d5058bdbb0cde924be008ca1e5a67fe39f46b9" } + [lib] crate-type = ["cdylib", "rlib"] [profile.release] lto = true opt-level = "z" + +[build-dependencies] +convert_case = "0.6.0" + +[features] +compiler = [] # if we only want to access flags, we don't want to additionally have all the compiler machinery +default = ["compiler"] diff --git a/README.md b/README.md index cf4740df..6a320e98 100644 --- a/README.md +++ b/README.md @@ -5,22 +5,42 @@ Compile scratch projects to WASM ## Prerequisites -- [Rust](https://rust-lang.org) (v1.65.0 or later) +- [Rust](https://rust-lang.org) v1.65.0 or later - the `wasm32-unknown-unknown` target (`rustup target add wasm32-unknown-unknown`) - wasm-bindgen-cli (`cargo install -f wasm-bindgen-cli`) - wasm-opt (install binaryen using whatever package manager you use) +- `cargo-outdir` (`cargo install cargo-outdir`) ## Building ```bash -./build.sh -pVW # use -dVW for a debug build without optimisation +./build.sh -Wp # use -Wd for a debug build without optimisation ``` You may need to run `chmod +x build.sh` if it says it doesn't have permission. The build script has additonal configuration options; run `./build.sh -h` for info on these. -If you experience runtime stack overflow errors in debug mode, try using the `-O` option to enable wasm-opt. +If you experience runtime stack overflow errors in debug mode, try using the `-s` or `-z` options to enable wasm-opt; weird wasm errors in production mode may conversely be solved by *disabling* wasm-opt using the `-o` flag. + +## Adding a new block + +To add a new block named `category_opcode`, if it cannot be reduced to simpler blocks: +1. create `src/instructions/category/opcode.rs`. Make sure to `use super::super::prelude::*` and create the relevant `pub` items: +- (optional) `pub struct Fields` (must be `Debug` and `Clone`) +- `pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>, (fields: &Fields)?) -> HQResult>;` +- - wasm is generated using the `wasm_gen::wasm` macro. See [its README](./wasm-gen/README.md) for usage instructions, or e.g. [say.rs](./src/instructions/looks/say.rs) for an example. +- `pub fn acceptable_inputs() -> Rc<[IrType]>;` +- - these should really be base types (see BASE_TYPES in [types.rs](./src/ir/types.rs)) +- `pub fn output_type(inputs: Rc<[IrType]>, (fields: &Fields)?) -> HQResult>;` +- - the output type should be as restrictive as possible; loose output types can cause us to lose out on some optimisations +- ensure to add relevant `instructions_test!`s - see [instructions/tests.rs](./src/instructions/tests.rs) for usage +2. add `pub mod opcode;` to `src/instructions/category.rs`, creating the file if needed +- if you're creating the category file, add `mod category;` to `src/instructions.rs` +3. add the block to `from_normal_block` in `src/ir/blocks.rs`; in most cases this should be a direct mapping of `BlockOpcode::category_opcode => IrOpcode::category_opcode` +4. add the block's input names to `input_names` in `src/ir/blocks.rs` + +If the block *can* be reduced to simpler steps, only carry out steps 3 and 4 above. ## generared WASM module memory layout diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..15699880 --- /dev/null +++ b/build.rs @@ -0,0 +1,223 @@ +use convert_case::{Case, Casing}; +use std::collections::HashSet; +use std::env; +use std::fs; +use std::path::Path; + +// I hate to admit this, but a fair bit of this file was written by chatgpt to speed things up +// and to allow me to continue to procrastinate about learning how to do i/o stuff in rust. +// But I did write some of it! + +fn main() { + println!("cargo::rerun-if-changed=src/instructions/**/*.rs"); + + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path_rs = Path::new(&out_dir).join("ir-opcodes.rs"); + let dest_path_ts = Path::new(&out_dir).join("imports.ts"); + + let base_dir = "src/instructions"; + + let mut paths = Vec::new(); + let mut ts_paths = Vec::new(); + visit_dirs(Path::new(base_dir), &mut |entry| { + if let Some(ext) = entry.path().extension() { + if ext == "rs" { + if let Ok(relative_path) = entry.path().strip_prefix(base_dir) { + let components: Vec<_> = relative_path + .components() + .filter_map(|comp| comp.as_os_str().to_str()) + .collect(); + + if components.len() == 2 { + let category = components[0]; + let opcode = components[1].trim_end_matches(".rs"); + let contents = + fs::read_to_string(format!("src/instructions/{category}/{opcode}.rs")) + .unwrap(); + let fields = contents.contains("pub struct Fields"); + let fields_name = + format!("{category}_{opcode}_fields").to_case(Case::Pascal); + paths.push(( + format!( + "{}::{}", + category, + match opcode { + "yield" | "loop" => format!("r#{opcode}"), + _ => opcode.to_string(), + } + ), + format!("{category}_{opcode}",), + fields, + fields_name, + )); + } + } + } + } + }); + visit_dirs(Path::new("js"), &mut |entry| { + if let Some(ext) = entry.path().extension() { + if ext == "ts" { + if let Ok(relative_path) = entry.path().strip_prefix("js") { + let components: Vec<_> = relative_path + .components() + .filter_map(|comp| comp.as_os_str().to_str()) + .collect(); + + if components.len() == 2 { + let category = components[0]; + if category == "compiler" || category == "no-compiler" { + return; + } + let func = components[1].trim_end_matches(".ts"); + ts_paths.push((category.to_string(), func.to_string())); + } + } + } + } + }); + + fs::write( + &dest_path_ts, + format!( + " +{} +export const imports = {{ + {} +}}; + ", + ts_paths + .iter() + .map(|(dir, name)| format!("import {{ {name} }} from './{dir}/{name}.ts';")) + .collect::>() + .join("\n"), + HashSet::::from_iter(ts_paths.iter().map(|(dir, _)| dir.clone())) + .iter() + .map(|dir| { + format!( + "\"{}\": {{ {} }}", + if dir == "wasm-js-string" { + "wasm:js-string" + } else { + dir + }, + ts_paths + .iter() + .filter(|(d, _)| d == dir) + .map(|(_, name)| name.clone()) + .collect::>() + .join(", ") + ) + }) + .collect::>() + .join(",\n\t") + ), + ) + .unwrap(); + + fs::write( + &dest_path_rs, + format!( + " +/// A list of all instructions. +#[expect(non_camel_case_types, reason = \"block opcode are snake_case\")] +#[derive(Clone, Debug)] +pub enum IrOpcode {{ + {} +}} + +impl IrOpcode {{ + /// maps an opcode to its acceptable input types + pub fn acceptable_inputs(&self) -> Rc<[crate::ir::Type]> {{ + match self {{ + {} + }} + }} + + /// maps an opcode to its WASM instructions + pub fn wasm(&self, step_func: &crate::wasm::StepFunc, inputs: Rc<[crate::ir::Type]>) -> HQResult> {{ + match self {{ + {} + }} + }} + + /// maps an opcode to its output type + pub fn output_type(&self, inputs: Rc<[crate::ir::Type]>) -> HQResult> {{ + match self {{ + {} + }} + }} + + /// does this opcode request a screen refresh (and by extension yields)? + pub const fn requests_screen_refresh(&self) -> bool {{ + match self {{ + {} + }} + }} +}} +pub mod fields {{ + #![expect(clippy::wildcard_imports, reason = \"we don't know what we need to import\")] + + use super::*; + + {} +}} +pub use fields::*; + ", + paths.iter().map(|(_, id, fields, fields_name)| { + if *fields { + format!("{}({})", id.clone(), fields_name.clone()) + } else { + id.clone() + } + }).collect::>().join(",\n\t"), + paths.iter().map(|(path, id, fields, _)| { + if *fields { + format!("Self::{id}(fields) => {path}::acceptable_inputs(fields),") + } else { + format!("Self::{id} => {path}::acceptable_inputs(),") + } + }).collect::>().join("\n\t\t\t"), + paths.iter().map(|(path, id, fields, _)| { + if *fields { + format!("Self::{id}(fields) => {path}::wasm(step_func, inputs, fields),") + } else { + format!("Self::{id} => {path}::wasm(step_func, inputs),") + } + }).collect::>().join("\n\t\t\t"), + paths.iter().map(|(path, id, fields, _)| { + if *fields { + format!("Self::{id}(fields) => {path}::output_type(inputs, fields),") + } else { + format!("Self::{id} => {path}::output_type(inputs),") + } + }).collect::>().join("\n\t\t\t"), + paths.iter().map(|(path, id, fields, _)| { + if *fields { + format!("Self::{id}(_) => {path}::REQUESTS_SCREEN_REFRESH,") + } else { + format!("Self::{id} => {path}::REQUESTS_SCREEN_REFRESH,") + } + }).collect::>().join("\n\t\t\t"), + paths.iter().filter(|(_, _, fields, _)| *fields) + .map(|(path, _, _, fields_name)| + format!("pub use {path}::Fields as {fields_name};") + ).collect::>().join("\n\t"), + )) + .unwrap(); +} + +fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&fs::DirEntry)) { + if dir.is_dir() { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + visit_dirs(&path, cb); + } else { + cb(&entry); + } + } + } + } +} diff --git a/build.sh b/build.sh index e0355a8b..04b1a96b 100755 --- a/build.sh +++ b/build.sh @@ -18,13 +18,11 @@ usage() echo " -d build for development" echo " -p build for production" echo " -V build the website with vite" - echo " -v do not build the website with vite" echo " -W build wasm" - echo " -w do not build wasm" echo " -o do not run wasm-opt" - echo " -O run wasm-opt" echo " -s run wasm-opt with -Os" - echo " -z run wasm-opt with -Os" + echo " -z run wasm-opt with -Oz" + echo " -v verbose output" exit 1 } @@ -41,33 +39,31 @@ set_variable() fi } -unset PROD VITE WASM -QUIET=1 -while getopts 'dpwvoWVOhi' c -while getopts 'dpwvoWVszhi' c +unset VITE WASM PROD; +QUIET=1; +while getopts 'dpVWoszvh' c do case $c in d) set_variable PROD 0 ;; p) set_variable PROD 1 ;; - v) set_variable VITE 0 ;; - w) set_variable WASM 0 ;; V) set_variable VITE 1 ;; W) set_variable WASM 1 ;; o) set_variable WOPT 0 ;; - O) set_variable WOPT 1 ;; s) set_variable WOPT 1 ;; z) set_variable WOPT 2 ;; - i) unset QUIET ;; + v) unset QUIET ;; h|?) usage ;; esac done -[ -z $PROD ] && usage -[ -z $VITE ] && usage -[ -z $WASM ] && usage +[ -z $WASM ] && set_variable WASM 0; +[ -z $VITE ] && set_variable VITE 0; + +[ -z $PROD ] && usage; + if [ -z $WOPT ]; then if [ $PROD = "1" ]; then - set_variable WOPT 1; + set_variable WOPT 2; else set_variable WOPT 0; fi @@ -76,27 +72,39 @@ fi if [ $WASM = "1" ]; then if [ $PROD = "1" ]; then - echo building hyperquark for production... + echo "building hyperquark (compiler) for production..." cargo build --target=wasm32-unknown-unknown --release ${QUIET:+--quiet} echo running wasm-bindgen... - wasm-bindgen target/wasm32-unknown-unknown/release/hyperquark.wasm --out-dir=js + wasm-bindgen target/wasm32-unknown-unknown/release/hyperquark.wasm --out-dir=js/compiler + echo "building hyperquark (no compiler) for production..." + cargo build --target=wasm32-unknown-unknown --release ${QUIET:+--quiet} --no-default-features + echo running wasm-bindgen... + wasm-bindgen target/wasm32-unknown-unknown/release/hyperquark.wasm --out-dir=js/no-compiler else - echo building hyperquark for development... + echo "building hyperquark (compiler) for development..." cargo build --target=wasm32-unknown-unknown ${QUIET:+--quiet} echo running wasm-bindgen... - wasm-bindgen target/wasm32-unknown-unknown/debug/hyperquark.wasm --out-dir=js + wasm-bindgen target/wasm32-unknown-unknown/debug/hyperquark.wasm --out-dir=js/compiler + echo "building hyperquark (no compiler) for development..." + cargo build --target=wasm32-unknown-unknown ${QUIET:+--quiet} --no-default-features + echo running wasm-bindgen... + wasm-bindgen target/wasm32-unknown-unknown/debug/hyperquark.wasm --out-dir=js/no-compiler fi + mv $(cargo outdir --no-names --quiet)/imports.ts js/imports.ts + node opcodes.mjs fi if [ $WOPT = "1" ]; then - echo running wasm-opt... - wasm-opt -Oz js/hyperquark_bg.wasm -o js/hyperquark_bg.wasm - wasm-opt -Os -g js/hyperquark_bg.wasm -o js/hyperquark_bg.wasm + echo running wasm-opt -Os... + wasm-opt -Os -g js/compiler/hyperquark_bg.wasm -o js/compiler/hyperquark_bg.wasm + wasm-opt -Os -g js/no-compiler/hyperquark_bg.wasm -o js/no-compiler/hyperquark_bg.wasm fi if [ $WOPT = "2" ]; then - echo running wasm-opt... - wasm-opt -Oz -g js/hyperquark_bg.wasm -o js/hyperquark_bg.wasm + echo running wasm-opt -Oz... + wasm-opt -Oz -g js/compiler/hyperquark_bg.wasm -o js/compiler/hyperquark_bg.wasm + wasm-opt -Oz -g js/no-compiler/hyperquark_bg.wasm -o js/no-compiler/hyperquark_bg.wasm fi if [ $VITE = "1" ]; then echo running npm build... npm run build -fi \ No newline at end of file +fi +echo finished! \ No newline at end of file diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..88c73d01 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,4 @@ +allowed-duplicate-crates = ["syn"] +too-many-lines-threshold = 150 +allow-panic-in-tests = true +allow-unwrap-in-tests = true \ No newline at end of file diff --git a/enum-field-getter/Cargo.toml b/enum-field-getter/Cargo.toml index e21eb2b0..bf9caf9f 100644 --- a/enum-field-getter/Cargo.toml +++ b/enum-field-getter/Cargo.toml @@ -3,7 +3,7 @@ name = "enum-field-getter" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" -repository = "hhttps://github.com/HyperQuark/hyperquark/tree/main/enum-field-getter" +repository = "https://github.com/HyperQuark/hyperquark/tree/main/enum-field-getter" authors = ["pufferfish101007"] description = "A derive macro to create mutable and immutable getters for tuple/struct members of enum variants" diff --git a/enum-field-getter/src/lib.rs b/enum-field-getter/src/lib.rs index 09b52f4d..637dc7e5 100644 --- a/enum-field-getter/src/lib.rs +++ b/enum-field-getter/src/lib.rs @@ -42,7 +42,7 @@ pub fn enum_field_getter(stream: TokenStream) -> TokenStream { tuple_field_info.entry(i).and_modify(|info| { let (ty, used_variants) = info; if quote!{#field_ty}.to_string() != quote!{#ty}.to_string() { - emit_warning!(field, "fields must be the same type across all variants - no getter will be emitted for this field"); + 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()); tuple_incompatible.insert(i); } else { used_variants.push(variant.ident.to_string()); diff --git a/js/cast/float2string.ts b/js/cast/float2string.ts new file mode 100644 index 00000000..eeb19af5 --- /dev/null +++ b/js/cast/float2string.ts @@ -0,0 +1,3 @@ +export function float2string(s: number): string { + return s.toString(); +} \ No newline at end of file diff --git a/js/cast/int2string.ts b/js/cast/int2string.ts new file mode 100644 index 00000000..b33b27e6 --- /dev/null +++ b/js/cast/int2string.ts @@ -0,0 +1,3 @@ +export function int2string(s: number): string { + return s.toString(); +} \ No newline at end of file diff --git a/js/cast/string2float.ts b/js/cast/string2float.ts new file mode 100644 index 00000000..1bc9e404 --- /dev/null +++ b/js/cast/string2float.ts @@ -0,0 +1,3 @@ +export function string2float(s: string): number { + return parseFloat(s); +} \ No newline at end of file diff --git a/js/looks/say_debug_float.ts b/js/looks/say_debug_float.ts new file mode 100644 index 00000000..5c60d03b --- /dev/null +++ b/js/looks/say_debug_float.ts @@ -0,0 +1,5 @@ +import { target_names } from "../shared"; + +export function say_debug_float(data: number, target_idx: number): void { + console.log('%s says: %d', target_names()[target_idx], data); +} \ No newline at end of file diff --git a/js/looks/say_debug_int.ts b/js/looks/say_debug_int.ts new file mode 100644 index 00000000..7eb74e9c --- /dev/null +++ b/js/looks/say_debug_int.ts @@ -0,0 +1,5 @@ +import { target_names } from "../shared"; + +export function say_debug_int(data: number, target_idx: number): void { + console.log('%s says: %d', target_names()[target_idx], data); +} \ No newline at end of file diff --git a/js/looks/say_debug_string.ts b/js/looks/say_debug_string.ts new file mode 100644 index 00000000..d0d2c5cb --- /dev/null +++ b/js/looks/say_debug_string.ts @@ -0,0 +1,5 @@ +import { target_names } from "../shared"; + +export function say_debug_string(data: string, target_idx: number): void { + console.log('%s says: %s', target_names()[target_idx], data); +} \ No newline at end of file diff --git a/js/looks/say_float.ts b/js/looks/say_float.ts new file mode 100644 index 00000000..f2e2eb82 --- /dev/null +++ b/js/looks/say_float.ts @@ -0,0 +1,5 @@ +import { update_bubble } from "../shared"; + +export function say_float(data: number, target_idx: number): void { + update_bubble(target_idx, "say", data.toString()); +} \ No newline at end of file diff --git a/js/looks/say_int.ts b/js/looks/say_int.ts new file mode 100644 index 00000000..0eef4c79 --- /dev/null +++ b/js/looks/say_int.ts @@ -0,0 +1,5 @@ +import { update_bubble } from "../shared"; + +export function say_int(data: number, target_idx: number): void { + update_bubble(target_idx, "say", data.toString()); +} \ No newline at end of file diff --git a/js/looks/say_string.ts b/js/looks/say_string.ts new file mode 100644 index 00000000..f478b025 --- /dev/null +++ b/js/looks/say_string.ts @@ -0,0 +1,5 @@ +import { update_bubble } from "../shared"; + +export function say_string(data: string, target_idx: number): void { + update_bubble(target_idx, "say", data); +} \ No newline at end of file diff --git a/js/looks/think_debug_float.ts b/js/looks/think_debug_float.ts new file mode 100644 index 00000000..2441325d --- /dev/null +++ b/js/looks/think_debug_float.ts @@ -0,0 +1,5 @@ +import { target_names } from "../shared"; + +export function think_debug_float(data: number, target_idx: number): void { + console.log('%s thinks: %d', target_names()[target_idx], data); +} \ No newline at end of file diff --git a/js/looks/think_debug_int.ts b/js/looks/think_debug_int.ts new file mode 100644 index 00000000..f74487da --- /dev/null +++ b/js/looks/think_debug_int.ts @@ -0,0 +1,5 @@ +import { target_names } from "../shared"; + +export function think_debug_int(data: number, target_idx: number): void { + console.log('%s thinks: %d', target_names()[target_idx], data); +} \ No newline at end of file diff --git a/js/looks/think_debug_string.ts b/js/looks/think_debug_string.ts new file mode 100644 index 00000000..44e54dc3 --- /dev/null +++ b/js/looks/think_debug_string.ts @@ -0,0 +1,5 @@ +import { target_names } from "../shared"; + +export function think_debug_string(data: string, target_idx: number): void { + console.log('%s thinks: %s', target_names()[target_idx], data); +} \ No newline at end of file diff --git a/js/looks/think_float.ts b/js/looks/think_float.ts new file mode 100644 index 00000000..42a75f20 --- /dev/null +++ b/js/looks/think_float.ts @@ -0,0 +1,5 @@ +import { update_bubble } from "../shared"; + +export function think_float(data: number, target_idx: number): void { + update_bubble(target_idx, "think", data.toString()); +} \ No newline at end of file diff --git a/js/looks/think_int.ts b/js/looks/think_int.ts new file mode 100644 index 00000000..6ddfb219 --- /dev/null +++ b/js/looks/think_int.ts @@ -0,0 +1,5 @@ +import { update_bubble } from "../shared"; + +export function think_int(data: number, target_idx: number): void { + update_bubble(target_idx, "think", data.toString()); +} \ No newline at end of file diff --git a/js/looks/think_string.ts b/js/looks/think_string.ts new file mode 100644 index 00000000..08dd1dea --- /dev/null +++ b/js/looks/think_string.ts @@ -0,0 +1,5 @@ +import { update_bubble } from "../shared"; + +export function think_string(data: string, target_idx: number): void { + update_bubble(target_idx, "think", data); +} \ No newline at end of file diff --git a/js/operator/contains.ts b/js/operator/contains.ts new file mode 100644 index 00000000..ca81e300 --- /dev/null +++ b/js/operator/contains.ts @@ -0,0 +1,3 @@ +export function contains(left: string, right: string): boolean { + return left.toLowerCase().includes(right.toLowerCase()); +} \ No newline at end of file diff --git a/js/operator/eq_string.ts b/js/operator/eq_string.ts new file mode 100644 index 00000000..d273ef36 --- /dev/null +++ b/js/operator/eq_string.ts @@ -0,0 +1,3 @@ +export function eq_string(left: string, right: string): boolean { + return left.toLowerCase() === right.toLowerCase(); +} \ No newline at end of file diff --git a/js/operator/gt_string.ts b/js/operator/gt_string.ts new file mode 100644 index 00000000..9d590b72 --- /dev/null +++ b/js/operator/gt_string.ts @@ -0,0 +1,3 @@ +export function gt_string(left: string, right: string): boolean { + return left.toLowerCase() > right.toLowerCase(); +} \ No newline at end of file diff --git a/js/operator/lt_string.ts b/js/operator/lt_string.ts new file mode 100644 index 00000000..6bfb6781 --- /dev/null +++ b/js/operator/lt_string.ts @@ -0,0 +1,3 @@ +export function lt_string(left: string, right: string): boolean { + return left.toLowerCase() < right.toLowerCase(); +} \ No newline at end of file diff --git a/js/sensing/dayssince2000.ts b/js/sensing/dayssince2000.ts new file mode 100644 index 00000000..8bede028 --- /dev/null +++ b/js/sensing/dayssince2000.ts @@ -0,0 +1,10 @@ +export function dayssince2000(): number { + // https://github.com/scratchfoundation/scratch-vm/blob/f10ab17bf351939153d9d0a17c577b5ba7b3c908/src/blocks/scratch3_sensing.js#L252 + const msPerDay = 24 * 60 * 60 * 1000; + const start = new Date(2000, 0, 1); // Months are 0-indexed. + const today = new Date(); + const dstAdjust = today.getTimezoneOffset() - start.getTimezoneOffset(); + let mSecsSinceStart = today.valueOf() - start.valueOf(); + mSecsSinceStart += ((today.getTimezoneOffset() - dstAdjust) * 60 * 1000); + return mSecsSinceStart / msPerDay; + } \ No newline at end of file diff --git a/js/shared.ts b/js/shared.ts new file mode 100644 index 00000000..fc3b0a8c --- /dev/null +++ b/js/shared.ts @@ -0,0 +1,42 @@ +let _target_names: Array = []; +let _setup = false; +let _target_bubbles; +let _renderer; + +export function setup(new_target_names: Array, renderer: object) { + _target_names = new_target_names; + _target_bubbles = _target_names.map(_ => null); + console.log(_target_names, _target_bubbles) + _renderer = renderer; + _setup = true; +} + +export function is_setup(): boolean { + return _setup; +} + +function check_setup() { + if (!_setup) { + throw new Error("shared state must be set up before use!") + } +} + +export function target_names(): Array { + check_setup(); + return _target_names +} + +export function update_bubble(target_index: number, verb: "say" | "think", text: string) { + check_setup(); + if (_target_bubbles[target_index] === null) { + _target_bubbles[target_index] = _renderer.createSkin( + "text", + "sprite", + verb, + text, + false + ); + } else { + _renderer.updateTextSkin(_target_bubbles[target_index][0], verb, text, false); + } +} \ No newline at end of file diff --git a/js/wasm-js-string/concat.ts b/js/wasm-js-string/concat.ts new file mode 100644 index 00000000..03fdaa47 --- /dev/null +++ b/js/wasm-js-string/concat.ts @@ -0,0 +1,3 @@ +export function concat(left: string, right: string): string { + return left.concat(right); +} \ No newline at end of file diff --git a/js/wasm-js-string/length.ts b/js/wasm-js-string/length.ts new file mode 100644 index 00000000..74a21af4 --- /dev/null +++ b/js/wasm-js-string/length.ts @@ -0,0 +1,3 @@ +export function length(string: string): number { + return string.length; +} \ No newline at end of file diff --git a/js/wasm-js-string/substring.ts b/js/wasm-js-string/substring.ts new file mode 100644 index 00000000..4527e982 --- /dev/null +++ b/js/wasm-js-string/substring.ts @@ -0,0 +1,3 @@ +export function substring(string: string, start: number, end: number): string { + return string.substring(start, end); +} \ No newline at end of file diff --git a/opcodes.mjs b/opcodes.mjs new file mode 100644 index 00000000..13ec85bb --- /dev/null +++ b/opcodes.mjs @@ -0,0 +1,5 @@ +import { readFile, writeFile } from 'node:fs/promises'; + +let blocksRs = (await readFile('./src/ir/blocks.rs', 'utf8')); +const opcodes = [...new Set(blocksRs.match(/BlockOpcode::[a-z_0-9]+? (?==>)/g).map(op => op.replace('BlockOpcode::', '').trim()))].sort() +await writeFile('./js/opcodes.js', `export const opcodes = ${JSON.stringify(opcodes)};`); \ No newline at end of file diff --git a/package.json b/package.json index eb4ed528..347d087e 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,14 @@ "preview": "vite preview" }, "dependencies": { + "binaryen": "github:pufferfish101007/binaryen.js#non-nullable-table", "pinia": "^2.1.3", "scratch-parser": "^5.1.1", "scratch-sb1-converter": "^0.2.7", "vite-plugin-wasm": "^3.2.2", "vue": "^3.3.4", - "vue-router": "^4.2.2" + "vue-router": "^4.2.2", + "wasm-feature-detect": "^1.8.0" }, "devDependencies": { "@vitejs/plugin-vue": "^4.2.3", diff --git a/playground/App.vue b/playground/App.vue index 7b15a3da..337d5f6f 100644 --- a/playground/App.vue +++ b/playground/App.vue @@ -1,5 +1,8 @@ \ No newline at end of file diff --git a/playground/lib/imports.js b/playground/lib/imports.js new file mode 100644 index 00000000..d7e41d8b --- /dev/null +++ b/playground/lib/imports.js @@ -0,0 +1 @@ +export { imports } from '../../js/imports.ts'; \ No newline at end of file diff --git a/playground/lib/project-runner.js b/playground/lib/project-runner.js index 9d13d505..18aeca8b 100644 --- a/playground/lib/project-runner.js +++ b/playground/lib/project-runner.js @@ -1,4 +1,10 @@ import { getSettings } from './settings.js'; +import { imports } from './imports.js'; +import { useDebugModeStore } from '../stores/debug.js'; +import { setup as sharedSetup } from '../../js/shared.ts' +import { render } from 'vue'; + +const debugModeStore = useDebugModeStore(); function createSkin(renderer, type, layer, ...params) { let drawableId = renderer.createDrawable(layer.toString()); @@ -13,408 +19,196 @@ function createSkin(renderer, type, layer, ...params) { } const spriteInfoLen = 80; +let _setup = false; + +function setup(renderer, project_json, assets, target_names) { + if (_setup) return; + _setup = true; + renderer.getDrawable = id => renderer._allDrawables[id]; + renderer.getSkin = id => renderer._allSkins[id]; + renderer.createSkin = (type, layer, ...params) => createSkin(renderer, type, layer, ...params); + + const costumes = project_json.targets.map( + (target, index) => target.costumes.map( + ({ md5ext }) => assets[md5ext] + ) + ); + + const costumeNameMap = project_json.targets.map( + target => Object.fromEntries(target.costumes.map( + ({ name }, index) => [name, index] + )) + ); + + // @ts-ignore + window.renderer = renderer; + renderer.setLayerGroupOrdering(["background", "video", "pen", "sprite"]); + //window.open(URL.createObjectURL(new Blob([wasm_bytes], { type: "octet/stream" }))); + const pen_skin = createSkin(renderer, "pen", "pen")[0]; + + const target_skins = project_json.targets.map((target, index) => { + const realCostume = target.costumes[target.currentCostume]; + const costume = costumes[index][target.currentCostume]; + const [skin, drawableId] = createSkin(renderer, costume[0], 'sprite', costume[1], [realCostume.rotationCenterX, realCostume.rotationCenterY]); + const drawable = renderer.getDrawable(drawableId); + if (!target.is_stage) { + drawable.updateVisible(target.visible); + drawable.updatePosition([target.x, target.y]); + drawable.updateDirection(target.rotation); + drawable.updateScale([target.size, target.size]); + } + return [skin, drawableId]; + }); + console.log(target_skins) + + sharedSetup(target_names, renderer); +} // @ts-ignore export default async ( - { framerate = 30, renderer, wasm_bytes, target_names, string_consts, project_json, assets } = { - framerate: 30, + { framerate = 30, turbo, renderer, wasm_bytes, target_names, string_consts, project_json, assets } = { + framerate: 30, turbo: false, } ) => { - if (window.debug) window.open(URL.createObjectURL(new Blob([wasm_bytes], { type: 'application/wasm' }))); - const framerate_wait = Math.round(1000 / framerate); - let assert; - let exit; - let browser = false; - let output_div; - let text_div; - - renderer.getDrawable = id => renderer._allDrawables[id]; - renderer.getSkin = id => renderer._allSkins[id]; - - const costumes = project_json.targets.map( - (target, index) => target.costumes.map( - ({ md5ext }) => assets[md5ext] - ) - ); + if (debugModeStore.debug) window.open(URL.createObjectURL(new Blob([wasm_bytes], { type: 'application/wasm' }))); + const framerate_wait = Math.round(1000 / framerate); + let assert; + let exit; + let browser = false; + let output_div; + let text_div; - - const costumeNameMap = project_json.targets.map( - target => Object.fromEntries(target.costumes.map( - ({ name }, index) => [name, index] - )) - ); - - // @ts-ignore - window.renderer = renderer; - renderer.setLayerGroupOrdering(["background", "video", "pen", "sprite"]); - //window.open(URL.createObjectURL(new Blob([wasm_bytes], { type: "octet/stream" }))); - const pen_skin = createSkin(renderer, "pen", "pen")[0]; - if (typeof require === "undefined") { - browser = true; - output_div = document.querySelector("div#hq-output"); - text_div = (txt) => - Object.assign(document.createElement("div"), { textContent: txt }); - assert = (bool) => { - if (!bool) { - throw new AssertionError("Assertion failed"); - } - }; - exit = (_) => null; - } else { - exit = process.exit; - assert = require("node:assert") /*.strict*/; + setup(renderer, project_json, assets, target_names); + + console.log('green flag setup complete') + + let strings_tbl; + + let updatePenColor; + let start_time = 0; + let sprite_info_offset = 0; + + const settings = getSettings(); + const builtins = [...(settings['js-string-builtins'] ? ['js-string'] : [])] + + try { + if (!WebAssembly.validate(wasm_bytes, { + builtins + })) { + throw Error(); } - let last_output; - let strings_tbl; - const target_bubbles = target_names.map(_ => null); - - const target_skins = project_json.targets.map((target, index) => { - const realCostume = target.costumes[target.currentCostume]; - const costume = costumes[index][target.currentCostume]; - const [skin, drawableId] = createSkin(renderer, costume[0], 'sprite', costume[1], [realCostume.rotationCenterX, realCostume.rotationCenterY]); - const drawable = renderer.getDrawable(drawableId); - if (!target.is_stage) { - drawable.updateVisible(target.visible); - drawable.updatePosition([target.x, target.y]); - drawable.updateDirection(target.rotation); - drawable.updateScale([target.size, target.size]); - } - return [skin, drawableId]; + } catch { + try { + new WebAssembly.Module(wasm_bytes); + throw new Error("invalid WASM module"); + } catch (e) { + throw new Error("invalid WASM module: " + e.message); + } + } + function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); }); - console.log(target_skins) - - const wasm_val_to_js = (type, value_i64) => { - return type === 0 - ? new Float64Array(new BigInt64Array([value_i64]).buffer)[0] - : type === 1 - ? Boolean(value_i64) - : type === 2 - ? strings_tbl.get(Number(value_i64)) - : null; - }; - const wasm_output = (...args) => { - const val = wasm_val_to_js(...args); - if (!browser) { - console.log("output: \x1b[34m%s\x1b[0m", val); - } else { - output_div.appendChild(text_div("output: " + String(val))); - } - last_output = val; - }; - const assert_output = (...args) => { - /*assert.equal(last_output, wasm_val_to_js(...args));*/ - const val = wasm_val_to_js(...args); - if (!browser) { - console.log("assert: \x1b[34m%s\x1b[0m", val); - } else { - output_div.appendChild(text_div("assert: " + String(val))); - } - }; - const targetOutput = (targetIndex, verb, text) => { - console.log("a", targetIndex, verb, text); - let targetName = target_names[targetIndex]; - if (!browser) { - console.log( - `\x1b[1;32m${targetName} ${verb}:\x1b[0m \x1b[35m${text}\x1b[0m` - ); - } else { - if (target_bubbles[targetIndex] === null) { - target_bubbles[targetIndex] = createSkin( - renderer, - "text", - "sprite", - verb, - text, - false - ); - } else { - renderer.updateTextSkin(target_bubbles[targetIndex][0], verb, text, false); - } + } + function waitAnimationFrame() { + return new Promise((resolve) => { + requestAnimationFrame(resolve); + }); + } + WebAssembly.instantiate(wasm_bytes, imports, { + builtins + }) + .then(async ({ instance }) => { + const { + flag_clicked, + tick, + memory, + strings, + step_funcs, + vars_num, + threads_count, + requests_refresh, + upc, + threads, + noop, + unreachable_dbg + } = instance.exports; + strings.grow(Object.entries(string_consts).length); + for (const [i, str] of Object.entries(string_consts || {})) { + // @ts-ignore + strings.set(i, str); } - }; - let updatePenColor; - let start_time = 0; - let sprite_info_offset = 0; - - const settings = getSettings(); - const builtins = [...(settings['js-string-builtins'] ? ['js-string'] : [])] - - const importObject = { - dbg: { - log: wasm_output, - assert: assert_output, - logi32(i32) { - console.log("logi32: %d", i32); - //console.log("logi32: \x1b[33m%d\x1b[0m", i32); - return i32; - }, - }, - runtime: { - looks_say: (ty, val, targetIndex) => { - targetOutput(targetIndex, "say", wasm_val_to_js(ty, val).toString()); - }, - looks_think: (ty, val, targetIndex) => { - targetOutput(targetIndex, "think", wasm_val_to_js(ty, val).toString()); - }, - /*operator_equals: (ty1, val1, ty2, val2) => { - if (ty1 === ty2 && val1 === val2) return true; - let j1 = wasm_val_to_js(ty1, val1); - let j2 = wasm_val_to_js(ty2, val2); - if (typeof j1 === "string") j1 = j1.toLowerCase(); - if (typeof j2 === "string") j2 = j2.toLowerCase(); - return j1 == j2; - },*/ - operator_random: (lower, upper) => - Math.random() * (upper - lower) + lower, - operator_letterof: (idx, str) => - str.toString()[idx - 1] ?? "", - operator_contains: (str1, str2) => - str1 - .toString() - .toLowerCase() - .includes(str2.toString().toLowerCase()), - mathop_sin: (n) => - parseFloat(Math.sin((Math.PI * n) / 180).toFixed(10)), - mathop_cos: (n) => - parseFloat(Math.cos((Math.PI * n) / 180).toFixed(10)), - mathop_tan: (n) => { - /* https://github.com/scratchfoundation/scratch-vm/blob/f1f10e0aa856fef6596a622af72b49e2f491f937/src/util/math-util.js#L53-65 */ - n = n % 360; - switch (n) { - case -270: - case 90: - return Infinity; - case -90: - case 270: - return -Infinity; - default: - return parseFloat(Math.tan((Math.PI * n) / 180).toFixed(10)); - } - }, - mathop_asin: (n) => (Math.asin(n) * 180) / Math.PI, - mathop_acos: (n) => (Math.acos(n) * 180) / Math.PI, - mathop_atan: (n) => (Math.atan(n) * 180) / Math.PI, - mathop_ln: (n) => Math.log(n), - mathop_log: (n) => Math.log(n) / Math.LN10, - mathop_pow_e: (n) => Math.exp(n), - mathop_pow10: (n) => Math.pow(10, n), - sensing_timer: () => (Date.now() - start_time) / 1000, - sensing_resettimer: () => (start_time = Date.now()), - sensing_dayssince2000 () { - // https://github.com/scratchfoundation/scratch-vm/blob/f10ab17bf351939153d9d0a17c577b5ba7b3c908/src/blocks/scratch3_sensing.js#L252 - const msPerDay = 24 * 60 * 60 * 1000; - const start = new Date(2000, 0, 1); // Months are 0-indexed. - const today = new Date(); - const dstAdjust = today.getTimezoneOffset() - start.getTimezoneOffset(); - let mSecsSinceStart = today.valueOf() - start.valueOf(); - mSecsSinceStart += ((today.getTimezoneOffset() - dstAdjust) * 60 * 1000); - return mSecsSinceStart / msPerDay; - }, - pen_clear: () => renderer.penClear(pen_skin), - pen_down: (radius, x, y, r, g, b, a) => - renderer.penPoint( - pen_skin, - { - diameter: radius, // awkward variable naming moment - color4f: [r, g, b, a], - }, - x, - y - ), - pen_lineto: (radius, x1, y1, x2, y2, r, g, b, a) => renderer.penLine( - pen_skin, - { - diameter: radius, - color4f: [r,g,b,a], - }, - x1, y1, x2, y2, - ), - pen_up: () => null, - pen_setcolor: () => null, - pen_changecolorparam: () => null, - pen_setcolorparam: (param, val, i) => { - switch (param) { - case "color": - new DataView(memory.buffer).setFloat32( - sprite_info_offset + (i - 1) * spriteInfoLen + 16, - val, - true - ); - break; - case "saturation": - new DataView(memory.buffer).setFloat32( - sprite_info_offset + (i - 1) * spriteInfoLen + 20, - val, - true - ); - break; - case "brightness": - new DataView(memory.buffer).setFloat32( - sprite_info_offset + (i - 1) * spriteInfoLen + 24, - val, - true - ); - break; - case "transparency": - new DataView(memory.buffer).setFloat32( - sprite_info_offset + (i - 1) * spriteInfoLen + 28, - val, - true - ); - break; - default: - console.warn(`can\'t update invalid color param ${param}`); + updatePenColor = (i) => null;//upc(i - 1); + strings_tbl = strings; + window.memory = memory; + window.flag_clicked = flag_clicked; + window.tick = tick; + window.stop = () => { + if (typeof threads == "undefined") { + let memArr = new Uint32Array(memory.buffer); + for (let i = 0; i < threads_count.value; i++) { + memArr[i] = 0; } - updatePenColor(i); - }, - pen_changesize: () => null, - pen_changehue: () => null, - pen_sethue: () => null, - emit_sprite_pos_change: (i) => { - const x = new Float64Array(memory.buffer)[(sprite_info_offset + (i - 1) * spriteInfoLen) / 8]; - const y = new Float64Array(memory.buffer)[(sprite_info_offset + (i - 1) * spriteInfoLen + 8) / 8]; - renderer.getDrawable(target_skins[i][1]).updatePosition([x, y]); - }, - emit_sprite_x_change: (i) => null, - emit_sprite_y_change: (i) => null, - emit_sprite_size_change: (i) => { - const size = new Float64Array(memory.buffer)[(sprite_info_offset + (i - 1) * spriteInfoLen + 64) / 8]; - renderer.getDrawable(target_skins[i][1]).updateScale([size, size]); - }, - emit_sprite_costume_change: (i) => { - const costumeNum = new Int32Array(memory.buffer)[(sprite_info_offset + (i - 1) * spriteInfoLen + 60) / 4]; - const costume = costumes[i][costumeNum]; - renderer.getSkin(target_skins[i][0]).setSVG(costume[1]); - }, - emit_sprite_rotation_change: (i) => { - const rot = new Float64Array(memory.buffer)[(sprite_info_offset + (i - 1) * spriteInfoLen + 72) / 8] - renderer.getDrawable(target_skins[i][1]).updateDirection(rot); - }, - emit_sprite_visibility_change: (i) => { - renderer.getDrawable(target_skins[i][1]).updateVisible(!!new Uint8Array(memory.buffer)[sprite_info_offset + (i - 1) * spriteInfoLen + 57]); - }, - }, - cast: { - stringtofloat: parseFloat, - stringtobool: Boolean, - floattostring: (i) => i.toString(), - }, - // for if the string builtina proposal isn't available - "wasm:js-string": { - equals(a, b) { - return a === b; - }, - length(a) { - return a.length; - }, - concat(a, b){ - return a.concat(b); + } else { + for (let i = 0; i < threads.length; i++) { + threads.set(i, noop); } } - }; - try { - assert(WebAssembly.validate(wasm_bytes, { - builtins - })); - } catch { + threads_count.value = 0; + }; + // @ts-ignore + //sprite_info_offset = vars_num.value * 16 + thn_offset + 4; + const dv = new DataView(memory.buffer); + /*for (let i = 0; i < target_names.length - 1; i++) { + dv.setFloat32( + sprite_info_offset + i * spriteInfoLen + 16, + 66.66, + true + ); + dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 20, 100, true); + dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 24, 100, true); + dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 28, 0, true); + dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 40, 1, true); + dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 44, 1, true); + dv.setFloat64(sprite_info_offset + i * spriteInfoLen + 48, 1, true); + }*/ try { - new WebAssembly.Module(wasm_bytes); - throw new Error("invalid WASM module"); - } catch (e) { - throw new Error("invalid WASM module: " + e.message); + // expose the module to devtools + unreachable_dbg(); + } catch (error) { + console.info('synthetic error to expose wasm modulee to devtools:', error) } - } - function sleep(ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - } - function waitAnimationFrame() { - return new Promise((resolve) => { - requestAnimationFrame(resolve); - }); - } - WebAssembly.instantiate(wasm_bytes, importObject, { - builtins - }) - .then(async ({ instance }) => { - const { - green_flag, - tick, - memory, - strings, - step_funcs, - vars_num, - rr_offset, - thn_offset, - upc, - unreachable_dbg - } = instance.exports; - for (const [i, str] of Object.entries(string_consts)) { - // @ts-ignore - strings.set(i, str); - } - updatePenColor = (i) => upc(i - 1); - strings_tbl = strings; - window.memory = memory; - window.stop = () => { - for (let i = 0; i < new Uint32Array(memory.buffer)[thn_offset.value / 4]; i++) { - new Uint32Array(memory.buffer)[(i + sprite_info_offset + spriteInfoLen * (target_names.length - 1)) / 4] = 0; - } - }; + flag_clicked(); + start_time = Date.now(); + console.log("green_flag()"); + $outertickloop: while (true) { + renderer.draw(); + const thisTickStartTime = Date.now(); // @ts-ignore - sprite_info_offset = vars_num.value * 16 + thn_offset + 4; - const dv = new DataView(memory.buffer); - for (let i = 0; i < target_names.length - 1; i++) { - dv.setFloat32( - sprite_info_offset + i * spriteInfoLen + 16, - 66.66, - true - ); - dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 20, 100, true); - dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 24, 100, true); - dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 28, 0, true); - dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 40, 1, true); - dv.setFloat32(sprite_info_offset + i * spriteInfoLen + 44, 1, true); - dv.setFloat64(sprite_info_offset + i * spriteInfoLen + 48, 1, true); - } - try { - // expose the module to devtools - unreachable_dbg(); - } catch (error) { - console.info('synthetic error to exose wasm modulee to devtools:', error) - } - green_flag(); - start_time = Date.now(); - console.log("green_flag()"); - $outertickloop: while (true) { - renderer.draw(); - const thisTickStartTime = Date.now(); + $innertickloop: do {//for (const _ of [1]) { // @ts-ignore - $innertickloop: do {//for (const _ of [1]) { - // @ts-ignore - tick(); - // @ts-ignore - if (new Uint32Array(memory.buffer)[thn_offset.value / 4] === 0) { - break $outertickloop; - } - } while ( - Date.now() - thisTickStartTime < framerate_wait * 0.8 && - new Uint8Array(memory.buffer)[rr_offset.value] === 0 - ) + tick(); // @ts-ignore - new Uint8Array(memory.buffer)[rr_offset.value] = 0; - if (framerate_wait > 0) { - await sleep( - Math.max(0, framerate_wait - (Date.now() - thisTickStartTime)) - ); - } else { - await waitAnimationFrame(); + if (threads_count.value === 0) { + break $outertickloop; } + } while ( + (Date.now() - thisTickStartTime) < (framerate_wait * 0.8) && + (!turbo && requests_refresh.value === 0) + ) + // @ts-ignore + requests_refresh.value = 0; + if (framerate_wait > 0) { + await sleep( + Math.max(0, framerate_wait - (Date.now() - thisTickStartTime)) + ); + } else { + await waitAnimationFrame(); } - }) - .catch((e) => { - throw new Error("error when instantiating module:\n" + e.stack); - /*exit(1);*/ - }); - }; + } + }) + .catch((e) => { + throw new Error("error when instantiating module:\n" + e.stack); + /*exit(1);*/ + }); +}; diff --git a/playground/lib/settings.js b/playground/lib/settings.js index acb639e8..fd15fb65 100644 --- a/playground/lib/settings.js +++ b/playground/lib/settings.js @@ -1,25 +1,91 @@ -const defaultSettings = { - 'js-string-builtins': true, -} +import * as hyperquarkExports from '../../js/no-compiler/hyperquark.js'; +import { WasmFlags, WasmFeature, all_wasm_features, wasm_feature_detect_name } from '../../js/no-compiler/hyperquark.js'; +import * as WasmFeatureDetect from 'wasm-feature-detect'; +export { WasmFlags }; + +console.log(WasmFeature) +window.hyperquarkExports = hyperquarkExports; + +export const supportedWasmFeatures = await getSupportedWasmFeatures(); +export const defaultSettings = new WasmFlags(Array.from(supportedWasmFeatures, (feat) => WasmFeature[feat])); +const defaultSettingsObj = defaultSettings.to_js(); -export const settingsInfo = { - 'js-string-builtins': { - name: 'Use the JS String Builtins proposal (where possible)', - description: 'May achieve a slight performance gain in some string operations', - type: 'switch' +window.defaultSettings = defaultSettings; + +function settingsInfoFromType(type) { + if (type === "boolean") { + return { + type: "checkbox" + } + } else if (type in hyperquarkExports) { + return { + type: "radio", + options: Object.keys(hyperquarkExports[type]).filter(key => typeof key === 'string' && !/\d+/.test(key)), + enum_obj: hyperquarkExports[type], + } + } else { + return null; } } -export function getSettings(){ +export const settingsInfo = Object.fromEntries(Object.entries(Object.getOwnPropertyDescriptors(WasmFlags.prototype)) + .filter(([_, descriptor]) => typeof descriptor.get === 'function') + .map(([key, _]) => key) + .map(key => { + let flag_info = WasmFlags.flag_info(key); + return [key, { + flag_info, + ...settingsInfoFromType(flag_info.ty) + }] + })); + +/** + * @returns {WasmFlags} + */ +export function getSettings() { let store = localStorage["settings"]; try { - return { ...defaultSettings, ...JSON.parse(store) }; + return WasmFlags.from_js({ ...defaultSettingsObj, ...JSON.parse(store) }); } catch { return defaultSettings; } } -export function saveSettings(settings){ +/** + * @param {WasmFlags} settings + */ +export function saveSettings(settings) { + console.log(settings.to_js()) + localStorage['settings'] = JSON.stringify(settings.to_js()); +} + +/** + * @returns {Set} + */ +export async function getSupportedWasmFeatures() { + const featureSet = new Set(); + for (const feature of all_wasm_features()) { + if (await WasmFeatureDetect[wasm_feature_detect_name(feature)]()) { + featureSet.add(WasmFeature[feature]); + } + } + return featureSet; +} + +console.log(await getSupportedWasmFeatures()) + +/** + * @returns {Set} + */ +export function getUsedWasmFeatures() { + const settings = getSettings().to_js(); console.log(settings) - localStorage['settings'] = JSON.stringify(settings); + const featureSet = new Set(); + for (const [id, info] of Object.entries(settingsInfo)) { + const theseFeatures = info.flag_info.wasm_features(settings[id])?.map?.((num) => WasmFeature[num]); + for (const feature of theseFeatures || []) { + featureSet.add(feature); + } + } + return featureSet; } \ No newline at end of file diff --git a/playground/router/index.js b/playground/router/index.js index 090ab8ab..e1b04102 100644 --- a/playground/router/index.js +++ b/playground/router/index.js @@ -1,5 +1,5 @@ -import { createRouter, createWebHistory } from 'vue-router'; -import { h, ref } from 'vue'; +import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'; +import { h, ref, onMounted } from 'vue'; import Loading from '../components/Loading.vue'; let componentCache = Object.setPrototypeOf({}, null); @@ -21,7 +21,7 @@ const view = (name) => ({ }); const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), + history: (import.meta.env.VITE_HASH_HISTORY ? createWebHashHistory : createWebHistory)(import.meta.env.BASE_URL), routes: [ { path: '/', @@ -55,6 +55,6 @@ const router = createRouter({ component: view('Settings'), } ] -}) +}); export default router; diff --git a/playground/stores/debug.js b/playground/stores/debug.js new file mode 100644 index 00000000..a0e4076a --- /dev/null +++ b/playground/stores/debug.js @@ -0,0 +1,18 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' + +export const useDebugModeStore = defineStore('debugMode', () => { + const debug = ref(typeof new URLSearchParams(window.location.search).get('debug') === 'string'); + const toggleDebug = () => { + debug.value = !debug.value; + if (!erudaEnabled && debug.value) { + eruda.init(); + } + } + let erudaEnabled = false; + if (debug.value) { + eruda.init(); + erudaEnabled = true; + } + return { debug, toggleDebug }; +}) diff --git a/playground/views/HomeView.vue b/playground/views/HomeView.vue index fe1f3d88..ddbe03c0 100644 --- a/playground/views/HomeView.vue +++ b/playground/views/HomeView.vue @@ -17,13 +17,21 @@ import ProjectInput from '../components/ProjectInput.vue'
+
+

Currently implemented blocks:

+
    +
  • + {{ opcode }}
@@ -56,8 +69,32 @@ import ProjectInput from '../components/ProjectInput.vue' font-style: italic; text-decoration: line-through; } - ul#featured-projects-container { + ul { list-style: none; padding: inherit; } + li.block-category-looks { + color:#9966FF; + } + li.block-category-operator { + color: #59C059; + } + li.block-category-motion { + color: #4C97FF; + } + li.block-category-control { + color: #FFAB19; + } + li.block-category-pen { + color: #0fBD8C; + } + li.block-category-data { + color: #FF8C1A; + } + li.block-category-sensing { + color: #5CB1D6; + } + li.block-category-procedures, li.block-category-argument { + color: #FF6680; + } \ No newline at end of file diff --git a/playground/views/ProjectFileView.vue b/playground/views/ProjectFileView.vue index 13ffef19..e2466938 100644 --- a/playground/views/ProjectFileView.vue +++ b/playground/views/ProjectFileView.vue @@ -1,22 +1,30 @@ \ No newline at end of file diff --git a/playground/views/Settings.vue b/playground/views/Settings.vue index dd2311fb..9f6a0ee2 100644 --- a/playground/views/Settings.vue +++ b/playground/views/Settings.vue @@ -3,35 +3,125 @@

Compiler settings

-
-
-

{{ settingsInfo[id].name }}

-

{{ settingsInfo[id].description }}

-
-
- + +

Currently used WASM features:

+
    +
  • {{ feature }}
  • +
  • None
  • +
+
+
+
+

+ + {{ settingsInfo[id].flag_info.name }} + +

+
+

+
+ + + {{ option }}
+
+
- \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..271800cb --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 78c8df70..9e01d1a5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,15 +1,19 @@ -use alloc::string::String; +use core::cell::{BorrowError, BorrowMutError}; + +use alloc::boxed::Box; use wasm_bindgen::JsValue; -#[derive(Debug)] // todo: get rid of this once all expects are gone +pub type HQResult = Result; + +#[derive(Clone, Debug)] // todo: get rid of this once all expects are gone pub struct HQError { pub err_type: HQErrorType, - pub msg: String, - pub file: String, + pub msg: Box, + pub file: Box, pub line: u32, pub column: u32, } -#[derive(Debug)] // todo: get rid of this once all expects are gone +#[derive(Clone, Debug, PartialEq, Eq)] // todo: get rid of this once all expects are gone pub enum HQErrorType { MalformedProject, InternalError, @@ -17,8 +21,8 @@ pub enum HQErrorType { } impl From for JsValue { - fn from(val: HQError) -> JsValue { - JsValue::from_str(match val.err_type { + fn from(val: HQError) -> Self { + Self::from_str(match val.err_type { 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), 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), 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), @@ -26,14 +30,47 @@ impl From for JsValue { } } +impl From for HQError { + fn from(_e: BorrowError) -> Self { + Self { + err_type: HQErrorType::InternalError, + msg: "couldn't borrow cell".into(), + file: file!().into(), + line: line!(), + column: column!(), + } + } +} + +impl From for HQError { + fn from(_e: BorrowMutError) -> Self { + Self { + err_type: HQErrorType::InternalError, + msg: "couldn't mutably borrow cell".into(), + file: file!().into(), + line: line!(), + column: column!(), + } + } +} + #[macro_export] +#[clippy::format_args] macro_rules! hq_todo { + () => {{ + return Err($crate::HQError { + err_type: $crate::HQErrorType::Unimplemented, + msg: "todo".into(), + file: file!().into(), + line: line!(), + column: column!() + }); + }}; ($($args:tt)+) => {{ - use $crate::alloc::string::ToString; return Err($crate::HQError { err_type: $crate::HQErrorType::Unimplemented, - msg: format!("{}", format_args!($($args)*)), - file: file!().to_string(), + msg: format!("{}", format_args!($($args)*)).into(), + file: file!().into(), line: line!(), column: column!() }); @@ -41,13 +78,13 @@ macro_rules! hq_todo { } #[macro_export] +#[clippy::format_args] macro_rules! hq_bug { ($($args:tt)+) => {{ - use $crate::alloc::string::ToString; return Err($crate::HQError { err_type: $crate::HQErrorType::InternalError, - msg: format!("{}", format_args!($($args)*)), - file: file!().to_string(), + msg: format!("{}", format_args!($($args)*)).into(), + file: file!().into(), line: line!(), column: column!() }); @@ -55,13 +92,71 @@ macro_rules! hq_bug { } #[macro_export] +#[clippy::format_args] +macro_rules! hq_assert { + ($expr:expr) => {{ + if !($expr) { + return Err($crate::HQError { + err_type: $crate::HQErrorType::InternalError, + msg: format!("Assertion failed: {}", stringify!($expr)).into(), + file: file!().into(), + line: line!(), + column: column!() + }); + }; + assert!($expr); + }}; + ($expr:expr, $($args:tt)+) => {{ + if !($expr) { + return Err($crate::HQError { + err_type: $crate::HQErrorType::InternalError, + msg: format!("Assertion failed: {}\nMessage: {}", stringify!($expr), format_args!($($args)*)).into(), + file: file!().into(), + line: line!(), + column: column!() + }); + }; + assert!($expr); + }}; +} + +#[macro_export] +#[clippy::format_args] +macro_rules! hq_assert_eq { + ($l:expr, $r:expr) => {{ + if $l != $r { + return Err($crate::HQError { + err_type: $crate::HQErrorType::InternalError, + msg: format!("Assertion failed: {} == {}\nLeft: {}\nRight: {}", stringify!($l), stringify!($r), $l, $r).into(), + file: file!().into(), + line: line!(), + column: column!() + }); + }; + assert_eq!($l, $r); + }}; + ($l:expr, $r:expr, $($args:tt)+) => {{ + if $l != $r { + return Err($crate::HQError { + err_type: $crate::HQErrorType::InternalError, + msg: format!("Assertion failed: {}\nLeft: {}\nRight: {}\nMessage: {}", stringify!($l), stringify!($r), $l, $r, format_args!($($args)*)).into(), + file: file!().into(), + line: line!(), + column: column!() + }); + }; + assert_eq!($l, $r); + }}; +} + +#[macro_export] +#[clippy::format_args] macro_rules! hq_bad_proj { ($($args:tt)+) => {{ - use $crate::alloc::string::ToString; return Err($crate::HQError { err_type: $crate::HQErrorType::MalformedProject, - msg: format!("{}", format_args!($($args)*)), - file: file!().to_string(), + msg: format!("{}", format_args!($($args)*)).into(), + file: file!().into(), line: line!(), column: column!() }); @@ -69,13 +164,14 @@ macro_rules! hq_bad_proj { } #[macro_export] +#[clippy::format_args] macro_rules! make_hq_todo { ($($args:tt)+) => {{ - use $crate::alloc::string::ToString; + use $crate::alloc::Box::ToBox; $crate::HQError { err_type: $crate::HQErrorType::Unimplemented, - msg: format!("{}", format_args!($($args)*)), - file: file!().to_string(), + msg: format!("{}", format_args!($($args)*)).into(), + file: file!().into(), line: line!(), column: column!() } @@ -83,13 +179,13 @@ macro_rules! make_hq_todo { } #[macro_export] +#[clippy::format_args] macro_rules! make_hq_bug { ($($args:tt)+) => {{ - use $crate::alloc::string::ToString; $crate::HQError { err_type: $crate::HQErrorType::InternalError, - msg: format!("{}", format_args!($($args)*)), - file: file!().to_string(), + msg: format!("{}", format_args!($($args)*)).into(), + file: file!().into(), line: line!(), column: column!() } @@ -97,13 +193,13 @@ macro_rules! make_hq_bug { } #[macro_export] +#[clippy::format_args] macro_rules! make_hq_bad_proj { ($($args:tt)+) => {{ - use $crate::alloc::string::ToString; $crate::HQError { err_type: $crate::HQErrorType::MalformedProject, - msg: format!("{}", format_args!($($args)*)), - file: file!().to_string(), + msg: format!("{}", format_args!($($args)*)).into(), + file: file!().into(), line: line!(), column: column!() } diff --git a/src/instructions.rs b/src/instructions.rs new file mode 100644 index 00000000..0ac402c2 --- /dev/null +++ b/src/instructions.rs @@ -0,0 +1,44 @@ +//! Contains information about instructions (roughly anaologus to blocks), +//! including input type validation, output type mapping, and WASM generation. + +#![allow( + clippy::unnecessary_wraps, + reason = "many functions here needlessly return `Result`s in order to keep type signatures consistent" +)] +#![allow( + clippy::needless_pass_by_value, + reason = "there are so many `Rc`s here which I don't want to change" +)] + +use crate::prelude::*; + +mod control; +mod data; +mod hq; +mod looks; +mod operator; +mod procedures; +mod sensing; + +#[macro_use] +mod tests; + +include!(concat!(env!("OUT_DIR"), "/ir-opcodes.rs")); + +mod input_switcher; +pub use input_switcher::wrap_instruction; + +pub use hq::r#yield::YieldMode; + +mod prelude { + pub use crate::ir::Type as IrType; + pub use crate::prelude::*; + pub use crate::wasm::{InternalInstruction, StepFunc}; + pub use wasm_encoder::{RefType, ValType}; + pub use wasm_gen::wasm; + + /// Canonical NaN + bit 33, + string pointer in bits 1-32 + pub const BOXED_STRING_PATTERN: i64 = 0x7FF8_0001 << 32; + /// Canonical NaN + bit 33, + i32 in bits 1-32 + pub const BOXED_INT_PATTERN: i64 = 0x7ff8_0002 << 32; +} diff --git a/src/instructions/control.rs b/src/instructions/control.rs new file mode 100644 index 00000000..cf906933 --- /dev/null +++ b/src/instructions/control.rs @@ -0,0 +1,2 @@ +pub mod if_else; +pub mod r#loop; diff --git a/src/instructions/control/if_else.rs b/src/instructions/control/if_else.rs new file mode 100644 index 00000000..c55ea827 --- /dev/null +++ b/src/instructions/control/if_else.rs @@ -0,0 +1,49 @@ +use super::super::prelude::*; +use crate::ir::Step; +use wasm_encoder::BlockType; + +#[derive(Clone, Debug)] +pub struct Fields { + pub branch_if: Rc, + pub branch_else: Rc, +} + +pub fn wasm( + func: &StepFunc, + _inputs: Rc<[IrType]>, + Fields { + branch_if, + branch_else, + }: &Fields, +) -> HQResult> { + let if_instructions = func.compile_inner_step(branch_if)?; + let else_instructions = func.compile_inner_step(branch_else)?; + let block_type = func + .registries() + .types() + .register_default((vec![ValType::I32], vec![]))?; + Ok(wasm![ + Block(BlockType::FunctionType(block_type)), + Block(BlockType::FunctionType(block_type)), + I32Eqz, + BrIf(0) + ] + .into_iter() + .chain(if_instructions) + .chain(wasm![Br(1), End,]) + .chain(else_instructions) + .chain(wasm![End]) + .collect()) +} + +pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { + Rc::new([IrType::Boolean]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { + Ok(None) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +// crate::instructions_test! {none; hq__if; @ super::Fields(None)} diff --git a/src/instructions/control/loop.rs b/src/instructions/control/loop.rs new file mode 100644 index 00000000..d4ccca5a --- /dev/null +++ b/src/instructions/control/loop.rs @@ -0,0 +1,60 @@ +// for use in warped contexts only. + +use super::super::prelude::*; +use crate::ir::Step; +use wasm_encoder::BlockType; + +#[derive(Clone, Debug)] +pub struct Fields { + pub first_condition: Option>, + pub condition: Rc, + pub body: Rc, + pub flip_if: bool, +} + +pub fn wasm( + func: &StepFunc, + _inputs: Rc<[IrType]>, + Fields { + first_condition, + condition, + body, + flip_if, + }: &Fields, +) -> HQResult> { + let inner_instructions = func.compile_inner_step(body)?; + let first_condition_instructions = func.compile_inner_step( + &first_condition + .clone() + .unwrap_or_else(|| Rc::clone(condition)), + )?; + let condition_instructions = func.compile_inner_step(condition)?; + Ok(wasm![Block(BlockType::Empty),] + .into_iter() + .chain(first_condition_instructions) + .chain(if *flip_if { + wasm![BrIf(0), Loop(BlockType::Empty)] + } else { + wasm![I32Eqz, BrIf(0), Loop(BlockType::Empty)] + }) + .chain(inner_instructions) + .chain(condition_instructions) + .chain(if *flip_if { + wasm![I32Eqz, BrIf(0), End, End] + } else { + wasm![BrIf(0), End, End] + }) + .collect()) +} + +pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { + Rc::new([]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { + Ok(None) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +// crate::instructions_test! {none; hq_repeat; @ super::Fields(None)} diff --git a/src/instructions/data.rs b/src/instructions/data.rs new file mode 100644 index 00000000..e08a1025 --- /dev/null +++ b/src/instructions/data.rs @@ -0,0 +1,3 @@ +pub mod setvariableto; +pub mod teevariable; +pub mod variable; diff --git a/src/instructions/data/setvariableto.rs b/src/instructions/data/setvariableto.rs new file mode 100644 index 00000000..b8a9f8b1 --- /dev/null +++ b/src/instructions/data/setvariableto.rs @@ -0,0 +1,184 @@ +use super::super::prelude::*; +use crate::ir::RcVar; + +#[derive(Debug, Clone)] +pub struct Fields(pub RcVar); + +pub fn wasm( + func: &StepFunc, + inputs: Rc<[IrType]>, + Fields(variable): &Fields, +) -> HQResult> { + let t1 = inputs[0]; + if variable.0.local() { + let local_index: u32 = func.local_variable(variable)?; + if variable.0.possible_types().is_base_type() { + Ok(wasm![LocalSet(local_index)]) + } else { + Ok(wasm![ + @boxed(t1), + LocalSet(local_index) + ]) + } + } else { + let global_index: u32 = func.registries().variables().register(variable)?; + if variable.0.possible_types().is_base_type() { + Ok(wasm![GlobalSet(global_index)]) + } else { + Ok(wasm![ + @boxed(t1), + GlobalSet(global_index), + ]) + } + } +} + +pub fn acceptable_inputs(Fields(rcvar): &Fields) -> Rc<[IrType]> { + Rc::new([if rcvar.0.possible_types().is_none() { + IrType::Any + } else { + *rcvar.0.possible_types() + }]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { + Ok(None) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test!( + any_global; + data_setvariableto; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::Any, + crate::sb3::VarVal::Float(0.0), + false + ) + ) + ) + ) +); + +crate::instructions_test!( + float_global; + data_setvariableto; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::Float, + crate::sb3::VarVal::Float(0.0), + false + ) + ) + ) + ) +); + +crate::instructions_test!( + string_global; + data_setvariableto; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::String, + crate::sb3::VarVal::String("".into()), + false + ) + ) + ) + ) +); + +crate::instructions_test!( + int_global; + data_setvariableto; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::QuasiInt, + crate::sb3::VarVal::Bool(true), + false + ) + ) + ) + ) +); + +crate::instructions_test!( + any_local; + data_setvariableto; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::Any, + crate::sb3::VarVal::Float(0.0), + true + ) + ) + ) + ) +); + +crate::instructions_test!( + float_local; + data_setvariableto; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::Float, + crate::sb3::VarVal::Float(0.0), + true + ) + ) + ) + ) +); + +crate::instructions_test!( + string_local; + data_setvariableto; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::String, + crate::sb3::VarVal::String("".into()), + true + ) + ) + ) + ) +); + +crate::instructions_test!( + int_local; + data_setvariableto; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::QuasiInt, + crate::sb3::VarVal::Bool(true), + true + ) + ) + ) + ) +); diff --git a/src/instructions/data/teevariable.rs b/src/instructions/data/teevariable.rs new file mode 100644 index 00000000..9111087e --- /dev/null +++ b/src/instructions/data/teevariable.rs @@ -0,0 +1,187 @@ +/// this is currently just a convenience block. if we get thread-scoped variables +/// (i.e. locals) then this can actually use the wasm tee instruction. +use super::super::prelude::*; +use crate::ir::RcVar; + +#[derive(Debug, Clone)] +pub struct Fields(pub RcVar); + +pub fn wasm( + func: &StepFunc, + inputs: Rc<[IrType]>, + Fields(variable): &Fields, +) -> HQResult> { + let t1 = inputs[0]; + if variable.0.local() { + let local_index: u32 = func.local_variable(variable)?; + if variable.0.possible_types().is_base_type() { + Ok(wasm![LocalTee(local_index)]) + } else { + Ok(wasm![ + @boxed(t1), + LocalTee(local_index) + ]) + } + } else { + let global_index: u32 = func.registries().variables().register(variable)?; + if variable.0.possible_types().is_base_type() { + Ok(wasm![GlobalSet(global_index), GlobalGet(global_index)]) + } else { + Ok(wasm![ + @boxed(t1), + GlobalSet(global_index), + GlobalGet(global_index) + ]) + } + } +} + +pub fn acceptable_inputs(Fields(rcvar): &Fields) -> Rc<[IrType]> { + Rc::new([if rcvar.0.possible_types().is_none() { + IrType::Any + } else { + *rcvar.0.possible_types() + }]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, Fields(rcvar): &Fields) -> HQResult> { + Ok(Some(*rcvar.0.possible_types())) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test!( + any_global; + data_teevariable; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::Any, + crate::sb3::VarVal::Float(0.0), + false + ) + ) + ) + ) +); + +crate::instructions_test!( + float_global; + data_teevariable; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::Float, + crate::sb3::VarVal::Float(0.0), + false + ) + ) + ) + ) +); + +crate::instructions_test!( + string_global; + data_teevariable; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::String, + crate::sb3::VarVal::String("".into()), + false + ) + ) + ) + ) +); + +crate::instructions_test!( + int_global; + data_teevariable; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::QuasiInt, + crate::sb3::VarVal::Bool(true), + false + ) + ) + ) + ) +); + +crate::instructions_test!( + any_local; + data_teevariable; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::Any, + crate::sb3::VarVal::Float(0.0), + true + ) + ) + ) + ) +); + +crate::instructions_test!( + float_local; + data_teevariable; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::Float, + crate::sb3::VarVal::Float(0.0), + true + ) + ) + ) + ) +); + +crate::instructions_test!( + string_local; + data_teevariable; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::String, + crate::sb3::VarVal::String("".into()), + true + ) + ) + ) + ) +); + +crate::instructions_test!( + int_local; + data_teevariable; + t + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::QuasiInt, + crate::sb3::VarVal::Bool(true), + true + ) + ) + ) + ) +); diff --git a/src/instructions/data/variable.rs b/src/instructions/data/variable.rs new file mode 100644 index 00000000..72b24489 --- /dev/null +++ b/src/instructions/data/variable.rs @@ -0,0 +1,161 @@ +use super::super::prelude::*; +use crate::ir::RcVar; + +#[derive(Debug, Clone)] +pub struct Fields(pub RcVar); + +pub fn wasm( + func: &StepFunc, + _inputs: Rc<[IrType]>, + Fields(variable): &Fields, +) -> HQResult> { + if variable.0.local() { + let local_index: u32 = func.local_variable(variable)?; + Ok(wasm![LocalGet(local_index)]) + } else { + let global_index: u32 = func.registries().variables().register(variable)?; + Ok(wasm![GlobalGet(global_index)]) + } +} + +pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { + Rc::new([]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, Fields(rcvar): &Fields) -> HQResult> { + Ok(Some(if rcvar.0.possible_types().is_none() { + IrType::Any + } else { + *rcvar.0.possible_types() + })) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test!( + any_global; + data_variable; + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::Any, + crate::sb3::VarVal::Float(0.0), + false, + ) + ) + ) + ) +); + +crate::instructions_test!( + float_global; + data_variable; + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::Float, + crate::sb3::VarVal::Float(0.0), + false, + ) + ) + ) + ) +); + +crate::instructions_test!( + string_global; + data_variable; + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::String, + crate::sb3::VarVal::String("".into()), + false, + ) + ) + ) + ) +); + +crate::instructions_test!( + int_global; + data_variable; + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::QuasiInt, + crate::sb3::VarVal::Bool(true), + false, + ) + ) + ) + ) +); + +crate::instructions_test!( + any_local; + data_variable; + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::Any, + crate::sb3::VarVal::Float(0.0), + true, + ) + ) + ) + ) +); + +crate::instructions_test!( + float_local; + data_variable; + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::Float, + crate::sb3::VarVal::Float(0.0), + true, + ) + ) + ) + ) +); + +crate::instructions_test!( + string_local; + data_variable; + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::String, + crate::sb3::VarVal::String("".into()), + true, + ) + ) + ) + ) +); + +crate::instructions_test!( + int_local; + data_variable; + @ super::Fields( + super::RcVar( + Rc::new( + crate::ir::Variable::new( + IrType::QuasiInt, + crate::sb3::VarVal::Bool(true), + true, + ) + ) + ) + ) +); diff --git a/src/instructions/hq.rs b/src/instructions/hq.rs new file mode 100644 index 00000000..dc983f50 --- /dev/null +++ b/src/instructions/hq.rs @@ -0,0 +1,6 @@ +pub mod cast; +pub mod drop; +pub mod float; +pub mod integer; +pub mod text; +pub mod r#yield; diff --git a/src/instructions/hq/cast.rs b/src/instructions/hq/cast.rs new file mode 100644 index 00000000..5e03e85e --- /dev/null +++ b/src/instructions/hq/cast.rs @@ -0,0 +1,111 @@ +use super::super::prelude::*; + +#[derive(Clone, Debug)] +pub struct Fields(pub IrType); + +fn best_cast_candidate(from: IrType, to: IrType) -> HQResult { + let to_base_types = to.base_types().collect::>(); + hq_assert!(!to_base_types.is_empty()); + let Some(from_base) = from.base_type() else { + hq_bug!("from type has no base type") + }; + Ok(if to_base_types.contains(&from_base) { + from_base + } else { + let mut candidates = vec![]; + for preference in match from_base { + IrType::QuasiInt => &[IrType::Float, IrType::String] as &[IrType], + IrType::Float => &[IrType::String, IrType::QuasiInt] as &[IrType], + IrType::String => &[IrType::Float, IrType::QuasiInt] as &[IrType], + _ => unreachable!(), + } { + if to_base_types.contains(preference) { + candidates.push(preference); + } + } + hq_assert!(!candidates.is_empty()); + *candidates[0] + }) +} + +pub fn wasm( + func: &StepFunc, + inputs: Rc<[IrType]>, + &Fields(to): &Fields, +) -> HQResult> { + let from = inputs[0]; + + let target = best_cast_candidate(from, to)?; + + let Some(from_base) = from.base_type() else { + hq_bug!("from type has no base type") + }; + + Ok(match target { + IrType::Float => match from_base { + IrType::Float => wasm![], + IrType::QuasiInt => wasm![F64ConvertI32S], + IrType::String => { + let func_index = func.registries().external_functions().register( + ("cast", "string2float".into()), + (vec![ValType::EXTERNREF], vec![ValType::F64]), + )?; + wasm![Call(func_index)] + } + _ => hq_todo!("bad cast: {:?} -> float", from_base), + }, + IrType::String => match from_base { + IrType::Float => { + let func_index = func.registries().external_functions().register( + ("cast", "float2string".into()), + (vec![ValType::F64], vec![ValType::EXTERNREF]), + )?; + wasm![Call(func_index)] + } + IrType::QuasiInt => { + let func_index = func.registries().external_functions().register( + ("cast", "int2string".into()), + (vec![ValType::I32], vec![ValType::EXTERNREF]), + )?; + wasm![Call(func_index)] + } + IrType::String => vec![], + _ => hq_todo!("bad cast: {:?} -> string", from_base), + }, + IrType::QuasiInt => match from_base { + IrType::Float => wasm![I32TruncSatF64S], + IrType::String => { + let func_index = func.registries().external_functions().register( + ("cast", "string2float".into()), + (vec![ValType::EXTERNREF], vec![ValType::F64]), + )?; + wasm![Call(func_index), I32TruncSatF64S] + } + IrType::QuasiInt => vec![], + _ => hq_todo!("unimplemented cast: {:?} -> Int", from_base), + }, + _ => hq_todo!("unimplemented cast: {:?} -> {:?}", from_base, target), + }) +} + +pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { + Rc::new([IrType::Number.or(IrType::String).or(IrType::Boolean)]) +} + +pub fn output_type(inputs: Rc<[IrType]>, &Fields(to): &Fields) -> HQResult> { + Ok(Some( + inputs[0] + .base_types() + .map(|from| best_cast_candidate(from, to)) + .collect::>>()? + .into_iter() + .reduce(IrType::or) + .ok_or_else(|| make_hq_bug!("input was empty"))?, + )) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {float; hq_cast; t @ super::Fields(IrType::Float)} +crate::instructions_test! {string; hq_cast; t @ super::Fields(IrType::String)} +crate::instructions_test! {int; hq_cast; t @ super::Fields(IrType::QuasiInt)} diff --git a/src/instructions/hq/drop.rs b/src/instructions/hq/drop.rs new file mode 100644 index 00000000..7e443015 --- /dev/null +++ b/src/instructions/hq/drop.rs @@ -0,0 +1,17 @@ +use super::super::prelude::*; + +pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { + Ok(wasm![Drop]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Number.or(IrType::String)]) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { + Ok(None) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; hq_drop; t} diff --git a/src/instructions/hq/float.rs b/src/instructions/hq/float.rs new file mode 100644 index 00000000..81f853a0 --- /dev/null +++ b/src/instructions/hq/float.rs @@ -0,0 +1,39 @@ +#![allow( + clippy::trivially_copy_pass_by_ref, + reason = "Fields should be passed by reference for type signature consistency" +)] + +use super::super::prelude::*; + +#[derive(Clone, Copy, Debug)] +pub struct Fields(pub f64); + +pub fn wasm( + _func: &StepFunc, + _inputs: Rc<[IrType]>, + fields: &Fields, +) -> HQResult> { + Ok(wasm![F64Const(fields.0)]) +} + +pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { + Rc::new([]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, &Fields(val): &Fields) -> HQResult> { + Ok(Some(match val { + 0.0 => IrType::FloatZero, + f64::INFINITY => IrType::FloatPosInf, + f64::NEG_INFINITY => IrType::FloatNegInf, + nan if f64::is_nan(nan) => IrType::FloatNan, + int if int % 1.0 == 0.0 && int > 0.0 => IrType::FloatPosInt, + int if int % 1.0 == 0.0 && int < 0.0 => IrType::FloatNegInt, + frac if frac > 0.0 => IrType::FloatPosFrac, + frac if frac < 0.0 => IrType::FloatNegFrac, + _ => unreachable!(), + })) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; hq_float; @ super::Fields(0.0)} diff --git a/src/instructions/hq/integer.rs b/src/instructions/hq/integer.rs new file mode 100644 index 00000000..ac7841ee --- /dev/null +++ b/src/instructions/hq/integer.rs @@ -0,0 +1,34 @@ +#![allow( + clippy::trivially_copy_pass_by_ref, + reason = "Fields should be passed by reference for type signature consistency" +)] + +use super::super::prelude::*; + +#[derive(Clone, Copy, Debug)] +pub struct Fields(pub i32); + +pub fn wasm( + _func: &StepFunc, + _inputs: Rc<[IrType]>, + fields: &Fields, +) -> HQResult> { + Ok(wasm![I32Const(fields.0)]) +} + +pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { + Rc::new([]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, &Fields(val): &Fields) -> HQResult> { + Ok(Some(match val { + 0 => IrType::IntZero, + pos if pos > 0 => IrType::IntPos, + neg if neg < 0 => IrType::IntNeg, + _ => unreachable!(), + })) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; hq_integer; @ super::Fields(0)} diff --git a/src/instructions/hq/text.rs b/src/instructions/hq/text.rs new file mode 100644 index 00000000..6975eb31 --- /dev/null +++ b/src/instructions/hq/text.rs @@ -0,0 +1,53 @@ +use crate::wasm::TableOptions; + +use super::super::prelude::*; +use wasm_encoder::RefType; + +#[derive(Clone, Debug)] +pub struct Fields(pub Box); + +pub fn wasm( + func: &StepFunc, + _inputs: Rc<[IrType]>, + fields: &Fields, +) -> HQResult> { + let string_idx = func + .registries() + .strings() + .register_default(fields.0.clone())?; + Ok(wasm![ + I32Const(string_idx), + TableGet(func.registries().tables().register( + "strings".into(), + TableOptions { + element_type: RefType::EXTERNREF, + min: 0, + max: None, + // this default gets fixed up in src/wasm/tables.rs + init: None, + } + )?,), + ]) +} + +pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { + Rc::new([]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, Fields(val): &Fields) -> HQResult> { + Ok(Some(match &**val { + bool if bool.to_lowercase() == "true" || bool.to_lowercase() == "false" => { + IrType::StringBoolean + } + num if let Ok(float) = num.parse::() + && !float.is_nan() => + { + IrType::StringNumber + } + _ => IrType::StringNan, + })) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; hq_text; @ super::Fields("hello, world!".into())} diff --git a/src/instructions/hq/yield.rs b/src/instructions/hq/yield.rs new file mode 100644 index 00000000..bb216f7b --- /dev/null +++ b/src/instructions/hq/yield.rs @@ -0,0 +1,162 @@ +use super::super::prelude::*; +use crate::ir::Step; +use crate::wasm::TableOptions; +use crate::wasm::{flags::Scheduler, GlobalExportable, GlobalMutable, StepFunc}; +use wasm_encoder::{ConstExpr, HeapType, MemArg}; + +#[derive(Clone, Debug)] +pub enum YieldMode { + Tail(Rc), + Inline(Rc), + Schedule(Weak), + None, +} + +#[derive(Clone, Debug)] +pub struct Fields { + pub mode: YieldMode, +} + +pub fn wasm( + func: &StepFunc, + _inputs: Rc<[IrType]>, + Fields { mode: yield_mode }: &Fields, +) -> HQResult> { + let noop_global = func.registries().globals().register( + "noop_func".into(), + ( + ValType::Ref(RefType { + nullable: false, + heap_type: HeapType::Concrete( + func.registries() + .types() + .register_default((vec![ValType::I32], vec![]))?, + ), + }), + ConstExpr::ref_func(0), // this is a placeholder. + GlobalMutable(false), + GlobalExportable(false), + ), + )?; + + let step_func_ty = func + .registries() + .types() + .register_default((vec![ValType::I32], vec![]))?; + + let threads_count = func.registries().globals().register( + "threads_count".into(), + ( + ValType::I32, + ConstExpr::i32_const(0), + GlobalMutable(true), + GlobalExportable(true), + ), + )?; + + Ok(match yield_mode { + YieldMode::None => match func.flags().scheduler { + Scheduler::CallIndirect => { + // Write a special value (e.g. 0 for noop) to linear memory for this thread + wasm![ + LocalGet(0), // thread index + I32Const(4), + I32Mul, + I32Const(0), // 0 = noop step index + // store at address (thread_index * 4) + I32Store(MemArg { + offset: 0, + align: 2, + memory_index: 0, + }), + // GlobalGet(threads_count), + // I32Const(1), + // I32Sub, + // GlobalSet(threads_count), + Return + ] + } + Scheduler::TypedFuncRef => { + let threads_table = func.registries().tables().register( + "threads".into(), + TableOptions { + element_type: RefType { + nullable: false, + heap_type: HeapType::Concrete(step_func_ty), + }, + min: 0, + max: None, + // this default gets fixed up in src/wasm/tables.rs + init: None, + }, + )?; + wasm![ + LocalGet(0), + GlobalGet(noop_global), + TableSet(threads_table), + GlobalGet(threads_count), + I32Const(1), + I32Sub, + GlobalSet(threads_count), + Return + ] + } + }, + YieldMode::Inline(step) => func.compile_inner_step(step)?, + YieldMode::Schedule(weak_step) => { + let step = Weak::upgrade(weak_step) + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; + step.make_used_non_inline()?; + match func.flags().scheduler { + Scheduler::CallIndirect => { + wasm![ + LocalGet(0), // thread index + I32Const(4), + I32Mul, + #LazyStepIndex(Weak::clone(weak_step)), + I32Store(MemArg { + offset: 0, + align: 2, + memory_index: 0, + }), + Return + ] + } + Scheduler::TypedFuncRef => { + let threads_table = func.registries().tables().register( + "threads".into(), + TableOptions { + element_type: RefType { + nullable: false, + heap_type: HeapType::Concrete(step_func_ty), + }, + min: 0, + max: None, + // this default gets fixed up in src/wasm/tables.rs + init: None, + }, + )?; + wasm![ + LocalGet(0), + #LazyStepRef(Weak::clone(weak_step)), + TableSet(threads_table), + Return + ] + } + } + } + YieldMode::Tail(_) => hq_todo!(), + }) +} + +pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { + Rc::new([]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { + Ok(None) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {none; hq_yield; @ super::Fields { mode: super::YieldMode::None }} diff --git a/src/instructions/input_switcher.rs b/src/instructions/input_switcher.rs new file mode 100644 index 00000000..d7767eeb --- /dev/null +++ b/src/instructions/input_switcher.rs @@ -0,0 +1,296 @@ +//! Provides the logic for having boxed input types to blocks + +use super::prelude::*; +use super::HqCastFields; +use super::IrOpcode; +use crate::wasm::GlobalExportable; +use crate::wasm::GlobalMutable; +use crate::wasm::TableOptions; +use crate::wasm::WasmProject; +use crate::{ir::Type as IrType, wasm::StepFunc}; +use itertools::Itertools; +use wasm_encoder::ConstExpr; +use wasm_encoder::{BlockType, Instruction as WInstruction, RefType}; +use wasm_gen::wasm; + +fn cast_instructions( + pos: usize, + base: IrType, + if_block_type: BlockType, + local_idx: u32, + func: &StepFunc, + possible_types_num: usize, +) -> HQResult> { + Ok(if pos == 0 { + match base { + IrType::QuasiInt => wasm![ + I64Const(BOXED_INT_PATTERN), + I64And, + I64Const(BOXED_INT_PATTERN), + I64Eq, + If(if_block_type), + LocalGet(local_idx), + I32WrapI64, + ], + IrType::String => { + let table_index = func.registries().tables().register( + "strings".into(), + TableOptions { + element_type: RefType::EXTERNREF, + min: 0, + // TODO: use js string imports for preknown strings + max: None, + init: None, + }, + )?; + wasm![ + I64Const(BOXED_STRING_PATTERN), + I64And, + I64Const(BOXED_STRING_PATTERN), + I64Eq, + If(if_block_type), + LocalGet(local_idx), + I32WrapI64, + TableGet(table_index), + ] + } + // float guaranteed to be last so no need to check + _ => unreachable!(), + } + } else if pos == possible_types_num - 1 { + match base { + IrType::Float => wasm![Else, LocalGet(local_idx), F64ReinterpretI64], // float guaranteed to be last so no need to check + IrType::QuasiInt => wasm![Else, LocalGet(local_idx), I32WrapI64], + IrType::String => { + let table_index = func.registries().tables().register( + "strings".into(), + TableOptions { + element_type: RefType::EXTERNREF, + min: 0, + // TODO: use js string imports for preknown strings + max: None, + init: None, + }, + )?; + wasm![Else, LocalGet(local_idx), I32WrapI64, TableGet(table_index)] + } + _ => unreachable!(), + } + } else { + match base { + // float guaranteed to be last so no need to check + IrType::Float => wasm![Else, LocalGet(local_idx), F64ReinterpretI64], + IrType::QuasiInt => wasm![ + Else, + LocalGet(local_idx), + I64Const(BOXED_INT_PATTERN), + I64And, + I64Const(BOXED_INT_PATTERN), + I64Eq, + If(if_block_type), + LocalGet(local_idx), + I32WrapI64, + ], + IrType::String => { + let table_index = func.registries().tables().register( + "strings".into(), + TableOptions { + element_type: RefType::EXTERNREF, + min: 0, + max: None, + init: None, + }, + )?; + wasm![ + Else, + LocalGet(local_idx), + I64Const(BOXED_STRING_PATTERN), + I64And, + I64Const(BOXED_STRING_PATTERN), + I64Eq, + If(if_block_type), + LocalGet(local_idx), + I32WrapI64, + TableGet(table_index), + ] + } + _ => unreachable!(), + } + }) +} + +/// generates branches (or not, if an input is not boxed) for a list of remaining input types. +/// This sort of recursion makes me feel distinctly uneasy; I'm just waiting for a stack +/// overflow. +/// TODO: tail-recursify/loopify? +fn generate_branches( + func: &StepFunc, + processed_inputs: &[IrType], + remaining_inputs: &[(Box<[IrType]>, u32)], // u32 is local index + opcode: &IrOpcode, + output_type: Option, +) -> HQResult> { + if remaining_inputs.is_empty() { + hq_assert!(processed_inputs.iter().copied().all(IrType::is_base_type)); + let rc_processed_inputs = processed_inputs.into(); + let mut wasm = opcode.wasm(func, Rc::clone(&rc_processed_inputs))?; + // if the overall output is boxed, but this particular branch produces an unboxed result + // (which i think all branches probably should?), box it. + // TODO: split this into another function somewhere? it seems like this should + // be useful somewhere else as well + if let Some(this_output) = opcode.output_type(rc_processed_inputs)? { + if this_output.is_base_type() + && !output_type + .ok_or_else(|| make_hq_bug!("expected no output type but got one"))? + .is_base_type() + { + #[expect(clippy::unwrap_used, reason = "asserted that type is base type")] + let this_base_type = this_output.base_type().unwrap(); + wasm.append(&mut wasm![@boxed(this_base_type)]); + } + } + return Ok(wasm); + } + let (curr_input, local_idx) = &remaining_inputs[0]; + let local_idx = *local_idx; // variable shadowing feels evil but hey it works + let mut wasm = wasm![LocalGet(local_idx)]; + if curr_input.len() == 1 { + let mut vec_processed_inputs = processed_inputs.to_vec(); + vec_processed_inputs.push(curr_input[0]); + wasm.append(&mut generate_branches( + func, + &vec_processed_inputs, + &remaining_inputs[1..], + opcode, + output_type, + )?); + } else { + let if_block_type = BlockType::FunctionType( + func.registries().types().register_default(( + processed_inputs + .iter() + .copied() + .map(WasmProject::ir_type_to_wasm) + .collect::>>()?, + if let Some(out_ty) = output_type { + vec![WasmProject::ir_type_to_wasm(out_ty)?] + } else { + vec![] + }, + ))?, + ); + let possible_types_num = curr_input.len(); + let allowed_input_types = opcode.acceptable_inputs()[processed_inputs.len()]; + for (i, ty) in curr_input.iter().enumerate() { + let base = ty + .base_type() + .ok_or_else(|| make_hq_bug!("non-base type found"))?; + wasm.append(&mut cast_instructions( + i, + base, + if_block_type, + local_idx, + func, + possible_types_num, + )?); + if !allowed_input_types.base_types().any(|ty| ty == base) { + wasm.append( + &mut IrOpcode::hq_cast(HqCastFields(allowed_input_types)) + .wasm(func, Rc::new([*ty]))?, + ); + } + let mut vec_processed_inputs = processed_inputs.to_vec(); + vec_processed_inputs.push( + IrOpcode::hq_cast(HqCastFields(allowed_input_types)) + .output_type(Rc::new([*ty]))? + .ok_or_else(|| make_hq_bug!("hq_cast output type was None"))?, + ); + wasm.append(&mut generate_branches( + func, + &vec_processed_inputs, + &remaining_inputs[1..], + opcode, + output_type, + )?); + } + wasm.extend(core::iter::repeat_n( + InternalInstruction::Immediate(WInstruction::End), + possible_types_num - 1, // the last else doesn't need an additional `end` instruction + )); + } + Ok(wasm) +} + +#[expect( + clippy::needless_pass_by_value, + reason = "passing an Rc by reference doesn't make much sense a lot of the time" +)] +pub fn wrap_instruction( + func: &StepFunc, + inputs: Rc<[IrType]>, + opcode: &IrOpcode, +) -> HQResult> { + let output = opcode.output_type(Rc::clone(&inputs))?; + + hq_assert!(inputs.len() == opcode.acceptable_inputs().len()); + + // possible base types for each input + let base_types = + // check for float last of all, because I don't think there's an easy way of checking + // if something is *not* a canonical NaN with extra bits + core::iter::repeat_n([IrType::QuasiInt, IrType::String, IrType::Float].into_iter(), inputs.len()) + .enumerate() + .map(|(i, tys)| { + tys.filter(|ty| inputs[i].intersects(*ty)).map(|ty| ty.and(inputs[i])) + .collect::>() + }).collect::>(); + + // sanity check; we have at least one possible input type for each input + hq_assert!( + !base_types.iter().any(|tys| tys.is_empty()), + "empty input type for block {:?}", + opcode + ); + + let locals = inputs + .iter() + .map(|ty| func.local(WasmProject::ir_type_to_wasm(*ty)?)) + .collect::>>()?; + + // for now, chuck each input into a local + // TODO: change this so that only the inputs following the first boxed input are local-ised + // ...or should we just let wasm-opt deal with this? + let mut wasm = locals + .iter() + .rev() + .copied() + .map(WInstruction::LocalSet) + .map(InternalInstruction::Immediate) + .collect::>(); + + wasm.append(&mut generate_branches( + func, + &[], + base_types + .into_iter() + .zip_eq(locals.iter().copied()) + .collect::>() + .as_slice(), + opcode, + output, + )?); + if opcode.requests_screen_refresh() { + let refresh_requested = func.registries().globals().register( + "requests_refresh".into(), + ( + ValType::I32, + ConstExpr::i32_const(0), + GlobalMutable(true), + GlobalExportable(true), + ), + )?; + + wasm.append(&mut wasm![I32Const(1), GlobalSet(refresh_requested),]); + } + Ok(wasm) +} diff --git a/src/instructions/looks.rs b/src/instructions/looks.rs new file mode 100644 index 00000000..80271d14 --- /dev/null +++ b/src/instructions/looks.rs @@ -0,0 +1,2 @@ +pub mod say; +pub mod think; diff --git a/src/instructions/looks/say.rs b/src/instructions/looks/say.rs new file mode 100644 index 00000000..6fd23fe1 --- /dev/null +++ b/src/instructions/looks/say.rs @@ -0,0 +1,55 @@ +use super::super::prelude::*; + +#[derive(Clone, Debug)] +pub struct Fields { + pub debug: bool, + pub target_idx: u32, +} + +pub fn wasm( + func: &StepFunc, + inputs: Rc<[IrType]>, + &Fields { debug, target_idx }: &Fields, +) -> HQResult> { + let prefix = String::from(if debug { "say_debug" } else { "say" }); + let itarget_idx: i32 = target_idx + .try_into() + .map_err(|_| make_hq_bug!("target index out of bounds"))?; + Ok(if IrType::QuasiInt.contains(inputs[0]) { + let func_index = func.registries().external_functions().register( + ("looks", format!("{prefix}_int").into_boxed_str()), + (vec![ValType::I32, ValType::I32], vec![]), + )?; + wasm![I32Const(itarget_idx), Call(func_index)] + } else if IrType::Float.contains(inputs[0]) { + let func_index = func.registries().external_functions().register( + ("looks", format!("{prefix}_float").into_boxed_str()), + (vec![ValType::F64, ValType::I32], vec![]), + )?; + wasm![I32Const(itarget_idx), Call(func_index)] + } else if IrType::String.contains(inputs[0]) { + let func_index = func.registries().external_functions().register( + ("looks", format!("{prefix}_string").into_boxed_str()), + (vec![ValType::EXTERNREF, ValType::I32], vec![]), + )?; + wasm![I32Const(itarget_idx), Call(func_index)] + } else { + hq_bug!("bad input") + }) +} + +pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { + Rc::new([IrType::String.or(IrType::Number)]) +} + +pub fn output_type(inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { + if !(IrType::Number.or(IrType::String).contains(inputs[0])) { + hq_todo!("unimplemented input type: {:?}", inputs) + } + Ok(None) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = true; + +crate::instructions_test! {tests_debug; looks_say; t @ super::Fields { debug: true, target_idx: 0 }} +crate::instructions_test! {tests_non_debug; looks_say; t @ super::Fields { debug: false, target_idx: 0, }} diff --git a/src/instructions/looks/think.rs b/src/instructions/looks/think.rs new file mode 100644 index 00000000..08b4e104 --- /dev/null +++ b/src/instructions/looks/think.rs @@ -0,0 +1,55 @@ +use super::super::prelude::*; + +#[derive(Clone, Debug)] +pub struct Fields { + pub debug: bool, + pub target_idx: u32, +} + +pub fn wasm( + func: &StepFunc, + inputs: Rc<[IrType]>, + &Fields { debug, target_idx }: &Fields, +) -> HQResult> { + let prefix = String::from(if debug { "think_debug" } else { "think" }); + let itarget_idx: i32 = target_idx + .try_into() + .map_err(|_| make_hq_bug!("target index out of bounds"))?; + Ok(if IrType::QuasiInt.contains(inputs[0]) { + let func_index = func.registries().external_functions().register( + ("looks", format!("{prefix}_int").into_boxed_str()), + (vec![ValType::I32, ValType::I32], vec![]), + )?; + wasm![I32Const(itarget_idx), Call(func_index)] + } else if IrType::Float.contains(inputs[0]) { + let func_index = func.registries().external_functions().register( + ("looks", format!("{prefix}_float").into_boxed_str()), + (vec![ValType::F64, ValType::I32], vec![]), + )?; + wasm![I32Const(itarget_idx), Call(func_index)] + } else if IrType::String.contains(inputs[0]) { + let func_index = func.registries().external_functions().register( + ("looks", format!("{prefix}_string").into_boxed_str()), + (vec![ValType::EXTERNREF, ValType::I32], vec![]), + )?; + wasm![I32Const(itarget_idx), Call(func_index)] + } else { + hq_bug!("bad input") + }) +} + +pub fn acceptable_inputs(_fields: &Fields) -> Rc<[IrType]> { + Rc::new([IrType::String.or(IrType::Number)]) +} + +pub fn output_type(inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { + if !(IrType::Number.or(IrType::String).contains(inputs[0])) { + hq_todo!("unimplemented input type: {:?}", inputs) + } + Ok(None) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = true; + +crate::instructions_test! {tests_debug; looks_think; t @ super::Fields { debug: true, target_idx: 0 }} +crate::instructions_test! {tests_non_debug; looks_think; t @ super::Fields { debug: false, target_idx: 0, }} diff --git a/src/instructions/operator.rs b/src/instructions/operator.rs new file mode 100644 index 00000000..9d28cb77 --- /dev/null +++ b/src/instructions/operator.rs @@ -0,0 +1,14 @@ +pub mod add; +pub mod and; +pub mod contains; +pub mod divide; +pub mod equals; +pub mod gt; +pub mod join; +pub mod length; +pub mod letter_of; +pub mod lt; +pub mod multiply; +pub mod not; +pub mod or; +pub mod subtract; diff --git a/src/instructions/operator/add.rs b/src/instructions/operator/add.rs new file mode 100644 index 00000000..3d447d71 --- /dev/null +++ b/src/instructions/operator/add.rs @@ -0,0 +1,94 @@ +use super::super::prelude::*; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); + let t1 = inputs[0]; + let t2 = inputs[1]; + Ok(if IrType::QuasiInt.contains(t1) { + if IrType::QuasiInt.contains(t2) { + wasm![I32Add] + } else if IrType::Float.contains(t2) { + let f64_local = func.local(ValType::F64)?; + wasm![ + LocalSet(f64_local), + F64ConvertI32S, + LocalGet(f64_local), + @nanreduce(t2), + F64Add, + ] + } else { + hq_bug!("bad input") + } + } else if IrType::Float.contains(t1) { + if IrType::Float.contains(t2) { + let f64_local = func.local(ValType::F64)?; + wasm![ + @nanreduce(t2), + LocalSet(f64_local), + @nanreduce(t1), + LocalGet(f64_local), + F64Add + ] + } else if IrType::QuasiInt.contains(t2) { + let i32_local = func.local(ValType::I32)?; + wasm![ + LocalSet(i32_local), + @nanreduce(t1), + LocalGet(i32_local), + F64ConvertI32S, + F64Add + ] + } else { + hq_bug!("bad input") + } + } else { + hq_bug!("bad input: {:?}", inputs) + }) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Number, IrType::Number]) +} + +pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); + let t1 = inputs[0]; + let t2 = inputs[1]; + let maybe_positive = t1.maybe_positive() || t2.maybe_positive(); + let maybe_negative = t1.maybe_negative() || t2.maybe_negative(); + let maybe_zero = (t1.maybe_zero() || t1.maybe_nan()) && (t2.maybe_zero() || t2.maybe_nan()); + let maybe_nan = (IrType::FloatNegInf.intersects(t1) && IrType::FloatPosInf.intersects(t2)) + || (IrType::FloatNegInf.intersects(t2) && IrType::FloatPosInf.intersects(t1)); + Ok(Some(if IrType::QuasiInt.contains(t1.or(t2)) { + IrType::none_if_false(maybe_positive, IrType::IntPos) + .or(IrType::none_if_false(maybe_negative, IrType::IntNeg)) + .or(IrType::none_if_false(maybe_zero, IrType::IntZero)) + } else if (IrType::QuasiInt.contains(t1) && IrType::Float.contains(t2)) + || (IrType::QuasiInt.contains(t2) && IrType::Float.contains(t1)) + || IrType::Float.contains(t1.or(t2)) + { + IrType::none_if_false(maybe_positive, IrType::FloatPos) + .or(IrType::none_if_false(maybe_negative, IrType::FloatNeg)) + .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) + .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) + } else { + // there is a boxed type somewhere + // TODO: can these bounds be tightened? e.g. it may only be a positive int or negative float? + // i have no idea if that would ever work, but it would be useful for considering when + // addition/subtraction may give NaN (since inf-inf=nan but inf+inf=inf) + IrType::none_if_false(maybe_positive, IrType::FloatPos.or(IrType::IntPos)) + .or(IrType::none_if_false( + maybe_negative, + IrType::FloatNeg.or(IrType::IntNeg), + )) + .or(IrType::none_if_false( + maybe_zero, + IrType::FloatZero.or(IrType::IntZero), + )) + .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) + })) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_add; t1, t2 ;} diff --git a/src/instructions/operator/and.rs b/src/instructions/operator/and.rs new file mode 100644 index 00000000..b4dd2bba --- /dev/null +++ b/src/instructions/operator/and.rs @@ -0,0 +1,17 @@ +use super::super::prelude::*; + +pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { + Ok(wasm![I32And]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Boolean, IrType::Boolean]) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { + Ok(Some(IrType::Boolean)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_and; t1, t2 ;} diff --git a/src/instructions/operator/contains.rs b/src/instructions/operator/contains.rs new file mode 100644 index 00000000..ba056a7d --- /dev/null +++ b/src/instructions/operator/contains.rs @@ -0,0 +1,25 @@ +use super::super::prelude::*; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); + let func_index = func.registries().external_functions().register( + ("operator", "contains".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + Ok(wasm![Call(func_index)]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::String, IrType::String]) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { + Ok(Some(IrType::Boolean)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_contains; t1, t2 ;} diff --git a/src/instructions/operator/divide.rs b/src/instructions/operator/divide.rs new file mode 100644 index 00000000..3ffa157a --- /dev/null +++ b/src/instructions/operator/divide.rs @@ -0,0 +1,44 @@ +use super::super::prelude::*; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); + let t1 = inputs[0]; + let t2 = inputs[1]; + let f64_local = func.local(ValType::F64)?; + Ok(wasm![ + LocalSet(f64_local), + @nanreduce(t1), + LocalGet(f64_local), + @nanreduce(t2), + F64Div + ]) +} + +// TODO: is integer division acceptable if we can prove that it will give an integer result (or if it is floored?) +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Float, IrType::Float]) +} + +pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); + let t1 = inputs[0]; + let t2 = inputs[1]; + let maybe_positive = (t1.maybe_positive() && t2.maybe_positive()) + || (t1.maybe_negative() && t2.maybe_negative()); + let maybe_negative = (t1.maybe_positive() && t2.maybe_negative()) + || (t1.maybe_negative() && t2.maybe_positive()); + let maybe_zero = t1.maybe_zero() || t1.maybe_nan(); // TODO: can this be narrowed to +/-0? + let maybe_infinity = t2.maybe_zero() || t2.maybe_nan(); // TODO: can this be narrowed to +/-infinity? + let maybe_nan = t1.maybe_zero() && t2.maybe_zero(); + Ok(Some( + IrType::none_if_false(maybe_positive, IrType::FloatPos) + .or(IrType::none_if_false(maybe_negative, IrType::FloatNeg)) + .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) + .or(IrType::none_if_false(maybe_infinity, IrType::FloatInf)) + .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)), + )) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_divide; t1, t2 ;} diff --git a/src/instructions/operator/equals.rs b/src/instructions/operator/equals.rs new file mode 100644 index 00000000..d92d3bdd --- /dev/null +++ b/src/instructions/operator/equals.rs @@ -0,0 +1,203 @@ +use wasm_encoder::BlockType; + +use super::super::prelude::*; + +#[expect(clippy::too_many_lines, reason = "long monomorphisation routine")] +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); + let t1 = inputs[0]; + let t2 = inputs[1]; + let block_type = BlockType::Result(ValType::I32); + Ok(if IrType::QuasiInt.contains(t1) { + if IrType::QuasiInt.contains(t2) { + wasm![I32Eq] + } else if IrType::Float.contains(t2) { + let local1 = func.local(ValType::F64)?; + let local2 = func.local(ValType::F64)?; + wasm![ + LocalSet(local2), + F64ConvertI32S, + LocalSet(local1), + LocalGet(local2), + @isnan(t1), + If(block_type), + LocalGet(local2), + @isnan(t2), + If(block_type), + I32Const(1), // NaN == NaN (in scratch) + Else, + I32Const(0), // NaN != number + End, + Else, + LocalGet(local2), + @isnan(t2), + If(block_type), + I32Const(0), // number != NaN + Else, + LocalGet(local1), + LocalGet(local2), + F64Eq, + End, + End, + ] + } else if IrType::String.contains(t2) { + // TODO: try converting string to number first (if applicable) + let int2string = func.registries().external_functions().register( + ("cast", "int2string".into()), + (vec![ValType::I32], vec![ValType::EXTERNREF]), + )?; + let string_eq = func.registries().external_functions().register( + // we can't use wasm:js-string/equals because scratch converts to lowercase first + ("operator", "eq_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + let extern_local = func.local(ValType::EXTERNREF)?; + wasm![ + LocalSet(extern_local), + Call(int2string), + LocalGet(extern_local), + Call(string_eq) + ] + } else { + hq_bug!("bad input") + } + } else if IrType::Float.contains(t1) { + if IrType::Float.contains(t2) { + let local1 = func.local(ValType::F64)?; + let local2 = func.local(ValType::F64)?; + wasm![ + LocalSet(local2), + LocalTee(local1), + @isnan(t1), + If(block_type), + LocalGet(local2), + @isnan(t2), + If(block_type), + I32Const(1), // NaN == NaN (in scratch) + Else, + I32Const(0), // NaN != number + End, + Else, + LocalGet(local2), + @isnan(t2), + If(block_type), + I32Const(0), // number != NaN + Else, + LocalGet(local1), + LocalGet(local2), + F64Eq, + End, + End, + ] + } else if IrType::QuasiInt.contains(t2) { + let local1 = func.local(ValType::F64)?; + let local2 = func.local(ValType::F64)?; + wasm![ + F64ConvertI32S, + LocalSet(local2), + LocalTee(local1), + @isnan(t1), + If(block_type), + LocalGet(local2), + @isnan(t2), + If(block_type), + I32Const(1), // NaN == NaN (in scratch) + Else, + I32Const(0), // NaN != number + End, + Else, + LocalGet(local2), + @isnan(t2), + If(block_type), + I32Const(0), // number != NaN + Else, + LocalGet(local1), + LocalGet(local2), + F64Eq, + End, + End, + ] + } else if IrType::String.contains(t2) { + // TODO: try converting string to number first + let float2string = func.registries().external_functions().register( + ("cast", "float2string".into()), + (vec![ValType::F64], vec![ValType::EXTERNREF]), + )?; + let string_eq = func.registries().external_functions().register( + ("operator", "eq_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + let extern_local = func.local(ValType::EXTERNREF)?; + wasm![ + LocalSet(extern_local), + Call(float2string), + LocalGet(extern_local), + Call(string_eq) + ] + } else { + hq_bug!("bad input") + } + } else if IrType::String.contains(t1) { + if IrType::QuasiInt.contains(t2) { + // TODO: try converting string to number first + let int2string = func.registries().external_functions().register( + ("cast", "int2string".into()), + (vec![ValType::I32], vec![ValType::EXTERNREF]), + )?; + let string_eq = func.registries().external_functions().register( + ("operator", "eq_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + wasm![Call(int2string), Call(string_eq)] + } else if IrType::Float.contains(t2) { + // TODO: try converting string to number first + let float2string = func.registries().external_functions().register( + ("cast", "float2string".into()), + (vec![ValType::F64], vec![ValType::EXTERNREF]), + )?; + let string_eq = func.registries().external_functions().register( + ("operator", "eq_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + wasm![Call(float2string), Call(string_eq)] + } else if IrType::String.contains(t2) { + // TODO: try converting string to number first + let string_eq = func.registries().external_functions().register( + ("operator", "eq_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + wasm![Call(string_eq)] + } else { + hq_bug!("bad input") + } + } else { + hq_bug!("bad input") + }) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Any, IrType::Any]) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { + Ok(Some(IrType::Boolean)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_equals; t1, t2 ;} diff --git a/src/instructions/operator/gt.rs b/src/instructions/operator/gt.rs new file mode 100644 index 00000000..771a2bfb --- /dev/null +++ b/src/instructions/operator/gt.rs @@ -0,0 +1,169 @@ +use wasm_encoder::BlockType; + +use super::super::prelude::*; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); + let t1 = inputs[0]; + let t2 = inputs[1]; + let block_type = BlockType::Result(ValType::I32); + Ok(if IrType::QuasiInt.contains(t1) { + if IrType::QuasiInt.contains(t2) { + wasm![I32GtS] + } else if IrType::Float.contains(t2) { + let local1 = func.local(ValType::F64)?; + let local2 = func.local(ValType::F64)?; + wasm![ + LocalSet(local2), + F64ConvertI32S, + LocalSet(local1), + LocalGet(local2), + @isnan(t2), + If(block_type), + I32Const(0), // (number) < NaN always false + Else, + LocalGet(local1), + LocalGet(local2), + F64Gt, + End, + ] + } else if IrType::String.contains(t2) { + // TODO: try converting string to number first (if applicable) + let int2string = func.registries().external_functions().register( + ("cast", "int2string".into()), + (vec![ValType::I32], vec![ValType::EXTERNREF]), + )?; + let string_gt = func.registries().external_functions().register( + ("operator", "gt_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + let extern_local = func.local(ValType::EXTERNREF)?; + wasm![ + LocalSet(extern_local), + Call(int2string), + LocalGet(extern_local), + Call(string_gt) + ] + } else { + hq_bug!("bad input") + } + } else if IrType::Float.contains(t1) { + if IrType::Float.contains(t2) { + let local1 = func.local(ValType::F64)?; + let local2 = func.local(ValType::F64)?; + wasm![ + LocalSet(local2), + LocalTee(local1), + @isnan(t1), + If(block_type), + LocalGet(local2), + @isnan(t2), + If(block_type), + I32Const(0), // NaN is not greater than NaN + Else, + I32Const(1), // NaN > number always true + End, + Else, + LocalGet(local1), + LocalGet(local2), + F64Gt, + End, + ] + } else if IrType::QuasiInt.contains(t2) { + let local1 = func.local(ValType::F64)?; + let local2 = func.local(ValType::F64)?; + wasm![ + F64ConvertI32S, + LocalSet(local2), + LocalTee(local1), + @isnan(t1), + If(block_type), + I32Const(1), // NaN > number is always true + Else, + LocalGet(local1), + LocalGet(local2), + F64Gt, + End, + ] + } else if IrType::String.contains(t2) { + let float2string = func.registries().external_functions().register( + ("cast", "float2string".into()), + (vec![ValType::F64], vec![ValType::EXTERNREF]), + )?; + let string_gt = func.registries().external_functions().register( + ("operator", "gt_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + let extern_local = func.local(ValType::EXTERNREF)?; + wasm![ + LocalSet(extern_local), + Call(float2string), + LocalGet(extern_local), + Call(string_gt) + ] + } else { + hq_bug!("bad input") + } + } else if IrType::String.contains(t1) { + if IrType::QuasiInt.contains(t2) { + // TODO: try converting string to number first + let int2string = func.registries().external_functions().register( + ("cast", "int2string".into()), + (vec![ValType::I32], vec![ValType::EXTERNREF]), + )?; + let string_gt = func.registries().external_functions().register( + ("operator", "gt_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + wasm![Call(int2string), Call(string_gt)] + } else if IrType::Float.contains(t2) { + // TODO: try converting string to number first + let float2string = func.registries().external_functions().register( + ("cast", "float2string".into()), + (vec![ValType::F64], vec![ValType::EXTERNREF]), + )?; + let string_gt = func.registries().external_functions().register( + ("operator", "gt_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + wasm![Call(float2string), Call(string_gt)] + } else if IrType::String.contains(t2) { + let string_gt = func.registries().external_functions().register( + ("operator", "gt_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + wasm![Call(string_gt)] + } else { + hq_bug!("bad input") + } + } else { + hq_bug!("bad input") + }) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Any, IrType::Any]) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { + Ok(Some(IrType::Boolean)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_lt; t1, t2 ;} diff --git a/src/instructions/operator/join.rs b/src/instructions/operator/join.rs new file mode 100644 index 00000000..86d4db75 --- /dev/null +++ b/src/instructions/operator/join.rs @@ -0,0 +1,30 @@ +use wasm_encoder::HeapType; + +use super::super::prelude::*; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); + let func_index = func.registries().external_functions().register( + ("wasm:js-string", "concat".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::Ref(RefType { + nullable: false, + heap_type: HeapType::EXTERN, + })], + ), + )?; + Ok(wasm![Call(func_index)]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::String, IrType::String]) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { + Ok(Some(IrType::String)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_join; t1, t2 ;} diff --git a/src/instructions/operator/length.rs b/src/instructions/operator/length.rs new file mode 100644 index 00000000..87282bdf --- /dev/null +++ b/src/instructions/operator/length.rs @@ -0,0 +1,22 @@ +use super::super::prelude::*; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 1); + let func_index = func.registries().external_functions().register( + ("wasm:js-string", "length".into()), + (vec![ValType::EXTERNREF], vec![ValType::I32]), + )?; + Ok(wasm![Call(func_index)]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::String]) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { + Ok(Some(IrType::IntPos)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_length; t ;} diff --git a/src/instructions/operator/letter_of.rs b/src/instructions/operator/letter_of.rs new file mode 100644 index 00000000..38a250c5 --- /dev/null +++ b/src/instructions/operator/letter_of.rs @@ -0,0 +1,41 @@ +use wasm_encoder::HeapType; + +use super::super::prelude::*; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); + let func_index = func.registries().external_functions().register( + ("wasm:js-string", "substring".into()), + ( + vec![ValType::EXTERNREF, ValType::I32, ValType::I32], + vec![ValType::Ref(RefType { + nullable: false, + heap_type: HeapType::EXTERN, + })], + ), + )?; + let i32_local = func.local(ValType::I32)?; + let ef_local = func.local(ValType::EXTERNREF)?; + Ok(wasm![ + LocalSet(ef_local), + LocalSet(i32_local), + LocalGet(ef_local), + LocalGet(i32_local), + I32Const(1), + I32Sub, + LocalGet(i32_local), + Call(func_index), + ]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Int, IrType::String]) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { + Ok(Some(IrType::String)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_letter_of; t1, t2 ;} diff --git a/src/instructions/operator/lt.rs b/src/instructions/operator/lt.rs new file mode 100644 index 00000000..58b1c3a1 --- /dev/null +++ b/src/instructions/operator/lt.rs @@ -0,0 +1,173 @@ +use wasm_encoder::BlockType; + +use super::super::prelude::*; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); + let t1 = inputs[0]; + let t2 = inputs[1]; + let block_type = BlockType::Result(ValType::I32); + Ok(if IrType::QuasiInt.contains(t1) { + if IrType::QuasiInt.contains(t2) { + wasm![I32LtS] + } else if IrType::Float.contains(t2) { + let local1 = func.local(ValType::F64)?; + let local2 = func.local(ValType::F64)?; + wasm![ + LocalSet(local2), + F64ConvertI32S, + LocalSet(local1), + LocalGet(local2), + @isnan(t2), + If(block_type), + I32Const(1), // (number) < NaN always true + Else, + LocalGet(local1), + LocalGet(local2), + F64Lt, + End, + ] + } else if IrType::String.contains(t2) { + // TODO: try converting string to number first (if applicable) + let int2string = func.registries().external_functions().register( + ("cast", "int2string".into()), + (vec![ValType::I32], vec![ValType::EXTERNREF]), + )?; + let string_lt = func.registries().external_functions().register( + ("operator", "lt_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + let extern_local = func.local(ValType::EXTERNREF)?; + wasm![ + LocalSet(extern_local), + Call(int2string), + LocalGet(extern_local), + Call(string_lt) + ] + } else { + hq_bug!("bad input") + } + } else if IrType::Float.contains(t1) { + if IrType::Float.contains(t2) { + let local1 = func.local(ValType::F64)?; + let local2 = func.local(ValType::F64)?; + wasm![ + LocalSet(local2), + LocalTee(local1), + @isnan(t1), + If(block_type), + I32Const(0), // NaN < number always false + Else, + LocalGet(local2), + @isnan(t2), + If(block_type), + I32Const(1), // number < NaN always true + Else, + LocalGet(local1), + LocalGet(local2), + F64Lt, + End, + End, + ] + } else if IrType::QuasiInt.contains(t2) { + let local1 = func.local(ValType::F64)?; + let local2 = func.local(ValType::F64)?; + wasm![ + F64ConvertI32S, + LocalSet(local2), + LocalTee(local1), + @isnan(t1), + If(block_type), + // if t2 is NaN, NaN < NaN is false + // if t2 is a number, NaN < (number) is false, since scratch converts to + // a string if one or both inputs are NaN, and compares the strings; + // numbers (and -) come before N. + I32Const(0), + Else, + LocalGet(local1), + LocalGet(local2), + F64Lt, + End, + ] + } else if IrType::String.contains(t2) { + let float2string = func.registries().external_functions().register( + ("cast", "float2string".into()), + (vec![ValType::F64], vec![ValType::EXTERNREF]), + )?; + let string_lt = func.registries().external_functions().register( + ("operator", "lt_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + let extern_local = func.local(ValType::EXTERNREF)?; + wasm![ + LocalSet(extern_local), + Call(float2string), + LocalGet(extern_local), + Call(string_lt) + ] + } else { + hq_bug!("bad input") + } + } else if IrType::String.contains(t1) { + if IrType::QuasiInt.contains(t2) { + // TODO: try converting string to number first + let int2string = func.registries().external_functions().register( + ("cast", "int2string".into()), + (vec![ValType::I32], vec![ValType::EXTERNREF]), + )?; + let string_lt = func.registries().external_functions().register( + ("operator", "lt_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + wasm![Call(int2string), Call(string_lt)] + } else if IrType::Float.contains(t2) { + // TODO: try converting string to number first + let float2string = func.registries().external_functions().register( + ("cast", "float2string".into()), + (vec![ValType::F64], vec![ValType::EXTERNREF]), + )?; + let string_lt = func.registries().external_functions().register( + ("operator", "lt_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + wasm![Call(float2string), Call(string_lt)] + } else if IrType::String.contains(t2) { + let string_lt = func.registries().external_functions().register( + ("operator", "lt_string".into()), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::I32], + ), + )?; + wasm![Call(string_lt)] + } else { + hq_bug!("bad input") + } + } else { + hq_bug!("bad input") + }) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Any, IrType::Any]) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { + Ok(Some(IrType::Boolean)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_lt; t1, t2 ;} diff --git a/src/instructions/operator/multiply.rs b/src/instructions/operator/multiply.rs new file mode 100644 index 00000000..6c239168 --- /dev/null +++ b/src/instructions/operator/multiply.rs @@ -0,0 +1,97 @@ +use super::super::prelude::*; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); + let t1 = inputs[0]; + let t2 = inputs[1]; + Ok(if IrType::QuasiInt.contains(t1) { + if IrType::QuasiInt.contains(t2) { + wasm![I32Mul] + } else if IrType::Float.contains(t2) { + let f64_local = func.local(ValType::F64)?; + wasm![ + LocalSet(f64_local), + F64ConvertI32S, + LocalGet(f64_local), + @nanreduce(t2), + F64Mul, + ] + } else { + hq_bug!("bad input") + } + } else if IrType::Float.contains(t1) { + if IrType::Float.contains(t2) { + let f64_local = func.local(ValType::F64)?; + wasm![ + @nanreduce(t2), + LocalSet(f64_local), + @nanreduce(t1), + LocalGet(f64_local), + F64Mul + ] + } else if IrType::QuasiInt.contains(t2) { + let i32_local = func.local(ValType::I32)?; + wasm![ + LocalSet(i32_local), + @nanreduce(t1), + LocalGet(i32_local), + F64ConvertI32S, + F64Mul + ] + } else { + hq_bug!("bad input") + } + } else { + hq_bug!("bad input") + }) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Number, IrType::Number]) +} + +pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); + let t1 = inputs[0]; + let t2 = inputs[1]; + let maybe_positive = (t1.maybe_positive() && t2.maybe_positive()) + || (t2.maybe_negative() && t1.maybe_negative()); + let maybe_negative = (t1.maybe_negative() && t2.maybe_positive()) + || (t2.maybe_negative() && t1.maybe_positive()); + let maybe_zero = t1.maybe_zero() || t1.maybe_nan() || t2.maybe_zero() || t2.maybe_nan(); + let maybe_nan = (IrType::FloatInf.intersects(t1) && (t2.maybe_zero() || t2.maybe_nan())) + || (IrType::FloatInf.intersects(t2) && (t1.maybe_zero() || t1.maybe_nan())); + let maybe_inf = IrType::FloatInf.intersects(t1.or(t2)); + Ok(Some(if IrType::QuasiInt.contains(t1.or(t2)) { + IrType::none_if_false(maybe_positive, IrType::IntPos) + .or(IrType::none_if_false(maybe_negative, IrType::IntNeg)) + .or(IrType::none_if_false(maybe_zero, IrType::IntZero)) + } else if (IrType::QuasiInt.contains(t1) && IrType::Float.contains(t2)) + || (IrType::QuasiInt.contains(t2) && IrType::Float.contains(t1)) + || IrType::Float.contains(t1.or(t2)) + { + IrType::none_if_false(maybe_positive, IrType::FloatPos) + .or(IrType::none_if_false(maybe_negative, IrType::FloatNeg)) + .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) + .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) + .or(IrType::none_if_false(maybe_inf, IrType::FloatInf)) + } else { + // there is a boxed type somewhere + // TODO: can these bounds be tightened? e.g. it may only be a positive int or negative float? + IrType::none_if_false(maybe_positive, IrType::FloatPos.or(IrType::IntPos)) + .or(IrType::none_if_false( + maybe_negative, + IrType::FloatNeg.or(IrType::IntNeg), + )) + .or(IrType::none_if_false( + maybe_zero, + IrType::FloatZero.or(IrType::IntZero), + )) + .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) + .or(IrType::none_if_false(maybe_inf, IrType::FloatInf)) + })) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_multiply; t1, t2 ;} diff --git a/src/instructions/operator/not.rs b/src/instructions/operator/not.rs new file mode 100644 index 00000000..a0e50486 --- /dev/null +++ b/src/instructions/operator/not.rs @@ -0,0 +1,17 @@ +use super::super::prelude::*; + +pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { + Ok(wasm![I32Eqz]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Boolean]) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { + Ok(Some(IrType::Boolean)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_not; t ;} diff --git a/src/instructions/operator/or.rs b/src/instructions/operator/or.rs new file mode 100644 index 00000000..759c0cb3 --- /dev/null +++ b/src/instructions/operator/or.rs @@ -0,0 +1,17 @@ +use super::super::prelude::*; + +pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { + Ok(wasm![I32Or]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Boolean, IrType::Boolean]) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { + Ok(Some(IrType::Boolean)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_or; t1, t2 ;} diff --git a/src/instructions/operator/subtract.rs b/src/instructions/operator/subtract.rs new file mode 100644 index 00000000..72cfc40a --- /dev/null +++ b/src/instructions/operator/subtract.rs @@ -0,0 +1,94 @@ +use super::super::prelude::*; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); + let t1 = inputs[0]; + let t2 = inputs[1]; + Ok(if IrType::QuasiInt.contains(t1) { + if IrType::QuasiInt.contains(t2) { + wasm![I32Sub] + } else if IrType::Float.contains(t2) { + let f64_local = func.local(ValType::F64)?; + wasm![ + LocalSet(f64_local), + F64ConvertI32S, + LocalGet(f64_local), + @nanreduce(t2), + F64Sub, + ] + } else { + hq_bug!("bad input") + } + } else if IrType::Float.contains(t1) { + if IrType::Float.contains(t2) { + let f64_local = func.local(ValType::F64)?; + wasm![ + @nanreduce(t2), + LocalSet(f64_local), + @nanreduce(t1), + LocalGet(f64_local), + F64Sub + ] + } else if IrType::QuasiInt.contains(t2) { + let i32_local = func.local(ValType::I32)?; + wasm![ + LocalSet(i32_local), + @nanreduce(t1), + LocalGet(i32_local), + F64ConvertI32S, + F64Sub + ] + } else { + hq_bug!("bad input") + } + } else { + hq_bug!("bad input") + }) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Number, IrType::Number]) +} + +pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { + hq_assert!(inputs.len() == 2); + let t1 = inputs[0]; + let t2 = inputs[1]; + let maybe_positive = t1.maybe_positive() || t2.maybe_negative(); + let maybe_negative = t1.maybe_negative() || t2.maybe_positive(); + let maybe_zero = ((t1.maybe_zero() || t1.maybe_nan()) && (t2.maybe_zero() || t2.maybe_nan())) + || (t1.maybe_positive() && t2.maybe_positive()) + || (t1.maybe_negative() && t2.maybe_negative()); + let maybe_nan = + IrType::FloatPosInf.intersects(t1.and(t2)) || IrType::FloatNegInf.intersects(t1.and(t2)); + Ok(Some(if IrType::QuasiInt.contains(t1.or(t2)) { + IrType::none_if_false(maybe_positive, IrType::IntPos) + .or(IrType::none_if_false(maybe_negative, IrType::IntNeg)) + .or(IrType::none_if_false(maybe_zero, IrType::IntZero)) + } else if (IrType::QuasiInt.contains(t1) && IrType::Float.contains(t2)) + || (IrType::QuasiInt.contains(t2) && IrType::Float.contains(t1)) + || IrType::Float.contains(t1.or(t2)) + { + IrType::none_if_false(maybe_positive, IrType::FloatPos) + .or(IrType::none_if_false(maybe_negative, IrType::FloatNeg)) + .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) + .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) + } else { + // there is a boxed type somewhere + // TODO: can these bounds be tightened? e.g. it may only be a positive int or negative float? + IrType::none_if_false(maybe_positive, IrType::FloatPos.or(IrType::IntPos)) + .or(IrType::none_if_false( + maybe_negative, + IrType::FloatNeg.or(IrType::IntNeg), + )) + .or(IrType::none_if_false( + maybe_zero, + IrType::FloatZero.or(IrType::IntZero), + )) + .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) + })) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; operator_subtract; t1, t2 ;} diff --git a/src/instructions/procedures.rs b/src/instructions/procedures.rs new file mode 100644 index 00000000..c5836123 --- /dev/null +++ b/src/instructions/procedures.rs @@ -0,0 +1,2 @@ +pub mod argument; +pub mod call_warp; diff --git a/src/instructions/procedures/argument.rs b/src/instructions/procedures/argument.rs new file mode 100644 index 00000000..59235022 --- /dev/null +++ b/src/instructions/procedures/argument.rs @@ -0,0 +1,32 @@ +use super::super::prelude::*; +use crate::wasm::{StepFunc, WasmProject}; + +#[derive(Clone, Debug)] +pub struct Fields(pub usize, pub IrType); + +pub fn wasm( + func: &StepFunc, + _inputs: Rc<[IrType]>, + Fields(index, ty): &Fields, +) -> HQResult> { + hq_assert!( + WasmProject::ir_type_to_wasm(*ty)? + == *func.params().get(*index).ok_or_else(|| make_hq_bug!( + "proc argument index was out of bounds for func params" + ))?, + "proc argument type didn't match that of the corresponding function param" + ); + Ok(wasm![LocalGet((*index).try_into().map_err( + |_| make_hq_bug!("argument index out of bounds") + )?)]) +} + +pub fn acceptable_inputs(_: &Fields) -> Rc<[IrType]> { + Rc::from([]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, &Fields(_, ty): &Fields) -> HQResult> { + Ok(Some(ty)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; diff --git a/src/instructions/procedures/call_warp.rs b/src/instructions/procedures/call_warp.rs new file mode 100644 index 00000000..256685f0 --- /dev/null +++ b/src/instructions/procedures/call_warp.rs @@ -0,0 +1,29 @@ +use super::super::prelude::*; +use crate::ir::Proc; +use crate::wasm::StepFunc; + +#[derive(Clone, Debug)] +pub struct Fields { + pub proc: Rc, +} + +pub fn wasm( + func: &StepFunc, + _inputs: Rc<[IrType]>, + Fields { proc }: &Fields, +) -> HQResult> { + Ok(wasm![ + LocalGet((func.params().len() - 1).try_into().map_err(|_| make_hq_bug!("local index out of bounds"))?), + #LazyWarpedProcCall(Rc::clone(proc)) + ]) +} + +pub fn acceptable_inputs(Fields { proc }: &Fields) -> Rc<[IrType]> { + Rc::from(proc.context().arg_types()) +} + +pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult> { + Ok(None) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; diff --git a/src/instructions/sensing.rs b/src/instructions/sensing.rs new file mode 100644 index 00000000..e7458afe --- /dev/null +++ b/src/instructions/sensing.rs @@ -0,0 +1 @@ +pub mod dayssince2000; diff --git a/src/instructions/sensing/dayssince2000.rs b/src/instructions/sensing/dayssince2000.rs new file mode 100644 index 00000000..5c4d2cd1 --- /dev/null +++ b/src/instructions/sensing/dayssince2000.rs @@ -0,0 +1,22 @@ +use super::super::prelude::*; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 0); + let func_index = func.registries().external_functions().register( + ("sensing", "dayssince2000".into()), + (vec![], vec![ValType::F64]), + )?; + Ok(wasm![Call(func_index)]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([]) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { + Ok(Some(IrType::Float)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +crate::instructions_test! {tests; sensing_dayssince2000; ;} diff --git a/src/instructions/tests.rs b/src/instructions/tests.rs new file mode 100644 index 00000000..d84e5996 --- /dev/null +++ b/src/instructions/tests.rs @@ -0,0 +1,309 @@ +/// Generates unit tests for instructions files. +/// +/// Takes a module name, followed by a semicolon, +/// collowed by the full name of the opcode (in the form of `_`), followed by an optional +/// comma-separated list of arbitrary identifiers corresponding to the number of inputs the block +/// takes, optionally followed by a semicolon and an expression for a sensible default for any fields, +/// optionally followed by a semicolon and a `WasmFlags` configuration (defaults to `Default::default()`). +/// If multiple field values or flags configurations need to be tested, the macro can be repeated with +/// different module names. +/// +/// Example: +/// For a block `foo_bar`, which takes 2 inputs, with Fields(bool), +/// ```ignore +/// instructions_test!(test; foo_bar; t1, t2 @ super::Fields(true)); +/// instructions_test!(test; foo_bar; t1, t2 @ super::Fields(false)); +/// ``` +#[macro_export] +macro_rules! instructions_test { + {$module:ident; $opcode:ident; $($type_arg:ident $(,)?)* $(@$fields:expr)? $(;)?} => { + $crate::instructions_test!{$module; $opcode; $($type_arg,)* $(@$fields)? ; WasmFlags::new(all_wasm_features())} + }; + {$module:ident; $opcode:ident; $($type_arg:ident $(,)?)* $(@ $fields:expr)? ; $flags:expr} => { + #[cfg(test)] + mod $module { + fn flags() -> $crate::wasm::WasmFlags { + $flags + } + + use super::{wasm, output_type, acceptable_inputs}; + use $crate::prelude::*; + use $crate::ir::Type as IrType; + use wasm_encoder::{ + CodeSection, ExportSection, FunctionSection, GlobalSection, HeapType, ImportSection, + Module, RefType, TableSection, TypeSection, MemorySection, MemoryType, ValType, + }; + use $crate::wasm::{flags::all_wasm_features, StepFunc, Registries, WasmProject, WasmFlags}; + + macro_rules! ident_as_irtype { + ( $_:ident ) => { IrType }; + } + + fn types_iter(base_only: bool) -> impl Iterator { + // we need to collect this iterator into a Vec because it doesn't implement clone for some reason, + // which makes itertools angry + $(let $type_arg = IrType::flags().map(|(_, ty)| *ty).collect::>();)* + itertools::iproduct!($($type_arg,)*).filter(move |($($type_arg,)*)| { + let types: &[&IrType] = &[$($type_arg,)*]; + for (i, input) in (*types).into_iter().enumerate() { + // non-base types should be handled and unboxed by a wrapper function + // contained in src/instructions/input_switcher.rs + if base_only && !input.is_base_type() { + return false; + } + // invalid base input types should be handled by insert_casts in + // src.ir/blocks.rs, so we won't test those here + if !acceptable_inputs($(&$fields)?)[i].contains(**input) { + return false; + } + } + true + }) + } + + #[test] + fn output_type_fails_when_wasm_fails() { + for ($($type_arg,)*) in types_iter(true) { + let output_type_result = output_type(Rc::new([$($type_arg,)*]), $(&$fields)?); + let registries = Rc::new(Registries::default()); + let step_func = StepFunc::new(Rc::clone(®istries), Default::default(), flags()); + let wasm_result = wasm(&step_func, Rc::new([$($type_arg,)*]), $(&$fields)?); + match (output_type_result.clone(), wasm_result.clone()) { + (Err(..), Ok(..)) | (Ok(..), Err(..)) => panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)*), output_type_result, wasm_result), + (Err(HQError { err_type: e1, .. }), Err(HQError { err_type: e2, .. })) => { + if e1 != e2 { + panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)*), output_type_result, wasm_result); + } + } + _ => continue, + } + } + } + + #[test] + fn wasm_output_type_matches_expected_output_type() -> HQResult<()> { + for ($($type_arg,)*) in types_iter(true) { + let Ok(output_type) = output_type(Rc::new([$($type_arg,)*]), $(&$fields)?) else { + println!("skipping failed output_type"); + continue; + }; + let registries = Rc::new(Registries::default()); + let types: &[IrType] = &[$($type_arg,)*]; + let params = [Ok(ValType::I32)].into_iter().chain([$($type_arg,)*].into_iter().map(|ty| WasmProject::ir_type_to_wasm(ty))).collect::>>()?; + let result = match output_type { + Some(output) => Some(WasmProject::ir_type_to_wasm(output)?), + None => None, + }; + let step_func = StepFunc::new_with_types(params.into(), result, Rc::clone(®istries), Default::default(), flags()); + let Ok(wasm) = wasm(&step_func, Rc::new([$($type_arg,)*]), $(&$fields)?) else { + println!("skipping failed wasm"); + continue; + }; + for (i, _) in types.iter().enumerate() { + step_func.add_instructions([$crate::wasm::InternalInstruction::Immediate(wasm_encoder::Instruction::LocalGet((i + 1).try_into().unwrap()))])? + } + step_func.add_instructions(wasm)?; + + let mut module = Module::new(); + + let mut imports = ImportSection::new(); + let mut types = TypeSection::new(); + let mut tables = TableSection::new(); + let mut functions = FunctionSection::new(); + let mut codes = CodeSection::new(); + let mut memories = MemorySection::new(); + let mut exports = ExportSection::new(); + let mut globals = GlobalSection::new(); + + memories.memory(MemoryType { + minimum: 1, + maximum: None, + memory64: false, + shared: false, + page_size_log2: None, + }); + + let imported_func_count: u32 = registries.external_functions().registry().borrow().len().try_into().unwrap(); + registries.external_functions().clone().finish(&mut imports, registries.types())?; + step_func.finish(&mut functions, &mut codes, &Default::default(), imported_func_count)?; + registries.types().clone().finish(&mut types); + registries.tables().clone().finish(&imports, &mut tables, &mut exports); + registries.globals().clone().finish(&imports, &mut globals, &mut exports); + + module.section(&types); + module.section(&imports); + module.section(&functions); + module.section(&tables); + module.section(&memories); + module.section(&globals); + module.section(&codes); + + let wasm_bytes = module.finish(); + + wasmparser::validate(&wasm_bytes).map_err(|err| make_hq_bug!("invalid wasm module with types {:?}. Original error message: {}", ($($type_arg,)*), err.message()))?; + } + Ok(()) + } + + #[test] + fn wasm_output_type_matches_wrapped_expected_output_type() -> HQResult<()> { + for ($($type_arg,)*) in types_iter(false) { + let Ok(output_type) = output_type(Rc::new([$($type_arg,)*]), $(&$fields)?) else { + println!("skipping failed output_type"); + continue; + }; + println!("{output_type:?}"); + let registries = Rc::new(Registries::default()); + let types: &[IrType] = &[$($type_arg,)*]; + let params = [Ok(ValType::I32)].into_iter().chain([$($type_arg,)*].into_iter().map(|ty| WasmProject::ir_type_to_wasm(ty))).collect::>>()?; + let result = match output_type { + Some(output) => Some(WasmProject::ir_type_to_wasm(output)?), + None => None, + }; + println!("{result:?}"); + let step_func = StepFunc::new_with_types(params.into(), result, Rc::clone(®istries), Default::default(), flags()); + let wasm = match $crate::instructions::wrap_instruction(&step_func, Rc::new([$($type_arg,)*]), &$crate::instructions::IrOpcode::$opcode$(($fields))?) { + Ok(a) => a, + Err(e) => { + println!("skipping failed wasm (message: {})", e.msg); + continue; + } + }; + println!("{wasm:?}"); + for (i, _) in types.iter().enumerate() { + step_func.add_instructions([$crate::wasm::InternalInstruction::Immediate(wasm_encoder::Instruction::LocalGet((i + 1).try_into().unwrap()))])? + } + step_func.add_instructions(wasm)?; + + println!("{:?}", step_func.instructions().borrow()); + + let mut module = Module::new(); + + let mut imports = ImportSection::new(); + let mut types = TypeSection::new(); + let mut tables = TableSection::new(); + let mut functions = FunctionSection::new(); + let mut codes = CodeSection::new(); + let mut memories = MemorySection::new(); + let mut exports = ExportSection::new(); + let mut globals = GlobalSection::new(); + + memories.memory(MemoryType { + minimum: 1, + maximum: None, + page_size_log2: None, + shared: false, + memory64: false, + }); + let imported_func_count: u32 = registries.external_functions().registry().borrow().len().try_into().unwrap(); + registries.external_functions().clone().finish(&mut imports, registries.types())?; + step_func.finish(&mut functions, &mut codes, &Default::default(), imported_func_count)?; + registries.types().clone().finish(&mut types); + registries.tables().clone().finish(&imports, &mut tables, &mut exports); + registries.globals().clone().finish(&imports, &mut globals, &mut exports); + + module.section(&types); + module.section(&imports); + module.section(&functions); + module.section(&tables); + module.section(&memories); + module.section(&globals); + module.section(&codes); + + let wasm_bytes = module.finish(); + + wasmparser::validate(&wasm_bytes).map_err(|err| make_hq_bug!("invalid wasm module with types {:?}. Original error message: {}", ($($type_arg,)*), err.message()))?; + } + Ok(()) + } + + fn wasm_to_js_type(ty: ValType) -> &'static str { + match ty { + ValType::I32 => "Integer", + ValType::F64 => "number", + ValType::EXTERNREF | ValType::Ref(RefType { + nullable: false, + heap_type: HeapType::EXTERN, + }) => "string", + _ => todo!("unknown js type for wasm type {:?}", ty) + } + } + + #[test] + fn js_functions_match_declared_types() { + use ezno_checker::{check_project as check_js, Diagnostic, INTERNAL_DEFINITION_FILE_PATH as ts_defs}; + use std::path::{Path, PathBuf}; + use std::fs; + + for ($($type_arg,)*) in types_iter(true) { + let registries = Rc::new(Registries::default()); + let step_func = StepFunc::new(Rc::clone(®istries), Default::default(), flags()); + if wasm(&step_func, Rc::new([$($type_arg,)*]), $(&$fields)?).is_err() { + println!("skipping failed wasm"); + continue; + }; + for ((module, name), (params, results)) in registries.external_functions().registry().try_borrow().unwrap().iter() { + assert!(results.len() <= 1, "external function {}::{} registered as returning multiple results", module, name); + let out = if results.len() == 0 { + "void" + } else { + wasm_to_js_type(results[0]) + }; + let arg_idents: Vec = params.iter().enumerate().map(|(i, _)| format!("_{i}")).collect(); + let ins = arg_idents.iter().enumerate().map(|(i, ident)| { + format!( + "{}: {}", + ident, + wasm_to_js_type(*params.get(i).unwrap()) + ) + }).collect::>().join(", "); + let module_path = if *module == "wasm:js-string" { + "wasm-js-string" + } else { + module + }; + let path_buf = PathBuf::from(format!("js/{}/{}.ts", module_path, name)); + let diagnostics = check_js::<_, ezno_checker::synthesis::EznoParser>( + vec![path_buf.clone()], + vec![ts_defs.into()], + &|path: &Path| { + let func_string = fs::read_to_string(path).ok()?; + let test_string = if path == path_buf.as_path() { + format!("function _({ins}): {out} {{ return {name}({ts}); }};", + ins=ins, + out=out, + name=name, + ts=arg_idents.join(", ") + ) + } else { String::from("") }; + let total_string = format!("{func_string};\n{test_string}"); + println!("{}", total_string.clone()); + Some(test_string + .as_str() + .as_bytes() + .into_iter() + .map(|&u| u) + .collect::>() + ) + }, + Default::default(), + (), + None, + ) + .diagnostics; + if diagnostics.contains_error() { + let reasons = diagnostics.into_iter().map(|d| { + match d { + Diagnostic::Global { reason, .. } => reason, + Diagnostic::Position { reason, .. } => reason, + Diagnostic::PositionWithAdditionalLabels { reason, .. } => reason, + } + }).collect::>().join("; "); + panic!("js for external function {}::{} is not type-safe; reason(s): {}", module, name, reasons); + } + } + } + } + } + } +} diff --git a/src/ir.rs b/src/ir.rs index 90a2ffa8..abf24b60 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -1,2345 +1,20 @@ -// intermediate representation -use crate::log; -use crate::sb3::{ - Block, BlockArray, BlockArrayOrId, BlockOpcode, CostumeDataFormat, Field, Input, Sb3Project, - VarVal, VariableInfo, -}; -use crate::HQError; -use alloc::boxed::Box; -use alloc::collections::BTreeMap; -use alloc::rc::Rc; -use alloc::string::{String, ToString}; -use alloc::vec::Vec; -use core::cell::RefCell; -use core::fmt; -use core::hash::BuildHasherDefault; -use hashers::fnv::FNV1aHasher64; -use indexmap::IndexMap; -use lazy_regex::{lazy_regex, Lazy}; -use regex::Regex; -use uuid::Uuid; - -#[derive(Debug, Clone, PartialEq)] -pub struct IrCostume { - pub name: String, - pub data_format: CostumeDataFormat, - pub md5ext: String, -} - -pub type ProcMap = BTreeMap<(String, String), Procedure>; -pub type StepMap = IndexMap<(String, String), Step, BuildHasherDefault>; - -#[derive(Debug)] -pub struct IrProject { - pub threads: Vec, - pub vars: Rc>>, - pub target_names: Vec, - pub costumes: Vec>, - pub steps: StepMap, - pub procedures: ProcMap, // maps (target_id, proccode) to (target_id, step_top_block_id, warp) - pub sb3: Sb3Project, -} - -impl fmt::Display for IrProject { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{{\n\tthreads: {:?},\n\tvars: {:?},\n\t,target_names: {:?},\n\tcostumes: {:?},\n\tsteps: {:?}\n}}", - self.threads, - self.vars, - self.target_names, - self.costumes, - self.steps - ) - } -} - -impl TryFrom for IrProject { - type Error = HQError; - - fn try_from(sb3: Sb3Project) -> Result { - let vars: Rc>> = Rc::new(RefCell::new( - sb3.targets - .iter() - .flat_map(|target| { - target.variables.iter().map(|(id, info)| match info { - VariableInfo::LocalVar(name, val) => { - IrVar::new(id.clone(), name.clone(), val.clone(), false) - } - VariableInfo::CloudVar(name, val, is_cloud) => { - IrVar::new(id.clone(), name.clone(), val.clone(), *is_cloud) - } - }) - }) - .collect(), - )); - - let costumes: Vec> = sb3 - .targets - .iter() - .map(|target| { - target - .costumes - .iter() - .map(|costume| { - IrCostume { - name: costume.name.clone(), - data_format: costume.data_format, - md5ext: costume.md5ext.clone(), - //data: load_asset(costume.md5ext.as_str()), - } - }) - .collect() - }) - .collect(); - - let mut procedures = BTreeMap::new(); - - let mut steps: StepMap = Default::default(); - // insert a noop step so that these step indices match up with the step function indices in the generated wasm - // (step function 0 is a noop) - steps.insert( - ("".into(), "".into()), - Step::new( - vec![], - Rc::new(ThreadContext { - target_index: u32::MAX, - dbg: false, - vars: Rc::new(RefCell::new(vec![])), - target_num: sb3.targets.len(), - costumes: vec![], - proc: None, - }), - ), - ); - let mut threads: Vec = vec![]; - for (target_index, target) in sb3.targets.iter().enumerate() { - for (id, block) in - target - .blocks - .clone() - .iter() - .filter(|(_id, b)| match b.block_info() { - Some(block_info) => { - block_info.top_level - && matches!(block_info.opcode, BlockOpcode::event_whenflagclicked) - } - None => false, - }) - { - let context = Rc::new(ThreadContext { - target_index: target_index.try_into().map_err(|_| make_hq_bug!(""))?, - dbg: target.comments.clone().iter().any(|(_id, comment)| { - matches!(comment.block_id.clone(), Some(d) if &d == id) - && comment.text.clone() == *"hq-dbg" - }), - vars: Rc::clone(&vars), - target_num: sb3.targets.len(), - costumes: costumes.get(target_index).ok_or(make_hq_bug!(""))?.clone(), - proc: None, - }); - let thread = Thread::from_hat( - block.clone(), - target.blocks.clone(), - context, - &mut steps, - target.name.clone(), - &mut procedures, - )?; - threads.push(thread); - } - } - let target_names = sb3 - .targets - .iter() - .map(|t| t.name.clone()) - .collect::>(); - Ok(Self { - vars, - threads, - target_names, - steps, - costumes, - sb3, - procedures, - }) - } -} - -#[allow(non_camel_case_types)] -#[allow(non_snake_case)] -#[derive(Debug, Clone, PartialEq)] -pub enum IrOpcode { - boolean { - BOOL: bool, - }, - control_repeat, - control_repeat_until, - control_while, - control_for_each { - VARIABLE: String, - }, - control_forever, - control_wait, - control_wait_until, - control_stop { - STOP_OPTION: String, - }, - control_create_clone_of, - control_create_clone_of_menu { - CLONE_OPTOON: String, - }, - control_delete_this_clone, - control_get_counter, - control_incr_counter, - control_clear_counter, - control_all_at_once, - control_start_as_clone, - data_variable { - VARIABLE: String, - assume_type: Option, - }, - data_setvariableto { - VARIABLE: String, - assume_type: Option, - }, - data_teevariable { - VARIABLE: String, - assume_type: Option, - }, - data_hidevariable { - VARIABLE: String, - }, - data_showvariable { - VARIABLE: String, - }, - data_listcontents { - LIST: String, - }, - data_addtolist, - data_deleteoflist, - data_deletealloflist, - data_insertatlist, - data_replaceitemoflist, - data_itemoflist, - data_itemnumoflist, - data_lengthoflist, - data_listcontainsitem, - data_hidelist, - data_showlist, - event_broadcast, - event_broadcastandwait, - event_whenflagclicked, - event_whenkeypressed, - event_whenthisspriteclicked, - event_whentouchingobject, - event_whenstageclicked, - event_whenbackdropswitchesto, - event_whengreaterthan, - event_whenbroadcastreceived, - hq_cast(InputType, InputType), - hq_drop(usize), - hq_goto { - step: Option<(String, String)>, - does_yield: bool, - }, - hq_goto_if { - step: Option<(String, String)>, - does_yield: bool, - }, - hq_launch_procedure(Procedure), - looks_say, - looks_sayforsecs, - looks_think, - looks_thinkforsecs, - looks_show, - looks_hide, - looks_hideallsprites, - looks_switchcostumeto, - looks_switchbackdropto, - looks_switchbackdroptoandwait, - looks_nextcostume, - looks_nextbackdrop, - looks_changeeffectby, - looks_seteffectto, - looks_cleargraphiceffects, - looks_changesizeby, - looks_setsizeto, - looks_changestretchby, - looks_setstretchto, - looks_gotofrontback, - looks_goforwardbackwardlayers, - looks_size, - looks_costumenumbername, - looks_backdropnumbername, - looks_backdrops, - math_angle { - NUM: i32, - }, - math_integer { - NUM: i32, - }, - math_number { - NUM: f64, - }, - math_positive_number { - NUM: i32, - }, - math_whole_number { - NUM: i32, - }, - motion_movesteps, - motion_gotoxy, - motion_goto, - motion_turnright, - motion_turnleft, - motion_pointindirection, - motion_pointtowards, - motion_glidesecstoxy, - motion_glideto, - motion_ifonedgebounce, - motion_setrotationstyle, - motion_changexby, - motion_setx, - motion_changeyby, - motion_sety, - motion_xposition, - motion_yposition, - motion_direction, - motion_scroll_right, - motion_scroll_up, - motion_align_scene, - motion_xscroll, - motion_yscroll, - motion_pointtowards_menu, - operator_add, - operator_subtract, - operator_multiply, - operator_divide, - operator_lt, - operator_equals, - operator_gt, - operator_and, - operator_or, - operator_not, - operator_random, - operator_join, - operator_letter_of, - operator_length, - operator_contains, - operator_mod, - operator_round, - operator_mathop { - OPERATOR: String, - }, - pen_clear, - pen_stamp, - pen_penDown, - pen_penUp, - pen_setPenColorToColor, - pen_changePenColorParamBy, - pen_setPenColorParamTo, - pen_changePenSizeBy, - pen_setPenSizeTo, - pen_setPenShadeToNumber, - pen_changePenShadeBy, - pen_setPenHueToNumber, - pen_changePenHueBy, - argument_reporter_string_number, - argument_reporter_boolean, - sensing_touchingobject, - sensing_touchingcolor, - sensing_coloristouchingcolor, - sensing_distanceto, - sensing_distancetomenu, - sensing_timer, - sensing_resettimer, - sensing_of, - sensing_mousex, - sensing_mousey, - sensing_setdragmode, - sensing_mousedown, - sensing_keypressed, - sensing_current, - sensing_dayssince2000, - sensing_loudness, - sensing_loud, - sensing_askandwait, - sensing_answer, - sensing_username, - sensing_userid, - sensing_touchingobjectmenu, - sensing_keyoptions, - sensing_of_object_menu, - sound_play, - sound_playuntildone, - sound_stopallsounds, - sound_seteffectto, - sound_changeeffectby, - sound_cleareffects, - sound_sounds_menu, - sound_beats_menu, - sound_effects_menu, - sound_setvolumeto, - sound_changevolumeby, - sound_volume, - text { - TEXT: String, - }, - unknown_const { - val: IrVal, - }, -} - -#[derive(Clone)] -pub struct TypeStack(pub Rc>>, pub InputType); - -impl TypeStack { - pub fn new_some(prev: TypeStack) -> Rc>> { - Rc::new(RefCell::new(Some(prev))) - } - pub fn new(prev: Option) -> Rc>> { - Rc::new(RefCell::new(prev)) - } -} - -#[allow(clippy::len_without_is_empty)] -pub trait TypeStackImpl { - fn get(&self, i: usize) -> Rc>>; - fn len(&self) -> usize; -} - -impl TypeStackImpl for Rc>> { - fn get(&self, i: usize) -> Rc>> { - if i == 0 || self.borrow().is_none() { - Rc::clone(self) - } else { - self.borrow().clone().unwrap().0.get(i - 1) - } - } - - fn len(&self) -> usize { - if self.borrow().is_none() { - 0 - } else { - 1 + self.borrow().clone().unwrap().0.len() - } - } -} - -#[derive(Debug, Clone)] -pub struct IrBlock { - pub opcode: IrOpcode, - pub type_stack: Rc>>, -} - -impl IrBlock { - pub fn new_with_stack( - opcode: IrOpcode, - type_stack: Rc>>, - add_cast: &mut F, - ) -> Result - where - F: FnMut(usize, &InputType), - { - let expected_inputs = opcode.expected_inputs()?; - if type_stack.len() < expected_inputs.len() { - hq_bug!( - "expected {} inputs, got {} at {:?}", - expected_inputs.len(), - type_stack.len(), - opcode, - ); - } - let arity = expected_inputs.len(); - for i in 0..expected_inputs.len() { - let expected = expected_inputs.get(i).ok_or(make_hq_bug!(""))?; - let actual = &type_stack.get(arity - 1 - i).borrow().clone().unwrap().1; - if !expected.includes(actual) { - add_cast(arity - 1 - i, expected); - } - } - let output_stack = opcode.output(type_stack)?; - Ok(IrBlock { - opcode, - type_stack: output_stack, - }) - } - - pub fn new_with_stack_no_cast( - opcode: IrOpcode, - type_stack: Rc>>, - ) -> Result { - let expected_inputs = opcode.expected_inputs()?; - if type_stack.len() < expected_inputs.len() { - hq_bug!( - "expected {} inputs, got {}", - expected_inputs.len(), - type_stack.len() - ); - } - let arity = expected_inputs.len(); - for i in 0..expected_inputs.len() { - let expected = expected_inputs.get(i).ok_or(make_hq_bug!(""))?; - let actual = &type_stack - .get(arity - 1 - i) - .borrow() - .clone() - .ok_or(make_hq_bug!(""))? - .1; - - if !expected.includes(actual) { - hq_bug!( - "cast needed at input position {:}, {:?} -> {:?}, but no casts were expected; at {:?}", - i, - actual, - expected, - opcode, - ); - } - } - let output_stack = opcode.output(type_stack)?; - Ok(IrBlock { - opcode, - type_stack: output_stack, - }) - } - - pub fn does_request_redraw(&self) -> bool { - use IrOpcode::*; - matches!( - self.opcode(), - looks_say - | looks_think - | hq_goto { - does_yield: true, - .. - } - | motion_gotoxy - | pen_penDown - | pen_clear - | looks_switchcostumeto - | motion_turnleft - | motion_turnright - | looks_setsizeto - | looks_changesizeby - ) - } - pub fn is_hat(&self) -> bool { - use IrOpcode::*; - matches!(self.opcode, event_whenflagclicked) - } - pub fn opcode(&self) -> &IrOpcode { - &self.opcode - } - pub fn type_stack(&self) -> Rc>> { - Rc::clone(&self.type_stack) - } - - pub fn is_const(&self) -> bool { - use IrOpcode::*; - matches!( - self.opcode(), - math_number { .. } - | math_whole_number { .. } - | math_integer { .. } - | math_angle { .. } - | math_positive_number { .. } - | text { .. } - ) - } - - pub fn const_value(&self, value_stack: &mut Vec) -> Result, HQError> { - use IrOpcode::*; - //dbg!(value_stack.len()); - let arity = self.opcode().expected_inputs()?.len(); - if arity > value_stack.len() { - return Ok(None); - } - match self.opcode() { - math_number { NUM } => value_stack.push(IrVal::Float(*NUM)), - math_positive_number { NUM } - | math_integer { NUM } - | math_angle { NUM } - | math_whole_number { NUM } => value_stack.push(IrVal::Int(*NUM)), - text { TEXT } => value_stack.push(IrVal::String(TEXT.to_string())), - operator_add => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Float(num1 + num2)); - } - operator_subtract => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Float(num1 - num2)); - } - operator_multiply => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Float(num1 * num2)); - } - operator_divide => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Float(num1 / num2)); - } - operator_mod => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Float(num1 % num2)); - } - operator_lt => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Boolean(num1 < num2)); - } - operator_gt => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Boolean(num1 > num2)); - } - operator_and => { - let bool2 = value_stack.pop().unwrap().to_bool(); - let bool1 = value_stack.pop().unwrap().to_bool(); - value_stack.push(IrVal::Boolean(bool1 && bool2)); - } - operator_or => { - let bool2 = value_stack.pop().unwrap().to_bool(); - let bool1 = value_stack.pop().unwrap().to_bool(); - value_stack.push(IrVal::Boolean(bool1 || bool2)); - } - operator_join => { - let str2 = value_stack.pop().unwrap().to_string(); - let str1 = value_stack.pop().unwrap().to_string(); - value_stack.push(IrVal::String(str1 + &str2)); - } - operator_length => { - let string = value_stack.pop().unwrap().to_string(); - value_stack.push(IrVal::Int(string.len().try_into().unwrap())); - } - operator_not => { - let b = value_stack.pop().unwrap().to_bool(); - value_stack.push(IrVal::Boolean(!b)); - } - operator_round => { - let num = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Int(num as i32)); - } - hq_cast(_from, to) => { - let val = value_stack.pop().unwrap(); - let ty = to.least_restrictive_concrete_type(); - match ty { - InputType::ConcreteInteger => value_stack.push(IrVal::Int(val.to_i32())), - InputType::Float => value_stack.push(IrVal::Float(val.to_f64())), - InputType::String => value_stack.push(IrVal::String(val.to_string())), - InputType::Boolean => value_stack.push(IrVal::Boolean(val.to_bool())), - InputType::Unknown => { - value_stack.push(IrVal::Unknown(Box::new(val))); - } - _ => hq_bug!("unexpected non-concrete type {:?}", ty), - }; - } - _ => return Ok(None), - }; - Ok(Some(())) - } -} - -/// represents an input (or output) type of a block. -/// output types shpuld always be exactly known, -/// so unions, or types that resolve to a union, -/// should never be used as output types -#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)] -pub enum InputType { - Any, - String, - Number, - Float, - Integer, - Boolean, - ConcreteInteger, - Unknown, - Union(Box, Box), -} - -impl InputType { - /// returns the base type that a type is aliased to, if present; - /// otherwise, returns a clone of itself - pub fn base_type(&self) -> InputType { - use InputType::*; - // unions of types should be represented with the least restrictive type first, - // so that casting chooses the less restrictive type to cast to - match self { - Any => Union( - Box::new(Union(Box::new(Unknown), Box::new(String))), - Box::new(Number), - ) - .base_type(), - Number => Union(Box::new(Float), Box::new(Integer)).base_type(), - Integer => Union(Box::new(ConcreteInteger), Box::new(Boolean)).base_type(), - Union(a, b) => Union(Box::new(a.base_type()), Box::new(b.base_type())), - _ => self.clone(), - } - } - - /// when a type is a union type, returns the first concrete type in that union - /// (this assumes that the least restrictive type is placed first in the union). - /// otherwise, returns itself. - pub fn least_restrictive_concrete_type(&self) -> InputType { - match self.base_type() { - InputType::Union(a, _) => a.least_restrictive_concrete_type(), - other => other, - } - } - - /// determines whether a type is a superyype of another type - pub fn includes(&self, other: &Self) -> bool { - if self.base_type() == other.base_type() { - true - } else if let InputType::Union(a, b) = self.base_type() { - a.includes(other) || b.includes(other) - } else { - false - } - } - - /// attempts to promote a type to one of the ones provided. - /// none of the provided types should overlap with each other, - /// so that there is no ambiguity over which type it should be promoted to. - /// if there are no types that can be prpmoted to, - /// an `Err(HQError)` will be returned. - pub fn loosen_to(&self, others: T) -> Result - where - T: IntoIterator, - T::IntoIter: ExactSizeIterator, - { - for other in others { - if other.includes(self) { - return Ok(other); - } - } - Err(make_hq_bug!( - "couldn't find any types that this type can be demoted to" - )) - } -} - -impl IrOpcode { - pub fn does_request_redraw(&self) -> bool { - use IrOpcode::*; - matches!(self, looks_say | looks_think) - } - - pub fn expected_inputs(&self) -> Result, HQError> { - use InputType::*; - use IrOpcode::*; - Ok(match self { - operator_add | operator_subtract | operator_multiply | operator_divide - | operator_mod | operator_random => vec![Number, Number], - operator_round | operator_mathop { .. } => vec![Number], - looks_say | looks_think => vec![Unknown], - data_setvariableto { - assume_type: None, .. - } - | data_teevariable { - assume_type: None, .. - } => vec![Unknown], - data_setvariableto { - assume_type: Some(ty), - .. - } - | data_teevariable { - assume_type: Some(ty), - .. - } => vec![ty.clone()], - math_integer { .. } - | math_angle { .. } - | math_whole_number { .. } - | math_positive_number { .. } - | math_number { .. } - | sensing_timer - | sensing_dayssince2000 - | looks_size - | data_variable { .. } - | text { .. } - | boolean { .. } - | unknown_const { .. } => vec![], - operator_lt | operator_gt => vec![Number, Number], - operator_equals => vec![Any, Any], - operator_and | operator_or => vec![Boolean, Boolean], - operator_not => vec![Boolean], - operator_join | operator_contains => vec![String, String], - operator_letter_of => vec![Number, String], - operator_length => vec![String], - hq_goto { .. } - | sensing_resettimer - | pen_clear - | pen_stamp - | pen_penDown - | pen_penUp => vec![], - hq_goto_if { .. } => vec![Boolean], - hq_drop(n) => vec![Any; *n], - hq_cast(from, _to) => vec![from.clone()], - pen_setPenColorToColor - | pen_changePenSizeBy - | pen_setPenSizeTo - | pen_setPenShadeToNumber - | pen_changePenShadeBy - | pen_setPenHueToNumber - | pen_changePenHueBy - | looks_setsizeto - | looks_changesizeby - | motion_turnleft - | motion_turnright => vec![Number], - // todo: looks_switchcostumeto waiting on generic monomorphisation to work properly - looks_switchcostumeto => vec![Any], - pen_changePenColorParamBy | pen_setPenColorParamTo => vec![String, Number], - motion_gotoxy => vec![Number, Number], - hq_launch_procedure(Procedure { arg_types, .. }) => arg_types.clone(), - _ => hq_todo!("{:?}", &self), - }) - } - - pub fn output( - &self, - type_stack: Rc>>, - ) -> Result>>, HQError> { - use InputType::*; - use IrOpcode::*; - let expected_inputs = self.expected_inputs()?; - if type_stack.len() < expected_inputs.len() { - hq_bug!( - "expected {} inputs, got {}", - expected_inputs.len(), - type_stack.len() - ); - } - let arity = expected_inputs.len(); - let get_input = |i| { - Ok(type_stack - .get(arity - 1 - i) - .borrow() - .clone() - .ok_or(make_hq_bug!(""))? - .1) - }; - let output = match self { - data_teevariable { .. } => Ok(Rc::clone(&type_stack)), - hq_cast(_from, to) => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(1)), - to.clone(), - ))), - operator_add | operator_subtract | operator_multiply | operator_random - | operator_mod => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(2)), - if Integer.includes(&get_input(0)?) && Integer.includes(&get_input(1)?) { - ConcreteInteger - } else { - Float - }, - ))), - operator_divide => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(2)), - Float, - ))), - looks_size | sensing_timer | sensing_dayssince2000 | math_number { .. } => Ok( - TypeStack::new_some(TypeStack(Rc::clone(&type_stack), Float)), - ), - data_variable { - assume_type: None, .. - } - | unknown_const { .. } => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack), - Unknown, - ))), - data_variable { - assume_type: Some(ty), - .. - } => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack), - ty.clone(), - ))), - math_integer { .. } - | math_angle { .. } - | math_whole_number { .. } - | math_positive_number { .. } => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack), - ConcreteInteger, - ))), - operator_round => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(1)), - ConcreteInteger, - ))), - operator_length => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(1)), - ConcreteInteger, - ))), - operator_mathop { OPERATOR } => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(1)), - match OPERATOR.as_str().to_uppercase().as_str() { - "CEILING" | "FLOOR" => ConcreteInteger, - _ => Float, - }, - ))), - text { .. } => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack), - String, - ))), - boolean { .. } => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack), - Boolean, - ))), - operator_join | operator_letter_of => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(2)), - String, - ))), - operator_contains | operator_and | operator_or | operator_gt | operator_lt - | operator_equals => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(2)), - Boolean, - ))), - operator_not => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(1)), - Boolean, - ))), - motion_gotoxy | pen_setPenColorParamTo => Ok(Rc::clone(&type_stack.get(2))), - data_setvariableto { .. } - | motion_turnleft - | motion_turnright - | looks_switchcostumeto - | looks_changesizeby - | looks_setsizeto - | looks_say - | looks_think - | pen_setPenColorToColor - | pen_changePenSizeBy - | pen_setPenSizeTo - | pen_setPenShadeToNumber - | pen_changePenShadeBy - | pen_setPenHueToNumber - | pen_changePenHueBy - | hq_goto_if { .. } => Ok(Rc::clone(&type_stack.get(1))), - hq_goto { .. } - | sensing_resettimer - | pen_clear - | pen_penUp - | pen_penDown - | pen_stamp => Ok(Rc::clone(&type_stack)), - hq_drop(n) => Ok(type_stack.get(*n)), - hq_launch_procedure(Procedure { arg_types, .. }) => Ok(type_stack.get(arg_types.len())), - _ => hq_todo!("{:?}", &self), - }; - output - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct IrVar { - id: String, - name: String, - initial_value: VarVal, - is_cloud: bool, -} - -impl IrVar { - pub fn new(id: String, name: String, initial_value: VarVal, is_cloud: bool) -> Self { - Self { - id, - name, - initial_value, - is_cloud, - } - } - pub fn id(&self) -> &String { - &self.id - } - pub fn name(&self) -> &String { - &self.name - } - pub fn initial_value(&self) -> &VarVal { - &self.initial_value - } - pub fn is_cloud(&self) -> &bool { - &self.is_cloud - } -} - -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] -pub enum ThreadStart { - GreenFlag, -} - -#[derive(Debug, Clone)] -pub struct Step { - pub opcodes: Vec, - pub context: Rc, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Procedure { - pub arg_types: Vec, - pub first_step: String, - pub target_id: String, - pub warp: bool, -} - -impl fmt::Debug for TypeStack { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let this = Rc::new(RefCell::new(Some(self.clone()))); - f.debug_list() - .entries( - (0..this.len()) - .rev() - .map(|i| this.get(this.len() - 1 - i).borrow().clone().unwrap().1), - ) - .finish() - } -} - -impl Step { - pub fn new(opcodes: Vec, context: Rc) -> Step { - Step { opcodes, context } - } - pub fn opcodes(&self) -> &Vec { - &self.opcodes - } - pub fn opcodes_mut(&mut self) -> &mut Vec { - &mut self.opcodes - } - pub fn context(&self) -> Rc { - Rc::clone(&self.context) - } - pub fn const_fold(&mut self) -> Result<(), HQError> { - let mut value_stack: Vec = vec![]; - let mut is_folding = false; - let mut fold_start = 0; - let mut to_splice/*: Vec<(Range, Vec)>*/ = vec![]; - for (i, opcode) in self.opcodes.iter().enumerate() { - if !is_folding { - if !opcode.is_const() { - continue; - } - is_folding = true; - fold_start = i; - } - let const_value = opcode.const_value(&mut value_stack)?; - if const_value.is_none() { - is_folding = false; - let mut type_stack = - Rc::clone(&self.opcodes.get(fold_start).unwrap().type_stack.get(1)); - for ty in value_stack.iter().map(|val| val.as_input_type()) { - let prev_type_stack = Rc::clone(&type_stack); - type_stack = TypeStack::new_some(TypeStack(prev_type_stack, ty)); - } - //dbg!(&value_stack); - let folded_blocks: Result, HQError> = value_stack - .iter() - .enumerate() - .map(|(j, val)| val.try_as_block(type_stack.get(value_stack.len() - j))) - .collect(); - to_splice.push((fold_start..i, folded_blocks?)); - value_stack = vec![]; - } - } - if is_folding { - let mut type_stack = - Rc::clone(&self.opcodes.get(fold_start).unwrap().type_stack.get(1)); - for ty in value_stack.iter().map(|val| val.as_input_type()) { - let prev_type_stack = Rc::clone(&type_stack); - type_stack = TypeStack::new_some(TypeStack(prev_type_stack, ty)); - } - let folded_blocks: Result, HQError> = value_stack - .iter() - .enumerate() - .map(|(j, val)| val.try_as_block(type_stack.get(value_stack.len() - j))) - .collect(); - to_splice.push((fold_start..self.opcodes().len(), folded_blocks?)); - } - for (range, replace) in to_splice.into_iter().rev() { - self.opcodes.splice(range, replace); - } - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum IrVal { - Int(i32), - Float(f64), - Boolean(bool), - String(String), - Unknown(Box), -} - -impl IrVal { - pub fn to_f64(self) -> f64 { - match self { - IrVal::Unknown(u) => u.to_f64(), - IrVal::Float(f) => f, - IrVal::Int(i) => i as f64, - IrVal::Boolean(b) => b as i32 as f64, - IrVal::String(s) => s.parse().unwrap_or(0.0), - } - } - pub fn to_i32(self) -> i32 { - match self { - IrVal::Unknown(u) => u.to_i32(), - IrVal::Float(f) => f as i32, - IrVal::Int(i) => i, - IrVal::Boolean(b) => b as i32, - IrVal::String(s) => s.parse().unwrap_or(0), - } - } - #[allow(clippy::inherent_to_string)] - pub fn to_string(self) -> String { - match self { - IrVal::Unknown(u) => u.to_string(), - IrVal::Float(f) => format!("{f}"), - IrVal::Int(i) => format!("{i}"), - IrVal::Boolean(b) => format!("{b}"), - IrVal::String(s) => s, - } - } - pub fn to_bool(self) -> bool { - match self { - IrVal::Unknown(u) => u.to_bool(), - IrVal::Boolean(b) => b, - _ => unreachable!(), - } - } - pub fn as_input_type(&self) -> InputType { - match self { - IrVal::Unknown(_) => InputType::Unknown, - IrVal::Float(_) => InputType::Float, - IrVal::Int(_) => InputType::ConcreteInteger, - IrVal::String(_) => InputType::String, - IrVal::Boolean(_) => InputType::Boolean, - } - } - pub fn try_as_block( - &self, - type_stack: Rc>>, - ) -> Result { - IrBlock::new_with_stack_no_cast( - match self { - IrVal::Int(num) => IrOpcode::math_integer { NUM: *num }, - IrVal::Float(num) => IrOpcode::math_number { NUM: *num }, - IrVal::String(text) => IrOpcode::text { TEXT: text.clone() }, - IrVal::Boolean(b) => IrOpcode::boolean { BOOL: *b }, - IrVal::Unknown(u) => IrOpcode::unknown_const { val: *u.clone() }, - }, - type_stack, - ) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct ThreadContext { - pub target_index: u32, - pub dbg: bool, - pub vars: Rc>>, // todo: fix variable id collisions between targets - pub target_num: usize, - pub costumes: Vec, - pub proc: Option, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Thread { - start: ThreadStart, - first_step: String, - target_id: String, -} - -static ARG_REGEX: Lazy = lazy_regex!(r#"[^\\]%[nbs]"#); - -fn arg_types_from_proccode(proccode: String) -> Result, HQError> { - // https://github.com/scratchfoundation/scratch-blocks/blob/abbfe93136fef57fdfb9a077198b0bc64726f012/blocks_vertical/procedures.js#L207-L215 - (*ARG_REGEX) - .find_iter(proccode.as_str()) - .map(|s| s.as_str().to_string().trim().to_string()) - .filter(|s| s.as_str().starts_with('%')) - .map(|s| s[..2].to_string()) - .map(|s| { - Ok(match s.as_str() { - "%n" => InputType::Number, - "%s" => InputType::String, - "%b" => InputType::Boolean, - other => hq_bug!("invalid proccode arg \"{other}\" found"), - }) - }) - .collect::, _>>() -} - -fn add_procedure( - target_id: String, - proccode: String, - expect_warp: bool, - blocks: &BTreeMap, - steps: &mut StepMap, - procedures: &mut ProcMap, - context: Rc, -) -> Result { - if let Some(procedure) = procedures.get(&(target_id.clone(), proccode.clone())) { - return Ok(procedure.clone()); - } - let arg_types = arg_types_from_proccode(proccode.clone())?; - let Some(prototype_block) = blocks.values().find(|block| { - let Some(info) = block.block_info() else { - return false; - }; - return info.opcode == BlockOpcode::procedures_prototype - && (match info.mutation.mutations.get("proccode") { - None => false, - Some(p) => { - if let serde_json::Value::String(ref s) = p { - log(s.as_str()); - *s == proccode - } else { - false - } - } - }); - }) else { - hq_bad_proj!("no prototype found for {proccode} in {target_id}") - }; - let Some(def_block) = blocks.get( - &prototype_block - .block_info() - .unwrap() - .parent - .clone() - .ok_or(make_hq_bad_proj!("prototype block without parent"))?, - ) else { - hq_bad_proj!("no definition found for {proccode} in {target_id}") - }; - if let Some(warp_val) = prototype_block - .block_info() - .unwrap() - .mutation - .mutations - .get("warp") - { - let warp = match warp_val { - serde_json::Value::Bool(w) => *w, - serde_json::Value::String(wstr) => match wstr.as_str() { - "true" => true, - "false" => false, - _ => hq_bad_proj!("unexpected string for warp mutation"), - }, - _ => hq_bad_proj!("bad type for warp mutation"), - }; - if warp != expect_warp { - hq_bad_proj!("proc call warp does not equal definition warp") - } - } else { - hq_bad_proj!("missing warp mutation on orocedures_dedinition") - } - let Some(ref next) = def_block.block_info().unwrap().next else { - hq_todo!("empty procedure definition") - }; - let procedure = Procedure { - arg_types, - first_step: next.clone(), - target_id: target_id.clone(), - warp: expect_warp, - }; - step_from_top_block( - next.clone(), - vec![], - blocks, - Rc::new(ThreadContext { - proc: Some(procedure.clone()), - dbg: false, - costumes: context.costumes.clone(), - target_index: context.target_index, - vars: Rc::clone(&context.vars), - target_num: context.target_num, - }), - steps, - target_id.clone(), - procedures, - )?; - procedures.insert((target_id.clone(), proccode.clone()), procedure.clone()); - Ok(procedure) -} - -trait IrBlockVec { - #[allow(clippy::too_many_arguments)] - fn add_block( - &mut self, - block_id: String, - blocks: &BTreeMap, - context: Rc, - last_nexts: Vec, - steps: &mut StepMap, - target_id: String, - procedures: &mut ProcMap, - ) -> Result<(), HQError>; - fn add_block_arr(&mut self, block_arr: &BlockArray) -> Result<(), HQError>; - fn add_inputs( - &mut self, - inputs: &BTreeMap, - blocks: &BTreeMap, - context: Rc, - steps: &mut StepMap, - target_id: String, - procedures: &mut ProcMap, - ) -> Result<(), HQError>; - fn get_type_stack(&self, i: Option) -> Rc>>; -} - -impl IrBlockVec for Vec { - fn add_inputs( - &mut self, - inputs: &BTreeMap, - blocks: &BTreeMap, - context: Rc, - steps: &mut StepMap, - target_id: String, - procedures: &mut ProcMap, - ) -> Result<(), HQError> { - for (name, input) in inputs { - if name.starts_with("SUBSTACK") { - continue; - } - match input { - Input::Shadow(_, maybe_block, _) | Input::NoShadow(_, maybe_block) => { - let Some(block) = maybe_block else { - hq_bad_proj!("block doesn't exist"); // is this a problem with the project, or is it a bug? - }; - match block { - BlockArrayOrId::Id(id) => { - self.add_block( - id.clone(), - blocks, - Rc::clone(&context), - vec![], - steps, - target_id.clone(), - procedures, - )?; - } - BlockArrayOrId::Array(arr) => { - self.add_block_arr(arr)?; - } - } - } - } - } - Ok(()) - } - fn add_block_arr(&mut self, block_arr: &BlockArray) -> Result<(), HQError> { - let prev_block = self.last(); - let type_stack = if let Some(block) = prev_block { - Rc::clone(&block.type_stack) - } else { - Rc::new(RefCell::new(None)) - }; - self.push(IrBlock::new_with_stack_no_cast( - match block_arr { - BlockArray::NumberOrAngle(ty, value) => match ty { - 4 => IrOpcode::math_number { NUM: *value }, - 5 => IrOpcode::math_positive_number { NUM: *value as i32 }, - 6 => IrOpcode::math_whole_number { NUM: *value as i32 }, - 7 => IrOpcode::math_integer { NUM: *value as i32 }, - 8 => IrOpcode::math_angle { NUM: *value as i32 }, - _ => hq_bad_proj!("bad project json (block array of type ({}, u32))", ty), - }, - BlockArray::ColorOrString(ty, value) => match ty { - 4 => IrOpcode::math_number { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, - 5 => IrOpcode::math_positive_number { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, - 6 => IrOpcode::math_whole_number { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, - 7 => IrOpcode::math_integer { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, - 8 => IrOpcode::math_angle { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, - 9 => hq_todo!(""), - 10 => IrOpcode::text { - TEXT: value.to_string(), - }, - _ => hq_bad_proj!("bad project json (block array of type ({}, string))", ty), - }, - BlockArray::Broadcast(ty, _name, id) => match ty { - 12 => IrOpcode::data_variable { - VARIABLE: id.to_string(), - assume_type: None, - }, - _ => hq_todo!(""), - }, - BlockArray::VariableOrList(ty, _name, id, _pos_x, _pos_y) => match ty { - 12 => IrOpcode::data_variable { - VARIABLE: id.to_string(), - assume_type: None, - }, - _ => hq_todo!(""), - }, - }, - type_stack, - )?); - Ok(()) - } - fn get_type_stack(&self, i: Option) -> Rc>> { - let prev_block = if let Some(j) = i { - self.get(j) - } else { - self.last() - }; - if let Some(block) = prev_block { - Rc::clone(&block.type_stack) - } else { - Rc::new(RefCell::new(None)) - } - } - fn add_block( - &mut self, - block_id: String, - blocks: &BTreeMap, - context: Rc, - last_nexts: Vec, - steps: &mut StepMap, - target_id: String, - procedures: &mut ProcMap, - ) -> Result<(), HQError> { - let block = blocks.get(&block_id).ok_or(make_hq_bug!(""))?; - match block { - Block::Normal { block_info, .. } => { - self.add_inputs( - &block_info.inputs, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - let ops: Vec<_> = match block_info.opcode { - BlockOpcode::motion_gotoxy => vec![IrOpcode::motion_gotoxy], - BlockOpcode::sensing_timer => vec![IrOpcode::sensing_timer], - BlockOpcode::sensing_dayssince2000 => vec![IrOpcode::sensing_dayssince2000], - BlockOpcode::sensing_resettimer => vec![IrOpcode::sensing_resettimer], - BlockOpcode::looks_say => vec![IrOpcode::looks_say], - BlockOpcode::looks_think => vec![IrOpcode::looks_think], - BlockOpcode::looks_show => vec![IrOpcode::looks_show], - BlockOpcode::looks_hide => vec![IrOpcode::looks_hide], - BlockOpcode::looks_hideallsprites => vec![IrOpcode::looks_hideallsprites], - BlockOpcode::looks_switchcostumeto => vec![IrOpcode::looks_switchcostumeto], - BlockOpcode::looks_costume => { - let val = match block_info.fields.get("COSTUME").ok_or( - make_hq_bad_proj!("invalid project.json - missing field COSTUME"), - )? { - Field::Value((val,)) => val, - Field::ValueId(val, _) => val, - }; - let VarVal::String(name) = val.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null costume name for COSTUME field" - ))? - else { - hq_bad_proj!( - "invalid project.json - COSTUME field is not of type String" - ); - }; - log(name.as_str()); - let index = context - .costumes - .iter() - .position(|costume| costume.name == name) - .ok_or(make_hq_bad_proj!("missing costume with name {}", name))?; - log(format!("{}", index).as_str()); - vec![IrOpcode::math_whole_number { NUM: index as i32 }] - } - BlockOpcode::looks_switchbackdropto => { - vec![IrOpcode::looks_switchbackdropto] - } - BlockOpcode::looks_switchbackdroptoandwait => { - vec![IrOpcode::looks_switchbackdroptoandwait] - } - BlockOpcode::looks_nextcostume => vec![IrOpcode::looks_nextcostume], - BlockOpcode::looks_nextbackdrop => vec![IrOpcode::looks_nextbackdrop], - BlockOpcode::looks_changeeffectby => vec![IrOpcode::looks_changeeffectby], - BlockOpcode::looks_seteffectto => vec![IrOpcode::looks_seteffectto], - BlockOpcode::looks_cleargraphiceffects => { - vec![IrOpcode::looks_cleargraphiceffects] - } - BlockOpcode::looks_changesizeby => vec![IrOpcode::looks_changesizeby], - BlockOpcode::looks_setsizeto => vec![IrOpcode::looks_setsizeto], - BlockOpcode::motion_turnleft => vec![IrOpcode::motion_turnleft], - BlockOpcode::looks_size => vec![IrOpcode::looks_size], - BlockOpcode::operator_add => vec![IrOpcode::operator_add], - BlockOpcode::operator_subtract => vec![IrOpcode::operator_subtract], - BlockOpcode::operator_multiply => vec![IrOpcode::operator_multiply], - BlockOpcode::operator_divide => vec![IrOpcode::operator_divide], - BlockOpcode::operator_mod => vec![IrOpcode::operator_mod], - BlockOpcode::operator_round => vec![IrOpcode::operator_round], - BlockOpcode::operator_lt => vec![IrOpcode::operator_lt], - BlockOpcode::operator_equals => vec![IrOpcode::operator_equals], - BlockOpcode::operator_gt => vec![IrOpcode::operator_gt], - BlockOpcode::operator_and => vec![IrOpcode::operator_and], - BlockOpcode::operator_or => vec![IrOpcode::operator_or], - BlockOpcode::operator_not => vec![IrOpcode::operator_not], - BlockOpcode::operator_random => vec![IrOpcode::operator_random], - BlockOpcode::operator_join => vec![IrOpcode::operator_join], - BlockOpcode::operator_letter_of => vec![IrOpcode::operator_letter_of], - BlockOpcode::operator_length => vec![IrOpcode::operator_length], - BlockOpcode::operator_contains => vec![IrOpcode::operator_contains], - BlockOpcode::pen_clear => vec![IrOpcode::pen_clear], - BlockOpcode::pen_stamp => vec![IrOpcode::pen_stamp], - BlockOpcode::pen_penDown => vec![IrOpcode::pen_penDown], - BlockOpcode::pen_penUp => vec![IrOpcode::pen_penUp], - BlockOpcode::pen_setPenColorToColor => { - vec![IrOpcode::pen_setPenColorToColor] - } - BlockOpcode::pen_changePenColorParamBy => { - vec![IrOpcode::pen_changePenColorParamBy] - } - BlockOpcode::pen_setPenColorParamTo => { - vec![IrOpcode::pen_setPenColorParamTo] - } - BlockOpcode::pen_changePenSizeBy => vec![IrOpcode::pen_changePenSizeBy], - BlockOpcode::pen_setPenSizeTo => vec![IrOpcode::pen_setPenSizeTo], - BlockOpcode::pen_setPenShadeToNumber => { - vec![IrOpcode::pen_setPenShadeToNumber] - } - BlockOpcode::pen_changePenShadeBy => vec![IrOpcode::pen_changePenShadeBy], - BlockOpcode::pen_setPenHueToNumber => vec![IrOpcode::pen_setPenHueToNumber], - BlockOpcode::pen_changePenHueBy => vec![IrOpcode::pen_changePenHueBy], - BlockOpcode::pen_menu_colorParam => { - let maybe_val = - match block_info - .fields - .get("colorParam") - .ok_or(make_hq_bad_proj!( - "invalid project.json - missing field colorParam" - ))? { - Field::Value((v,)) | Field::ValueId(v, _) => v, - }; - let val_varval = maybe_val.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null value for OPERATOR field" - ))?; - let VarVal::String(val) = val_varval else { - hq_bad_proj!( - "invalid project.json - expected colorParam field to be string" - ); - }; - vec![IrOpcode::text { TEXT: val }] - } - BlockOpcode::operator_mathop => { - let maybe_val = match block_info.fields.get("OPERATOR").ok_or( - make_hq_bad_proj!("invalid project.json - missing field OPERATOR"), - )? { - Field::Value((v,)) | Field::ValueId(v, _) => v, - }; - let val_varval = maybe_val.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null value for OPERATOR field" - ))?; - let VarVal::String(val) = val_varval else { - hq_bad_proj!( - "invalid project.json - expected OPERATOR field to be string" - ); - }; - vec![IrOpcode::operator_mathop { OPERATOR: val }] - } - BlockOpcode::data_variable => { - let Field::ValueId(_val, maybe_id) = - block_info.fields.get("VARIABLE").ok_or(make_hq_bad_proj!( - "invalid project.json - missing field VARIABLE" - ))? - else { - hq_bad_proj!( - "invalid project.json - missing variable id for VARIABLE field" - ); - }; - let id = maybe_id.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null variable id for VARIABLE field" - ))?; - vec![IrOpcode::data_variable { - VARIABLE: id, - assume_type: None, - }] - } - BlockOpcode::data_setvariableto => { - let Field::ValueId(_val, maybe_id) = - block_info.fields.get("VARIABLE").ok_or(make_hq_bad_proj!( - "invalid project.json - missing field VARIABLE" - ))? - else { - hq_bad_proj!( - "invalid project.json - missing variable id for VARIABLE field" - ); - }; - let id = maybe_id.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null variable id for VARIABLE field" - ))?; - vec![IrOpcode::data_setvariableto { - VARIABLE: id, - assume_type: None, - }] - } - BlockOpcode::data_changevariableby => { - let Field::ValueId(_val, maybe_id) = - block_info.fields.get("VARIABLE").ok_or(make_hq_bad_proj!( - "invalid project.json - missing field VARIABLE" - ))? - else { - hq_bad_proj!( - "invalid project.json - missing variable id for VARIABLE field" - ); - }; - let id = maybe_id.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null id for VARIABLE field" - ))?; - vec![ - IrOpcode::data_variable { - VARIABLE: id.to_string(), - assume_type: None, - }, - IrOpcode::operator_add, - IrOpcode::data_setvariableto { - VARIABLE: id, - assume_type: None, - }, - ] - } - BlockOpcode::control_if => { - let substack_id = if let BlockArrayOrId::Id(id) = block_info - .inputs - .get("SUBSTACK") - .ok_or(make_hq_bad_proj!("missing SUBSTACK input for control_if"))? - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - { - id - } else { - hq_bad_proj!("malformed SUBSTACK input") - }; - let mut new_nexts = last_nexts.clone(); - if let Some(ref next) = block_info.next { - new_nexts.push(next.clone()); - } - step_from_top_block( - substack_id.clone(), - new_nexts, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - step_from_top_block( - block_info.next.clone().ok_or(make_hq_bug!(""))?, - last_nexts, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - vec![ - IrOpcode::hq_goto_if { - step: Some((target_id.clone(), substack_id)), - does_yield: false, - }, - IrOpcode::hq_goto { - step: if block_info.next.is_some() { - Some(( - target_id, - block_info.next.clone().ok_or(make_hq_bug!(""))?, - )) - } else { - None - }, - does_yield: false, - }, - ] - } - BlockOpcode::control_if_else => { - let substack_id = if let BlockArrayOrId::Id(id) = block_info - .inputs - .get("SUBSTACK") - .ok_or(make_hq_bad_proj!("missing SUBSTACK input for control_if"))? - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - { - id - } else { - hq_bad_proj!("malformed SUBSTACK input") - }; - let substack2_id = if let BlockArrayOrId::Id(id) = block_info - .inputs - .get("SUBSTACK2") - .ok_or(make_hq_bad_proj!("missing SUBSTACK input for control_if"))? - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - { - id - } else { - hq_bad_proj!("malformed SUBSTACK2 input") - }; - let mut new_nexts = last_nexts; - if let Some(ref next) = block_info.next { - new_nexts.push(next.clone()); - } - step_from_top_block( - substack_id.clone(), - new_nexts.clone(), - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - step_from_top_block( - substack2_id.clone(), - new_nexts.clone(), - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - vec![ - IrOpcode::hq_goto_if { - step: Some((target_id.clone(), substack_id)), - does_yield: false, - }, - IrOpcode::hq_goto { - step: Some((target_id, substack2_id)), - does_yield: false, - }, - ] - } - BlockOpcode::control_repeat => { - let substack_id = if let BlockArrayOrId::Id(id) = block_info - .inputs - .get("SUBSTACK") - .ok_or(make_hq_bad_proj!("missing SUBSTACK input"))? - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - { - id - } else { - hq_bad_proj!("malformed SUBSTACK input") - }; - let next_step = match block_info.next.as_ref() { - Some(next) => Some((target_id.clone(), next.clone())), - None => last_nexts - .first() - .map(|next| (target_id.clone(), next.clone())), - }; - let condition_opcodes = vec![ - IrOpcode::hq_goto_if { - step: next_step.clone(), - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - IrOpcode::hq_goto { - step: Some((target_id.clone(), substack_id.clone())), - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - ]; - let looper_id = Uuid::new_v4().to_string(); - context.vars.borrow_mut().push(IrVar::new( - looper_id.clone(), - looper_id.clone(), - VarVal::Float(0.0), - false, - )); - if !steps.contains_key(&(target_id.clone(), looper_id.clone())) { - let type_stack = Rc::new(RefCell::new(None)); - let mut looper_opcodes = vec![IrBlock::new_with_stack_no_cast( - IrOpcode::data_variable { - VARIABLE: looper_id.clone(), - assume_type: Some(InputType::ConcreteInteger), - }, - type_stack, - )?]; - for op in [ - //IrOpcode::hq_cast(InputType::Unknown, InputType::Float), // todo: integer - IrOpcode::math_whole_number { NUM: 1 }, - IrOpcode::operator_subtract, - //IrOpcode::hq_cast(Float, Unknown), - IrOpcode::data_teevariable { - VARIABLE: looper_id.clone(), - assume_type: Some(InputType::ConcreteInteger), - }, - //IrOpcode::hq_cast(Unknown, Float), - IrOpcode::math_whole_number { NUM: 1 }, - IrOpcode::operator_lt, - ] - .into_iter() - { - looper_opcodes.push(IrBlock::new_with_stack_no_cast( - op, - Rc::clone( - &looper_opcodes.last().ok_or(make_hq_bug!(""))?.type_stack, - ), - )?); - } - for op in condition_opcodes.iter() { - looper_opcodes.push(IrBlock::new_with_stack_no_cast( - op.clone(), - Rc::clone( - &looper_opcodes.last().ok_or(make_hq_bug!(""))?.type_stack, - ), - )?); - } - //looper_opcodes.fixup_types()?; - steps.insert( - (target_id.clone(), looper_id.clone()), - Step::new(looper_opcodes, Rc::clone(&context)), - ); - } - if block_info.next.is_some() { - step_from_top_block( - block_info.next.clone().ok_or(make_hq_bug!(""))?, - last_nexts, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - } - step_from_top_block( - substack_id.clone(), - vec![looper_id.clone()], - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - let mut opcodes = vec![]; - opcodes.add_inputs( - &block_info.inputs, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - for op in [ - IrOpcode::math_whole_number { NUM: 1 }, - IrOpcode::operator_lt, - ] - .into_iter() - .chain(condition_opcodes.into_iter()) - { - opcodes.push(IrBlock::new_with_stack_no_cast( - op, - Rc::clone(&opcodes.last().ok_or(make_hq_bug!(""))?.type_stack), - )?); - } - steps.insert( - (target_id.clone(), block_id.clone()), - Step::new(opcodes.clone(), Rc::clone(&context)), - ); - vec![ - IrOpcode::operator_round, // this is correct, scratch rounds rather than floor - //IrOpcode::hq_cast(ConcreteInteger, Unknown), - IrOpcode::data_teevariable { - VARIABLE: looper_id, - assume_type: Some(InputType::ConcreteInteger), - }, - //IrOpcode::hq_cast(Unknown, Float), - IrOpcode::math_whole_number { NUM: 1 }, - IrOpcode::operator_lt, - IrOpcode::hq_goto_if { - step: next_step, - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - IrOpcode::hq_goto { - step: Some((target_id, substack_id)), - does_yield: false, - }, - ] - } - BlockOpcode::control_repeat_until => { - let substack_id = if let BlockArrayOrId::Id(id) = block_info - .inputs - .get("SUBSTACK") - .ok_or(make_hq_bad_proj!("missing SUBSTACK input"))? - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - { - id - } else { - hq_bad_proj!("malformed SUBSTACK input") - }; - let next_step = match block_info.next.as_ref() { - Some(next) => Some((target_id.clone(), next.clone())), - None => last_nexts - .first() - .map(|next| (target_id.clone(), next.clone())), - }; - let condition_opcodes = vec![ - IrOpcode::hq_goto_if { - step: next_step.clone(), - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - IrOpcode::hq_goto { - step: Some((target_id.clone(), substack_id.clone())), - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - ]; - let looper_id = Uuid::new_v4().to_string(); - if !steps.contains_key(&(target_id.clone(), looper_id.clone())) { - let mut looper_opcodes = vec![]; - looper_opcodes.add_inputs( - &block_info.inputs, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - for op in condition_opcodes.clone().into_iter() { - looper_opcodes.push(IrBlock::new_with_stack_no_cast( - op, - Rc::clone( - &looper_opcodes.last().ok_or(make_hq_bug!(""))?.type_stack, - ), - )?); - } - steps.insert( - (target_id.clone(), looper_id.clone()), - Step::new(looper_opcodes, Rc::clone(&context)), - ); - } - if block_info.next.is_some() { - step_from_top_block( - block_info.next.clone().ok_or(make_hq_bug!(""))?, - last_nexts, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - } - step_from_top_block( - substack_id.clone(), - vec![looper_id], - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - let mut opcodes = vec![]; - opcodes.add_inputs( - &block_info.inputs, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - for op in condition_opcodes.into_iter() { - opcodes.push(IrBlock::new_with_stack_no_cast( - op, - Rc::clone(&opcodes.last().ok_or(make_hq_bug!(""))?.type_stack), - )?); - } - steps.insert( - (target_id.clone(), block_id.clone()), - Step::new(opcodes.clone(), Rc::clone(&context)), - ); - vec![ - IrOpcode::hq_goto_if { - step: next_step, - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - IrOpcode::hq_goto { - step: Some((target_id, substack_id)), - does_yield: false, - }, - ] - } - BlockOpcode::control_forever => { - let substack_id = if let BlockArrayOrId::Id(id) = block_info - .inputs - .get("SUBSTACK") - .ok_or(make_hq_bad_proj!("missing SUBSTACK input for control_if"))? - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - { - id - } else { - hq_bad_proj!("malformed SUBSTACK input") - }; - let goto_opcode = IrOpcode::hq_goto { - step: Some((target_id.clone(), substack_id.clone())), - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }; - let looper_id = Uuid::new_v4().to_string(); - if !steps.contains_key(&(target_id.clone(), looper_id.clone())) { - let looper_opcodes = vec![IrBlock::new_with_stack_no_cast( - goto_opcode.clone(), - Rc::new(RefCell::new(None)), - )?]; - steps.insert( - (target_id.clone(), looper_id.clone()), - Step::new(looper_opcodes, Rc::clone(&context)), - ); - } - if let Some(next) = block_info.next.clone() { - step_from_top_block( - next, - last_nexts, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - } - step_from_top_block( - substack_id.clone(), - vec![looper_id], - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - let opcodes = vec![IrBlock::new_with_stack_no_cast( - goto_opcode, - Rc::new(RefCell::new(None)), - )?]; - steps.insert( - (target_id.clone(), block_id.clone()), - Step::new(opcodes.clone(), Rc::clone(&context)), - ); - vec![IrOpcode::hq_goto { - step: Some((target_id, substack_id)), - does_yield: false, - }] - } - BlockOpcode::procedures_call => { - let serde_json::Value::String(proccode) = - block_info.mutation.mutations.get("proccode").ok_or( - make_hq_bad_proj!("missing proccode mutation in procedures_call"), - )? - else { - hq_bad_proj!("non-string proccode mutation") - }; - let warp = match block_info.mutation.mutations.get("warp").ok_or( - make_hq_bad_proj!("missing warp mutation in procedures_call"), - )? { - serde_json::Value::Bool(w) => *w, - serde_json::Value::String(wstr) => match wstr.as_str() { - "true" => true, - "false" => false, - _ => hq_bad_proj!("unexpected string for warp mutation"), - }, - _ => hq_bad_proj!("bad type for warp mutation"), - }; - if !warp { - hq_todo!("non-warp procedure"); - } - let procedure = add_procedure( - target_id.clone(), - proccode.clone(), - warp, - blocks, - steps, - procedures, - Rc::clone(&context), - )?; - vec![IrOpcode::hq_launch_procedure(procedure.clone())] - } - ref other => hq_todo!("unknown block {:?}", other), - }; - - for op in ops.into_iter() { - let type_stack = self.get_type_stack(None); - let mut casts: BTreeMap = BTreeMap::new(); - let mut add_cast = |i, ty: &InputType| { - casts.insert(i, ty.clone()); - }; - let block = - IrBlock::new_with_stack(op.clone(), Rc::clone(&type_stack), &mut add_cast)?; - for (j, cast_type) in casts.iter() { - let cast_pos = self - .iter() - .rposition(|b| b.type_stack.len() == type_stack.len() - j) - .ok_or(make_hq_bug!(""))?; - let cast_stack = self.get_type_stack(Some(cast_pos)); - #[allow(clippy::let_and_return)] - let cast_block = IrBlock::new_with_stack_no_cast( - IrOpcode::hq_cast( - { - let from = cast_stack - .borrow() - .clone() - .ok_or(make_hq_bug!( - "tried to cast to {:?} from empty type stack at {:?}", - cast_type.clone(), - op - ))? - .1; - from - }, - cast_type.clone(), - ), - cast_stack, - )?; - self.insert(cast_pos + 1, cast_block); - } - self.push(block); - /*if let Some(ty) = casts.get(&(type_stack.len() - 1 - i) { - let this_prev_block = self.last(); - let this_type_stack = if let Some(block) = this_prev_block { - Rc::clone(&block.type_stack) - } else { - hq_bug!("tried to cast from an empty type stack") - //Rc::new(RefCell::new(None)) - }; - self.push(IrBlock::new_with_stack_no_cast( - IrOpcode::hq_cast({ let from = this_type_stack.borrow().clone().ok_or(make_hq_bug!("tried to cast to {:?} from empty type stack at {:?}", ty, op))?.1; from }, ty.clone()), - this_type_stack, - )?); - }*/ - } - } - Block::Special(a) => self.add_block_arr(a)?, - }; - Ok(()) - } -} - -pub fn step_from_top_block<'a>( - top_id: String, - mut last_nexts: Vec, - blocks: &BTreeMap, - context: Rc, - steps: &'a mut StepMap, - target_id: String, - procedures: &mut ProcMap, -) -> Result<&'a Step, HQError> { - if steps.contains_key(&(target_id.clone(), top_id.clone())) { - return steps.get(&(target_id, top_id)).ok_or(make_hq_bug!("")); - } - let mut ops: Vec = vec![]; - let mut next_block = blocks.get(&top_id).ok_or(make_hq_bug!(""))?; - let mut next_id = Some(top_id.clone()); - loop { - ops.add_block( - next_id.clone().ok_or(make_hq_bug!(""))?, - blocks, - Rc::clone(&context), - last_nexts.clone(), - steps, - target_id.clone(), - procedures, - )?; - if next_block - .block_info() - .ok_or(make_hq_bug!(""))? - .next - .is_none() - { - next_id = last_nexts.pop(); - } else { - next_id.clone_from(&next_block.block_info().ok_or(make_hq_bug!(""))?.next); - } - if ops.is_empty() { - hq_bug!("assertion failed: !ops.is_empty()") - }; - if matches!( - ops.last().ok_or(make_hq_bug!(""))?.opcode(), - IrOpcode::hq_goto { .. } - ) { - next_id = None; - } - if next_id.is_none() { - break; - } else if let Some(block) = blocks.get(&next_id.clone().ok_or(make_hq_bug!(""))?) { - next_block = block; - } else if steps.contains_key(&(target_id.clone(), next_id.clone().ok_or(make_hq_bug!(""))?)) - { - ops.push(IrBlock::new_with_stack_no_cast( - IrOpcode::hq_goto { - step: Some((target_id.clone(), next_id.clone().ok_or(make_hq_bug!(""))?)), - does_yield: false, - }, - Rc::clone(&ops.last().unwrap().type_stack), - )?); - next_id = None; - break; - } else { - hq_bad_proj!("invalid next_id"); - } - let Some(last_block) = ops.last() else { - unreachable!() - }; - if last_block.does_request_redraw() - && !context.proc.clone().is_some_and(|p| p.warp) - && !(*last_block.opcode() == IrOpcode::looks_say && context.dbg) - { - break; - } - } - //ops.fixup_types()?; - let mut step = Step::new(ops.clone(), Rc::clone(&context)); - let goto = if let Some(ref id) = next_id { - step_from_top_block( - id.clone(), - last_nexts, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - IrBlock::new_with_stack_no_cast( - IrOpcode::hq_goto { - step: Some((target_id.clone(), id.clone())), - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - Rc::clone(&step.opcodes().last().unwrap().type_stack), - )? - } else { - IrBlock::new_with_stack_no_cast( - IrOpcode::hq_goto { - step: None, - does_yield: false, - }, - Rc::clone(&step.opcodes().last().unwrap().type_stack), - )? - }; - step.opcodes_mut().push(goto); - steps.insert((target_id.clone(), top_id.clone()), step); - steps.get(&(target_id, top_id)).ok_or(make_hq_bug!("")) -} - -impl Thread { - pub fn new(start: ThreadStart, first_step: String, target_id: String) -> Thread { - Thread { - start, - first_step, - target_id, - } - } - pub fn start(&self) -> &ThreadStart { - &self.start - } - pub fn first_step(&self) -> &String { - &self.first_step - } - pub fn target_id(&self) -> &String { - &self.target_id - } - pub fn from_hat( - hat: Block, - blocks: BTreeMap, - context: Rc, - steps: &mut StepMap, - target_id: String, - procedures: &mut ProcMap, - ) -> Result { - let (first_step_id, _first_step) = if let Block::Normal { block_info, .. } = &hat { - if let Some(next_id) = &block_info.next { - ( - next_id.clone(), - step_from_top_block( - next_id.clone(), - vec![], - &blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?, - ) - } else { - unreachable!(); - } - } else { - unreachable!(); - }; - let start_type = if let Block::Normal { block_info, .. } = &hat { - match block_info.opcode { - BlockOpcode::event_whenflagclicked => ThreadStart::GreenFlag, - _ => hq_todo!(""), - } - } else { - unreachable!() - }; - Ok(Self::new(start_type, first_step_id, target_id)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn create_ir() -> Result<(), HQError> { - use crate::sb3::Sb3Project; - use std::fs; - let proj: Sb3Project = fs::read_to_string("./project.json") - .expect("couldn't read hq-test.project.json") - .try_into()?; - let ir: IrProject = proj.try_into()?; - println!("{}", ir); - Ok(()) - } - - #[test] - fn const_fold() -> Result<(), HQError> { - use crate::sb3::Sb3Project; - use std::fs; - let proj: Sb3Project = fs::read_to_string("./project.json") - .expect("couldn't read hq-test.project.json") - .try_into()?; - let mut ir: IrProject = proj.try_into()?; - ir.const_fold()?; - println!("{}", ir); - Ok(()) - } - #[test] - fn opt() -> Result<(), HQError> { - use crate::sb3::Sb3Project; - use std::fs; - let proj: Sb3Project = fs::read_to_string("./project.json") - .expect("couldn't read hq-test.project.json") - .try_into()?; - let mut ir: IrProject = proj.try_into()?; - ir.optimise()?; - println!("{}", ir); - Ok(()) - } - - #[test] - fn input_types() { - use InputType::*; - assert!(Number.includes(&Float)); - assert!(Number.includes(&Integer)); - assert!(Number.includes(&ConcreteInteger)); - assert!(Number.includes(&Boolean)); - assert!(Any.includes(&Float)); - assert!(Any.includes(&Integer)); - assert!(Any.includes(&ConcreteInteger)); - assert!(Any.includes(&Boolean)); - assert!(Any.includes(&Number)); - assert!(Any.includes(&String)); - assert!(!String.includes(&Float)); - } -} +mod blocks; +mod context; +mod event; +mod proc; +mod project; +mod step; +mod target; +mod thread; +mod types; +mod variable; + +use context::StepContext; +pub use event::Event; +pub use proc::{PartialStep, Proc, ProcContext}; +pub use project::IrProject; +pub use step::Step; +use target::Target; +use thread::Thread; +pub use types::Type; +pub use variable::{RcVar, Variable}; diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs new file mode 100644 index 00000000..aabaaff0 --- /dev/null +++ b/src/ir/blocks.rs @@ -0,0 +1,1013 @@ +use super::context::StepContext; +use super::{IrProject, RcVar, Step, Type as IrType}; +use crate::instructions::{ + fields::{ + ControlIfElseFields, ControlLoopFields, DataSetvariabletoFields, DataTeevariableFields, + DataVariableFields, HqCastFields, HqFloatFields, HqIntegerFields, HqTextFields, + HqYieldFields, LooksSayFields, LooksThinkFields, ProceduresArgumentFields, + ProceduresCallWarpFields, + }, + IrOpcode, YieldMode, +}; +use crate::ir::Variable; +use crate::prelude::*; +use crate::sb3; +use sb3::{Block, BlockArray, BlockArrayOrId, BlockInfo, BlockMap, BlockOpcode, Input}; + +fn insert_casts(mut blocks: Vec) -> HQResult> { + let mut type_stack: Vec<(IrType, usize)> = vec![]; // a vector of types, and where they came from + let mut casts: Vec<(usize, IrType)> = vec![]; // a vector of cast targets, and where they're needed + for (i, block) in blocks.iter().enumerate() { + let expected_inputs = block.acceptable_inputs(); + if type_stack.len() < expected_inputs.len() { + hq_bug!("didn't have enough inputs on the type stack") + } + let actual_inputs: Vec<_> = type_stack + .splice((type_stack.len() - expected_inputs.len()).., []) + .collect(); + for (&expected, actual) in core::iter::zip(expected_inputs.iter(), actual_inputs) { + if !expected + .base_types() + .any(|ty1| actual.0.base_types().any(|ty2| ty2 == ty1)) + { + casts.push((actual.1, expected)); + } + } + // TODO: make this more specific by using the actual input types post-cast + if let Some(output) = block.output_type(expected_inputs)? { + type_stack.push((output, i)); + } + } + for (pos, ty) in casts { + blocks.insert(pos + 1, IrOpcode::hq_cast(HqCastFields(ty))); + } + Ok(blocks) +} + +#[derive(Clone, Debug)] +pub enum NextBlock { + ID(Box), + Step(Weak), +} + +#[derive(Clone, Debug)] +pub struct NextBlockInfo { + pub yield_first: bool, + pub block: NextBlock, +} + +/// contains a vector of next blocks, as well as information on how to proceed when +/// there are no next blocks: true => terminate the thread, false => do nothing +/// (useful for e.g. loop bodies, or for non-stack blocks) +#[derive(Clone, Debug)] +pub struct NextBlocks(Vec, bool); + +impl NextBlocks { + pub const fn new(terminating: bool) -> Self { + Self(vec![], terminating) + } + + pub const fn terminating(&self) -> bool { + self.1 + } + + pub fn extend_with_inner(&self, new: NextBlockInfo) -> Self { + let mut cloned = self.0.clone(); + cloned.push(new); + Self(cloned, self.terminating()) + } + + pub fn pop_inner(self) -> (Option, Self) { + let terminating = self.terminating(); + let mut vec = self.0; + let popped = vec.pop(); + (popped, Self(vec, terminating)) + } +} + +pub fn from_block( + block: &Block, + blocks: &BlockMap, + context: &StepContext, + project: &Weak, + final_next_blocks: NextBlocks, +) -> HQResult> { + insert_casts(match block { + Block::Normal { block_info, .. } => { + from_normal_block(block_info, blocks, context, project, final_next_blocks)?.to_vec() + } + Block::Special(block_array) => vec![from_special_block(block_array, context)?], + }) +} + +pub fn input_names(block_info: &BlockInfo, context: &StepContext) -> HQResult> { + let opcode = &block_info.opcode; + // target and procs need to be declared outside of the match block + // to prevent lifetime issues + let target = context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; + let procs = target.procedures()?; + Ok( + #[expect( + clippy::wildcard_enum_match_arm, + reason = "too many opcodes to match individually" + )] + match opcode { + BlockOpcode::looks_say | BlockOpcode::looks_think => vec!["MESSAGE"], + BlockOpcode::operator_add + | BlockOpcode::operator_divide + | BlockOpcode::operator_subtract + | BlockOpcode::operator_multiply => vec!["NUM1", "NUM2"], + BlockOpcode::operator_lt + | BlockOpcode::operator_gt + | BlockOpcode::operator_equals + | BlockOpcode::operator_and + | BlockOpcode::operator_or => vec!["OPERAND1", "OPERAND2"], + BlockOpcode::operator_join | BlockOpcode::operator_contains => { + vec!["STRING1", "STRING2"] + } + BlockOpcode::operator_letter_of => vec!["LETTER", "STRING"], + BlockOpcode::sensing_dayssince2000 + | BlockOpcode::data_variable + | BlockOpcode::argument_reporter_boolean + | BlockOpcode::argument_reporter_string_number => vec![], + BlockOpcode::data_setvariableto | BlockOpcode::data_changevariableby => vec!["VALUE"], + BlockOpcode::control_if + | BlockOpcode::control_if_else + | BlockOpcode::control_repeat_until => vec!["CONDITION"], + BlockOpcode::operator_not => vec!["OPERAND"], + BlockOpcode::control_repeat => vec!["TIMES"], + BlockOpcode::operator_length => vec!["STRING"], + BlockOpcode::procedures_call => { + let serde_json::Value::String(proccode) = block_info + .mutation + .mutations + .get("proccode") + .ok_or_else(|| make_hq_bad_proj!("missing proccode on procedures_call"))? + else { + hq_bad_proj!("non-string proccode on procedures_call"); + }; + let Some(proc) = procs.get(proccode.as_str()) else { + hq_bad_proj!("procedures_call proccode doesn't exist") + }; + proc.context().arg_ids().iter().map(|b| &**b).collect() + } + other => hq_todo!("unimplemented input_names for {:?}", other), + } + .into_iter() + .map(String::from) + .collect(), + ) +} + +pub fn inputs( + block_info: &BlockInfo, + blocks: &BlockMap, + context: &StepContext, + project: &Weak, +) -> HQResult> { + Ok(input_names(block_info, context)? + .into_iter() + .map(|name| -> HQResult> { + let input = match block_info.inputs.get((*name).into()) { + Some(input) => input, + None => { + if name.starts_with("CONDITION") { + &Input::NoShadow( + 0, + Some(BlockArrayOrId::Array(BlockArray::NumberOrAngle(6, 0.0))), + ) + } else { + hq_bad_proj!("missing input {}", name) + } + } + }; + #[expect( + clippy::wildcard_enum_match_arm, + reason = "all variants covered in previous match guards" + )] + match input { + Input::NoShadow(_, Some(block)) | Input::Shadow(_, Some(block), _) => match block { + BlockArrayOrId::Array(arr) => Ok(vec![from_special_block(arr, context)?]), + BlockArrayOrId::Id(id) => from_block( + blocks.get(id).ok_or_else(|| { + make_hq_bad_proj!("block for input {} doesn't exist", name) + })?, + blocks, + context, + project, + NextBlocks::new(false), + ), + }, + _ => hq_bad_proj!("missing input block for {}", name), + } + }) + .collect::>>()? + .iter() + .flatten() + .cloned() + .collect()) +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ProcArgType { + Boolean, + StringNumber, +} + +impl ProcArgType { + fn default_block(self) -> Vec { + vec![match self { + Self::Boolean => IrOpcode::hq_integer(HqIntegerFields(0)), + Self::StringNumber => IrOpcode::hq_text(HqTextFields("".into())), + }] + } +} + +fn procedure_argument( + arg_type: ProcArgType, + block_info: &BlockInfo, + context: &StepContext, +) -> HQResult> { + let Some(proc_context) = context.proc_context.clone() else { + return Ok(arg_type.default_block()); + }; + let sb3::VarVal::String(arg_name) = block_info + .fields + .get("VALUE") + .ok_or_else(|| make_hq_bad_proj!("missing VALUE field for proc argument"))? + .get_0() + .ok_or_else(|| make_hq_bad_proj!("missing value of VALUE field"))? + else { + hq_bad_proj!("non-string proc argument name") + }; + let Some(index) = proc_context + .arg_names() + .iter() + .position(|name| name == arg_name) + else { + return Ok(arg_type.default_block()); + }; + let expected_type = proc_context + .arg_types() + .get(index) + .ok_or_else(|| make_hq_bad_proj!("argument index not in range of argumenttypes"))?; + hq_assert!( + (arg_type == ProcArgType::Boolean && *expected_type == IrType::Boolean) + || (arg_type == ProcArgType::StringNumber + && (*expected_type == IrType::String || *expected_type == IrType::Number)), + "argument block doesn't match actual argument type" + ); + Ok(vec![IrOpcode::procedures_argument( + ProceduresArgumentFields(index, *expected_type), + )]) +} + +#[expect(clippy::too_many_arguments, reason = "too many arguments!")] +// TODO: put these arguments into a struct? +fn generate_loop( + warp: bool, + should_break: &mut bool, + block_info: &BlockInfo, + blocks: &BTreeMap, Block>, + context: &StepContext, + final_next_blocks: NextBlocks, + first_condition_instructions: Option>, + condition_instructions: Vec, + flip_if: bool, + setup_instructions: Vec, +) -> HQResult> { + let BlockArrayOrId::Id(substack_id) = match block_info.inputs.get("SUBSTACK") { + Some(input) => input, + None => return Ok(vec![IrOpcode::hq_drop]), // TODO: consider loops without input (i.e. forever) + } + .get_1() + .ok_or_else(|| make_hq_bug!(""))? + .clone() + .ok_or_else(|| make_hq_bug!(""))? + else { + hq_bad_proj!("malformed SUBSTACK input") + }; + let Some(substack_block) = blocks.get(&substack_id) else { + hq_bad_proj!("SUBSTACK block doesn't seem to exist") + }; + if warp { + // TODO: can this be expressed in the same way as non-warping loops, + // just with yield_first: false? + let substack_blocks = from_block( + substack_block, + blocks, + context, + &context.project()?, + NextBlocks::new(false), + )?; + let substack_step = + Step::new_rc(None, context.clone(), substack_blocks, &context.project()?)?; + let condition_step = Step::new_rc( + None, + context.clone(), + condition_instructions, + &context.project()?, + )?; + let first_condition_step = if let Some(instrs) = first_condition_instructions { + Some(Step::new_rc( + None, + context.clone(), + instrs, + &context.project()?, + )?) + } else { + None + }; + Ok(setup_instructions + .into_iter() + .chain(vec![IrOpcode::control_loop(ControlLoopFields { + first_condition: first_condition_step, + condition: condition_step, + body: substack_step, + flip_if, + })]) + .collect()) + } else { + *should_break = true; + let (next_block, outer_next_blocks) = if let Some(ref next_block) = block_info.next { + (Some(NextBlock::ID(next_block.clone())), final_next_blocks) + } else if let (Some(next_block_info), popped_next_blocks) = + final_next_blocks.clone().pop_inner() + { + (Some(next_block_info.block), popped_next_blocks) + } else { + (None, final_next_blocks) + }; + let next_step = match next_block { + Some(NextBlock::ID(id)) => { + let Some(next_block_block) = blocks.get(&id) else { + hq_bad_proj!("next block doesn't exist") + }; + Step::from_block( + next_block_block, + id, + blocks, + context, + &context.project()?, + outer_next_blocks, + )? + } + Some(NextBlock::Step(ref step)) => step + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?, + None => Step::new_terminating(context.clone(), &context.project()?)?, + }; + let condition_step = Step::new_rc( + None, + context.clone(), + condition_instructions.clone(), + &context.project()?, + )?; + let substack_blocks = from_block( + substack_block, + blocks, + context, + &context.project()?, + NextBlocks::new(false).extend_with_inner(NextBlockInfo { + yield_first: true, + block: NextBlock::Step(Rc::downgrade(&condition_step)), + }), + )?; + let substack_step = + Step::new_rc(None, context.clone(), substack_blocks, &context.project()?)?; + condition_step + .opcodes_mut()? + .push(IrOpcode::control_if_else(ControlIfElseFields { + branch_if: Rc::clone(if flip_if { &next_step } else { &substack_step }), + branch_else: Rc::clone(if flip_if { &substack_step } else { &next_step }), + })); + Ok(setup_instructions + .into_iter() + .chain(first_condition_instructions.map_or(condition_instructions, |instrs| instrs)) + .chain(vec![IrOpcode::control_if_else(ControlIfElseFields { + branch_if: if flip_if { + Rc::clone(&next_step) + } else { + Rc::clone(&substack_step) + }, + branch_else: if flip_if { substack_step } else { next_step }, + })]) + .collect()) + } +} + +#[expect( + clippy::too_many_lines, + reason = "a big monolithic function is somewhat unavoidable here" +)] +fn from_normal_block( + block_info: &BlockInfo, + blocks: &BlockMap, + context: &StepContext, + project: &Weak, + final_next_blocks: NextBlocks, +) -> HQResult> { + let mut curr_block = Some(block_info); + let mut final_next_blocks = final_next_blocks; + let mut opcodes = vec![]; + let mut should_break = false; + while let Some(block_info) = curr_block { + opcodes.append( + &mut inputs(block_info, blocks, context, project)? + .into_iter() + .chain( + #[expect( + clippy::wildcard_enum_match_arm, + reason = "too many opcodes to match individually" + )] + match &block_info.opcode { + BlockOpcode::operator_add => vec![IrOpcode::operator_add], + BlockOpcode::operator_subtract => vec![IrOpcode::operator_subtract], + BlockOpcode::operator_multiply => vec![IrOpcode::operator_multiply], + BlockOpcode::operator_divide => vec![IrOpcode::operator_divide], + BlockOpcode::looks_say => vec![IrOpcode::looks_say(LooksSayFields { + debug: context.debug, + target_idx: context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .index(), + })], + BlockOpcode::looks_think => vec![IrOpcode::looks_think(LooksThinkFields { + debug: context.debug, + target_idx: context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .index(), + })], + BlockOpcode::operator_join => vec![IrOpcode::operator_join], + BlockOpcode::operator_length => vec![IrOpcode::operator_length], + BlockOpcode::operator_contains => vec![IrOpcode::operator_contains], + BlockOpcode::operator_letter_of => vec![IrOpcode::operator_letter_of], + BlockOpcode::sensing_dayssince2000 => vec![IrOpcode::sensing_dayssince2000], + BlockOpcode::operator_lt => vec![IrOpcode::operator_lt], + BlockOpcode::operator_gt => vec![IrOpcode::operator_gt], + BlockOpcode::operator_equals => vec![IrOpcode::operator_equals], + BlockOpcode::operator_not => vec![IrOpcode::operator_not], + BlockOpcode::data_setvariableto => { + let sb3::Field::ValueId(_val, maybe_id) = + block_info.fields.get("VARIABLE").ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - missing field VARIABLE" + ) + })? + else { + hq_bad_proj!( + "invalid project.json - missing variable id for VARIABLE field" + ); + }; + let id = maybe_id.clone().ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - null variable id for VARIABLE field" + ) + })?; + let target = context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; + let variable = if let Some(var) = target.variables().get(&id) { + var + } else if let Some(var) = context + .project()? + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .global_variables() + .get(&id) + { + &Rc::clone(var) + } else { + hq_bad_proj!("variable not found") + }; + vec![IrOpcode::data_setvariableto(DataSetvariabletoFields( + RcVar(Rc::clone(variable)), + ))] + } + BlockOpcode::data_changevariableby => { + let sb3::Field::ValueId(_val, maybe_id) = + block_info.fields.get("VARIABLE").ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - missing field VARIABLE" + ) + })? + else { + hq_bad_proj!( + "invalid project.json - missing variable id for VARIABLE field" + ); + }; + let id = maybe_id.clone().ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - null variable id for VARIABLE field" + ) + })?; + let target = context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; + let variable = if let Some(var) = target.variables().get(&id) { + var + } else if let Some(var) = context + .project()? + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .global_variables() + .get(&id) + { + &Rc::clone(var) + } else { + hq_bad_proj!("variable not found") + }; + vec![ + IrOpcode::data_variable(DataVariableFields(RcVar(Rc::clone( + variable, + )))), + IrOpcode::operator_add, + IrOpcode::data_setvariableto(DataSetvariabletoFields(RcVar( + Rc::clone(variable), + ))), + ] + } + BlockOpcode::data_variable => { + let sb3::Field::ValueId(_val, maybe_id) = + block_info.fields.get("VARIABLE").ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - missing field VARIABLE" + ) + })? + else { + hq_bad_proj!( + "invalid project.json - missing variable id for VARIABLE field" + ); + }; + let id = maybe_id.clone().ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - null variable id for VARIABLE field" + ) + })?; + let target = context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; + let variable = if let Some(var) = target.variables().get(&id) { + var + } else if let Some(var) = context + .project()? + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .global_variables() + .get(&id) + { + &Rc::clone(var) + } else { + hq_bad_proj!("variable not found") + }; + vec![IrOpcode::data_variable(DataVariableFields(RcVar( + Rc::clone(variable), + )))] + } + BlockOpcode::control_if => 'block: { + let BlockArrayOrId::Id(substack_id) = + match block_info.inputs.get("SUBSTACK") { + Some(input) => input, + None => break 'block vec![IrOpcode::hq_drop], + } + .get_1() + .ok_or_else(|| make_hq_bug!(""))? + .clone() + .ok_or_else(|| make_hq_bug!(""))? + else { + hq_bad_proj!("malformed SUBSTACK input") + }; + let Some(substack_block) = blocks.get(&substack_id) else { + hq_bad_proj!("SUBSTACK block doesn't seem to exist") + }; + let (next_block, if_next_blocks, else_next_blocks) = + #[expect( + clippy::option_if_let_else, + reason = "map_or_else alternative is too complex" + )] + if let Some(ref next_block) = block_info.next { + ( + Some(NextBlock::ID(next_block.clone())), + final_next_blocks.extend_with_inner(NextBlockInfo { + yield_first: false, + block: NextBlock::ID(next_block.clone()), + }), + final_next_blocks.clone(), + ) + } else if let (Some(next_block_info), popped_next_blocks) = + final_next_blocks.clone().pop_inner() + { + ( + Some(next_block_info.block), + final_next_blocks.clone(), + popped_next_blocks, + ) + } else { + ( + None, + final_next_blocks.clone(), // preserve termination behaviour + final_next_blocks.clone(), + ) + }; + let if_step = Step::from_block( + substack_block, + substack_id, + blocks, + context, + &context.project()?, + if_next_blocks, + )?; + let else_step = match next_block { + Some(NextBlock::ID(id)) => { + let Some(next_block_block) = blocks.get(&id.clone()) else { + hq_bad_proj!("next block doesn't exist") + }; + Step::from_block( + next_block_block, + id.clone(), + blocks, + context, + &context.project()?, + else_next_blocks, + )? + } + Some(NextBlock::Step(step)) => step + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?, + None => { + Step::new_terminating(context.clone(), &context.project()?)? + } + }; + should_break = true; + vec![IrOpcode::control_if_else(ControlIfElseFields { + branch_if: if_step, + branch_else: else_step, + })] + } + BlockOpcode::control_if_else => 'block: { + let BlockArrayOrId::Id(substack1_id) = + match block_info.inputs.get("SUBSTACK") { + Some(input) => input, + None => break 'block vec![IrOpcode::hq_drop], + } + .get_1() + .ok_or_else(|| make_hq_bug!(""))? + .clone() + .ok_or_else(|| make_hq_bug!(""))? + else { + hq_bad_proj!("malformed SUBSTACK input") + }; + let Some(substack1_block) = blocks.get(&substack1_id) else { + hq_bad_proj!("SUBSTACK block doesn't seem to exist") + }; + let BlockArrayOrId::Id(substack2_id) = + match block_info.inputs.get("SUBSTACK2") { + Some(input) => input, + None => break 'block vec![IrOpcode::hq_drop], + } + .get_1() + .ok_or_else(|| make_hq_bug!(""))? + .clone() + .ok_or_else(|| make_hq_bug!(""))? + else { + hq_bad_proj!("malformed SUBSTACK2 input") + }; + let Some(substack2_block) = blocks.get(&substack2_id) else { + hq_bad_proj!("SUBSTACK2 block doesn't seem to exist") + }; + let next_blocks = block_info.next.as_ref().map_or_else( + || final_next_blocks.clone(), // preserve termination behaviour + |next_block| { + final_next_blocks.extend_with_inner(NextBlockInfo { + yield_first: false, + block: NextBlock::ID(next_block.clone()), + }) + }, + ); + let if_step = Step::from_block( + substack1_block, + substack1_id, + blocks, + context, + &context.project()?, + next_blocks.clone(), + )?; + let else_step = Step::from_block( + substack2_block, + substack2_id, + blocks, + context, + &context.project()?, + next_blocks, + )?; + should_break = true; + vec![IrOpcode::control_if_else(ControlIfElseFields { + branch_if: if_step, + branch_else: else_step, + })] + } + BlockOpcode::control_repeat => { + let variable = RcVar(Rc::new(Variable::new( + IrType::Int, + sb3::VarVal::Float(0.0), + context.warp, + ))); + let condition_instructions = vec![ + IrOpcode::data_variable(DataVariableFields(variable.clone())), + IrOpcode::hq_integer(HqIntegerFields(1)), + IrOpcode::operator_subtract, + IrOpcode::data_teevariable(DataTeevariableFields(variable.clone())), + IrOpcode::hq_integer(HqIntegerFields(0)), + IrOpcode::operator_gt, + ]; + let first_condition_instructions = Some(vec![ + IrOpcode::data_variable(DataVariableFields(variable.clone())), + IrOpcode::hq_integer(HqIntegerFields(0)), + IrOpcode::operator_gt, + ]); + let setup_instructions = vec![ + IrOpcode::hq_cast(HqCastFields(IrType::Int)), + IrOpcode::data_setvariableto(DataSetvariabletoFields(variable)), + ]; + generate_loop( + context.warp, + &mut should_break, + block_info, + blocks, + context, + final_next_blocks.clone(), + first_condition_instructions, + condition_instructions, + false, + setup_instructions, + )? + } + BlockOpcode::control_repeat_until => { + let condition_instructions = + inputs(block_info, blocks, context, &context.project()?)?; + let first_condition_instructions = None; + let setup_instructions = vec![IrOpcode::hq_drop]; + generate_loop( + context.warp, + &mut should_break, + block_info, + blocks, + context, + final_next_blocks.clone(), + first_condition_instructions, + condition_instructions, + true, + setup_instructions, + )? + } + BlockOpcode::procedures_call => { + let target = context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; + let procs = target.procedures()?; + let serde_json::Value::String(proccode) = block_info + .mutation + .mutations + .get("proccode") + .ok_or_else(|| { + make_hq_bad_proj!("missing proccode on procedures_call") + })? + else { + hq_bad_proj!("non-string proccode on procedures_call") + }; + let proc = procs.get(proccode.as_str()).ok_or_else(|| { + make_hq_bad_proj!("non-existant proccode on procedures_call") + })?; + let warp = context.warp || proc.always_warped(); + if warp { + proc.compile_warped(blocks)?; + vec![IrOpcode::procedures_call_warp(ProceduresCallWarpFields { + proc: Rc::clone(proc), + })] + } else { + hq_todo!("non-warped procedures") + } + } + BlockOpcode::argument_reporter_boolean => { + procedure_argument(ProcArgType::Boolean, block_info, context)? + } + BlockOpcode::argument_reporter_string_number => { + procedure_argument(ProcArgType::StringNumber, block_info, context)? + } + other => hq_todo!("unimplemented block: {:?}", other), + }, + ) + .collect(), + ); + if should_break { + break; + } + curr_block = if let Some(ref next_id) = block_info.next { + let next_block = blocks + .get(next_id) + .ok_or_else(|| make_hq_bad_proj!("missing next block"))?; + if opcodes + .last() + .is_some_and(super::super::instructions::IrOpcode::requests_screen_refresh) + && !context.warp + { + opcodes.push(IrOpcode::hq_yield(HqYieldFields { + mode: YieldMode::Schedule(Rc::downgrade(&Step::from_block( + next_block, + next_id.clone(), + blocks, + context, + project, + final_next_blocks.clone(), + )?)), + })); + None + } else { + next_block.block_info() + } + } else if let (Some(popped_next), new_next_blocks_stack) = + final_next_blocks.clone().pop_inner() + { + match popped_next.block { + NextBlock::ID(id) => { + let next_block = blocks + .get(&id) + .ok_or_else(|| make_hq_bad_proj!("missing next block"))?; + if (popped_next.yield_first + || opcodes.last().is_some_and( + super::super::instructions::IrOpcode::requests_screen_refresh, + )) + && !context.warp + { + opcodes.push(IrOpcode::hq_yield(HqYieldFields { + mode: YieldMode::Schedule(Rc::downgrade(&Step::from_block( + next_block, + id.clone(), + blocks, + context, + project, + new_next_blocks_stack, + )?)), + })); + None + } else { + final_next_blocks = new_next_blocks_stack; + next_block.block_info() + } + } + NextBlock::Step(ref step) => { + if (popped_next.yield_first + || opcodes.last().is_some_and( + super::super::instructions::IrOpcode::requests_screen_refresh, + )) + && !context.warp + { + opcodes.push(IrOpcode::hq_yield(HqYieldFields { + mode: YieldMode::Schedule(Weak::clone(step)), + })); + } else { + opcodes.push(IrOpcode::hq_yield(HqYieldFields { + mode: YieldMode::Inline( + step.upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?, + ), + })); + } + None + } + } + } else if final_next_blocks.terminating() { + opcodes.push(IrOpcode::hq_yield(HqYieldFields { + mode: YieldMode::None, + })); + None + } else { + None + } + } + Ok(opcodes.into_iter().collect()) +} + +fn from_special_block(block_array: &BlockArray, context: &StepContext) -> HQResult { + Ok(match block_array { + BlockArray::NumberOrAngle(ty, value) => match ty { + // number, positive number or angle + 4 | 5 | 8 => { + // proactively convert to an integer if possible; + // if a float is needed, it will be cast at const-fold time (TODO), + // and if integers are disabled (TODO) a float will be emitted anyway + if value % 1.0 == 0.0 { + #[expect( + clippy::cast_possible_truncation, + reason = "integer-ness already confirmed; `as` is saturating." + )] + IrOpcode::hq_integer(HqIntegerFields(*value as i32)) + } else { + IrOpcode::hq_float(HqFloatFields(*value)) + } + } + // positive integer, integer + 6 | 7 => { + hq_assert!( + value % 1.0 == 0.0, + "inputs of integer or positive integer types should be integers" + ); + #[expect( + clippy::cast_possible_truncation, + reason = "integer-ness already confirmed; `as` is saturating." + )] + IrOpcode::hq_integer(HqIntegerFields(*value as i32)) + } + _ => hq_bad_proj!("bad project json (block array of type ({}, f64))", ty), + }, + // a string input should really be a colour or a string, but often numbers + // are serialised as strings in the project.json + BlockArray::ColorOrString(ty, value) => match ty { + // number, positive number or integer + 4 | 5 | 8 => { + let float = value + .parse() + .map_err(|_| make_hq_bug!("expected a float-parseable value"))?; + // proactively convert to an integer if possible; + // if a float is needed, it will be cast at const-fold time (TODO), + // and if integers are disabled (TODO) a float will be emitted anyway + if float % 1.0 == 0.0 { + #[expect( + clippy::cast_possible_truncation, + reason = "integer-ness already confirmed; `as` is saturating." + )] + IrOpcode::hq_integer(HqIntegerFields(float as i32)) + } else { + IrOpcode::hq_float(HqFloatFields(float)) + } + } + // integer, positive integer + 6 | 7 => IrOpcode::hq_integer(HqIntegerFields( + value + .parse() + .map_err(|_| make_hq_bug!("expected and int-parseable value"))?, + )), + // colour + 9 => hq_todo!("colour inputs"), + // string + 10 => 'textBlock: { + // proactively convert to a number + if let Ok(float) = value.parse::() { + if *float.to_string() == **value { + break 'textBlock if float % 1.0 == 0.0 { + #[expect( + clippy::cast_possible_truncation, + reason = "integer-ness already confirmed; `as` is saturating." + )] + IrOpcode::hq_integer(HqIntegerFields(float as i32)) + } else { + IrOpcode::hq_float(HqFloatFields(float)) + }; + } + } + IrOpcode::hq_text(HqTextFields(value.clone())) + } + _ => hq_bad_proj!("bad project json (block array of type ({}, string))", ty), + }, + BlockArray::Broadcast(ty, _name, id) | BlockArray::VariableOrList(ty, _name, id, _, _) => { + match ty { + 11 => hq_todo!("broadcast input"), + 12 => { + let target = context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; + let variable = if let Some(var) = target.variables().get(id) { + var + } else if let Some(var) = context + .project()? + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .global_variables() + .get(id) + { + &Rc::clone(var) + } else { + hq_bad_proj!("variable not found") + }; + IrOpcode::data_variable(DataVariableFields(RcVar(Rc::clone(variable)))) + } + 13 => hq_todo!("list input"), + _ => hq_bad_proj!( + "bad project json (block array of type ({}, string, string))", + ty + ), + } + } + }) +} diff --git a/src/ir/context.rs b/src/ir/context.rs new file mode 100644 index 00000000..acfbbf1c --- /dev/null +++ b/src/ir/context.rs @@ -0,0 +1,24 @@ +use super::{IrProject, ProcContext, Target}; +use crate::prelude::*; + +#[derive(Debug, Clone)] +pub struct StepContext { + pub target: Weak, + /// whether or not the current thread is warped. this may be because the current + /// procedure is warped, or because a procedure higher up the call stack was warped. + pub warp: bool, + pub proc_context: Option, + /// enables certain behaviours such as `console.log` say/think rather than + /// displaying in bubbles + pub debug: bool, +} + +impl StepContext { + pub fn project(&self) -> HQResult> { + Ok(self + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .project()) + } +} diff --git a/src/ir/event.rs b/src/ir/event.rs new file mode 100644 index 00000000..ba34b02a --- /dev/null +++ b/src/ir/event.rs @@ -0,0 +1,5 @@ +// Ord is required to be used in a BTreeMap; Ord requires PartialOrd, Eq and PartialEq +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Event { + FlagCLicked, +} diff --git a/src/ir/proc.rs b/src/ir/proc.rs new file mode 100644 index 00000000..31e6ff5a --- /dev/null +++ b/src/ir/proc.rs @@ -0,0 +1,288 @@ +use super::blocks::NextBlocks; +use super::context::StepContext; +use super::{Step, Target as IrTarget, Type as IrType}; +use crate::prelude::*; +use crate::sb3::{Block, BlockMap, BlockOpcode, Target as Sb3Target}; +use core::cell::Ref; +use lazy_regex::{lazy_regex, Lazy}; +use regex::Regex; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PartialStep { + None, + StartedCompilation, + Finished(Rc), +} + +impl PartialStep { + pub const fn is_finished(&self) -> bool { + matches!(self, Self::Finished(_)) + } +} + +#[derive(Clone, Debug)] +pub struct ProcContext { + arg_types: Box<[IrType]>, + arg_ids: Box<[Box]>, + arg_names: Box<[Box]>, + target: Weak, +} + +impl ProcContext { + pub fn arg_ids(&self) -> &[Box] { + &self.arg_ids + } + + pub fn arg_names(&self) -> &[Box] { + &self.arg_names + } + + pub fn arg_types(&self) -> &[IrType] { + &self.arg_types + } + + pub fn target(&self) -> Weak { + Weak::clone(&self.target) + } +} + +#[derive(Clone, Debug)] +pub struct Proc { + /// whether a procedure is 'run without screen refresh' - a procedure can be warped + /// even without this condition holding true, if a procedure higher up in the call + /// stack is warped. + always_warped: bool, + non_warped_first_step: RefCell, + warped_first_step: RefCell, + first_step_id: Option>, + proccode: Box, + context: ProcContext, +} + +impl Proc { + pub fn non_warped_first_step(&self) -> HQResult> { + Ok(self.non_warped_first_step.try_borrow()?) + } + + pub fn warped_first_step(&self) -> HQResult> { + Ok(self.warped_first_step.try_borrow()?) + } + + #[expect( + clippy::borrowed_box, + reason = "reference is inside borrow so difficult to unbox" + )] + pub const fn first_step_id(&self) -> Option<&Box> { + self.first_step_id.as_ref() + } + + pub const fn always_warped(&self) -> bool { + self.always_warped + } + + pub const fn context(&self) -> &ProcContext { + &self.context + } + + pub fn proccode(&self) -> &str { + &self.proccode + } +} + +static ARG_REGEX: Lazy = lazy_regex!(r#"[^\\]%[nbs]"#); + +fn arg_types_from_proccode(proccode: &str) -> Result, HQError> { + // https://github.com/scratchfoundation/scratch-blocks/blob/abbfe9/blocks_vertical/procedures.js#L207-L215 + (*ARG_REGEX) + .find_iter(proccode) + .map(|s| s.as_str().to_string().trim().to_string()) + .filter(|s| s.as_str().starts_with('%')) + .map(|s| s[..2].to_string()) + .map(|s| { + Ok(match s.as_str() { + "%n" => IrType::Number, + "%s" => IrType::String, + "%b" => IrType::Boolean, + other => hq_bug!("invalid proccode arg \"{other}\" found"), + }) + }) + .collect() +} + +impl Proc { + fn string_vec_mutation( + mutations: &BTreeMap, serde_json::Value>, + id: &str, + ) -> HQResult]>> { + Ok( + match mutations + .get(id) + .ok_or_else(|| make_hq_bad_proj!("missing {id} mutation"))? + { + serde_json::Value::Array(values) => values + .iter() + .map( + #[expect( + clippy::wildcard_enum_match_arm, + reason = "too many variants to match" + )] + |val| match val { + serde_json::Value::String(s) => Ok(s.clone().into_boxed_str()), + _ => hq_bad_proj!("non-string {id} member in"), + }, + ) + .collect::>>()?, + serde_json::Value::String(string_arr) => { + if string_arr == "[]" { + Box::new([]) + } else { + string_arr + .strip_prefix("[\"") + .ok_or_else(|| make_hq_bug!("malformed {id} array"))? + .strip_suffix("\"]") + .ok_or_else(|| make_hq_bug!("malformed {id} array"))? + .split("\",\"") + .map(Box::from) + .collect::>() + } + } + serde_json::Value::Null + | serde_json::Value::Bool(_) + | serde_json::Value::Number(_) + | serde_json::Value::Object(_) => hq_bad_proj!("non-array {id}"), + }, + ) + } + + pub fn from_prototype( + prototype: &Block, + blocks: &BlockMap, + target: Weak, + ) -> HQResult> { + hq_assert!(prototype + .block_info() + .is_some_and(|info| info.opcode == BlockOpcode::procedures_prototype)); + #[expect( + clippy::unwrap_used, + reason = "previously asserted that block_info is Some" + )] + let mutations = &prototype.block_info().unwrap().mutation.mutations; + let serde_json::Value::String(proccode) = mutations + .get("proccode") + .ok_or_else(|| make_hq_bad_proj!("missing proccode on procedures_prototype"))? + else { + hq_bad_proj!("proccode wasn't a string"); + }; + let arg_types = arg_types_from_proccode(proccode.as_str())?; + let Some(def_block) = blocks.get( + #[expect( + clippy::unwrap_used, + reason = "previously asserted that block_info is Some" + )] + &prototype + .block_info() + .unwrap() + .parent + .clone() + .ok_or_else(|| make_hq_bad_proj!("prototype block without parent"))?, + ) else { + hq_bad_proj!("no definition block found for {proccode}") + }; + let first_step_id = def_block + .block_info() + .ok_or_else(|| make_hq_bad_proj!("special block where normal def expected"))? + .next + .clone(); + let Some(warp_val) = mutations.get("warp") else { + hq_bad_proj!("missing warp mutation on procedures_definition for {proccode}") + }; + let warp = match warp_val { + serde_json::Value::Bool(w) => *w, + serde_json::Value::String(wstr) => match wstr.as_str() { + "true" => true, + "false" => false, + _ => hq_bad_proj!("unexpected string for warp mutation for {proccode}"), + }, + serde_json::Value::Null + | serde_json::Value::Number(_) + | serde_json::Value::Array(_) + | serde_json::Value::Object(_) => { + hq_bad_proj!("bad type for warp mutation for {proccode}") + } + }; + let arg_ids = Self::string_vec_mutation(mutations, "argumentids")?; + let arg_names = Self::string_vec_mutation(mutations, "argumentnames")?; + let context = ProcContext { + arg_types, + arg_ids, + arg_names, + target, + }; + Ok(Rc::new(Self { + proccode: proccode.as_str().into(), + always_warped: warp, + non_warped_first_step: RefCell::new(PartialStep::None), + warped_first_step: RefCell::new(PartialStep::None), + first_step_id, + context, + })) + } + + pub fn compile_warped(&self, blocks: &BTreeMap, Block>) -> HQResult<()> { + { + if *self.non_warped_first_step()? != PartialStep::None { + return Ok(()); + } + } + { + *self.warped_first_step.try_borrow_mut()? = PartialStep::StartedCompilation; + } + let step_context = StepContext { + warp: true, + proc_context: Some(self.context.clone()), + target: Weak::clone(&self.context.target), + debug: false, // TODO: allow procedures to be dbg? + }; + let step = match self.first_step_id { + None => Rc::new(Step::new( + None, + step_context.clone(), + vec![], + step_context.project()?, + )), + Some(ref id) => { + let block = blocks.get(id).ok_or_else(|| { + make_hq_bad_proj!("procedure's first step block doesn't exist") + })?; + Step::from_block( + block, + id.clone(), + blocks, + &step_context, + &step_context.project()?, + NextBlocks::new(false), + )? + } + }; + *self.warped_first_step.try_borrow_mut()? = PartialStep::Finished(step); + Ok(()) + } +} + +pub type ProcMap = BTreeMap, Rc>; + +pub fn procs_from_target(sb3_target: &Sb3Target, ir_target: &Rc) -> HQResult<()> { + let mut proc_map = ir_target.procedures_mut()?; + for block in sb3_target.blocks.values() { + let Block::Normal { block_info, .. } = block else { + continue; + }; + if block_info.opcode != BlockOpcode::procedures_prototype { + continue; + } + let proc = Proc::from_prototype(block, &sb3_target.blocks, Rc::downgrade(ir_target))?; + let proccode = proc.proccode(); + proc_map.insert(proccode.into(), proc); + } + Ok(()) +} diff --git a/src/ir/project.rs b/src/ir/project.rs new file mode 100644 index 00000000..e778018d --- /dev/null +++ b/src/ir/project.rs @@ -0,0 +1,118 @@ +use super::proc::{procs_from_target, ProcMap}; +use super::variable::variables_from_target; +use super::{Step, Target, Thread, Variable}; +use crate::prelude::*; +use crate::sb3::Sb3Project; + +pub type StepSet = IndexSet>; + +#[derive(Clone, Debug)] +pub struct IrProject { + threads: RefCell>, + steps: RefCell, + global_variables: BTreeMap, Rc>, + targets: RefCell, Rc>>, +} + +impl IrProject { + pub const fn threads(&self) -> &RefCell> { + &self.threads + } + + pub const fn steps(&self) -> &RefCell { + &self.steps + } + + pub const fn targets(&self) -> &RefCell, Rc>> { + &self.targets + } + + pub const fn global_variables(&self) -> &BTreeMap, Rc> { + &self.global_variables + } + + pub fn new(global_variables: BTreeMap, Rc>) -> Self { + Self { + threads: RefCell::new(Box::new([])), + steps: RefCell::new(IndexSet::default()), + global_variables, + targets: RefCell::new(IndexMap::default()), + } + } + + pub fn register_step(&self, step: Rc) -> HQResult<()> { + self.steps() + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? + .insert(step); + Ok(()) + } +} + +impl TryFrom for Rc { + type Error = HQError; + + fn try_from(sb3: Sb3Project) -> HQResult { + let global_variables = variables_from_target( + sb3.targets + .iter() + .find(|target| target.is_stage) + .ok_or_else(|| make_hq_bad_proj!("missing stage target"))?, + ); + + let project = Self::new(IrProject::new(global_variables)); + + let (threads_vec, targets): (Vec<_>, Vec<_>) = sb3 + .targets + .iter() + .enumerate() + .map(|(index, target)| { + let variables = variables_from_target(target); + let procedures = RefCell::new(ProcMap::new()); + let ir_target = Rc::new(Target::new( + target.is_stage, + variables, + Self::downgrade(&project), + procedures, + index + .try_into() + .map_err(|_| make_hq_bug!("target index out of bounds"))?, + )); + procs_from_target(target, &ir_target)?; + let blocks = &target.blocks; + let threads = blocks + .iter() + .filter_map(|(id, block)| { + let thread = Thread::try_from_top_block( + block, + blocks, + Rc::downgrade(&ir_target), + &Self::downgrade(&project), + target.comments.clone().iter().any(|(_id, comment)| { + matches!(comment.block_id.clone(), Some(d) if &d == id) + && *comment.text.clone() == *"hq-dbg" + }), + ) + .transpose()?; + Some(thread) + }) + .collect::>>()?; + Ok((threads, (target.name.clone(), ir_target))) + }) + .collect::>>()? + .iter() + .cloned() + .unzip(); + let threads = threads_vec.into_iter().flatten().collect::>(); + *project + .threads + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? = threads; + *project + .targets + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? = + targets.into_iter().collect(); + Ok(project) + } +} diff --git a/src/ir/step.rs b/src/ir/step.rs new file mode 100644 index 00000000..1fda47a5 --- /dev/null +++ b/src/ir/step.rs @@ -0,0 +1,189 @@ +use super::blocks::{self, NextBlocks}; +use super::{IrProject, StepContext}; +use crate::instructions::{HqYieldFields, IrOpcode, YieldMode}; +use crate::prelude::*; +use crate::sb3::{Block, BlockMap}; +use uuid::Uuid; + +#[derive(Clone, Debug)] +pub struct Step { + context: StepContext, + opcodes: RefCell>, + used_non_inline: RefCell, + inline: RefCell, + /// used for `Hash`. Should be obtained from a block in the `Step` where possible. + id: Box, + project: Weak, +} + +impl PartialEq for Step { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Step {} + +impl Step { + pub const fn context(&self) -> &StepContext { + &self.context + } + + pub const fn opcodes(&self) -> &RefCell> { + &self.opcodes + } + + pub const fn used_non_inline(&self) -> &RefCell { + &self.used_non_inline + } + + pub const fn inline(&self) -> &RefCell { + &self.inline + } + + pub fn project(&self) -> Weak { + Weak::clone(&self.project) + } + + pub fn new( + id: Option>, + context: StepContext, + opcodes: Vec, + project: Weak, + ) -> Self { + Self { + id: id.unwrap_or_else(|| Uuid::new_v4().to_string().into()), + context, + opcodes: RefCell::new(opcodes), + used_non_inline: RefCell::new(false), + inline: RefCell::new(false), + project, + } + } + + pub fn new_rc( + id: Option>, + context: StepContext, + opcodes: Vec, + project: &Weak, + ) -> HQResult> { + let step = Rc::new(Self::new(id, context, opcodes, Weak::clone(project))); + project + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .steps() + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? + .insert(Rc::clone(&step)); + Ok(step) + } + + pub fn new_empty() -> Self { + Self { + id: "".into(), + context: StepContext { + target: Weak::new(), + proc_context: None, + warp: false, // this is a fairly arbitrary choice and doesn't matter at all + debug: false, + }, + opcodes: RefCell::new(vec![]), + used_non_inline: RefCell::new(false), + inline: RefCell::new(false), + project: Weak::new(), + } + } + + pub fn new_terminating(context: StepContext, project: &Weak) -> HQResult> { + const ID: &str = "__terminating_step_hopefully_this_id_wont_cause_any_clashes"; + let step = Rc::new(Self { + id: ID.into(), + context, + opcodes: RefCell::new(vec![IrOpcode::hq_yield(HqYieldFields { + mode: YieldMode::None, + })]), + used_non_inline: RefCell::new(false), + inline: RefCell::new(false), + project: Weak::clone(project), + }); + project + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .steps() + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? + .insert(Rc::clone(&step)); + Ok(step) + } + + pub fn opcodes_mut(&self) -> HQResult>> { + self.opcodes + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell")) + } + + pub fn from_block( + block: &Block, + block_id: Box, + blocks: &BlockMap, + context: &StepContext, + project: &Weak, + final_next_blocks: NextBlocks, + ) -> HQResult> { + if let Some(existing_step) = project + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .steps() + .try_borrow() + .map_err(|_| make_hq_bug!("couldn't immutably borrow cell"))? + .iter() + .find(|step| step.id == block_id) + { + crate::log("step from_block already exists!"); + return Ok(Rc::clone(existing_step)); + } + let step = Rc::new(Self::new( + Some(block_id), + context.clone(), + blocks::from_block(block, blocks, context, project, final_next_blocks)?, + Weak::clone(project), + )); + project + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .steps() + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? + .insert(Rc::clone(&step)); + + Ok(step) + } + + pub fn make_used_non_inline(&self) -> HQResult<()> { + if *self.used_non_inline.try_borrow()? { + return Ok(()); + } + *self + .used_non_inline + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? = true; + Ok(()) + } + + pub fn make_inlined(&self) -> HQResult<()> { + if *self.inline.try_borrow()? { + return Ok(()); + } + *self + .inline + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? = true; + Ok(()) + } +} + +impl core::hash::Hash for Step { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} diff --git a/src/ir/target.rs b/src/ir/target.rs new file mode 100644 index 00000000..09bcb5a0 --- /dev/null +++ b/src/ir/target.rs @@ -0,0 +1,54 @@ +use super::{proc::Proc, IrProject, Variable}; +use crate::prelude::*; +use core::cell::{Ref, RefMut}; + +#[derive(Debug, Clone)] +pub struct Target { + is_stage: bool, + variables: BTreeMap, Rc>, + project: Weak, + procedures: RefCell, Rc>>, + index: u32, +} + +impl Target { + pub const fn is_stage(&self) -> bool { + self.is_stage + } + + pub const fn variables(&self) -> &BTreeMap, Rc> { + &self.variables + } + + pub fn project(&self) -> Weak { + Weak::clone(&self.project) + } + + pub fn procedures(&self) -> HQResult, Rc>>> { + Ok(self.procedures.try_borrow()?) + } + + pub fn procedures_mut(&self) -> HQResult, Rc>>> { + Ok(self.procedures.try_borrow_mut()?) + } + + pub const fn index(&self) -> u32 { + self.index + } + + pub const fn new( + is_stage: bool, + variables: BTreeMap, Rc>, + project: Weak, + procedures: RefCell, Rc>>, + index: u32, + ) -> Self { + Self { + is_stage, + variables, + project, + procedures, + index, + } + } +} diff --git a/src/ir/thread.rs b/src/ir/thread.rs new file mode 100644 index 00000000..19716146 --- /dev/null +++ b/src/ir/thread.rs @@ -0,0 +1,77 @@ +use super::blocks::NextBlocks; +use super::{Event, IrProject, Step, StepContext, Target}; +use crate::prelude::*; +use crate::sb3::{Block, BlockMap, BlockOpcode}; + +#[derive(Clone, Debug)] +pub struct Thread { + event: Event, + first_step: Rc, + target: Weak, +} + +impl Thread { + pub const fn event(&self) -> Event { + self.event + } + + pub const fn first_step(&self) -> &Rc { + &self.first_step + } + + pub fn target(&self) -> Weak { + Weak::clone(&self.target) + } + + /// tries to construct a thread from a top-level block. + /// Returns Ok(None) if the top-level block is not a valid event or if there is no next block. + pub fn try_from_top_block( + block: &Block, + blocks: &BlockMap, + target: Weak, + project: &Weak, + debug: bool, + ) -> HQResult> { + let Some(block_info) = block.block_info() else { + return Ok(None); + }; + #[expect(clippy::wildcard_enum_match_arm, reason = "too many variants to match")] + let event = match block_info.opcode { + BlockOpcode::event_whenflagclicked => Event::FlagCLicked, + BlockOpcode::event_whenbackdropswitchesto + | BlockOpcode::event_whenbroadcastreceived + | BlockOpcode::event_whengreaterthan + | BlockOpcode::event_whenkeypressed + | BlockOpcode::event_whenstageclicked + | BlockOpcode::event_whenthisspriteclicked + | BlockOpcode::event_whentouchingobject => { + hq_todo!("unimplemented event {:?}", block_info.opcode) + } + _ => return Ok(None), + }; + let Some(next_id) = &block_info.next else { + return Ok(None); + }; + let next = blocks + .get(next_id) + .ok_or_else(|| make_hq_bug!("block not found in BlockMap"))?; + let first_step = Step::from_block( + next, + next_id.clone(), + blocks, + &StepContext { + target: Weak::clone(&target), + proc_context: None, + warp: false, // steps from top blocks are never warped + debug, + }, + project, + NextBlocks::new(true), + )?; + Ok(Some(Self { + event, + first_step, + target, + })) + } +} diff --git a/src/ir/types.rs b/src/ir/types.rs new file mode 100644 index 00000000..599790f3 --- /dev/null +++ b/src/ir/types.rs @@ -0,0 +1,123 @@ +use crate::prelude::*; +use bitmask_enum::bitmask; + +/// a bitmask of possible IR types +#[bitmask(u32)] +#[bitmask_config(vec_debug, flags_iter)] +pub enum Type { + IntZero, + IntPos, + IntNeg, + IntNonZero = Self::IntPos.or(Self::IntNeg).bits, + Int = Self::IntNonZero.or(Self::IntZero).bits, + + FloatPosZero, + FloatNegZero, + FloatZero = Self::FloatPosZero.or(Self::FloatNegZero).bits, + + FloatPosInt, + FloatPosFrac, + FloatPosReal = Self::FloatPosInt.or(Self::FloatPosFrac).bits, + + FloatNegInt, + FloatNegFrac, + FloatNegReal = Self::FloatNegInt.or(Self::FloatNegFrac).bits, + + FloatPosInf, + FloatNegInf, + FloatInf = Self::FloatPosInf.or(Self::FloatNegInf).bits, + + FloatNan, + + FloatPos = Self::FloatPosReal.or(Self::FloatPosInf).bits, + FloatNeg = Self::FloatNegReal.or(Self::FloatNegInf).bits, + + FloatPosWhole = Self::FloatPosInt.or(Self::FloatPosZero).bits, + FloatNegWhole = Self::FloatNegInt.or(Self::FloatNegZero).bits, + + FloatInt = Self::FloatPosWhole.or(Self::FloatNegWhole).bits, + FloatFrac = Self::FloatPosFrac.or(Self::FloatNegFrac).bits, + FloatReal = Self::FloatInt.or(Self::FloatFrac).bits, + FloatNotNan = Self::FloatReal.or(Self::FloatInf).bits, + Float = Self::FloatNotNan.or(Self::FloatNan).bits, + + BooleanTrue, + BooleanFalse, + Boolean = Self::BooleanTrue.or(Self::BooleanFalse).bits, + + QuasiInt = Self::Int.or(Self::Boolean).bits, + + Number = Self::QuasiInt.or(Self::Float).bits, + + StringNumber, // a string which can be interpreted as a non-nan number + StringBoolean, // "true" or "false" + StringNan, // some other string which can only be interpreted as NaN + String = Self::StringNumber + .or(Self::StringBoolean) + .or(Self::StringNan) + .bits, + + QuasiBoolean = Self::Boolean.or(Self::StringBoolean).bits, + QuasiNumber = Self::Number.or(Self::StringNumber).bits, + + Any = Self::String.or(Self::Number).bits, + + Color, +} +impl Type { + pub const BASE_TYPES: [Self; 3] = [Self::String, Self::QuasiInt, Self::Float]; + + pub fn is_base_type(self) -> bool { + (!self.is_none()) && Self::BASE_TYPES.iter().any(|ty| ty.contains(self)) + } + + pub fn base_type(self) -> Option { + if !self.is_base_type() { + return None; + } + Self::BASE_TYPES + .iter() + .copied() + .find(|&ty| ty.contains(self)) + } + + pub fn base_types(self) -> Box> { + if self.is_none() { + return Box::new(core::iter::empty()); + } + Box::new( + Self::BASE_TYPES + .iter() + .filter(move |ty| ty.intersects(self)) + .copied(), + ) + } + + pub const fn maybe_positive(self) -> bool { + self.contains(Self::IntPos) + || self.intersects(Self::FloatPos) + || self.contains(Self::BooleanTrue) + } + + pub const fn maybe_negative(self) -> bool { + self.contains(Self::IntNeg) || self.intersects(Self::FloatNeg) + } + + pub const fn maybe_zero(self) -> bool { + self.contains(Self::IntZero) + || self.contains(Self::BooleanFalse) + || self.intersects(Self::FloatZero) + } + + pub const fn maybe_nan(self) -> bool { + self.intersects(Self::FloatNan) || self.contains(Self::StringNan) + } + + pub const fn none_if_false(condition: bool, if_true: Self) -> Self { + if condition { + if_true + } else { + Self::none() + } + } +} diff --git a/src/ir/variable.rs b/src/ir/variable.rs new file mode 100644 index 00000000..a6f1b28b --- /dev/null +++ b/src/ir/variable.rs @@ -0,0 +1,76 @@ +use super::Type; +use crate::{ + prelude::*, + sb3::{Target as Sb3Target, VarVal}, +}; +use core::cell::Ref; +use core::hash::{Hash, Hasher}; + +#[derive(Debug, Clone, PartialEq)] +pub struct Variable { + possible_types: RefCell, + initial_value: VarVal, + local: bool, +} + +impl Variable { + pub const fn new(ty: Type, initial_value: VarVal, local: bool) -> Self { + Self { + possible_types: RefCell::new(ty), + initial_value, + local, + } + } + + pub fn add_type(&self, ty: Type) { + let current = *self.possible_types.borrow(); + *self.possible_types.borrow_mut() = current.or(ty); + } + + pub fn possible_types(&self) -> Ref { + self.possible_types.borrow() + } + + pub const fn initial_value(&self) -> &VarVal { + &self.initial_value + } + + pub const fn local(&self) -> bool { + self.local + } +} + +#[derive(Clone, Debug)] +pub struct RcVar(pub Rc); + +impl PartialEq for RcVar { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for RcVar {} + +impl Hash for RcVar { + fn hash(&self, state: &mut H) { + core::ptr::hash(Rc::as_ptr(&self.0), state); + } +} + +pub fn variables_from_target(target: &Sb3Target) -> BTreeMap, Rc> { + target + .variables + .iter() + .map(|(id, var_info)| { + ( + id.clone(), + Rc::new(Variable::new( + Type::none(), + #[expect(clippy::unwrap_used, reason = "this field exists on all variants")] + var_info.get_1().unwrap().clone(), + false, + )), + ) + }) + .collect() +} diff --git a/src/ir_opt.rs b/src/ir_opt.rs deleted file mode 100644 index fdb45ae6..00000000 --- a/src/ir_opt.rs +++ /dev/null @@ -1,200 +0,0 @@ -use crate::ir::{InputType, IrBlock, IrOpcode, IrProject, IrVal, TypeStack, TypeStackImpl}; -use crate::log; -use crate::HQError; -use alloc::collections::{BTreeMap, BTreeSet}; -use alloc::rc::Rc; -use alloc::string::String; -use alloc::vec::Vec; -use core::cell::RefCell; - -impl IrProject { - pub fn optimise(&mut self) -> Result<(), HQError> { - self.const_fold()?; - self.variable_types()?; - Ok(()) - } - pub fn variable_types(&mut self) -> Result<(), HQError> { - let mut var_map: BTreeMap> = BTreeMap::new(); - #[allow(clippy::type_complexity)] - let mut block_swaps: BTreeMap< - String, - BTreeMap< - (String, String), - Vec<(usize, Option<(IrOpcode, Rc>>)>)>, - >, - > = BTreeMap::new(); // here Some(_) means swap the block, None means remove the block - for (step_identifier, step) in self.steps.clone() { - for (i, opcode) in step.opcodes().iter().enumerate() { - match opcode.opcode() { - IrOpcode::data_setvariableto { VARIABLE, .. } - | IrOpcode::data_teevariable { VARIABLE, .. } => { - let var_type_set = var_map.entry(VARIABLE.clone()).or_default(); - if i == 0 { - hq_bug!("found a data_setvariableto or data_teevariable in position 0"); - } - let previous_block = step.opcodes().get(i - 1).unwrap(); - let input_type = match previous_block.opcode() { - IrOpcode::hq_cast(from, InputType::Unknown) => { - block_swaps - .entry(VARIABLE.clone()) - .or_default() - .entry(step_identifier.clone()) - .or_default() - .push((i - 1, None)); - from - } - IrOpcode::unknown_const { val } => &match val { - IrVal::Boolean(b) => { - block_swaps - .entry(VARIABLE.clone()) - .or_default() - .entry(step_identifier.clone()) - .or_default() - .push(( - i - 1, - Some(( - IrOpcode::boolean { BOOL: *b }, - Rc::new(RefCell::new(Some(TypeStack( - previous_block.type_stack().get(1), - InputType::Boolean, - )))), - )), - )); - InputType::Boolean - } - IrVal::Int(i) => { - block_swaps - .entry(VARIABLE.clone()) - .or_default() - .entry(step_identifier.clone()) - .or_default() - .push(( - (i - 1).try_into().map_err(|_| { - make_hq_bug!("i32 out of range for usize") - })?, - Some(( - IrOpcode::math_integer { NUM: *i }, - Rc::new(RefCell::new(Some(TypeStack( - previous_block.type_stack().get(1), - InputType::ConcreteInteger, - )))), - )), - )); - InputType::ConcreteInteger - } - IrVal::Float(f) => { - block_swaps - .entry(VARIABLE.clone()) - .or_default() - .entry(step_identifier.clone()) - .or_default() - .push(( - i - 1, - Some(( - IrOpcode::math_number { NUM: *f }, - Rc::new(RefCell::new(Some(TypeStack( - previous_block.type_stack().get(1), - InputType::Float, - )))), - )), - )); - InputType::Float - } - IrVal::String(s) => { - block_swaps - .entry(VARIABLE.clone()) - .or_default() - .entry(step_identifier.clone()) - .or_default() - .push(( - i - 1, - Some(( - IrOpcode::text { TEXT: s.clone() }, - Rc::new(RefCell::new(Some(TypeStack( - previous_block.type_stack().get(1), - InputType::String, - )))), - )), - )); - InputType::String - } - IrVal::Unknown(_) => { - hq_bug!("unexpected unknown value inside of unknown const") - } - }, - _ => &previous_block - .type_stack() - .borrow() - .clone() - .ok_or(make_hq_bug!("unexpected empty type stack"))? - .1 - .clone(), - }; - var_type_set.insert(input_type.clone()); - } - _ => (), - } - } - } - for (var_id, types) in var_map { - if types.len() == 1 { - let ty = types.iter().next().unwrap(); - log(format!("found variable of single type {:?}", ty).as_str()); - for (step_identifier, mut step) in self.steps.clone() { - let maybe_swaps_from_this_var = block_swaps.get(&var_id); - if let Some(swaps_from_this_var) = maybe_swaps_from_this_var { - let to_swap = swaps_from_this_var.get(&step_identifier); - if to_swap.is_none() { - continue; - } - let Some(swap_vec) = to_swap else { - unreachable!() - }; - for (index, swap) in swap_vec.iter().rev() { - if let Some((opcode, type_stack)) = swap { - *(step - .opcodes_mut() - .get_mut(*index) - .ok_or(make_hq_bug!("swap index out of range")))? = - IrBlock::new_with_stack_no_cast( - opcode.clone(), - type_stack.clone(), - )? - } else { - step.opcodes_mut().remove(*index); - } - } - } - for opcode in step.opcodes_mut() { - match &mut opcode.opcode { - IrOpcode::data_setvariableto { - VARIABLE, - assume_type: ref mut assume_type @ None, - } - | IrOpcode::data_teevariable { - VARIABLE, - assume_type: ref mut assume_type @ None, - } if VARIABLE == &var_id => { - *assume_type = Some(ty.clone()); - } - IrOpcode::data_variable { - VARIABLE, - assume_type: ref mut assume_type @ None, - } if VARIABLE == &var_id => { - *assume_type = Some(ty.clone()); - } - _ => (), - } - } - } - } - } - Ok(()) - } - pub fn const_fold(&mut self) -> Result<(), HQError> { - for step in self.steps.values_mut() { - step.const_fold()?; - } - Ok(()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 828f2aef..6b0e3d9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,34 @@ -#![cfg_attr(not(test), no_std)] +#![feature(stmt_expr_attributes)] +#![feature(if_let_guard)] +#![doc(html_logo_url = "https://hyperquark.github.io/hyperquark/logo.png")] +#![doc(html_favicon_url = "https://hyperquark.github.io/hyperquark/favicon.ico")] +#![warn(clippy::cargo, clippy::nursery, clippy::pedantic)] +#![allow( + clippy::non_std_lazy_statics, + reason = "bug in clippy (https://github.com/rust-lang/rust-clippy/issues/14729)" +)] +#![allow( + clippy::missing_errors_doc, + reason = "Too many Results everywhere to document every possible error case. Errors should be self-descriptive and user readable anyway." +)] +#![deny(clippy::allow_attributes, clippy::allow_attributes_without_reason)] +#![warn( + clippy::alloc_instead_of_core, + clippy::clone_on_ref_ptr, + clippy::dbg_macro, + clippy::expect_used, + clippy::get_unwrap, + clippy::missing_asserts_for_indexing, + clippy::panic, + clippy::rc_buffer, + clippy::redundant_type_annotations, + clippy::shadow_reuse, + clippy::std_instead_of_alloc, + clippy::std_instead_of_core, + clippy::string_to_string, + clippy::unwrap_used, + clippy::wildcard_enum_match_arm +)] #[macro_use] extern crate alloc; @@ -8,29 +38,58 @@ use wasm_bindgen::prelude::*; #[macro_use] mod error; -pub mod ir; -pub mod ir_opt; -pub mod sb3; -pub mod targets; +mod ir; +mod optimisation; +// pub mod ir_opt; +mod sb3; +mod wasm; +#[macro_use] +mod instructions; + +#[doc(inline)] +pub use error::{HQError, HQErrorType, HQResult}; + +mod registry; -pub use error::{HQError, HQErrorType}; +/// commonly used _things_ which would be nice not to have to type out every time +pub mod prelude { + pub use crate::registry::{Registry, RegistryDefault}; + pub use crate::{HQError, HQResult}; + pub use alloc::borrow::Cow; + pub use alloc::boxed::Box; + pub use alloc::collections::{BTreeMap, BTreeSet}; + pub use alloc::rc::{Rc, Weak}; + pub use alloc::string::{String, ToString}; + pub use alloc::vec::Vec; + pub use core::borrow::Borrow; + pub use core::cell::RefCell; + pub use core::fmt; -use targets::wasm; + use core::hash::BuildHasherDefault; + use hashers::fnv::FNV1aHasher64; + use indexmap; + pub type IndexMap = indexmap::IndexMap>; + pub type IndexSet = indexmap::IndexSet>; + + pub use itertools::Itertools; +} -#[cfg(not(test))] +#[cfg(target_family = "wasm")] #[wasm_bindgen(js_namespace=console)] extern "C" { pub fn log(s: &str); } -#[cfg(test)] +#[cfg(not(target_family = "wasm"))] pub fn log(s: &str) { - println!("{s}") + println!("{s}"); } +#[cfg(feature = "compiler")] #[wasm_bindgen] -pub fn sb3_to_wasm(proj: &str) -> Result { - let mut ir_proj = ir::IrProject::try_from(sb3::Sb3Project::try_from(proj)?)?; - ir_proj.optimise()?; - ir_proj.try_into() +pub fn sb3_to_wasm(proj: &str, flags: wasm::WasmFlags) -> HQResult { + let sb3_proj = sb3::Sb3Project::try_from(proj)?; + let ir_proj = sb3_proj.try_into()?; + optimisation::ir_optimise(&ir_proj)?; + wasm::WasmProject::from_ir(&ir_proj, flags)?.finish() } diff --git a/src/optimisation.rs b/src/optimisation.rs new file mode 100644 index 00000000..fe26e438 --- /dev/null +++ b/src/optimisation.rs @@ -0,0 +1,9 @@ +use crate::ir::IrProject; +use crate::prelude::*; + +mod var_types; + +pub fn ir_optimise(ir: &Rc) -> HQResult<()> { + var_types::optimise_var_types(ir)?; + Ok(()) +} diff --git a/src/optimisation/var_types.rs b/src/optimisation/var_types.rs new file mode 100644 index 00000000..8d8277d1 --- /dev/null +++ b/src/optimisation/var_types.rs @@ -0,0 +1,26 @@ +use crate::instructions::{DataSetvariabletoFields, IrOpcode}; +use crate::ir::{IrProject, Type as IrType}; +use crate::prelude::*; + +pub fn optimise_var_types(project: &Rc) -> HQResult<()> { + crate::log("optimise vars"); + for step in project.steps().borrow().iter() { + let mut type_stack: Vec = vec![]; // a vector of types, and where they came from + for block in &*step.opcodes().try_borrow()?.clone() { + let expected_inputs = block.acceptable_inputs(); + if type_stack.len() < expected_inputs.len() { + hq_bug!("didn't have enough inputs on the type stack") + } + let inputs = type_stack + .splice((type_stack.len() - expected_inputs.len()).., []) + .collect::>(); + if let IrOpcode::data_setvariableto(DataSetvariabletoFields(var)) = block { + var.0.add_type(inputs[0]); + } + if let Some(output) = block.output_type(expected_inputs)? { + type_stack.push(output); + } + } + } + Ok(()) +} diff --git a/src/registry.rs b/src/registry.rs new file mode 100644 index 00000000..39cdcfe8 --- /dev/null +++ b/src/registry.rs @@ -0,0 +1,118 @@ +use crate::prelude::*; +use core::hash::Hash; + +#[derive(Clone)] +pub struct MapRegistry(RefCell>) +where + K: Hash + Eq + Clone; + +// deriving Default doesn't work if V: !Default, so we implement it manually +impl Default for MapRegistry +where + K: Hash + Eq + Clone, +{ + fn default() -> Self { + Self(RefCell::new(IndexMap::default())) + } +} + +impl Registry for MapRegistry +where + K: Hash + Eq + Clone, +{ + type Key = K; + type Value = V; + + fn registry(&self) -> &RefCell> { + &self.0 + } +} + +#[derive(Clone)] +pub struct SetRegistry(RefCell>) +where + K: Hash + Eq + Clone; + +impl Default for SetRegistry +where + K: Hash + Eq + Clone, +{ + fn default() -> Self { + Self(RefCell::new(IndexMap::default())) + } +} + +impl Registry for SetRegistry +where + K: Hash + Eq + Clone, +{ + type Key = K; + type Value = (); + + fn registry(&self) -> &RefCell> { + &self.0 + } +} + +pub trait Registry: Sized { + const IS_SET: bool = false; + + type Key: Hash + Clone + Eq; + type Value; + + fn registry(&self) -> &RefCell>; + + /// get the index of the specified item, inserting it if it doesn't exist in the map already. + /// Doesn't check if the provided value matches what's already there. This is generic because + /// various different numeric types are needed in different places, so it's easiest to encapsulate + /// the casting logic in here. + fn register(&self, key: Self::Key, value: Self::Value) -> HQResult + where + N: TryFrom, + >::Error: core::fmt::Debug, + { + self.registry() + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? + .entry(key.clone()) + .or_insert(value); + N::try_from( + self.registry() + .try_borrow()? + .get_index_of(&key) + .ok_or_else(|| make_hq_bug!("couldn't find entry in Registry"))?, + ) + .map_err(|_| make_hq_bug!("registry item index out of bounds")) + } + + fn register_override(&self, key: Self::Key, value: Self::Value) -> HQResult + where + N: TryFrom, + >::Error: core::fmt::Debug, + { + self.registry() + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? + .entry(key.clone()) + .insert_entry(value); + N::try_from( + self.registry() + .try_borrow()? + .get_index_of(&key) + .ok_or_else(|| make_hq_bug!("couldn't find entry in Registry"))?, + ) + .map_err(|_| make_hq_bug!("registry item index out of bounds")) + } +} + +pub trait RegistryDefault: Registry { + fn register_default(&self, key: Self::Key) -> HQResult + where + N: TryFrom, + >::Error: core::fmt::Debug, + { + self.register(key, Self::Value::default()) + } +} + +impl RegistryDefault for R where R: Registry {} diff --git a/src/sb3.rs b/src/sb3.rs index 51811857..acfbb2d2 100644 --- a/src/sb3.rs +++ b/src/sb3.rs @@ -1,32 +1,39 @@ -use crate::error::HQError; -use alloc::collections::BTreeMap; -use alloc::string::String; -use alloc::vec::Vec; +//! 1-1 representation of `project.json` or `sprite.json` files +//! in the `sb3` format. `sb` or `sb2` files must be converted first; +//! `sb3` files must be unzipped first. See +//! for a loose informal specification. + +use crate::prelude::*; use enum_field_getter::EnumFieldGetter; use serde::{Deserialize, Serialize}; use serde_json::Value; +/// A scratch project #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Sb3Project { pub targets: Vec, pub monitors: Vec, - pub extensions: Vec, + pub extensions: Vec>, pub meta: Meta, } +/// A comment, possibly attached to a block #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Comment { - pub block_id: Option, + pub block_id: Option>, pub x: Option, pub y: Option, pub width: f64, pub height: f64, pub minimized: bool, - pub text: String, + pub text: Box, } -#[allow(non_camel_case_types)] +/// A possible block opcode, encompassing the default block pallette, the pen extension, +/// and a few hidden but non-obsolete blocks. A block being listed here does not imply that +/// it is supported by `HyperQuark`. +#[expect(non_camel_case_types, reason = "opcodes are snake_case")] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub enum BlockOpcode { control_repeat, @@ -201,6 +208,7 @@ pub enum BlockOpcode { // other, } +/// A scratch block - either special or not #[derive(Serialize, Deserialize, Debug, Clone, EnumFieldGetter)] #[serde(untagged)] pub enum Block { @@ -216,22 +224,26 @@ pub enum Block { Special(BlockArray), } +/// A special representation of a scratch block. #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum BlockArray { NumberOrAngle(u32, f64), - ColorOrString(u32, String), - Broadcast(u32, String, String), // might also be variable or list if not top level? - VariableOrList(u32, String, String, f64, f64), + ColorOrString(u32, Box), + /// This might also represent a variable or list if the block is not top-level, in theory + Broadcast(u32, Box, Box), + VariableOrList(u32, Box, Box, f64, f64), } +/// Either a block array or a block id #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum BlockArrayOrId { - Id(String), + Id(Box), Array(BlockArray), } +/// Possible inputs (round or predicate) in a block #[derive(Serialize, Deserialize, Debug, Clone, EnumFieldGetter)] #[serde(untagged)] pub enum Input { @@ -239,49 +251,65 @@ pub enum Input { NoShadow(u32, Option), } +/// Possible fields (rectangular) in a block #[derive(Serialize, Deserialize, Debug, Clone, EnumFieldGetter)] #[serde(untagged)] pub enum Field { Value((Option,)), - ValueId(Option, Option), + ValueId(Option, Option>), } +impl Field { + // this isn't auto implemented by EnumFieldGetter because Field::Value is actually a tuple + // in a tuple, so that serde correctly parses a single-item array + pub const fn get_0(&self) -> Option<&VarVal> { + match self { + Self::ValueId(val, _) | Self::Value((val,)) => val.as_ref(), + } + } +} + +/// Represents a mutation on a block. See #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Mutation { - pub tag_name: String, + /// ignored - should always be "mutation" + pub tag_name: Box, + /// ignored - should always be [] #[serde(default)] pub children: Vec<()>, #[serde(flatten)] - pub mutations: BTreeMap, + pub mutations: BTreeMap, Value>, } impl Default for Mutation { fn default() -> Self { - Mutation { - tag_name: String::from("mutation"), - children: Default::default(), + Self { + tag_name: "mutation".into(), + children: vec![], mutations: BTreeMap::new(), } } } +/// Represents a non-special block #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct BlockInfo { pub opcode: BlockOpcode, - pub next: Option, - pub parent: Option, - pub inputs: BTreeMap, - pub fields: BTreeMap, + pub next: Option>, + pub parent: Option>, + pub inputs: BTreeMap, Input>, + pub fields: BTreeMap, Field>, pub shadow: bool, pub top_level: bool, #[serde(default)] pub mutation: Mutation, } -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] -#[allow(non_camel_case_types)] +/// the data format of a costume +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[expect(non_camel_case_types, reason = "lowercase in project.json")] pub enum CostumeDataFormat { png, svg, @@ -291,12 +319,13 @@ pub enum CostumeDataFormat { gif, } +/// A costume #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Costume { - pub asset_id: String, - pub name: String, - pub md5ext: String, + pub asset_id: Box, + pub name: Box, + pub md5ext: Box, pub data_format: CostumeDataFormat, #[serde(default)] pub bitmap_resolution: f64, @@ -304,43 +333,49 @@ pub struct Costume { pub rotation_center_y: f64, } +/// A sound #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Sound { - pub asset_id: String, - pub name: String, - pub md5ext: String, - pub data_format: String, + pub asset_id: Box, + pub name: Box, + pub md5ext: Box, + pub data_format: Box, // TODO: enumerate pub rate: f64, pub sample_count: f64, } +/// The (default) value of a variable #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(untagged)] pub enum VarVal { Float(f64), Bool(bool), - String(String), + String(Box), } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +/// Represents a variable +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, EnumFieldGetter)] #[serde(untagged)] pub enum VariableInfo { - CloudVar(String, VarVal, bool), - LocalVar(String, VarVal), + CloudVar(Box, VarVal, bool), + LocalVar(Box, VarVal), } +pub type BlockMap = BTreeMap, Block>; + +/// A target (sprite or stage) #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Target { pub is_stage: bool, - pub name: String, - pub variables: BTreeMap, - pub lists: BTreeMap)>, + pub name: Box, + pub variables: BTreeMap, VariableInfo>, + pub lists: BTreeMap, (Box, Vec)>, #[serde(default)] - pub broadcasts: BTreeMap, - pub blocks: BTreeMap, - pub comments: BTreeMap, + pub broadcasts: BTreeMap, Box>, + pub blocks: BlockMap, + pub comments: BTreeMap, Comment>, pub current_costume: u32, pub costumes: Vec, pub sounds: Vec, @@ -349,11 +384,11 @@ pub struct Target { #[serde(default)] pub tempo: f64, #[serde(default)] - pub video_state: Option, + pub video_state: Option>, #[serde(default)] pub video_transparency: f64, #[serde(default)] - pub text_to_speech_language: Option, + pub text_to_speech_language: Option>, #[serde(default)] pub visible: bool, #[serde(default)] @@ -367,28 +402,31 @@ pub struct Target { #[serde(default)] pub draggable: bool, #[serde(default)] - pub rotation_style: String, + pub rotation_style: Box, #[serde(flatten)] - pub unknown: BTreeMap, + pub unknown: BTreeMap, Value>, } +/// The value of a list monitor #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum ListMonitorValue { List(Vec), - String(String), + String(Box), } +/// A monitor #[derive(Serialize, Deserialize, Debug, Clone, EnumFieldGetter)] #[serde(untagged)] pub enum Monitor { #[serde(rename_all = "camelCase")] ListMonitor { - id: String, - mode: String, // The name of the monitor's mode: "default", "large", "slider", or "list" - should be "list" - opcode: String, - params: BTreeMap, - sprite_name: Option, + id: Box, + /// The name of the monitor's mode: "default", "large", "slider", or "list" - should be "list" + mode: Box, + opcode: Box, + params: BTreeMap, Box>, + sprite_name: Option>, width: f64, height: f64, x: f64, @@ -398,11 +436,12 @@ pub enum Monitor { }, #[serde(rename_all = "camelCase")] VarMonitor { - id: String, - mode: String, // The name of the monitor's mode: "default", "large", "slider", or "list". - opcode: String, - params: BTreeMap, - sprite_name: Option, + id: Box, + /// The name of the monitor's mode: "default", "large", "slider", or "list". + mode: Box, + opcode: Box, + params: BTreeMap, Box>, + sprite_name: Option>, value: VarVal, width: f64, height: f64, @@ -415,11 +454,12 @@ pub enum Monitor { }, } +/// metadata about a scratch project - only included here for completeness #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Meta { - pub semver: String, - pub vm: String, - pub agent: String, + pub semver: Box, + pub vm: Box, + pub agent: Box, } impl TryFrom for Sb3Project { @@ -454,7 +494,7 @@ impl TryFrom<&str> for Sb3Project { err.line(), err.column() ), - _ => hq_bad_proj!("Failed to deserialize json"), + Category::Io => hq_bug!("Failed to deserialize json due to IO error"), }, } } @@ -464,7 +504,7 @@ impl TryFrom<&str> for Sb3Project { pub mod tests { use super::*; - pub fn test_project_id(id: &str) -> String { + pub fn test_project_id(id: &str) -> String { use std::time::{SystemTime, UNIX_EPOCH}; println!("https://api.scratch.mit.edu/projects/{:}/", id); let token_val = serde_json::from_str::( @@ -500,7 +540,7 @@ pub mod tests { } #[test] - fn paper_minecraft() { + fn paper_minecraft() { let resp = self::test_project_id("10128407"); let j: Sb3Project = resp.try_into().unwrap(); dbg!(j); @@ -527,7 +567,7 @@ pub mod tests { } #[test] - fn level_eaten() { + fn level_eaten() { let resp = self::test_project_id("704676520"); let j: Sb3Project = resp.try_into().unwrap(); dbg!(j); @@ -554,7 +594,7 @@ pub mod tests { } #[test] - fn hq_test_project() { + fn hq_test_project() { let resp = self::test_project_id("771449498"); dbg!(&resp); let j: Sb3Project = resp.try_into().unwrap(); @@ -562,7 +602,7 @@ pub mod tests { } #[test] - fn default_project() { + fn default_project() { let resp = self::test_project_id("510186917"); let j: Sb3Project = resp.try_into().unwrap(); dbg!(j); diff --git a/src/targets/mod.rs b/src/targets/mod.rs deleted file mode 100644 index ce1d9f82..00000000 --- a/src/targets/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod wasm; diff --git a/src/targets/wasm.rs b/src/targets/wasm.rs deleted file mode 100644 index c7020526..00000000 --- a/src/targets/wasm.rs +++ /dev/null @@ -1,2893 +0,0 @@ -use crate::ir::{ - InputType, IrBlock, IrOpcode, IrProject, IrVal, Procedure, Step, ThreadContext, ThreadStart, - TypeStackImpl, -}; -use crate::sb3::VarVal; -use crate::HQError; -use alloc::collections::BTreeMap; -use alloc::rc::Rc; -use alloc::string::String; -use alloc::vec::Vec; -use core::hash::BuildHasherDefault; -use hashers::fnv::FNV1aHasher64; -use indexmap::IndexMap; -use wasm_bindgen::prelude::*; -use wasm_encoder::{ - AbstractHeapType, BlockType as WasmBlockType, CodeSection, ConstExpr, DataSection, - ElementSection, Elements, EntityType, ExportKind, ExportSection, Function, FunctionSection, - GlobalSection, GlobalType, HeapType, ImportSection, Instruction, MemArg, MemorySection, - MemoryType, Module, RefType, TableSection, TableType, TypeSection, ValType, -}; - -fn instructions( - op: &IrBlock, - context: Rc, - string_consts: &mut Vec, - steps: &IndexMap<(String, String), Step, BuildHasherDefault>, - input_types: Vec, -) -> Result>, HQError> { - use InputType::*; - use Instruction::*; - use IrOpcode::*; - let locals_shift = match context.proc { - None => 0, - Some(Procedure { - warp, - ref arg_types, - .. - }) => { - if !warp { - hq_todo!("non-warp procedure") - } else { - i32::try_from(arg_types.len()) - .map_err(|_| make_hq_bug!("arg types len out of bounds"))? - } - } - }; - macro_rules! local { - ($id:ident) => {{ - u32::try_from( - i32::try_from(step_func_locals::$id).map_err(|_| make_hq_bug!(""))? + locals_shift, - ) - .map_err(|_| { - make_hq_bug!( - "shifted local from {:} out of bounds", - step_func_locals::$id - ) - })? - }}; - } - //let expected_output = *op.expected_output(); - //let mut actual_output = *op.actual_output(); - //dbg!(&op.opcode(), op.type_stack.len()); - let mut instructions = match &op.opcode() { - looks_think => { - if context.dbg { - vec![Call(func_indices::DBG_ASSERT)] - } else { - vec![ - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!("target index out of bounds"))?, - ), - Call(func_indices::LOOKS_THINK), - I32Const(0), - I32Const(0), - I32Store8(MemArg { - offset: 0, - align: 0, - memory_index: 0, - }), - ] - } - } - looks_say => { - if context.dbg { - vec![Call(func_indices::DBG_LOG)] - } else { - vec![ - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!("target index out of bounds"))?, - ), - Call(func_indices::LOOKS_SAY), - I32Const(0), - I32Const(0), - I32Store8(MemArg { - offset: 0, - align: 0, - memory_index: 0, - }), - ] - } - } - unknown_const { val } => match val { - IrVal::Unknown(_) => hq_bug!("unknown const inside of unknown const"), - IrVal::Int(i) => vec![I32Const(hq_value_types::INT64), I64Const(*i as i64)], - IrVal::Float(f) => vec![ - I32Const(hq_value_types::FLOAT64), - I64Const(i64::from_le_bytes(f.to_le_bytes())), - ], - IrVal::Boolean(b) => vec![I32Const(hq_value_types::BOOL64), I64Const(*b as i64)], - IrVal::String(s) => vec![ - I32Const(hq_value_types::EXTERN_STRING_REF64), - I64Const( - { - if let Some(idx) = string_consts.iter().position(|string| string == s) { - idx - } else { - string_consts.push(s.clone()); - string_consts.len() - 1 - } - } - .try_into() - .map_err(|_| make_hq_bug!("string index out of bounds"))?, - ), - ], - }, - operator_add => { - if InputType::Integer.includes(input_types.first().unwrap()) - && InputType::Integer.includes(input_types.get(1).unwrap()) - { - vec![I64Add] - } else if InputType::Integer.includes(input_types.get(1).unwrap()) { - vec![F64ConvertI64S, F64Add] - } else if InputType::Integer.includes(input_types.first().unwrap()) { - vec![ - LocalSet(local!(F64)), - F64ConvertI64S, - LocalGet(local!(F64)), - F64Add, - ] - } else { - vec![F64Add] - } - } - operator_subtract => { - if InputType::Integer.includes(input_types.first().unwrap()) - && InputType::Integer.includes(input_types.get(1).unwrap()) - { - vec![I64Sub] - } else if InputType::Integer.includes(input_types.get(1).unwrap()) { - vec![F64ConvertI64S, F64Sub] - } else if InputType::Integer.includes(input_types.first().unwrap()) { - vec![ - LocalSet(local!(F64)), - F64ConvertI64S, - LocalGet(local!(F64)), - F64Sub, - ] - } else { - vec![F64Sub] - } - } - operator_divide => vec![F64Div], - operator_multiply => { - if InputType::Integer.includes(input_types.first().unwrap()) - && InputType::Integer.includes(input_types.get(1).unwrap()) - { - vec![I64Mul] - } else if InputType::Integer.includes(input_types.get(1).unwrap()) { - vec![F64ConvertI64S, F64Mul] - } else if InputType::Integer.includes(input_types.first().unwrap()) { - vec![ - LocalSet(local!(F64)), - F64ConvertI64S, - LocalGet(local!(F64)), - F64Mul, - ] - } else { - vec![F64Mul] - } - } - operator_mod => { - if InputType::Integer.includes(input_types.first().unwrap()) - && InputType::Integer.includes(input_types.get(1).unwrap()) - { - vec![I64RemS] - } else if InputType::Integer.includes(input_types.get(1).unwrap()) { - vec![F64ConvertI64S, Call(func_indices::FMOD)] - } else if InputType::Integer.includes(input_types.first().unwrap()) { - vec![ - LocalSet(local!(F64)), - F64ConvertI64S, - LocalGet(local!(F64)), - Call(func_indices::FMOD), - ] - } else { - vec![Call(func_indices::FMOD)] - } - } - operator_round => { - if InputType::Integer.includes(input_types.first().unwrap()) { - vec![] - } else { - vec![F64Nearest, I64TruncF64S] - } - } - math_number { NUM } => { - vec![F64Const(*NUM)] - } - math_integer { NUM } - | math_angle { NUM } - | math_whole_number { NUM } - | math_positive_number { NUM } => { - vec![I64Const(*NUM as i64)] - } - boolean { BOOL } => vec![I32Const(*BOOL as i32)], - text { TEXT } => { - let str_idx: i32 = { - if let Some(idx) = string_consts.iter().position(|string| string == TEXT) { - idx - } else { - string_consts.push(TEXT.clone()); - string_consts.len() - 1 - } - } - .try_into() - .map_err(|_| make_hq_bug!("string index out of bounds"))?; - vec![I32Const(str_idx), TableGet(table_indices::STRINGS)] - } - data_variable { - VARIABLE, - assume_type, - } => { - let var_index = context - .vars - .borrow() - .iter() - .position(|var| VARIABLE == var.id()) - .ok_or(make_hq_bug!("couldn't find variable index"))?; - let var_offset: u64 = (usize::try_from(byte_offset::VARS) - .map_err(|_| make_hq_bug!("variable offset out of bounds"))? - + VAR_INFO_LEN as usize * var_index) - .try_into() - .map_err(|_| make_hq_bug!("variable offset out of bounds"))?; - if assume_type.is_none() { - vec![ - I32Const(0), - I32Load(MemArg { - offset: var_offset, - align: 2, - memory_index: 0, - }), - I32Const(0), - I64Load(MemArg { - offset: var_offset + 8, - align: 3, - memory_index: 0, - }), - ] - } else { - match assume_type - .as_ref() - .unwrap() - .least_restrictive_concrete_type() - { - InputType::Float => vec![ - I32Const(0), - F64Load(MemArg { - offset: var_offset + 8, - align: 3, - memory_index: 0, - }), - ], - InputType::ConcreteInteger => vec![ - I32Const(0), - I64Load(MemArg { - offset: var_offset + 8, - align: 3, - memory_index: 0, - }), - ], - InputType::Boolean => vec![ - I32Const(0), - I32Load(MemArg { - offset: var_offset + 8, - align: 2, - memory_index: 0, - }), // here we can load an i32 directly because a boolean will never be signed - ], - InputType::String => vec![GlobalGet( - BUILTIN_GLOBALS - + TryInto::::try_into(var_index) - .map_err(|_| make_hq_bug!("var index out of bounds"))?, - )], - other => hq_bug!("unexpected concrete type {:?}", other), - } - } - } - data_setvariableto { - VARIABLE, - assume_type, - } => { - let var_index = context - .vars - .borrow() - .iter() - .position(|var| VARIABLE == var.id()) - .ok_or(make_hq_bug!("couldn't find variable index"))?; - let var_offset: u64 = (usize::try_from(byte_offset::VARS) - .map_err(|_| make_hq_bug!("variable offset out of bounds"))? - + VAR_INFO_LEN as usize * var_index) - .try_into() - .map_err(|_| make_hq_bug!("variable offset out of bounds"))?; - if assume_type.is_none() { - vec![ - LocalSet(local!(I64)), - LocalSet(local!(I32)), - I32Const(0), - LocalGet(local!(I32)), - I32Store(MemArg { - offset: var_offset, - align: 2, - memory_index: 0, - }), - I32Const(0), - LocalGet(local!(I64)), - I64Store(MemArg { - offset: var_offset + 8, - align: 3, - memory_index: 0, - }), - ] - } else { - match assume_type - .as_ref() - .unwrap() - .least_restrictive_concrete_type() - { - InputType::Float => vec![ - LocalSet(local!(F64)), - I32Const(0), - LocalGet(local!(F64)), - F64Store(MemArg { - offset: var_offset + 8, - align: 3, - memory_index: 0, - }), - ], - InputType::Boolean => vec![ - LocalSet(local!(I32)), - I32Const(0), - LocalGet(local!(I32)), - I32Store(MemArg { - offset: var_offset + 8, - align: 2, - memory_index: 0, - }), - ], - InputType::ConcreteInteger => vec![ - LocalSet(local!(I64)), - I32Const(0), - LocalGet(local!(I64)), - I64Store(MemArg { - offset: var_offset + 8, - align: 3, - memory_index: 0, - }), - ], - InputType::String => vec![GlobalSet( - BUILTIN_GLOBALS - + TryInto::::try_into(var_index) - .map_err(|_| make_hq_bug!("var index out of bounds"))?, - )], - other => hq_bug!("unexpected concrete type {:?}", other), - } - } - } - data_teevariable { - VARIABLE, - assume_type, - } => { - let var_index = context - .vars - .borrow() - .iter() - .position(|var| VARIABLE == var.id()) - .ok_or(make_hq_bug!("couldn't find variable index"))?; - let var_offset: u64 = (usize::try_from(byte_offset::VARS) - .map_err(|_| make_hq_bug!("variable offset out of bounds"))? - + VAR_INFO_LEN as usize * var_index) - .try_into() - .map_err(|_| make_hq_bug!("variable offset out of bounds"))?; - if assume_type.is_none() { - vec![ - LocalSet(local!(I64)), - LocalSet(local!(I32)), - I32Const(0), - LocalGet(local!(I32)), - I32Store(MemArg { - offset: var_offset, - align: 2, - memory_index: 0, - }), - I32Const(0), - LocalGet(local!(I64)), - I64Store(MemArg { - offset: var_offset + 8, - align: 3, - memory_index: 0, - }), - LocalGet(local!(I32)), - LocalGet(local!(I64)), - ] - } else { - match assume_type - .as_ref() - .unwrap() - .least_restrictive_concrete_type() - { - InputType::Float => vec![ - LocalTee(local!(F64)), - I32Const(0), - LocalGet(local!(F64)), - F64Store(MemArg { - offset: var_offset + 8, - align: 3, - memory_index: 0, - }), - ], - InputType::Boolean => vec![ - LocalTee(local!(I32)), - I32Const(0), - LocalGet(local!(I32)), - I32Store(MemArg { - offset: var_offset + 8, - align: 2, - memory_index: 0, - }), - ], - InputType::ConcreteInteger => vec![ - LocalTee(local!(I64)), - I32Const(0), - LocalGet(local!(I64)), - I64Store(MemArg { - offset: var_offset + 8, - align: 3, - memory_index: 0, - }), - ], - InputType::String => vec![ - LocalTee(local!(EXTERNREF)), - GlobalSet( - BUILTIN_GLOBALS - + TryInto::::try_into(var_index) - .map_err(|_| make_hq_bug!("var index out of bounds"))?, - ), - LocalGet(local!(EXTERNREF)), - ], - other => hq_bug!("unexpected concrete type {:?}", other), - } - } - } - operator_lt => { - if InputType::Integer.includes(input_types.first().unwrap()) - && InputType::Integer.includes(input_types.get(1).unwrap()) - { - vec![I64LtS] - } else if InputType::Integer.includes(input_types.get(1).unwrap()) { - vec![F64ConvertI64S, F64Lt] - } else if InputType::Integer.includes(input_types.first().unwrap()) { - vec![ - LocalSet(local!(F64)), - F64ConvertI64S, - LocalGet(local!(F64)), - F64Lt, - ] - } else { - vec![F64Lt] - } - } - operator_gt => { - if InputType::Integer.includes(input_types.first().unwrap()) - && InputType::Integer.includes(input_types.get(1).unwrap()) - { - vec![I64GtS] - } else if InputType::Integer.includes(input_types.get(1).unwrap()) { - vec![F64ConvertI64S, F64Gt] - } else if InputType::Integer.includes(input_types.first().unwrap()) { - vec![ - LocalSet(local!(F64)), - F64ConvertI64S, - LocalGet(local!(F64)), - F64Gt, - ] - } else { - vec![F64Gt] - } - } - operator_and => vec![I32And], - operator_or => vec![I32Or], - operator_not => vec![I32Eqz], - operator_equals => match ( - input_types.first().unwrap().loosen_to([ - InputType::Integer, - InputType::Float, - InputType::String, - InputType::Unknown, - ])?, - input_types.get(1).unwrap().loosen_to([ - InputType::Integer, - InputType::Float, - InputType::String, - InputType::Unknown, - ])?, - ) { - (InputType::Integer, InputType::Integer) => vec![I64Eq], - (InputType::Float, InputType::Float) => vec![F64Eq], - (InputType::String, InputType::String) => vec![Call(func_indices::STRING_EQUALS)], - (InputType::Float, InputType::Integer) => vec![F64ConvertI64S, F64Eq], - (InputType::Integer, InputType::Float) => vec![ - LocalSet(local!(F64)), - F64ConvertI64S, - LocalGet(local!(F64)), - F64Eq, - ], - (InputType::String, InputType::Unknown) => vec![ - Call(func_indices::CAST_ANY_STRING), - Call(func_indices::STRING_EQUALS), - ], - (InputType::Unknown, InputType::String) => vec![ - LocalSet(local!(EXTERNREF)), - Call(func_indices::CAST_ANY_STRING), - LocalGet(local!(EXTERNREF)), - Call(func_indices::STRING_EQUALS), - ], - (a, b) => hq_todo!("({:?}, {:?}) input types for operator_equals", a, b), - }, - operator_random => vec![Call(func_indices::OPERATOR_RANDOM)], - operator_join => vec![Call(func_indices::OPERATOR_JOIN)], - operator_letter_of => vec![ - LocalSet(local!(EXTERNREF)), - I32WrapI64, - LocalGet(local!(EXTERNREF)), - Call(func_indices::OPERATOR_LETTEROF), - ], - operator_length => vec![Call(func_indices::OPERATOR_LENGTH), I64ExtendI32U], - operator_contains => vec![Call(func_indices::OPERATOR_CONTAINS)], - operator_mathop { OPERATOR } => match OPERATOR.as_str() { - "abs" => vec![F64Abs], - "floor" => { - if InputType::Integer.includes(input_types.first().unwrap()) { - vec![] - } else { - vec![F64Floor, I64TruncF64S] - } - } - "ceiling" => { - if InputType::Integer.includes(input_types.first().unwrap()) { - vec![] - } else { - vec![F64Ceil, I64TruncF64S] - } - } - "sqrt" => vec![F64Sqrt], - "sin" => vec![Call(func_indices::MATHOP_SIN)], - "cos" => vec![Call(func_indices::MATHOP_COS)], - "tan" => vec![Call(func_indices::MATHOP_TAN)], - "asin" => vec![Call(func_indices::MATHOP_ASIN)], - "acos" => vec![Call(func_indices::MATHOP_ACOS)], - "atan" => vec![Call(func_indices::MATHOP_ATAN)], - "ln" => vec![Call(func_indices::MATHOP_LN)], - "log" => vec![Call(func_indices::MATHOP_LOG)], - "e ^" => vec![Call(func_indices::MATHOP_POW_E)], - "10 ^" => vec![Call(func_indices::MATHOP_POW10)], - other => hq_bad_proj!("invalid OPERATOR field \"{}\"", other), - }, - sensing_timer => vec![Call(func_indices::SENSING_TIMER)], - sensing_resettimer => vec![Call(func_indices::SENSING_RESETTIMER)], - sensing_dayssince2000 => vec![Call(func_indices::SENSING_DAYSSINCE2000)], - pen_clear => vec![Call(func_indices::PEN_CLEAR)], - pen_stamp => hq_todo!(""), - pen_penDown => vec![ - I32Const(0), - I32Const(0), - I32Store8(MemArg { - offset: 0, - align: 0, - memory_index: 0, - }), - I32Const(0), - I32Const(1), - I32Store8(MemArg { - offset: (context.target_index - 1) as u64 * u64::try_from(SPRITE_INFO_LEN).unwrap() - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_DOWN).map_err(|_| make_hq_bug!(""))?, - align: 0, - memory_index: 0, - }), - I32Const(0), - F64Load(MemArg { - offset: (context.target_index - 1) as u64 * u64::try_from(SPRITE_INFO_LEN).unwrap() - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_SIZE).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - I32Const(0), - F64Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::X_POS).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - I32Const(0), - F64Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::Y_POS).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - I32Const(0), - F32Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_R).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - }), - I32Const(0), - F32Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_G).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - }), - I32Const(0), - F32Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_B).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - }), - I32Const(0), - F32Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_A).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - }), - Call(func_indices::PEN_DOWN), - ], - motion_gotoxy => vec![ - I32Const(0), - I32Const(0), - I32Store8(MemArg { - offset: 0, - align: 0, - memory_index: 0, - }), - LocalSet(local!(F64)), // y - LocalSet(local!(F64_2)), // x - I32Const(0), - I32Load8S(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_DOWN).map_err(|_| make_hq_bug!(""))?, - align: 0, - memory_index: 0, - }), - If(WasmBlockType::Empty), - I32Const(0), - F64Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_SIZE).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - I32Const(0), - F64Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::X_POS).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - I32Const(0), - F64Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::Y_POS).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - LocalGet(local!(F64_2)), - LocalGet(local!(F64)), - I32Const(0), - F32Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_R).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - }), - I32Const(0), - F32Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_G).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - }), - I32Const(0), - F32Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_B).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - }), - I32Const(0), - F32Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_A).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - }), - Call(func_indices::PEN_LINETO), - End, - I32Const(0), - LocalGet(local!(F64_2)), - F64Store(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::X_POS).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - I32Const(0), - LocalGet(local!(F64)), - F64Store(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::Y_POS).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!(""))?, - ), - Call(func_indices::EMIT_SPRITE_POS_CHANGE), - ], - pen_penUp => vec![ - I32Const(0), - I32Const(0), - I32Store8(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_DOWN).map_err(|_| make_hq_bug!(""))?, - align: 0, - memory_index: 0, - }), - ], - pen_setPenColorToColor => vec![ - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!("target index out of bounds"))?, - ), - Call(func_indices::PEN_SETCOLOR), - ], - pen_changePenColorParamBy => vec![ - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!("target index out of bounds"))?, - ), - Call(func_indices::PEN_CHANGECOLORPARAM), - ], - pen_setPenColorParamTo => vec![ - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!("target index out of bounds"))?, - ), - Call(func_indices::PEN_SETCOLORPARAM), - ], - pen_changePenSizeBy => vec![ - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!("target index out of bounds"))?, - ), - Call(func_indices::PEN_CHANGESIZE), - ], - pen_setPenSizeTo => vec![ - LocalSet(local!(F64)), - I32Const(0), - LocalGet(local!(F64)), - F64Store(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::PEN_SIZE).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - ], - pen_setPenShadeToNumber => hq_todo!(""), - pen_changePenShadeBy => hq_todo!(""), - pen_setPenHueToNumber => vec![ - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!("target index out of bounds"))?, - ), - Call(func_indices::PEN_SETHUE), - ], - pen_changePenHueBy => vec![ - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!("target index out of bounds"))?, - ), - Call(func_indices::PEN_CHANGEHUE), - ], - looks_size => vec![ - I32Const(0), - F64Load(MemArg { - offset: (context.target_index - 1) as u64 * u64::try_from(SPRITE_INFO_LEN).unwrap() - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::SIZE).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - ], - looks_setsizeto => vec![ - I32Const(0), - I32Const(0), - I32Store8(MemArg { - offset: 0, - align: 0, - memory_index: 0, - }), - LocalSet(local!(F64)), - I32Const(0), - LocalGet(local!(F64)), - F64Store(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::SIZE).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!(""))?, - ), - Call(func_indices::EMIT_SPRITE_SIZE_CHANGE), - ], - motion_turnleft => vec![ - I32Const(0), - I32Const(0), - I32Store8(MemArg { - offset: 0, - align: 0, - memory_index: 0, - }), - LocalSet(local!(F64)), - I32Const(0), - F64Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::ROTATION).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - LocalGet(local!(F64)), - F64Sub, - LocalSet(local!(F64)), - I32Const(0), - LocalGet(local!(F64)), - F64Store(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::ROTATION).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!(""))?, - ), - Call(func_indices::EMIT_SPRITE_ROTATION_CHANGE), - ], - motion_turnright => vec![ - I32Const(0), - I32Const(0), - I32Store8(MemArg { - offset: 0, - align: 0, - memory_index: 0, - }), - LocalSet(local!(F64)), - I32Const(0), - F64Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::ROTATION).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - LocalGet(local!(F64)), - F64Add, - LocalSet(local!(F64)), - I32Const(0), - LocalGet(local!(F64)), - F64Store(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::ROTATION).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!(""))?, - ), - Call(func_indices::EMIT_SPRITE_ROTATION_CHANGE), - ], - looks_changesizeby => vec![ - I32Const(0), - I32Const(0), - I32Store8(MemArg { - offset: 0, - align: 0, - memory_index: 0, - }), - LocalSet(local!(F64)), - I32Const(0), - F64Load(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::SIZE).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - LocalGet(local!(F64)), - F64Add, - LocalSet(local!(F64)), - I32Const(0), - LocalGet(local!(F64)), - F64Store(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::SIZE).map_err(|_| make_hq_bug!(""))?, - align: 3, - memory_index: 0, - }), - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!(""))?, - ), - Call(func_indices::EMIT_SPRITE_SIZE_CHANGE), - ], - looks_switchcostumeto => vec![ - I32Const(0), - I32Const(0), - I32Store8(MemArg { - offset: 0, - align: 0, - memory_index: 0, - }), - LocalSet(local!(I64)), - I32Const(0), - LocalGet(local!(I64)), - I32WrapI64, - I32Store(MemArg { - offset: (context.target_index - 1) as u64 - * u64::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - + u64::try_from(byte_offset::VARS).map_err(|_| make_hq_bug!(""))? - + u64::try_from(context.vars.borrow().len()).map_err(|_| make_hq_bug!(""))? - * VAR_INFO_LEN - + u64::try_from(sprite_info_offsets::COSTUME).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - }), - I32Const( - context - .target_index - .try_into() - .map_err(|_| make_hq_bug!(""))?, - ), - Call(func_indices::EMIT_SPRITE_COSTUME_CHANGE), - ], - hq_drop(n) => vec![Drop; 2 * *n], - hq_goto { step: None, .. } => { - let threads_offset: i32 = (byte_offset::VARS as usize - + VAR_INFO_LEN as usize * context.vars.borrow().len() - + usize::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - * (context.target_num - 1)) - .try_into() - .map_err(|_| make_hq_bug!("thread_offset out of bounds"))?; - vec![ - LocalGet(0), - I32Const(threads_offset), - I32Add, // destination (= current thread pos in memory) - LocalGet(0), - I32Const(threads_offset + 4), - I32Add, // source (= current thread pos + 4) - I32Const(0), - I32Load(MemArg { - offset: byte_offset::THREAD_NUM - .try_into() - .map_err(|_| make_hq_bug!("THREAD_NUM out of bounds"))?, - align: 2, - memory_index: 0, - }), - I32Const(4), - I32Mul, - LocalGet(0), - I32Sub, // length (threadnum * 4 - current thread pos) - MemoryCopy { - src_mem: 0, - dst_mem: 0, - }, - I32Const(0), - I32Const(0), - I32Load(MemArg { - offset: byte_offset::THREAD_NUM - .try_into() - .map_err(|_| make_hq_bug!("THREAD_NUM out of bounds"))?, - align: 2, - memory_index: 0, - }), - I32Const(1), - I32Sub, - I32Store(MemArg { - offset: byte_offset::THREAD_NUM - .try_into() - .map_err(|_| make_hq_bug!("THREAD_NUM out of bounds"))?, - align: 2, - memory_index: 0, - }), - I32Const(0), - Return, - ] - } - hq_goto { - step: Some(next_step_id), - does_yield: true, - } => { - let next_step_index = steps.get_index_of(next_step_id).ok_or(make_hq_bug!(""))?; - let threads_offset: u64 = (byte_offset::VARS as usize - + VAR_INFO_LEN as usize * context.vars.borrow().len() - + usize::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - * (context.target_num - 1)) - .try_into() - .map_err(|_| make_hq_bug!("threads_offset length out of bounds"))?; - vec![ - LocalGet(0), - I32Const( - next_step_index - .try_into() - .map_err(|_| make_hq_bug!("step index out of bounds"))?, - ), - I32Store(MemArg { - offset: threads_offset, - align: 2, - memory_index: 0, - }), - I32Const(1), - Return, - ] - } - hq_goto { - step: Some(next_step_id), - does_yield: false, - } => { - let next_step_index = steps.get_index_of(next_step_id).ok_or(make_hq_bug!(""))?; - vec![ - LocalGet(local!(MEM_LOCATION)), - ReturnCall( - BUILTIN_FUNCS - + u32::try_from(next_step_index) - .map_err(|_| make_hq_bug!("next_step_index out of bounds"))?, - ), - ] - } - hq_goto_if { step: None, .. } => { - let threads_offset: i32 = (byte_offset::VARS as usize - + VAR_INFO_LEN as usize * context.vars.borrow().len() - + usize::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - * (context.target_num - 1)) - .try_into() - .map_err(|_| make_hq_bug!("thread_offset out of bounds"))?; - vec![ - If(WasmBlockType::Empty), - LocalGet(0), - I32Const(threads_offset), - I32Add, // destination (= current thread pos in memory) - LocalGet(0), - I32Const(threads_offset + 4), - I32Add, // source (= current thread pos + 4) - I32Const(0), - I32Load(MemArg { - offset: byte_offset::THREAD_NUM - .try_into() - .map_err(|_| make_hq_bug!("THREAD_NUM out of bounds"))?, - align: 2, - memory_index: 0, - }), - I32Const(4), - I32Mul, - LocalGet(0), - I32Sub, // length (threadnum * 4 - current thread pos) - MemoryCopy { - src_mem: 0, - dst_mem: 0, - }, - I32Const(0), - I32Const(0), - I32Load(MemArg { - offset: byte_offset::THREAD_NUM - .try_into() - .map_err(|_| make_hq_bug!("THREAD_NUM out of bounds"))?, - align: 2, - memory_index: 0, - }), - I32Const(1), - I32Sub, - I32Store(MemArg { - offset: byte_offset::THREAD_NUM - .try_into() - .map_err(|_| make_hq_bug!("THREAD_NUM out of bounds"))?, - align: 2, - memory_index: 0, - }), - I32Const(0), - Return, - End, - ] - } - hq_goto_if { - step: Some(next_step_id), - does_yield: true, - } => { - let next_step_index = steps.get_index_of(next_step_id).ok_or(make_hq_bug!(""))?; - let threads_offset: u64 = (byte_offset::VARS as usize - + VAR_INFO_LEN as usize * context.vars.borrow().len() - + usize::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - * (context.target_num - 1)) - .try_into() - .map_err(|_| make_hq_bug!("threads_offset length out of bounds"))?; - vec![ - If(WasmBlockType::Empty), - LocalGet(0), - I32Const( - next_step_index - .try_into() - .map_err(|_| make_hq_bug!("step index out of bounds"))?, - ), - I32Store(MemArg { - offset: threads_offset, - align: 2, - memory_index: 0, - }), - I32Const(1), - Return, - End, - ] - } - hq_goto_if { - step: Some(next_step_id), - does_yield: false, - } => { - let next_step_index = steps.get_index_of(next_step_id).ok_or(make_hq_bug!(""))?; - vec![ - If(WasmBlockType::Empty), - LocalGet(local!(MEM_LOCATION)), - ReturnCall( - BUILTIN_FUNCS - + u32::try_from(next_step_index) - .map_err(|_| make_hq_bug!("next_step_index out of bounds"))?, - ), - End, - ] - } - hq_cast(from, to) => match (from.clone(), to.least_restrictive_concrete_type()) { - // cast from type should always be a concrete type - (String, Float) => vec![Call(func_indices::CAST_PRIMITIVE_STRING_FLOAT)], - (String, Boolean) => vec![Call(func_indices::CAST_PRIMITIVE_STRING_BOOL)], - (String, Unknown) => vec![ - LocalSet(local!(EXTERNREF)), - I32Const(hq_value_types::EXTERN_STRING_REF64), - LocalGet(local!(EXTERNREF)), - Call(func_indices::TABLE_ADD_STRING), - I64ExtendI32U, - ], - (Boolean, Float) => vec![Call(func_indices::CAST_BOOL_FLOAT)], - (Boolean, String) => vec![Call(func_indices::CAST_BOOL_STRING)], - (Boolean, Unknown) => vec![ - I64ExtendI32S, - LocalSet(local!(I64)), - I32Const(hq_value_types::BOOL64), - LocalGet(local!(I64)), - ], - (Float, String) => vec![Call(func_indices::CAST_PRIMITIVE_FLOAT_STRING)], - (Float, Boolean) => vec![Call(func_indices::CAST_FLOAT_BOOL)], - (Float, Unknown) => vec![ - LocalSet(local!(F64)), - I32Const(hq_value_types::FLOAT64), - LocalGet(local!(F64)), - I64ReinterpretF64, - ], - (ConcreteInteger, Unknown) => vec![ - //I64ExtendI32S, - LocalSet(local!(I64)), - I32Const(hq_value_types::INT64), - LocalGet(local!(I64)), - ], - (Unknown, String) => vec![Call(func_indices::CAST_ANY_STRING)], - (Unknown, Float) => vec![Call(func_indices::CAST_ANY_FLOAT)], - (Unknown, Boolean) => vec![Call(func_indices::CAST_ANY_BOOL)], - (Unknown, ConcreteInteger) => vec![Call(func_indices::CAST_ANY_INT)], - _ => hq_todo!("unimplemented cast: {:?} -> {:?} at {:?}", from, to, op), - }, - hq_launch_procedure(procedure) => { - let step_tuple = (procedure.target_id.clone(), procedure.first_step.clone()); - let step_idx = steps - .get_index_of(&step_tuple) - .ok_or(make_hq_bug!("couldn't find step"))?; - if procedure.warp { - vec![ - LocalGet(local!(MEM_LOCATION)), - Call( - BUILTIN_FUNCS - + u32::try_from(step_idx) - .map_err(|_| make_hq_bug!("step_idx out of bounds"))?, - ), - ] - } else { - hq_todo!("non-warping procedure") - } - } - other => hq_todo!("missing WASM impl for {:?}", other), - }; - if op.does_request_redraw() - && !context.proc.clone().is_some_and(|p| p.warp) - && !(*op.opcode() == looks_say && context.dbg) - { - instructions.append(&mut vec![ - I32Const(byte_offset::REDRAW_REQUESTED), - I32Const(1), - I32Store8(MemArg { - offset: 0, - align: 0, - memory_index: 0, - }), - ]); - } - Ok(instructions) -} - -pub trait CompileToWasm { - fn compile_wasm( - &self, - step_funcs: &mut IndexMap< - Option<(String, String)>, - Function, - BuildHasherDefault, - >, - string_consts: &mut Vec, - steps: &IndexMap<(String, String), Step, BuildHasherDefault>, - ) -> Result; -} - -impl CompileToWasm for (&(String, String), &Step) { - fn compile_wasm( - &self, - step_funcs: &mut IndexMap< - Option<(String, String)>, - Function, - BuildHasherDefault, - >, - string_consts: &mut Vec, - steps: &IndexMap<(String, String), Step, BuildHasherDefault>, - ) -> Result { - if step_funcs.contains_key(&Some(self.0.clone())) { - return u32::try_from( - step_funcs - .get_index_of(&Some(self.0.clone())) - .ok_or(make_hq_bug!(""))?, - ) - .map_err(|_| make_hq_bug!("IndexMap index out of bounds")); - } - let locals = vec![ - ValType::Ref(RefType::EXTERNREF), - ValType::F64, - ValType::I64, - ValType::I32, - ValType::I32, - ValType::F64, - ]; - let mut func = Function::new_with_locals_types(locals); - for (i, op) in self.1.opcodes().iter().enumerate() { - let arity = op.opcode().expected_inputs()?.len(); - let input_types = (0..arity) - .map(|j| { - Ok(self - .1 - .opcodes() - .get(i - 1) - .ok_or(make_hq_bug!(""))? - //.unwrap() - .type_stack - .get(arity - 1 - j) - .borrow() - .clone() - .ok_or(make_hq_bug!("{:?}", op.opcode()))? - //.unwrap() - .1) - }) - .collect::, _>>()?; - let instrs = instructions(op, self.1.context(), string_consts, steps, input_types)?; - for instr in instrs { - func.instruction(&instr); - } - } - func.instruction(&Instruction::End); - step_funcs.insert(Some(self.0.clone()), func); - u32::try_from(step_funcs.len() - 1) - .map_err(|_| make_hq_bug!("step_funcs length out of bounds")) - } -} - -#[wasm_bindgen(getter_with_clone)] -pub struct WasmProject { - pub wasm_bytes: Vec, - pub string_consts: Vec, - pub target_names: Vec, -} -/* -#[wasm_bindgen] -impl WasmProject { - pub fn wasm_bytes(&self) -> &Vec { - &self.wasm_bytes - } - pub fn string_consts(&self) -> &Vec { - &self.string_consts - } - pub fn target_names(&self) -> &Vec { - &self.target_names - } -}*/ - -pub mod step_func_locals { - pub const MEM_LOCATION: u32 = 0; - pub const EXTERNREF: u32 = 1; - pub const F64: u32 = 2; - pub const I64: u32 = 3; - pub const I32: u32 = 4; - pub const I32_2: u32 = 5; - pub const F64_2: u32 = 6; -} - -pub mod func_indices { - /* imported funcs */ - pub const DBG_LOG: u32 = 0; - pub const DBG_ASSERT: u32 = 1; - pub const LOOKS_SAY: u32 = 2; - pub const LOOKS_THINK: u32 = 3; - pub const CAST_PRIMITIVE_FLOAT_STRING: u32 = 4; // js functions can only return 1 value so need wrapper functions for casting - pub const CAST_PRIMITIVE_STRING_FLOAT: u32 = 5; - pub const CAST_PRIMITIVE_STRING_BOOL: u32 = 6; - pub const DBG_LOGI32: u32 = 7; - pub const STRING_EQUALS: u32 = 8; - pub const OPERATOR_RANDOM: u32 = 9; - pub const OPERATOR_JOIN: u32 = 10; - pub const OPERATOR_LETTEROF: u32 = 11; - pub const OPERATOR_LENGTH: u32 = 12; - pub const OPERATOR_CONTAINS: u32 = 13; - pub const MATHOP_SIN: u32 = 14; - pub const MATHOP_COS: u32 = 15; - pub const MATHOP_TAN: u32 = 16; - pub const MATHOP_ASIN: u32 = 17; - pub const MATHOP_ACOS: u32 = 18; - pub const MATHOP_ATAN: u32 = 19; - pub const MATHOP_LN: u32 = 20; - pub const MATHOP_LOG: u32 = 21; - pub const MATHOP_POW_E: u32 = 22; - pub const MATHOP_POW10: u32 = 23; - pub const SENSING_TIMER: u32 = 24; - pub const SENSING_RESETTIMER: u32 = 25; - pub const SENSING_DAYSSINCE2000: u32 = 26; - pub const PEN_CLEAR: u32 = 27; - pub const PEN_DOWN: u32 = 28; - pub const PEN_LINETO: u32 = 29; - pub const PEN_SETCOLOR: u32 = 30; - pub const PEN_CHANGECOLORPARAM: u32 = 31; - pub const PEN_SETCOLORPARAM: u32 = 32; - pub const PEN_CHANGESIZE: u32 = 33; - pub const PEN_SETHUE: u32 = 34; - pub const PEN_CHANGEHUE: u32 = 35; - pub const EMIT_SPRITE_POS_CHANGE: u32 = 36; - pub const EMIT_SPRITE_SIZE_CHANGE: u32 = 37; - pub const EMIT_SPRITE_COSTUME_CHANGE: u32 = 38; - pub const EMIT_SPRITE_X_CHANGE: u32 = 39; - pub const EMIT_SPRITE_Y_CHANGE: u32 = 40; - pub const EMIT_SPRITE_ROTATION_CHANGE: u32 = 41; - pub const EMIT_SPRITE_VISIBILITY_CHANGE: u32 = 42; - - /* wasm funcs */ - pub const UNREACHABLE: u32 = 43; - pub const FMOD: u32 = 44; - pub const CAST_FLOAT_BOOL: u32 = 45; - pub const CAST_BOOL_FLOAT: u32 = 46; - pub const CAST_BOOL_STRING: u32 = 47; - pub const CAST_ANY_STRING: u32 = 48; - pub const CAST_ANY_FLOAT: u32 = 49; - pub const CAST_ANY_BOOL: u32 = 50; - pub const CAST_ANY_INT: u32 = 51; - pub const TABLE_ADD_STRING: u32 = 52; - pub const SPRITE_UPDATE_PEN_COLOR: u32 = 53; -} -pub const BUILTIN_FUNCS: u32 = 54; -pub const IMPORTED_FUNCS: u32 = 43; - -pub mod types { - #![allow(non_upper_case_globals)] - pub const F64_NORESULT: u32 = 0; - pub const NOPARAM_NORESULT: u32 = 1; - pub const F64x2_F64: u32 = 2; - pub const F64I32_NORESULT: u32 = 3; - pub const I32_NORESULT: u32 = 4; - pub const I32_I32: u32 = 5; - pub const I32_F64: u32 = 6; - pub const F64_I32: u32 = 7; - pub const F64_EXTERNREF: u32 = 8; - pub const EXTERNREF_F64: u32 = 9; - pub const EXTERNREF_I32: u32 = 10; - pub const I32x2_I32x2: u32 = 11; - pub const I32I64_I32F64: u32 = 12; - pub const I32F64_I32I64: u32 = 13; - pub const NOPARAM_I32I64: u32 = 14; - pub const I32I64_I32I64: u32 = 15; - pub const NOPARAM_I32F64: u32 = 16; - pub const F64_I64: u32 = 17; - pub const I64_F64: u32 = 18; - pub const I64_I64: u32 = 19; - pub const I32I64_I64: u32 = 20; - pub const I32I64_F64: u32 = 21; - pub const F64_I32I64: u32 = 22; - pub const NOPARAM_I64: u32 = 23; - pub const NOPARAM_F64: u32 = 24; - pub const I32I64_EXTERNREF: u32 = 25; - pub const NOPARAM_EXTERNREF: u32 = 26; - pub const I64_EXTERNREF: u32 = 27; - pub const I32I64I32_NORESULT: u32 = 28; - pub const I32I64_NORESULT: u32 = 29; - pub const I32I64I32I64_I32: u32 = 30; - pub const F64I32I64_EXTERNREF: u32 = 31; - pub const I32I64I32I64_EXTERNREF: u32 = 32; - pub const F64_F64: u32 = 33; - pub const NOPARAM_I32: u32 = 34; - pub const I32x2_NORESULT: u32 = 35; - pub const EXTERNREFF64I32_NORESULT: u32 = 36; - pub const F64x3F32x4_NORESULT: u32 = 37; - pub const F64x5F32x4_NORESULT: u32 = 38; - pub const EXTERNREFx2_EXTERNREF: u32 = 39; - pub const EXTERNREFx2_I32: u32 = 40; - pub const F64EXTERNREF_EXTERNREF: u32 = 41; - pub const I32EXTERNREF_EXTERNREF: u32 = 42; - pub const I32_EXTERNREF: u32 = 43; - pub const I32I64_I32: u32 = 44; - pub const EXTERNREFx2_REFEXTERN: u32 = 45; -} - -pub mod table_indices { - pub const STEP_FUNCS: u32 = 0; - pub const STRINGS: u32 = 1; -} - -pub mod hq_value_types { - pub const FLOAT64: i32 = 0; - pub const BOOL64: i32 = 1; - pub const EXTERN_STRING_REF64: i32 = 2; - pub const INT64: i32 = 3; -} - -// the number of bytes that one step takes up in linear memory -pub const THREAD_BYTE_LEN: i32 = 4; - -pub mod byte_offset { - pub const REDRAW_REQUESTED: i32 = 0; - pub const THREAD_NUM: i32 = 4; - pub const VARS: i32 = 8; -} - -pub const SPRITE_INFO_LEN: i32 = 80; - -pub mod sprite_info_offsets { - pub const X_POS: i32 = 0; - pub const Y_POS: i32 = 8; - pub const PEN_COLOR: i32 = 16; - pub const PEN_SATURATION: i32 = 20; - pub const PEN_VALUE: i32 = 24; - pub const PEN_TRANSPARENCY: i32 = 28; - pub const PEN_R: i32 = 32; - pub const PEN_G: i32 = 36; - pub const PEN_B: i32 = 40; - pub const PEN_A: i32 = 44; - pub const PEN_SIZE: i32 = 48; - pub const PEN_DOWN: i32 = 56; - pub const VISIBLE: i32 = 57; - //pub const RESERVED1: i32 = 58; - pub const COSTUME: i32 = 60; - pub const SIZE: i32 = 64; - pub const ROTATION: i32 = 72; -} - -pub const VAR_INFO_LEN: u64 = 16; - -pub mod var_info_offsets { - pub const VAR_TYPE: i32 = 0; - pub const VAR_VAL: i32 = 8; -} - -pub const BUILTIN_GLOBALS: u32 = 3; - -impl TryFrom for WasmProject { - type Error = HQError; - - fn try_from(project: IrProject) -> Result { - let mut module = Module::new(); - - let mut imports = ImportSection::new(); - let mut functions = FunctionSection::new(); - let mut types = TypeSection::new(); - let mut code = CodeSection::new(); - let mut exports = ExportSection::new(); - let mut tables = TableSection::new(); - let mut elements = ElementSection::new(); - let mut memories = MemorySection::new(); - let mut globals = GlobalSection::new(); - - memories.memory(MemoryType { - minimum: 1, - maximum: None, - memory64: false, - shared: false, - page_size_log2: None, - }); - - types.function([ValType::F64], []); - types.function([], []); - types.function([ValType::F64, ValType::F64], [ValType::F64]); - types.function([ValType::F64, ValType::I32], []); - types.function([ValType::I32], []); - types.function([ValType::I32], [ValType::I32]); - types.function([ValType::I32], [ValType::F64]); - types.function([ValType::F64], [ValType::I32]); - types.function([ValType::F64], [ValType::Ref(RefType::EXTERNREF)]); - types.function([ValType::Ref(RefType::EXTERNREF)], [ValType::F64]); - types.function([ValType::Ref(RefType::EXTERNREF)], [ValType::I32]); - types.function([ValType::I32, ValType::I32], [ValType::I32, ValType::I32]); - types.function([ValType::I32, ValType::I64], [ValType::I32, ValType::F64]); - types.function([ValType::I32, ValType::F64], [ValType::I32, ValType::I64]); - types.function([], [ValType::I32, ValType::I64]); - types.function([ValType::I32, ValType::I64], [ValType::I32, ValType::I64]); - types.function([], [ValType::I32, ValType::F64]); - types.function([ValType::F64], [ValType::I64]); - types.function([ValType::I64], [ValType::F64]); - types.function([ValType::I64], [ValType::I64]); - types.function([ValType::I32, ValType::I64], [ValType::I64]); - types.function([ValType::I32, ValType::I64], [ValType::F64]); - types.function([ValType::F64], [ValType::I32, ValType::I64]); - types.function([], [ValType::I64]); - types.function([], [ValType::F64]); - types.function( - [ValType::I32, ValType::I64], - [ValType::Ref(RefType::EXTERNREF)], - ); - types.function([], [ValType::Ref(RefType::EXTERNREF)]); - types.function([ValType::I64], [ValType::Ref(RefType::EXTERNREF)]); - types.function([ValType::I32, ValType::I64, ValType::I32], []); - types.function([ValType::I32, ValType::I64], []); - types.function( - [ValType::I32, ValType::I64, ValType::I32, ValType::I64], - [ValType::I32], - ); - types.function( - [ValType::F64, ValType::I32, ValType::I64], - [ValType::Ref(RefType::EXTERNREF)], - ); - types.function( - [ValType::I32, ValType::I64, ValType::I32, ValType::I64], - [ValType::Ref(RefType::EXTERNREF)], - ); - types.function([ValType::F64], [ValType::F64]); - types.function([], [ValType::I32]); - types.function([ValType::I32, ValType::I32], []); - types.function( - [ValType::Ref(RefType::EXTERNREF), ValType::F64, ValType::I32], - [], - ); - types.function( - [ - ValType::F64, - ValType::F64, - ValType::F64, - ValType::F32, - ValType::F32, - ValType::F32, - ValType::F32, - ], - [], - ); - types.function( - [ - ValType::F64, - ValType::F64, - ValType::F64, - ValType::F64, - ValType::F64, - ValType::F32, - ValType::F32, - ValType::F32, - ValType::F32, - ], - [], - ); - types.function( - [ - ValType::Ref(RefType::EXTERNREF), - ValType::Ref(RefType::EXTERNREF), - ], - [ValType::Ref(RefType::EXTERNREF)], - ); - types.function( - [ - ValType::Ref(RefType::EXTERNREF), - ValType::Ref(RefType::EXTERNREF), - ], - [ValType::I32], - ); - types.function( - [ValType::F64, ValType::Ref(RefType::EXTERNREF)], - [ValType::Ref(RefType::EXTERNREF)], - ); - types.function( - [ValType::I32, ValType::Ref(RefType::EXTERNREF)], - [ValType::Ref(RefType::EXTERNREF)], - ); - types.function([ValType::I32], [ValType::Ref(RefType::EXTERNREF)]); - types.function([ValType::I32, ValType::I64], [ValType::I32]); - types.function( - [ - ValType::Ref(RefType::EXTERNREF), - ValType::Ref(RefType::EXTERNREF), - ], - [ValType::Ref(RefType { - nullable: false, - heap_type: HeapType::Abstract { - shared: false, // what does this even mean?! - ty: AbstractHeapType::Extern, - }, - })], - ); - - imports.import("dbg", "log", EntityType::Function(types::I32I64_NORESULT)); - imports.import( - "dbg", - "assert", - EntityType::Function(types::I32I64_NORESULT), - ); - imports.import( - "runtime", - "looks_say", - EntityType::Function(types::I32I64I32_NORESULT), - ); - imports.import( - "runtime", - "looks_think", - EntityType::Function(types::I32I64I32_NORESULT), - ); - imports.import( - "cast", - "floattostring", - EntityType::Function(types::F64_EXTERNREF), - ); - imports.import( - "cast", - "stringtofloat", - EntityType::Function(types::EXTERNREF_F64), - ); - imports.import( - "cast", - "stringtobool", - EntityType::Function(types::EXTERNREF_I32), - ); - imports.import("dbg", "logi32", EntityType::Function(types::I32_I32)); - imports.import( - "wasm:js-string", - "equals", - EntityType::Function(types::EXTERNREFx2_I32), - ); - imports.import( - "runtime", - "operator_random", - EntityType::Function(types::F64x2_F64), - ); - imports.import( - "wasm:js-string", - "concat", - EntityType::Function(types::EXTERNREFx2_REFEXTERN), - ); - imports.import( - "runtime", - "operator_letterof", - EntityType::Function(types::I32EXTERNREF_EXTERNREF), - ); - imports.import( - "wasm:js-string", - "length", - EntityType::Function(types::EXTERNREF_I32), - ); - imports.import( - "runtime", - "operator_contains", - EntityType::Function(types::EXTERNREFx2_I32), - ); - imports.import( - "runtime", - "mathop_sin", - EntityType::Function(types::F64_F64), - ); - imports.import( - "runtime", - "mathop_cos", - EntityType::Function(types::F64_F64), - ); - imports.import( - "runtime", - "mathop_tan", - EntityType::Function(types::F64_F64), - ); - imports.import( - "runtime", - "mathop_asin", - EntityType::Function(types::F64_F64), - ); - imports.import( - "runtime", - "mathop_acos", - EntityType::Function(types::F64_F64), - ); - imports.import( - "runtime", - "mathop_atan", - EntityType::Function(types::F64_F64), - ); - imports.import("runtime", "mathop_ln", EntityType::Function(types::F64_F64)); - imports.import( - "runtime", - "mathop_log", - EntityType::Function(types::F64_F64), - ); - imports.import( - "runtime", - "mathop_pow_e", - EntityType::Function(types::F64_F64), - ); - imports.import( - "runtime", - "mathop_pow10", - EntityType::Function(types::F64_F64), - ); - imports.import( - "runtime", - "sensing_timer", - EntityType::Function(types::NOPARAM_F64), - ); - imports.import( - "runtime", - "sensing_resettimer", - EntityType::Function(types::NOPARAM_NORESULT), - ); - imports.import( - "runtime", - "sensing_dayssince2000", - EntityType::Function(types::NOPARAM_F64), - ); - imports.import( - "runtime", - "pen_clear", - EntityType::Function(types::NOPARAM_NORESULT), - ); - imports.import( - "runtime", - "pen_down", - EntityType::Function(types::F64x3F32x4_NORESULT), - ); - imports.import( - "runtime", - "pen_lineto", - EntityType::Function(types::F64x5F32x4_NORESULT), - ); - imports.import( - "runtime", - "pen_setcolor", - EntityType::Function(types::I32x2_NORESULT), // todo: decide how to pass around colours - numbers (i32 or i64?) or strings? needs a new type or shares an integer type (needs generic monomorphisation) - ); - imports.import( - "runtime", - "pen_changecolorparam", - EntityType::Function(types::EXTERNREFF64I32_NORESULT), - ); - imports.import( - "runtime", - "pen_setcolorparam", - EntityType::Function(types::EXTERNREFF64I32_NORESULT), - ); - imports.import( - "runtime", - "pen_changesize", - EntityType::Function(types::F64I32_NORESULT), - ); - imports.import( - "runtime", - "pen_changehue", - EntityType::Function(types::F64I32_NORESULT), - ); - imports.import( - "runtime", - "pen_sethue", - EntityType::Function(types::F64I32_NORESULT), - ); - imports.import( - "runtime", - "emit_sprite_pos_change", - EntityType::Function(types::I32_NORESULT), - ); - imports.import( - "runtime", - "emit_sprite_size_change", - EntityType::Function(types::I32_NORESULT), - ); - imports.import( - "runtime", - "emit_sprite_costume_change", - EntityType::Function(types::I32_NORESULT), - ); - imports.import( - "runtime", - "emit_sprite_x_change", - EntityType::Function(types::I32_NORESULT), - ); - imports.import( - "runtime", - "emit_sprite_y_change", - EntityType::Function(types::I32_NORESULT), - ); - imports.import( - "runtime", - "emit_sprite_rotation_change", - EntityType::Function(types::I32_NORESULT), - ); - imports.import( - "runtime", - "emit_sprite_visibility_change", - EntityType::Function(types::I32_NORESULT), - ); - - // used to expose the wasm module to devtools - functions.function(types::NOPARAM_NORESULT); - let mut unreachable_func = Function::new(vec![]); - unreachable_func.instruction(&Instruction::Unreachable); - unreachable_func.instruction(&Instruction::End); - code.function(&unreachable_func); - exports.export( - "unreachable_dbg", - ExportKind::Func, - func_indices::UNREACHABLE, - ); - - functions.function(types::F64x2_F64); - let mut fmod_func = Function::new(vec![]); - // (a, b) => a - (truncate(a / b)) * b) + (b if a/b < 0 else 0) - fmod_func.instruction(&Instruction::LocalGet(0)); // a - fmod_func.instruction(&Instruction::LocalGet(0)); // a - fmod_func.instruction(&Instruction::LocalGet(1)); // b - fmod_func.instruction(&Instruction::F64Div); // a / b - fmod_func.instruction(&Instruction::F64Trunc); // truncate(a / b) - fmod_func.instruction(&Instruction::LocalGet(1)); // b - fmod_func.instruction(&Instruction::F64Mul); // truncate(a / b) * b - fmod_func.instruction(&Instruction::F64Sub); // a - (truncate(a / b) * b) - fmod_func.instruction(&Instruction::LocalGet(1)); // b - fmod_func.instruction(&Instruction::F64Const(0.0)); // 0 - fmod_func.instruction(&Instruction::LocalGet(0)); // a - fmod_func.instruction(&Instruction::LocalGet(1)); // b - fmod_func.instruction(&Instruction::F64Div); // a / b - fmod_func.instruction(&Instruction::F64Const(0.0)); // 0 - fmod_func.instruction(&Instruction::F64Lt); // a / b < 0 - fmod_func.instruction(&Instruction::Select); // b if a / b < 0 else 0 - fmod_func.instruction(&Instruction::F64Add); // a - (truncate(a / b)) * b) + (b if a/b < 0 else 0) - fmod_func.instruction(&Instruction::End); - code.function(&fmod_func); - - functions.function(types::F64_I32); - let mut float2bool_func = Function::new(vec![]); - float2bool_func.instruction(&Instruction::LocalGet(0)); - float2bool_func.instruction(&Instruction::F64Abs); - float2bool_func.instruction(&Instruction::F64Const(0.0)); - float2bool_func.instruction(&Instruction::F64Eq); - float2bool_func.instruction(&Instruction::End); - code.function(&float2bool_func); - - functions.function(types::I32_F64); - let mut bool2float_func = Function::new(vec![]); - bool2float_func.instruction(&Instruction::LocalGet(0)); - bool2float_func.instruction(&Instruction::F64ConvertI32S); - bool2float_func.instruction(&Instruction::End); - code.function(&bool2float_func); - - functions.function(types::I32_EXTERNREF); - let mut bool2string_func = Function::new(vec![]); - bool2string_func.instruction(&Instruction::LocalGet(0)); - bool2string_func.instruction(&Instruction::TableGet(table_indices::STRINGS)); - bool2string_func.instruction(&Instruction::End); - code.function(&bool2string_func); - - functions.function(types::I32I64_EXTERNREF); - let mut any2string_func = Function::new(vec![]); - any2string_func.instruction(&Instruction::LocalGet(0)); - any2string_func.instruction(&Instruction::I32Const(hq_value_types::BOOL64)); - any2string_func.instruction(&Instruction::I32Eq); - any2string_func.instruction(&Instruction::If(WasmBlockType::FunctionType( - types::NOPARAM_EXTERNREF, - ))); - any2string_func.instruction(&Instruction::LocalGet(1)); - any2string_func.instruction(&Instruction::I32WrapI64); - any2string_func.instruction(&Instruction::Call(func_indices::CAST_BOOL_STRING)); - any2string_func.instruction(&Instruction::Else); - any2string_func.instruction(&Instruction::LocalGet(0)); - any2string_func.instruction(&Instruction::I32Const(hq_value_types::FLOAT64)); - any2string_func.instruction(&Instruction::I32Eq); - any2string_func.instruction(&Instruction::If(WasmBlockType::FunctionType( - types::NOPARAM_EXTERNREF, - ))); - any2string_func.instruction(&Instruction::LocalGet(1)); - any2string_func.instruction(&Instruction::F64ReinterpretI64); - any2string_func.instruction(&Instruction::Call( - func_indices::CAST_PRIMITIVE_FLOAT_STRING, - )); - any2string_func.instruction(&Instruction::Else); - any2string_func.instruction(&Instruction::LocalGet(0)); - any2string_func.instruction(&Instruction::I32Const(hq_value_types::EXTERN_STRING_REF64)); - any2string_func.instruction(&Instruction::I32Eq); - any2string_func.instruction(&Instruction::If(WasmBlockType::FunctionType( - types::NOPARAM_EXTERNREF, - ))); - any2string_func.instruction(&Instruction::LocalGet(1)); - any2string_func.instruction(&Instruction::I32WrapI64); - any2string_func.instruction(&Instruction::TableGet(table_indices::STRINGS)); - any2string_func.instruction(&Instruction::Else); - any2string_func.instruction(&Instruction::LocalGet(0)); - any2string_func.instruction(&Instruction::I32Const(hq_value_types::INT64)); - any2string_func.instruction(&Instruction::I32Eq); - any2string_func.instruction(&Instruction::If(WasmBlockType::FunctionType( - types::NOPARAM_EXTERNREF, - ))); - any2string_func.instruction(&Instruction::LocalGet(1)); - any2string_func.instruction(&Instruction::F64ConvertI64S); // just convert to a float and then to a string, seeing as all valid scratch integers are valid floats. hopefully it won't break. - any2string_func.instruction(&Instruction::Call( - func_indices::CAST_PRIMITIVE_FLOAT_STRING, - )); - any2string_func.instruction(&Instruction::Else); - any2string_func.instruction(&Instruction::Unreachable); - any2string_func.instruction(&Instruction::End); - any2string_func.instruction(&Instruction::End); - any2string_func.instruction(&Instruction::End); - any2string_func.instruction(&Instruction::End); - any2string_func.instruction(&Instruction::End); - code.function(&any2string_func); - - functions.function(types::I32I64_F64); - let mut any2float_func = Function::new(vec![]); - any2float_func.instruction(&Instruction::LocalGet(0)); - any2float_func.instruction(&Instruction::I32Const(hq_value_types::BOOL64)); - any2float_func.instruction(&Instruction::I32Eq); - any2float_func.instruction(&Instruction::If(WasmBlockType::FunctionType( - types::NOPARAM_F64, - ))); - any2float_func.instruction(&Instruction::LocalGet(1)); - any2float_func.instruction(&Instruction::I32WrapI64); - any2float_func.instruction(&Instruction::Call(func_indices::CAST_BOOL_FLOAT)); - any2float_func.instruction(&Instruction::Else); - any2float_func.instruction(&Instruction::LocalGet(0)); - any2float_func.instruction(&Instruction::I32Const(hq_value_types::EXTERN_STRING_REF64)); - any2float_func.instruction(&Instruction::I32Eq); - any2float_func.instruction(&Instruction::If(WasmBlockType::FunctionType( - types::NOPARAM_F64, - ))); - any2float_func.instruction(&Instruction::LocalGet(1)); - any2float_func.instruction(&Instruction::I32WrapI64); - any2float_func.instruction(&Instruction::TableGet(table_indices::STRINGS)); - any2float_func.instruction(&Instruction::Call( - func_indices::CAST_PRIMITIVE_STRING_FLOAT, - )); - any2float_func.instruction(&Instruction::Else); - any2float_func.instruction(&Instruction::LocalGet(0)); - any2float_func.instruction(&Instruction::I32Const(hq_value_types::FLOAT64)); - any2float_func.instruction(&Instruction::I32Eq); - any2float_func.instruction(&Instruction::If(WasmBlockType::FunctionType( - types::NOPARAM_F64, - ))); - any2float_func.instruction(&Instruction::LocalGet(1)); - any2float_func.instruction(&Instruction::F64ReinterpretI64); - any2float_func.instruction(&Instruction::Else); - any2float_func.instruction(&Instruction::LocalGet(0)); - any2float_func.instruction(&Instruction::I32Const(hq_value_types::INT64)); - any2float_func.instruction(&Instruction::I32Eq); - any2float_func.instruction(&Instruction::If(WasmBlockType::FunctionType( - types::NOPARAM_F64, - ))); - any2float_func.instruction(&Instruction::LocalGet(1)); - any2float_func.instruction(&Instruction::F64ConvertI64S); - any2float_func.instruction(&Instruction::Else); - any2float_func.instruction(&Instruction::Unreachable); - any2float_func.instruction(&Instruction::End); - any2float_func.instruction(&Instruction::End); - any2float_func.instruction(&Instruction::End); - any2float_func.instruction(&Instruction::End); - any2float_func.instruction(&Instruction::End); - code.function(&any2float_func); - - functions.function(types::I32I64_I32); - let mut any2bool_func = Function::new(vec![]); - any2bool_func.instruction(&Instruction::LocalGet(0)); - any2bool_func.instruction(&Instruction::I32Const(hq_value_types::EXTERN_STRING_REF64)); - any2bool_func.instruction(&Instruction::I32Eq); - any2bool_func.instruction(&Instruction::If(WasmBlockType::FunctionType( - types::NOPARAM_I32, - ))); - any2bool_func.instruction(&Instruction::LocalGet(1)); - any2bool_func.instruction(&Instruction::I32WrapI64); - any2bool_func.instruction(&Instruction::TableGet(table_indices::STRINGS)); - any2bool_func.instruction(&Instruction::Call(func_indices::CAST_PRIMITIVE_STRING_BOOL)); - any2bool_func.instruction(&Instruction::Else); - any2bool_func.instruction(&Instruction::LocalGet(0)); - any2bool_func.instruction(&Instruction::I32Const(hq_value_types::FLOAT64)); - any2bool_func.instruction(&Instruction::I32Eq); - any2bool_func.instruction(&Instruction::If(WasmBlockType::FunctionType( - types::NOPARAM_I32, - ))); - any2bool_func.instruction(&Instruction::LocalGet(1)); - any2bool_func.instruction(&Instruction::F64ReinterpretI64); - any2bool_func.instruction(&Instruction::Call(func_indices::CAST_FLOAT_BOOL)); - any2bool_func.instruction(&Instruction::Else); - any2bool_func.instruction(&Instruction::LocalGet(0)); - any2bool_func.instruction(&Instruction::I32Const(hq_value_types::BOOL64)); - any2bool_func.instruction(&Instruction::I32Eq); - any2bool_func.instruction(&Instruction::If(WasmBlockType::FunctionType( - types::NOPARAM_I32, - ))); - any2bool_func.instruction(&Instruction::LocalGet(1)); - any2bool_func.instruction(&Instruction::I32WrapI64); - any2bool_func.instruction(&Instruction::Else); - any2bool_func.instruction(&Instruction::Unreachable); - any2bool_func.instruction(&Instruction::End); - any2bool_func.instruction(&Instruction::End); - any2bool_func.instruction(&Instruction::End); - any2bool_func.instruction(&Instruction::End); - code.function(&any2bool_func); - - functions.function(types::I32I64_I64); - let mut any2int_func = Function::new(vec![]); - any2int_func.instruction(&Instruction::Unreachable); - any2int_func.instruction(&Instruction::End); - code.function(&any2int_func); - - functions.function(types::EXTERNREF_I32); - let mut tbl_add_string_func = Function::new(vec![(1, ValType::I32)]); - tbl_add_string_func.instruction(&Instruction::LocalGet(0)); - tbl_add_string_func.instruction(&Instruction::I32Const(1)); - tbl_add_string_func.instruction(&Instruction::TableGrow(table_indices::STRINGS)); - tbl_add_string_func.instruction(&Instruction::LocalTee(1)); - tbl_add_string_func.instruction(&Instruction::I32Const(-1)); - tbl_add_string_func.instruction(&Instruction::I32Eq); - tbl_add_string_func.instruction(&Instruction::If(WasmBlockType::Empty)); - tbl_add_string_func.instruction(&Instruction::Unreachable); - tbl_add_string_func.instruction(&Instruction::End); - tbl_add_string_func.instruction(&Instruction::LocalGet(1)); - tbl_add_string_func.instruction(&Instruction::End); - code.function(&tbl_add_string_func); - - mod supc_locals { - pub const SPRITE_INDEX: u32 = 0; - pub const MEM_POS: u32 = 1; - pub const HUE: u32 = 2; - pub const SAT: u32 = 3; - pub const VAL: u32 = 4; - pub const REGION: u32 = 5; - pub const REMAINDER: u32 = 6; - pub const P: u32 = 7; - pub const Q: u32 = 8; - pub const T: u32 = 9; - pub const R: u32 = 10; - pub const G: u32 = 11; - pub const B: u32 = 12; - pub const VAL_F: u32 = 13; - } - - // hsv->rgb based off of https://stackoverflow.com/a/14733008 - functions.function(types::I32_NORESULT); - let mut sprite_update_pen_color_func = - Function::new(vec![(12, ValType::I32), (1, ValType::F32)]); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::SPRITE_INDEX)); // sprite index - this is (target index - 1), assuming that the stage is target 0, which could be an issue if we don't confirm this - sprite_update_pen_color_func.instruction(&Instruction::I32Const(SPRITE_INFO_LEN)); - sprite_update_pen_color_func.instruction(&Instruction::I32Mul); - sprite_update_pen_color_func.instruction(&Instruction::I32Const( - (byte_offset::VARS as usize + project.vars.borrow().len() * VAR_INFO_LEN as usize) - .try_into() - .map_err(|_| make_hq_bug!(""))?, - )); - sprite_update_pen_color_func.instruction(&Instruction::I32Add); - sprite_update_pen_color_func.instruction(&Instruction::LocalTee(supc_locals::MEM_POS)); // position in memory of sprite info - sprite_update_pen_color_func.instruction(&Instruction::F32Load(MemArg { - offset: u64::try_from(sprite_info_offsets::PEN_COLOR).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - })); - sprite_update_pen_color_func.instruction(&Instruction::F32Const(2.55)); - sprite_update_pen_color_func.instruction(&Instruction::F32Mul); - sprite_update_pen_color_func.instruction(&Instruction::I32TruncF32S); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::HUE)); // hue - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::MEM_POS)); - sprite_update_pen_color_func.instruction(&Instruction::F32Load(MemArg { - offset: u64::try_from(sprite_info_offsets::PEN_SATURATION) - .map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - })); - sprite_update_pen_color_func.instruction(&Instruction::F32Const(2.55)); - sprite_update_pen_color_func.instruction(&Instruction::F32Mul); - sprite_update_pen_color_func.instruction(&Instruction::I32TruncF32S); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::SAT)); // saturation ∈ [0, 256) - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::MEM_POS)); - sprite_update_pen_color_func.instruction(&Instruction::F32Load(MemArg { - offset: u64::try_from(sprite_info_offsets::PEN_VALUE).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - })); - sprite_update_pen_color_func.instruction(&Instruction::F32Const(2.55)); - sprite_update_pen_color_func.instruction(&Instruction::F32Mul); - sprite_update_pen_color_func.instruction(&Instruction::I32TruncF32S); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::VAL)); // value ∈ [0, 256) - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::MEM_POS)); - sprite_update_pen_color_func.instruction(&Instruction::F32Const(100.0)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::MEM_POS)); - sprite_update_pen_color_func.instruction(&Instruction::F32Load(MemArg { - offset: u64::try_from(sprite_info_offsets::PEN_TRANSPARENCY) - .map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - })); // transparency ∈ [0, 100] - sprite_update_pen_color_func.instruction(&Instruction::F32Sub); - sprite_update_pen_color_func.instruction(&Instruction::F32Const(100.0)); - sprite_update_pen_color_func.instruction(&Instruction::F32Div); // alpha ∈ [0, 1] - sprite_update_pen_color_func.instruction(&Instruction::F32Store(MemArg { - offset: u64::try_from(sprite_info_offsets::PEN_A).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - })); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::MEM_POS)); - sprite_update_pen_color_func.instruction(&Instruction::F32Load(MemArg { - offset: u64::try_from(sprite_info_offsets::PEN_A).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - })); - sprite_update_pen_color_func.instruction(&Instruction::F32Const(0.01)); - sprite_update_pen_color_func.instruction(&Instruction::F32Lt); - sprite_update_pen_color_func.instruction(&Instruction::If(WasmBlockType::Empty)); - sprite_update_pen_color_func.instruction(&Instruction::I32Const( - supc_locals::MEM_POS - .try_into() - .map_err(|_| make_hq_bug!(""))?, - )); - sprite_update_pen_color_func.instruction(&Instruction::F32Const(0.0)); - sprite_update_pen_color_func.instruction(&Instruction::F32Store(MemArg { - offset: u64::try_from(sprite_info_offsets::PEN_A).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - })); - sprite_update_pen_color_func.instruction(&Instruction::Return); // if alpha is 0, return (it is already set to 0 so it doesn't matter what r, g & b are) - sprite_update_pen_color_func.instruction(&Instruction::End); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::SAT)); - sprite_update_pen_color_func.instruction(&Instruction::I32Eqz); - sprite_update_pen_color_func.instruction(&Instruction::If(WasmBlockType::Empty)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::VAL)); - sprite_update_pen_color_func.instruction(&Instruction::F32ConvertI32S); - sprite_update_pen_color_func.instruction(&Instruction::F32Const(255.0)); - sprite_update_pen_color_func.instruction(&Instruction::F32Div); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::VAL_F)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::MEM_POS)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::VAL_F)); - sprite_update_pen_color_func.instruction(&Instruction::F32Store(MemArg { - offset: u64::try_from(sprite_info_offsets::PEN_R).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - })); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::MEM_POS)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::VAL_F)); - sprite_update_pen_color_func.instruction(&Instruction::F32Store(MemArg { - offset: u64::try_from(sprite_info_offsets::PEN_G).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - })); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::MEM_POS)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::VAL_F)); - sprite_update_pen_color_func.instruction(&Instruction::F32Store(MemArg { - offset: u64::try_from(sprite_info_offsets::PEN_B).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - })); - sprite_update_pen_color_func.instruction(&Instruction::Return); - sprite_update_pen_color_func.instruction(&Instruction::End); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::HUE)); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(43)); - sprite_update_pen_color_func.instruction(&Instruction::I32DivU); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::REGION)); // 'region' - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::HUE)); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(43)); - sprite_update_pen_color_func.instruction(&Instruction::I32RemU); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(6)); - sprite_update_pen_color_func.instruction(&Instruction::I32Mul); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::REMAINDER)); // 'remainder' - sprite_update_pen_color_func.instruction(&Instruction::I32Const(255)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::SAT)); - sprite_update_pen_color_func.instruction(&Instruction::I32Sub); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::VAL)); - sprite_update_pen_color_func.instruction(&Instruction::I32Mul); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(8)); - sprite_update_pen_color_func.instruction(&Instruction::I32ShrU); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::P)); // 'p' - sprite_update_pen_color_func.instruction(&Instruction::I32Const(255)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::REMAINDER)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::SAT)); - sprite_update_pen_color_func.instruction(&Instruction::I32Mul); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(8)); - sprite_update_pen_color_func.instruction(&Instruction::I32ShrU); - sprite_update_pen_color_func.instruction(&Instruction::I32Sub); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::VAL)); - sprite_update_pen_color_func.instruction(&Instruction::I32Mul); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(8)); - sprite_update_pen_color_func.instruction(&Instruction::I32ShrU); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::Q)); // 'q' - sprite_update_pen_color_func.instruction(&Instruction::I32Const(255)); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(255)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::REMAINDER)); - sprite_update_pen_color_func.instruction(&Instruction::I32Sub); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::SAT)); - sprite_update_pen_color_func.instruction(&Instruction::I32Mul); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(8)); - sprite_update_pen_color_func.instruction(&Instruction::I32ShrU); - sprite_update_pen_color_func.instruction(&Instruction::I32Sub); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::VAL)); - sprite_update_pen_color_func.instruction(&Instruction::I32Mul); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(8)); - sprite_update_pen_color_func.instruction(&Instruction::I32ShrU); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::T)); // 't' - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::REGION)); - sprite_update_pen_color_func.instruction(&Instruction::I32Eqz); - sprite_update_pen_color_func.instruction(&Instruction::If(WasmBlockType::Empty)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::VAL)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::R)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::T)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::G)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::P)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::B)); - sprite_update_pen_color_func.instruction(&Instruction::End); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::REGION)); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(1)); - sprite_update_pen_color_func.instruction(&Instruction::I32Eq); - sprite_update_pen_color_func.instruction(&Instruction::If(WasmBlockType::Empty)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::Q)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::R)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::VAL)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::G)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::P)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::B)); - sprite_update_pen_color_func.instruction(&Instruction::End); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::REGION)); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(2)); - sprite_update_pen_color_func.instruction(&Instruction::I32Eq); - sprite_update_pen_color_func.instruction(&Instruction::If(WasmBlockType::Empty)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::P)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::R)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::VAL)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::G)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::T)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::B)); - sprite_update_pen_color_func.instruction(&Instruction::End); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::REGION)); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(3)); - sprite_update_pen_color_func.instruction(&Instruction::I32Eq); - sprite_update_pen_color_func.instruction(&Instruction::If(WasmBlockType::Empty)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::P)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::R)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::Q)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::G)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::VAL)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::B)); - sprite_update_pen_color_func.instruction(&Instruction::End); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::REGION)); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(4)); - sprite_update_pen_color_func.instruction(&Instruction::I32Eq); - sprite_update_pen_color_func.instruction(&Instruction::If(WasmBlockType::Empty)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::T)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::R)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::P)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::G)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::VAL)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::B)); - sprite_update_pen_color_func.instruction(&Instruction::End); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::REGION)); - sprite_update_pen_color_func.instruction(&Instruction::I32Const(5)); - sprite_update_pen_color_func.instruction(&Instruction::I32Eq); - sprite_update_pen_color_func.instruction(&Instruction::If(WasmBlockType::Empty)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::VAL)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::R)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::P)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::G)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::Q)); - sprite_update_pen_color_func.instruction(&Instruction::LocalSet(supc_locals::B)); - sprite_update_pen_color_func.instruction(&Instruction::End); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::MEM_POS)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::R)); - sprite_update_pen_color_func.instruction(&Instruction::F32ConvertI32S); - sprite_update_pen_color_func.instruction(&Instruction::F32Const(255.0)); - sprite_update_pen_color_func.instruction(&Instruction::F32Div); - sprite_update_pen_color_func.instruction(&Instruction::F32Store(MemArg { - offset: u64::try_from(sprite_info_offsets::PEN_R).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - })); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::MEM_POS)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::G)); - sprite_update_pen_color_func.instruction(&Instruction::F32ConvertI32S); - sprite_update_pen_color_func.instruction(&Instruction::F32Const(255.0)); - sprite_update_pen_color_func.instruction(&Instruction::F32Div); - sprite_update_pen_color_func.instruction(&Instruction::F32Store(MemArg { - offset: u64::try_from(sprite_info_offsets::PEN_G).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - })); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::MEM_POS)); - sprite_update_pen_color_func.instruction(&Instruction::LocalGet(supc_locals::B)); - sprite_update_pen_color_func.instruction(&Instruction::F32ConvertI32S); - sprite_update_pen_color_func.instruction(&Instruction::F32Const(255.0)); - sprite_update_pen_color_func.instruction(&Instruction::F32Div); - sprite_update_pen_color_func.instruction(&Instruction::F32Store(MemArg { - offset: u64::try_from(sprite_info_offsets::PEN_B).map_err(|_| make_hq_bug!(""))?, - align: 2, - memory_index: 0, - })); - sprite_update_pen_color_func.instruction(&Instruction::End); - code.function(&sprite_update_pen_color_func); - - let mut gf_func = Function::new(vec![]); - let mut tick_func = Function::new(vec![(2, ValType::I32)]); - - let mut noop_func = Function::new(vec![]); - noop_func.instruction(&Instruction::I32Const(1)); - noop_func.instruction(&Instruction::End); - - let mut thread_indices: Vec<(ThreadStart, u32)> = vec![]; // (start type, first step index) - - let mut string_consts = vec![String::from("false"), String::from("true")]; - - let mut step_funcs: IndexMap, Function, _> = Default::default(); - step_funcs.insert(None, noop_func); - - for step in &project.steps { - // make sure to skip the 0th (noop) step because we've added the noop step function 3 lines above - if step.0 == &("".into(), "".into()) { - continue; - }; - step.compile_wasm(&mut step_funcs, &mut string_consts, &project.steps)?; - } - - for thread in project.threads { - let first_idx = project - .steps - .get_index_of(&(thread.target_id().clone(), thread.first_step().clone())) - .ok_or(make_hq_bug!(""))? - .try_into() - .map_err(|_| make_hq_bug!("step index out of bounds"))?; - thread_indices.push((thread.start().clone(), first_idx)); - } - - let mut thread_start_counts: BTreeMap = Default::default(); - - macro_rules! func_for_thread_start { - ($start_type:ident) => { - match $start_type { - ThreadStart::GreenFlag => &mut gf_func, - } - }; - } - - for (maybe_step, func) in &step_funcs { - code.function(func); - if maybe_step.is_none() { - functions.function(types::I32_I32); - continue; - } - functions.function( - match project - .steps - .get(&maybe_step.clone().unwrap()) - .ok_or(make_hq_bug!("missing step"))? - .context - .proc - { - None => types::I32_I32, - Some(Procedure { - warp, - ref arg_types, - .. - }) => { - if !warp { - hq_todo!("non-warp procedure") - } else { - if !arg_types.is_empty() { - hq_todo!("proc args") - } - types::I32_I32 - } - } - }, - ); - } - - for (start_type, index) in thread_indices { - let func = func_for_thread_start!(start_type); - func.instruction(&Instruction::I32Const(0)); - func.instruction(&Instruction::I32Load(MemArg { - offset: byte_offset::THREAD_NUM - .try_into() - .map_err(|_| make_hq_bug!("THREAD_NUM out of bounds"))?, - align: 2, - memory_index: 0, - })); - func.instruction(&Instruction::I32Const(THREAD_BYTE_LEN)); - func.instruction(&Instruction::I32Mul); - let thread_start_count: i32 = (*thread_start_counts.get(&start_type).unwrap_or(&0)) - .try_into() - .map_err(|_| make_hq_bug!("start_type count out of bounds"))?; - func.instruction(&Instruction::I32Const(thread_start_count * THREAD_BYTE_LEN)); - func.instruction(&Instruction::I32Add); - func.instruction(&Instruction::I32Const( - index - .try_into() - .map_err(|_| make_hq_bug!("step func index out of bounds"))?, - )); - func.instruction(&Instruction::I32Store(MemArg { - offset: (byte_offset::VARS as usize - + VAR_INFO_LEN as usize * project.vars.borrow().len() - + usize::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - * (project.target_names.len() - 1)) - .try_into() - .map_err(|_| make_hq_bug!("i32.store offset out of bounds"))?, - align: 2, // 2 ** 2 = 4 (bytes) - memory_index: 0, - })); - - thread_start_counts - .entry(start_type) - .and_modify(|u| *u += 1) - .or_insert(1); - } - - for (start_type, count) in thread_start_counts { - let func = func_for_thread_start!(start_type); - func.instruction(&Instruction::I32Const(0)); - func.instruction(&Instruction::I32Const(0)); - func.instruction(&Instruction::I32Load(MemArg { - offset: byte_offset::THREAD_NUM - .try_into() - .map_err(|_| make_hq_bug!("THREAD_NUM out of bounds"))?, - align: 2, - memory_index: 0, - })); - func.instruction(&Instruction::I32Const( - count - .try_into() - .map_err(|_| make_hq_bug!("thread_start count out of bounds"))?, - )); - func.instruction(&Instruction::I32Add); - func.instruction(&Instruction::I32Store(MemArg { - offset: byte_offset::THREAD_NUM - .try_into() - .map_err(|_| make_hq_bug!("THREAD_NUM out of bounds"))?, - align: 2, - memory_index: 0, - })); - } - - { - tick_func.instruction(&Instruction::I32Const(0)); - tick_func.instruction(&Instruction::I32Load(MemArg { - offset: byte_offset::THREAD_NUM - .try_into() - .map_err(|_| make_hq_bug!("THREAD_NUM out of bounds"))?, - align: 2, - memory_index: 0, - })); - tick_func.instruction(&Instruction::LocalTee(1)); - tick_func.instruction(&Instruction::I32Eqz); - tick_func.instruction(&Instruction::BrIf(0)); - tick_func.instruction(&Instruction::LocalGet(1)); - tick_func.instruction(&Instruction::I32Const(THREAD_BYTE_LEN)); - tick_func.instruction(&Instruction::I32Mul); - tick_func.instruction(&Instruction::I32Const(THREAD_BYTE_LEN)); - tick_func.instruction(&Instruction::I32Sub); - tick_func.instruction(&Instruction::LocalSet(1)); - tick_func.instruction(&Instruction::Loop(WasmBlockType::Empty)); - - tick_func.instruction(&Instruction::LocalGet(0)); - tick_func.instruction(&Instruction::LocalGet(0)); - tick_func.instruction(&Instruction::I32Load(MemArg { - offset: (byte_offset::VARS as usize - + VAR_INFO_LEN as usize * project.vars.borrow().len() - + usize::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? - * (project.target_names.len() - 1)) - .try_into() - .map_err(|_| make_hq_bug!("i32.store offset out of bounds"))?, - align: 2, // 2 ** 2 = 4 (bytes) - memory_index: 0, - })); - tick_func.instruction(&Instruction::CallIndirect { - type_index: types::I32_I32, - table_index: table_indices::STEP_FUNCS, - }); - - tick_func.instruction(&Instruction::If(WasmBlockType::Empty)); - tick_func.instruction(&Instruction::LocalGet(0)); - tick_func.instruction(&Instruction::I32Const(THREAD_BYTE_LEN)); - tick_func.instruction(&Instruction::I32Add); - tick_func.instruction(&Instruction::LocalSet(0)); - tick_func.instruction(&Instruction::Else); - tick_func.instruction(&Instruction::LocalGet(1)); - tick_func.instruction(&Instruction::I32Const(THREAD_BYTE_LEN)); - tick_func.instruction(&Instruction::I32Sub); - tick_func.instruction(&Instruction::LocalSet(1)); - tick_func.instruction(&Instruction::End); - tick_func.instruction(&Instruction::LocalGet(0)); - tick_func.instruction(&Instruction::LocalGet(1)); - tick_func.instruction(&Instruction::I32LeS); - tick_func.instruction(&Instruction::BrIf(0)); - tick_func.instruction(&Instruction::End); - } - - gf_func.instruction(&Instruction::End); - functions.function(types::NOPARAM_NORESULT); - code.function(&gf_func); - exports.export( - "green_flag", - ExportKind::Func, - code.len() + IMPORTED_FUNCS - 1, - ); - - tick_func.instruction(&Instruction::End); - functions.function(types::NOPARAM_NORESULT); - code.function(&tick_func); - exports.export("tick", ExportKind::Func, code.len() + IMPORTED_FUNCS - 1); - - tables.table(TableType { - element_type: RefType::FUNCREF, - minimum: step_funcs - .len() - .try_into() - .map_err(|_| make_hq_bug!("step_funcs length out of bounds"))?, - maximum: Some( - step_funcs - .len() - .try_into() - .map_err(|_| make_hq_bug!("step_funcs length out of bounds"))?, - ), - table64: false, - shared: false, - }); - - globals.global( - GlobalType { - val_type: ValType::I32, - mutable: false, - shared: false, - }, - &ConstExpr::i32_const(byte_offset::REDRAW_REQUESTED), - ); - globals.global( - GlobalType { - val_type: ValType::I32, - mutable: false, - shared: false, - }, - &ConstExpr::i32_const(byte_offset::THREAD_NUM), - ); - globals.global( - GlobalType { - val_type: ValType::I32, - mutable: false, - shared: false, - }, - &ConstExpr::i32_const( - project - .vars - .borrow() - .len() - .try_into() - .map_err(|_| make_hq_bug!("vars length out of bounds"))?, - ), - ); - for _ in 0..project.vars.borrow().len() { - // TODO: only create globals for variables that are at some point assumed to be strings - globals.global( - GlobalType { - val_type: ValType::Ref(RefType::EXTERNREF), - mutable: true, - shared: false, - }, - &ConstExpr::ref_null(HeapType::Abstract { - shared: false, - ty: AbstractHeapType::Extern, - }), - ); - } - - exports.export("step_funcs", ExportKind::Table, table_indices::STEP_FUNCS); - exports.export("strings", ExportKind::Table, table_indices::STRINGS); - exports.export("memory", ExportKind::Memory, 0); - exports.export("rr_offset", ExportKind::Global, 0); - exports.export("thn_offset", ExportKind::Global, 1); - exports.export("vars_num", ExportKind::Global, 2); - exports.export( - "upc", - ExportKind::Func, - func_indices::SPRITE_UPDATE_PEN_COLOR, - ); - - let mut data = DataSection::new(); - - let mut default_data: Vec = Vec::with_capacity( - 8 + 16 * project.vars.borrow().len() + 80 * (project.target_names.len() - 1), - ); - - default_data.extend([0; 8]); - - //project.vars; - - for var in project.vars.take() { - match var.initial_value() { - VarVal::Float(float) => { - default_data.extend([0; 8]); - default_data.extend(float.to_le_bytes()); - } - VarVal::Bool(boolean) => { - default_data.extend(1i64.to_le_bytes()); - default_data.extend([0; 4]); - default_data.extend((*boolean as i64).to_le_bytes()); - } - VarVal::String(string) => { - default_data.extend(2i64.to_le_bytes()); - default_data.extend([0; 4]); - let index = string_consts.len(); - string_consts.push(string.clone()); - default_data.extend((index as u64).to_le_bytes()); - } - } - } - - for target in project.sb3.targets { - if target.is_stage { - continue; - } - default_data.extend(target.x.to_le_bytes()); - default_data.extend(target.y.to_le_bytes()); - default_data.extend([0; 41]); - default_data.push(target.visible as u8); - default_data.extend([0; 2]); - default_data.extend(target.current_costume.to_le_bytes()); - default_data.extend(target.size.to_le_bytes()); - default_data.extend(target.direction.to_le_bytes()); - } - - data.active(0, &ConstExpr::i32_const(0), default_data); - - tables.table(TableType { - element_type: RefType::EXTERNREF, - minimum: string_consts - .len() - .try_into() - .map_err(|_| make_hq_bug!("string_consts len out of bounds"))?, - maximum: None, - table64: false, - shared: false, - }); - - let step_indices = (BUILTIN_FUNCS - ..(u32::try_from(step_funcs.len()) - .map_err(|_| make_hq_bug!("step_funcs length out of bounds"))? - + BUILTIN_FUNCS)) - .collect::>(); - let step_func_indices = Elements::Functions(&step_indices[..]); - elements.active( - Some(table_indices::STEP_FUNCS), - &ConstExpr::i32_const(0), - step_func_indices, - ); - - module - .section(&types) - .section(&imports) - .section(&functions) - .section(&tables) - .section(&memories) - .section(&globals) - .section(&exports) - // start - .section(&elements) - // datacount - .section(&code) - .section(&data); - - let wasm_bytes = module.finish(); - Ok(Self { - target_names: project.target_names.clone(), - wasm_bytes, - string_consts, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::process::{Command, Stdio}; - - #[test] - fn make_wasm() -> Result<(), HQError> { - use crate::sb3::Sb3Project; - use std::fs; - let proj: Sb3Project = fs::read_to_string("./benchmark (3.1).json") - .expect("couldn't read hq-test.project.json") - .try_into() - .unwrap(); - let ir: IrProject = proj.try_into()?; - let wasm: WasmProject = ir.try_into()?; - Ok(()) - /*fs::write("./bad.wasm", wasm.wasm_bytes()).expect("failed to write to bad.wasm"); - let output = Command::new("node") - .arg("-e") - .arg(format!( - "({})().catch(e => {{ console.error(e); process.exit(1) }})", - wasm.js_string() - )) - .stdout(Stdio::inherit()) - .output() - .expect("failed to execute process"); - println!( - "{:}", - String::from_utf8(output.stdout).expect("failed to convert stdout from utf8") - ); - println!( - "{:}", - String::from_utf8(output.stderr).expect("failed to convert stderr from utf8") - ); - if !output.status.success() { - panic!("couldn't run wasm"); - }*/ - } -} diff --git a/src/wasm.rs b/src/wasm.rs new file mode 100644 index 00000000..db8d0614 --- /dev/null +++ b/src/wasm.rs @@ -0,0 +1,21 @@ +mod external; +pub mod flags; +mod func; +mod globals; +mod project; +mod registries; +mod strings; +mod tables; +mod type_registry; +mod variable; + +pub use external::{ExternalEnvironment, ExternalFunctionRegistry}; +pub use flags::WasmFlags; +pub use func::{Instruction as InternalInstruction, StepFunc}; +pub use globals::{Exportable as GlobalExportable, GlobalRegistry, Mutable as GlobalMutable}; +pub use project::{FinishedWasm, WasmProject}; +pub use registries::Registries; +pub use strings::StringRegistry; +pub use tables::{TableOptions, TableRegistry}; +pub use type_registry::TypeRegistry; +pub use variable::VariableRegistry; diff --git a/src/wasm/external.rs b/src/wasm/external.rs new file mode 100644 index 00000000..d365a24c --- /dev/null +++ b/src/wasm/external.rs @@ -0,0 +1,23 @@ +use super::TypeRegistry; +use crate::prelude::*; +use crate::registry::MapRegistry; +use wasm_encoder::{EntityType, ImportSection, ValType}; + +pub type ExternalFunctionRegistry = + MapRegistry<(&'static str, Box), (Vec, Vec)>; + +impl ExternalFunctionRegistry { + pub fn finish(self, imports: &mut ImportSection, type_registry: &TypeRegistry) -> HQResult<()> { + for ((module, name), (params, results)) in self.registry().take() { + let type_index = type_registry.register_default((params, results))?; + imports.import(module, &name, EntityType::Function(type_index)); + } + Ok(()) + } +} + +#[derive(Debug, Copy, Clone)] +#[non_exhaustive] +pub enum ExternalEnvironment { + WebBrowser, +} diff --git a/src/wasm/flags.rs b/src/wasm/flags.rs new file mode 100644 index 00000000..c3e73111 --- /dev/null +++ b/src/wasm/flags.rs @@ -0,0 +1,208 @@ +use crate::prelude::*; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[derive(Copy, Clone, Serialize, Deserialize)] +#[wasm_bindgen] +pub enum WasmStringType { + ExternRef, + JsStringBuiltins, + //Manual, +} + +#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[wasm_bindgen] +pub enum WasmOpt { + On, + Off, +} + +#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[wasm_bindgen] +pub enum Scheduler { + TypedFuncRef, + CallIndirect, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[wasm_bindgen] +pub enum WasmFeature { + ReferenceTypes, + TypedFunctionReferences, + JSStringBuiltins, +} + +#[wasm_bindgen] +pub fn all_wasm_features() -> Vec { + use WasmFeature::{JSStringBuiltins, ReferenceTypes, TypedFunctionReferences}; + vec![ReferenceTypes, TypedFunctionReferences, JSStringBuiltins] +} + +// no &self because wasm_bidgen doesn't like it +#[wasm_bindgen] +pub fn wasm_feature_detect_name(feat: WasmFeature) -> String { + use WasmFeature::{JSStringBuiltins, ReferenceTypes, TypedFunctionReferences}; + match feat { + ReferenceTypes => "referenceTypes", + TypedFunctionReferences => "typedFunctionReferences", + JSStringBuiltins => "jsStringBuiltins", + } + .into() +} + +#[derive(Clone, Serialize, Deserialize)] +#[wasm_bindgen(getter_with_clone)] +#[expect( + clippy::unsafe_derive_deserialize, + reason = "wasm-bindgen introduces unsafe methods" +)] +pub struct FlagInfo { + /// a human-readable name for the flag + pub name: String, + pub description: String, + pub ty: String, + /// which WASM features does this flag rely on? + wasm_features: BTreeMap>, +} + +#[wasm_bindgen] +impl FlagInfo { + fn new() -> Self { + Self { + name: String::new(), + description: String::new(), + ty: String::new(), + wasm_features: BTreeMap::default(), + } + } + + fn with_name(mut self, name: &str) -> Self { + self.name = name.to_string(); + self + } + + fn with_description(mut self, description: &str) -> Self { + self.description = description.to_string(); + self + } + + fn with_ty(mut self, ty: &str) -> Self { + self.ty = ty.to_string(); + self + } + + fn with_wasm_features(mut self, wasm_features: BTreeMap>) -> Self { + self.wasm_features = wasm_features; + self + } + + #[wasm_bindgen] + pub fn wasm_features(&self, flag: &str) -> Option> { + self.wasm_features.get(flag).cloned() + } + + #[wasm_bindgen] + pub fn to_js(&self) -> HQResult { + serde_wasm_bindgen::to_value(&self) + .map_err(|_| make_hq_bug!("couldn't convert FlagInfo to JsValue")) + } +} + +macro_rules! stringmap { + ($($k:ident : $v:expr),+ $(,)?) => {{ + BTreeMap::from([$((String::from(stringify!($k)), $v),)+]) + }} +} + +/// stringifies the name of a type whilst ensuring that the type is valid +macro_rules! ty_str { + ($ty:ty) => {{ + let _ = core::any::TypeId::of::<$ty>(); // forces the type to be valid + stringify!($ty) + }}; +} + +/// compilation flags +#[derive(Copy, Clone, Serialize, Deserialize)] +#[wasm_bindgen] +#[expect( + clippy::unsafe_derive_deserialize, + reason = "wasm-bindgen introduces unsafe methods" +)] +pub struct WasmFlags { + pub string_type: WasmStringType, + pub wasm_opt: WasmOpt, + pub scheduler: Scheduler, +} + +#[wasm_bindgen] +impl WasmFlags { + // these attributes should be at the item level, but they don't seem to work there. + #![expect( + clippy::wrong_self_convention, + clippy::trivially_copy_pass_by_ref, + reason = "to_js taking `self` causes weird errors when wasm-fied" + )] + #![expect( + clippy::needless_pass_by_value, + reason = "wasm-bindgen does not support &[T]" + )] + + #[wasm_bindgen] + pub fn from_js(js: JsValue) -> HQResult { + serde_wasm_bindgen::from_value(js) + .map_err(|_| make_hq_bug!("couldn't convert JsValue to WasmFlags")) + } + + #[wasm_bindgen] + pub fn to_js(&self) -> HQResult { + serde_wasm_bindgen::to_value(&self) + .map_err(|_| make_hq_bug!("couldn't convert WasmFlags to JsValue")) + } + + #[wasm_bindgen(constructor)] + pub fn new(wasm_features: Vec) -> Self { + crate::log(format!("{wasm_features:?}").as_str()); + Self { + wasm_opt: WasmOpt::On, + string_type: if wasm_features.contains(&WasmFeature::JSStringBuiltins) { + WasmStringType::JsStringBuiltins + } else { + WasmStringType::ExternRef + }, + scheduler: Scheduler::CallIndirect, + } + } + + #[wasm_bindgen] + pub fn flag_info(flag: &str) -> FlagInfo { + match flag { + "string_type" => FlagInfo::new() + .with_name("Internal string representation") + .with_description( + "ExternRef - uses JavaScript strings.\ +
\ + JsStringBuiltins (recommended) - uses JavaScript strings with the JS String Builtins proposal", + ) + .with_ty(ty_str!(WasmStringType)) + .with_wasm_features(stringmap! { + ExternRef : vec![WasmFeature::ReferenceTypes], + JsStringBuiltins : vec![WasmFeature::ReferenceTypes, WasmFeature::JSStringBuiltins], + }), + "wasm_opt" => FlagInfo::new() + .with_name("WASM optimisation") + .with_description("Should we try to optimise generated WASM modules using wasm-opt?") + .with_ty(ty_str!(WasmOpt)), + "scheduler" => FlagInfo::new() + .with_name("Scheduler") + .with_description("TypedFuncRef - uses typed function references to eliminate runtime checks.\ +
\ + CallIndirect - stores function indices, then uses CallIndirect to call them.") + .with_ty(ty_str!(Scheduler)) + .with_wasm_features(stringmap! { + TypedFuncRef : vec![WasmFeature::TypedFunctionReferences] + }), + _ => FlagInfo::new().with_name(format!("unknown setting '{flag}'").as_str()) + } + } +} diff --git a/src/wasm/func.rs b/src/wasm/func.rs new file mode 100644 index 00000000..beed5b9e --- /dev/null +++ b/src/wasm/func.rs @@ -0,0 +1,257 @@ +use super::{Registries, WasmFlags, WasmProject}; +use crate::ir::{PartialStep, RcVar, Step}; +use crate::prelude::*; +use crate::{instructions::wrap_instruction, ir::Proc}; +use std::collections::{hash_map, HashMap}; +use wasm_encoder::{ + self, CodeSection, Function, FunctionSection, Instruction as WInstruction, ValType, +}; + +#[derive(Clone, Debug)] +pub enum Instruction { + Immediate(wasm_encoder::Instruction<'static>), + LazyStepRef(Weak), + LazyStepIndex(Weak), + LazyWarpedProcCall(Rc), +} + +impl Instruction { + pub fn eval( + &self, + steps: &Rc, StepFunc>>>, + imported_func_count: u32, + ) -> HQResult> { + Ok(match self { + Self::Immediate(instr) => instr.clone(), + Self::LazyStepRef(step) => { + let step_index: u32 = steps + .try_borrow()? + .get_index_of( + &step + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?, + ) + .ok_or_else(|| make_hq_bug!("couldn't find step in step map"))? + .try_into() + .map_err(|_| make_hq_bug!("step index out of bounds"))?; + WInstruction::RefFunc(imported_func_count + step_index) + } + Self::LazyStepIndex(step) => { + let step_index: i32 = steps + .try_borrow()? + .get_index_of( + &step + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?, + ) + .ok_or_else(|| make_hq_bug!("couldn't find step in step map"))? + .try_into() + .map_err(|_| make_hq_bug!("step index out of bounds"))?; + WInstruction::I32Const(step_index) + } + Self::LazyWarpedProcCall(proc) => { + let PartialStep::Finished(ref step) = *proc.warped_first_step()? else { + hq_bug!("tried to use uncompiled warped procedure step") + }; + let step_index: u32 = steps + .try_borrow()? + .get_index_of(step) + .ok_or_else(|| make_hq_bug!("couldn't find step in step map"))? + .try_into() + .map_err(|_| make_hq_bug!("step index out of bounds"))?; + WInstruction::Call(imported_func_count + step_index) + } + }) + } +} + +/// representation of a step's function +#[derive(Clone)] +pub struct StepFunc { + locals: RefCell>, + instructions: RefCell>, + params: Box<[ValType]>, + output: Option, + registries: Rc, + flags: WasmFlags, + steps: Rc, StepFunc>>>, + local_variables: RefCell>, +} + +impl StepFunc { + pub fn registries(&self) -> Rc { + Rc::clone(&self.registries) + } + + pub const fn flags(&self) -> WasmFlags { + self.flags + } + + pub const fn instructions(&self) -> &RefCell> { + &self.instructions + } + + pub fn steps(&self) -> Rc, Self>>> { + Rc::clone(&self.steps) + } + + pub fn params(&self) -> &[ValType] { + &self.params + } + + /// creates a new step function, with one paramter + pub fn new( + registries: Rc, + steps: Rc, Self>>>, + flags: WasmFlags, + ) -> Self { + Self { + locals: RefCell::new(vec![]), + instructions: RefCell::new(vec![]), + params: Box::new([ValType::I32]), + output: None, + registries, + flags, + steps, + local_variables: RefCell::new(HashMap::default()), + } + } + + /// creates a new step function with the specified amount of paramters. + /// currently only used in testing to validate types + pub fn new_with_types( + params: Box<[ValType]>, + output: Option, + registries: Rc, + steps: Rc, Self>>>, + flags: WasmFlags, + ) -> Self { + Self { + locals: RefCell::new(vec![]), + instructions: RefCell::new(vec![]), + params, + output, + registries, + flags, + steps, + local_variables: RefCell::new(HashMap::default()), + } + } + + pub fn local_variable(&self, var: &RcVar) -> HQResult { + Ok( + match self.local_variables.try_borrow_mut()?.entry(var.clone()) { + hash_map::Entry::Occupied(entry) => *entry.get(), + hash_map::Entry::Vacant(entry) => { + let index = + self.local(WasmProject::ir_type_to_wasm(*var.0.possible_types())?)?; + entry.insert(index); + index + } + }, + ) + } + + /// Registers a new local in this function, and returns its index + pub fn local(&self, val_type: ValType) -> HQResult { + self.locals + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? + .push(val_type); + u32::try_from(self.locals.try_borrow()?.len() + self.params.len() - 1) + .map_err(|_| make_hq_bug!("local index was out of bounds")) + } + + pub fn add_instructions( + &self, + instructions: impl IntoIterator, + ) -> HQResult<()> { + self.instructions + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? + .extend(instructions); + Ok(()) + } + + /// Takes ownership of the function and returns the backing `wasm_encoder` `Function` + pub fn finish( + self, + funcs: &mut FunctionSection, + code: &mut CodeSection, + steps: &Rc, Self>>>, + imported_func_count: u32, + ) -> HQResult<()> { + let mut func = Function::new_with_locals_types(self.locals.take()); + for instruction in self.instructions.take() { + func.instruction(&instruction.eval(steps, imported_func_count)?); + } + func.instruction(&wasm_encoder::Instruction::End); + let type_index = self.registries().types().register_default(( + self.params.into(), + self.output.map_or_else(Vec::new, |output| vec![output]), + ))?; + funcs.function(type_index); + code.function(&func); + Ok(()) + } + + pub fn compile_step( + step: Rc, + steps: &Rc, Self>>>, + registries: Rc, + flags: WasmFlags, + ) -> HQResult { + if let Some(step_func) = steps.try_borrow()?.get(&step) { + return Ok(step_func.clone()); + } + let step_func = if let Some(ref proc_context) = step.context().proc_context { + let arg_types = proc_context + .arg_types() + .iter() + .copied() + .map(WasmProject::ir_type_to_wasm) + .collect::>>()?; + let input_types = arg_types.iter().chain(&[ValType::I32]).copied().collect(); + Self::new_with_types(input_types, None, registries, Rc::clone(steps), flags) + } else { + Self::new(registries, Rc::clone(steps), flags) + }; + let mut instrs = vec![]; + let mut type_stack = vec![]; + for opcode in &*step.opcodes().try_borrow()?.clone() { + let inputs = type_stack + .splice((type_stack.len() - opcode.acceptable_inputs().len()).., []) + .collect(); + instrs.append(&mut wrap_instruction( + &step_func, + Rc::clone(&inputs), + opcode, + )?); + if let Some(output) = opcode.output_type(inputs)? { + type_stack.push(output); + } + } + step_func.add_instructions(instrs)?; + steps + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? + .insert(step, step_func.clone()); + Ok(step_func) + } + + pub fn compile_inner_step(&self, step: &Rc) -> HQResult> { + step.make_inlined()?; + let mut instrs = vec![]; + let mut type_stack = vec![]; + for opcode in &*step.opcodes().try_borrow()?.clone() { + let inputs = type_stack + .splice((type_stack.len() - opcode.acceptable_inputs().len()).., []) + .collect(); + instrs.append(&mut wrap_instruction(self, Rc::clone(&inputs), opcode)?); + if let Some(output) = opcode.output_type(inputs)? { + type_stack.push(output); + } + } + Ok(instrs) + } +} diff --git a/src/wasm/globals.rs b/src/wasm/globals.rs new file mode 100644 index 00000000..078b4620 --- /dev/null +++ b/src/wasm/globals.rs @@ -0,0 +1,55 @@ +use crate::prelude::*; +use crate::registry::MapRegistry; +use core::ops::Deref; +use wasm_encoder::{ + ConstExpr, ExportKind, ExportSection, GlobalSection, GlobalType, ImportSection, ValType, +}; + +#[derive(Copy, Clone, Debug)] +pub struct Mutable(pub bool); + +impl Deref for Mutable { + type Target = bool; + fn deref(&self) -> &bool { + &self.0 + } +} + +#[derive(Copy, Clone, Debug)] +pub struct Exportable(pub bool); + +impl Deref for Exportable { + type Target = bool; + fn deref(&self) -> &bool { + &self.0 + } +} + +pub type GlobalRegistry = MapRegistry, (ValType, ConstExpr, Mutable, Exportable)>; + +impl GlobalRegistry { + pub fn finish( + self, + imports: &ImportSection, + globals: &mut GlobalSection, + exports: &mut ExportSection, + ) { + for (key, (ty, suggested_initial, mutable, export)) in self.registry().take() { + if *export { + exports.export(&key, ExportKind::Global, globals.len()); + } + let actual_initial = match &*key { + "noop_func" => ConstExpr::ref_func(imports.len()), + _ => suggested_initial, + }; + globals.global( + GlobalType { + val_type: ty, + mutable: *mutable, + shared: false, + }, + &actual_initial, + ); + } + } +} diff --git a/src/wasm/project.rs b/src/wasm/project.rs new file mode 100644 index 00000000..7f8835ca --- /dev/null +++ b/src/wasm/project.rs @@ -0,0 +1,576 @@ +use super::flags::Scheduler; +use super::{ExternalEnvironment, GlobalExportable, GlobalMutable, Registries, TableOptions}; +use crate::ir::{Event, IrProject, Step, Type as IrType}; +use crate::prelude::*; +use crate::wasm::{StepFunc, WasmFlags}; +use itertools::Itertools; +use wasm_bindgen::prelude::*; +use wasm_encoder::{ + BlockType as WasmBlockType, CodeSection, ConstExpr, ElementSection, Elements, ExportKind, + ExportSection, Function, FunctionSection, GlobalSection, HeapType, ImportSection, Instruction, + MemArg, MemorySection, MemoryType, Module, RefType, TableSection, TypeSection, ValType, +}; +use wasm_gen::wasm; + +/// A respresentation of a WASM representation of a project. Cannot be created directly; +/// use `TryFrom`. +pub struct WasmProject { + flags: WasmFlags, + steps: Rc, StepFunc>>>, + /// maps an event to a list of *`step_func`* indices (NOT function indices) which are + /// triggered by that event. + events: BTreeMap>, + registries: Rc, + target_names: Vec>, + environment: ExternalEnvironment, +} + +impl WasmProject { + #[expect(dead_code, reason = "pub item may be used in the future")] + pub fn new(flags: WasmFlags, environment: ExternalEnvironment) -> Self { + Self { + flags, + steps: Rc::new(RefCell::new(IndexMap::default())), + events: BTreeMap::default(), + environment, + registries: Rc::new(Registries::default()), + target_names: vec![], + } + } + + pub fn registries(&self) -> Rc { + Rc::clone(&self.registries) + } + + #[expect(dead_code, reason = "pub item may be used in future")] + pub const fn environment(&self) -> ExternalEnvironment { + self.environment + } + + pub const fn steps(&self) -> &Rc, StepFunc>>> { + &self.steps + } + + /// maps a broad IR type to a WASM type + pub fn ir_type_to_wasm(ir_type: IrType) -> HQResult { + let base = ir_type.base_type(); + Ok(match base { + Some(IrType::Float) => ValType::F64, + Some(IrType::QuasiInt) => ValType::I32, + Some(IrType::String) => ValType::EXTERNREF, + Some(IrType::Color) => hq_todo!("colours"), //ValType::V128 // f32x4? + None => ValType::I64, // NaN boxed value... let's worry about colors later + Some(_) => unreachable!(), + }) + } + + pub fn finish(self) -> HQResult { + let mut module = Module::new(); + + let mut memories = MemorySection::new(); + let mut imports = ImportSection::new(); + let mut types = TypeSection::new(); + let mut functions = FunctionSection::new(); + let mut codes = CodeSection::new(); + let mut tables = TableSection::new(); + let mut exports = ExportSection::new(); + let mut elements = ElementSection::new(); + //let mut data = DataSection::new(); + let mut globals = GlobalSection::new(); + + memories.memory(MemoryType { + minimum: 1, + maximum: None, + memory64: false, + shared: false, + page_size_log2: None, + }); + + let strings = self.registries().strings().clone().finish(); + + self.registries().tables().register_override::( + "strings".into(), + TableOptions { + element_type: RefType::EXTERNREF, + min: strings + .len() + .try_into() + .map_err(|_| make_hq_bug!("strings length out of bounds"))?, + // TODO: use js string imports for preknown strings + max: None, + init: None, + }, + )?; + + self.registries() + .external_functions() + .clone() + .finish(&mut imports, self.registries().types())?; + for step_func in self.steps().try_borrow()?.values().cloned() { + step_func.finish( + &mut functions, + &mut codes, + self.steps(), + self.imported_func_count()?, + )?; + } + + self.tick_func(&mut functions, &mut codes, &mut exports)?; + + self.finish_events(&mut functions, &mut codes, &mut exports)?; + + self.unreachable_dbg_func(&mut functions, &mut codes, &mut exports)?; + + match self.flags.scheduler { + Scheduler::CallIndirect => { + let step_count = self.steps().try_borrow()?.len() as u64; + // use register_override just in case we've accidentally defined the threads table elsewhere + let steps_table_index = self.registries().tables().register_override( + "steps".into(), + TableOptions { + element_type: RefType::FUNCREF, + min: step_count, + max: Some(step_count), + init: None, + }, + )?; + #[expect( + clippy::cast_possible_truncation, + reason = "step count should never get near to u32::MAX" + )] + let func_indices: Vec = (0..step_count) + .map(|i| Ok(self.imported_func_count()? + i as u32)) + .collect::>()?; + elements.active( + Some(steps_table_index), + &ConstExpr::i32_const(0), + Elements::Functions(func_indices.into()), + ); + } + Scheduler::TypedFuncRef => { + elements.declared(Elements::Functions( + (self.imported_func_count()? + ..functions.len() + self.imported_func_count()? - 2) + .collect(), + )); + } + } + + self.registries().types().clone().finish(&mut types); + + self.registries() + .tables() + .clone() + .finish(&imports, &mut tables, &mut exports); + + exports.export("memory", ExportKind::Memory, 0); + exports.export("noop", ExportKind::Func, self.imported_func_count()?); + + self.registries() + .globals() + .clone() + .finish(&imports, &mut globals, &mut exports); + + module + .section(&types) + .section(&imports) + .section(&functions) + .section(&tables) + .section(&memories) + .section(&globals) + .section(&exports) + // start + .section(&elements) + //.section(&data_count) + .section(&codes); + //.section(&data); + + let wasm_bytes = module.finish(); + + Ok(FinishedWasm { + wasm_bytes: wasm_bytes.into_boxed_slice(), + strings, + target_names: self + .target_names + .into_iter() + .map(core::convert::Into::into) + .collect(), + }) + } + + fn imported_func_count(&self) -> HQResult { + self.registries() + .external_functions() + .registry() + .try_borrow()? + .len() + .try_into() + .map_err(|_| make_hq_bug!("external function map len out of bounds")) + } + + fn unreachable_dbg_func( + &self, + functions: &mut FunctionSection, + codes: &mut CodeSection, + exports: &mut ExportSection, + ) -> HQResult<()> { + let mut func = Function::new(vec![]); + func.instruction(&Instruction::Unreachable); + func.instruction(&Instruction::End); + codes.function(&func); + functions.function( + self.registries() + .types() + .register_default((vec![], vec![]))?, + ); + exports.export( + "unreachable_dbg", + ExportKind::Func, + self.imported_func_count()? + functions.len() - 1, + ); + + Ok(()) + } + + fn threads_table_index(&self) -> HQResult + where + N: TryFrom, + >::Error: core::fmt::Debug, + { + let step_func_ty = self + .registries() + .types() + .register_default((vec![ValType::I32], vec![]))?; + match self.flags.scheduler { + Scheduler::TypedFuncRef => self.registries().tables().register( + "threads".into(), + TableOptions { + element_type: RefType { + nullable: false, + heap_type: HeapType::Concrete(step_func_ty), + }, + min: 0, + max: None, + // default to noop, just so the module validates. + init: Some(ConstExpr::ref_func(self.imported_func_count()?)), + }), + Scheduler::CallIndirect => hq_bug!("tried to access threads_table_index outside of `WasmProject::Finish` when the scheduler is not TypedFuncRef") + } + } + + fn steps_table_index(&self) -> HQResult + where + N: TryFrom, + >::Error: core::fmt::Debug, + { + match self.flags.scheduler { + Scheduler::CallIndirect => self.registries().tables().register( + "steps".into(), + TableOptions { + element_type: RefType::FUNCREF, + min: 0, + max: None, + init: None + }), + Scheduler::TypedFuncRef => hq_bug!("tried to access steps_table_index outside of `WasmProject::Finish` when the scheduler is not CallIndirect") + } + } + + fn finish_event( + &self, + export_name: &str, + indices: &[u32], + funcs: &mut FunctionSection, + codes: &mut CodeSection, + exports: &mut ExportSection, + ) -> HQResult<()> { + let mut func = Function::new(vec![]); + + let threads_count = self.registries().globals().register( + "threads_count".into(), + ( + ValType::I32, + ConstExpr::i32_const(0), + GlobalMutable(true), + GlobalExportable(true), + ), + )?; + + let instrs = indices + .iter() + .enumerate() + .map(|(position, &i)| { + crate::log( + format!( + "event step idx: {}; func idx: {}", + i, + i + self.imported_func_count()? + ) + .as_str(), + ); + Ok(match self.flags.scheduler { + Scheduler::TypedFuncRef => wasm![ + RefFunc(i + self.imported_func_count()?), + I32Const(1), + TableGrow(self.threads_table_index()?), + Drop, + ], + Scheduler::CallIndirect => wasm![ + GlobalGet(threads_count), + I32Const(4), + I32Mul, + I32Const( + i.try_into() + .map_err(|_| make_hq_bug!("step index out of bounds"))? + ), + I32Store(MemArg { + offset: position as u64 * 4, + align: 2, + memory_index: 0, + }), + ], + }) + }) + .flatten_ok() + .collect::>>()?; + + for instruction in instrs { + func.instruction(&instruction.eval(self.steps(), self.imported_func_count()?)?); + } + for instruction in wasm![ + GlobalGet(threads_count), + I32Const( + i32::try_from(indices.len()) + .map_err(|_| make_hq_bug!("indices len out of bounds"))? + ), + I32Add, + GlobalSet(threads_count) + ] { + func.instruction(&instruction.eval(self.steps(), self.imported_func_count()?)?); + } + func.instruction(&Instruction::End); + + funcs.function( + self.registries() + .types() + .register_default((vec![], vec![]))?, + ); + codes.function(&func); + exports.export( + export_name, + ExportKind::Func, + self.imported_func_count()? + funcs.len() - 1, + ); + + Ok(()) + } + + fn finish_events( + &self, + funcs: &mut FunctionSection, + codes: &mut CodeSection, + exports: &mut ExportSection, + ) -> HQResult<()> { + for (event, indices) in &self.events { + self.finish_event( + match event { + Event::FlagCLicked => "flag_clicked", + }, + indices, + funcs, + codes, + exports, + )?; + } + + Ok(()) + } + + fn tick_func( + &self, + funcs: &mut FunctionSection, + codes: &mut CodeSection, + exports: &mut ExportSection, + ) -> HQResult<()> { + let mut tick_func = Function::new(vec![(2, ValType::I32)]); + + let step_func_ty = self + .registries() + .types() + .register_default((vec![ValType::I32], vec![]))?; + + let threads_count = self.registries().globals().register( + "threads_count".into(), + ( + ValType::I32, + ConstExpr::i32_const(0), + GlobalMutable(true), + GlobalExportable(true), + ), + )?; + + let instructions = match self.flags.scheduler { + crate::wasm::flags::Scheduler::CallIndirect => wasm![ + // For call_indirect: read step indices from linear memory and call_indirect + GlobalGet(threads_count), + LocalTee(1), + I32Eqz, + BrIf(0), + Loop(WasmBlockType::Empty), + LocalGet(0), + LocalGet(0), // thread index + I32Const(4), // 4 bytes per index (i32) + I32Mul, + I32Load(MemArg { + offset: 0, + align: 2, + memory_index: 0, + }), // load step index from memory + CallIndirect { + type_index: step_func_ty, + table_index: self.steps_table_index()?, + }, + LocalGet(0), + I32Const(1), + I32Add, + LocalTee(0), + LocalGet(1), + I32LtS, + BrIf(0), + End, + ], + Scheduler::TypedFuncRef => wasm![ + TableSize(self.threads_table_index()?), + LocalTee(1), + I32Eqz, + BrIf(0), + Loop(WasmBlockType::Empty), + LocalGet(0), + LocalGet(0), + TableGet(self.threads_table_index()?), + CallRef(step_func_ty), + LocalGet(0), + I32Const(1), + I32Add, + LocalTee(0), + LocalGet(1), + I32LtS, + BrIf(0), + End, + ], + }; + for instr in instructions { + tick_func.instruction(&instr.eval(self.steps(), self.imported_func_count()?)?); + } + tick_func.instruction(&Instruction::End); + funcs.function( + self.registries() + .types() + .register_default((vec![], vec![]))?, + ); + codes.function(&tick_func); + exports.export( + "tick", + ExportKind::Func, + funcs.len() + self.imported_func_count()? - 1, + ); + Ok(()) + } + + pub fn from_ir(ir_project: &Rc, flags: WasmFlags) -> HQResult { + let steps: Rc, StepFunc>>> = + Rc::new(RefCell::new(IndexMap::default())); + let registries = Rc::new(Registries::default()); + let mut events: BTreeMap> = BTreeMap::default(); + StepFunc::compile_step( + Rc::new(Step::new_empty()), + &steps, + Rc::clone(®istries), + flags, + )?; + // compile every step + for step in ir_project.steps().try_borrow()?.iter() { + StepFunc::compile_step(Rc::clone(step), &steps, Rc::clone(®istries), flags)?; + } + // mark first steps as used in a non-inline context + for thread in ir_project.threads().try_borrow()?.iter() { + thread.first_step().make_used_non_inline()?; + } + // get rid of steps which aren't used in a non-inlined context + for step in ir_project.steps().try_borrow()?.iter() { + if *step.inline().try_borrow()? && !*step.used_non_inline().try_borrow()? { + steps.try_borrow_mut()?.swap_remove(step); + } + } + // add thread event handlers for them + for thread in ir_project.threads().try_borrow()?.iter() { + events.entry(thread.event()).or_default().push( + u32::try_from( + steps + .try_borrow()? + .get_index_of(thread.first_step()) + .ok_or_else(|| { + make_hq_bug!("Thread's first_step wasn't found in Thread::steps()") + })?, + ) + .map_err(|_| make_hq_bug!("step func index out of bounds"))?, + ); + } + Ok(Self { + flags, + steps, + events, + registries, + environment: ExternalEnvironment::WebBrowser, + target_names: ir_project.targets().try_borrow()?.keys().cloned().collect(), + }) + } +} + +#[wasm_bindgen] +#[derive(Clone)] +pub struct FinishedWasm { + #[wasm_bindgen(getter_with_clone)] + pub wasm_bytes: Box<[u8]>, + #[wasm_bindgen(getter_with_clone)] + pub strings: Vec, + #[wasm_bindgen(getter_with_clone)] + pub target_names: Vec, +} + +#[cfg(test)] +mod tests { + use super::{Registries, WasmProject}; + use crate::ir::Step; + use crate::prelude::*; + use crate::wasm::{flags::all_wasm_features, ExternalEnvironment, StepFunc, WasmFlags}; + + #[test] + fn empty_project_is_valid_wasm() { + let registries = Rc::new(Registries::default()); + let steps = Rc::new(RefCell::new(IndexMap::default())); + StepFunc::compile_step( + Rc::new(Step::new_empty()), + &steps, + Rc::clone(®istries), + WasmFlags::new(all_wasm_features()), + ) + .unwrap(); + let project = WasmProject { + flags: WasmFlags::new(all_wasm_features()), + steps, + events: BTreeMap::new(), + environment: ExternalEnvironment::WebBrowser, + registries, + target_names: vec![], + }; + let wasm_bytes = project.finish().unwrap().wasm_bytes; + if let Err(err) = wasmparser::validate(&wasm_bytes) { + panic!( + "wasmparser error: {:?}\nwasm:\n{}", + err, + wasmprinter::print_bytes(wasm_bytes).unwrap() + ) + } + } +} diff --git a/src/wasm/registries.rs b/src/wasm/registries.rs new file mode 100644 index 00000000..19947d0e --- /dev/null +++ b/src/wasm/registries.rs @@ -0,0 +1,55 @@ +use super::{ + ExternalFunctionRegistry, GlobalRegistry, StringRegistry, TableRegistry, TypeRegistry, + VariableRegistry, +}; +use crate::prelude::*; + +pub struct Registries { + strings: StringRegistry, + external_functions: ExternalFunctionRegistry, + types: TypeRegistry, + tables: TableRegistry, + globals: Rc, + variables: VariableRegistry, +} + +impl Default for Registries { + fn default() -> Self { + let globals = Rc::new(GlobalRegistry::default()); + let variables = VariableRegistry::new(&globals); + Self { + globals, + variables, + strings: StringRegistry::default(), + external_functions: ExternalFunctionRegistry::default(), + tables: TableRegistry::default(), + types: TypeRegistry::default(), + } + } +} + +impl Registries { + pub const fn strings(&self) -> &StringRegistry { + &self.strings + } + + pub const fn external_functions(&self) -> &ExternalFunctionRegistry { + &self.external_functions + } + + pub const fn types(&self) -> &TypeRegistry { + &self.types + } + + pub const fn tables(&self) -> &TableRegistry { + &self.tables + } + + pub fn globals(&self) -> &GlobalRegistry { + &self.globals + } + + pub const fn variables(&self) -> &VariableRegistry { + &self.variables + } +} diff --git a/src/wasm/strings.rs b/src/wasm/strings.rs new file mode 100644 index 00000000..5b764f76 --- /dev/null +++ b/src/wasm/strings.rs @@ -0,0 +1,15 @@ +use crate::prelude::*; +use crate::registry::SetRegistry; + +pub type StringRegistry = SetRegistry>; + +impl StringRegistry { + pub fn finish(self) -> Vec { + self.registry() + .take() + .keys() + .cloned() + .map(str::into_string) + .collect() + } +} diff --git a/src/wasm/tables.rs b/src/wasm/tables.rs new file mode 100644 index 00000000..779d11c8 --- /dev/null +++ b/src/wasm/tables.rs @@ -0,0 +1,62 @@ +use crate::prelude::*; +use crate::registry::MapRegistry; +use wasm_encoder::{ + ConstExpr, ExportKind, ExportSection, ImportSection, RefType, TableSection, TableType, +}; + +#[derive(Clone, Debug)] +pub struct TableOptions { + pub element_type: RefType, + pub min: u64, + pub max: Option, + pub init: Option, +} + +pub type TableRegistry = MapRegistry, TableOptions>; + +impl TableRegistry { + pub fn finish( + self, + imports: &ImportSection, + tables: &mut TableSection, + exports: &mut ExportSection, + ) { + for ( + key, + TableOptions { + element_type, + min, + max, + init, + }, + ) in self.registry().take() + { + // TODO: allow choosing whether to export a table or not? + exports.export(&key, ExportKind::Table, tables.len()); + let maybe_init = match &*key { + "threads" => Some(ConstExpr::ref_func(imports.len())), + _ => init, + }; + if let Some(init) = maybe_init { + tables.table_with_init( + TableType { + element_type, + minimum: min, + maximum: max, + table64: false, + shared: false, + }, + &init, + ); + } else { + tables.table(TableType { + element_type, + minimum: min, + maximum: max, + table64: false, + shared: false, + }); + } + } + } +} diff --git a/src/wasm/type_registry.rs b/src/wasm/type_registry.rs new file mode 100644 index 00000000..646391cc --- /dev/null +++ b/src/wasm/type_registry.rs @@ -0,0 +1,13 @@ +use crate::prelude::*; +use crate::registry::SetRegistry; +use wasm_encoder::{TypeSection, ValType}; + +pub type TypeRegistry = SetRegistry<(Vec, Vec)>; + +impl TypeRegistry { + pub fn finish(self, types: &mut TypeSection) { + for (params, results) in self.registry().take().keys().cloned() { + types.ty().function(params, results); + } + } +} diff --git a/src/wasm/variable.rs b/src/wasm/variable.rs new file mode 100644 index 00000000..783bddad --- /dev/null +++ b/src/wasm/variable.rs @@ -0,0 +1,34 @@ +use crate::ir::{RcVar, Type as IrType}; +use crate::prelude::*; +use wasm_encoder::{ConstExpr, HeapType}; + +use super::{GlobalExportable, GlobalMutable, GlobalRegistry, WasmProject}; + +pub struct VariableRegistry(Rc); + +impl VariableRegistry { + const fn globals(&self) -> &Rc { + &self.0 + } + + pub fn new(globals: &Rc) -> Self { + Self(Rc::clone(globals)) + } + + pub fn register(&self, var: &RcVar) -> HQResult { + self.globals().register( + format!("__rcvar_{:p}", Rc::as_ptr(&var.0)).into(), + ( + WasmProject::ir_type_to_wasm(*var.0.possible_types())?, + match var.0.possible_types().base_type() { + Some(IrType::Float) => ConstExpr::f64_const(0.0), + Some(IrType::QuasiInt) => ConstExpr::i32_const(0), + Some(IrType::String) => ConstExpr::ref_null(HeapType::EXTERN), + _ => ConstExpr::i64_const(0), // TODO: use the variable's initial value + }, + GlobalMutable(true), + GlobalExportable(false), + ), + ) + } +} diff --git a/wasm-gen/.gitignore b/wasm-gen/.gitignore new file mode 100644 index 00000000..f2f9e58e --- /dev/null +++ b/wasm-gen/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock \ No newline at end of file diff --git a/wasm-gen/Cargo.toml b/wasm-gen/Cargo.toml new file mode 100644 index 00000000..f3eb4b88 --- /dev/null +++ b/wasm-gen/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "wasm-gen" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "1.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" +proc-macro-error = "1.0" \ No newline at end of file diff --git a/wasm-gen/README.md b/wasm-gen/README.md new file mode 100644 index 00000000..bc5a6d5c --- /dev/null +++ b/wasm-gen/README.md @@ -0,0 +1,10 @@ +# wasm-gen + +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. + +## Usage + +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: +- `@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`. +- `@box(input_name)` - boxes the top value on the stack if `input_name` is a base type +- `@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`. \ No newline at end of file diff --git a/wasm-gen/src/lib.rs b/wasm-gen/src/lib.rs new file mode 100644 index 00000000..32cb3d33 --- /dev/null +++ b/wasm-gen/src/lib.rs @@ -0,0 +1,276 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote, quote_spanned}; +use std::collections::{HashMap, HashSet}; +use syn::parse::{Parse, ParseStream}; +use syn::{parenthesized, Error as SynError, Expr, Ident, Token}; + +enum Item { + Instruction { expr: Expr }, + SpecialInstruction { expr: Expr }, + NanReduce { input_ident: Ident }, + IsNan { input_ident: Ident }, + Box { input_ident: Ident }, + Error(TokenStream2), +} + +impl Parse for Item { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(Token![@]) { + input.parse::()?; + let ident: Ident = input.parse()?; + match ident.to_string().as_str() { + "nanreduce" => { + let content; + parenthesized!(content in input); + if let Ok(input_ident) = content.parse::() { + Ok(Item::NanReduce { input_ident }) + } else { + let span = content.span(); + Ok(Item::Error( + quote_spanned! { span=> compile_error!("Expected an ident in @nanreduce") }, + )) + } + } + "isnan" => { + let content; + parenthesized!(content in input); + if let Ok(input_ident) = content.parse::() { + Ok(Item::IsNan { input_ident }) + } else { + let span = content.span(); + Ok(Item::Error( + quote_spanned! { span=> compile_error!("Expected an ident in @isnan") }, + )) + } + } + "boxed" => { + let content; + parenthesized!(content in input); + if let Ok(input_ident) = content.parse::() { + Ok(Item::Box { input_ident }) + } else { + let span = content.span(); + Ok(Item::Error( + quote_spanned! { span=> compile_error!("Expected an ident in @boxed") }, + )) + } + } + _ => Err(SynError::new(ident.span(), "Unknown special instruction")), + } + } else if input.peek(Token![#]) { + input.parse::()?; + let expr: Expr = input.parse()?; + Ok(Item::SpecialInstruction { expr }) + } else { + let expr: Expr = input.parse()?; + Ok(Item::Instruction { expr }) + } + } +} + +struct WasmInput { + items: Vec, + nan_checks: HashSet, + boxed_checks: HashSet, +} + +impl Parse for WasmInput { + fn parse(input: ParseStream) -> syn::Result { + let mut items = Vec::new(); + let mut nan_checks = HashSet::new(); + let mut boxed_checks = HashSet::new(); + while !input.is_empty() { + let item = input.parse()?; + if let Item::NanReduce { ref input_ident } | Item::IsNan { ref input_ident } = item { + nan_checks.insert(input_ident.clone().to_string()); + } + if let Item::Box { ref input_ident } = item { + boxed_checks.insert(input_ident.clone().to_string()); + } + items.push(item); + if input.peek(Token![,]) { + input.parse::()?; + } else { + break; + } + } + Ok(WasmInput { + items, + nan_checks, + boxed_checks, + }) + } +} + +#[proc_macro] +pub fn wasm(input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as WasmInput); + + let nan_checks: Vec<_> = parsed.nan_checks.into_iter().collect(); + let boxed_checks: Vec<_> = parsed.boxed_checks.into_iter().collect(); + + if nan_checks.is_empty() && boxed_checks.is_empty() { + let instructions = parsed.items.iter().filter_map(|item| { + if let Item::Instruction { expr } = item { + Some(quote! { Immediate(wasm_encoder::Instruction::#expr) }) + } else if let Item::SpecialInstruction { expr } = item { + Some(quote! { #expr }) + } else { + None + } + }); + quote! { vec![#(crate::wasm::InternalInstruction::#instructions),*] } + } else { + let conditions = (0..(1 << (nan_checks.len() + 2 * boxed_checks.len()))).map(|mask| { + let checks = nan_checks.iter().enumerate().map(|(i, ident)| { + let ident = format_ident!("{ident}"); + let nan_check = quote! { #ident.contains(crate::ir::Type::FloatNan) }; + let not_nan_check = quote! { !#ident.contains(crate::ir::Type::FloatNan) }; + if (mask & (1 << i)) == 0 { + nan_check + } else { + not_nan_check + } + }).chain(boxed_checks.iter().enumerate().map(|(i, ident)| (i * 2 + nan_checks.len(), ident)).map(|(i,ident)| { + let ident = format_ident!("{ident}"); + let boxed_check = quote! { !#ident.is_base_type() }; + let string_check = quote! { #ident.base_type() == Some(crate::ir::Type::String) }; + let float_check = quote! { #ident.base_type() == Some(crate::ir::Type::Float) }; + let int_check = quote! { #ident.base_type() == Some(crate::ir::Type::QuasiInt) }; + if (mask & ((1 << i) + (1 << (i + 1)))) == 0 { + boxed_check + } else if (mask & ((1 << i) + (1 << (i + 1)))) == 1 { + string_check + } else if (mask & ((1 << i) + (1 << (i + 1)))) == 2 { + float_check + } else { + int_check + } + })); + let these_nan: HashSet<_> = nan_checks + .iter() + .enumerate() + .filter_map(|(i, expr)| { + if (mask & (1 << i)) == 0 { + Some(expr) + } else { + None + } + }) + .collect(); + let these_unboxed: HashMap<_, _> = boxed_checks + .iter() + .enumerate() + .filter_map(|(i, expr)| { + let state = mask & ((1 << i) + (1 << (i + 1))); + if state == 0 { + None + } else { + Some((expr, state)) + } + }) + .collect(); + let (instructions, locals): (Vec<_>, Vec<_>) = parsed + .items + .iter() + .enumerate() + .filter_map(|(i, item)| match item { + Item::Instruction { expr } => { + Some((vec![quote! { Immediate(wasm_encoder::Instruction::#expr) }].into_iter(), None)) + } + Item::NanReduce { input_ident } => { + if these_nan.contains(&input_ident.clone().to_string()) { + let local_ident = format_ident!("__local_{}", i); + Some(( + vec![ + quote! { Immediate(wasm_encoder::Instruction::LocalTee(#local_ident)) }, + quote! { Immediate(wasm_encoder::Instruction::LocalGet(#local_ident)) }, + quote! { Immediate(wasm_encoder::Instruction::F64Eq) }, + quote! { Immediate(wasm_encoder::Instruction::If(wasm_encoder::BlockType::Result(wasm_encoder::ValType::F64))) }, + quote! { Immediate(wasm_encoder::Instruction::LocalGet(#local_ident)) }, + quote! { Immediate(wasm_encoder::Instruction::Else) }, + quote! { Immediate(wasm_encoder::Instruction::F64Const(0.0)) }, + quote! { Immediate(wasm_encoder::Instruction::End) } + ] + .into_iter(), + Some((local_ident, format_ident!("F64"))) + )) + } else { + None + } + } + Item::IsNan { input_ident } => { + if these_nan.contains(&input_ident.clone().to_string()) { + let local_ident = format_ident!("__local_{}", i); + Some(( + vec![ + quote! { Immediate(wasm_encoder::Instruction::LocalTee(#local_ident)) }, + quote! { Immediate(wasm_encoder::Instruction::LocalGet(#local_ident)) }, + quote! { Immediate(wasm_encoder::Instruction::F64Ne) }, + ] + .into_iter(), + Some((local_ident, format_ident!("F64"))) + )) + } else { + Some((vec![ + quote! { Immediate(wasm_encoder::Instruction::Drop) }, + quote! { Immediate(wasm_encoder::Instruction::I32Const(0)) } + ].into_iter(), None)) + } + } + Item::Box { input_ident} => { + let local_ident = format_ident!("__local_{}", i); + if let Some(state) = these_unboxed.get(&input_ident.clone().to_string()) { + match state { + 1 => Some((vec![ + quote! { Immediate(wasm_encoder::Instruction::I32Const(1)) }, + quote! { Immediate(wasm_encoder::Instruction::TableGrow(__strings_table_index)) }, + quote! { Immediate(wasm_encoder::Instruction::I64ExtendI32S) }, + quote! { Immediate(wasm_encoder::Instruction::I64Const(BOXED_STRING_PATTERN)) }, + quote! { Immediate(wasm_encoder::Instruction::I64Or) }, + ].into_iter(), Some((local_ident, format_ident!("EXTERNREF"))))), + 2 => Some((vec![quote! { Immediate(wasm_encoder::Instruction::I64ReinterpretF64) }].into_iter(), None)), + 3 => Some((vec![ + quote! { Immediate(wasm_encoder::Instruction::I64ExtendI32S) }, + quote! { Immediate(wasm_encoder::Instruction::I64Const(BOXED_INT_PATTERN)) }, + quote! { Immediate(wasm_encoder::Instruction::I64Or) }, + ].into_iter(), None)), + _ => panic!("invalid state") + } + } else { + None + } + } + Item::SpecialInstruction { expr } => Some((vec![quote! { #expr }].into_iter(), None)), + Item::Error(ts) => Some((vec![ts.clone()].into_iter(), None)) + }) + .unzip(); + let instructions = instructions.into_iter().flatten(); + let (local_names, local_types): (Vec<_>, Vec<_>) = locals.into_iter().flatten().unzip(); + quote! { + if #(#checks)&&* { + #(let #local_names = func.local(wasm_encoder::ValType::#local_types)?;)* + vec![#(crate::wasm::InternalInstruction::#instructions),*] + } + } + }); + quote! { + { + let __strings_table_index: u32 = func + .registries() + .tables() + .register("strings".into(), crate::wasm::TableOptions { + element_type: RefType::EXTERNREF, + min: 0, + max: None, + init: None, + })?; + #(#conditions) else * else { + unreachable!() + } + } + } + } + .into() +}