├── rustfmt.toml ├── wasm-gen ├── .gitignore ├── Cargo.toml └── README.md ├── enum-field-getter ├── .gitignore ├── Cargo.toml └── README.md ├── rust-toolchain.toml ├── src ├── instructions │ ├── sensing.rs │ ├── control.rs │ ├── procedures.rs │ ├── data.rs │ ├── motion.rs │ ├── pen.rs │ ├── hq.rs │ ├── looks.rs │ ├── hq │ │ ├── drop.rs │ │ ├── integer.rs │ │ ├── boolean.rs │ │ ├── color_rgb.rs │ │ ├── text.rs │ │ └── float.rs │ ├── operator │ │ ├── not.rs │ │ ├── and.rs │ │ ├── or.rs │ │ ├── length.rs │ │ ├── contains.rs │ │ ├── acos.rs │ │ ├── asin.rs │ │ ├── atan.rs │ │ ├── join.rs │ │ ├── sqrt.rs │ │ ├── tan.rs │ │ ├── cos.rs │ │ ├── sin.rs │ │ ├── exp.rs │ │ ├── pow10.rs │ │ ├── letter_of.rs │ │ ├── log.rs │ │ ├── divide.rs │ │ ├── ceiling.rs │ │ ├── floor.rs │ │ ├── abs.rs │ │ ├── add.rs │ │ ├── subtract.rs │ │ └── multiply.rs │ ├── operator.rs │ ├── pen │ │ ├── clear.rs │ │ ├── penup.rs │ │ ├── setpensizeto.rs │ │ ├── pendown.rs │ │ ├── setpencolortocolor.rs │ │ └── setpencolorparamto.rs │ ├── sensing │ │ └── dayssince2000.rs │ ├── looks │ │ ├── size.rs │ │ ├── costumenumber.rs │ │ ├── setvisible.rs │ │ ├── setsizeto.rs │ │ ├── switchcostumeto.rs │ │ ├── say.rs │ │ └── think.rs │ ├── motion │ │ ├── direction.rs │ │ ├── pointindirection.rs │ │ └── gotoxy.rs │ ├── procedures │ │ ├── argument.rs │ │ └── call_warp.rs │ ├── control │ │ ├── if_else.rs │ │ └── loop.rs │ └── data │ │ ├── variable.rs │ │ ├── setvariableto.rs │ │ └── teevariable.rs ├── wasm │ ├── external.rs │ ├── registries │ │ ├── targets.rs │ │ ├── types.rs │ │ ├── strings.rs │ │ ├── variables.rs │ │ ├── globals.rs │ │ └── tables.rs │ ├── registries.rs │ └── mem_layout.rs ├── ir │ ├── event.rs │ ├── context.rs │ ├── thread.rs │ ├── target.rs │ └── variable.rs ├── optimisation.rs ├── wasm.rs ├── ir.rs ├── alloc.rs ├── rc.rs ├── instructions.rs └── lib.rs ├── playground ├── lib │ ├── imports.js │ ├── project-loader.js │ └── settings.js ├── public │ ├── logo.png │ └── favicon.ico ├── assets │ ├── logo.svg │ ├── main.css │ └── base.css ├── main.js ├── views │ ├── 404.vue │ ├── ProjectIdView.vue │ ├── TestProject.vue │ ├── AboutView.vue │ ├── ProjectFileView.vue │ ├── HomeView.vue │ └── Settings.vue ├── stores │ ├── projectfile.js │ └── debug.js ├── components │ ├── Loading.vue │ ├── ProjectIdPlayer.vue │ ├── ProjectFileInput.vue │ └── ProjectInput.vue ├── index.html ├── router │ └── index.js └── App.vue ├── js ├── operator │ ├── acos.ts │ ├── asin.ts │ ├── atan.ts │ ├── exp.ts │ ├── log.ts │ ├── random.ts │ ├── sin.ts │ ├── cos.ts │ ├── pow10.ts │ ├── gt_string.ts │ ├── lt_string.ts │ ├── contains.ts │ ├── eq_string.ts │ └── tan.ts ├── cast │ ├── int2string.ts │ ├── float2string.ts │ └── string2float.ts ├── wasm-js-string │ ├── length.ts │ ├── concat.ts │ └── substring.ts ├── pen │ ├── clear.ts │ ├── point.ts │ └── line.ts ├── looks │ ├── say_string.ts │ ├── say_float.ts │ ├── say_int.ts │ ├── think_string.ts │ ├── think_int.ts │ ├── think_float.ts │ ├── say_debug_int.ts │ ├── say_debug_float.ts │ ├── say_debug_string.ts │ ├── think_debug_int.ts │ ├── think_debug_float.ts │ ├── think_debug_string.ts │ ├── setsizeto.ts │ ├── setvisible.ts │ └── switchcostumeto.ts ├── motion │ ├── gotoxy.ts │ └── pointindirection.ts ├── sensing │ └── dayssince2000.ts └── shared.ts ├── clippy.toml ├── .gitignore ├── .devcontainer └── devcontainer.json ├── opcodes.mjs ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── deploy.yml │ └── ci-checks.yaml ├── vite.config.js ├── package.json ├── LICENSE-MIT ├── CONTRIBUTING.md └── Cargo.toml /rustfmt.toml: -------------------------------------------------------------------------------- 1 | unstable_features = true -------------------------------------------------------------------------------- /wasm-gen/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock -------------------------------------------------------------------------------- /enum-field-getter/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" -------------------------------------------------------------------------------- /src/instructions/sensing.rs: -------------------------------------------------------------------------------- 1 | pub mod dayssince2000; 2 | -------------------------------------------------------------------------------- /src/instructions/control.rs: -------------------------------------------------------------------------------- 1 | pub mod if_else; 2 | pub mod r#loop; 3 | -------------------------------------------------------------------------------- /playground/lib/imports.js: -------------------------------------------------------------------------------- 1 | export { imports } from '../../js/imports.ts'; -------------------------------------------------------------------------------- /src/instructions/procedures.rs: -------------------------------------------------------------------------------- 1 | pub mod argument; 2 | pub mod call_warp; 3 | -------------------------------------------------------------------------------- /js/operator/acos.ts: -------------------------------------------------------------------------------- 1 | export function acos(n: number): number { 2 | return Math.acos(n); 3 | } -------------------------------------------------------------------------------- /js/operator/asin.ts: -------------------------------------------------------------------------------- 1 | export function asin(n: number): number { 2 | return Math.asin(n); 3 | } -------------------------------------------------------------------------------- /js/operator/atan.ts: -------------------------------------------------------------------------------- 1 | export function atan(n: number): number { 2 | return Math.atan(n); 3 | } -------------------------------------------------------------------------------- /js/operator/exp.ts: -------------------------------------------------------------------------------- 1 | export function exp(n: number): number { 2 | return Math.exp(n); 3 | } -------------------------------------------------------------------------------- /js/operator/log.ts: -------------------------------------------------------------------------------- 1 | export function log(n: number): number { 2 | return Math.log(n); 3 | } -------------------------------------------------------------------------------- /js/operator/random.ts: -------------------------------------------------------------------------------- 1 | export function random(): number { 2 | return Math.random(); 3 | } 4 | -------------------------------------------------------------------------------- /js/operator/sin.ts: -------------------------------------------------------------------------------- 1 | export function sin(theta: number): number { 2 | return Math.sin(theta); 3 | } -------------------------------------------------------------------------------- /src/instructions/data.rs: -------------------------------------------------------------------------------- 1 | pub mod setvariableto; 2 | pub mod teevariable; 3 | pub mod variable; 4 | -------------------------------------------------------------------------------- /js/cast/int2string.ts: -------------------------------------------------------------------------------- 1 | export function int2string(s: number): string { 2 | return s.toString(); 3 | } -------------------------------------------------------------------------------- /js/operator/cos.ts: -------------------------------------------------------------------------------- 1 | export function cos(theta: number): number { 2 | return Math.cos(theta); 3 | } -------------------------------------------------------------------------------- /js/operator/pow10.ts: -------------------------------------------------------------------------------- 1 | export function pow10(n: number): number { 2 | return Math.pow(10, n); 3 | } 4 | -------------------------------------------------------------------------------- /src/instructions/motion.rs: -------------------------------------------------------------------------------- 1 | pub mod direction; 2 | pub mod gotoxy; 3 | pub mod pointindirection; 4 | -------------------------------------------------------------------------------- /js/cast/float2string.ts: -------------------------------------------------------------------------------- 1 | export function float2string(s: number): string { 2 | return s.toString(); 3 | } -------------------------------------------------------------------------------- /js/cast/string2float.ts: -------------------------------------------------------------------------------- 1 | export function string2float(s: string): number { 2 | return parseFloat(s); 3 | } -------------------------------------------------------------------------------- /js/wasm-js-string/length.ts: -------------------------------------------------------------------------------- 1 | export function length(string: string): number { 2 | return string.length; 3 | } -------------------------------------------------------------------------------- /playground/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperQuark/hyperquark/HEAD/playground/public/logo.png -------------------------------------------------------------------------------- /playground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperQuark/hyperquark/HEAD/playground/public/favicon.ico -------------------------------------------------------------------------------- /js/wasm-js-string/concat.ts: -------------------------------------------------------------------------------- 1 | export function concat(left: string, right: string): string { 2 | return left.concat(right); 3 | } -------------------------------------------------------------------------------- /src/wasm/external.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Copy, Clone)] 2 | #[non_exhaustive] 3 | pub enum ExternalEnvironment { 4 | WebBrowser, 5 | } 6 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | allowed-duplicate-crates = ["syn"] 2 | too-many-lines-threshold = 150 3 | allow-panic-in-tests = true 4 | allow-unwrap-in-tests = true -------------------------------------------------------------------------------- /js/operator/gt_string.ts: -------------------------------------------------------------------------------- 1 | export function gt_string(left: string, right: string): boolean { 2 | return left.toLowerCase() > right.toLowerCase(); 3 | } -------------------------------------------------------------------------------- /js/operator/lt_string.ts: -------------------------------------------------------------------------------- 1 | export function lt_string(left: string, right: string): boolean { 2 | return left.toLowerCase() < right.toLowerCase(); 3 | } -------------------------------------------------------------------------------- /js/pen/clear.ts: -------------------------------------------------------------------------------- 1 | import { renderer, pen_skin } from '../shared'; 2 | 3 | export function clear() { 4 | renderer().penClear(pen_skin()); 5 | } 6 | -------------------------------------------------------------------------------- /js/operator/contains.ts: -------------------------------------------------------------------------------- 1 | export function contains(left: string, right: string): boolean { 2 | return left.toLowerCase().includes(right.toLowerCase()); 3 | } -------------------------------------------------------------------------------- /js/operator/eq_string.ts: -------------------------------------------------------------------------------- 1 | export function eq_string(left: string, right: string): boolean { 2 | return left.toLowerCase() === right.toLowerCase(); 3 | } -------------------------------------------------------------------------------- /js/wasm-js-string/substring.ts: -------------------------------------------------------------------------------- 1 | export function substring(string: string, start: number, end: number): string { 2 | return string.substring(start, end); 3 | } -------------------------------------------------------------------------------- /src/instructions/pen.rs: -------------------------------------------------------------------------------- 1 | pub mod clear; 2 | pub mod pendown; 3 | pub mod penup; 4 | pub mod setpencolorparamto; 5 | pub mod setpencolortocolor; 6 | pub mod setpensizeto; 7 | -------------------------------------------------------------------------------- /src/instructions/hq.rs: -------------------------------------------------------------------------------- 1 | pub mod boolean; 2 | pub mod cast; 3 | pub mod color_rgb; 4 | pub mod drop; 5 | pub mod float; 6 | pub mod integer; 7 | pub mod text; 8 | pub mod r#yield; 9 | -------------------------------------------------------------------------------- /src/instructions/looks.rs: -------------------------------------------------------------------------------- 1 | pub mod costumenumber; 2 | pub mod say; 3 | pub mod setsizeto; 4 | pub mod setvisible; 5 | pub mod size; 6 | pub mod switchcostumeto; 7 | pub mod think; 8 | -------------------------------------------------------------------------------- /js/looks/say_string.ts: -------------------------------------------------------------------------------- 1 | import { update_bubble } from "../shared"; 2 | 3 | export function say_string(data: string, target_idx: number): void { 4 | update_bubble(target_idx, "say", data); 5 | } -------------------------------------------------------------------------------- /src/wasm/registries/targets.rs: -------------------------------------------------------------------------------- 1 | use crate::ir::Target as IrTarget; 2 | use crate::prelude::*; 3 | use crate::registry::SetRegistry; 4 | 5 | pub type SpriteRegistry = SetRegistry>; 6 | -------------------------------------------------------------------------------- /js/looks/say_float.ts: -------------------------------------------------------------------------------- 1 | import { update_bubble } from "../shared"; 2 | 3 | export function say_float(data: number, target_idx: number): void { 4 | update_bubble(target_idx, "say", data.toString()); 5 | } -------------------------------------------------------------------------------- /js/looks/say_int.ts: -------------------------------------------------------------------------------- 1 | import { update_bubble } from "../shared"; 2 | 3 | export function say_int(data: number, target_idx: number): void { 4 | update_bubble(target_idx, "say", data.toString()); 5 | } -------------------------------------------------------------------------------- /js/looks/think_string.ts: -------------------------------------------------------------------------------- 1 | import { update_bubble } from "../shared"; 2 | 3 | export function think_string(data: string, target_idx: number): void { 4 | update_bubble(target_idx, "think", data); 5 | } -------------------------------------------------------------------------------- /js/looks/think_int.ts: -------------------------------------------------------------------------------- 1 | import { update_bubble } from "../shared"; 2 | 3 | export function think_int(data: number, target_idx: number): void { 4 | update_bubble(target_idx, "think", data.toString()); 5 | } -------------------------------------------------------------------------------- /js/looks/think_float.ts: -------------------------------------------------------------------------------- 1 | import { update_bubble } from "../shared"; 2 | 3 | export function think_float(data: number, target_idx: number): void { 4 | update_bubble(target_idx, "think", data.toString()); 5 | } -------------------------------------------------------------------------------- /js/looks/say_debug_int.ts: -------------------------------------------------------------------------------- 1 | import { target_names } from "../shared"; 2 | 3 | export function say_debug_int(data: number, target_idx: number): void { 4 | console.log('%s says: %d', target_names()[target_idx], data); 5 | } -------------------------------------------------------------------------------- /src/ir/event.rs: -------------------------------------------------------------------------------- 1 | // Ord is required to be used in a BTreeMap; Ord requires PartialOrd, Eq and PartialEq 2 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 3 | pub enum Event { 4 | FlagCLicked, 5 | } 6 | -------------------------------------------------------------------------------- /js/looks/say_debug_float.ts: -------------------------------------------------------------------------------- 1 | import { target_names } from "../shared"; 2 | 3 | export function say_debug_float(data: number, target_idx: number): void { 4 | console.log('%s says: %d', target_names()[target_idx], data); 5 | } -------------------------------------------------------------------------------- /js/looks/say_debug_string.ts: -------------------------------------------------------------------------------- 1 | import { target_names } from "../shared"; 2 | 3 | export function say_debug_string(data: string, target_idx: number): void { 4 | console.log('%s says: %s', target_names()[target_idx], data); 5 | } -------------------------------------------------------------------------------- /js/looks/think_debug_int.ts: -------------------------------------------------------------------------------- 1 | import { target_names } from "../shared"; 2 | 3 | export function think_debug_int(data: number, target_idx: number): void { 4 | console.log('%s thinks: %d', target_names()[target_idx], data); 5 | } -------------------------------------------------------------------------------- /js/looks/think_debug_float.ts: -------------------------------------------------------------------------------- 1 | import { target_names } from "../shared"; 2 | 3 | export function think_debug_float(data: number, target_idx: number): void { 4 | console.log('%s thinks: %d', target_names()[target_idx], data); 5 | } -------------------------------------------------------------------------------- /js/looks/think_debug_string.ts: -------------------------------------------------------------------------------- 1 | import { target_names } from "../shared"; 2 | 3 | export function think_debug_string(data: string, target_idx: number): void { 4 | console.log('%s thinks: %s', target_names()[target_idx], data); 5 | } -------------------------------------------------------------------------------- /js/motion/gotoxy.ts: -------------------------------------------------------------------------------- 1 | import { renderer, target_skins } from '../shared'; 2 | 3 | export function gotoxy(x: number, y: number, target_index: number) { 4 | renderer().getDrawable(target_skins()[target_index][1]).updatePosition([x, y]); 5 | } 6 | -------------------------------------------------------------------------------- /js/looks/setsizeto.ts: -------------------------------------------------------------------------------- 1 | import { renderer, costumes, target_skins } from '../shared'; 2 | 3 | export function setsizeto(size: number, target_index: number) { 4 | renderer().getDrawable(target_skins()[target_index][1]).updateScale([size, size]); 5 | } 6 | -------------------------------------------------------------------------------- /js/looks/setvisible.ts: -------------------------------------------------------------------------------- 1 | import { renderer, costumes, target_skins } from '../shared'; 2 | 3 | export function setvisible(visible: boolean, target_index: number) { 4 | renderer().getDrawable(target_skins()[target_index][1]).updateVisible(visible); 5 | } 6 | -------------------------------------------------------------------------------- /js/motion/pointindirection.ts: -------------------------------------------------------------------------------- 1 | import { renderer, target_skins } from '../shared'; 2 | 3 | export function pointindirection(target_index: number, direction: number) { 4 | renderer().getDrawable(target_skins()[target_index][1]).updateDirection(direction); 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /Cargo.lock 3 | /bad.wasm 4 | /bad.mjs 5 | /bad.wat 6 | /wabt* 7 | /js/compiler 8 | /js/no-compiler 9 | /js/imports.ts 10 | /js/opcodes.js 11 | /node_modules/ 12 | /package-lock.json 13 | /playground/dist/ 14 | /.vite/ 15 | /.vscode/ 16 | rustc-ice* -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/universal:2", 3 | "features": { 4 | "ghcr.io/devcontainers/features/rust:1": {}, 5 | "devwasm.azurecr.io/dev-wasm/dev-wasm-feature/rust-wasi:0": {} 6 | }, 7 | "ghcr.io/devcontainers/features/rust:1": {} 8 | } 9 | -------------------------------------------------------------------------------- /playground/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /wasm-gen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-gen" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | syn = { version = "1.0", features = ["full"] } 12 | quote = "1.0" 13 | proc-macro2 = "1.0" 14 | proc-macro-error = "1.0" -------------------------------------------------------------------------------- /js/looks/switchcostumeto.ts: -------------------------------------------------------------------------------- 1 | import { renderer, costumes, target_skins } from '../shared'; 2 | 3 | export function switchcostumeto(costume_num: number, target_index: number) { 4 | const costume = costumes()[target_index][costume_num]; 5 | renderer().getSkin(target_skins()[target_index][0]).setSVG(costume.data); 6 | } -------------------------------------------------------------------------------- /js/operator/tan.ts: -------------------------------------------------------------------------------- 1 | export function tan(angle: number) { 2 | angle = angle % 360; 3 | switch (angle) { 4 | case -270: 5 | case 90: 6 | return Infinity; 7 | case -90: 8 | case 270: 9 | return -Infinity; 10 | default: 11 | return Math.tan((Math.PI * angle) / 180); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /playground/main.js: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | 3 | import { createApp } from 'vue' 4 | import { createPinia } from 'pinia' 5 | 6 | import App from './App.vue' 7 | import router from './router' 8 | 9 | const app = createApp(App) 10 | 11 | app.use(createPinia()) 12 | app.use(router) 13 | 14 | app.mount('#app') 15 | 16 | -------------------------------------------------------------------------------- /playground/views/404.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /src/optimisation.rs: -------------------------------------------------------------------------------- 1 | use crate::ir::IrProject; 2 | use crate::prelude::*; 3 | 4 | mod ssa; 5 | 6 | pub use ssa::SSAToken; 7 | 8 | pub fn ir_optimise(ir: &Rc) -> HQResult { 9 | //variables::optimise_var_types(ir)?; 10 | let ssa_token = ssa::optimise_variables(ir)?; 11 | Ok(ssa_token) 12 | } 13 | -------------------------------------------------------------------------------- /playground/stores/projectfile.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | export const useProjectFileStore = defineStore('projectFile', () => { 5 | const json = ref(null); 6 | const assets = ref([]); 7 | const title = ref('untitled'); 8 | const author = ref('unknown'); 9 | 10 | return { json, assets, title, author }; 11 | }) 12 | -------------------------------------------------------------------------------- /opcodes.mjs: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'node:fs/promises'; 2 | 3 | let blocksRs = (await readFile('./src/ir/blocks.rs', 'utf8')); 4 | const opcodes = [...new Set(blocksRs.match(/BlockOpcode::[a-zA-Z_0-9]+? (?==>)/g).map(op => op.replace('BlockOpcode::', '').trim()))].sort() 5 | await writeFile('/tmp/hq-build/js/opcodes.js', `export const opcodes = ${JSON.stringify(opcodes)};`); 6 | -------------------------------------------------------------------------------- /js/pen/point.ts: -------------------------------------------------------------------------------- 1 | import { renderer, target_skins, pen_skin } from '../shared'; 2 | 3 | export function point(radius: number, x: number, y: number, r: number, g: number, b: number, a: number) { 4 | renderer().penPoint( 5 | pen_skin(), 6 | { 7 | diameter: radius, // awkward variable naming moment 8 | color4f: [r, g, b, a], 9 | }, 10 | x, 11 | y 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /playground/views/ProjectIdView.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /src/wasm/registries/types.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::registry::SetRegistry; 3 | use wasm_encoder::{TypeSection, ValType}; 4 | 5 | pub type TypeRegistry = SetRegistry<(Vec, Vec)>; 6 | 7 | impl TypeRegistry { 8 | pub fn finish(self, types: &mut TypeSection) { 9 | for (params, results) in self.registry().take().keys().cloned() { 10 | types.ty().function(params, results); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /playground/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 1rem; 7 | font-weight: normal; 8 | } 9 | 10 | div, p { 11 | margin: 1rem auto; 12 | } 13 | 14 | a, 15 | .green { 16 | text-decoration: none; 17 | color: hsl(39.3,100%,37%); 18 | transition: var(--transition-time); 19 | } 20 | 21 | @media (hover: hover) { 22 | a:hover { 23 | background-color: hsla(33.1,100%,41%,0.2); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /playground/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/wasm.rs: -------------------------------------------------------------------------------- 1 | pub mod external; 2 | pub mod flags; 3 | pub mod func; 4 | #[macro_use] 5 | pub mod mem_layout; 6 | pub mod project; 7 | pub mod registries; 8 | 9 | pub use external::ExternalEnvironment; 10 | pub use flags::WasmFlags; 11 | pub use func::{Instruction as InternalInstruction, StepFunc, StepTarget}; 12 | pub use project::{FinishedWasm, WasmProject}; 13 | pub use registries::Registries; 14 | pub use registries::{GlobalExportable, GlobalMutable, StepsTable, StringsTable, ThreadsTable}; 15 | -------------------------------------------------------------------------------- /playground/views/TestProject.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /src/ir.rs: -------------------------------------------------------------------------------- 1 | mod blocks; 2 | mod context; 3 | mod event; 4 | mod proc; 5 | mod project; 6 | mod step; 7 | mod target; 8 | mod thread; 9 | mod types; 10 | mod variable; 11 | 12 | pub use blocks::insert_casts; 13 | pub use context::StepContext; 14 | pub use event::Event; 15 | pub use proc::{PartialStep, Proc, ProcContext}; 16 | pub use project::IrProject; 17 | pub use step::Step; 18 | pub use target::Target; 19 | use thread::Thread; 20 | pub use types::{ReturnType, Type, base_types}; 21 | pub use variable::{RcVar, used_vars}; 22 | -------------------------------------------------------------------------------- /js/pen/line.ts: -------------------------------------------------------------------------------- 1 | import { renderer, pen_skin } from "../shared"; 2 | 3 | export function line( 4 | radius: number, 5 | x1: number, 6 | y1: number, 7 | x2: number, 8 | y2: number, 9 | r: number, 10 | g: number, 11 | b: number, 12 | a: number 13 | ) { 14 | renderer().penLine( 15 | pen_skin(), 16 | { 17 | diameter: radius, 18 | color4f: [r, g, b, a], 19 | }, 20 | x1, 21 | y1, 22 | x2, 23 | y2 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/instructions/hq/drop.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 4 | Ok(wasm![Drop]) 5 | } 6 | 7 | pub fn acceptable_inputs() -> HQResult> { 8 | Ok(Rc::from([IrType::Number.or(IrType::String)])) 9 | } 10 | 11 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 12 | Ok(ReturnType::None) 13 | } 14 | 15 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 16 | 17 | crate::instructions_test! {tests; hq_drop; t} 18 | -------------------------------------------------------------------------------- /src/instructions/operator/not.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 4 | Ok(wasm![I32Eqz]) 5 | } 6 | 7 | pub fn acceptable_inputs() -> HQResult> { 8 | Ok(Rc::from([IrType::Boolean])) 9 | } 10 | 11 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 12 | Ok(Singleton(IrType::Boolean)) 13 | } 14 | 15 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 16 | 17 | crate::instructions_test! {tests; operator_not; t ;} 18 | -------------------------------------------------------------------------------- /enum-field-getter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "enum-field-getter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | repository = "https://github.com/HyperQuark/hyperquark/tree/main/enum-field-getter" 7 | authors = ["pufferfish101007"] 8 | description = "A derive macro to create mutable and immutable getters for tuple/struct members of enum variants" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | syn = { version = "1.0", features = ["full"] } 15 | quote = "1.0" 16 | proc-macro2 = "1.0" 17 | proc-macro-error = "1.0" -------------------------------------------------------------------------------- /src/instructions/operator/and.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 4 | Ok(wasm![I32And]) 5 | } 6 | 7 | pub fn acceptable_inputs() -> HQResult> { 8 | Ok(Rc::from([IrType::Boolean, IrType::Boolean])) 9 | } 10 | 11 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 12 | Ok(Singleton(IrType::Boolean)) 13 | } 14 | 15 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 16 | 17 | crate::instructions_test! {tests; operator_and; t1, t2 ;} 18 | -------------------------------------------------------------------------------- /src/instructions/operator/or.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 4 | Ok(wasm![I32Or]) 5 | } 6 | 7 | pub fn acceptable_inputs() -> HQResult> { 8 | Ok(Rc::from([IrType::Boolean, IrType::Boolean])) 9 | } 10 | 11 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 12 | Ok(Singleton(IrType::Boolean)) 13 | } 14 | 15 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 16 | 17 | crate::instructions_test! {tests; operator_or; t1, t2 ;} 18 | -------------------------------------------------------------------------------- /src/instructions/operator.rs: -------------------------------------------------------------------------------- 1 | pub mod abs; 2 | pub mod acos; 3 | pub mod add; 4 | pub mod and; 5 | pub mod asin; 6 | pub mod atan; 7 | pub mod ceiling; 8 | pub mod contains; 9 | pub mod cos; 10 | pub mod divide; 11 | pub mod equals; 12 | pub mod exp; 13 | pub mod floor; 14 | pub mod gt; 15 | pub mod join; 16 | pub mod length; 17 | pub mod letter_of; 18 | pub mod log; 19 | pub mod lt; 20 | pub mod modulo; 21 | pub mod multiply; 22 | pub mod not; 23 | pub mod or; 24 | pub mod pow10; 25 | pub mod random; 26 | pub mod sin; 27 | pub mod sqrt; 28 | pub mod subtract; 29 | pub mod tan; 30 | -------------------------------------------------------------------------------- /js/sensing/dayssince2000.ts: -------------------------------------------------------------------------------- 1 | export function dayssince2000(): number { 2 | // https://github.com/scratchfoundation/scratch-vm/blob/f10ab17bf351939153d9d0a17c577b5ba7b3c908/src/blocks/scratch3_sensing.js#L252 3 | const msPerDay = 24 * 60 * 60 * 1000; 4 | const start = new Date(2000, 0, 1); // Months are 0-indexed. 5 | const today = new Date(); 6 | const dstAdjust = today.getTimezoneOffset() - start.getTimezoneOffset(); 7 | let mSecsSinceStart = today.valueOf() - start.valueOf(); 8 | mSecsSinceStart += ((today.getTimezoneOffset() - dstAdjust) * 60 * 1000); 9 | return mSecsSinceStart / msPerDay; 10 | } -------------------------------------------------------------------------------- /playground/stores/debug.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | export const useDebugModeStore = defineStore('debugMode', () => { 5 | const debug = ref(typeof new URLSearchParams(window.location.search).get('debug') === 'string'); 6 | const toggleDebug = () => { 7 | debug.value = !debug.value; 8 | if (!erudaEnabled && debug.value) { 9 | eruda.init(); 10 | } 11 | } 12 | let erudaEnabled = false; 13 | if (debug.value) { 14 | eruda.init(); 15 | erudaEnabled = true; 16 | } 17 | return { debug, toggleDebug }; 18 | }) 19 | -------------------------------------------------------------------------------- /src/instructions/pen/clear.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 4 | let func_index = func 5 | .registries() 6 | .external_functions() 7 | .register(("pen", "clear".into()), (vec![], vec![]))?; 8 | Ok(wasm![Call(func_index),]) 9 | } 10 | 11 | pub fn acceptable_inputs() -> HQResult> { 12 | Ok(Rc::from([])) 13 | } 14 | 15 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 16 | Ok(ReturnType::None) 17 | } 18 | 19 | pub const REQUESTS_SCREEN_REFRESH: bool = true; 20 | 21 | crate::instructions_test! {tests; pen_clear; ; } 22 | -------------------------------------------------------------------------------- /src/alloc.rs: -------------------------------------------------------------------------------- 1 | use buddy_alloc::{BuddyAllocParam, FastAllocParam, NonThreadsafeAlloc}; 2 | 3 | // These values can be tuned 4 | const FAST_HEAP_SIZE: usize = 4 * 1024; // 4 KB 5 | const HEAP_SIZE: usize = 16 * 1024; // 16 KB 6 | const LEAF_SIZE: usize = 16; 7 | 8 | static mut FAST_HEAP: [u8; FAST_HEAP_SIZE] = [0u8; FAST_HEAP_SIZE]; 9 | static mut HEAP: [u8; HEAP_SIZE] = [0u8; HEAP_SIZE]; 10 | 11 | #[global_allocator] 12 | static ALLOC: NonThreadsafeAlloc = unsafe { 13 | let fast_param = FastAllocParam::new(FAST_HEAP.as_ptr(), FAST_HEAP_SIZE); 14 | let buddy_param = BuddyAllocParam::new(HEAP.as_ptr(), HEAP_SIZE, LEAF_SIZE); 15 | NonThreadsafeAlloc::new(fast_param, buddy_param) 16 | }; 17 | -------------------------------------------------------------------------------- /src/wasm/registries/strings.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::registry::SetRegistry; 3 | use wasm_encoder::{EntityType, GlobalType, ImportSection, ValType}; 4 | 5 | pub type StringRegistry = SetRegistry>; 6 | 7 | impl StringRegistry { 8 | pub fn finish(self, imports: &mut ImportSection) { 9 | for string in self.registry().take().keys() { 10 | imports.import( 11 | "", 12 | string, 13 | EntityType::Global(GlobalType { 14 | val_type: ValType::EXTERNREF, 15 | mutable: false, 16 | shared: false, 17 | }), 18 | ); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import wasm from "vite-plugin-wasm"; 5 | import vue from '@vitejs/plugin-vue' 6 | import { nodePolyfills } from 'vite-plugin-node-polyfills' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | root: './playground', 11 | base: '/hyperquark/', 12 | plugins: [ 13 | wasm(), 14 | vue(), 15 | nodePolyfills({ 16 | globals: { 17 | Buffer: true, 18 | }, 19 | }), 20 | ], 21 | resolve: { 22 | alias: { 23 | '@': fileURLToPath(new URL('./playground', import.meta.url)) 24 | } 25 | }, 26 | build: { 27 | target: 'esnext', 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperquark", 3 | "version": "0.0.6", 4 | "private": true, 5 | "scripts": { 6 | "watch": "vite build --watch & vite preview &", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "binaryen": "github:pufferfish101007/binaryen.js#non-nullable-table", 12 | "pinia": "2.1.3", 13 | "scratch-parser": "5.1.1", 14 | "scratch-sb1-converter": "2.0.228", 15 | "vite-plugin-wasm": "3.5.0", 16 | "vue": "3.5.21", 17 | "vue-router": "4.5.1", 18 | "wasm-feature-detect": "1.8.0" 19 | }, 20 | "devDependencies": { 21 | "@vitejs/plugin-vue": "6.0.1", 22 | "vite": "6.3.5", 23 | "vite-plugin-node-polyfills": "0.24.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/instructions/sensing/dayssince2000.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 0); 5 | let func_index = func.registries().external_functions().register( 6 | ("sensing", "dayssince2000".into()), 7 | (vec![], vec![ValType::F64]), 8 | )?; 9 | Ok(wasm![Call(func_index)]) 10 | } 11 | 12 | pub fn acceptable_inputs() -> HQResult> { 13 | Ok(Rc::from([])) 14 | } 15 | 16 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 17 | Ok(Singleton(IrType::FloatPos)) 18 | } 19 | 20 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 21 | 22 | crate::instructions_test! {tests; sensing_dayssince2000; ;} 23 | -------------------------------------------------------------------------------- /src/instructions/operator/length.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 1); 5 | let func_index = func.registries().external_functions().register( 6 | ("wasm:js-string", "length".into()), 7 | (vec![ValType::EXTERNREF], vec![ValType::I32]), 8 | )?; 9 | Ok(wasm![Call(func_index)]) 10 | } 11 | 12 | pub fn acceptable_inputs() -> HQResult> { 13 | Ok(Rc::from([IrType::String])) 14 | } 15 | 16 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 17 | Ok(Singleton(IrType::IntPos)) 18 | } 19 | 20 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 21 | 22 | crate::instructions_test! {tests; operator_length; t ;} 23 | -------------------------------------------------------------------------------- /src/instructions/operator/contains.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 2); 5 | let func_index = func.registries().external_functions().register( 6 | ("operator", "contains".into()), 7 | ( 8 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 9 | vec![ValType::I32], 10 | ), 11 | )?; 12 | Ok(wasm![Call(func_index)]) 13 | } 14 | 15 | pub fn acceptable_inputs() -> HQResult> { 16 | Ok(Rc::from([IrType::String, IrType::String])) 17 | } 18 | 19 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 20 | Ok(Singleton(IrType::Boolean)) 21 | } 22 | 23 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 24 | 25 | crate::instructions_test! {tests; operator_contains; t1, t2 ;} 26 | -------------------------------------------------------------------------------- /src/ir/context.rs: -------------------------------------------------------------------------------- 1 | use super::{IrProject, ProcContext, Target}; 2 | use crate::prelude::*; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct StepContext { 6 | pub target: Rc, 7 | /// whether or not the current thread is warped. this may be because the current 8 | /// procedure is warped, or because a procedure higher up the call stack was warped. 9 | pub warp: bool, 10 | pub proc_context: Option, 11 | /// enables certain behaviours such as `console.log` say/think rather than 12 | /// displaying in bubbles 13 | pub debug: bool, 14 | } 15 | 16 | impl StepContext { 17 | pub fn project(&self) -> HQResult> { 18 | self.target() 19 | .project() 20 | .upgrade() 21 | .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak")) 22 | } 23 | 24 | #[must_use] 25 | pub const fn target(&self) -> &Rc { 26 | &self.target 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/instructions/operator/acos.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 1); 5 | let t1 = inputs[0]; 6 | let imported_func = func.registries().external_functions().register( 7 | ("operator", "acos".into()), 8 | (vec![ValType::F64], vec![ValType::F64]), 9 | )?; 10 | Ok(wasm![ 11 | @nanreduce(t1), 12 | Call(imported_func), 13 | F64Const(core::f64::consts::PI), 14 | F64Div, 15 | F64Const(180.0), 16 | F64Mul, 17 | ]) 18 | } 19 | 20 | pub fn acceptable_inputs() -> HQResult> { 21 | Ok(Rc::from([IrType::Float])) 22 | } 23 | 24 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 25 | Ok(Singleton(IrType::Float)) 26 | } 27 | 28 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 29 | 30 | crate::instructions_test! {tests; operator_acos; t } 31 | -------------------------------------------------------------------------------- /src/instructions/operator/asin.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 1); 5 | let t1 = inputs[0]; 6 | let imported_func = func.registries().external_functions().register( 7 | ("operator", "asin".into()), 8 | (vec![ValType::F64], vec![ValType::F64]), 9 | )?; 10 | Ok(wasm![ 11 | @nanreduce(t1), 12 | Call(imported_func), 13 | F64Const(core::f64::consts::PI), 14 | F64Div, 15 | F64Const(180.0), 16 | F64Mul, 17 | ]) 18 | } 19 | 20 | pub fn acceptable_inputs() -> HQResult> { 21 | Ok(Rc::from([IrType::Float])) 22 | } 23 | 24 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 25 | Ok(Singleton(IrType::Float)) 26 | } 27 | 28 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 29 | 30 | crate::instructions_test! {tests; operator_asin; t } 31 | -------------------------------------------------------------------------------- /src/instructions/operator/atan.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 1); 5 | let t1 = inputs[0]; 6 | let imported_func = func.registries().external_functions().register( 7 | ("operator", "atan".into()), 8 | (vec![ValType::F64], vec![ValType::F64]), 9 | )?; 10 | Ok(wasm![ 11 | @nanreduce(t1), 12 | Call(imported_func), 13 | F64Const(core::f64::consts::PI), 14 | F64Div, 15 | F64Const(180.0), 16 | F64Mul, 17 | ]) 18 | } 19 | 20 | pub fn acceptable_inputs() -> HQResult> { 21 | Ok(Rc::from([IrType::Float])) 22 | } 23 | 24 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 25 | Ok(Singleton(IrType::Float)) 26 | } 27 | 28 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 29 | 30 | crate::instructions_test! {tests; operator_atan; t } 31 | -------------------------------------------------------------------------------- /playground/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | HyperQuark 10 | 11 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/instructions/operator/join.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::HeapType; 2 | 3 | use super::super::prelude::*; 4 | 5 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 6 | hq_assert_eq!(inputs.len(), 2); 7 | let func_index = func.registries().external_functions().register( 8 | ("wasm:js-string", "concat".into()), 9 | ( 10 | vec![ValType::EXTERNREF, ValType::EXTERNREF], 11 | vec![ValType::Ref(RefType { 12 | nullable: false, 13 | heap_type: HeapType::EXTERN, 14 | })], 15 | ), 16 | )?; 17 | Ok(wasm![Call(func_index)]) 18 | } 19 | 20 | pub fn acceptable_inputs() -> HQResult> { 21 | Ok(Rc::from([IrType::String, IrType::String])) 22 | } 23 | 24 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 25 | Ok(Singleton(IrType::String)) 26 | } 27 | 28 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 29 | 30 | crate::instructions_test! {tests; operator_join; t1, t2 ;} 31 | -------------------------------------------------------------------------------- /wasm-gen/README.md: -------------------------------------------------------------------------------- 1 | # wasm-gen 2 | 3 | This is an internal crate used to generate WebAssembly instructions for blocks. It relies upon HyperQuark's internal types so cannot be used outside of HyperQuark. 4 | 5 | ## Usage 6 | 7 | The `wasm![]` macro produces a `Vec>`. Inputs to the macro are (non-namespaced) wasm_encoder [`Instruction`](https://docs.rs/wasm-encoder/latest/wasm_encoder/enum.Instruction.html)s, or a 'special' instruction. Special instructions are currently: 8 | - `@nanreduce(input_name)` - checks the top value on the stack for NaN-ness (and replaces it with a zero if it is), only if `input_name` (must be an in-scope [`IrType`](../src/ir/types.rs)) could possibly be `NaN`. Assumes (rightly so) that the top item on the stack is an `f64`. 9 | - `@box(input_name)` - boxes the top value on the stack if `input_name` is a base type 10 | - `@isnan(input_name)` - checks if the top value on the stack for NaN-ness, only if `input_name` could possibly be NaN. Assumes the top item on the stack is an `f64`. -------------------------------------------------------------------------------- /playground/views/ProjectFileView.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behaviour: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Example project that this occurs on:** 21 | A link to or `sb3` file of a project which exhibits this unexpected behaviour. 22 | 23 | **Expected behaviour** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Device information (please complete the following information):** 30 | - OS: [e.g. windows 11] 31 | - Browser [e.g. chrome, firefox] 32 | - Browser version [e.g. 122] 33 | - Hyperquark version (e.g. the one on the website, a specific commit or some local version) 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /src/instructions/looks/size.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::MemArg; 2 | 3 | use super::super::prelude::*; 4 | use crate::wasm::{StepTarget, mem_layout}; 5 | 6 | pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 7 | let StepTarget::Sprite(wasm_target_index) = func.target() else { 8 | hq_bad_proj!("looks_size called in stage") 9 | }; 10 | Ok(wasm![ 11 | I32Const(0), 12 | F64Load(MemArg { 13 | offset: (mem_layout::stage::BLOCK_SIZE 14 | + wasm_target_index * mem_layout::sprite::BLOCK_SIZE 15 | + mem_layout::sprite::SIZE) 16 | .into(), 17 | align: 3, 18 | memory_index: 0, 19 | }), 20 | ]) 21 | } 22 | 23 | pub fn acceptable_inputs() -> HQResult> { 24 | Ok(Rc::from([])) 25 | } 26 | 27 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 28 | Ok(ReturnType::Singleton(IrType::FloatPos)) 29 | } 30 | 31 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 32 | 33 | crate::instructions_test! {tests; looks_size; ; } 34 | -------------------------------------------------------------------------------- /src/instructions/pen/penup.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::MemArg; 2 | 3 | use super::super::prelude::*; 4 | use crate::wasm::{StepTarget, mem_layout}; 5 | 6 | pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 7 | let StepTarget::Sprite(wasm_target_index) = func.target() else { 8 | hq_bad_proj!("looks_setpendown called in stage") 9 | }; 10 | let sprite_offset = 11 | mem_layout::stage::BLOCK_SIZE + wasm_target_index * mem_layout::sprite::BLOCK_SIZE; 12 | Ok(wasm![ 13 | I32Const(0), 14 | I32Const(0), 15 | I32Store8(MemArg { 16 | offset: (sprite_offset + mem_layout::sprite::PEN_DOWN).into(), 17 | align: 0, 18 | memory_index: 0, 19 | }), 20 | ]) 21 | } 22 | 23 | pub fn acceptable_inputs() -> HQResult> { 24 | Ok(Rc::from([])) 25 | } 26 | 27 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 28 | Ok(ReturnType::None) 29 | } 30 | 31 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 32 | 33 | crate::instructions_test! {tests; pen_penup; ; } 34 | -------------------------------------------------------------------------------- /src/instructions/looks/costumenumber.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::MemArg; 2 | 3 | use super::super::prelude::*; 4 | use crate::wasm::{StepTarget, mem_layout}; 5 | 6 | pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 7 | let wasm_target_index = match func.target() { 8 | StepTarget::Sprite(index) => index, 9 | StepTarget::Stage => 0, 10 | }; 11 | let offset = mem_layout::stage::BLOCK_SIZE 12 | + wasm_target_index * mem_layout::sprite::BLOCK_SIZE 13 | + mem_layout::sprite::COSTUME; 14 | 15 | Ok(wasm![ 16 | I32Const(0), 17 | I32Load(MemArg { 18 | offset: offset.into(), 19 | align: 2, 20 | memory_index: 0, 21 | }), 22 | ]) 23 | } 24 | 25 | pub fn acceptable_inputs() -> HQResult> { 26 | Ok(Rc::from([])) 27 | } 28 | 29 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 30 | Ok(ReturnType::Singleton(IrType::IntPos)) 31 | } 32 | 33 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 34 | 35 | crate::instructions_test! {tests; looks_costumenumber; ; } 36 | -------------------------------------------------------------------------------- /src/instructions/operator/sqrt.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 4 | Ok(wasm![F64Sqrt]) 5 | } 6 | 7 | pub fn acceptable_inputs() -> HQResult> { 8 | Ok(Rc::from([IrType::Float])) 9 | } 10 | 11 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 12 | hq_assert_eq!(inputs.len(), 1); 13 | let t1 = inputs[0]; 14 | let maybe_zero = t1.maybe_zero() || t1.maybe_nan(); 15 | let maybe_nan = t1.maybe_negative(); 16 | let maybe_pos_real = t1.intersects(IrType::FloatPosReal); 17 | let maybe_inf = t1.intersects(IrType::FloatPosInf); 18 | Ok(Singleton( 19 | IrType::none_if_false(maybe_pos_real, IrType::FloatPosReal) 20 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) 21 | .or(IrType::none_if_false(maybe_inf, IrType::FloatPosInf)) 22 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)), 23 | )) 24 | } 25 | 26 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 27 | 28 | crate::instructions_test! {tests; operator_sqrt; t } 29 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Gregor Hutchison 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/instructions/motion/direction.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::wasm::{StepTarget, mem_layout}; 3 | use mem_layout::{sprite as sprite_layout, stage as stage_layout}; 4 | use wasm_encoder::MemArg; 5 | 6 | pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 7 | let StepTarget::Sprite(wasm_target_index) = func.target() else { 8 | hq_bad_proj!("motion_direction called in stage") 9 | }; 10 | Ok(wasm![ 11 | I32Const(0), 12 | F64Load(MemArg { 13 | offset: (stage_layout::BLOCK_SIZE 14 | + wasm_target_index * sprite_layout::BLOCK_SIZE 15 | + sprite_layout::ROTATION) 16 | .into(), 17 | align: 3, 18 | memory_index: 0 19 | }), 20 | ]) 21 | } 22 | 23 | pub fn acceptable_inputs() -> HQResult> { 24 | Ok(Rc::from([])) 25 | } 26 | 27 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 28 | Ok(ReturnType::Singleton(IrType::FloatReal)) 29 | } 30 | 31 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 32 | 33 | crate::instructions_test! {tests; motion_direction; } 34 | -------------------------------------------------------------------------------- /src/instructions/operator/tan.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 1); 5 | let t1 = inputs[0]; 6 | let imported_func = func.registries().external_functions().register( 7 | ("operator", "tan".into()), 8 | (vec![ValType::F64], vec![ValType::F64]), 9 | )?; 10 | Ok(wasm![ 11 | @nanreduce(t1), 12 | // todo: should we move some of this into wasm? 13 | Call(imported_func), 14 | ]) 15 | } 16 | 17 | pub fn acceptable_inputs() -> HQResult> { 18 | Ok(Rc::from([IrType::Float])) 19 | } 20 | 21 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 22 | hq_assert_eq!(inputs.len(), 1); 23 | let t1 = inputs[0]; 24 | let maybe_nan = t1.maybe_inf(); 25 | let maybe_real = t1.intersects(IrType::FloatReal.or(IrType::FloatNan)); 26 | Ok(Singleton( 27 | IrType::none_if_false(maybe_real, IrType::FloatReal) 28 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)), 29 | )) 30 | } 31 | 32 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 33 | 34 | crate::instructions_test! {tests; operator_tan; t } 35 | -------------------------------------------------------------------------------- /src/instructions/operator/cos.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 1); 5 | let t1 = inputs[0]; 6 | let imported_func = func.registries().external_functions().register( 7 | ("operator", "cos".into()), 8 | (vec![ValType::F64], vec![ValType::F64]), 9 | )?; 10 | Ok(wasm![ 11 | @nanreduce(t1), 12 | F64Const(core::f64::consts::PI), 13 | F64Mul, 14 | F64Const(180.0), 15 | F64Div, 16 | Call(imported_func), 17 | ]) 18 | } 19 | 20 | pub fn acceptable_inputs() -> HQResult> { 21 | Ok(Rc::from([IrType::Float])) 22 | } 23 | 24 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 25 | hq_assert_eq!(inputs.len(), 1); 26 | let t1 = inputs[0]; 27 | let maybe_nan = t1.maybe_inf(); 28 | let maybe_real = t1.intersects(IrType::FloatReal.or(IrType::FloatNan)); 29 | Ok(Singleton( 30 | IrType::none_if_false(maybe_real, IrType::FloatReal) 31 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)), 32 | )) 33 | } 34 | 35 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 36 | 37 | crate::instructions_test! {tests; operator_cos; t } 38 | -------------------------------------------------------------------------------- /src/instructions/operator/sin.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 1); 5 | let t1 = inputs[0]; 6 | let imported_func = func.registries().external_functions().register( 7 | ("operator", "sin".into()), 8 | (vec![ValType::F64], vec![ValType::F64]), 9 | )?; 10 | Ok(wasm![ 11 | @nanreduce(t1), 12 | F64Const(core::f64::consts::PI), 13 | F64Mul, 14 | F64Const(180.0), 15 | F64Div, 16 | Call(imported_func), 17 | ]) 18 | } 19 | 20 | pub fn acceptable_inputs() -> HQResult> { 21 | Ok(Rc::from([IrType::Float])) 22 | } 23 | 24 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 25 | hq_assert_eq!(inputs.len(), 1); 26 | let t1 = inputs[0]; 27 | let maybe_nan = t1.maybe_inf(); 28 | let maybe_real = t1.intersects(IrType::FloatReal.or(IrType::FloatNan)); 29 | Ok(Singleton( 30 | IrType::none_if_false(maybe_real, IrType::FloatReal) 31 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)), 32 | )) 33 | } 34 | 35 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 36 | 37 | crate::instructions_test! {tests; operator_sin; t } 38 | -------------------------------------------------------------------------------- /src/instructions/pen/setpensizeto.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::MemArg; 2 | 3 | use super::super::prelude::*; 4 | use crate::wasm::{StepTarget, mem_layout}; 5 | 6 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 7 | let t1 = inputs[0]; 8 | let StepTarget::Sprite(wasm_target_index) = func.target() else { 9 | hq_bad_proj!("looks_setsizeto called in stage") 10 | }; 11 | let local_index = func.local(ValType::F64)?; 12 | Ok(wasm![ 13 | @nanreduce(t1), 14 | LocalSet(local_index), 15 | I32Const(0), 16 | LocalGet(local_index), 17 | F64Store(MemArg { 18 | offset: (mem_layout::stage::BLOCK_SIZE 19 | + wasm_target_index * mem_layout::sprite::BLOCK_SIZE 20 | + mem_layout::sprite::PEN_SIZE) 21 | .into(), 22 | align: 3, 23 | memory_index: 0, 24 | }), 25 | ]) 26 | } 27 | 28 | pub fn acceptable_inputs() -> HQResult> { 29 | Ok(Rc::from([IrType::Float])) 30 | } 31 | 32 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 33 | Ok(ReturnType::None) 34 | } 35 | 36 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 37 | 38 | crate::instructions_test! {tests; pen_setpensizeto; t ; } 39 | -------------------------------------------------------------------------------- /src/instructions/operator/exp.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | let imported_func = func.registries().external_functions().register( 5 | ("operator", "exp".into()), 6 | (vec![ValType::F64], vec![ValType::F64]), 7 | )?; 8 | let t1 = inputs[0]; 9 | Ok(wasm![ 10 | @nanreduce(t1), 11 | Call(imported_func), 12 | ]) 13 | } 14 | 15 | pub fn acceptable_inputs() -> HQResult> { 16 | Ok(Rc::from([IrType::Float])) 17 | } 18 | 19 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 20 | hq_assert_eq!(inputs.len(), 1); 21 | let t1 = inputs[0]; 22 | let maybe_pos = t1.intersects(IrType::FloatReal) || t1.maybe_nan(); 23 | let maybe_pos_inf = t1.intersects(IrType::FloatPosInf); 24 | let maybe_zero = t1.intersects(IrType::FloatNegInf); 25 | Ok(Singleton( 26 | IrType::none_if_false(maybe_pos, IrType::FloatPosReal) 27 | .or(IrType::none_if_false(maybe_pos_inf, IrType::FloatPosInf)) 28 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)), 29 | )) 30 | } 31 | 32 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 33 | 34 | crate::instructions_test! {tests; operator_exp; t } 35 | -------------------------------------------------------------------------------- /src/instructions/hq/integer.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | clippy::trivially_copy_pass_by_ref, 3 | reason = "Fields should be passed by reference for type signature consistency" 4 | )] 5 | 6 | use super::super::prelude::*; 7 | 8 | #[derive(Clone, Copy, Debug)] 9 | pub struct Fields(pub i32); 10 | 11 | impl fmt::Display for Fields { 12 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 13 | write!( 14 | f, 15 | r#"{{ 16 | "value": {} 17 | }}"#, 18 | self.0 19 | ) 20 | } 21 | } 22 | 23 | pub fn wasm( 24 | _func: &StepFunc, 25 | _inputs: Rc<[IrType]>, 26 | fields: &Fields, 27 | ) -> HQResult> { 28 | Ok(wasm![I32Const(fields.0)]) 29 | } 30 | 31 | pub fn acceptable_inputs(_fields: &Fields) -> HQResult> { 32 | Ok(Rc::from([])) 33 | } 34 | 35 | pub fn output_type(_inputs: Rc<[IrType]>, &Fields(val): &Fields) -> HQResult { 36 | Ok(Singleton(match val { 37 | 0 => IrType::IntZero, 38 | pos if pos > 0 => IrType::IntPos, 39 | neg if neg < 0 => IrType::IntNeg, 40 | _ => unreachable!(), 41 | })) 42 | } 43 | 44 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 45 | 46 | crate::instructions_test! {tests; hq_integer; @ super::Fields(0)} 47 | -------------------------------------------------------------------------------- /src/instructions/operator/pow10.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | let imported_func = func.registries().external_functions().register( 5 | ("operator", "pow10".into()), 6 | (vec![ValType::F64], vec![ValType::F64]), 7 | )?; 8 | let t1 = inputs[0]; 9 | Ok(wasm![ 10 | @nanreduce(t1), 11 | Call(imported_func), 12 | ]) 13 | } 14 | 15 | pub fn acceptable_inputs() -> HQResult> { 16 | Ok(Rc::from([IrType::Float])) 17 | } 18 | 19 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 20 | hq_assert_eq!(inputs.len(), 1); 21 | let t1 = inputs[0]; 22 | let maybe_pos = t1.intersects(IrType::FloatReal) || t1.maybe_nan(); 23 | let maybe_pos_inf = t1.intersects(IrType::FloatPosInf); 24 | let maybe_zero = t1.intersects(IrType::FloatNegInf); 25 | Ok(Singleton( 26 | IrType::none_if_false(maybe_pos, IrType::FloatPosReal) 27 | .or(IrType::none_if_false(maybe_pos_inf, IrType::FloatPosInf)) 28 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)), 29 | )) 30 | } 31 | 32 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 33 | 34 | crate::instructions_test! {tests; operator_pow10; t } 35 | -------------------------------------------------------------------------------- /src/instructions/hq/boolean.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | clippy::trivially_copy_pass_by_ref, 3 | reason = "Fields should be passed by reference for type signature consistency" 4 | )] 5 | 6 | use super::super::prelude::*; 7 | 8 | #[derive(Clone, Copy, Debug)] 9 | pub struct Fields(pub bool); 10 | 11 | impl fmt::Display for Fields { 12 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 13 | write!( 14 | f, 15 | r#"{{ 16 | "value": {} 17 | }}"#, 18 | self.0 19 | ) 20 | } 21 | } 22 | 23 | pub fn wasm( 24 | _func: &StepFunc, 25 | _inputs: Rc<[IrType]>, 26 | fields: &Fields, 27 | ) -> HQResult> { 28 | Ok(wasm![I32Const(fields.0.into())]) 29 | } 30 | 31 | pub fn acceptable_inputs(_fields: &Fields) -> HQResult> { 32 | Ok(Rc::from([])) 33 | } 34 | 35 | pub fn output_type(_inputs: Rc<[IrType]>, &Fields(val): &Fields) -> HQResult { 36 | Ok(Singleton(if val { 37 | IrType::BooleanTrue 38 | } else { 39 | IrType::BooleanFalse 40 | })) 41 | } 42 | 43 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 44 | 45 | crate::instructions_test! {tests_false; hq_boolean; @ super::Fields(false)} 46 | crate::instructions_test! {tests_true; hq_boolean; @ super::Fields(true)} 47 | -------------------------------------------------------------------------------- /src/instructions/hq/color_rgb.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | clippy::trivially_copy_pass_by_ref, 3 | reason = "Fields should be passed by reference for type signature consistency" 4 | )] 5 | 6 | use super::super::prelude::*; 7 | 8 | #[derive(Clone, Copy, Debug)] 9 | pub struct Fields { 10 | pub r: u8, 11 | pub g: u8, 12 | pub b: u8, 13 | } 14 | 15 | impl fmt::Display for Fields { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | write!( 18 | f, 19 | r#"{{ 20 | "r": {}, 21 | "g": {}, 22 | "b": {} 23 | }}"#, 24 | self.r, self.g, self.b 25 | ) 26 | } 27 | } 28 | 29 | pub fn wasm( 30 | _func: &StepFunc, 31 | _inputs: Rc<[IrType]>, 32 | fields: &Fields, 33 | ) -> HQResult> { 34 | Ok(wasm![I32Const( 35 | i32::from(fields.r) << 16 | i32::from(fields.g) << 8 | i32::from(fields.b) 36 | )]) 37 | } 38 | 39 | pub fn acceptable_inputs(_fields: &Fields) -> HQResult> { 40 | Ok(Rc::from([])) 41 | } 42 | 43 | pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult { 44 | Ok(Singleton(IrType::ColorRGB)) 45 | } 46 | 47 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 48 | 49 | crate::instructions_test! {tests; hq_color_rgb; @ super::Fields { r: 134, g: 56, b: 109 }} 50 | -------------------------------------------------------------------------------- /src/instructions/operator/letter_of.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::HeapType; 2 | 3 | use super::super::prelude::*; 4 | 5 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 6 | hq_assert_eq!(inputs.len(), 2); 7 | let func_index = func.registries().external_functions().register( 8 | ("wasm:js-string", "substring".into()), 9 | ( 10 | vec![ValType::EXTERNREF, ValType::I32, ValType::I32], 11 | vec![ValType::Ref(RefType { 12 | nullable: false, 13 | heap_type: HeapType::EXTERN, 14 | })], 15 | ), 16 | )?; 17 | let i32_local = func.local(ValType::I32)?; 18 | let ef_local = func.local(ValType::EXTERNREF)?; 19 | Ok(wasm![ 20 | LocalSet(ef_local), 21 | LocalSet(i32_local), 22 | LocalGet(ef_local), 23 | LocalGet(i32_local), 24 | I32Const(1), 25 | I32Sub, 26 | LocalGet(i32_local), 27 | Call(func_index), 28 | ]) 29 | } 30 | 31 | pub fn acceptable_inputs() -> HQResult> { 32 | Ok(Rc::from([IrType::Int, IrType::String])) 33 | } 34 | 35 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 36 | Ok(Singleton(IrType::String)) 37 | } 38 | 39 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 40 | 41 | crate::instructions_test! {tests; operator_letter_of; t1, t2 ;} 42 | -------------------------------------------------------------------------------- /src/instructions/operator/log.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | let imported_func = func.registries().external_functions().register( 5 | ("operator", "log".into()), 6 | (vec![ValType::F64], vec![ValType::F64]), 7 | )?; 8 | let t1 = inputs[0]; 9 | Ok(wasm![ 10 | @nanreduce(t1), 11 | Call(imported_func), 12 | ]) 13 | } 14 | 15 | pub fn acceptable_inputs() -> HQResult> { 16 | Ok(Rc::from([IrType::Float])) 17 | } 18 | 19 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 20 | hq_assert_eq!(inputs.len(), 1); 21 | let t1 = inputs[0]; 22 | let maybe_nan = t1.maybe_negative(); 23 | let maybe_real = t1.intersects(IrType::FloatPosReal); 24 | let maybe_pos_inf = t1.intersects(IrType::FloatPosInf); 25 | let maybe_neg_inf = t1.maybe_zero() || t1.maybe_nan(); 26 | Ok(Singleton( 27 | IrType::none_if_false(maybe_real, IrType::FloatReal) 28 | .or(IrType::none_if_false(maybe_pos_inf, IrType::FloatPosInf)) 29 | .or(IrType::none_if_false(maybe_neg_inf, IrType::FloatNegInf)) 30 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)), 31 | )) 32 | } 33 | 34 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 35 | 36 | crate::instructions_test! {tests; operator_log; t } 37 | -------------------------------------------------------------------------------- /src/wasm/registries/variables.rs: -------------------------------------------------------------------------------- 1 | use super::super::WasmProject; 2 | use super::{GlobalExportable, GlobalMutable, GlobalRegistry}; 3 | use crate::ir::{RcVar, Type as IrType}; 4 | use crate::prelude::*; 5 | use wasm_encoder::{ConstExpr, HeapType}; 6 | 7 | pub struct VariableRegistry(Rc); 8 | 9 | impl VariableRegistry { 10 | const fn globals(&self) -> &Rc { 11 | &self.0 12 | } 13 | 14 | #[must_use] 15 | pub fn new(globals: &Rc) -> Self { 16 | Self(Rc::clone(globals)) 17 | } 18 | 19 | pub fn register(&self, var: &RcVar) -> HQResult { 20 | self.globals().register( 21 | // format!("__rcvar_{:p}", Rc::as_ptr(&var.0)).into(), 22 | format!("__rcvar_{}", var.id()).into(), 23 | ( 24 | WasmProject::ir_type_to_wasm(*var.possible_types())?, 25 | match var.possible_types().base_type() { 26 | Some(IrType::Float) => ConstExpr::f64_const(0.0), 27 | Some(IrType::QuasiInt) => ConstExpr::i32_const(0), 28 | Some(IrType::String) => ConstExpr::ref_null(HeapType::EXTERN), 29 | _ => ConstExpr::i64_const(0), // TODO: use the variable's initial value 30 | }, 31 | GlobalMutable(true), 32 | GlobalExportable(false), 33 | ), 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to HyperQuark 2 | 3 | ## I found a bug 4 | You should check first if there is already an issue for the bug. If there is, feel free to add any supporting information that you feel would be helpful in a comment. If there isn't, you should [create an issue](https://github.com/hyperquark/hyperquark), including as much detail as possible, including a description of the expected and observed behaviour, which projects you see this behaviour in, and whether this was observed on the hyperquark website or a local build. 5 | 6 | ## There's a feature I'd like 7 | First think: is it reasonable? If the answer is yes, check if there are any issues for this feature (remember to check for closed issues), and if there aren't, you can create one yourself. 8 | 9 | ## I'd like to contribute to the codebase 10 | Does the thing you want to code have an open, triaged issue for it? If not, it probably won't be accepted (unless it's a trivial, obviously beneficial change), so you should consider opening an issue first to discuss it. If there is an open issue, it's probably fine to work on it, however, if there is an assignee on the issue then you should ask them first before starting work on it. 11 | 12 | ## First time contributions 13 | If you're looking for a good place to get started, "easier" issues have the ["good first issue" label](https://github.com/HyperQuark/hyperquark/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). 14 | -------------------------------------------------------------------------------- /src/instructions/procedures/argument.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::{ 3 | ir::RcVar, 4 | wasm::{StepFunc, WasmProject}, 5 | }; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct Fields(pub usize, pub RcVar); 9 | 10 | impl fmt::Display for Fields { 11 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 12 | write!( 13 | f, 14 | r#"{{ 15 | "arg_index": {}, 16 | "arg_var": {}, 17 | }}"#, 18 | self.0, self.1 19 | ) 20 | } 21 | } 22 | 23 | pub fn wasm( 24 | func: &StepFunc, 25 | _inputs: Rc<[IrType]>, 26 | Fields(index, var): &Fields, 27 | ) -> HQResult> { 28 | hq_assert!( 29 | WasmProject::ir_type_to_wasm(*var.possible_types())? 30 | == *func.params().get(*index).ok_or_else(|| make_hq_bug!( 31 | "proc argument index was out of bounds for func params" 32 | ))?, 33 | "proc argument type didn't match that of the corresponding function param" 34 | ); 35 | Ok(wasm![LocalGet((*index).try_into().map_err( 36 | |_| make_hq_bug!("argument index out of bounds") 37 | )?)]) 38 | } 39 | 40 | pub fn acceptable_inputs(_: &Fields) -> HQResult> { 41 | Ok(Rc::from([])) 42 | } 43 | 44 | pub fn output_type(_inputs: Rc<[IrType]>, Fields(_, var): &Fields) -> HQResult { 45 | Ok(Singleton(*var.possible_types())) 46 | } 47 | 48 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 49 | -------------------------------------------------------------------------------- /playground/lib/project-loader.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import validate from 'scratch-parser'; 3 | import { SB1File, ValidationError } from 'scratch-sb1-converter'; 4 | 5 | // adapted from https://github.com/scratchfoundation/scratch-vm/blob/6cfea59e7aaff880a1c4e709b2adc14d0113ecab/src/virtual-machine.js#L320 6 | /** 7 | * @param {ArrayBuffer|String} input 8 | */ 9 | export const unpackProject = (input) => { 10 | if (typeof input !== 'string') { 11 | input = Buffer.from(input); 12 | } 13 | return new Promise((resolve, reject) => { 14 | // The second argument of false below indicates to the validator that the 15 | // input should be parsed/validated as an entire project (and not a single sprite) 16 | validate(input, false, (error, res) => { 17 | if (error) return reject(error); 18 | resolve(res); 19 | }); 20 | }).catch(error => { 21 | try { 22 | const sb1 = new SB1File(input); 23 | const json = sb1.json; 24 | json.projectVersion = 2; 25 | return Promise.resolve([json, sb1.zip]); 26 | } catch (sb1Error) { 27 | if (sb1Error instanceof ValidationError) { 28 | // The input does not validate as a Scratch 1 file. 29 | } else { 30 | // The project appears to be a Scratch 1 file but it 31 | // could not be successfully translated into a Scratch 2 32 | // project. 33 | return Promise.reject(sb1Error); 34 | } 35 | } 36 | // Throw original error since the input does not appear to be 37 | // an SB1File. 38 | return Promise.reject(error); 39 | }); 40 | }; -------------------------------------------------------------------------------- /src/instructions/hq/text.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Fields(pub Box); 5 | 6 | impl fmt::Display for Fields { 7 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 8 | write!( 9 | f, 10 | r#"{{ 11 | "value": {:?} 12 | }}"#, 13 | self.0 14 | ) 15 | } 16 | } 17 | 18 | pub fn wasm( 19 | func: &StepFunc, 20 | _inputs: Rc<[IrType]>, 21 | fields: &Fields, 22 | ) -> HQResult> { 23 | let string_idx = func 24 | .registries() 25 | .strings() 26 | .register_default(fields.0.clone())?; 27 | Ok(wasm![ 28 | // string imports always come before any other global, so we don't need to use #LazyGlobalGet 29 | GlobalGet(string_idx), 30 | ]) 31 | } 32 | 33 | pub fn acceptable_inputs(_fields: &Fields) -> HQResult> { 34 | Ok(Rc::from([])) 35 | } 36 | 37 | pub fn output_type(_inputs: Rc<[IrType]>, Fields(val): &Fields) -> HQResult { 38 | Ok(Singleton(match &**val { 39 | bool if bool.to_lowercase() == "true" || bool.to_lowercase() == "false" => { 40 | IrType::StringBoolean 41 | } 42 | num if let Ok(float) = num.parse::() 43 | && !float.is_nan() => 44 | { 45 | IrType::StringNumber 46 | } 47 | _ => IrType::StringNan, 48 | })) 49 | } 50 | 51 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 52 | 53 | crate::instructions_test! {tests; hq_text; @ super::Fields("hello, world!".into())} 54 | -------------------------------------------------------------------------------- /src/instructions/hq/float.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | clippy::trivially_copy_pass_by_ref, 3 | reason = "Fields should be passed by reference for type signature consistency" 4 | )] 5 | 6 | use super::super::prelude::*; 7 | 8 | #[derive(Clone, Copy, Debug)] 9 | pub struct Fields(pub f64); 10 | 11 | impl fmt::Display for Fields { 12 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 13 | write!( 14 | f, 15 | r#"{{ 16 | "value": {} 17 | }}"#, 18 | self.0 19 | ) 20 | } 21 | } 22 | 23 | pub fn wasm( 24 | _func: &StepFunc, 25 | _inputs: Rc<[IrType]>, 26 | fields: &Fields, 27 | ) -> HQResult> { 28 | Ok(wasm![F64Const(fields.0)]) 29 | } 30 | 31 | pub fn acceptable_inputs(_fields: &Fields) -> HQResult> { 32 | Ok(Rc::from([])) 33 | } 34 | 35 | pub fn output_type(_inputs: Rc<[IrType]>, &Fields(val): &Fields) -> HQResult { 36 | Ok(Singleton(match val { 37 | 0.0 => IrType::FloatZero, 38 | f64::INFINITY => IrType::FloatPosInf, 39 | f64::NEG_INFINITY => IrType::FloatNegInf, 40 | nan if f64::is_nan(nan) => IrType::FloatNan, 41 | int if int % 1.0 == 0.0 && int > 0.0 => IrType::FloatPosInt, 42 | int if int % 1.0 == 0.0 && int < 0.0 => IrType::FloatNegInt, 43 | frac if frac > 0.0 => IrType::FloatPosFrac, 44 | frac if frac < 0.0 => IrType::FloatNegFrac, 45 | _ => unreachable!(), 46 | })) 47 | } 48 | 49 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 50 | 51 | crate::instructions_test! {tests; hq_float; @ super::Fields(0.0)} 52 | -------------------------------------------------------------------------------- /playground/components/ProjectIdPlayer.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 42 | 43 | -------------------------------------------------------------------------------- /src/instructions/looks/setvisible.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::MemArg; 2 | 3 | use super::super::prelude::*; 4 | use crate::wasm::{StepTarget, mem_layout}; 5 | 6 | pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 7 | let ir_target_index: i32 = func 8 | .target_index() 9 | .try_into() 10 | .map_err(|_| make_hq_bug!("target index out of bounds"))?; 11 | let func_index = func.registries().external_functions().register( 12 | ("looks", "setvisible".into()), 13 | (vec![ValType::I32, ValType::I32], vec![]), 14 | )?; 15 | let StepTarget::Sprite(wasm_target_index) = func.target() else { 16 | hq_bad_proj!("looks_setvisible called in stage") 17 | }; 18 | let local_index = func.local(ValType::I32)?; 19 | Ok(wasm![ 20 | LocalSet(local_index), 21 | I32Const(0), 22 | LocalGet(local_index), 23 | I32Store8(MemArg { 24 | offset: (mem_layout::stage::BLOCK_SIZE 25 | + wasm_target_index * mem_layout::sprite::BLOCK_SIZE 26 | + mem_layout::sprite::VISIBLE) 27 | .into(), 28 | align: 0, 29 | memory_index: 0, 30 | }), 31 | LocalGet(local_index), 32 | I32Const(ir_target_index), 33 | Call(func_index), 34 | ]) 35 | } 36 | 37 | pub fn acceptable_inputs() -> HQResult> { 38 | Ok(Rc::from([IrType::Boolean])) 39 | } 40 | 41 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 42 | Ok(ReturnType::None) 43 | } 44 | 45 | pub const REQUESTS_SCREEN_REFRESH: bool = true; 46 | 47 | crate::instructions_test! {tests; looks_setvisible; t ; } 48 | -------------------------------------------------------------------------------- /enum-field-getter/README.md: -------------------------------------------------------------------------------- 1 | # `enum-field-getter` 2 | 3 | A simple derive macro used to implement methods to access fields which are of the same type across every tuple/struct enum variant. 4 | 5 | ## Usage 6 | 7 | Derive `EnumFieldGetter`. For tuple enum variants, produces `get_n(&self) -> Option<&_>` and `get_n_mut(&mut self) -> Option<&mut _>` methods for each tuple member (starting from 0); for struct variants, getters are of the form `prop(&self) -> Option<&_>` and `prop_mut(&mut self) -> Option<&mut _>`. Getters are only produced so long as that member is the same type across all enum variants - if they are of different types, no getter will be produced for that member. These methods are produced even if that member doesn't exist for all enum variants - in the case that it doesn't exist, the getter will return `None`. 8 | 9 | ## Examples 10 | 11 | ```rust 12 | use enum_field_getter::EnumFieldGetter; 13 | 14 | #[derive(EnumFieldGetter)] 15 | enum Foo { 16 | Bar(u32), 17 | Baz(u32, u32), 18 | } 19 | 20 | let foo = Foo::Bar(16); 21 | let foo0 = foo.get_0(); 22 | assert_eq!(foo0, Some(&16)); 23 | let foo1 = foo.get_1(); 24 | assert!(foo1.is_none()); 25 | ``` 26 | 27 | ```rust 28 | use enum_field_getter::EnumFieldGetter; 29 | 30 | #[derive(EnumFieldGetter)] 31 | enum Boop { 32 | Moo { 33 | a: i32, 34 | b: i32, 35 | }, 36 | Baa { 37 | a: i32, 38 | b: i32, 39 | c: i32, 40 | } 41 | } 42 | 43 | let mut boop = Boop::Baa { a: 0, b: 42, c: 180 }; 44 | let boop_a = boop.a(); 45 | assert_eq!(boop_a, Some(&0)); 46 | let boop_c = boop.c(); 47 | assert_eq!(boop_c, Some(&180)); 48 | *boop.b_mut().unwrap() = 43; 49 | assert_eq!(boop.b(), Some(&43)); 50 | ``` -------------------------------------------------------------------------------- /src/instructions/looks/setsizeto.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::MemArg; 2 | 3 | use super::super::prelude::*; 4 | use crate::wasm::{StepTarget, mem_layout}; 5 | 6 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 7 | let t1 = inputs[0]; 8 | let ir_target_index: i32 = func 9 | .target_index() 10 | .try_into() 11 | .map_err(|_| make_hq_bug!("target index out of bounds"))?; 12 | let func_index = func.registries().external_functions().register( 13 | ("looks", "setsizeto".into()), 14 | (vec![ValType::F64, ValType::I32], vec![]), 15 | )?; 16 | let StepTarget::Sprite(wasm_target_index) = func.target() else { 17 | hq_bad_proj!("looks_setsizeto called in stage") 18 | }; 19 | let local_index = func.local(ValType::F64)?; 20 | Ok(wasm![ 21 | @nanreduce(t1), 22 | LocalSet(local_index), 23 | I32Const(0), 24 | LocalGet(local_index), 25 | F64Store(MemArg { 26 | offset: (mem_layout::stage::BLOCK_SIZE 27 | + wasm_target_index * mem_layout::sprite::BLOCK_SIZE 28 | + mem_layout::sprite::SIZE) 29 | .into(), 30 | align: 3, 31 | memory_index: 0, 32 | }), 33 | LocalGet(local_index), 34 | I32Const(ir_target_index), 35 | Call(func_index), 36 | ]) 37 | } 38 | 39 | pub fn acceptable_inputs() -> HQResult> { 40 | Ok(Rc::from([IrType::Float])) 41 | } 42 | 43 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 44 | Ok(ReturnType::None) 45 | } 46 | 47 | pub const REQUESTS_SCREEN_REFRESH: bool = true; 48 | 49 | crate::instructions_test! {tests; looks_setsizeto; t ; } 50 | -------------------------------------------------------------------------------- /playground/components/ProjectFileInput.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/instructions/motion/pointindirection.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::wasm::{StepTarget, mem_layout}; 3 | use mem_layout::{sprite as sprite_layout, stage as stage_layout}; 4 | use wasm_encoder::MemArg; 5 | 6 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 7 | let StepTarget::Sprite(wasm_target_index) = func.target() else { 8 | hq_bad_proj!("motion_pointindirection called in stage") 9 | }; 10 | let ir_target_index: i32 = func 11 | .target_index() 12 | .try_into() 13 | .map_err(|_| make_hq_bug!("target index out of bounds"))?; 14 | let local_idx = func.local(ValType::F64)?; 15 | let t1 = inputs[0]; 16 | let imported_func = func.registries().external_functions().register( 17 | ("motion", "pointindirection".into()), 18 | (vec![ValType::I32, ValType::F64], vec![]), 19 | )?; 20 | Ok(wasm![ 21 | @nanreduce(t1), 22 | LocalSet(local_idx), 23 | I32Const(0), 24 | LocalGet(local_idx), 25 | F64Store(MemArg { 26 | offset: (stage_layout::BLOCK_SIZE 27 | + wasm_target_index * sprite_layout::BLOCK_SIZE 28 | + sprite_layout::ROTATION) 29 | .into(), 30 | align: 3, 31 | memory_index: 0 32 | }), 33 | I32Const(ir_target_index), 34 | LocalGet(local_idx), 35 | Call(imported_func), 36 | ]) 37 | } 38 | 39 | pub fn acceptable_inputs() -> HQResult> { 40 | Ok(Rc::from([IrType::Float])) 41 | } 42 | 43 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 44 | Ok(ReturnType::None) 45 | } 46 | 47 | pub const REQUESTS_SCREEN_REFRESH: bool = true; 48 | 49 | crate::instructions_test! {tests; motion_pointindirection; t } 50 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hyperquark" 3 | version = "0.0.6" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } 9 | serde_json = { version = "1.0.145", default-features = false, features = ["alloc"] } 10 | enum-field-getter = { path = "enum-field-getter" } 11 | wasm-encoder = "0.226.0" 12 | indexmap = { version = "2.11.4", default-features = false } 13 | hashers = "1.0.1" 14 | uuid = { version = "1.18.1", default-features = false, features = ["v4", "js"] } 15 | regex = "1.11.2" 16 | lazy-regex = "3.2.0" 17 | bitmask-enum = "2.2.5" 18 | itertools = { version = "0.14.0", default-features = false, features = ["use_alloc"] } 19 | wasm-bindgen = "0.2.103" 20 | serde-wasm-bindgen = "0.6.5" 21 | wasm-gen = { path = "wasm-gen" } 22 | petgraph = { version = "0.8.1", default-features = false, features = ["stable_graph"] } 23 | 24 | [dev-dependencies] 25 | wasmparser = "0.226.0" 26 | wasmprinter = "0.226.0" 27 | #reqwest = { version = "0.11", features = ["blocking"] } 28 | 29 | [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] 30 | # ezno-checker = { git = "https://github.com/kaleidawave/ezno.git", rev = "96d5058bdbb0cde924be008ca1e5a67fe39f46b9" } 31 | 32 | [lib] 33 | crate-type = ["cdylib", "rlib"] 34 | 35 | [profile.release] 36 | debug = true 37 | lto = true 38 | opt-level = "z" 39 | 40 | [target.wasm32-unknown-unknown] 41 | rustflags = [ 42 | "-C", "link-args=-z stack-size=4194304", 43 | ] 44 | 45 | [build-dependencies] 46 | convert_case = "0.8.0" 47 | 48 | [features] 49 | compiler = [] # if we only want to access flags, we don't want to additionally have all the compiler machinery 50 | panic = [] # if we have DWARF debugging enabled, panicking is more useful than our clunky manual error propagation 51 | default = ["compiler"] 52 | -------------------------------------------------------------------------------- /src/wasm/registries/globals.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::registry::MapRegistry; 3 | use core::ops::Deref; 4 | use wasm_encoder::{ConstExpr, ExportKind, ExportSection, GlobalSection, GlobalType, ValType}; 5 | 6 | #[derive(Copy, Clone, Debug)] 7 | pub struct GlobalMutable(pub bool); 8 | 9 | impl Deref for GlobalMutable { 10 | type Target = bool; 11 | fn deref(&self) -> &bool { 12 | &self.0 13 | } 14 | } 15 | 16 | #[derive(Copy, Clone, Debug)] 17 | pub struct GlobalExportable(pub bool); 18 | 19 | impl Deref for GlobalExportable { 20 | type Target = bool; 21 | fn deref(&self) -> &bool { 22 | &self.0 23 | } 24 | } 25 | 26 | pub type GlobalRegistry = 27 | MapRegistry, (ValType, ConstExpr, GlobalMutable, GlobalExportable)>; 28 | 29 | impl GlobalRegistry { 30 | pub fn finish( 31 | self, 32 | globals: &mut GlobalSection, 33 | exports: &mut ExportSection, 34 | imported_global_count: u32, 35 | imported_function_count: u32, 36 | static_function_count: u32, 37 | ) { 38 | for (key, (ty, suggested_initial, mutable, export)) in self.registry().take() { 39 | if *export { 40 | exports.export( 41 | &key, 42 | ExportKind::Global, 43 | imported_global_count + globals.len(), 44 | ); 45 | } 46 | let actual_initial = match &*key { 47 | "noop_func" => ConstExpr::ref_func(imported_function_count + static_function_count), 48 | _ => suggested_initial, 49 | }; 50 | globals.global( 51 | GlobalType { 52 | val_type: ty, 53 | mutable: *mutable, 54 | shared: false, 55 | }, 56 | &actual_initial, 57 | ); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/instructions/operator/divide.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 2); 5 | let t1 = inputs[0]; 6 | let t2 = inputs[1]; 7 | let f64_local = func.local(ValType::F64)?; 8 | Ok(wasm![ 9 | LocalSet(f64_local), 10 | @nanreduce(t1), 11 | LocalGet(f64_local), 12 | @nanreduce(t2), 13 | F64Div 14 | ]) 15 | } 16 | 17 | // TODO: is integer division acceptable if we can prove that it will give an integer result (or if it is floored?) 18 | pub fn acceptable_inputs() -> HQResult> { 19 | Ok(Rc::from([IrType::Float, IrType::Float])) 20 | } 21 | 22 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 23 | hq_assert_eq!(inputs.len(), 2); 24 | let t1 = inputs[0]; 25 | let t2 = inputs[1]; 26 | let maybe_positive = (t1.maybe_positive() && t2.maybe_positive()) 27 | || (t1.maybe_negative() && t2.maybe_negative()); 28 | let maybe_negative = (t1.maybe_positive() && t2.maybe_negative()) 29 | || (t1.maybe_negative() && t2.maybe_positive()); 30 | let maybe_zero = t1.maybe_zero() || t1.maybe_nan(); // TODO: can this be narrowed to +/-0? 31 | let maybe_infinity = t2.maybe_zero() || t2.maybe_nan(); // TODO: can this be narrowed to +/-infinity? 32 | let maybe_nan = t1.maybe_zero() && t2.maybe_zero(); 33 | Ok(Singleton( 34 | IrType::none_if_false(maybe_positive, IrType::FloatPos) 35 | .or(IrType::none_if_false(maybe_negative, IrType::FloatNeg)) 36 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) 37 | .or(IrType::none_if_false(maybe_infinity, IrType::FloatInf)) 38 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)), 39 | )) 40 | } 41 | 42 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 43 | 44 | crate::instructions_test! {tests; operator_divide; t1, t2 ;} 45 | -------------------------------------------------------------------------------- /src/instructions/looks/switchcostumeto.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::MemArg; 2 | 3 | use super::super::prelude::*; 4 | use crate::wasm::{StepTarget, mem_layout}; 5 | 6 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 7 | let ir_target_index: i32 = func 8 | .target_index() 9 | .try_into() 10 | .map_err(|_| make_hq_bug!("target index out of bounds"))?; 11 | let func_index = func.registries().external_functions().register( 12 | ("looks", "switchcostumeto".into()), 13 | (vec![ValType::I32, ValType::I32], vec![]), 14 | )?; 15 | let wasm_target_index = match func.target() { 16 | StepTarget::Sprite(index) => index, 17 | StepTarget::Stage => 0, 18 | }; 19 | let offset = mem_layout::stage::BLOCK_SIZE 20 | + wasm_target_index * mem_layout::sprite::BLOCK_SIZE 21 | + mem_layout::sprite::COSTUME; 22 | 23 | let local_index = func.local(ValType::I32)?; 24 | Ok(if IrType::QuasiInt.contains(inputs[0]) { 25 | wasm![ 26 | LocalSet(local_index), 27 | I32Const(0), 28 | LocalGet(local_index), 29 | I32Store(MemArg { 30 | offset: offset.into(), 31 | align: 2, 32 | memory_index: 0, 33 | }), 34 | LocalGet(local_index), 35 | I32Const(ir_target_index), 36 | Call(func_index), 37 | ] 38 | } else { 39 | hq_todo!("non-integer input types for looks_switchcostumetos") 40 | }) 41 | } 42 | 43 | pub fn acceptable_inputs() -> HQResult> { 44 | // TODO: accept non-integer values (try to find costume name) 45 | Ok(Rc::from([IrType::IntPos])) 46 | } 47 | 48 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 49 | Ok(ReturnType::None) 50 | } 51 | 52 | pub const REQUESTS_SCREEN_REFRESH: bool = true; 53 | 54 | crate::instructions_test! {tests; looks_switchcostumeto; t ; } 55 | -------------------------------------------------------------------------------- /playground/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'; 2 | import { h, ref, onMounted } from 'vue'; 3 | import Loading from '../components/Loading.vue'; 4 | 5 | let componentCache = Object.setPrototypeOf({}, null); 6 | 7 | 8 | const view = (name) => ({ 9 | setup() { 10 | let component = componentCache[name]; 11 | const loading = ref(!Boolean(component)); 12 | if (loading.value) { 13 | import(`../views/${name}.vue`).then((c) => { 14 | loading.value = false; 15 | component = c.default; 16 | componentCache[name] = component; 17 | }); 18 | } 19 | return () => loading.value ? h(Loading) : h(component); 20 | } 21 | }); 22 | 23 | const router = createRouter({ 24 | history: (import.meta.env.VITE_HASH_HISTORY ? createWebHashHistory : createWebHistory)(import.meta.env.BASE_URL), 25 | routes: [ 26 | { 27 | path: '/', 28 | name: 'home', 29 | component: view('HomeView'), 30 | }, 31 | { 32 | path: '/projects/:id(\\d+)', 33 | name: 'projectIdPlayer', 34 | component: view('ProjectIdView'), 35 | props: true, 36 | }, 37 | { 38 | path: '/projects/file', 39 | name: 'projectFilePlayer', 40 | component: view('ProjectFileView'), 41 | }, 42 | { 43 | path: '/projects/test', 44 | name: 'testProjectPlayer', 45 | component: view('TestProject'), 46 | }, 47 | { 48 | path: '/about', 49 | name: 'about', 50 | component: view('AboutView'), 51 | }, 52 | { 53 | path: '/settings', 54 | name: 'settings', 55 | component: view('Settings'), 56 | }, 57 | { 58 | path: '/:_(.*)*', 59 | name: '404', 60 | component: view('404'), 61 | } 62 | ] 63 | }); 64 | 65 | router.afterEach((to, from) => { 66 | document.getElementById('canonical-rel').setAttribute('href', `https://hyperquark.edgecompute.app${to.path}`); 67 | }) 68 | 69 | export default router; 70 | -------------------------------------------------------------------------------- /playground/components/ProjectInput.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 39 | 40 | 62 | -------------------------------------------------------------------------------- /js/shared.ts: -------------------------------------------------------------------------------- 1 | let _target_names: Array; 2 | let _setup = false; 3 | let _target_bubbles: Array; 4 | let _renderer; 5 | let _pen_skin: number; 6 | let _target_skins: Array<[number, number]>; 7 | let _costumes: Array>; 8 | 9 | type Costume = { 10 | data: string, 11 | dataFormat: string, 12 | } 13 | 14 | export function setup( 15 | target_names: Array, 16 | renderer: object, 17 | pen_skin: number, 18 | target_skins: Array<[number, number]>, 19 | costumes: Array>, 20 | ) { 21 | _target_names = target_names; 22 | _target_bubbles = _target_names.map((_) => null); 23 | console.log(_target_names, _target_bubbles); 24 | _renderer = renderer; 25 | _pen_skin = pen_skin; 26 | _target_skins = target_skins; 27 | _costumes = costumes; 28 | _setup = true; 29 | } 30 | 31 | export function is_setup(): boolean { 32 | return _setup; 33 | } 34 | 35 | function check_setup() { 36 | if (!_setup) { 37 | throw new Error("shared state must be set up before use!"); 38 | } 39 | } 40 | 41 | export function target_names(): Array { 42 | check_setup(); 43 | return _target_names; 44 | } 45 | 46 | export function renderer(): object { 47 | check_setup(); 48 | return _renderer; 49 | } 50 | 51 | export function pen_skin(): number { 52 | check_setup(); 53 | return _pen_skin; 54 | } 55 | 56 | export function target_skins(): Array<[number, number]> { 57 | check_setup(); 58 | return _target_skins; 59 | } 60 | 61 | export function costumes(): Array> { 62 | check_setup(); 63 | return _costumes; 64 | } 65 | 66 | export function update_bubble( 67 | target_index: number, 68 | verb: "say" | "think", 69 | text: string 70 | ) { 71 | check_setup(); 72 | if (!_target_bubbles[target_index]) { 73 | _target_bubbles[target_index] = _renderer.createSkin( 74 | "text", 75 | "sprite", 76 | verb, 77 | text, 78 | false 79 | ); 80 | } else { 81 | _renderer.updateTextSkin( 82 | _target_bubbles[target_index][0], 83 | verb, 84 | text, 85 | false 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/instructions/control/if_else.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::ir::Step; 3 | use wasm_encoder::BlockType; 4 | 5 | #[derive(Debug)] 6 | pub struct Fields { 7 | pub branch_if: Rc, 8 | pub branch_else: Rc, 9 | } 10 | 11 | impl Clone for Fields { 12 | fn clone(&self) -> Self { 13 | #[expect(clippy::unwrap_used, reason = "clone does not return Result")] 14 | Self { 15 | branch_if: Step::clone(&self.branch_if, false).unwrap(), 16 | branch_else: Step::clone(&self.branch_else, false).unwrap(), 17 | } 18 | } 19 | } 20 | 21 | impl fmt::Display for Fields { 22 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 23 | write!( 24 | f, 25 | r#"{{ 26 | "branch_if": {}, 27 | "branch_else": {}, 28 | }}"#, 29 | self.branch_if, self.branch_else 30 | ) 31 | } 32 | } 33 | 34 | pub fn wasm( 35 | func: &StepFunc, 36 | _inputs: Rc<[IrType]>, 37 | Fields { 38 | branch_if, 39 | branch_else, 40 | }: &Fields, 41 | ) -> HQResult> { 42 | let if_instructions = func.compile_inner_step(branch_if)?; 43 | let else_instructions = func.compile_inner_step(branch_else)?; 44 | let block_type = func 45 | .registries() 46 | .types() 47 | .register_default((vec![ValType::I32], vec![]))?; 48 | Ok(wasm![ 49 | Block(BlockType::FunctionType(block_type)), 50 | Block(BlockType::FunctionType(block_type)), 51 | I32Eqz, 52 | BrIf(0) 53 | ] 54 | .into_iter() 55 | .chain(if_instructions) 56 | .chain(wasm![Br(1), End,]) 57 | .chain(else_instructions) 58 | .chain(wasm![End]) 59 | .collect()) 60 | } 61 | 62 | pub fn acceptable_inputs(_fields: &Fields) -> HQResult> { 63 | Ok(Rc::from([IrType::Boolean])) 64 | } 65 | 66 | pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult { 67 | Ok(ReturnType::None) 68 | } 69 | 70 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 71 | 72 | // crate::instructions_test! {none; hq__if; @ super::Fields(None)} 73 | -------------------------------------------------------------------------------- /src/instructions/operator/ceiling.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 1); 5 | let t1 = inputs[0]; 6 | Ok( 7 | if IrType::QuasiInt 8 | .or(IrType::FloatInt) 9 | .or(IrType::FloatInf) 10 | .contains(t1) 11 | { 12 | wasm![] 13 | } else if IrType::FloatNan.contains(t1) { 14 | wasm![Drop, F64Const(0.0)] 15 | } else if IrType::Float.contains(t1) { 16 | wasm![ 17 | @nanreduce(t1), 18 | F64Ceil, 19 | ] 20 | } else { 21 | hq_bug!("bad input: {:?}", inputs) 22 | }, 23 | ) 24 | } 25 | 26 | pub fn acceptable_inputs() -> HQResult> { 27 | Ok(Rc::from([IrType::Number])) 28 | } 29 | 30 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 31 | hq_assert_eq!(inputs.len(), 1); 32 | let t1 = inputs[0]; 33 | let maybe_positive = t1.maybe_positive(); 34 | let maybe_negative = t1.maybe_negative(); 35 | let maybe_zero = maybe_negative || t1.maybe_zero() || t1.maybe_nan(); 36 | Ok(Singleton(if IrType::QuasiInt.contains(t1) { 37 | IrType::none_if_false(maybe_positive, IrType::IntPos) 38 | .or(IrType::none_if_false(maybe_negative, IrType::IntNeg)) 39 | .or(IrType::none_if_false(t1.maybe_zero(), IrType::IntZero)) 40 | } else { 41 | IrType::none_if_false(maybe_positive, IrType::FloatPosInt) 42 | .or(IrType::none_if_false(maybe_negative, IrType::FloatNegInt)) 43 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) 44 | .or(IrType::none_if_false( 45 | IrType::FloatPosInf.intersects(t1), 46 | IrType::FloatPosInf, 47 | )) 48 | .or(IrType::none_if_false( 49 | IrType::FloatNegInf.intersects(t1), 50 | IrType::FloatNegInf, 51 | )) 52 | })) 53 | } 54 | 55 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 56 | 57 | crate::instructions_test! {tests; operator_ceiling; t } 58 | -------------------------------------------------------------------------------- /src/instructions/operator/floor.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 1); 5 | let t1 = inputs[0]; 6 | Ok( 7 | if IrType::QuasiInt 8 | .or(IrType::FloatInt) 9 | .or(IrType::FloatInf) 10 | .contains(t1) 11 | { 12 | wasm![] 13 | } else if IrType::FloatNan.contains(t1) { 14 | wasm![Drop, F64Const(0.0)] 15 | } else if IrType::Float.contains(t1) { 16 | wasm![ 17 | @nanreduce(t1), 18 | F64Floor, 19 | ] 20 | } else { 21 | hq_bug!("bad input: {:?}", inputs) 22 | }, 23 | ) 24 | } 25 | 26 | pub fn acceptable_inputs() -> HQResult> { 27 | Ok(Rc::from([IrType::Number])) 28 | } 29 | 30 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 31 | hq_assert_eq!(inputs.len(), 1); 32 | let t1 = inputs[0]; 33 | let maybe_positive = t1.maybe_positive(); 34 | let maybe_negative = t1.maybe_negative(); 35 | let maybe_zero = maybe_positive || t1.maybe_zero() || t1.maybe_nan(); 36 | Ok(Singleton(if IrType::QuasiInt.contains(t1) { 37 | IrType::none_if_false(maybe_positive, IrType::IntPos) 38 | .or(IrType::none_if_false(maybe_negative, IrType::IntNeg)) 39 | .or(IrType::none_if_false(t1.maybe_zero(), IrType::IntZero)) 40 | } else { 41 | IrType::none_if_false(maybe_positive, IrType::FloatPosInt) 42 | .or(IrType::none_if_false(maybe_negative, IrType::FloatNegInt)) 43 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) 44 | .or(IrType::none_if_false( 45 | IrType::FloatPosInf.intersects(t1), 46 | IrType::FloatPosInf, 47 | )) 48 | .or(IrType::none_if_false( 49 | IrType::FloatNegInf.intersects(t1), 50 | IrType::FloatNegInf, 51 | )) 52 | })) 53 | } 54 | 55 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 56 | 57 | crate::instructions_test! {tests; operator_floor; t } 58 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | on: [push, workflow_dispatch] 2 | 3 | name: Deploy 4 | 5 | jobs: 6 | deploy: 7 | name: Build WASM & website 8 | runs-on: ubuntu-latest 9 | env: 10 | BRANCH_NAME: ${{ github.head_ref || github.ref_name }} 11 | permissions: 12 | contents: write 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v4 16 | 17 | - name: Install nightly toolchain 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: nightly 22 | override: true 23 | target: wasm32-unknown-unknown 24 | 25 | - name: Install wasm-bindgen 26 | uses: actions-rs/cargo@v1 27 | with: 28 | command: install 29 | args: -f wasm-bindgen-cli 30 | 31 | - name: Install cargo-outdir 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: install 35 | args: cargo-outdir 36 | 37 | - name: Install binaryen 38 | run: sudo apt-get install binaryen 39 | 40 | - name: Install node 41 | uses: actions/setup-node@v3 42 | with: 43 | node-version: "20.x" 44 | 45 | - name: Run npm install 46 | run: | 47 | npm install 48 | npm i -g vite 49 | npm i -g binaryen@nightly 50 | 51 | - name: Build 52 | env: 53 | VITE_HASH_HISTORY: true 54 | run: | 55 | chmod +x build.sh && ./build.sh -Wpz 56 | vite build --base=/hyperquark/$BRANCH_NAME/ 57 | 58 | - name: Move files to tmp 59 | run: mv ./playground/dist /tmp/hq-dist 60 | 61 | - name: checkout gh-pages 62 | uses: actions/checkout@v4 63 | with: 64 | ref: gh-pages 65 | 66 | - name: move file to gh-pages 67 | run: | 68 | rm -rf ./$BRANCH_NAME 69 | mv /tmp/hq-dist ./$BRANCH_NAME 70 | #mv ./main/* ./ 71 | 72 | - name: Commit changes 73 | uses: stefanzweifel/git-auto-commit-action@v5 74 | with: 75 | branch: gh-pages 76 | push_options: '--force-with-lease' 77 | -------------------------------------------------------------------------------- /src/instructions/operator/abs.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 1); 5 | let t1 = inputs[0]; 6 | Ok(if IrType::QuasiInt.contains(t1) { 7 | if IrType::IntPos 8 | .or(IrType::IntZero) 9 | .or(IrType::Boolean) 10 | .contains(t1) 11 | { 12 | wasm![] 13 | } else { 14 | let local_index = func.local(ValType::I32)?; 15 | wasm![ 16 | LocalTee(local_index), 17 | LocalGet(local_index), 18 | I32Const(31), 19 | I32ShrS, 20 | I32Xor, 21 | LocalGet(local_index), 22 | I32Const(31), 23 | I32ShrS, 24 | I32Sub, 25 | ] 26 | } 27 | } else if IrType::Float.contains(t1) { 28 | if IrType::FloatPos.contains(t1) { 29 | wasm![] 30 | } else { 31 | wasm![ 32 | @nanreduce(t1), 33 | F64Abs, 34 | ] 35 | } 36 | } else { 37 | hq_bug!("bad input: {:?}", inputs) 38 | }) 39 | } 40 | 41 | pub fn acceptable_inputs() -> HQResult> { 42 | Ok(Rc::from([IrType::Number])) 43 | } 44 | 45 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 46 | hq_assert_eq!(inputs.len(), 1); 47 | let t1 = inputs[0]; 48 | let maybe_zero = t1.maybe_zero() || t1.maybe_nan(); 49 | let maybe_real = t1.intersects( 50 | IrType::FloatReal 51 | .or(IrType::IntNonZero) 52 | .or(IrType::BooleanTrue), 53 | ); 54 | Ok(Singleton(if IrType::QuasiInt.contains(t1) { 55 | IrType::none_if_false(maybe_real, IrType::IntPos) 56 | .or(IrType::none_if_false(maybe_zero, IrType::IntZero)) 57 | } else { 58 | IrType::none_if_false(maybe_real, IrType::FloatPosReal) 59 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) 60 | .or(IrType::none_if_false( 61 | IrType::FloatInf.intersects(t1), 62 | IrType::FloatPosInf, 63 | )) 64 | })) 65 | } 66 | 67 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 68 | 69 | crate::instructions_test! {tests; operator_abs; t } 70 | -------------------------------------------------------------------------------- /playground/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | :root { 54 | --transition-time: 0.4s; 55 | } 56 | 57 | *, 58 | *::before, 59 | *::after { 60 | box-sizing: border-box; 61 | margin: 0; 62 | font-weight: normal; 63 | } 64 | 65 | body { 66 | min-height: 100vh; 67 | color: var(--color-text); 68 | background: var(--color-background); 69 | transition: color 0.5s, background-color 0.5s; 70 | line-height: 1.6; 71 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 72 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 73 | font-size: 15px; 74 | text-rendering: optimizeLegibility; 75 | -webkit-font-smoothing: antialiased; 76 | -moz-osx-font-smoothing: grayscale; 77 | } -------------------------------------------------------------------------------- /src/wasm/registries.rs: -------------------------------------------------------------------------------- 1 | pub mod functions; 2 | pub mod globals; 3 | pub mod strings; 4 | pub mod tables; 5 | pub mod targets; 6 | pub mod types; 7 | pub mod variables; 8 | 9 | use crate::prelude::*; 10 | pub use functions::{ExternalFunctionRegistry, StaticFunctionRegistry}; 11 | pub use globals::{GlobalExportable, GlobalMutable, GlobalRegistry}; 12 | pub use strings::StringRegistry; 13 | pub use tables::{StepsTable, StringsTable, TableRegistry, ThreadsTable}; 14 | pub use targets::SpriteRegistry; 15 | pub use types::TypeRegistry; 16 | pub use variables::VariableRegistry; 17 | 18 | pub struct Registries { 19 | strings: StringRegistry, 20 | external_functions: ExternalFunctionRegistry, 21 | static_functions: StaticFunctionRegistry, 22 | types: TypeRegistry, 23 | tables: TableRegistry, 24 | globals: Rc, 25 | variables: VariableRegistry, 26 | sprites: SpriteRegistry, 27 | } 28 | 29 | impl Default for Registries { 30 | fn default() -> Self { 31 | let globals = Rc::new(GlobalRegistry::default()); 32 | let variables = VariableRegistry::new(&globals); 33 | Self { 34 | globals, 35 | variables, 36 | strings: StringRegistry::default(), 37 | external_functions: ExternalFunctionRegistry::default(), 38 | tables: TableRegistry::default(), 39 | types: TypeRegistry::default(), 40 | sprites: SpriteRegistry::default(), 41 | static_functions: StaticFunctionRegistry::default(), 42 | } 43 | } 44 | } 45 | 46 | impl Registries { 47 | pub const fn strings(&self) -> &StringRegistry { 48 | &self.strings 49 | } 50 | 51 | pub const fn external_functions(&self) -> &ExternalFunctionRegistry { 52 | &self.external_functions 53 | } 54 | 55 | pub const fn static_functions(&self) -> &StaticFunctionRegistry { 56 | &self.static_functions 57 | } 58 | 59 | pub const fn types(&self) -> &TypeRegistry { 60 | &self.types 61 | } 62 | 63 | pub const fn tables(&self) -> &TableRegistry { 64 | &self.tables 65 | } 66 | 67 | pub fn globals(&self) -> &GlobalRegistry { 68 | &self.globals 69 | } 70 | 71 | pub const fn variables(&self) -> &VariableRegistry { 72 | &self.variables 73 | } 74 | 75 | pub const fn sprites(&self) -> &SpriteRegistry { 76 | &self.sprites 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /playground/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 41 | 42 | 89 | -------------------------------------------------------------------------------- /src/instructions/procedures/call_warp.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::ir::Proc; 3 | use crate::wasm::{StepFunc, WasmProject}; 4 | use wasm_encoder::Instruction as WInstruction; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct Fields { 8 | pub proc: Rc, 9 | } 10 | 11 | impl fmt::Display for Fields { 12 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 13 | write!( 14 | f, 15 | r#"{{ 16 | "proc": {:?}, 17 | }}"#, 18 | self.proc.proccode() 19 | ) 20 | } 21 | } 22 | 23 | pub fn wasm( 24 | func: &StepFunc, 25 | inputs: Rc<[IrType]>, 26 | Fields { proc }: &Fields, 27 | ) -> HQResult> { 28 | let locals = inputs 29 | .iter() 30 | .map(|ty| func.local(WasmProject::ir_type_to_wasm(*ty)?)) 31 | .collect::>>()?; 32 | 33 | let mut wasm = locals 34 | .iter() 35 | .rev() 36 | .copied() 37 | .map(WInstruction::LocalSet) 38 | .map(InternalInstruction::Immediate) 39 | .collect::>(); 40 | 41 | for ((&input, local), param) in inputs.iter().zip(locals).zip( 42 | proc.context() 43 | .arg_vars() 44 | .try_borrow()? 45 | .iter() 46 | .map(|var| **var.possible_types().borrow()), 47 | ) { 48 | wasm.extend(if param.is_base_type() { 49 | wasm![LocalGet(local)] 50 | } else { 51 | wasm![ 52 | LocalGet(local), 53 | @boxed(input), 54 | ] 55 | }); 56 | } 57 | 58 | wasm.extend(wasm![ 59 | LocalGet((func.params().len() - 1).try_into().map_err(|_| make_hq_bug!("local index out of bounds"))?), 60 | #LazyWarpedProcCall(Rc::clone(proc)) 61 | ]); 62 | 63 | Ok(wasm) 64 | } 65 | 66 | pub fn acceptable_inputs(Fields { proc }: &Fields) -> HQResult> { 67 | Ok(proc 68 | .context() 69 | .arg_vars() 70 | .try_borrow()? 71 | .iter() 72 | .map(|var| *var.possible_types()) 73 | .collect()) 74 | } 75 | 76 | pub fn output_type(_inputs: Rc<[IrType]>, Fields { proc }: &Fields) -> HQResult { 77 | Ok(MultiValue( 78 | proc.context() 79 | .return_vars() 80 | .try_borrow()? 81 | .iter() 82 | .map(|var| *var.possible_types()) 83 | .collect(), 84 | )) 85 | } 86 | 87 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 88 | -------------------------------------------------------------------------------- /src/instructions/looks/say.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Fields { 5 | pub debug: bool, 6 | pub target_idx: u32, 7 | } 8 | 9 | impl fmt::Display for Fields { 10 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 11 | write!( 12 | f, 13 | r#"{{ 14 | "debug": {}, 15 | "target_idx": {}, 16 | }}"#, 17 | self.debug, self.target_idx 18 | ) 19 | } 20 | } 21 | 22 | pub fn wasm( 23 | func: &StepFunc, 24 | inputs: Rc<[IrType]>, 25 | &Fields { debug, target_idx }: &Fields, 26 | ) -> HQResult> { 27 | let prefix = String::from(if debug { "say_debug" } else { "say" }); 28 | let itarget_idx: i32 = target_idx 29 | .try_into() 30 | .map_err(|_| make_hq_bug!("target index out of bounds"))?; 31 | Ok(if IrType::QuasiInt.contains(inputs[0]) { 32 | let func_index = func.registries().external_functions().register( 33 | ("looks", format!("{prefix}_int").into_boxed_str()), 34 | (vec![ValType::I32, ValType::I32], vec![]), 35 | )?; 36 | wasm![I32Const(itarget_idx), Call(func_index)] 37 | } else if IrType::Float.contains(inputs[0]) { 38 | let func_index = func.registries().external_functions().register( 39 | ("looks", format!("{prefix}_float").into_boxed_str()), 40 | (vec![ValType::F64, ValType::I32], vec![]), 41 | )?; 42 | wasm![I32Const(itarget_idx), Call(func_index)] 43 | } else if IrType::String.contains(inputs[0]) { 44 | let func_index = func.registries().external_functions().register( 45 | ("looks", format!("{prefix}_string").into_boxed_str()), 46 | (vec![ValType::EXTERNREF, ValType::I32], vec![]), 47 | )?; 48 | wasm![I32Const(itarget_idx), Call(func_index)] 49 | } else { 50 | hq_bug!("bad input") 51 | }) 52 | } 53 | 54 | pub fn acceptable_inputs(_fields: &Fields) -> HQResult> { 55 | Ok(Rc::from([IrType::String.or(IrType::Number)])) 56 | } 57 | 58 | pub fn output_type(inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult { 59 | if !(IrType::Number.or(IrType::String).contains(inputs[0])) { 60 | hq_todo!("unimplemented input type: {:?}", inputs) 61 | } 62 | Ok(ReturnType::None) 63 | } 64 | 65 | pub const REQUESTS_SCREEN_REFRESH: bool = true; 66 | 67 | crate::instructions_test! {tests_debug; looks_say; t @ super::Fields { debug: true, target_idx: 0 }} 68 | crate::instructions_test! {tests_non_debug; looks_say; t @ super::Fields { debug: false, target_idx: 0, }} 69 | -------------------------------------------------------------------------------- /src/instructions/looks/think.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Fields { 5 | pub debug: bool, 6 | pub target_idx: u32, 7 | } 8 | 9 | impl fmt::Display for Fields { 10 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 11 | write!( 12 | f, 13 | r#"{{ 14 | "debug": {}, 15 | "target_idx": {}, 16 | }}"#, 17 | self.debug, self.target_idx 18 | ) 19 | } 20 | } 21 | 22 | pub fn wasm( 23 | func: &StepFunc, 24 | inputs: Rc<[IrType]>, 25 | &Fields { debug, target_idx }: &Fields, 26 | ) -> HQResult> { 27 | let prefix = String::from(if debug { "think_debug" } else { "think" }); 28 | let itarget_idx: i32 = target_idx 29 | .try_into() 30 | .map_err(|_| make_hq_bug!("target index out of bounds"))?; 31 | Ok(if IrType::QuasiInt.contains(inputs[0]) { 32 | let func_index = func.registries().external_functions().register( 33 | ("looks", format!("{prefix}_int").into_boxed_str()), 34 | (vec![ValType::I32, ValType::I32], vec![]), 35 | )?; 36 | wasm![I32Const(itarget_idx), Call(func_index)] 37 | } else if IrType::Float.contains(inputs[0]) { 38 | let func_index = func.registries().external_functions().register( 39 | ("looks", format!("{prefix}_float").into_boxed_str()), 40 | (vec![ValType::F64, ValType::I32], vec![]), 41 | )?; 42 | wasm![I32Const(itarget_idx), Call(func_index)] 43 | } else if IrType::String.contains(inputs[0]) { 44 | let func_index = func.registries().external_functions().register( 45 | ("looks", format!("{prefix}_string").into_boxed_str()), 46 | (vec![ValType::EXTERNREF, ValType::I32], vec![]), 47 | )?; 48 | wasm![I32Const(itarget_idx), Call(func_index)] 49 | } else { 50 | hq_bug!("bad input") 51 | }) 52 | } 53 | 54 | pub fn acceptable_inputs(_fields: &Fields) -> HQResult> { 55 | Ok(Rc::from([IrType::String.or(IrType::Number)])) 56 | } 57 | 58 | pub fn output_type(inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult { 59 | if !(IrType::Number.or(IrType::String).contains(inputs[0])) { 60 | hq_todo!("unimplemented input type: {:?}", inputs) 61 | } 62 | Ok(ReturnType::None) 63 | } 64 | 65 | pub const REQUESTS_SCREEN_REFRESH: bool = true; 66 | 67 | crate::instructions_test! {tests_debug; looks_think; t @ super::Fields { debug: true, target_idx: 0 }} 68 | crate::instructions_test! {tests_non_debug; looks_think; t @ super::Fields { debug: false, target_idx: 0, }} 69 | -------------------------------------------------------------------------------- /src/instructions/control/loop.rs: -------------------------------------------------------------------------------- 1 | // for use in warped contexts only. 2 | 3 | use super::super::prelude::*; 4 | use crate::ir::Step; 5 | use wasm_encoder::BlockType; 6 | 7 | #[derive(Debug)] 8 | pub struct Fields { 9 | pub first_condition: Option>, 10 | pub condition: Rc, 11 | pub body: Rc, 12 | pub flip_if: bool, 13 | } 14 | 15 | impl Clone for Fields { 16 | fn clone(&self) -> Self { 17 | #[expect(clippy::unwrap_used, reason = "clone does not return Result")] 18 | Self { 19 | first_condition: self 20 | .first_condition 21 | .as_ref() 22 | .map(|step| Step::clone(step, false).unwrap()), 23 | condition: Step::clone(&self.condition, false).unwrap(), 24 | body: Step::clone(&self.body, false).unwrap(), 25 | flip_if: self.flip_if, 26 | } 27 | } 28 | } 29 | 30 | impl fmt::Display for Fields { 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | write!(f, "{{")?; 33 | if let Some(ref step) = self.first_condition { 34 | write!(f, r#""first_condition": {step},"#)?; 35 | } 36 | write!( 37 | f, 38 | r#" 39 | "condition": {}, 40 | "body": {}, 41 | "flip_if": {} 42 | }}"#, 43 | self.condition, self.body, self.flip_if 44 | ) 45 | } 46 | } 47 | 48 | pub fn wasm( 49 | func: &StepFunc, 50 | _inputs: Rc<[IrType]>, 51 | Fields { 52 | first_condition, 53 | condition, 54 | body, 55 | flip_if, 56 | }: &Fields, 57 | ) -> HQResult> { 58 | let inner_instructions = func.compile_inner_step(body)?; 59 | let first_condition_instructions = func.compile_inner_step( 60 | &first_condition 61 | .clone() 62 | .unwrap_or_else(|| Rc::clone(condition)), 63 | )?; 64 | let condition_instructions = func.compile_inner_step(condition)?; 65 | Ok(wasm![Block(BlockType::Empty),] 66 | .into_iter() 67 | .chain(first_condition_instructions) 68 | .chain(if *flip_if { 69 | wasm![BrIf(0), Loop(BlockType::Empty)] 70 | } else { 71 | wasm![I32Eqz, BrIf(0), Loop(BlockType::Empty)] 72 | }) 73 | .chain(inner_instructions) 74 | .chain(condition_instructions) 75 | .chain(if *flip_if { 76 | wasm![I32Eqz, BrIf(0), End, End] 77 | } else { 78 | wasm![BrIf(0), End, End] 79 | }) 80 | .collect()) 81 | } 82 | 83 | pub fn acceptable_inputs(_fields: &Fields) -> HQResult> { 84 | Ok(Rc::from([])) 85 | } 86 | 87 | pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult { 88 | Ok(ReturnType::None) 89 | } 90 | 91 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 92 | 93 | // crate::instructions_test! {none; hq_repeat; @ super::Fields(None)} 94 | -------------------------------------------------------------------------------- /src/ir/thread.rs: -------------------------------------------------------------------------------- 1 | use super::blocks::NextBlocks; 2 | use super::{Event, IrProject, Step, StepContext, Target}; 3 | use crate::prelude::*; 4 | use crate::sb3::{Block, BlockMap, BlockOpcode}; 5 | use crate::wasm::WasmFlags; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct Thread { 9 | event: Event, 10 | first_step: Rc, 11 | } 12 | 13 | impl Thread { 14 | pub const fn event(&self) -> Event { 15 | self.event 16 | } 17 | 18 | pub const fn first_step(&self) -> &Rc { 19 | &self.first_step 20 | } 21 | 22 | /// tries to construct a thread from a top-level block. 23 | /// Returns Ok(None) if the top-level block is not a valid event or if there is no next block. 24 | pub fn try_from_top_block( 25 | block: &Block, 26 | blocks: &BlockMap, 27 | target: &Rc, 28 | project: &Weak, 29 | debug: bool, 30 | flags: &WasmFlags, 31 | ) -> HQResult> { 32 | let Some(block_info) = block.block_info() else { 33 | return Ok(None); 34 | }; 35 | #[expect(clippy::wildcard_enum_match_arm, reason = "too many variants to match")] 36 | let event = match block_info.opcode { 37 | BlockOpcode::event_whenflagclicked => Event::FlagCLicked, 38 | BlockOpcode::event_whenbackdropswitchesto 39 | | BlockOpcode::event_whenbroadcastreceived 40 | | BlockOpcode::event_whengreaterthan 41 | | BlockOpcode::event_whenkeypressed 42 | | BlockOpcode::event_whenstageclicked 43 | | BlockOpcode::event_whenthisspriteclicked 44 | | BlockOpcode::event_whentouchingobject => { 45 | hq_todo!("unimplemented event {:?}", block_info.opcode) 46 | } 47 | _ => return Ok(None), 48 | }; 49 | let Some(next_id) = &block_info.next else { 50 | return Ok(None); 51 | }; 52 | let next = blocks 53 | .get(next_id) 54 | .ok_or_else(|| make_hq_bug!("block not found in BlockMap"))?; 55 | let first_step = Step::from_block( 56 | next, 57 | next_id.clone(), 58 | blocks, 59 | &StepContext { 60 | target: Rc::clone(target), 61 | proc_context: None, 62 | warp: false, // steps from top blocks are never warped 63 | debug, 64 | }, 65 | project, 66 | NextBlocks::new(true), 67 | true, 68 | flags, 69 | )?; 70 | Ok(Some(Self { event, first_step })) 71 | } 72 | } 73 | 74 | impl fmt::Display for Thread { 75 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 76 | let event = self.event(); 77 | let first_step = self.first_step().id(); 78 | write!( 79 | f, 80 | r#"{{ 81 | "event": "{event:?}", 82 | "first_step": "{first_step}", 83 | }}"# 84 | ) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/instructions/pen/pendown.rs: -------------------------------------------------------------------------------- 1 | use wasm_encoder::MemArg; 2 | 3 | use super::super::prelude::*; 4 | use crate::wasm::{StepTarget, mem_layout}; 5 | 6 | pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 7 | let func_index = func.registries().external_functions().register( 8 | ("pen", "point".into()), 9 | ( 10 | vec![ 11 | ValType::F64, 12 | ValType::F64, 13 | ValType::F64, 14 | ValType::F32, 15 | ValType::F32, 16 | ValType::F32, 17 | ValType::F32, 18 | ], 19 | vec![], 20 | ), 21 | )?; 22 | let StepTarget::Sprite(wasm_target_index) = func.target() else { 23 | hq_bad_proj!("looks_setpendown called in stage") 24 | }; 25 | let sprite_offset = 26 | mem_layout::stage::BLOCK_SIZE + wasm_target_index * mem_layout::sprite::BLOCK_SIZE; 27 | Ok(wasm![ 28 | I32Const(0), 29 | I32Const(1), 30 | I32Store8(MemArg { 31 | offset: (sprite_offset + mem_layout::sprite::PEN_DOWN).into(), 32 | align: 0, 33 | memory_index: 0, 34 | }), 35 | I32Const(0), 36 | F64Load(MemArg { 37 | offset: (sprite_offset + mem_layout::sprite::PEN_SIZE).into(), 38 | align: 3, 39 | memory_index: 0, 40 | }), 41 | I32Const(0), 42 | F64Load(MemArg { 43 | offset: (sprite_offset + mem_layout::sprite::X).into(), 44 | align: 3, 45 | memory_index: 0, 46 | }), 47 | I32Const(0), 48 | F64Load(MemArg { 49 | offset: (sprite_offset + mem_layout::sprite::Y).into(), 50 | align: 3, 51 | memory_index: 0, 52 | }), 53 | I32Const(0), 54 | F32Load(MemArg { 55 | offset: (sprite_offset + mem_layout::sprite::PEN_COLOR_R).into(), 56 | align: 2, 57 | memory_index: 0, 58 | }), 59 | I32Const(0), 60 | F32Load(MemArg { 61 | offset: (sprite_offset + mem_layout::sprite::PEN_COLOR_G).into(), 62 | align: 2, 63 | memory_index: 0, 64 | }), 65 | I32Const(0), 66 | F32Load(MemArg { 67 | offset: (sprite_offset + mem_layout::sprite::PEN_COLOR_B).into(), 68 | align: 2, 69 | memory_index: 0, 70 | }), 71 | I32Const(0), 72 | F32Load(MemArg { 73 | offset: (sprite_offset + mem_layout::sprite::PEN_COLOR_A).into(), 74 | align: 2, 75 | memory_index: 0, 76 | }), 77 | Call(func_index), 78 | ]) 79 | } 80 | 81 | pub fn acceptable_inputs() -> HQResult> { 82 | Ok(Rc::from([])) 83 | } 84 | 85 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 86 | Ok(ReturnType::None) 87 | } 88 | 89 | pub const REQUESTS_SCREEN_REFRESH: bool = true; 90 | 91 | crate::instructions_test! {tests; pen_pendown; ; } 92 | -------------------------------------------------------------------------------- /playground/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 36 | 37 | 65 | 66 | -------------------------------------------------------------------------------- /src/instructions/operator/add.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 2); 5 | let t1 = inputs[0]; 6 | let t2 = inputs[1]; 7 | Ok(if IrType::QuasiInt.contains(t1) { 8 | if IrType::QuasiInt.contains(t2) { 9 | wasm![I32Add] 10 | } else if IrType::Float.contains(t2) { 11 | let f64_local = func.local(ValType::F64)?; 12 | wasm![ 13 | LocalSet(f64_local), 14 | F64ConvertI32S, 15 | LocalGet(f64_local), 16 | @nanreduce(t2), 17 | F64Add, 18 | ] 19 | } else { 20 | hq_bug!("bad input") 21 | } 22 | } else if IrType::Float.contains(t1) { 23 | if IrType::Float.contains(t2) { 24 | let f64_local = func.local(ValType::F64)?; 25 | wasm![ 26 | @nanreduce(t2), 27 | LocalSet(f64_local), 28 | @nanreduce(t1), 29 | LocalGet(f64_local), 30 | F64Add 31 | ] 32 | } else if IrType::QuasiInt.contains(t2) { 33 | let i32_local = func.local(ValType::I32)?; 34 | wasm![ 35 | LocalSet(i32_local), 36 | @nanreduce(t1), 37 | LocalGet(i32_local), 38 | F64ConvertI32S, 39 | F64Add 40 | ] 41 | } else { 42 | hq_bug!("bad input") 43 | } 44 | } else { 45 | hq_bug!("bad input: {:?}", inputs) 46 | }) 47 | } 48 | 49 | pub fn acceptable_inputs() -> HQResult> { 50 | Ok(Rc::from([IrType::Number, IrType::Number])) 51 | } 52 | 53 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 54 | hq_assert_eq!(inputs.len(), 2); 55 | let t1 = inputs[0]; 56 | let t2 = inputs[1]; 57 | let maybe_positive = t1.maybe_positive() || t2.maybe_positive(); 58 | let maybe_negative = t1.maybe_negative() || t2.maybe_negative(); 59 | let maybe_zero = (t1.maybe_zero() || t1.maybe_nan()) && (t2.maybe_zero() || t2.maybe_nan()); 60 | let maybe_nan = (IrType::FloatNegInf.intersects(t1) && IrType::FloatPosInf.intersects(t2)) 61 | || (IrType::FloatNegInf.intersects(t2) && IrType::FloatPosInf.intersects(t1)); 62 | Ok(Singleton(if IrType::QuasiInt.contains(t1.or(t2)) { 63 | IrType::none_if_false(maybe_positive, IrType::IntPos) 64 | .or(IrType::none_if_false(maybe_negative, IrType::IntNeg)) 65 | .or(IrType::none_if_false(maybe_zero, IrType::IntZero)) 66 | } else { 67 | IrType::none_if_false(maybe_positive, IrType::FloatPos) 68 | .or(IrType::none_if_false(maybe_negative, IrType::FloatNeg)) 69 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) 70 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) 71 | })) 72 | } 73 | 74 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 75 | 76 | crate::instructions_test! {tests; operator_add; t1, t2 ;} 77 | -------------------------------------------------------------------------------- /playground/lib/settings.js: -------------------------------------------------------------------------------- 1 | import * as hyperquarkExports from '../../js/no-compiler/hyperquark.js'; 2 | import { WasmFlags, WasmFeature, all_wasm_features, wasm_feature_detect_name } from '../../js/no-compiler/hyperquark.js'; 3 | import * as WasmFeatureDetect from 'wasm-feature-detect'; 4 | export { WasmFlags }; 5 | 6 | console.log(WasmFeature) 7 | window.hyperquarkExports = hyperquarkExports; 8 | 9 | export const supportedWasmFeatures = await getSupportedWasmFeatures(); 10 | export const defaultSettings = new WasmFlags(Array.from(supportedWasmFeatures, (feat) => WasmFeature[feat])); 11 | const defaultSettingsObj = defaultSettings.to_js(); 12 | 13 | window.defaultSettings = defaultSettings; 14 | 15 | function settingsInfoFromType(type) { 16 | if (type === "boolean") { 17 | return { 18 | type: "checkbox" 19 | } 20 | } else if (type in hyperquarkExports) { 21 | return { 22 | type: "radio", 23 | options: Object.keys(hyperquarkExports[type]).filter(key => typeof key === 'string' && !/\d+/.test(key)), 24 | enum_obj: hyperquarkExports[type], 25 | } 26 | } else { 27 | return null; 28 | } 29 | } 30 | 31 | export const settingsInfo = Object.fromEntries(Object.entries(Object.getOwnPropertyDescriptors(WasmFlags.prototype)) 32 | .filter(([_, descriptor]) => typeof descriptor.get === 'function') 33 | .map(([key, _]) => key) 34 | .map(key => { 35 | let flag_info = WasmFlags.flag_info(key); 36 | return [key, { 37 | flag_info, 38 | ...settingsInfoFromType(flag_info.ty) 39 | }] 40 | })); 41 | 42 | /** 43 | * @returns {WasmFlags} 44 | */ 45 | export function getSettings() { 46 | let store = localStorage["settings"]; 47 | try { 48 | return WasmFlags.from_js({ ...defaultSettingsObj, ...JSON.parse(store) }); 49 | } catch { 50 | return defaultSettings; 51 | } 52 | } 53 | 54 | /** 55 | * @param {WasmFlags} settings 56 | */ 57 | export function saveSettings(settings) { 58 | console.log(settings.to_js()) 59 | localStorage['settings'] = JSON.stringify(settings.to_js()); 60 | } 61 | 62 | /** 63 | * @returns {Set} 64 | */ 65 | export async function getSupportedWasmFeatures() { 66 | const featureSet = new Set(); 67 | for (const feature of all_wasm_features()) { 68 | if (await WasmFeatureDetect[wasm_feature_detect_name(feature)]()) { 69 | featureSet.add(WasmFeature[feature]); 70 | } 71 | } 72 | return featureSet; 73 | } 74 | 75 | console.log(await getSupportedWasmFeatures()) 76 | 77 | /** 78 | * @returns {Set} 79 | */ 80 | export function getUsedWasmFeatures() { 81 | const settings = getSettings().to_js(); 82 | console.log(settings) 83 | const featureSet = new Set(); 84 | for (const [id, info] of Object.entries(settingsInfo)) { 85 | const theseFeatures = info.flag_info.wasm_features(settings[id])?.map?.((num) => WasmFeature[num]); 86 | for (const feature of theseFeatures || []) { 87 | featureSet.add(feature); 88 | } 89 | } 90 | return featureSet; 91 | } -------------------------------------------------------------------------------- /src/instructions/operator/subtract.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 2); 5 | let t1 = inputs[0]; 6 | let t2 = inputs[1]; 7 | Ok(if IrType::QuasiInt.contains(t1) { 8 | if IrType::QuasiInt.contains(t2) { 9 | wasm![I32Sub] 10 | } else if IrType::Float.contains(t2) { 11 | let f64_local = func.local(ValType::F64)?; 12 | wasm![ 13 | LocalSet(f64_local), 14 | F64ConvertI32S, 15 | LocalGet(f64_local), 16 | @nanreduce(t2), 17 | F64Sub, 18 | ] 19 | } else { 20 | hq_bug!("bad input") 21 | } 22 | } else if IrType::Float.contains(t1) { 23 | if IrType::Float.contains(t2) { 24 | let f64_local = func.local(ValType::F64)?; 25 | wasm![ 26 | @nanreduce(t2), 27 | LocalSet(f64_local), 28 | @nanreduce(t1), 29 | LocalGet(f64_local), 30 | F64Sub 31 | ] 32 | } else if IrType::QuasiInt.contains(t2) { 33 | let i32_local = func.local(ValType::I32)?; 34 | wasm![ 35 | LocalSet(i32_local), 36 | @nanreduce(t1), 37 | LocalGet(i32_local), 38 | F64ConvertI32S, 39 | F64Sub 40 | ] 41 | } else { 42 | hq_bug!("bad input") 43 | } 44 | } else { 45 | hq_bug!("bad input") 46 | }) 47 | } 48 | 49 | pub fn acceptable_inputs() -> HQResult> { 50 | Ok(Rc::from([IrType::Number, IrType::Number])) 51 | } 52 | 53 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 54 | hq_assert!(inputs.len() == 2); 55 | let t1 = inputs[0]; 56 | let t2 = inputs[1]; 57 | let maybe_positive = t1.maybe_positive() || t2.maybe_negative(); 58 | let maybe_negative = t1.maybe_negative() || t2.maybe_positive(); 59 | let maybe_zero = ((t1.maybe_zero() || t1.maybe_nan()) && (t2.maybe_zero() || t2.maybe_nan())) 60 | || (t1.maybe_positive() && t2.maybe_positive()) 61 | || (t1.maybe_negative() && t2.maybe_negative()); 62 | let maybe_nan = 63 | IrType::FloatPosInf.intersects(t1.and(t2)) || IrType::FloatNegInf.intersects(t1.and(t2)); 64 | Ok(Singleton(if IrType::QuasiInt.contains(t1.or(t2)) { 65 | IrType::none_if_false(maybe_positive, IrType::IntPos) 66 | .or(IrType::none_if_false(maybe_negative, IrType::IntNeg)) 67 | .or(IrType::none_if_false(maybe_zero, IrType::IntZero)) 68 | } else { 69 | IrType::none_if_false(maybe_positive, IrType::FloatPos) 70 | .or(IrType::none_if_false(maybe_negative, IrType::FloatNeg)) 71 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) 72 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) 73 | })) 74 | } 75 | 76 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 77 | 78 | crate::instructions_test! {tests; operator_subtract; t1, t2 ;} 79 | -------------------------------------------------------------------------------- /src/ir/target.rs: -------------------------------------------------------------------------------- 1 | use super::{IrProject, proc::Proc}; 2 | use crate::ir::variable::TargetVars; 3 | use crate::prelude::*; 4 | use crate::sb3::CostumeDataFormat; 5 | use core::cell::{Ref, RefMut}; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq)] 8 | pub struct IrCostume { 9 | pub name: Box, 10 | pub data_format: CostumeDataFormat, 11 | pub md5ext: Box, 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct Target { 16 | is_stage: bool, 17 | variables: TargetVars, 18 | project: Weak, 19 | procedures: RefCell, Rc>>, 20 | index: u32, 21 | costumes: Box<[IrCostume]>, 22 | } 23 | 24 | impl Target { 25 | pub const fn is_stage(&self) -> bool { 26 | self.is_stage 27 | } 28 | 29 | pub const fn variables(&self) -> &TargetVars { 30 | &self.variables 31 | } 32 | 33 | pub fn project(&self) -> Weak { 34 | Weak::clone(&self.project) 35 | } 36 | 37 | pub fn procedures(&self) -> HQResult, Rc>>> { 38 | Ok(self.procedures.try_borrow()?) 39 | } 40 | 41 | pub fn procedures_mut(&self) -> HQResult, Rc>>> { 42 | Ok(self.procedures.try_borrow_mut()?) 43 | } 44 | 45 | pub const fn index(&self) -> u32 { 46 | self.index 47 | } 48 | 49 | pub const fn costumes(&self) -> &[IrCostume] { 50 | &self.costumes 51 | } 52 | 53 | pub const fn new( 54 | is_stage: bool, 55 | variables: TargetVars, 56 | project: Weak, 57 | procedures: RefCell, Rc>>, 58 | index: u32, 59 | costumes: Box<[IrCostume]>, 60 | ) -> Self { 61 | Self { 62 | is_stage, 63 | variables, 64 | project, 65 | procedures, 66 | index, 67 | costumes, 68 | } 69 | } 70 | } 71 | 72 | impl fmt::Display for Target { 73 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 74 | let is_stage = self.is_stage; 75 | let index = self.index; 76 | let variables = self 77 | .variables 78 | .iter() 79 | .map(|(id, var)| format!(r#""{id}": {var}"#)) 80 | .join(", "); 81 | let procedures = self 82 | .procedures 83 | .borrow() 84 | .iter() 85 | .map(|(id, proc)| format!(r#""{id}": {proc}"#)) 86 | .join(", "); 87 | write!( 88 | f, 89 | r#"{{ 90 | "is_stage": {is_stage}, 91 | "index": {index}, 92 | "variables": {{ {variables} }}, 93 | "procedures": {{ {procedures} }}, 94 | }}"# 95 | ) 96 | } 97 | } 98 | 99 | impl core::hash::Hash for Target { 100 | fn hash(&self, state: &mut H) { 101 | self.index().hash(state); 102 | } 103 | } 104 | 105 | impl core::cmp::PartialEq for Target { 106 | fn eq(&self, other: &Self) -> bool { 107 | self.index() == other.index() 108 | } 109 | } 110 | 111 | impl core::cmp::Eq for Target {} 112 | -------------------------------------------------------------------------------- /src/instructions/operator/multiply.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 4 | hq_assert_eq!(inputs.len(), 2); 5 | let t1 = inputs[0]; 6 | let t2 = inputs[1]; 7 | Ok(if IrType::QuasiInt.contains(t1) { 8 | if IrType::QuasiInt.contains(t2) { 9 | wasm![I32Mul] 10 | } else if IrType::Float.contains(t2) { 11 | let f64_local = func.local(ValType::F64)?; 12 | wasm![ 13 | LocalSet(f64_local), 14 | F64ConvertI32S, 15 | LocalGet(f64_local), 16 | @nanreduce(t2), 17 | F64Mul, 18 | ] 19 | } else { 20 | hq_bug!("bad input") 21 | } 22 | } else if IrType::Float.contains(t1) { 23 | if IrType::Float.contains(t2) { 24 | let f64_local = func.local(ValType::F64)?; 25 | wasm![ 26 | @nanreduce(t2), 27 | LocalSet(f64_local), 28 | @nanreduce(t1), 29 | LocalGet(f64_local), 30 | F64Mul 31 | ] 32 | } else if IrType::QuasiInt.contains(t2) { 33 | let i32_local = func.local(ValType::I32)?; 34 | wasm![ 35 | LocalSet(i32_local), 36 | @nanreduce(t1), 37 | LocalGet(i32_local), 38 | F64ConvertI32S, 39 | F64Mul 40 | ] 41 | } else { 42 | hq_bug!("bad input") 43 | } 44 | } else { 45 | hq_bug!("bad input") 46 | }) 47 | } 48 | 49 | pub fn acceptable_inputs() -> HQResult> { 50 | Ok(Rc::from([IrType::Number, IrType::Number])) 51 | } 52 | 53 | pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { 54 | hq_assert_eq!(inputs.len(), 2); 55 | let t1 = inputs[0]; 56 | let t2 = inputs[1]; 57 | let maybe_positive = (t1.maybe_positive() && t2.maybe_positive()) 58 | || (t2.maybe_negative() && t1.maybe_negative()); 59 | let maybe_negative = (t1.maybe_negative() && t2.maybe_positive()) 60 | || (t2.maybe_negative() && t1.maybe_positive()); 61 | let maybe_zero = t1.maybe_zero() || t1.maybe_nan() || t2.maybe_zero() || t2.maybe_nan(); 62 | let maybe_nan = (IrType::FloatInf.intersects(t1) && (t2.maybe_zero() || t2.maybe_nan())) 63 | || (IrType::FloatInf.intersects(t2) && (t1.maybe_zero() || t1.maybe_nan())); 64 | let maybe_inf = IrType::FloatInf.intersects(t1.or(t2)); 65 | Ok(Singleton(if IrType::QuasiInt.contains(t1.or(t2)) { 66 | IrType::none_if_false(maybe_positive, IrType::IntPos) 67 | .or(IrType::none_if_false(maybe_negative, IrType::IntNeg)) 68 | .or(IrType::none_if_false(maybe_zero, IrType::IntZero)) 69 | } else { 70 | IrType::none_if_false(maybe_positive, IrType::FloatPos) 71 | .or(IrType::none_if_false(maybe_negative, IrType::FloatNeg)) 72 | .or(IrType::none_if_false(maybe_zero, IrType::FloatZero)) 73 | .or(IrType::none_if_false(maybe_nan, IrType::FloatNan)) 74 | .or(IrType::none_if_false(maybe_inf, IrType::FloatInf)) 75 | })) 76 | } 77 | 78 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 79 | 80 | crate::instructions_test! {tests; operator_multiply; t1, t2 ;} 81 | -------------------------------------------------------------------------------- /src/wasm/mem_layout.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::allow_attributes_without_reason)] 2 | #![allow(clippy::allow_attributes)] 3 | #![allow(dead_code)] 4 | 5 | macro_rules! size_from_type { 6 | (i8) => {{ 1 }}; 7 | (i16) => {{ 2 }}; 8 | (i32) => {{ 4 }}; 9 | (f32) => {{ 4 }}; 10 | (i64) => {{ 8 }}; 11 | (f64) => {{ 8 }}; 12 | } 13 | 14 | #[macro_export] 15 | macro_rules! memory_layout { 16 | { 17 | $mod:ident 18 | $(#[$attr:meta] $name:ident : $ty:tt)+ 19 | } => { 20 | memory_layout!( 21 | $mod 22 | $(#[$attr] $name : $ty)+ 23 | @ 0; 24 | ); 25 | }; 26 | 27 | { 28 | $mod:ident 29 | #[$attr:meta] $name:ident : $ty:tt 30 | $(#[$attr2:meta] $name2:ident : $ty2:tt)+ 31 | @ $tot:expr; 32 | $(#[$attr3:meta] $name3:ident $tot3:expr;)* 33 | } => { 34 | memory_layout!( 35 | $mod 36 | $(#[$attr2] $name2 : $ty2)+ 37 | @ $tot + size_from_type!($ty); 38 | $(#[$attr3] $name3 $tot3;)* 39 | #[$attr] $name $tot; 40 | ); 41 | }; 42 | ($mod:ident #[$attr:meta] $name:ident : $ty:tt @ $tot:expr; $(#[$attr3:meta] $name3:ident $tot3:expr;)*) => { 43 | pub mod $mod { 44 | #![allow(clippy::empty_docs, reason = "index_counter doesn't need docs")] 45 | 46 | $(#[$attr3] pub const $name3: u32 = $tot3;)* 47 | #[$attr] pub const $name: u32 = $tot; 48 | 49 | pub const BLOCK_SIZE: u32 = $tot + size_from_type!($ty); 50 | } 51 | } 52 | } 53 | 54 | #[macro_export] 55 | macro_rules! index_counter { 56 | { 57 | $mod:ident 58 | $($name:ident)+ 59 | } => { 60 | memory_layout!( 61 | $mod 62 | $(#[doc=""] $name : i8)+ 63 | ); 64 | }; 65 | } 66 | 67 | memory_layout! { 68 | stage 69 | /// Backdrop number of stage (i32) 70 | COSTUME: i32 71 | /// 4-byte adding (so that sprite chunks are aligned to 8 bits) 72 | _PADDING: i32 73 | } 74 | 75 | memory_layout! { 76 | sprite 77 | /// x-position of sprite (f64) 78 | X: f64 79 | /// y-position of sprite (f64) 80 | Y: f64 81 | /// hue component of pen colour (0-100) (f32) 82 | PEN_COLOR: f32 83 | /// saturation component of pen colour (0-100) (f32) 84 | PEN_SATURATION: f32 85 | /// value component of pen colour (0-100) (f32) 86 | PEN_BRIGHTNESS: f32 87 | /// transparency of pen (f32) 88 | PEN_TRANSPARENCY: f32 89 | /// red component of rgba representation of pen colour (0-1) (f32) 90 | PEN_COLOR_R: f32 91 | /// green component of rgba representation of pen colour (0-1) (f32) 92 | PEN_COLOR_G: f32 93 | /// blue component of rgba representation of pen colour (0-1) (f32) 94 | PEN_COLOR_B: f32 95 | /// alpha component of rgba representation of pen colour (0-1) (f32) 96 | PEN_COLOR_A: f32 97 | /// radius of pen (f64) 98 | PEN_SIZE: f64 99 | /// non-zero if pen down, 0 otherwise (i8) 100 | PEN_DOWN: i8 101 | /// non-zero if sprite is visible, 0 otherwise (i8) 102 | VISIBLE: i8 103 | /// bytes 58-59 padding 104 | _PADDING: i16 105 | /// current costume number, 0-indexed (i32) 106 | COSTUME: i32 107 | /// sprite size, where default is 100(%) (f64) 108 | SIZE: f64 109 | /// sprite rotation, in scratch angles (0 = up, 90 = right) (f64) 110 | ROTATION: f64 111 | } 112 | -------------------------------------------------------------------------------- /.github/workflows/ci-checks.yaml: -------------------------------------------------------------------------------- 1 | on: [push, workflow_dispatch, pull_request] 2 | 3 | name: CI checks 4 | 5 | jobs: 6 | build: 7 | name: Build check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v2 12 | 13 | - name: Install nightly toolchain 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: nightly 18 | override: true 19 | target: wasm32-unknown-unknown 20 | 21 | - name: Run cargo check 22 | uses: actions-rs/cargo@v1 23 | with: 24 | command: check 25 | args: --target=wasm32-unknown-unknown 26 | 27 | clippy: 28 | name: Lint (clippy) 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout sources 32 | uses: actions/checkout@v4 33 | 34 | - name: Install nightly toolchain with clippy available 35 | uses: actions-rs/toolchain@v1 36 | with: 37 | profile: minimal 38 | toolchain: nightly 39 | override: true 40 | components: clippy 41 | 42 | - name: Run cargo clippy 43 | uses: actions-rs/cargo@v1 44 | with: 45 | command: clippy 46 | args: --all-targets -- -D warnings 47 | 48 | rustfmt: 49 | name: Format 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout sources 53 | uses: actions/checkout@v4 54 | 55 | - name: Install nightly toolchain with rustfmt available 56 | uses: actions-rs/toolchain@v1 57 | with: 58 | profile: minimal 59 | toolchain: nightly 60 | override: true 61 | components: rustfmt 62 | 63 | - name: Run cargo fmt 64 | uses: actions-rs/cargo@v1 65 | with: 66 | command: fmt 67 | args: --all -- --check 68 | 69 | test: 70 | name: Run unit tests 71 | runs-on: ubuntu-latest 72 | steps: 73 | - name: Checkout sources 74 | uses: actions/checkout@v2 75 | 76 | - name: Install nightly toolchain 77 | uses: actions-rs/toolchain@v1 78 | with: 79 | profile: minimal 80 | toolchain: nightly 81 | override: true 82 | 83 | - name: Run cargo test 84 | uses: actions-rs/cargo@v1 85 | with: 86 | command: test 87 | args: >- 88 | -- 89 | --skip cast::float::js_functions_match_declared_types 90 | --skip cast::int::js_functions_match_declared_types 91 | --skip cast::string::js_functions_match_declared_types 92 | --skip join::tests::js_functions_match_declared_types 93 | --skip lt::tests::js_functions_match_declared_types 94 | --skip gt::tests::js_functions_match_declared_types 95 | --skip equals::tests::js_functions_match_declared_types 96 | --skip length::tests::js_functions_match_declared_types 97 | --skip letter_of::tests::js_functions_match_declared_types 98 | --skip contains::tests::js_functions_match_declared_types 99 | --skip dayssince2000::tests::js_functions_match_declared_types 100 | --skip looks::say::tests_debug::js_functions_match_declared_types 101 | --skip looks::say::tests_non_debug::js_functions_match_declared_types 102 | --skip looks::think::tests_debug::js_functions_match_declared_types 103 | --skip looks::think::tests_non_debug::js_functions_match_declared_types 104 | --skip js_functions 105 | 106 | -------------------------------------------------------------------------------- /playground/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 77 | 78 | -------------------------------------------------------------------------------- /src/rc.rs: -------------------------------------------------------------------------------- 1 | use alloc::rc::{Rc as CoreRc, Weak as CoreWeak}; 2 | use core::cmp::{Eq, PartialEq}; 3 | use core::fmt::{Debug, Display}; 4 | use core::hash::Hash; 5 | use core::iter::FromIterator; 6 | use core::ops::Deref; 7 | 8 | pub struct Rc(CoreRc) 9 | where 10 | T: ?Sized; 11 | 12 | impl Rc { 13 | pub fn new(value: T) -> Self { 14 | Self(CoreRc::new(value)) 15 | } 16 | 17 | #[must_use] 18 | pub fn downgrade(this: &Self) -> Weak { 19 | Weak(CoreRc::downgrade(&this.0)) 20 | } 21 | 22 | #[must_use] 23 | pub fn as_ptr(this: &Self) -> *const T { 24 | CoreRc::as_ptr(&this.0) 25 | } 26 | 27 | #[must_use] 28 | pub fn ptr_eq(this: &Self, other: &Self) -> bool { 29 | CoreRc::ptr_eq(&this.0, &other.0) 30 | } 31 | } 32 | 33 | impl AsRef for Rc { 34 | fn as_ref(&self) -> &T { 35 | self.0.as_ref() 36 | } 37 | } 38 | 39 | impl FromIterator for Rc<[T]> { 40 | fn from_iter>(iter: I) -> Self { 41 | Self(CoreRc::from_iter(iter)) 42 | } 43 | } 44 | 45 | impl Clone for Rc 46 | where 47 | T: ?Sized, 48 | { 49 | fn clone(&self) -> Self { 50 | Self(CoreRc::clone(&self.0)) 51 | } 52 | } 53 | 54 | impl Deref for Rc 55 | where 56 | T: ?Sized, 57 | { 58 | type Target = T; 59 | fn deref(&self) -> &Self::Target { 60 | &self.0 61 | } 62 | } 63 | 64 | impl Default for Rc 65 | where 66 | T: Default, 67 | { 68 | fn default() -> Self { 69 | Self(CoreRc::default()) 70 | } 71 | } 72 | 73 | impl Debug for Rc 74 | where 75 | T: Debug + ?Sized, 76 | { 77 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 78 | Debug::fmt(&self.0, f) 79 | } 80 | } 81 | 82 | impl Display for Rc 83 | where 84 | T: Display + ?Sized, 85 | { 86 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 87 | Display::fmt(&self.0, f) 88 | } 89 | } 90 | 91 | impl PartialEq for Rc 92 | where 93 | T: PartialEq + ?Sized, 94 | { 95 | fn eq(&self, other: &Self) -> bool { 96 | PartialEq::eq(&self.0, &other.0) 97 | } 98 | } 99 | 100 | impl PartialOrd for Rc 101 | where 102 | T: PartialOrd + ?Sized, 103 | { 104 | fn partial_cmp(&self, other: &Self) -> Option { 105 | PartialOrd::partial_cmp(&self.0, &other.0) 106 | } 107 | } 108 | 109 | impl Ord for Rc 110 | where 111 | T: Ord, 112 | { 113 | fn cmp(&self, other: &Self) -> core::cmp::Ordering { 114 | Ord::cmp(&self.0, &other.0) 115 | } 116 | } 117 | 118 | impl Eq for Rc where T: Eq {} 119 | 120 | impl From for Rc 121 | where 122 | CoreRc: From, 123 | T: ?Sized, 124 | { 125 | fn from(value: U) -> Self { 126 | Self(value.into()) 127 | } 128 | } 129 | 130 | impl Hash for Rc 131 | where 132 | T: Hash + ?Sized, 133 | { 134 | fn hash(&self, state: &mut H) { 135 | Hash::hash(&self.0, state); 136 | } 137 | } 138 | 139 | pub struct Weak(CoreWeak) 140 | where 141 | T: ?Sized; 142 | 143 | impl Weak { 144 | #[must_use] 145 | pub const fn new() -> Self { 146 | Self(CoreWeak::new()) 147 | } 148 | 149 | #[must_use] 150 | pub fn upgrade(&self) -> Option> { 151 | self.0.upgrade().map(|rc| Rc(rc)) 152 | } 153 | } 154 | 155 | impl Clone for Weak 156 | where 157 | T: ?Sized, 158 | { 159 | fn clone(&self) -> Self { 160 | Self(CoreWeak::clone(&self.0)) 161 | } 162 | } 163 | 164 | impl Debug for Weak 165 | where 166 | T: Debug + ?Sized, 167 | { 168 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 169 | Debug::fmt(&self.0, f) 170 | } 171 | } 172 | 173 | impl Default for Weak { 174 | fn default() -> Self { 175 | Self::new() 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/instructions/pen/setpencolortocolor.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::wasm::{StepTarget, mem_layout, registries}; 3 | use registries::functions::static_functions::UpdatePenColorFromRGB; 4 | use wasm_encoder::{BlockType, MemArg}; 5 | 6 | pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { 7 | let t1 = inputs[0]; 8 | let StepTarget::Sprite(wasm_target_index) = func.target() else { 9 | hq_bad_proj!("looks_setsizeto called in stage") 10 | }; 11 | let mem_pos = 12 | mem_layout::stage::BLOCK_SIZE + wasm_target_index * mem_layout::sprite::BLOCK_SIZE; 13 | let local_index = func.local(ValType::I32)?; 14 | let rgb2hsv_func = func 15 | .registries() 16 | .static_functions() 17 | .register::()?; 18 | Ok(wasm![LocalSet(local_index), I32Const(0),] 19 | .into_iter() 20 | .chain(match t1 { 21 | IrType::ColorARGB => { 22 | let temp_local = func.local(ValType::I32)?; 23 | wasm![ 24 | LocalGet(local_index), 25 | I32Const(24), 26 | I32ShrS, 27 | I32Const(0xFF), 28 | I32And, 29 | LocalTee(temp_local), 30 | If(BlockType::Result(ValType::F32)), 31 | LocalGet(temp_local), 32 | F32ConvertI32S, 33 | F32Const(255.0), 34 | F32Div, 35 | Else, 36 | // scratch doesn't allow totally transparent alpha values for rgb colours - see 37 | // https://github.com/scratchfoundation/scratch-vm/blob/b3266a0cfe5122f20b72ccd738a3dd4dff4fc5a5/src/util/color.js#L50 38 | F32Const(1.0), 39 | End, 40 | ] 41 | } 42 | IrType::ColorRGB => wasm![F32Const(1.0)], 43 | _ => hq_bug!("bad input type to pen_setPenColorToColor"), 44 | }) 45 | .chain(wasm![ 46 | 47 | F32Store(MemArg { 48 | offset: (mem_pos + mem_layout::sprite::PEN_COLOR_A).into(), 49 | align: 2, 50 | memory_index: 0 51 | }), 52 | I32Const(0), 53 | LocalGet(local_index), 54 | I32Const(16), 55 | I32ShrS, 56 | I32Const(0xFF), 57 | I32And, 58 | F32ConvertI32S, 59 | F32Const(255.0), 60 | F32Div, 61 | F32Store(MemArg { 62 | offset: (mem_pos + mem_layout::sprite::PEN_COLOR_R).into(), 63 | align: 2, 64 | memory_index: 0 65 | }), 66 | I32Const(0), 67 | LocalGet(local_index), 68 | I32Const(8), 69 | I32ShrS, 70 | I32Const(0xFF), 71 | I32And, 72 | F32ConvertI32S, 73 | F32Const(255.0), 74 | F32Div, 75 | F32Store(MemArg { 76 | offset: (mem_pos + mem_layout::sprite::PEN_COLOR_G).into(), 77 | align: 2, 78 | memory_index: 0 79 | }), 80 | I32Const(0), 81 | LocalGet(local_index), 82 | I32Const(0xFF), 83 | I32And, 84 | F32ConvertI32S, 85 | F32Const(255.0), 86 | F32Div, 87 | F32Store(MemArg { 88 | offset: (mem_pos + mem_layout::sprite::PEN_COLOR_B).into(), 89 | align: 2, 90 | memory_index: 0 91 | }), 92 | I32Const( 93 | wasm_target_index 94 | .try_into() 95 | .map_err(|_| make_hq_bug!("target index out of bounds"))? 96 | ), 97 | #StaticFunctionCall(rgb2hsv_func), 98 | ]) 99 | .collect()) 100 | } 101 | 102 | pub fn acceptable_inputs() -> HQResult> { 103 | Ok(Rc::from([IrType::Color])) 104 | } 105 | 106 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 107 | Ok(ReturnType::None) 108 | } 109 | 110 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 111 | 112 | crate::instructions_test! {tests; pen_setpencolortocolor; t ; } 113 | -------------------------------------------------------------------------------- /src/wasm/registries/tables.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use wasm_encoder::{ 3 | ConstExpr, ExportKind, ExportSection, HeapType, RefType, TableSection, TableType, 4 | }; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct TableOptions { 8 | pub element_type: RefType, 9 | pub min: u64, 10 | pub max: Option, 11 | pub init: Option, 12 | pub export_name: Option<&'static str>, 13 | } 14 | 15 | pub struct TableRegistrar; 16 | impl RegistryType for TableRegistrar { 17 | type Key = Box; 18 | type Value = TableOptions; 19 | } 20 | 21 | pub type TableRegistry = NamedRegistry; 22 | 23 | impl TableRegistry { 24 | pub fn finish(self, tables: &mut TableSection, exports: &mut ExportSection) { 25 | for ( 26 | _key, 27 | TableOptions { 28 | element_type, 29 | min, 30 | max, 31 | init: maybe_init, 32 | export_name, 33 | }, 34 | ) in self.registry().take() 35 | { 36 | // TODO: allow choosing whether to export a table or not? 37 | if let Some(export_key) = export_name { 38 | exports.export(export_key, ExportKind::Table, tables.len()); 39 | } 40 | // let maybe_init = match &*key { 41 | // "threads" => Some(ConstExpr::ref_func(imports.len())), 42 | // _ => init, 43 | // }; 44 | if let Some(init) = maybe_init { 45 | tables.table_with_init( 46 | TableType { 47 | element_type, 48 | minimum: min, 49 | maximum: max, 50 | table64: false, 51 | shared: false, 52 | }, 53 | &init, 54 | ); 55 | } else { 56 | tables.table(TableType { 57 | element_type, 58 | minimum: min, 59 | maximum: max, 60 | table64: false, 61 | shared: false, 62 | }); 63 | } 64 | } 65 | } 66 | } 67 | 68 | pub struct StringsTable; 69 | impl NamedRegistryItem for StringsTable { 70 | const VALUE: TableOptions = TableOptions { 71 | element_type: RefType::EXTERNREF, 72 | min: 0, 73 | // TODO: use js string imports for preknown strings 74 | max: None, 75 | init: None, 76 | export_name: None, 77 | }; 78 | } 79 | 80 | pub struct StepsTable; 81 | impl NamedRegistryItem for StepsTable { 82 | const VALUE: TableOptions = TableOptions { 83 | element_type: RefType::FUNCREF, 84 | min: 0, 85 | max: None, 86 | init: None, 87 | export_name: None, 88 | }; 89 | } 90 | impl NamedRegistryItemOverride for StepsTable { 91 | fn r#override(step_count: u64) -> TableOptions { 92 | TableOptions { 93 | element_type: RefType::FUNCREF, 94 | min: step_count, 95 | max: Some(step_count), 96 | init: None, 97 | export_name: None, 98 | } 99 | } 100 | } 101 | 102 | pub struct ThreadsTable; 103 | impl NamedRegistryItem for ThreadsTable { 104 | const VALUE: TableOptions = TableOptions { 105 | element_type: RefType::FUNCREF, 106 | min: 0, 107 | max: None, 108 | // default to noop, just so the module validates. 109 | init: None, 110 | export_name: Some("threads"), 111 | }; 112 | } 113 | impl NamedRegistryItemOverride for ThreadsTable { 114 | fn r#override( 115 | (step_func_ty, imported_func_count, static_func_count): (u32, u32, u32), 116 | ) -> TableOptions { 117 | TableOptions { 118 | element_type: RefType { 119 | nullable: false, 120 | heap_type: HeapType::Concrete(step_func_ty), 121 | }, 122 | min: 0, 123 | max: None, 124 | init: Some(ConstExpr::ref_func(imported_func_count + static_func_count)), 125 | export_name: Some("threads"), 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/ir/variable.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | 3 | use super::Type; 4 | use crate::{ 5 | prelude::*, 6 | sb3::{Target as Sb3Target, VarVal}, 7 | }; 8 | use core::cell::Ref; 9 | use core::hash::{Hash, Hasher}; 10 | 11 | #[derive(Debug)] 12 | struct Variable { 13 | possible_types: RefCell, 14 | initial_value: VarVal, 15 | id: String, 16 | } 17 | 18 | #[derive(Clone, Debug)] 19 | pub struct RcVar(Rc); 20 | 21 | impl RcVar { 22 | #[must_use] 23 | pub fn new(ty: Type, initial_value: VarVal) -> Self { 24 | Self(Rc::new(Variable { 25 | possible_types: RefCell::new(ty), 26 | initial_value, 27 | id: Uuid::new_v4().to_string(), 28 | })) 29 | } 30 | 31 | pub fn add_type(&self, ty: Type) { 32 | let current = *self.0.possible_types.borrow(); 33 | *self.0.possible_types.borrow_mut() = current.or(ty); 34 | } 35 | 36 | #[must_use] 37 | pub fn possible_types(&self) -> Ref<'_, Type> { 38 | self.0.possible_types.borrow() 39 | } 40 | 41 | #[must_use] 42 | pub fn _initial_value(&self) -> &VarVal { 43 | &self.0.initial_value 44 | } 45 | 46 | #[must_use] 47 | pub fn id(&self) -> &str { 48 | &self.0.id 49 | } 50 | } 51 | 52 | impl PartialEq for RcVar { 53 | fn eq(&self, other: &Self) -> bool { 54 | self.0.id == other.0.id 55 | // Rc::ptr_eq(self.0.get_ref(), other.0.get_ref()) 56 | } 57 | } 58 | 59 | impl Eq for RcVar {} 60 | 61 | impl PartialOrd for RcVar { 62 | fn partial_cmp(&self, other: &Self) -> Option { 63 | Some(self.cmp(other)) 64 | } 65 | } 66 | 67 | impl Ord for RcVar { 68 | fn cmp(&self, other: &Self) -> core::cmp::Ordering { 69 | self.0.id.cmp(&other.0.id) 70 | //Rc::as_ptr(&self.0).cmp(&Rc::as_ptr(&other.0)) 71 | } 72 | } 73 | 74 | impl Hash for RcVar { 75 | fn hash(&self, state: &mut H) { 76 | self.0.id.hash(state); //core::ptr::hash(Rc::as_ptr(&self.0), state); 77 | } 78 | } 79 | 80 | #[derive(Debug)] 81 | pub struct TargetVar { 82 | pub var: RcVar, 83 | /// this MUST not be modified once the `IrProject` is emitted, i.e. once optimisation has begun 84 | pub is_used: RefCell, 85 | } 86 | 87 | impl fmt::Display for TargetVar { 88 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 89 | write!( 90 | f, 91 | r#"{{ "var": {}, "is_used": {} }}"#, 92 | self.var, 93 | *self.is_used.borrow() 94 | ) 95 | } 96 | } 97 | 98 | pub type TargetVars = BTreeMap, Rc>; 99 | 100 | pub fn variables_from_target(target: &Sb3Target) -> TargetVars { 101 | target 102 | .variables 103 | .iter() 104 | .map(|(id, var_info)| { 105 | ( 106 | id.clone(), 107 | Rc::new(TargetVar { 108 | var: RcVar::new( 109 | Type::none(), 110 | #[expect( 111 | clippy::unwrap_used, 112 | reason = "this field exists on all variants" 113 | )] 114 | var_info.get_1().unwrap().clone(), 115 | ), 116 | is_used: RefCell::new(false), 117 | }), 118 | ) 119 | }) 120 | .collect() 121 | } 122 | 123 | /// A list of variables in a target that are used somewhere (whether read or written to) 124 | #[must_use] 125 | pub fn used_vars(vars: &TargetVars) -> Box<[RcVar]> { 126 | vars.values() 127 | .filter_map(|var| { 128 | if *var.is_used.borrow() { 129 | Some(var.var.clone()) 130 | } else { 131 | None 132 | } 133 | }) 134 | .collect() 135 | } 136 | 137 | impl fmt::Display for RcVar { 138 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 139 | let possible_types = self.0.possible_types.borrow(); 140 | //let id = Rc::as_ptr(&self.0) as usize; 141 | let id = self.id(); 142 | write!( 143 | f, 144 | r#"{{ 145 | "possible_types": "{possible_types}", 146 | "id": {id:?} 147 | }}"# 148 | ) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/instructions.rs: -------------------------------------------------------------------------------- 1 | //! Contains information about instructions (roughly anaologus to blocks), 2 | //! including input type validation, output type mapping, and WASM generation. 3 | 4 | #![allow( 5 | clippy::unnecessary_wraps, 6 | reason = "many functions here needlessly return `Result`s in order to keep type signatures consistent" 7 | )] 8 | #![allow( 9 | clippy::needless_pass_by_value, 10 | reason = "there are so many `Rc`s here which I don't want to change" 11 | )] 12 | 13 | use crate::prelude::*; 14 | 15 | mod control; 16 | mod data; 17 | mod hq; 18 | mod looks; 19 | mod motion; 20 | mod operator; 21 | mod pen; 22 | mod procedures; 23 | mod sensing; 24 | 25 | #[macro_use] 26 | mod tests; 27 | 28 | fn boxed_output_type( 29 | // we provide a function so this can be used by tests without access to the whole IrOpcode enum stuff 30 | outputs_func: F, 31 | inputs: Rc<[crate::ir::Type]>, 32 | ) -> HQResult 33 | where 34 | F: Fn(Rc<[crate::ir::Type]>) -> HQResult, 35 | { 36 | use crate::ir::ReturnType; 37 | if inputs.is_empty() { 38 | let out = outputs_func(Rc::from([]))?; 39 | // crate::log!("{out:?}"); 40 | Ok(out) 41 | } else { 42 | // crate::log!("{inputs:?}"); 43 | if inputs.iter().any(crate::ir::Type::is_none) { 44 | hq_bug!("got none input type :scream:") 45 | } 46 | let bases = crate::ir::base_types(&inputs)?; 47 | let mapped = bases 48 | .iter() 49 | .enumerate() 50 | .map(|(i, tys)| { 51 | let input = inputs[i]; 52 | tys.iter().map(move |ty| ty.and(input)) 53 | }) 54 | .collect::>(); 55 | // crate::log!("{mapped:?}"); 56 | let inins = mapped 57 | .iter() 58 | .cloned() 59 | .multi_cartesian_product() 60 | .map(|ins| outputs_func(ins.into_iter().collect())) 61 | .collect::>(); 62 | // crate::log!("{:?}", inins); 63 | inins 64 | .iter() 65 | .cloned() 66 | .try_reduce(|acc, el| { 67 | #[expect(clippy::redundant_clone, reason = "false positives")] 68 | Ok(match acc? { 69 | ReturnType::None => { 70 | hq_assert!(matches!(el.clone()?, ReturnType::None)); 71 | Ok(ReturnType::None) 72 | } 73 | ReturnType::Singleton(ty) => { 74 | if let ReturnType::Singleton(ty2) = el.clone()? { 75 | Ok(ReturnType::Singleton(ty.or(ty2))) 76 | } else { 77 | hq_bug!("") 78 | } 79 | } 80 | ReturnType::MultiValue(tys) => { 81 | let ReturnType::MultiValue(tys2) = el.clone()? else { 82 | hq_bug!("") 83 | }; 84 | hq_assert_eq!(tys.len(), tys2.len()); 85 | Ok(ReturnType::MultiValue( 86 | tys.iter() 87 | .zip(tys2.iter()) 88 | .map(|(ty1, ty2)| ty1.or(*ty2)) 89 | .collect(), 90 | )) 91 | } 92 | }) 93 | })? 94 | .ok_or_else(|| make_hq_bug!(""))? 95 | } 96 | } 97 | 98 | include!(concat!(env!("OUT_DIR"), "/ir-opcodes.rs")); 99 | 100 | pub mod input_switcher; 101 | pub use input_switcher::wrap_instruction; 102 | 103 | pub use hq::r#yield::YieldMode; 104 | 105 | mod prelude { 106 | pub use crate::ir::{ReturnType, Type as IrType}; 107 | pub use crate::prelude::*; 108 | pub use crate::wasm::{InternalInstruction, StepFunc}; 109 | pub use ReturnType::{MultiValue, Singleton}; 110 | pub use wasm_encoder::{RefType, ValType}; 111 | pub use wasm_gen::wasm; 112 | 113 | /// Canonical NaN + bit 33, + string pointer in bits 1-32 114 | pub const BOXED_STRING_PATTERN: i64 = 0x7FF8_0001 << 32; 115 | /// Canonical NaN + bit 34, + i32 in bits 1-32 116 | pub const BOXED_INT_PATTERN: i64 = 0x7ff8_0002 << 32; 117 | /// Canonical NaN + bit 35, + i32 in bits 1-32 118 | pub const BOXED_COLOR_RGB_PATTERN: i64 = 0x7ff8_0004 << 32; 119 | /// Canonical NaN + bit 36, + i32 in bits 1-32 120 | pub const BOXED_COLOR_ARGB_PATTERN: i64 = 0x7ff8_0008 << 32; 121 | } 122 | -------------------------------------------------------------------------------- /src/instructions/pen/setpencolorparamto.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::instructions::{HqTextFields, IrOpcode}; 3 | use crate::wasm::{StepTarget, mem_layout, registries}; 4 | use registries::functions::static_functions::UpdatePenColorFromHSV; 5 | use wasm_encoder::{BlockType as WasmBlockType, MemArg}; 6 | 7 | pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 8 | let StepTarget::Sprite(wasm_target_index) = func.target() else { 9 | hq_bad_proj!("looks_setsizeto called in stage") 10 | }; 11 | let mem_pos = 12 | mem_layout::stage::BLOCK_SIZE + wasm_target_index * mem_layout::sprite::BLOCK_SIZE; 13 | let hsv2rgb_func = func 14 | .registries() 15 | .static_functions() 16 | .register::()?; 17 | let param_local = func.local(ValType::EXTERNREF)?; 18 | let value_local = func.local(ValType::F32)?; 19 | Ok( 20 | wasm![F32DemoteF64, LocalSet(value_local), LocalTee(param_local),] 21 | .into_iter() 22 | .chain(IrOpcode::hq_text(HqTextFields("color".into())).wasm(func, Rc::from([]))?) 23 | .chain( 24 | IrOpcode::operator_equals 25 | .wasm(func, Rc::from([IrType::String, IrType::StringNan]))?, 26 | ) 27 | .chain(wasm![ 28 | If(WasmBlockType::Empty), 29 | I32Const(0), 30 | LocalGet(value_local), 31 | F32Store(MemArg { 32 | offset: (mem_pos + mem_layout::sprite::PEN_COLOR).into(), 33 | align: 2, 34 | memory_index: 0 35 | }), 36 | Else, 37 | LocalGet(param_local), 38 | ]) 39 | .chain(IrOpcode::hq_text(HqTextFields("saturation".into())).wasm(func, Rc::from([]))?) 40 | .chain( 41 | IrOpcode::operator_equals 42 | .wasm(func, Rc::from([IrType::String, IrType::StringNan]))?, 43 | ) 44 | .chain(wasm![ 45 | If(WasmBlockType::Empty), 46 | I32Const(0), 47 | LocalGet(value_local), 48 | F32Store(MemArg { 49 | offset: (mem_pos + mem_layout::sprite::PEN_SATURATION).into(), 50 | align: 2, 51 | memory_index: 0 52 | }), 53 | Else, 54 | LocalGet(param_local), 55 | ]) 56 | .chain(IrOpcode::hq_text(HqTextFields("brightness".into())).wasm(func, Rc::from([]))?) 57 | .chain( 58 | IrOpcode::operator_equals 59 | .wasm(func, Rc::from([IrType::String, IrType::StringNan]))?, 60 | ) 61 | .chain(wasm![ 62 | If(WasmBlockType::Empty), 63 | I32Const(0), 64 | LocalGet(value_local), 65 | F32Store(MemArg { 66 | offset: (mem_pos + mem_layout::sprite::PEN_BRIGHTNESS).into(), 67 | align: 2, 68 | memory_index: 0 69 | }), 70 | Else, 71 | LocalGet(param_local), 72 | ]) 73 | .chain(IrOpcode::hq_text(HqTextFields("transparency".into())).wasm(func, Rc::from([]))?) 74 | .chain( 75 | IrOpcode::operator_equals 76 | .wasm(func, Rc::from([IrType::String, IrType::StringNan]))?, 77 | ) 78 | .chain(wasm![ 79 | If(WasmBlockType::Empty), 80 | I32Const(0), 81 | LocalGet(value_local), 82 | F32Store(MemArg { 83 | offset: (mem_pos + mem_layout::sprite::PEN_TRANSPARENCY).into(), 84 | align: 2, 85 | memory_index: 0 86 | }), 87 | End, 88 | End, 89 | End, 90 | End, 91 | I32Const( 92 | wasm_target_index 93 | .try_into() 94 | .map_err(|_| make_hq_bug!("target index out of bounds"))? 95 | ), 96 | #StaticFunctionCall(hsv2rgb_func), 97 | ]) 98 | .collect(), 99 | ) 100 | } 101 | 102 | pub fn acceptable_inputs() -> HQResult> { 103 | Ok(Rc::from([IrType::String, IrType::Float])) 104 | } 105 | 106 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 107 | Ok(ReturnType::None) 108 | } 109 | 110 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 111 | 112 | crate::instructions_test! {tests_colour; pen_setpencolorparamto; t1, t2 } 113 | -------------------------------------------------------------------------------- /src/instructions/data/variable.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::ir::RcVar; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct Fields { 6 | pub var: RefCell, 7 | pub local_read: RefCell, 8 | } 9 | 10 | impl fmt::Display for Fields { 11 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 12 | write!( 13 | f, 14 | r#"{{ 15 | "variable": {}, 16 | "local_read": {} 17 | }}"#, 18 | self.var.borrow(), 19 | self.local_read.borrow() 20 | ) 21 | } 22 | } 23 | 24 | pub fn wasm( 25 | func: &StepFunc, 26 | _inputs: Rc<[IrType]>, 27 | Fields { var, local_read }: &Fields, 28 | ) -> HQResult> { 29 | if *local_read.try_borrow()? { 30 | let local_index: u32 = func.local_variable(&*var.try_borrow()?)?; 31 | Ok(wasm![LocalGet(local_index)]) 32 | } else { 33 | let global_index: u32 = func 34 | .registries() 35 | .variables() 36 | .register(&*var.try_borrow()?)?; 37 | Ok(wasm![#LazyGlobalGet(global_index)]) 38 | } 39 | } 40 | 41 | pub fn acceptable_inputs(_fields: &Fields) -> HQResult> { 42 | Ok(Rc::from([])) 43 | } 44 | 45 | pub fn output_type(_inputs: Rc<[IrType]>, Fields { var, .. }: &Fields) -> HQResult { 46 | Ok(Singleton(if var.borrow().possible_types().is_none() { 47 | IrType::Any 48 | } else { 49 | *var.borrow().possible_types() 50 | })) 51 | } 52 | 53 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 54 | 55 | crate::instructions_test!( 56 | any_global; 57 | data_variable; 58 | @ super::Fields { 59 | var: RefCell::new 60 | ( 61 | crate::ir::RcVar::new( 62 | IrType::Any, 63 | crate::sb3::VarVal::Float(0.0), 64 | 65 | ) 66 | ) 67 | , 68 | local_read: RefCell::new(false) 69 | } 70 | ); 71 | 72 | crate::instructions_test!( 73 | float_global; 74 | data_variable; 75 | @ super::Fields { 76 | var: RefCell::new( 77 | crate::ir::RcVar::new( 78 | IrType::Float, 79 | crate::sb3::VarVal::Float(0.0), 80 | ) 81 | ), 82 | local_read: RefCell::new(false) 83 | } 84 | ); 85 | 86 | crate::instructions_test!( 87 | string_global; 88 | data_variable; 89 | @ super::Fields { 90 | var: RefCell::new 91 | ( 92 | crate::ir::RcVar::new( 93 | IrType::String, 94 | crate::sb3::VarVal::String("".into()), 95 | 96 | ) 97 | ) 98 | , 99 | local_read: RefCell::new(false) 100 | } 101 | ); 102 | 103 | crate::instructions_test!( 104 | int_global; 105 | data_variable; 106 | @ super::Fields { 107 | var: RefCell::new 108 | ( 109 | crate::ir::RcVar::new( 110 | IrType::QuasiInt, 111 | crate::sb3::VarVal::Bool(true), 112 | 113 | ) 114 | ) 115 | , 116 | local_read: RefCell::new(false) 117 | } 118 | ); 119 | 120 | crate::instructions_test!( 121 | any_local; 122 | data_variable; 123 | @ super::Fields { 124 | var: RefCell::new 125 | ( 126 | crate::ir::RcVar::new( 127 | IrType::Any, 128 | crate::sb3::VarVal::Float(0.0), 129 | 130 | ) 131 | ) 132 | , 133 | local_read: RefCell::new(true) 134 | } 135 | ); 136 | 137 | crate::instructions_test!( 138 | float_local; 139 | data_variable; 140 | @ super::Fields { 141 | var: RefCell::new 142 | ( 143 | crate::ir::RcVar::new( 144 | IrType::Float, 145 | crate::sb3::VarVal::Float(0.0), 146 | 147 | ) 148 | ) 149 | , 150 | local_read: RefCell::new(true) 151 | } 152 | ); 153 | 154 | crate::instructions_test!( 155 | string_local; 156 | data_variable; 157 | @ super::Fields { 158 | var: RefCell::new( 159 | 160 | crate::ir::RcVar::new( 161 | IrType::String, 162 | crate::sb3::VarVal::String("".into()), 163 | 164 | ) 165 | 166 | ), 167 | local_read: RefCell::new(true) 168 | } 169 | ); 170 | 171 | crate::instructions_test!( 172 | int_local; 173 | data_variable; 174 | @ super::Fields { 175 | var: RefCell::new( 176 | crate::ir::RcVar::new( 177 | IrType::QuasiInt, 178 | crate::sb3::VarVal::Bool(true), 179 | 180 | ) 181 | ), 182 | local_read: RefCell::new(true) 183 | } 184 | ); 185 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(stmt_expr_attributes)] // used in error.rs for panic mode 2 | #![feature(if_let_guard)] 3 | #![feature(associated_type_defaults)] // used in registry.rs for default key type for NamedRegistry 4 | #![feature(box_patterns)] 5 | #![feature(iterator_try_reduce)] // used in instructions/input_switcher.rs for building return type 6 | #![feature(try_find)] // used in ir/proc.rs for finding prototype/def blocks 7 | #![doc(html_logo_url = "https://hyperquark.github.io/hyperquark/logo.png")] 8 | #![doc(html_favicon_url = "https://hyperquark.github.io/hyperquark/favicon.ico")] 9 | #![warn(clippy::cargo, clippy::nursery, clippy::pedantic)] 10 | #![allow( 11 | clippy::non_std_lazy_statics, 12 | reason = "bug in clippy (https://github.com/rust-lang/rust-clippy/issues/14729)" 13 | )] 14 | #![allow( 15 | clippy::missing_errors_doc, 16 | reason = "Too many Results everywhere to document every possible error case. Errors should be self-descriptive and user readable anyway." 17 | )] 18 | #![allow(clippy::too_many_arguments, reason = "unavoidable at this stage")] 19 | #![allow( 20 | clippy::trivially_copy_pass_by_ref, 21 | reason = "too many false positives on WasmFlags, which will grow in future" 22 | )] 23 | #![allow(clippy::missing_panics_doc, reason = "too many false positives")] 24 | #![allow( 25 | clippy::multiple_crate_versions, 26 | reason = "difficult to fix w.r.t. dependencies" 27 | )] 28 | #![deny(clippy::allow_attributes, clippy::allow_attributes_without_reason)] 29 | #![warn( 30 | clippy::alloc_instead_of_core, 31 | clippy::clone_on_ref_ptr, 32 | clippy::dbg_macro, 33 | clippy::expect_used, 34 | clippy::get_unwrap, 35 | clippy::missing_asserts_for_indexing, 36 | clippy::panic, 37 | clippy::rc_buffer, 38 | clippy::redundant_type_annotations, 39 | clippy::shadow_reuse, 40 | clippy::std_instead_of_alloc, 41 | clippy::std_instead_of_core, 42 | clippy::unwrap_used, 43 | clippy::wildcard_enum_match_arm 44 | )] 45 | 46 | #[macro_use] 47 | extern crate alloc; 48 | extern crate enum_field_getter; 49 | 50 | use wasm_bindgen::prelude::*; 51 | 52 | #[macro_use] 53 | pub mod error; 54 | pub mod ir; 55 | pub mod optimisation; 56 | // pub mod ir_opt; 57 | pub mod sb3; 58 | pub mod wasm; 59 | #[macro_use] 60 | pub mod instructions; 61 | 62 | #[doc(inline)] 63 | pub use error::{HQError, HQErrorType, HQResult}; 64 | 65 | pub mod registry; 66 | 67 | pub mod rc; 68 | 69 | /// commonly used _things_ which would be nice not to have to type out every time 70 | pub mod prelude { 71 | pub use crate::registry::{ 72 | NamedRegistrar, NamedRegistry, NamedRegistryItem, NamedRegistryItemOverride, Registry, 73 | RegistryDefault, RegistryType, 74 | }; 75 | pub use crate::{HQError, HQResult}; 76 | pub use alloc::borrow::Cow; 77 | pub use alloc::boxed::Box; 78 | pub use alloc::collections::{BTreeMap, BTreeSet}; 79 | pub use alloc::string::{String, ToString}; 80 | pub use alloc::vec::Vec; 81 | pub use core::borrow::Borrow; 82 | pub use core::cell::RefCell; 83 | pub use core::fmt; 84 | pub use core::marker::PhantomPinned; 85 | pub use core::pin::Pin; 86 | 87 | pub use crate::rc::{Rc, Weak}; 88 | 89 | use core::hash::BuildHasherDefault; 90 | use hashers::fnv::FNV1aHasher64; 91 | use indexmap; 92 | pub type IndexMap = indexmap::IndexMap>; 93 | pub type IndexSet = indexmap::IndexSet>; 94 | 95 | pub use itertools::Itertools; 96 | } 97 | 98 | #[cfg(target_family = "wasm")] 99 | #[wasm_bindgen(js_namespace=console)] 100 | extern "C" { 101 | pub fn log(s: &str); 102 | pub fn warn(s: &str); 103 | } 104 | 105 | #[cfg(not(target_family = "wasm"))] 106 | pub fn log(s: &str) { 107 | println!("{s}"); 108 | } 109 | 110 | #[macro_export] 111 | macro_rules! log { 112 | ($($args:tt)+) => {{ 113 | $crate::log(format!($($args)+).as_str()); 114 | }} 115 | } 116 | 117 | #[cfg(not(target_family = "wasm"))] 118 | pub fn warn(s: &str) { 119 | println!("{s}"); 120 | } 121 | 122 | #[macro_export] 123 | macro_rules! warn { 124 | ($($args:tt)+) => {{ 125 | $crate::warn(format!($($args)+).as_str()); 126 | }} 127 | } 128 | 129 | #[cfg(feature = "compiler")] 130 | #[wasm_bindgen] 131 | pub fn sb3_to_wasm(proj: &str, flags: wasm::WasmFlags) -> HQResult { 132 | use ir::IrProject; 133 | use wasm::flags::PrintIR; 134 | 135 | let sb3_proj = sb3::Sb3Project::try_from(proj)?; 136 | let ir_proj = IrProject::try_from_sb3(&sb3_proj, &flags)?; 137 | if flags.print_ir == PrintIR::On { 138 | crate::log("ir (before optimisation):"); 139 | crate::log(format!("{ir_proj}").as_str()); 140 | } 141 | let ssa_token = optimisation::ir_optimise(&ir_proj)?; 142 | if flags.print_ir == PrintIR::On { 143 | crate::log("ir (after optimisation):"); 144 | crate::log(format!("{ir_proj}").as_str()); 145 | } 146 | wasm::WasmProject::from_ir(&ir_proj, ssa_token, flags)?.finish() 147 | } 148 | -------------------------------------------------------------------------------- /src/instructions/data/setvariableto.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::ir::RcVar; 3 | 4 | /// we need these fields to be mutable for optimisations to be feasible 5 | #[derive(Debug, Clone)] 6 | pub struct Fields { 7 | pub var: RefCell, 8 | pub local_write: RefCell, 9 | } 10 | 11 | impl fmt::Display for Fields { 12 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 13 | write!( 14 | f, 15 | r#"{{ 16 | "variable": {}, 17 | "local_write": {} 18 | }}"#, 19 | self.var.borrow(), 20 | self.local_write.borrow() 21 | ) 22 | } 23 | } 24 | 25 | pub fn wasm( 26 | func: &StepFunc, 27 | inputs: Rc<[IrType]>, 28 | Fields { var, local_write }: &Fields, 29 | ) -> HQResult> { 30 | let t1 = inputs[0]; 31 | if *local_write.try_borrow()? { 32 | let local_index: u32 = func.local_variable(&*var.try_borrow()?)?; 33 | if var.borrow().possible_types().is_base_type() { 34 | Ok(wasm![LocalSet(local_index)]) 35 | } else { 36 | Ok(wasm![ 37 | @boxed(t1), 38 | LocalSet(local_index) 39 | ]) 40 | } 41 | } else { 42 | let global_index: u32 = func 43 | .registries() 44 | .variables() 45 | .register(&*var.try_borrow()?)?; 46 | if var.try_borrow()?.possible_types().is_base_type() { 47 | Ok(wasm![#LazyGlobalSet(global_index)]) 48 | } else { 49 | Ok(wasm![ 50 | @boxed(t1), 51 | #LazyGlobalSet(global_index), 52 | ]) 53 | } 54 | } 55 | } 56 | 57 | pub fn acceptable_inputs(Fields { var, .. }: &Fields) -> HQResult> { 58 | Ok(Rc::from([ 59 | if var.try_borrow()?.possible_types().is_none() { 60 | IrType::Any 61 | } else { 62 | *var.try_borrow()?.possible_types() 63 | }, 64 | ])) 65 | } 66 | 67 | pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult { 68 | Ok(ReturnType::None) 69 | } 70 | 71 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 72 | 73 | crate::instructions_test!( 74 | any_global; 75 | data_setvariableto; 76 | t @ super::Fields { 77 | var: RefCell::new( 78 | crate::ir::RcVar::new( 79 | IrType::Any, 80 | crate::sb3::VarVal::Float(0.0), 81 | 82 | 83 | )), 84 | local_write: RefCell::new(false) 85 | } 86 | ); 87 | 88 | crate::instructions_test!( 89 | float_global; 90 | data_setvariableto; 91 | t @ super::Fields { 92 | var: RefCell::new( 93 | crate::ir::RcVar::new( 94 | IrType::Float, 95 | crate::sb3::VarVal::Float(0.0), 96 | 97 | 98 | )), 99 | local_write: RefCell::new(false) 100 | } 101 | ); 102 | 103 | crate::instructions_test!( 104 | string_global; 105 | data_setvariableto; 106 | t @ super::Fields { 107 | var: RefCell::new( 108 | crate::ir::RcVar::new( 109 | IrType::String, 110 | crate::sb3::VarVal::String("".into()), 111 | 112 | 113 | )), 114 | local_write: RefCell::new(false) 115 | } 116 | ); 117 | 118 | crate::instructions_test!( 119 | int_global; 120 | data_setvariableto; 121 | t @ super::Fields { 122 | var: RefCell::new( 123 | crate::ir::RcVar::new( 124 | IrType::QuasiInt, 125 | crate::sb3::VarVal::Bool(true), 126 | 127 | 128 | )), 129 | local_write: RefCell::new(false) 130 | } 131 | ); 132 | 133 | crate::instructions_test!( 134 | any_local; 135 | data_setvariableto; 136 | t @ super::Fields { 137 | var: RefCell::new( 138 | crate::ir::RcVar::new( 139 | IrType::Any, 140 | crate::sb3::VarVal::Float(0.0), 141 | 142 | 143 | )), 144 | local_write: RefCell::new(true) 145 | } 146 | ); 147 | 148 | crate::instructions_test!( 149 | float_local; 150 | data_setvariableto; 151 | t @ super::Fields { 152 | var: RefCell::new( 153 | crate::ir::RcVar::new( 154 | IrType::Float, 155 | crate::sb3::VarVal::Float(0.0), 156 | 157 | 158 | )), 159 | local_write: RefCell::new(true) 160 | } 161 | ); 162 | 163 | crate::instructions_test!( 164 | string_local; 165 | data_setvariableto; 166 | t @ super::Fields { 167 | var: RefCell::new( 168 | crate::ir::RcVar::new( 169 | IrType::String, 170 | crate::sb3::VarVal::String("".into()), 171 | 172 | 173 | )), 174 | local_write: RefCell::new(true) 175 | } 176 | ); 177 | 178 | crate::instructions_test!( 179 | int_local; 180 | data_setvariableto; 181 | t @ super::Fields { 182 | var: RefCell::new( 183 | crate::ir::RcVar::new( 184 | IrType::QuasiInt, 185 | crate::sb3::VarVal::Bool(true), 186 | 187 | 188 | )), 189 | local_write: RefCell::new(true) 190 | } 191 | ); 192 | -------------------------------------------------------------------------------- /src/instructions/motion/gotoxy.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use crate::wasm::{StepTarget, mem_layout}; 3 | use mem_layout::{sprite as sprite_layout, stage as stage_layout}; 4 | use wasm_encoder::MemArg; 5 | 6 | pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { 7 | let ir_target_index: i32 = func 8 | .target_index() 9 | .try_into() 10 | .map_err(|_| make_hq_bug!("target index out of bounds"))?; 11 | let move_func_index = func.registries().external_functions().register( 12 | ("motion", "gotoxy".into()), 13 | (vec![ValType::F64, ValType::F64, ValType::I32], vec![]), 14 | )?; 15 | let pen_func_index = func.registries().external_functions().register( 16 | ("pen", "line".into()), 17 | ( 18 | vec![ 19 | ValType::F64, 20 | ValType::F64, 21 | ValType::F64, 22 | ValType::F64, 23 | ValType::F64, 24 | ValType::F32, 25 | ValType::F32, 26 | ValType::F32, 27 | ValType::F32, 28 | ], 29 | vec![], 30 | ), 31 | )?; 32 | let StepTarget::Sprite(wasm_target_index) = func.target() else { 33 | hq_bad_proj!("motion_gotoxy called in stage") 34 | }; 35 | let x_local = func.local(ValType::F64)?; 36 | let y_local = func.local(ValType::F64)?; 37 | Ok(wasm![ 38 | LocalSet(y_local), 39 | LocalSet(x_local), 40 | I32Const(0), 41 | I32Load8U(MemArg { 42 | offset: (stage_layout::BLOCK_SIZE 43 | + wasm_target_index * sprite_layout::BLOCK_SIZE 44 | + sprite_layout::PEN_DOWN) 45 | .into(), 46 | align: 0, 47 | memory_index: 0 48 | }), 49 | If(wasm_encoder::BlockType::Empty), 50 | I32Const(0), 51 | F64Load(MemArg { 52 | offset: (stage_layout::BLOCK_SIZE 53 | + wasm_target_index * sprite_layout::BLOCK_SIZE 54 | + sprite_layout::PEN_SIZE) 55 | .into(), 56 | align: 3, 57 | memory_index: 0 58 | }), 59 | I32Const(0), 60 | F64Load(MemArg { 61 | offset: (stage_layout::BLOCK_SIZE 62 | + wasm_target_index * sprite_layout::BLOCK_SIZE 63 | + sprite_layout::X) 64 | .into(), 65 | align: 3, 66 | memory_index: 0 67 | }), 68 | I32Const(0), 69 | F64Load(MemArg { 70 | offset: (stage_layout::BLOCK_SIZE 71 | + wasm_target_index * sprite_layout::BLOCK_SIZE 72 | + sprite_layout::Y) 73 | .into(), 74 | align: 3, 75 | memory_index: 0 76 | }), 77 | LocalGet(x_local), 78 | LocalGet(y_local), 79 | I32Const(0), 80 | F32Load(MemArg { 81 | offset: (stage_layout::BLOCK_SIZE 82 | + wasm_target_index * sprite_layout::BLOCK_SIZE 83 | + sprite_layout::PEN_COLOR_R) 84 | .into(), 85 | align: 2, 86 | memory_index: 0 87 | }), 88 | I32Const(0), 89 | F32Load(MemArg { 90 | offset: (stage_layout::BLOCK_SIZE 91 | + wasm_target_index * sprite_layout::BLOCK_SIZE 92 | + sprite_layout::PEN_COLOR_G) 93 | .into(), 94 | align: 2, 95 | memory_index: 0 96 | }), 97 | I32Const(0), 98 | F32Load(MemArg { 99 | offset: (stage_layout::BLOCK_SIZE 100 | + wasm_target_index * sprite_layout::BLOCK_SIZE 101 | + sprite_layout::PEN_COLOR_B) 102 | .into(), 103 | align: 2, 104 | memory_index: 0 105 | }), 106 | I32Const(0), 107 | F32Load(MemArg { 108 | offset: (stage_layout::BLOCK_SIZE 109 | + wasm_target_index * sprite_layout::BLOCK_SIZE 110 | + sprite_layout::PEN_COLOR_A) 111 | .into(), 112 | align: 2, 113 | memory_index: 0 114 | }), 115 | Call(pen_func_index), 116 | End, 117 | I32Const(0), 118 | LocalGet(x_local), 119 | F64Store(MemArg { 120 | offset: (stage_layout::BLOCK_SIZE 121 | + wasm_target_index * sprite_layout::BLOCK_SIZE 122 | + sprite_layout::X) 123 | .into(), 124 | align: 3, 125 | memory_index: 0, 126 | }), 127 | I32Const(0), 128 | LocalGet(y_local), 129 | F64Store(MemArg { 130 | offset: (stage_layout::BLOCK_SIZE 131 | + wasm_target_index * sprite_layout::BLOCK_SIZE 132 | + sprite_layout::Y) 133 | .into(), 134 | align: 3, 135 | memory_index: 0, 136 | }), 137 | LocalGet(x_local), 138 | LocalGet(y_local), 139 | I32Const(ir_target_index), 140 | Call(move_func_index), 141 | ]) 142 | } 143 | 144 | pub fn acceptable_inputs() -> HQResult> { 145 | Ok(Rc::from([IrType::Float, IrType::Float])) 146 | } 147 | 148 | pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { 149 | Ok(ReturnType::None) 150 | } 151 | 152 | pub const REQUESTS_SCREEN_REFRESH: bool = true; 153 | 154 | crate::instructions_test! {tests; motion_gotoxy; t1, t2 ; } 155 | -------------------------------------------------------------------------------- /src/instructions/data/teevariable.rs: -------------------------------------------------------------------------------- 1 | /// this is currently just a convenience block. if we get thread-scoped variables 2 | /// (i.e. locals) then this can actually use the wasm tee instruction. 3 | use super::super::prelude::*; 4 | use crate::ir::RcVar; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct Fields { 8 | pub var: RefCell, 9 | pub local_read_write: RefCell, 10 | } 11 | 12 | impl fmt::Display for Fields { 13 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 14 | write!( 15 | f, 16 | r#"{{ 17 | "variable": {}, 18 | "local_read_write": {} 19 | }}"#, 20 | self.var.borrow(), 21 | self.local_read_write.borrow() 22 | ) 23 | } 24 | } 25 | 26 | pub fn wasm( 27 | func: &StepFunc, 28 | inputs: Rc<[IrType]>, 29 | Fields { 30 | var, 31 | local_read_write, 32 | }: &Fields, 33 | ) -> HQResult> { 34 | let t1 = inputs[0]; 35 | if *local_read_write.try_borrow()? { 36 | let local_index: u32 = func.local_variable(&*var.try_borrow()?)?; 37 | if var.borrow().possible_types().is_base_type() { 38 | Ok(wasm![LocalTee(local_index)]) 39 | } else { 40 | Ok(wasm![ 41 | @boxed(t1), 42 | LocalTee(local_index) 43 | ]) 44 | } 45 | } else { 46 | let global_index: u32 = func 47 | .registries() 48 | .variables() 49 | .register(&*var.try_borrow()?)?; 50 | if var.borrow().possible_types().is_base_type() { 51 | Ok(wasm![#LazyGlobalSet(global_index), #LazyGlobalGet(global_index)]) 52 | } else { 53 | Ok(wasm![ 54 | @boxed(t1), 55 | #LazyGlobalSet(global_index), 56 | #LazyGlobalGet(global_index) 57 | ]) 58 | } 59 | } 60 | } 61 | 62 | pub fn acceptable_inputs(Fields { var, .. }: &Fields) -> HQResult> { 63 | Ok(Rc::from([ 64 | if var.try_borrow()?.possible_types().is_none() { 65 | IrType::Any 66 | } else { 67 | *var.try_borrow()?.possible_types() 68 | }, 69 | ])) 70 | } 71 | 72 | pub fn output_type(_inputs: Rc<[IrType]>, Fields { var, .. }: &Fields) -> HQResult { 73 | Ok(Singleton(*var.try_borrow()?.possible_types())) 74 | } 75 | 76 | pub const REQUESTS_SCREEN_REFRESH: bool = false; 77 | 78 | crate::instructions_test!( 79 | any_global; 80 | data_teevariable; 81 | t 82 | @ super::Fields { 83 | var: RefCell::new( 84 | crate::ir::RcVar::new( 85 | IrType::Any, 86 | crate::sb3::VarVal::Float(0.0), 87 | 88 | )) 89 | , 90 | local_read_write: RefCell::new(false), 91 | } 92 | ); 93 | 94 | crate::instructions_test!( 95 | float_global; 96 | data_teevariable; 97 | t 98 | @ super::Fields { 99 | var: RefCell::new( 100 | crate::ir::RcVar::new( 101 | IrType::Float, 102 | crate::sb3::VarVal::Float(0.0), 103 | 104 | )) 105 | , 106 | local_read_write: RefCell::new(false), 107 | } 108 | ); 109 | 110 | crate::instructions_test!( 111 | string_global; 112 | data_teevariable; 113 | t 114 | @ super::Fields { 115 | var: RefCell::new( 116 | crate::ir::RcVar::new( 117 | IrType::String, 118 | crate::sb3::VarVal::String("".into()), 119 | 120 | )) 121 | , 122 | local_read_write: RefCell::new(false), 123 | } 124 | ); 125 | 126 | crate::instructions_test!( 127 | int_global; 128 | data_teevariable; 129 | t 130 | @ super::Fields { 131 | var: RefCell::new( 132 | crate::ir::RcVar::new( 133 | IrType::QuasiInt, 134 | crate::sb3::VarVal::Bool(true), 135 | 136 | )) 137 | , 138 | local_read_write: RefCell::new(false), 139 | } 140 | ); 141 | 142 | crate::instructions_test!( 143 | any_local; 144 | data_teevariable; 145 | t 146 | @ super::Fields { 147 | var: RefCell::new( 148 | crate::ir::RcVar::new( 149 | IrType::Any, 150 | crate::sb3::VarVal::Float(0.0), 151 | 152 | ) 153 | ), 154 | local_read_write: RefCell::new(true), 155 | } 156 | ); 157 | 158 | crate::instructions_test!( 159 | float_local; 160 | data_teevariable; 161 | t 162 | @ super::Fields { 163 | var: RefCell::new( 164 | crate::ir::RcVar::new( 165 | IrType::Float, 166 | crate::sb3::VarVal::Float(0.0), 167 | 168 | ) 169 | ), 170 | local_read_write: RefCell::new(true), 171 | } 172 | ); 173 | 174 | crate::instructions_test!( 175 | string_local; 176 | data_teevariable; 177 | t 178 | @ super::Fields { 179 | var: RefCell::new( 180 | crate::ir::RcVar::new( 181 | IrType::String, 182 | crate::sb3::VarVal::String("".into()), 183 | 184 | 185 | )), 186 | local_read_write: RefCell::new(true), 187 | } 188 | ); 189 | 190 | crate::instructions_test!( 191 | int_local; 192 | data_teevariable; 193 | t 194 | @ super::Fields { 195 | var: RefCell::new( 196 | crate::ir::RcVar::new( 197 | IrType::QuasiInt, 198 | crate::sb3::VarVal::Bool(true), 199 | 200 | )), 201 | local_read_write: RefCell::new(true), 202 | } 203 | ); 204 | --------------------------------------------------------------------------------