From fedf56d0c68837e26b001268e28679208c3464ac Mon Sep 17 00:00:00 2001 From: pufferfish101007 <--get> Date: Tue, 10 Feb 2026 22:31:56 +0000 Subject: [PATCH 01/46] add rudimentary support for sensing_askandwait and sensing_answer --- js/sensing/queue_ask.ts | 3 + js/shared.ts | 8 ++ playground/components/ProjectPlayer.vue | 38 ++++++++- playground/lib/project-runner.js | 30 ++++++- src/instructions.rs | 5 +- src/instructions/hq.rs | 1 + src/instructions/hq/poll_waiting_event.rs | 44 ++++++++++ src/instructions/sensing.rs | 2 + src/instructions/sensing/answer.rs | 39 +++++++++ src/instructions/sensing/askandwait.rs | 98 +++++++++++++++++++++++ src/ir/blocks.rs | 28 ++++++- src/ir/step.rs | 27 +++++++ src/registry.rs | 4 + src/wasm/project.rs | 21 ++++- src/wasm/registries/functions.rs | 59 +++++++++++++- 15 files changed, 394 insertions(+), 13 deletions(-) create mode 100644 js/sensing/queue_ask.ts create mode 100644 src/instructions/hq/poll_waiting_event.rs create mode 100644 src/instructions/sensing/answer.rs create mode 100644 src/instructions/sensing/askandwait.rs diff --git a/js/sensing/queue_ask.ts b/js/sensing/queue_ask.ts new file mode 100644 index 00000000..a3644c60 --- /dev/null +++ b/js/sensing/queue_ask.ts @@ -0,0 +1,3 @@ +import { queue_question } from "../shared"; + +export const queue_ask = queue_question; diff --git a/js/shared.ts b/js/shared.ts index bab55bec..bcbfd1fd 100644 --- a/js/shared.ts +++ b/js/shared.ts @@ -5,6 +5,7 @@ let _renderer: object; let _pen_skin: number; let _target_skins: Array<[number, number]>; let _costumes: Array>; +let _queue_question: (question: string, struct: object) => void = () => {}; type Costume = { data: string; @@ -17,6 +18,7 @@ export function setup( pen_skin: number, target_skins: Array<[number, number]>, costumes: Array>, + queue_question: (question: string, struct: object) => void, ) { _target_names = target_names; _target_bubbles = _target_names.map((_) => null); @@ -25,6 +27,7 @@ export function setup( _pen_skin = pen_skin; _target_skins = target_skins; _costumes = costumes; + _queue_question = queue_question; _setup = true; } @@ -86,3 +89,8 @@ export function update_bubble( ); } } + +export function queue_question(question: string, struct: object) { + check_setup(); + _queue_question(question, struct); +} diff --git a/playground/components/ProjectPlayer.vue b/playground/components/ProjectPlayer.vue index 677b74cd..134780a8 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -31,6 +31,14 @@ {{ props.description }} +
+ {{ queued_questions[0]?.[0] }} +
+
+ + +
+
{{ loadingMsg }} @@ -43,7 +51,7 @@ import { WasmFlags, } from "../../js/compiler/hyperquark.js"; import { instantiateProject } from "../lib/project-runner.js"; -import { ref, onMounted, registerRuntimeCompiler } from "vue"; +import { ref, onMounted, reactive } from "vue"; import { getSettings } from "../lib/settings.js"; import { useDebugModeStore } from "../stores/debug.js"; @@ -89,6 +97,31 @@ const declareError = (e, terminate, mode, stage, extra) => { } }; +const queued_questions = reactive([]); +let question_response = ref(""); +let mark_question_resolved = () => {}; + +function set_mark_question_resolved(func) { + mark_question_resolved = func; +} + +let setAnswer = () => {}; + +const setSetAnswer = (_setAnswer) => { + setAnswer = _setAnswer; +}; + +function submitQuestion() { + setAnswer(question_response.value); + question_response.value = ""; + const [_, struct] = queued_questions.shift(); + mark_question_resolved(struct); +} + +function queue_question(question, struct) { + queued_questions.push([question, struct]); +} + onMounted(async () => { const load_asset = async (md5ext) => { try { @@ -203,6 +236,9 @@ onMounted(async () => { return new RenderWebGL(canvas.value); }, isDebug: () => debugModeStore.debug, + queue_question, + set_mark_question_resolved, + setSetAnswer, }); loaded.value = true; diff --git a/playground/lib/project-runner.js b/playground/lib/project-runner.js index 99b99f54..80d2bdf1 100644 --- a/playground/lib/project-runner.js +++ b/playground/lib/project-runner.js @@ -19,7 +19,13 @@ function createSkin(renderer, type, layer, ...params) { return [skin, drawableId]; } -async function setup(makeRenderer, project_json, assets, target_names) { +async function setup( + makeRenderer, + project_json, + assets, + target_names, + queue_question, +) { if (is_setup()) return; let renderer = await makeRenderer(); @@ -65,7 +71,14 @@ async function setup(makeRenderer, project_json, assets, target_names) { }); console.log(target_skins); - sharedSetup(target_names, renderer, pen_skin, target_skins, costumes); + sharedSetup( + target_names, + renderer, + pen_skin, + target_skins, + costumes, + queue_question, + ); } // @ts-ignore @@ -82,13 +95,16 @@ export async function instantiateProject({ timeout, onTimeout = () => null, importOverrides, + queue_question, + set_mark_question_resolved, + setSetAnswer, }) { if (isDebug() && typeof window === "object") window.open( URL.createObjectURL(new Blob([wasm_bytes], { type: "application/wasm" })), ); - await setup(makeRenderer, project_json, assets, target_names); + await setup(makeRenderer, project_json, assets, target_names, queue_question); const renderer = get_renderer(); @@ -156,6 +172,8 @@ export async function instantiateProject({ threads, unreachable_dbg, sensing_timer, + mark_waiting_flag, + sensing_answer, } = instance.exports; if (typeof window === "object") { window.memory = memory; @@ -163,6 +181,12 @@ export async function instantiateProject({ window.tick = tick; } + set_mark_question_resolved(mark_waiting_flag); + + setSetAnswer((answerText) => { + sensing_answer.value = answerText; + }); + try { // expose the module to devtools unreachable_dbg(); diff --git a/src/instructions.rs b/src/instructions.rs index 3bfcdc4d..2b9c0263 100644 --- a/src/instructions.rs +++ b/src/instructions.rs @@ -113,7 +113,10 @@ impl IrOpcode { }) | Self::event_broadcast_and_wait(EventBroadcastAndWaitFields { next_step, .. }) | Self::procedures_call_nonwarp(ProceduresCallNonwarpFields { next_step, .. }) - | Self::control_wait(ControlWaitFields { next_step, .. }) => Some(*next_step), + | Self::control_wait(ControlWaitFields { next_step, .. }) + | Self::sensing_askandwait(SensingAskandwaitFields { next_step, .. }) => { + Some(*next_step) + } _ => None, } } diff --git a/src/instructions/hq.rs b/src/instructions/hq.rs index 5c273ea2..d62c1f17 100644 --- a/src/instructions/hq.rs +++ b/src/instructions/hq.rs @@ -6,6 +6,7 @@ pub mod drop; pub mod dup; pub mod float; pub mod integer; +pub mod poll_waiting_event; pub mod swap; pub mod text; pub mod r#yield; diff --git a/src/instructions/hq/poll_waiting_event.rs b/src/instructions/hq/poll_waiting_event.rs new file mode 100644 index 00000000..dc17ec58 --- /dev/null +++ b/src/instructions/hq/poll_waiting_event.rs @@ -0,0 +1,44 @@ +//! This is a bit of a strange instruction in that it relies on only ever being used inside a step +//! that was spawned from a `sensing_askandwait` block (for now; there is potential to be expanded +//! to other usecases which is why this is in the hq category rather than sensing). Any other usage +//! will result in invalid wasm. +//! +//! Returns 1 if still waiting on any threads, 0 otherwise. + +use wasm_encoder::{FieldType, HeapType, StorageType}; + +use super::super::prelude::*; +use crate::wasm::StepFunc; + +pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { + let i8_struct_type = func.registries().types().struct_(vec![FieldType { + element_type: StorageType::I8, + mutable: true, + }])?; + + Ok(wasm![ + LocalGet(1), // this should never have additional function arguments so this is fine + RefCastNonNull(HeapType::Concrete(i8_struct_type)), + StructGetS { + struct_type_index: i8_struct_type, + field_index: 0 + }, + ]) +} + +pub fn acceptable_inputs() -> HQResult> { + Ok(Rc::from([])) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { + Ok(Singleton(IrType::Boolean)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +pub const fn const_fold( + _inputs: &[ConstFoldItem], + _state: &mut ConstFoldState, +) -> HQResult { + Ok(NotFoldable) +} diff --git a/src/instructions/sensing.rs b/src/instructions/sensing.rs index afa8c348..6ac15a6c 100644 --- a/src/instructions/sensing.rs +++ b/src/instructions/sensing.rs @@ -1,3 +1,5 @@ +pub mod answer; +pub mod askandwait; pub mod dayssince2000; pub mod reset_timer; pub mod timer; diff --git a/src/instructions/sensing/answer.rs b/src/instructions/sensing/answer.rs new file mode 100644 index 00000000..e8827639 --- /dev/null +++ b/src/instructions/sensing/answer.rs @@ -0,0 +1,39 @@ +use wasm_encoder::ConstExpr; + +use super::super::prelude::*; +use crate::wasm::{GlobalExportable, GlobalMutable}; + +pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { + let empty_string_index = func.registries().strings().register_default("".into())?; + let global_index = func.registries().globals().register( + "sensing_answer".into(), + ( + ValType::EXTERNREF, + ConstExpr::global_get(empty_string_index), + GlobalMutable(true), + GlobalExportable(true), + ), + )?; + Ok(wasm![ + #LazyGlobalGet(global_index), + ]) +} + +pub fn acceptable_inputs() -> HQResult> { + Ok(Rc::from([])) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { + Ok(Singleton(IrType::String)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +pub const fn const_fold( + _inputs: &[ConstFoldItem], + _state: &mut ConstFoldState, +) -> HQResult { + Ok(NotFoldable) +} + +crate::instructions_test! {tests; sensing_answer; ;} diff --git a/src/instructions/sensing/askandwait.rs b/src/instructions/sensing/askandwait.rs new file mode 100644 index 00000000..86ec46fa --- /dev/null +++ b/src/instructions/sensing/askandwait.rs @@ -0,0 +1,98 @@ +use wasm_encoder::{FieldType, HeapType, StorageType}; + +use super::super::prelude::*; +use crate::ir::StepIndex; +use crate::wasm::StepFunc; +use crate::wasm::registries::functions::static_functions::{MarkWaitingFlag, SpawnThreadInStack}; + +#[derive(Clone, Debug)] +pub struct Fields { + pub poll_step: StepIndex, + pub next_step: StepIndex, +} + +impl fmt::Display for Fields { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + r#"{{ + "poll_step": {}, + "next_step": {} + }}"#, + self.poll_step.0, self.next_step.0, + ) + } +} + +pub fn wasm( + func: &StepFunc, + _inputs: Rc<[IrType]>, + Fields { + poll_step, + next_step, + }: &Fields, +) -> HQResult> { + let i8_struct_type = func.registries().types().struct_(vec![FieldType { + element_type: StorageType::I8, + mutable: true, + }])?; + let struct_valtype = ValType::Ref(RefType { + nullable: false, + heap_type: HeapType::Concrete(i8_struct_type), + }); + let struct_local = func.local(struct_valtype)?; + + let spawn_thread_func = func + .registries() + .static_functions() + .register::()?; + + let queue_ask = func.registries().external_functions().register( + ("sensing", "queue_ask".into()), + (vec![ValType::EXTERNREF, struct_valtype], vec![]), + )?; + + // register the exported function that is called by JS, + // otherwise it won't be registered and thus will be undefined! + func.registries() + .static_functions() + .register::()?; + + Ok(wasm![ + LocalGet( + (func.params().len() - 2) + .try_into() + .map_err(|_| make_hq_bug!("local index out of bounds"))? + ), + #LazyStepRef(*poll_step), + StructNewDefault(i8_struct_type), + LocalTee(struct_local), + #LazyStepRef(*next_step), + #StaticFunctionCall(spawn_thread_func), + LocalGet(struct_local), + Call(queue_ask), + ]) +} + +pub fn acceptable_inputs(_fields: &Fields) -> HQResult> { + Ok(Rc::from([IrType::String])) +} + +pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult { + Ok(ReturnType::None) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +pub const fn const_fold( + _inputs: &[ConstFoldItem], + _state: &mut ConstFoldState, + _fields: &Fields, +) -> HQResult { + Ok(NotFoldable) +} + +// crate::instructions_test! { tests; sensing_askandwait; t @ super::Fields { +// poll_step: super::StepIndex(0), +// next_step: super::StepIndex(0), +// }} diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index fd266c18..8b6d7b4f 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -13,7 +13,7 @@ use crate::instructions::{ DataVariableFields, EventBroadcastAndWaitFields, EventBroadcastFields, HqBooleanFields, HqCastFields, HqColorRgbFields, HqFloatFields, HqIntegerFields, HqTextFields, HqYieldFields, IrOpcode, LooksSayFields, LooksThinkFields, ProceduresArgumentFields, - ProceduresCallNonwarpFields, ProceduresCallWarpFields, YieldMode, + ProceduresCallNonwarpFields, ProceduresCallWarpFields, SensingAskandwaitFields, YieldMode, }; use crate::ir::{InlinedStep, MaybeInlinedStep, RcList, ReturnType, StepIndex}; use crate::prelude::*; @@ -264,7 +264,9 @@ pub fn input_names(block_info: &BlockInfo, context: &StepContext) -> HQResult vec![], + | BlockOpcode::sensing_resettimer + | BlockOpcode::sensing_answer => vec![], + BlockOpcode::sensing_askandwait => vec!["QUESTION"], BlockOpcode::event_broadcast | BlockOpcode::event_broadcastandwait => { vec!["BROADCAST_INPUT"] } @@ -1256,6 +1258,7 @@ fn from_normal_block( BlockOpcode::operator_letter_of => vec![IrOpcode::operator_letter_of], BlockOpcode::sensing_dayssince2000 => vec![IrOpcode::sensing_dayssince2000], BlockOpcode::sensing_timer => vec![IrOpcode::sensing_timer], + BlockOpcode::sensing_answer => vec![IrOpcode::sensing_answer], BlockOpcode::sensing_resettimer => vec![IrOpcode::sensing_reset_timer], BlockOpcode::operator_lt => vec![IrOpcode::operator_lt], BlockOpcode::operator_gt => vec![IrOpcode::operator_gt], @@ -1364,6 +1367,27 @@ fn from_normal_block( project, )? } + BlockOpcode::sensing_askandwait => { + let poll_step = + context + .project()? + .new_owned_step(Step::new_poll_waiting_event( + context.clone(), + Weak::clone(project), + ))?; + should_break = true; + let next_step = generate_next_step_non_inlined( + block_info, + blocks, + context, + final_next_blocks.clone(), + flags, + )?; + vec![IrOpcode::sensing_askandwait(SensingAskandwaitFields { + poll_step, + next_step, + })] + } BlockOpcode::control_wait => { let poll_step = context.project()?.new_owned_step( Step::new_poll_timer(context.clone(), Weak::clone(project)), diff --git a/src/ir/step.rs b/src/ir/step.rs index 3d158f47..37808104 100644 --- a/src/ir/step.rs +++ b/src/ir/step.rs @@ -133,6 +133,33 @@ impl Step { ) } + #[must_use] + pub fn new_poll_waiting_event(context: StepContext, project: Weak) -> Self { + Self::new( + None, + context.clone(), + vec![ + IrOpcode::hq_poll_waiting_event, + IrOpcode::control_if_else(ControlIfElseFields { + branch_else: Rc::new(RefCell::new(Self::new( + None, + context.clone(), + vec![], + Weak::clone(&project), + false, + ))), + branch_if: Rc::new(RefCell::new(Self::new_terminating( + context, + Weak::clone(&project), + false, + ))), + }), + ], + project, + true, + ) + } + #[must_use] pub fn new_poll_timer(context: StepContext, project: Weak) -> Self { Self::new( diff --git a/src/registry.rs b/src/registry.rs index 858e1550..289183d5 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -114,6 +114,10 @@ pub trait Registry: Sized + RegistryType { ) .map_err(|_| make_hq_bug!("registry item index out of bounds")) } + + // TODO: register_override_ifexists or similar - for things like mark_waiting_flag, + // which need to be overriden if they are registered, but don't actually need to be + // registered always. } pub trait RegistryDefault: Registry { diff --git a/src/wasm/project.rs b/src/wasm/project.rs index 964534d7..7b55bc9b 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -2,16 +2,18 @@ use itertools::Itertools; use wasm_bindgen::prelude::*; use wasm_encoder::{ AbstractHeapType, BlockType as WasmBlockType, CodeSection, ConstExpr, DataCountSection, - DataSection, ElementSection, Elements, ExportKind, ExportSection, Function, FunctionSection, - GlobalSection, HeapType, ImportSection, Instruction, MemorySection, MemoryType, Module, - RefType, StartSection, TableSection, TypeSection, ValType, + DataSection, ElementSection, Elements, ExportKind, ExportSection, FieldType, Function, + FunctionSection, GlobalSection, HeapType, ImportSection, Instruction, MemorySection, + MemoryType, Module, RefType, StartSection, StorageType, TableSection, TypeSection, ValType, }; use wasm_gen::wasm; use super::{ExternalEnvironment, GlobalExportable, GlobalMutable, Registries}; use crate::ir::{Event, IrProject, StepIndex, Type as IrType}; use crate::prelude::*; -use crate::wasm::registries::functions::static_functions::{SpawnNewThread, SpawnThreadInStack}; +use crate::wasm::registries::functions::static_functions::{ + MarkWaitingFlag, SpawnNewThread, SpawnThreadInStack, +}; use crate::wasm::{StepFunc, StringsTable, ThreadsTable, WasmFlags}; /// A respresentation of a WASM representation of a project. Cannot be created directly; @@ -153,10 +155,21 @@ impl WasmProject { self.threads_table_index()?, ))?; + self.registries() + .static_functions() + .register_override::(self.registries().types().struct_( + vec![FieldType { + element_type: StorageType::I8, + mutable: true, + }], + )?)?; + self.registries().static_functions().clone().finish( &mut functions, + &mut exports, &mut codes, self.registries.types(), + self.imported_func_count()?, )?; for step_func in self.steps().try_borrow()?.iter().cloned() { diff --git a/src/wasm/registries/functions.rs b/src/wasm/registries/functions.rs index 8a61cde7..996db3c0 100644 --- a/src/wasm/registries/functions.rs +++ b/src/wasm/registries/functions.rs @@ -1,8 +1,8 @@ #![allow(clippy::cast_possible_wrap, reason = "can't use try_into in const")] use wasm_encoder::{ - CodeSection, EntityType, Function, FunctionSection, ImportSection, Instruction as WInstruction, - ValType, + CodeSection, EntityType, ExportKind, ExportSection, Function, FunctionSection, ImportSection, + Instruction as WInstruction, ValType, }; use super::TypeRegistry; @@ -28,6 +28,7 @@ pub struct StaticFunction { pub params: Box<[ValType]>, pub returns: Box<[ValType]>, pub locals: Box<[ValType]>, + pub export: Option>, } #[derive(Clone)] @@ -46,8 +47,10 @@ impl StaticFunctionRegistry { pub fn finish( self, functions: &mut FunctionSection, + exports: &mut ExportSection, codes: &mut CodeSection, type_registry: &TypeRegistry, + imported_func_count: u32, ) -> HQResult<()> { for ( _name, @@ -62,6 +65,7 @@ impl StaticFunctionRegistry { params, returns, locals, + export, }) = static_function.map_or_else(maybe_populate, Some) else { hq_bug!( @@ -76,6 +80,13 @@ impl StaticFunctionRegistry { func.instruction(&instruction); } codes.function(&func); + if let Some(export_name) = export { + exports.export( + &export_name, + ExportKind::Func, + imported_func_count + functions.len() - 1, + ); + } } Ok(()) } @@ -92,6 +103,46 @@ pub mod static_functions { use crate::prelude::*; use crate::wasm::{f32_to_ieeef32, mem_layout}; + /// Mark a waiting flag as done. + /// + /// This is designed to be exported (as `"mark_waiting_flag"`) and called by JS. + /// + /// Takes 1 parameter: + /// - A nonnull struct with a single i8 field. + pub struct MarkWaitingFlag; + impl NamedRegistryItem for MarkWaitingFlag { + const VALUE: MaybeStaticFunction = MaybeStaticFunction { + static_function: None, + maybe_populate: || None, + }; + } + pub type MarkWaitingFlagOverride = u32; + impl NamedRegistryItemOverride for MarkWaitingFlag { + fn r#override(i8_struct_ty: u32) -> MaybeStaticFunction { + MaybeStaticFunction { + static_function: Some(StaticFunction { + export: Some("mark_waiting_flag".into()), + instructions: Box::from(wasm_const![ + LocalGet(0), + I32Const(1), + StructSet { + struct_type_index: i8_struct_ty, + field_index: 0 + }, + End, + ] as &[_]), + params: Box::new([ValType::Ref(RefType { + nullable: false, + heap_type: HeapType::Concrete(i8_struct_ty), + })]), + returns: Box::new([]), + locals: Box::new([]), + }), + maybe_populate: || None, + } + } + } + /// Spawns a new thread in the same stack (i.e. a thread that yields back to the current /// thread once it completes.) /// @@ -116,6 +167,7 @@ pub mod static_functions { ) -> MaybeStaticFunction { MaybeStaticFunction { static_function: Some(StaticFunction { + export: None, instructions: Box::from(wasm_const![ LocalGet(1), LocalGet(2), @@ -218,6 +270,7 @@ pub mod static_functions { ) -> MaybeStaticFunction { MaybeStaticFunction { static_function: Some(StaticFunction { + export: None, params: Box::from([ ValType::Ref(RefType { nullable: false, @@ -281,6 +334,7 @@ pub mod static_functions { static_function: None, maybe_populate: || { Some(StaticFunction { + export: None, params: Box::from([ValType::I32]), returns: Box::from([]), locals: Box::from({ @@ -554,6 +608,7 @@ pub mod static_functions { static_function: None, maybe_populate: || { Some(StaticFunction { + export: None, params: Box::from([ValType::I32]), returns: Box::from([]), locals: Box::from({ From 546eb51f2b6e5fea6cc4dd5e5a00c38cebc6d058 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <--get> Date: Wed, 11 Feb 2026 20:51:35 +0000 Subject: [PATCH 02/46] add comments to StaticFuncRegistry and items --- src/wasm/registries/functions.rs | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/wasm/registries/functions.rs b/src/wasm/registries/functions.rs index 996db3c0..886e9851 100644 --- a/src/wasm/registries/functions.rs +++ b/src/wasm/registries/functions.rs @@ -31,6 +31,15 @@ pub struct StaticFunction { pub export: Option>, } +/// A `const`-able representation of a static function. +/// +/// `maybe_populate` should return a `Some(StaticFunction)` if the function instructions +/// are known at compile-time; +/// `static_function` should be overriden to a `Some(StaticFunction)` if the function +/// is overriden. +/// +/// It is not possible to populate `static_function` in a `const` context, hence the existence +/// of the `maybe_populate` field. #[derive(Clone)] pub struct MaybeStaticFunction { pub static_function: Option, @@ -109,6 +118,8 @@ pub mod static_functions { /// /// Takes 1 parameter: /// - A nonnull struct with a single i8 field. + /// + /// Override with one u32, the single-field i8 struct type index pub struct MarkWaitingFlag; impl NamedRegistryItem for MarkWaitingFlag { const VALUE: MaybeStaticFunction = MaybeStaticFunction { @@ -151,6 +162,13 @@ pub mod static_functions { /// - step funcref - the step to spawn /// - structref - the structref to pass to the step being spawned /// - step funcref - the step to return to after + /// + /// Override with: + /// - u32 - the index of the step func type + /// - u32 - the index of the stack struct type + /// - u32 - the index of the stack array type + /// - u32 - the index of the thread struct type + /// - u32 - the index of the threads table pub struct SpawnThreadInStack; impl NamedRegistryItem for SpawnThreadInStack { const VALUE: MaybeStaticFunction = MaybeStaticFunction { @@ -256,6 +274,19 @@ pub mod static_functions { } } + /// Spawn a new thread with the provided step function. This does not call it + /// immediately, instead leaving that for the scheduler or calling function to do so. + /// + /// Takes 2 parameters: + /// - step funcref - the step to spawn + /// - ref null struct - the stack struct to spawn it with + /// + /// Override with: + /// - u32 - the index of the step func type + /// - u32 - the index of the stack struct type + /// - u32 - the index of the stack array type + /// - u32 - the index of the thread struct type + /// - u32 - the index of the threads table pub struct SpawnNewThread; impl NamedRegistryItem for SpawnNewThread { const VALUE: MaybeStaticFunction = MaybeStaticFunction { @@ -328,6 +359,11 @@ pub mod static_functions { VAL_F } + /// Updates the stored RGBA pen colour from the HSV colour. + /// + /// Takes 1 paramter, an i32 corresponding to the target index + /// + /// Not overridable. pub struct UpdatePenColorFromHSV; impl NamedRegistryItem for UpdatePenColorFromHSV { const VALUE: MaybeStaticFunction = MaybeStaticFunction { @@ -602,6 +638,11 @@ pub mod static_functions { HUE SAT } + /// Updates the stored HSV pen colour from the RGBA colour. + /// + /// Takes one parameter, an i32 corresponding to the target index. + /// + /// Not overridable. pub struct UpdatePenColorFromRGB; impl NamedRegistryItem for UpdatePenColorFromRGB { const VALUE: MaybeStaticFunction = MaybeStaticFunction { From 3918b0a6f9f9057081efc6c94ed2b923f099eef2 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <--get> Date: Wed, 11 Feb 2026 20:53:48 +0000 Subject: [PATCH 03/46] fix stage dimensions to 480x360 --- playground/components/ProjectPlayer.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/playground/components/ProjectPlayer.vue b/playground/components/ProjectPlayer.vue index 134780a8..8490b45c 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -259,6 +259,8 @@ canvas { float: left; margin-right: 1em; margin-bottom: 1.5em; + width: 480px; + height: 360px; } div.instructions { From 77cc1be17bd986632ac32930592df1686e832877 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <--get> Date: Wed, 11 Feb 2026 21:54:00 +0000 Subject: [PATCH 04/46] display questions over stage, and using bubbles when appropriate --- js/shared.ts | 6 +- playground/components/ProjectPlayer.vue | 84 ++++++++++++++++++------- playground/lib/project-runner.js | 14 ++--- src/ir/blocks.rs | 60 ++++++++++++++---- 4 files changed, 123 insertions(+), 41 deletions(-) diff --git a/js/shared.ts b/js/shared.ts index bcbfd1fd..d45b3217 100644 --- a/js/shared.ts +++ b/js/shared.ts @@ -72,7 +72,7 @@ export function update_bubble( text: string, ) { check_setup(); - if (!_target_bubbles[target_index]) { + if (!_target_bubbles[target_index] && text !== "") { _target_bubbles[target_index] = _renderer.createSkin( "text", "sprite", @@ -80,6 +80,10 @@ export function update_bubble( text, false, ); + } else if (text == "") { + _renderer.destroyDrawable(_target_bubbles[target_index][1], "sprite"); + _renderer.destroySkin(_target_bubbles[target_index][0]); + _target_bubbles[target_index] = null; } else { _renderer.updateTextSkin( _target_bubbles[target_index][0], diff --git a/playground/components/ProjectPlayer.vue b/playground/components/ProjectPlayer.vue index 8490b45c..cfa79f50 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -20,7 +20,23 @@ - +
+ +
+
+ {{ queued_questions[0]?.[0] }} +
+
+ + +
+
+

Instructions

@@ -31,14 +47,6 @@ {{ props.description }}
-
- {{ queued_questions[0]?.[0] }} -
-
- - -
-
{{ loadingMsg }} @@ -51,7 +59,7 @@ import { WasmFlags, } from "../../js/compiler/hyperquark.js"; import { instantiateProject } from "../lib/project-runner.js"; -import { ref, onMounted, reactive } from "vue"; +import { ref, onMounted, reactive, watch } from "vue"; import { getSettings } from "../lib/settings.js"; import { useDebugModeStore } from "../stores/debug.js"; @@ -75,6 +83,7 @@ let turbo = ref(false); let canvas = ref(null); let loadingMsg = ref("compiling project"); let loaded = ref(false); +let questionInput = ref(null); let greenFlag = () => null; let stop = () => null; @@ -101,16 +110,8 @@ const queued_questions = reactive([]); let question_response = ref(""); let mark_question_resolved = () => {}; -function set_mark_question_resolved(func) { - mark_question_resolved = func; -} - let setAnswer = () => {}; -const setSetAnswer = (_setAnswer) => { - setAnswer = _setAnswer; -}; - function submitQuestion() { setAnswer(question_response.value); question_response.value = ""; @@ -118,6 +119,12 @@ function submitQuestion() { mark_question_resolved(struct); } +watch(queued_questions, () => { + if (queued_questions.length > 0) { + questionInput.value.focus(); + } +}); + function queue_question(question, struct) { queued_questions.push([question, struct]); } @@ -237,14 +244,17 @@ onMounted(async () => { }, isDebug: () => debugModeStore.debug, queue_question, - set_mark_question_resolved, - setSetAnswer, + onStop: () => { + queued_questions.splice(0); + }, }); loaded.value = true; greenFlag = runner.greenFlag; stop = runner.stop; + setAnswer = runner.setAnswer; + mark_question_resolved = runner.mark_question_resolved; } catch (e) { declareError(e, true, "An error", "instantiating"); } @@ -255,12 +265,44 @@ onMounted(async () => { canvas { border: 1px solid black; background: white; - max-width: calc((100vw - 1rem) * 0.95); + width: 100%; + height: 100%; +} + +div#stage-container { float: left; margin-right: 1em; margin-bottom: 1.5em; width: 480px; height: 360px; + position: relative; + + & > div#question-div { + width: calc(100% - 1em); + position: absolute; + bottom: 0; + margin: 0.5em; + padding: 0.5em; + background: var(--color-background-soft); + border-radius: 5px; + box-sizing: border-box; + + & > div { + padding: 0; + margin-top: 0; + line-height: 1em; + margin-bottom: 0.4em; + } + + & > form { + display: flex; + + & > input { + flex-grow: 100; + border-radius: 5px; + } + } + } } div.instructions { diff --git a/playground/lib/project-runner.js b/playground/lib/project-runner.js index 80d2bdf1..18e70cd4 100644 --- a/playground/lib/project-runner.js +++ b/playground/lib/project-runner.js @@ -96,8 +96,7 @@ export async function instantiateProject({ onTimeout = () => null, importOverrides, queue_question, - set_mark_question_resolved, - setSetAnswer, + onStop = () => null, }) { if (isDebug() && typeof window === "object") window.open( @@ -181,12 +180,6 @@ export async function instantiateProject({ window.tick = tick; } - set_mark_question_resolved(mark_waiting_flag); - - setSetAnswer((answerText) => { - sensing_answer.value = answerText; - }); - try { // expose the module to devtools unreachable_dbg(); @@ -258,10 +251,15 @@ export async function instantiateProject({ for (let i = 0; i < threads.length; i++) { threads.set(i, null); } + onStop(); }, pause: () => { running = false; }, run, + mark_question_resolved: mark_waiting_flag, + setAnswer(answerText) { + sensing_answer.value = answerText; + }, }; } diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index 8b6d7b4f..be7401bc 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -1376,17 +1376,55 @@ fn from_normal_block( Weak::clone(project), ))?; should_break = true; - let next_step = generate_next_step_non_inlined( - block_info, - blocks, - context, - final_next_blocks.clone(), - flags, - )?; - vec![IrOpcode::sensing_askandwait(SensingAskandwaitFields { - poll_step, - next_step, - })] + if context.target().is_stage() { + let next_step = generate_next_step_non_inlined( + block_info, + blocks, + context, + final_next_blocks.clone(), + flags, + )?; + vec![IrOpcode::sensing_askandwait(SensingAskandwaitFields { + poll_step, + next_step, + })] + } else { + let next_step = generate_next_step_inlined( + block_info, + blocks, + context, + final_next_blocks.clone(), + flags, + )?; + let real_next_step = Step::new( + None, + context.clone(), + vec![ + IrOpcode::hq_text(HqTextFields("".into())), + IrOpcode::looks_say(LooksSayFields { + debug: false, + target_idx: context.target().index(), + }), + IrOpcode::hq_yield(HqYieldFields { + mode: YieldMode::Inline(next_step), + }), + ], + Weak::clone(project), + true, + ) + .clone_to_non_inlined(project)?; + vec![ + IrOpcode::looks_say(LooksSayFields { + debug: false, + target_idx: context.target().index(), + }), + IrOpcode::hq_text(HqTextFields("".into())), + IrOpcode::sensing_askandwait(SensingAskandwaitFields { + poll_step, + next_step: real_next_step, + }), + ] + } } BlockOpcode::control_wait => { let poll_step = context.project()?.new_owned_step( From 34dbd50e806965995d94ff85418129c2bf59fa58 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <--get> Date: Wed, 11 Feb 2026 23:33:03 +0000 Subject: [PATCH 05/46] disable automplete on askandwait input --- playground/components/ProjectPlayer.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/playground/components/ProjectPlayer.vue b/playground/components/ProjectPlayer.vue index cfa79f50..c0fa5dc3 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -32,6 +32,7 @@ name="answer" v-model="question_response" ref="questionInput" + autocomplete="off" /> From 486fd62f98cf733fa7a30f2591d58e9f1a7db6ec Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:19:13 +0000 Subject: [PATCH 06/46] make ProjectRunner a bit nicer by making it a class and EventTarget --- playground/components/ProjectPlayer.vue | 22 +- playground/lib/project-runner.js | 416 +++++++++--------- playground/lib/setup.js | 75 ++++ .../turbowarp-integration-execute.test.mjs | 11 +- 4 files changed, 299 insertions(+), 225 deletions(-) create mode 100644 playground/lib/setup.js diff --git a/playground/components/ProjectPlayer.vue b/playground/components/ProjectPlayer.vue index c0fa5dc3..5c5fd188 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -59,7 +59,7 @@ import { FinishedWasm, WasmFlags, } from "../../js/compiler/hyperquark.js"; -import { instantiateProject } from "../lib/project-runner.js"; +import { ProjectRunner } from "../lib/project-runner.js"; import { ref, onMounted, reactive, watch } from "vue"; import { getSettings } from "../lib/settings.js"; import { useDebugModeStore } from "../stores/debug.js"; @@ -229,7 +229,7 @@ onMounted(async () => { try { loadingMsg.value = "instantiating project"; - const runner = await instantiateProject({ + const runner = await ProjectRunner.init({ framerate: 30, turbo: turbo.value, wasm_bytes: wasmBytes, @@ -244,18 +244,20 @@ onMounted(async () => { return new RenderWebGL(canvas.value); }, isDebug: () => debugModeStore.debug, - queue_question, - onStop: () => { - queued_questions.splice(0); - }, }); loaded.value = true; - greenFlag = runner.greenFlag; - stop = runner.stop; - setAnswer = runner.setAnswer; - mark_question_resolved = runner.mark_question_resolved; + runner.addEventListener("stopped", () => queued_questions.splice(0)); + runner.addEventListener( + "queueQuestion", + ({ detail: { question, struct } }) => queue_question(question, struct), + ); + + greenFlag = runner.greenFlag.bind(runner); + stop = runner.stop.bind(runner); + setAnswer = runner.setAnswer.bind(runner); + mark_question_resolved = runner.mark_question_resolved.bind(runner); } catch (e) { declareError(e, true, "An error", "instantiating"); } diff --git a/playground/lib/project-runner.js b/playground/lib/project-runner.js index 18e70cd4..78707fbd 100644 --- a/playground/lib/project-runner.js +++ b/playground/lib/project-runner.js @@ -1,265 +1,261 @@ import { getSettings } from "./settings.js"; import { imports as baseImports } from "./imports.js"; -import { - setup as sharedSetup, - is_setup, - renderer as get_renderer, -} from "../../js/shared.ts"; +import { renderer as get_renderer } from "../../js/shared.ts"; import { WasmStringType } from "../../js/no-compiler/hyperquark.js"; +import { setup } from "./setup.js"; -function createSkin(renderer, type, layer, ...params) { - let drawableId = renderer.createDrawable(layer.toString()); - const realType = { - pen: "Pen", - text: "Text", - svg: "SVG", - }[type.toLowerCase()]; - let skin = renderer[`create${realType}Skin`](...params); - renderer.updateDrawableSkinId(drawableId, skin); - return [skin, drawableId]; +function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); } -async function setup( - makeRenderer, - project_json, - assets, - target_names, - queue_question, -) { - if (is_setup()) return; - - let renderer = await makeRenderer(); +function waitAnimationFrame() { + return new Promise((resolve) => { + if (typeof requestAnimationFrame === "function") + requestAnimationFrame(resolve); + else setTimeout(resolve, 1); + }); +} - renderer.getDrawable = (id) => renderer._allDrawables[id]; - renderer.getSkin = (id) => renderer._allSkins[id]; - renderer.createSkin = (type, layer, ...params) => - createSkin(renderer, type, layer, ...params); +export class ProjectRunner extends EventTarget { + #sensing_answer; + #mark_question_resolved_func; + #running = false; + #renderer; + #tick; + #timeout; + #framerate_wait; + #requests_refresh; + turbo; + #sensing_timer; + #threads_count; + flag_clicked; + #threads; - const costumes = project_json.targets.map((target, index) => - target.costumes.map(({ md5ext }) => assets[md5ext]), - ); + constructor({ + sensing_answer, + mark_question_resolved, + renderer, + tick, + timeout, + framerate_wait, + requests_refresh, + turbo, + threads_count, + flag_clicked, + threads, + sensing_timer, + }) { + super(); - if (typeof window === "object") { - window.renderer = renderer; + this.#sensing_answer = sensing_answer; + this.#mark_question_resolved_func = mark_question_resolved; + this.#renderer = renderer; + this.#tick = tick; + this.#timeout = timeout; + this.#framerate_wait = framerate_wait; + this.#requests_refresh = requests_refresh; + this.turbo = turbo; + this.#sensing_timer = sensing_timer; + this.#threads_count = threads_count; + this.flag_clicked = flag_clicked; + this.#threads = threads; } - renderer.setLayerGroupOrdering(["background", "video", "pen", "sprite"]); - //window.open(URL.createObjectURL(new Blob([wasm_bytes], { type: "octet/stream" }))); - const pen_skin = renderer.createSkin("pen", "pen")[0]; - - const target_skins = project_json.targets.map((target, index) => { - const realCostume = target.costumes[target.currentCostume]; - const costume = costumes[index][target.currentCostume]; - if (costume.dataFormat.toLowerCase() !== "svg") { - throw new Error("todo: non-svg costumes"); - } - - const [skin, drawableId] = renderer.createSkin( - costume.dataFormat, - "sprite", - costume.data, - [realCostume.rotationCenterX, realCostume.rotationCenterY], - ); - const drawable = renderer.getDrawable(drawableId); - if (!target.is_stage) { - drawable.updateVisible(!!target.visible); - drawable.updatePosition([target.x, target.y]); - drawable.updateDirection(target.direction); - drawable.updateScale([target.size, target.size]); - } - return [skin, drawableId]; - }); - console.log(target_skins); - - sharedSetup( + static async init({ + framerate = 30, + turbo, + wasm_bytes, target_names, - renderer, - pen_skin, - target_skins, - costumes, - queue_question, - ); -} + strings, + project_json, + assets, + makeRenderer, + isDebug = () => false, + timeout, + importOverrides, + }) { + if (isDebug() && typeof window === "object") + window.open( + URL.createObjectURL( + new Blob([wasm_bytes], { type: "application/wasm" }), + ), + ); -// @ts-ignore -export async function instantiateProject({ - framerate = 30, - turbo, - wasm_bytes, - target_names, - strings, - project_json, - assets, - makeRenderer, - isDebug = () => false, - timeout, - onTimeout = () => null, - importOverrides, - queue_question, - onStop = () => null, -}) { - if (isDebug() && typeof window === "object") - window.open( - URL.createObjectURL(new Blob([wasm_bytes], { type: "application/wasm" })), + await setup( + makeRenderer, + project_json, + assets, + target_names, + (question, struct) => { + // runner doesn't exist yet, but it will by the time the function is called + runner.dispatchEvent( + new CustomEvent("queueQuestion", { detail: { question, struct } }), + ); + }, ); - await setup(makeRenderer, project_json, assets, target_names, queue_question); + const renderer = get_renderer(); - const renderer = get_renderer(); + const framerate_wait = Math.round(1000 / framerate); - console.log("project setup complete"); + const settings = getSettings(); + const builtins = [ + ...(WasmStringType[settings.string_type] === "JsStringBuiltins" + ? ["js-string"] + : []), + ]; - const framerate_wait = Math.round(1000 / framerate); + const imports = Object.assign(baseImports, { + "": Object.fromEntries(strings.map((string) => [string, string])), + }); - const settings = getSettings(); - const builtins = [ - ...(WasmStringType[settings.string_type] === "JsStringBuiltins" - ? ["js-string"] - : []), - ]; + for (const [module, _obj] of Object.entries(importOverrides ?? {})) { + for (const [name, val] of Object.entries(importOverrides[module])) { + imports[module][name] = val; + } + } - const imports = Object.assign(baseImports, { - "": Object.fromEntries(strings.map((string) => [string, string])), - }); + try { + if ( + !WebAssembly.validate(wasm_bytes, { + builtins, + }) + ) { + throw Error(); + } + } catch { + try { + new WebAssembly.Module(wasm_bytes); + throw new Error("invalid WASM module"); + } catch (e) { + throw new Error("invalid WASM module: " + e.message); + } + } - console.log(importOverrides); + let { instance } = await WebAssembly.instantiate(wasm_bytes, imports, { + builtins, + importedStringConstants: "", + }); - for (const [module, obj] of Object.entries(importOverrides ?? {})) { - for (const [name, val] of Object.entries(importOverrides[module])) { - imports[module][name] = val; - } - } + const { + flag_clicked, + tick, + memory, + threads_count, + requests_refresh, + threads, + unreachable_dbg, + sensing_timer, + mark_waiting_flag, + sensing_answer, + } = instance.exports; - try { - if ( - !WebAssembly.validate(wasm_bytes, { - builtins, - }) - ) { - throw Error(); + if (typeof window === "object") { + window.memory = memory; + window.flag_clicked = flag_clicked; + window.tick = tick; } - } catch { + try { - new WebAssembly.Module(wasm_bytes); - throw new Error("invalid WASM module"); - } catch (e) { - throw new Error("invalid WASM module: " + e.message); + // expose the module to devtools + unreachable_dbg(); + } catch (error) { + console.info("synthetic error to expose wasm module to devtools:", error); } - } - function sleep(ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - } - function waitAnimationFrame() { - return new Promise((resolve) => { - if (typeof requestAnimationFrame === "function") - requestAnimationFrame(resolve); - else setTimeout(resolve, 1); + + const runner = new ProjectRunner({ + sensing_answer, + mark_question_resolved: mark_waiting_flag, + renderer, + tick, + timeout, + framerate_wait, + requests_refresh, + turbo, + sensing_timer, + threads_count, + flag_clicked, + threads, }); - } - let { instance } = await WebAssembly.instantiate(wasm_bytes, imports, { - builtins, - importedStringConstants: "", - }); - const { - flag_clicked, - tick, - memory, - threads_count, - requests_refresh, - threads, - unreachable_dbg, - sensing_timer, - mark_waiting_flag, - sensing_answer, - } = instance.exports; - if (typeof window === "object") { - window.memory = memory; - window.flag_clicked = flag_clicked; - window.tick = tick; - } - try { - // expose the module to devtools - unreachable_dbg(); - } catch (error) { - console.info("synthetic error to expose wasm module to devtools:", error); + return runner; } - let running = false; - - const run = async () => { + async run() { console.log("running"); - if (running) return; + if (this.#running) return; - running = true; + this.#running = true; - renderer.draw(); + this.#renderer.draw(); let startTime = Date.now(); let previousTickStartTime = startTime; - $outertickloop: while (running) { - if (timeout && Date.now() - startTime > timeout) { - return onTimeout(); + $outertickloop: while (this.#running) { + if (this.#timeout && Date.now() - startTime > this.#timeout) { + return this.dispatchEcent(new CustomEvent("timeout")); } let thisTickStartTime = Date.now(); - if (typeof sensing_timer !== "undefined") { - sensing_timer.value += + if (typeof this.#sensing_timer !== "undefined") { + this.#sensing_timer.value += (thisTickStartTime - previousTickStartTime) / 1000; } previousTickStartTime = thisTickStartTime; do { - tick(); - if (threads_count.value === 0) { + this.#tick(); + if (this.#threads_count.value === 0) { break $outertickloop; } } while ( - Date.now() - thisTickStartTime < framerate_wait * 0.8 && - !turbo && - requests_refresh.value === 0 + Date.now() - thisTickStartTime < this.#framerate_wait * 0.8 && + !this.turbo && + this.#requests_refresh.value === 0 ); - requests_refresh.value = 0; - renderer.draw(); - if (framerate_wait > 0) { + this.#requests_refresh.value = 0; + this.#renderer.draw(); + if (this.#framerate_wait > 0) { await sleep( - Math.max(0, framerate_wait - (Date.now() - thisTickStartTime)), + Math.max(0, this.#framerate_wait - (Date.now() - thisTickStartTime)), ); } else { await waitAnimationFrame(); } } - renderer.draw(); - running = false; + this.#renderer.draw(); + this.#running = false; console.log("project stopped (or maybe paused)"); - }; + } - return { - greenFlag: () => { - console.log("green flag clicked"); - if (typeof sensing_timer !== "undefined") { - sensing_timer.value = 0.0; - } - flag_clicked(); - run(); - }, - flag_clicked, - stop: () => { - console.log("stopping"); - threads_count.value = 0; - running = false; - for (let i = 0; i < threads.length; i++) { - threads.set(i, null); - } - onStop(); - }, - pause: () => { - running = false; - }, - run, - mark_question_resolved: mark_waiting_flag, - setAnswer(answerText) { - sensing_answer.value = answerText; - }, - }; + greenFlag() { + console.log("green flag clicked"); + if (typeof this.#sensing_timer !== "undefined") { + this.#sensing_timer.value = 0.0; + } + this.flag_clicked(); + this.run(); + } + + pause() { + this.#running = false; + } + + stop() { + console.log("stopping"); + this.#threads_count.value = 0; + this.#running = false; + for (let i = 0; i < this.#threads.length; i++) { + this.#threads.set(i, null); + } + this.dispatchEvent(new CustomEvent("stopped")); + } + + mark_question_resolved(struct) { + this.#mark_question_resolved_func(struct); + } + + setAnswer(answerText) { + this.#sensing_answer.value = answerText; + } } diff --git a/playground/lib/setup.js b/playground/lib/setup.js new file mode 100644 index 00000000..9d0f1ae4 --- /dev/null +++ b/playground/lib/setup.js @@ -0,0 +1,75 @@ +import { setup as sharedSetup, is_setup } from "../../js/shared.ts"; + +function createSkin(renderer, type, layer, ...params) { + let drawableId = renderer.createDrawable(layer.toString()); + const realType = { + pen: "Pen", + text: "Text", + svg: "SVG", + }[type.toLowerCase()]; + let skin = renderer[`create${realType}Skin`](...params); + renderer.updateDrawableSkinId(drawableId, skin); + return [skin, drawableId]; +} + +export async function setup( + makeRenderer, + project_json, + assets, + target_names, + queue_question, +) { + if (is_setup()) return; + + let renderer = await makeRenderer(); + + renderer.getDrawable = (id) => renderer._allDrawables[id]; + renderer.getSkin = (id) => renderer._allSkins[id]; + renderer.createSkin = (type, layer, ...params) => + createSkin(renderer, type, layer, ...params); + + const costumes = project_json.targets.map((target, index) => + target.costumes.map(({ md5ext }) => assets[md5ext]), + ); + + if (typeof window === "object") { + window.renderer = renderer; + } + renderer.setLayerGroupOrdering(["background", "video", "pen", "sprite"]); + //window.open(URL.createObjectURL(new Blob([wasm_bytes], { type: "octet/stream" }))); + const pen_skin = renderer.createSkin("pen", "pen")[0]; + + const target_skins = project_json.targets.map((target, index) => { + const realCostume = target.costumes[target.currentCostume]; + const costume = costumes[index][target.currentCostume]; + if (costume.dataFormat.toLowerCase() !== "svg") { + throw new Error("todo: non-svg costumes"); + } + + const [skin, drawableId] = renderer.createSkin( + costume.dataFormat, + "sprite", + costume.data, + [realCostume.rotationCenterX, realCostume.rotationCenterY], + ); + + const drawable = renderer.getDrawable(drawableId); + if (!target.is_stage) { + drawable.updateVisible(!!target.visible); + drawable.updatePosition([target.x, target.y]); + drawable.updateDirection(target.direction); + drawable.updateScale([target.size, target.size]); + } + return [skin, drawableId]; + }); + console.log(target_skins); + + sharedSetup( + target_names, + renderer, + pen_skin, + target_skins, + costumes, + queue_question, + ); +} diff --git a/test/integration/turbowarp-integration-execute.test.mjs b/test/integration/turbowarp-integration-execute.test.mjs index 3bcd31ed..13088fcb 100644 --- a/test/integration/turbowarp-integration-execute.test.mjs +++ b/test/integration/turbowarp-integration-execute.test.mjs @@ -15,7 +15,7 @@ import { describe, test } from "vitest"; import { imports as baseImports } from "../../js/imports.ts"; import { unpackProject } from "../../playground/lib/project-loader.js"; -import { instantiateProject } from "../../playground/lib/project-runner.js"; +import { ProjectRunner } from "../../playground/lib/project-runner.js"; import { sb3_to_wasm, WasmFlags } from "../../js/compiler/hyperquark.js"; import { WasmStringType } from "../../js/no-compiler/hyperquark"; import { defaultSettings } from "../../playground/lib/settings.js"; @@ -172,7 +172,7 @@ describe("Integration tests", () => { // todo: run wasm-opt if specified in flags? // Run the project and once all threads are complete check the results. - const runner = await instantiateProject({ + const runner = await ProjectRunner.init({ wasm_bytes: project_wasm.wasm_bytes, target_names: project_wasm.target_names, project_json, @@ -190,9 +190,6 @@ describe("Integration tests", () => { }, }, ), - onTimeout: () => { - throw new Error(`Timeout waiting for threads to complete: ${uri}`); - }, importOverrides: { looks: { say_string: (string) => reportVmResult(string), @@ -201,6 +198,10 @@ describe("Integration tests", () => { makeRenderer: makeTestRenderer, }); + runner.addEventListener("timeout", () => { + throw new Error(`Timeout waiting for threads to complete: ${uri}`); + }); + runner.flag_clicked(); await runner.run(); From 740db39f57981e465e6b257d0408e5b134a19ac4 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Fri, 13 Feb 2026 01:11:23 +0000 Subject: [PATCH 07/46] implement event_whenthisspriteclicked --- js/shared.ts | 9 ++ playground/components/ProjectPlayer.vue | 22 +++++ playground/lib/project-runner.js | 104 +++++++++++++++------- playground/lib/setup.js | 3 + src/ir/event.rs | 3 +- src/ir/thread.rs | 7 +- src/wasm/project.rs | 112 ++++++++++++++++++++---- 7 files changed, 210 insertions(+), 50 deletions(-) diff --git a/js/shared.ts b/js/shared.ts index d45b3217..fdb52866 100644 --- a/js/shared.ts +++ b/js/shared.ts @@ -6,6 +6,7 @@ let _pen_skin: number; let _target_skins: Array<[number, number]>; let _costumes: Array>; let _queue_question: (question: string, struct: object) => void = () => {}; +let _stageIndex: number; type Costume = { data: string; @@ -19,6 +20,7 @@ export function setup( target_skins: Array<[number, number]>, costumes: Array>, queue_question: (question: string, struct: object) => void, + stageIndex: number, ) { _target_names = target_names; _target_bubbles = _target_names.map((_) => null); @@ -28,6 +30,7 @@ export function setup( _target_skins = target_skins; _costumes = costumes; _queue_question = queue_question; + _stageIndex = stageIndex; _setup = true; } @@ -66,6 +69,11 @@ export function costumes(): Array> { return _costumes; } +export function stageIndex(): number { + check_setup(); + return _stageIndex; +} + export function update_bubble( target_index: number, verb: "say" | "think", @@ -81,6 +89,7 @@ export function update_bubble( false, ); } else if (text == "") { + if (!_target_bubbles[target_index]) return; _renderer.destroyDrawable(_target_bubbles[target_index][1], "sprite"); _renderer.destroySkin(_target_bubbles[target_index][0]); _target_bubbles[target_index] = null; diff --git a/playground/components/ProjectPlayer.vue b/playground/components/ProjectPlayer.vue index 5c5fd188..d0b14975 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -254,6 +254,28 @@ onMounted(async () => { ({ detail: { question, struct } }) => queue_question(question, struct), ); + const onMouseMove = (e, isDown) => { + const rect = canvas.value.getBoundingClientRect(); + runner.onMouseMove({ + clientX: e.clientX, + clientY: e.clientY, + rect, + isDown, + }); + }; + + document.addEventListener("mousemove", (e) => { + onMouseMove(e); + }); + canvas.value.addEventListener("mousedown", (e) => { + onMouseMove(e, true); + e.preventDefault(); + }); + canvas.value.addEventListener("mouseup", (e) => { + onMouseMove(e, false); + e.preventDefault(); + }); + greenFlag = runner.greenFlag.bind(runner); stop = runner.stop.bind(runner); setAnswer = runner.setAnswer.bind(runner); diff --git a/playground/lib/project-runner.js b/playground/lib/project-runner.js index 78707fbd..00ac811c 100644 --- a/playground/lib/project-runner.js +++ b/playground/lib/project-runner.js @@ -1,6 +1,10 @@ import { getSettings } from "./settings.js"; import { imports as baseImports } from "./imports.js"; -import { renderer as get_renderer } from "../../js/shared.ts"; +import { + renderer as get_renderer, + stageIndex, + target_skins, +} from "../../js/shared.ts"; import { WasmStringType } from "../../js/no-compiler/hyperquark.js"; import { setup } from "./setup.js"; @@ -32,35 +36,30 @@ export class ProjectRunner extends EventTarget { #threads_count; flag_clicked; #threads; + #mouseX; + #mouseY; + #mouseDown; + #triggerSpriteClicked; - constructor({ - sensing_answer, - mark_question_resolved, - renderer, - tick, - timeout, - framerate_wait, - requests_refresh, - turbo, - threads_count, - flag_clicked, - threads, - sensing_timer, - }) { + constructor({ renderer, timeout, framerate_wait, turbo, exports }) { super(); - this.#sensing_answer = sensing_answer; - this.#mark_question_resolved_func = mark_question_resolved; + this.#sensing_answer = exports.sensing_answer; + this.#mark_question_resolved_func = exports.mark_waiting_flag; this.#renderer = renderer; - this.#tick = tick; + this.#tick = exports.tick; this.#timeout = timeout; this.#framerate_wait = framerate_wait; - this.#requests_refresh = requests_refresh; + this.#requests_refresh = exports.requests_refresh; this.turbo = turbo; - this.#sensing_timer = sensing_timer; - this.#threads_count = threads_count; - this.flag_clicked = flag_clicked; - this.#threads = threads; + this.#sensing_timer = exports.sensing_timer; + this.#threads_count = exports.threads_count; + this.flag_clicked = exports.flag_clicked; + this.#threads = exports.threads; + this.#mouseX = exports.mouseX ?? { value: 0 }; + this.#mouseY = exports.mouseY ?? { value: 0 }; + this.#mouseDown = exports.mouseDown ?? { value: false }; + this.#triggerSpriteClicked = exports.trigger_sprite_clicked; } static async init({ @@ -150,6 +149,9 @@ export class ProjectRunner extends EventTarget { sensing_timer, mark_waiting_flag, sensing_answer, + mouseX, + mouseY, + mouseDown, } = instance.exports; if (typeof window === "object") { @@ -166,18 +168,11 @@ export class ProjectRunner extends EventTarget { } const runner = new ProjectRunner({ - sensing_answer, - mark_question_resolved: mark_waiting_flag, renderer, - tick, + exports: instance.exports, timeout, framerate_wait, - requests_refresh, turbo, - sensing_timer, - threads_count, - flag_clicked, - threads, }); return runner; @@ -258,4 +253,51 @@ export class ProjectRunner extends EventTarget { setAnswer(answerText) { this.#sensing_answer.value = answerText; } + + onMouseMove({ clientX, clientY, rect, isDown }) { + const x = clamp((clientX - rect.left) / rect.width, 0, 1) * 480 - 240; + const y = clamp((clientY - rect.top) / rect.height, 0, 1) * 360 - 180; + this.#mouseX.value = x; + this.#mouseY.value = y; + + if (typeof isDown !== "undefined") { + const prevIsDown = this.#mouseDown.value; + if (prevIsDown && !isDown) { + // TODO: update 'this sprite clicked?' values to be not clicked + } + if (!prevIsDown && isDown) { + const clickedTarget = this.#pickMouseOverTarget( + clientX - rect.left, + clientY - rect.top, + ); + this.#triggerSpriteClicked?.(clickedTarget); + if (!this.#running) this.run(); + } + + this.#mouseDown.value = isDown; + } + + if (this.#mouseDown.value) { + // TODO: update 'this sprite clicked?' values to be maybe clicked depending on mouse position + } + } + + #pickMouseOverTarget(x, y) { + // adapted from https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1f/src/io/mouse.js#L40 + // (licensed under BSD-3.0 - see https://raw.githubusercontent.com/scratchfoundation/scratch-vm/8dbcc1f/LICENSE) + const drawableID = this.#renderer.pick(x, y); + const targetSkins = target_skins(); + for (let i = 0; i < targetSkins.length; i++) { + const thisDrawableID = targetSkins[i][1]; + if (thisDrawableID === drawableID) { + return i; + } + } + // Return the stage if no target was found + return stageIndex(); + } +} + +function clamp(val, min, max) { + return Math.max(Math.min(val, max), min); } diff --git a/playground/lib/setup.js b/playground/lib/setup.js index 9d0f1ae4..62b8ce1f 100644 --- a/playground/lib/setup.js +++ b/playground/lib/setup.js @@ -64,6 +64,8 @@ export async function setup( }); console.log(target_skins); + const stageIndex = project_json.targets.findIndex((target) => target.isStage); + sharedSetup( target_names, renderer, @@ -71,5 +73,6 @@ export async function setup( target_skins, costumes, queue_question, + stageIndex, ); } diff --git a/src/ir/event.rs b/src/ir/event.rs index b6c59d59..5633c992 100644 --- a/src/ir/event.rs +++ b/src/ir/event.rs @@ -1,6 +1,7 @@ // Ord is required to be used in a BTreeMap; Ord requires PartialOrd, Eq and PartialEq #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Event { - FlagCLicked, + FlagClicked, Broadcast(Box), + SpriteClicked(u32), } diff --git a/src/ir/thread.rs b/src/ir/thread.rs index 58da3388..606330be 100644 --- a/src/ir/thread.rs +++ b/src/ir/thread.rs @@ -36,7 +36,7 @@ impl Thread { }; #[expect(clippy::wildcard_enum_match_arm, reason = "too many variants to match")] let event = match block_info.opcode { - BlockOpcode::event_whenflagclicked => Event::FlagCLicked, + BlockOpcode::event_whenflagclicked => Event::FlagClicked, BlockOpcode::event_whenbroadcastreceived => { let sb3::Field::ValueId(val, _id) = block_info.fields.get("BROADCAST_OPTION").ok_or_else(|| { @@ -57,11 +57,12 @@ impl Thread { }; Event::Broadcast(name) } + BlockOpcode::event_whenthisspriteclicked | BlockOpcode::event_whenstageclicked => { + Event::SpriteClicked(target.index()) + } BlockOpcode::event_whenbackdropswitchesto | BlockOpcode::event_whengreaterthan | BlockOpcode::event_whenkeypressed - | BlockOpcode::event_whenstageclicked - | BlockOpcode::event_whenthisspriteclicked | BlockOpcode::event_whentouchingobject => { hq_todo!("unimplemented event {:?}", block_info.opcode) } diff --git a/src/wasm/project.rs b/src/wasm/project.rs index 7b55bc9b..6499ad1c 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -373,14 +373,15 @@ impl WasmProject { ) } + #[expect(clippy::needless_pass_by_value, reason = "annoying to borrow a box")] fn finish_event( &self, - export_name: &str, + export_name: Box, indices: &[u32], funcs: &mut FunctionSection, codes: &mut CodeSection, exports: &mut ExportSection, - ) -> HQResult<()> { + ) -> HQResult { let mut func = Function::new(vec![]); let threads_count = self.threads_count_global()?; @@ -445,12 +446,12 @@ impl WasmProject { funcs.function(self.registries().types().function(vec![], vec![])?); codes.function(&func); exports.export( - export_name, + &export_name, ExportKind::Func, self.imported_func_count()? + funcs.len() - 1, ); - Ok(()) + Ok(self.imported_func_count()? + funcs.len() - 1) } fn finish_events( @@ -459,17 +460,98 @@ impl WasmProject { codes: &mut CodeSection, exports: &mut ExportSection, ) -> HQResult<()> { - for (event, indices) in &self.events { - self.finish_event( - match event { - Event::FlagCLicked => "flag_clicked", - Event::Broadcast(_) => continue, // broadcasts handled in the sender blocks - }, - indices, - funcs, - codes, - exports, - )?; + let event_funcs = self + .events + .iter() + .map(|(event, indices)| { + Ok(Some(( + event, + self.finish_event( + match event { + Event::FlagClicked => "flag_clicked".into(), + Event::Broadcast(_) => return Ok(None), // broadcasts handled in the sender blocks + Event::SpriteClicked(index) => { + format!("spriteClicked{index}").into_boxed_str() + } + }, + indices, + funcs, + codes, + exports, + )?, + ))) + }) + .collect::>>()? + .into_iter() + .flatten() + .collect::>(); + let sprite_clicked_indices: Box<[i32]> = self + .events + .keys() + .filter_map(|e| { + #[expect(clippy::redundant_else, reason = "false positive")] + if let Event::SpriteClicked(index) = e { + Some( + i32::try_from(*index) + .map_err(|_| make_hq_bug!("target index out of bounds")), + ) + } else { + None + } + }) + .collect::>()?; + if !sprite_clicked_indices.is_empty() { + let mut sprite_clicked_func = Function::new([]); + let sprite_clicked_instrs: Vec<_> = sprite_clicked_indices + .iter() + .flat_map(|index| { + wasm![ + LocalGet(0), + I32Const(*index), + I32Eq, + If(WasmBlockType::Empty), + Call( + event_funcs[&Event::SpriteClicked( + #[expect( + clippy::unwrap_used, + reason = "guaranteed to succeed because i32 was originally a \ + u32" + )] + (*index).try_into().unwrap() + )] + ), + Return, + End, + ] + }) + .chain(wasm![End]) + .collect(); + for instruction in sprite_clicked_instrs { + for real_instruction in instruction.eval( + &self.events, + self.registries().types(), + self.threads_count_global()?, + self.spawn_new_thread_func()?, + self.spawn_thread_in_stack_func()?, + self.threads_table_index()?, + self.imported_func_count()?, + self.static_func_count()?, + self.imported_global_count()?, + )? { + sprite_clicked_func.instruction(&real_instruction); + } + } + funcs.function( + self.registries() + .types() + .function(vec![ValType::I32], vec![])?, + ); + codes.function(&sprite_clicked_func); + exports.export( + "trigger_sprite_clicked", + ExportKind::Func, + self.imported_func_count()? + funcs.len() - 1, + ); } Ok(()) From 86ec4c1ecd23c8ae91c05abf4a6d40efe88b5438 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Fri, 13 Feb 2026 01:22:19 +0000 Subject: [PATCH 08/46] add hat blocks to implemented opcode list --- opcodes.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/opcodes.mjs b/opcodes.mjs index fe046a78..68d90b37 100644 --- a/opcodes.mjs +++ b/opcodes.mjs @@ -7,6 +7,11 @@ const opcodes = [ .match(/BlockOpcode::[a-zA-Z_0-9]+? (?==>)/g) .map((op) => op.replace("BlockOpcode::", "").trim()), ), + "event_whenflagclicked", + "event_whenbroadcastreceived", + "event_whenthisspriteclicked", + "events_whenstageclicked", + "procedures_definition", ].sort(); await writeFile( "/tmp/hq-build/js/opcodes.js", From 561e9362adff56dd7433fbe53bfabf449315d85f Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:55:02 +0000 Subject: [PATCH 09/46] looks_nextcostume: loop to beginning once end is reached --- src/ir/blocks.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index be7401bc..58ce9071 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -2231,6 +2231,14 @@ fn from_normal_block( BlockOpcode::looks_nextcostume => vec![ IrOpcode::looks_costumenumber, IrOpcode::hq_integer(HqIntegerFields(1)), + IrOpcode::operator_add, + IrOpcode::hq_integer(HqIntegerFields( + context.target().costumes().len() + .try_into().map_err(|_| { + make_hq_bug!("costumes length out of bounds") + })?, + )), + IrOpcode::operator_modulo, IrOpcode::looks_switchcostumeto, ], BlockOpcode::looks_costume => { From 814a5ef916629a8e4ad599a696790fe7f56b16aa Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:45:45 +0000 Subject: [PATCH 10/46] add backdrop switching --- js/looks/switchbackdropto.ts | 8 ++ playground/lib/project-runner.js | 1 + playground/lib/setup.js | 9 +- src/instructions/looks.rs | 2 + src/instructions/looks/backdropnumber.rs | 36 +++++++ src/instructions/looks/switchbackdropto.rs | 50 ++++++++++ src/instructions/looks/switchcostumeto.rs | 13 +-- src/ir/blocks.rs | 104 ++++++++++++++++++--- src/ir/project.rs | 41 +++++++- 9 files changed, 243 insertions(+), 21 deletions(-) create mode 100644 js/looks/switchbackdropto.ts create mode 100644 src/instructions/looks/backdropnumber.rs create mode 100644 src/instructions/looks/switchbackdropto.rs diff --git a/js/looks/switchbackdropto.ts b/js/looks/switchbackdropto.ts new file mode 100644 index 00000000..7783782e --- /dev/null +++ b/js/looks/switchbackdropto.ts @@ -0,0 +1,8 @@ +import { renderer, costumes, target_skins, stageIndex } from "../shared"; + +export function switchbackdropto(costume_num: number) { + console.log("switch backdrop to", costume_num); + console.log(stageIndex(), costumes()); + const costume = costumes()[stageIndex()][costume_num]; + renderer().getSkin(target_skins()[stageIndex()][0]).setSVG(costume.data); +} diff --git a/playground/lib/project-runner.js b/playground/lib/project-runner.js index 00ac811c..70cd5bb1 100644 --- a/playground/lib/project-runner.js +++ b/playground/lib/project-runner.js @@ -218,6 +218,7 @@ export class ProjectRunner extends EventTarget { await waitAnimationFrame(); } } + await waitAnimationFrame(); this.#renderer.draw(); this.#running = false; console.log("project stopped (or maybe paused)"); diff --git a/playground/lib/setup.js b/playground/lib/setup.js index 62b8ce1f..e3d406a8 100644 --- a/playground/lib/setup.js +++ b/playground/lib/setup.js @@ -48,17 +48,22 @@ export async function setup( const [skin, drawableId] = renderer.createSkin( costume.dataFormat, - "sprite", + target.isStage ? "background" : "sprite", costume.data, [realCostume.rotationCenterX, realCostume.rotationCenterY], ); const drawable = renderer.getDrawable(drawableId); - if (!target.is_stage) { + if (!target.isStage) { drawable.updateVisible(!!target.visible); drawable.updatePosition([target.x, target.y]); drawable.updateDirection(target.direction); drawable.updateScale([target.size, target.size]); + } else { + drawable.updateVisible(true); + drawable.updatePosition([0, 0]); + drawable.updateDirection(90); + drawable.updateScale([100, 100]); } return [skin, drawableId]; }); diff --git a/src/instructions/looks.rs b/src/instructions/looks.rs index 1b86638a..200dbc39 100644 --- a/src/instructions/looks.rs +++ b/src/instructions/looks.rs @@ -1,7 +1,9 @@ +pub mod backdropnumber; pub mod costumenumber; pub mod say; pub mod setsizeto; pub mod setvisible; pub mod size; +pub mod switchbackdropto; pub mod switchcostumeto; pub mod think; diff --git a/src/instructions/looks/backdropnumber.rs b/src/instructions/looks/backdropnumber.rs new file mode 100644 index 00000000..fb677d80 --- /dev/null +++ b/src/instructions/looks/backdropnumber.rs @@ -0,0 +1,36 @@ +use wasm_encoder::MemArg; + +use super::super::prelude::*; +use crate::wasm::mem_layout; + +pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { + let offset = mem_layout::stage::COSTUME; + + Ok(wasm![ + I32Const(0), + I32Load(MemArg { + offset: offset.into(), + align: 2, + memory_index: 0, + }), + ]) +} + +pub fn acceptable_inputs() -> HQResult> { + Ok(Rc::from([])) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { + Ok(ReturnType::Singleton(IrType::IntPos)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +pub const fn const_fold( + _inputs: &[ConstFoldItem], + _state: &mut ConstFoldState, +) -> HQResult { + Ok(NotFoldable) +} + +crate::instructions_test! {tests; looks_backdropnumber; ; } diff --git a/src/instructions/looks/switchbackdropto.rs b/src/instructions/looks/switchbackdropto.rs new file mode 100644 index 00000000..c35dbf6e --- /dev/null +++ b/src/instructions/looks/switchbackdropto.rs @@ -0,0 +1,50 @@ +use wasm_encoder::MemArg; + +use super::super::prelude::*; +use crate::wasm::mem_layout; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + let func_index = func.registries().external_functions().register( + ("looks", "switchbackdropto".into()), + (vec![ValType::I32], vec![]), + )?; + let offset = mem_layout::stage::COSTUME; + + let local_index = func.local(ValType::I32)?; + Ok(if IrType::QuasiInt.contains(inputs[0]) { + wasm![ + LocalSet(local_index), + I32Const(0), + LocalGet(local_index), + I32Store(MemArg { + offset: offset.into(), + align: 2, + memory_index: 0, + }), + LocalGet(local_index), + Call(func_index), + ] + } else { + hq_todo!("non-integer input types for looks_switchbackdropto") + }) +} + +pub fn acceptable_inputs() -> HQResult> { + // TODO: accept non-integer values (try to find costume name) + Ok(Rc::from([IrType::Int])) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { + Ok(ReturnType::None) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = true; + +pub const fn const_fold( + _inputs: &[ConstFoldItem], + _state: &mut ConstFoldState, +) -> HQResult { + Ok(NotFoldable) +} + +crate::instructions_test! {tests; looks_switchbackdropto; t ; } diff --git a/src/instructions/looks/switchcostumeto.rs b/src/instructions/looks/switchcostumeto.rs index 14d57ee2..dae9ba69 100644 --- a/src/instructions/looks/switchcostumeto.rs +++ b/src/instructions/looks/switchcostumeto.rs @@ -12,13 +12,14 @@ pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult index, - StepTarget::Stage => 0, + let offset = match func.target() { + StepTarget::Sprite(index) => { + mem_layout::stage::BLOCK_SIZE + + index * mem_layout::sprite::BLOCK_SIZE + + mem_layout::sprite::COSTUME + } + StepTarget::Stage => mem_layout::stage::COSTUME, }; - let offset = mem_layout::stage::BLOCK_SIZE - + wasm_target_index * mem_layout::sprite::BLOCK_SIZE - + mem_layout::sprite::COSTUME; let local_index = func.local(ValType::I32)?; Ok(if IrType::QuasiInt.contains(inputs[0]) { diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index 58ce9071..d590b28a 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -250,6 +250,7 @@ pub fn input_names(block_info: &BlockInfo, context: &StepContext) -> HQResult HQResult vec![], + | BlockOpcode::sensing_answer + | BlockOpcode::looks_backdropnumbername + | BlockOpcode::looks_nextbackdrop => vec![], BlockOpcode::sensing_askandwait => vec!["QUESTION"], BlockOpcode::event_broadcast | BlockOpcode::event_broadcastandwait => { vec!["BROADCAST_INPUT"] @@ -283,6 +286,7 @@ pub fn input_names(block_info: &BlockInfo, context: &StepContext) -> HQResult vec!["TIMES"], BlockOpcode::operator_length => vec!["STRING"], BlockOpcode::looks_switchcostumeto => vec!["COSTUME"], + BlockOpcode::looks_switchbackdropto => vec!["BACKDROP"], BlockOpcode::looks_setsizeto | BlockOpcode::pen_setPenSizeTo => vec!["SIZE"], BlockOpcode::looks_changesizeby => vec!["CHANGE"], BlockOpcode::pen_setPenColorToColor => vec!["COLOR"], @@ -678,6 +682,8 @@ fn generate_if_else( this_project.global_variables().clone(), this_project.global_lists().clone(), Box::from(this_project.broadcasts()), + 0, + vec![], )); let dummy_target = Rc::new(Target::new( false, @@ -2202,6 +2208,9 @@ fn from_normal_block( IrOpcode::looks_setsizeto, ], BlockOpcode::looks_switchcostumeto => vec![IrOpcode::looks_switchcostumeto], + BlockOpcode::looks_switchbackdropto => { + vec![IrOpcode::looks_switchbackdropto] + } BlockOpcode::looks_costumenumbername => { let (sb3::Field::Value((val,)) | sb3::Field::ValueId(val, _)) = block_info.fields.get("NUMBER_NAME").ok_or_else(|| { @@ -2228,19 +2237,90 @@ fn from_normal_block( _ => hq_bad_proj!("invalid value for NUMBER_NAME field"), } } - BlockOpcode::looks_nextcostume => vec![ - IrOpcode::looks_costumenumber, - IrOpcode::hq_integer(HqIntegerFields(1)), - IrOpcode::operator_add, - IrOpcode::hq_integer(HqIntegerFields( - context.target().costumes().len() - .try_into().map_err(|_| { + BlockOpcode::looks_backdropnumbername => { + let (sb3::Field::Value((val,)) | sb3::Field::ValueId(val, _)) = + block_info.fields.get("NUMBER_NAME").ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - missing field NUMBER_NAME" + ) + })?; + let sb3::VarVal::String(number_name) = + val.clone().ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - null backdrop name for \ + NUMBER_NAME field" + ) + })? + else { + hq_bad_proj!( + "invalid project.json - NUMBER_NAME field is not of type \ + String" + ); + }; + match &*number_name { + "number" => vec![IrOpcode::looks_backdropnumber], + "name" => hq_todo!("backdrop name"), + _ => hq_bad_proj!("invalid value for NUMBER_NAME field"), + } + } + BlockOpcode::looks_backdrops => { + let (sb3::Field::Value((val,)) | sb3::Field::ValueId(val, _)) = + block_info.fields.get("BACKDROP").ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - missing field BACKDROP" + ) + })?; + let sb3::VarVal::String(backdrop_name) = + val.clone().ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - null backdrop name for BACKROP \ + field" + ) + })? + else { + hq_bad_proj!( + "invalid project.json - BACKDROP field is not of type String" + ); + }; + let backdrop_index: i32 = context + .project()? + .backdrops() + .iter() + .find_position(|costume| costume.name == backdrop_name) + .ok_or_else(|| make_hq_bug!("backdrop index not found"))? + .0 + .try_into() + .map_err(|_| make_hq_bug!("backdrop index out of bounds"))?; + vec![IrOpcode::hq_integer(HqIntegerFields(backdrop_index))] + } + BlockOpcode::looks_nextcostume => { + vec![ + IrOpcode::looks_costumenumber, + IrOpcode::hq_integer(HqIntegerFields(1)), + IrOpcode::operator_add, + IrOpcode::hq_integer(HqIntegerFields( + context.target().costumes().len().try_into().map_err(|_| { make_hq_bug!("costumes length out of bounds") })?, - )), - IrOpcode::operator_modulo, - IrOpcode::looks_switchcostumeto, - ], + )), + IrOpcode::operator_modulo, + IrOpcode::looks_switchcostumeto, + ] + } + BlockOpcode::looks_nextbackdrop => { + vec![ + IrOpcode::looks_backdropnumber, + IrOpcode::hq_integer(HqIntegerFields(1)), + IrOpcode::operator_add, + IrOpcode::hq_integer(HqIntegerFields( + context.project()?.backdrops().len().try_into().map_err( + |_| make_hq_bug!("backdrops length out of bounds"), + )?, + )), + IrOpcode::operator_modulo, + IrOpcode::looks_switchbackdropto, + ] + } BlockOpcode::looks_costume => { let (sb3::Field::Value((val,)) | sb3::Field::ValueId(val, _)) = block_info.fields.get("COSTUME").ok_or_else(|| { diff --git a/src/ir/project.rs b/src/ir/project.rs index f1224341..efed9ff7 100644 --- a/src/ir/project.rs +++ b/src/ir/project.rs @@ -21,6 +21,8 @@ pub struct IrProject { global_lists: TargetLists, broadcasts: Box<[Box]>, targets: RefCell, Rc>>, + stage_index: usize, + backdrops: Vec, } impl IrProject { @@ -48,11 +50,21 @@ impl IrProject { &self.broadcasts } + pub const fn stage_index(&self) -> usize { + self.stage_index + } + + pub const fn backdrops(&self) -> &Vec { + &self.backdrops + } + #[must_use] pub fn new( global_variables: TargetVars, global_lists: TargetLists, broadcasts: Box<[Box]>, + stage_index: usize, + backdrops: Vec, ) -> Self { Self { threads: RefCell::new(Box::new([])), @@ -61,6 +73,8 @@ impl IrProject { global_lists, broadcasts, targets: RefCell::new(IndexMap::default()), + stage_index, + backdrops, } } @@ -95,7 +109,32 @@ impl IrProject { .cloned() .collect(); - let project = Rc::new(Self::new(global_variables, global_lists, broadcasts)); + let (stage_index, stage_target) = sb3 + .targets + .iter() + .find_position(|target| target.is_stage) + .ok_or_else(|| make_hq_bug!("couldn't find stage target"))?; + + let backdrops: Vec<_> = stage_target + .costumes + .iter() + .map(|costume| { + IrCostume { + name: costume.name.clone(), + data_format: costume.data_format, + md5ext: costume.md5ext.clone(), + //data: load_asset(costume.md5ext.as_str()), + } + }) + .collect(); + + let project = Rc::new(Self::new( + global_variables, + global_lists, + broadcasts, + stage_index, + backdrops, + )); let (threads_vec, targets): (Vec<_>, Vec<_>) = sb3 .targets From 75943a8542e5e49af73faa81d9c0f97c3d34c3ea Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Fri, 13 Feb 2026 17:18:33 +0000 Subject: [PATCH 11/46] Gracefully tear down setup when navigation occurs Fixes #39 --- js/shared.ts | 12 ++++++++++ playground/components/ProjectPlayer.vue | 30 ++++++++++++++++++------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/js/shared.ts b/js/shared.ts index fdb52866..f9a93b37 100644 --- a/js/shared.ts +++ b/js/shared.ts @@ -13,6 +13,18 @@ type Costume = { dataFormat: string; }; +export function unsetup() { + _target_names = null; + _target_bubbles = null; + _renderer = null; + _pen_skin = null; + _target_skins = null; + _costumes = null; + _queue_question = () => {}; + _stageIndex = null; + _setup = false; +} + export function setup( target_names: Array, renderer: object, diff --git a/playground/components/ProjectPlayer.vue b/playground/components/ProjectPlayer.vue index d0b14975..2c254a17 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -60,9 +60,10 @@ import { WasmFlags, } from "../../js/compiler/hyperquark.js"; import { ProjectRunner } from "../lib/project-runner.js"; -import { ref, onMounted, reactive, watch } from "vue"; +import { ref, onMounted, reactive, watch, onBeforeUnmount } from "vue"; import { getSettings } from "../lib/settings.js"; import { useDebugModeStore } from "../stores/debug.js"; +import { unsetup } from "../../js/shared.js"; const debugModeStore = useDebugModeStore(); @@ -130,6 +131,16 @@ function queue_question(question, struct) { queued_questions.push([question, struct]); } +let mouseMove, mouseDown, mouseUp, runner; + +onBeforeUnmount(() => { + document.removeEventListener("mousemove", mouseMove); + canvas.value.removeEventListener("mousedown", mouseDown); + canvas.value.removeEventListener("mouseup", mouseUp); + runner.stop(); + unsetup(); +}); + onMounted(async () => { const load_asset = async (md5ext) => { try { @@ -229,7 +240,7 @@ onMounted(async () => { try { loadingMsg.value = "instantiating project"; - const runner = await ProjectRunner.init({ + runner = await ProjectRunner.init({ framerate: 30, turbo: turbo.value, wasm_bytes: wasmBytes, @@ -264,17 +275,20 @@ onMounted(async () => { }); }; - document.addEventListener("mousemove", (e) => { + mouseMove = (e) => { onMouseMove(e); - }); - canvas.value.addEventListener("mousedown", (e) => { + }; + document.addEventListener("mousemove", mouseMove); + mouseDown = (e) => { onMouseMove(e, true); e.preventDefault(); - }); - canvas.value.addEventListener("mouseup", (e) => { + }; + canvas.value.addEventListener("mousedown", mouseDown); + mouseUp = (e) => { onMouseMove(e, false); e.preventDefault(); - }); + }; + canvas.value.addEventListener("mouseup", mouseUp); greenFlag = runner.greenFlag.bind(runner); stop = runner.stop.bind(runner); From 71be336e4a7e4a1c75303c781f57c37297a8a704 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:12:07 +0000 Subject: [PATCH 12/46] add variable show/hide blocks --- js/data/update_var_val_boolean.ts | 5 ++ js/data/update_var_val_float.ts | 5 ++ js/data/update_var_val_int.ts | 5 ++ js/data/update_var_val_string.ts | 5 ++ js/data/update_var_visible.ts | 5 ++ js/shared.ts | 18 ++++ playground/components/ProjectPlayer.vue | 53 ++++++++++++ playground/lib/project-runner.js | 54 ++++++++---- playground/lib/setup.js | 4 + src/instructions/data.rs | 1 + src/instructions/data/setvariableto.rs | 59 +++++++++++-- src/instructions/data/teevariable.rs | 59 +++++++++++-- src/instructions/data/variable.rs | 8 ++ src/instructions/data/visvariable.rs | 86 +++++++++++++++++++ src/ir/blocks.rs | 106 ++++++++++++++++++++++-- src/ir/project.rs | 3 +- src/ir/variable.rs | 43 +++++++++- 17 files changed, 475 insertions(+), 44 deletions(-) create mode 100644 js/data/update_var_val_boolean.ts create mode 100644 js/data/update_var_val_float.ts create mode 100644 js/data/update_var_val_int.ts create mode 100644 js/data/update_var_val_string.ts create mode 100644 js/data/update_var_visible.ts create mode 100644 src/instructions/data/visvariable.rs diff --git a/js/data/update_var_val_boolean.ts b/js/data/update_var_val_boolean.ts new file mode 100644 index 00000000..c6850f21 --- /dev/null +++ b/js/data/update_var_val_boolean.ts @@ -0,0 +1,5 @@ +import { update_var_val } from "../shared.ts"; + +export function update_var_val_boolean(val: boolean, id: string) { + update_var_val(id, val); +} diff --git a/js/data/update_var_val_float.ts b/js/data/update_var_val_float.ts new file mode 100644 index 00000000..1874843e --- /dev/null +++ b/js/data/update_var_val_float.ts @@ -0,0 +1,5 @@ +import { update_var_val } from "../shared.ts"; + +export function update_var_val_float(val: number, id: string) { + update_var_val(id, val); +} diff --git a/js/data/update_var_val_int.ts b/js/data/update_var_val_int.ts new file mode 100644 index 00000000..f7acc710 --- /dev/null +++ b/js/data/update_var_val_int.ts @@ -0,0 +1,5 @@ +import { update_var_val } from "../shared.ts"; + +export function update_var_val_int(val: number, id: string) { + update_var_val(id, val); +} diff --git a/js/data/update_var_val_string.ts b/js/data/update_var_val_string.ts new file mode 100644 index 00000000..3e367056 --- /dev/null +++ b/js/data/update_var_val_string.ts @@ -0,0 +1,5 @@ +import { update_var_val } from "../shared.ts"; + +export function update_var_val_string(val: string, id: string) { + update_var_val(id, val); +} diff --git a/js/data/update_var_visible.ts b/js/data/update_var_visible.ts new file mode 100644 index 00000000..df84d28a --- /dev/null +++ b/js/data/update_var_visible.ts @@ -0,0 +1,5 @@ +import { update_var_visible as update_var_visibility } from "../shared.ts"; + +export function update_var_visible(id: string, visible: boolean) { + update_var_visibility(id, visible); +} diff --git a/js/shared.ts b/js/shared.ts index f9a93b37..014df406 100644 --- a/js/shared.ts +++ b/js/shared.ts @@ -7,6 +7,8 @@ let _target_skins: Array<[number, number]>; let _costumes: Array>; let _queue_question: (question: string, struct: object) => void = () => {}; let _stageIndex: number; +let _update_var_val: (id: string, val: any) => void = () => {}; +let _update_var_visible: (id: string, visible: boolean) => void = () => {}; type Costume = { data: string; @@ -22,6 +24,8 @@ export function unsetup() { _costumes = null; _queue_question = () => {}; _stageIndex = null; + _update_var_val = () => {}; + _update_var_visible = () => {}; _setup = false; } @@ -33,6 +37,8 @@ export function setup( costumes: Array>, queue_question: (question: string, struct: object) => void, stageIndex: number, + update_var_val: (id: string, val: any) => void, + update_var_visible: (id: string, visible: boolean) => void, ) { _target_names = target_names; _target_bubbles = _target_names.map((_) => null); @@ -43,6 +49,8 @@ export function setup( _costumes = costumes; _queue_question = queue_question; _stageIndex = stageIndex; + _update_var_val = update_var_val; + _update_var_visible = update_var_visible; _setup = true; } @@ -119,3 +127,13 @@ export function queue_question(question: string, struct: object) { check_setup(); _queue_question(question, struct); } + +export function update_var_val(id: string, val: any) { + check_setup(); + _update_var_val(id, val); +} + +export function update_var_visible(id: string, visible: boolean) { + check_setup(); + _update_var_visible(id, visible); +} diff --git a/playground/components/ProjectPlayer.vue b/playground/components/ProjectPlayer.vue index 2c254a17..83e46dd9 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -22,6 +22,23 @@
+
+ + {{ sprite }}: {{ name }} + + {{ value }} +
{{ queued_questions[0]?.[0] }} @@ -86,6 +103,7 @@ let canvas = ref(null); let loadingMsg = ref("compiling project"); let loaded = ref(false); let questionInput = ref(null); +let monitors = ref({}); let greenFlag = () => null; let stop = () => null; @@ -264,6 +282,18 @@ onMounted(async () => { "queueQuestion", ({ detail: { question, struct } }) => queue_question(question, struct), ); + runner.addEventListener( + "updateVariableVal", + ({ detail: { id, value } }) => { + monitors.value[id].value = value; + }, + ); + runner.addEventListener( + "updateVariableVisibility", + ({ detail: { id, visible } }) => { + monitors.value[id].visible = visible; + }, + ); const onMouseMove = (e, isDown) => { const rect = canvas.value.getBoundingClientRect(); @@ -294,6 +324,8 @@ onMounted(async () => { stop = runner.stop.bind(runner); setAnswer = runner.setAnswer.bind(runner); mark_question_resolved = runner.mark_question_resolved.bind(runner); + monitors.value = runner.monitors; + console.log(monitors.value); } catch (e) { declareError(e, true, "An error", "instantiating"); } @@ -361,4 +393,25 @@ div.instructions { white-space: pre-wrap; } + +div.variable-monitor { + position: absolute; + background-color: var(--color-background-soft); + border-radius: 5px; + padding: 0 0.4em 0.2em 0.4em; + + & span { + padding: 0; + margin: 0; + vertical-align: middle; + } + + & > span.variable-value { + background-color: hsl(39.3, 100%, 37%); + color: var(--color-background); + border-radius: 5px; + padding: 0 0.3em; + margin-left: 0.3em; + } +} diff --git a/playground/lib/project-runner.js b/playground/lib/project-runner.js index 70cd5bb1..b571e751 100644 --- a/playground/lib/project-runner.js +++ b/playground/lib/project-runner.js @@ -40,8 +40,16 @@ export class ProjectRunner extends EventTarget { #mouseY; #mouseDown; #triggerSpriteClicked; + monitors; - constructor({ renderer, timeout, framerate_wait, turbo, exports }) { + constructor({ + renderer, + timeout, + framerate_wait, + turbo, + exports, + project_json, + }) { super(); this.#sensing_answer = exports.sensing_answer; @@ -60,6 +68,21 @@ export class ProjectRunner extends EventTarget { this.#mouseY = exports.mouseY ?? { value: 0 }; this.#mouseDown = exports.mouseDown ?? { value: false }; this.#triggerSpriteClicked = exports.trigger_sprite_clicked; + this.monitors = Object.fromEntries( + project_json.monitors.map((monitor) => { + return [ + monitor.id, + { + name: monitor.params.VARIABLE, + x: monitor.x, + y: monitor.y, + visible: monitor.visible, + value: monitor.value, + sprite: monitor.spriteName, + }, + ]; + }), + ); } static async init({ @@ -93,6 +116,18 @@ export class ProjectRunner extends EventTarget { new CustomEvent("queueQuestion", { detail: { question, struct } }), ); }, + (id, value) => { + runner.dispatchEvent( + new CustomEvent("updateVariableVal", { detail: { id, value } }), + ); + }, + (id, visible) => { + runner.dispatchEvent( + new CustomEvent("updateVariableVisibility", { + detail: { id, visible }, + }), + ); + }, ); const renderer = get_renderer(); @@ -138,21 +173,7 @@ export class ProjectRunner extends EventTarget { importedStringConstants: "", }); - const { - flag_clicked, - tick, - memory, - threads_count, - requests_refresh, - threads, - unreachable_dbg, - sensing_timer, - mark_waiting_flag, - sensing_answer, - mouseX, - mouseY, - mouseDown, - } = instance.exports; + const { flag_clicked, tick, memory, unreachable_dbg } = instance.exports; if (typeof window === "object") { window.memory = memory; @@ -173,6 +194,7 @@ export class ProjectRunner extends EventTarget { timeout, framerate_wait, turbo, + project_json, }); return runner; diff --git a/playground/lib/setup.js b/playground/lib/setup.js index e3d406a8..f9f3d40a 100644 --- a/playground/lib/setup.js +++ b/playground/lib/setup.js @@ -18,6 +18,8 @@ export async function setup( assets, target_names, queue_question, + update_var_val, + update_var_visible, ) { if (is_setup()) return; @@ -79,5 +81,7 @@ export async function setup( costumes, queue_question, stageIndex, + update_var_val, + update_var_visible, ); } diff --git a/src/instructions/data.rs b/src/instructions/data.rs index 55e729ec..c5525252 100644 --- a/src/instructions/data.rs +++ b/src/instructions/data.rs @@ -9,3 +9,4 @@ pub mod replaceitemoflist; pub mod setvariableto; pub mod teevariable; pub mod variable; +pub mod visvariable; diff --git a/src/instructions/data/setvariableto.rs b/src/instructions/data/setvariableto.rs index a9dc40c8..c8454f1f 100644 --- a/src/instructions/data/setvariableto.rs +++ b/src/instructions/data/setvariableto.rs @@ -1,5 +1,6 @@ use super::super::prelude::*; use crate::ir::RcVar; +use crate::wasm::WasmProject; /// we need these fields to be mutable for optimisations to be feasible #[derive(Debug, Clone)] @@ -28,15 +29,48 @@ pub fn wasm( Fields { var, local_write }: &Fields, ) -> HQResult> { let t1 = inputs[0]; - if *local_write.try_borrow()? { + Ok(if let Some(monitor) = var.borrow().monitor().as_ref() + && *monitor.is_ever_visible.borrow() + { + let wasm_input_ty = WasmProject::ir_type_to_wasm(t1)?; + let local = func.local(wasm_input_ty)?; + let update_func = func.registries().external_functions().register( + ( + "data", + match t1.base_type() { + Some(IrType::Boolean) => "update_var_val_bool", + Some(IrType::String) => "update_var_val_string", + Some(IrType::Int) => "update_var_val_int", + Some(IrType::Float) => "update_var_val_float", + _ => hq_bug!("bad input type for variable with monitor"), + } + .into(), + ), + (vec![wasm_input_ty, ValType::EXTERNREF], vec![]), + )?; + let variable_string = func + .registries() + .strings() + .register_default(monitor.id.clone())?; + wasm![ + LocalTee(local), + GlobalGet(variable_string), + Call(update_func), + LocalGet(local), + ] + } else { + wasm![] + } + .into_iter() + .chain(if *local_write.try_borrow()? { let local_index: u32 = func.local_variable(&*var.try_borrow()?)?; if var.borrow().possible_types().is_base_type() { - Ok(wasm![LocalSet(local_index)]) + wasm![LocalSet(local_index)] } else { - Ok(wasm![ + wasm![ @boxed(t1), LocalSet(local_index) - ]) + ] } } else { let global_index: u32 = func @@ -44,14 +78,15 @@ pub fn wasm( .variables() .register(&*var.try_borrow()?)?; if var.try_borrow()?.possible_types().is_base_type() { - Ok(wasm![#LazyGlobalSet(global_index)]) + wasm![#LazyGlobalSet(global_index)] } else { - Ok(wasm![ + wasm![ @boxed(t1), #LazyGlobalSet(global_index), - ]) + ] } - } + }) + .collect()) } pub fn acceptable_inputs(Fields { var, .. }: &Fields) -> HQResult> { @@ -102,6 +137,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Any, crate::sb3::VarVal::Float(0.0), + None, ).unwrap()), local_write: RefCell::new(false) } @@ -115,6 +151,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Float, crate::sb3::VarVal::Float(0.0), + None, ).unwrap()), local_write: RefCell::new(false) } @@ -128,6 +165,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::String, crate::sb3::VarVal::String("".into()), + None, ).unwrap()), local_write: RefCell::new(false) } @@ -141,6 +179,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Int, crate::sb3::VarVal::Int(1), + None, ).unwrap()), local_write: RefCell::new(false) } @@ -154,6 +193,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Any, crate::sb3::VarVal::Float(0.0), + None, ).unwrap()), local_write: RefCell::new(true) } @@ -167,6 +207,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Float, crate::sb3::VarVal::Float(0.0), + None, ).unwrap()), local_write: RefCell::new(true) } @@ -180,6 +221,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::String, crate::sb3::VarVal::String("".into()), + None, ).unwrap()), local_write: RefCell::new(true) } @@ -193,6 +235,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Int, crate::sb3::VarVal::Int(1), + None, ).unwrap()), local_write: RefCell::new(true) } diff --git a/src/instructions/data/teevariable.rs b/src/instructions/data/teevariable.rs index 6efa06fa..a9409e8a 100644 --- a/src/instructions/data/teevariable.rs +++ b/src/instructions/data/teevariable.rs @@ -2,6 +2,7 @@ /// (i.e. locals) then this can actually use the wasm tee instruction. use super::super::prelude::*; use crate::ir::RcVar; +use crate::wasm::WasmProject; #[derive(Debug, Clone)] pub struct Fields { @@ -32,15 +33,48 @@ pub fn wasm( }: &Fields, ) -> HQResult> { let t1 = inputs[0]; - if *local_read_write.try_borrow()? { + Ok(if let Some(monitor) = var.borrow().monitor().as_ref() + && *monitor.is_ever_visible.borrow() + { + let wasm_input_ty = WasmProject::ir_type_to_wasm(t1)?; + let local = func.local(wasm_input_ty)?; + let update_func = func.registries().external_functions().register( + ( + "data", + match t1.base_type() { + Some(IrType::Boolean) => "update_var_val_bool", + Some(IrType::String) => "update_var_val_string", + Some(IrType::Int) => "update_var_val_int", + Some(IrType::Float) => "update_var_val_float", + _ => hq_bug!("bad input type for variable with monitor"), + } + .into(), + ), + (vec![wasm_input_ty, ValType::EXTERNREF], vec![]), + )?; + let variable_string = func + .registries() + .strings() + .register_default(monitor.id.clone())?; + wasm![ + LocalTee(local), + GlobalGet(variable_string), + Call(update_func), + LocalGet(local), + ] + } else { + wasm![] + } + .into_iter() + .chain(if *local_read_write.try_borrow()? { let local_index: u32 = func.local_variable(&*var.try_borrow()?)?; if var.borrow().possible_types().is_base_type() { - Ok(wasm![LocalTee(local_index)]) + wasm![LocalTee(local_index)] } else { - Ok(wasm![ + wasm![ @boxed(t1), LocalTee(local_index) - ]) + ] } } else { let global_index: u32 = func @@ -48,15 +82,16 @@ pub fn wasm( .variables() .register(&*var.try_borrow()?)?; if var.borrow().possible_types().is_base_type() { - Ok(wasm![#LazyGlobalSet(global_index), #LazyGlobalGet(global_index)]) + wasm![#LazyGlobalSet(global_index), #LazyGlobalGet(global_index)] } else { - Ok(wasm![ + wasm![ @boxed(t1), #LazyGlobalSet(global_index), #LazyGlobalGet(global_index) - ]) + ] } - } + }) + .collect()) } pub fn acceptable_inputs(Fields { var, .. }: &Fields) -> HQResult> { @@ -108,6 +143,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Any, crate::sb3::VarVal::Float(0.0), + None, ).unwrap()) , local_read_write: RefCell::new(false), @@ -123,6 +159,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Float, crate::sb3::VarVal::Float(0.0), + None, ).unwrap()) , local_read_write: RefCell::new(false), @@ -138,6 +175,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::String, crate::sb3::VarVal::String("".into()), + None, ).unwrap()) , local_read_write: RefCell::new(false), @@ -153,6 +191,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Int, crate::sb3::VarVal::Int(1), + None, ).unwrap()) , local_read_write: RefCell::new(false), @@ -168,6 +207,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Any, crate::sb3::VarVal::Float(0.0), + None, ).unwrap() ), local_read_write: RefCell::new(true), @@ -183,6 +223,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Float, crate::sb3::VarVal::Float(0.0), + None, ).unwrap() ), local_read_write: RefCell::new(true), @@ -198,6 +239,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::String, crate::sb3::VarVal::String("".into()), + None, ).unwrap()), local_read_write: RefCell::new(true), } @@ -212,6 +254,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Int, crate::sb3::VarVal::Int(1), + None, ).unwrap()), local_read_write: RefCell::new(true), } diff --git a/src/instructions/data/variable.rs b/src/instructions/data/variable.rs index 269d6db3..248aa7ab 100644 --- a/src/instructions/data/variable.rs +++ b/src/instructions/data/variable.rs @@ -85,6 +85,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Any, crate::sb3::VarVal::Float(0.0), + None, ).unwrap() ) , @@ -100,6 +101,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Float, crate::sb3::VarVal::Float(0.0), + None, ).unwrap() ), local_read: RefCell::new(false) @@ -115,6 +117,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::String, crate::sb3::VarVal::String("".into()), + None, ).unwrap() ) , @@ -131,6 +134,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Int, crate::sb3::VarVal::Int(1), + None, ).unwrap() ) , @@ -147,6 +151,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Any, crate::sb3::VarVal::Float(0.0), + None, ).unwrap() ) , @@ -163,6 +168,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Float, crate::sb3::VarVal::Float(0.0), + None, ).unwrap() ) , @@ -178,6 +184,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::String, crate::sb3::VarVal::String("".into()), + None, ).unwrap() ), @@ -193,6 +200,7 @@ crate::instructions_test!( crate::ir::RcVar::new( IrType::Int, crate::sb3::VarVal::Int(1), + None, ).unwrap() ), local_read: RefCell::new(true) diff --git a/src/instructions/data/visvariable.rs b/src/instructions/data/visvariable.rs new file mode 100644 index 00000000..c42e8921 --- /dev/null +++ b/src/instructions/data/visvariable.rs @@ -0,0 +1,86 @@ +/// this is currently just a convenience block. if we get thread-scoped variables +/// (i.e. locals) then this can actually use the wasm tee instruction. +use super::super::prelude::*; +use crate::ir::RcVar; + +#[derive(Debug, Clone)] +pub struct Fields { + pub var: RefCell, + pub visible: bool, +} + +impl fmt::Display for Fields { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + r#"{{ + "variable": {}, + "visible": {} + }}"#, + self.var.borrow(), + self.visible, + ) + } +} + +pub fn wasm( + func: &StepFunc, + _inputs: Rc<[IrType]>, + Fields { var, visible }: &Fields, +) -> HQResult> { + let borrowed_var = var.borrow(); + let Some(monitor) = borrowed_var.monitor().as_ref() else { + hq_bug!("tried to change visibility of variable without monitor") + }; + hq_assert!( + *monitor.is_ever_visible.borrow(), + "tried to change visibility of unused monitor" + ); + let update_func = func.registries().external_functions().register( + ("data", "update_var_visible".into()), + (vec![ValType::EXTERNREF, ValType::I32], vec![]), + )?; + let variable_string = func + .registries() + .strings() + .register_default(monitor.id.clone())?; + Ok(wasm![ + GlobalGet(variable_string), + I32Const((*visible).into()), + Call(update_func), + ]) +} + +pub fn acceptable_inputs(_fields: &Fields) -> HQResult> { + Ok(Rc::from([])) +} + +pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult { + Ok(ReturnType::None) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +pub const fn const_fold( + _inputs: &[ConstFoldItem], + _state: &mut ConstFoldState, + _fields: &Fields, +) -> HQResult { + Ok(NotFoldable) +} + +crate::instructions_test!( + any_global; + data_visvariable; + t + @ super::Fields { + var: RefCell::new( + crate::ir::RcVar::new( + IrType::Any, + crate::sb3::VarVal::Float(0.0), + None, + ).unwrap()) + , + visible: true + } +); diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index d590b28a..a97f1ab7 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -10,9 +10,9 @@ use crate::instructions::{ DataDeletealloflistFields, DataDeleteoflistFields, DataInsertatlistFields, DataItemoflistFields, DataLengthoflistFields, DataListcontentsFields, DataReplaceitemoflistFields, DataSetvariabletoFields, DataTeevariableFields, - DataVariableFields, EventBroadcastAndWaitFields, EventBroadcastFields, HqBooleanFields, - HqCastFields, HqColorRgbFields, HqFloatFields, HqIntegerFields, HqTextFields, HqYieldFields, - IrOpcode, LooksSayFields, LooksThinkFields, ProceduresArgumentFields, + DataVariableFields, DataVisvariableFields, EventBroadcastAndWaitFields, EventBroadcastFields, + HqBooleanFields, HqCastFields, HqColorRgbFields, HqFloatFields, HqIntegerFields, HqTextFields, + HqYieldFields, IrOpcode, LooksSayFields, LooksThinkFields, ProceduresArgumentFields, ProceduresCallNonwarpFields, ProceduresCallWarpFields, SensingAskandwaitFields, YieldMode, }; use crate::ir::{InlinedStep, MaybeInlinedStep, RcList, ReturnType, StepIndex}; @@ -268,7 +268,9 @@ pub fn input_names(block_info: &BlockInfo, context: &StepContext) -> HQResult vec![], + | BlockOpcode::looks_nextbackdrop + | BlockOpcode::data_showvariable + | BlockOpcode::data_hidevariable => vec![], BlockOpcode::sensing_askandwait => vec!["QUESTION"], BlockOpcode::event_broadcast | BlockOpcode::event_broadcastandwait => { vec!["BROADCAST_INPUT"] @@ -906,8 +908,8 @@ fn generate_list_index_op( where B: Fn() -> IrOpcode, { - let text_var = RcVar::new(IrType::String, VarVal::String("".into()))?; - let int_var = RcVar::new(IrType::Int, VarVal::Int(0))?; + let text_var = RcVar::new(IrType::String, VarVal::String("".into()), None)?; + let int_var = RcVar::new(IrType::Int, VarVal::Int(0), None)?; let extra_var = RcVar::new_empty(); let result_var = RcVar::new_empty(); @@ -1151,7 +1153,7 @@ where S: Into> + Clone, F: Fn(Box) -> IrOpcode, { - let var = RcVar::new(IrType::String, VarVal::String("".into()))?; + let var = RcVar::new(IrType::String, VarVal::String("".into()), None)?; Ok(vec![ IrOpcode::hq_cast(HqCastFields(IrType::String)), IrOpcode::data_setvariableto(DataSetvariabletoFields { @@ -1573,6 +1575,94 @@ fn from_normal_block( local_read: RefCell::new(false), })] } + BlockOpcode::data_showvariable => { + let sb3::Field::ValueId(_val, maybe_id) = + block_info.fields.get("VARIABLE").ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - missing field VARIABLE" + ) + })? + else { + hq_bad_proj!( + "invalid project.json - missing variable id for VARIABLE field" + ); + }; + let id = maybe_id.clone().ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - null variable id for VARIABLE field" + ) + })?; + let target = context.target(); + let variable = if let Some(var) = target.variables().get(&id) { + var.clone() + } else if let Some(var) = context + .target() + .project() + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .global_variables() + .get(&id) + { + var.clone() + } else { + hq_bad_proj!("variable not found") + }; + *variable.is_used.try_borrow_mut()? = true; + let Some(monitor) = variable.var.monitor().as_ref() else { + hq_bad_proj!( + "tried to change visibility of variable without monitor" + ); + }; + *monitor.is_ever_visible.try_borrow_mut()? = true; + // crate::log!("marked variable {:?} as used", id); + vec![IrOpcode::data_visvariable(DataVisvariableFields { + var: RefCell::new(variable.var.clone()), + visible: true, + })] + } + BlockOpcode::data_hidevariable => { + let sb3::Field::ValueId(_val, maybe_id) = + block_info.fields.get("VARIABLE").ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - missing field VARIABLE" + ) + })? + else { + hq_bad_proj!( + "invalid project.json - missing variable id for VARIABLE field" + ); + }; + let id = maybe_id.clone().ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - null variable id for VARIABLE field" + ) + })?; + let target = context.target(); + let variable = if let Some(var) = target.variables().get(&id) { + var.clone() + } else if let Some(var) = context + .target() + .project() + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .global_variables() + .get(&id) + { + var.clone() + } else { + hq_bad_proj!("variable not found") + }; + *variable.is_used.try_borrow_mut()? = true; + hq_assert!( + variable.var.monitor().is_some(), + "tried to change visibility of variable without monitor" + ); + // crate::log!("marked variable {:?} as used", id); + vec![IrOpcode::data_visvariable(DataVisvariableFields { + var: RefCell::new(variable.var.clone()), + visible: false, + })] + } BlockOpcode::data_deletealloflist => { let sb3::Field::ValueId(_val, maybe_id) = block_info.fields.get("LIST").ok_or_else(|| { @@ -2027,7 +2117,7 @@ fn from_normal_block( )? } BlockOpcode::control_repeat => { - let variable = RcVar::new(IrType::Int, sb3::VarVal::Int(0))?; + let variable = RcVar::new(IrType::Int, sb3::VarVal::Int(0), None)?; let local = context.warp; let condition_instructions = vec![ IrOpcode::data_variable(DataVariableFields { diff --git a/src/ir/project.rs b/src/ir/project.rs index efed9ff7..1ee6937a 100644 --- a/src/ir/project.rs +++ b/src/ir/project.rs @@ -92,6 +92,7 @@ impl IrProject { .iter() .find(|target| target.is_stage) .ok_or_else(|| make_hq_bad_proj!("missing stage target"))?, + &sb3.monitors, )?; let global_lists = lists_from_target( @@ -144,7 +145,7 @@ impl IrProject { let variables = if target.is_stage { BTreeMap::new() } else { - variables_from_target(target)? + variables_from_target(target, &sb3.monitors)? }; let lists = if target.is_stage { BTreeMap::new() diff --git a/src/ir/variable.rs b/src/ir/variable.rs index 588a4fa8..bc8b3281 100644 --- a/src/ir/variable.rs +++ b/src/ir/variable.rs @@ -6,7 +6,7 @@ use uuid::Uuid; use super::Type; use crate::ir::types::var_val_type; use crate::prelude::*; -use crate::sb3::{Target as Sb3Target, VarVal}; +use crate::sb3::{Monitor as Sb3Monitor, Target as Sb3Target, VarVal}; use crate::wasm::WasmFlags; use crate::wasm::flags::Switch; @@ -15,17 +15,30 @@ struct Variable { possible_types: RefCell, initial_value: VarVal, id: String, + monitor: Option, } #[derive(Clone, Debug)] pub struct RcVar(Rc); +#[derive(Debug)] +pub struct IrMonitor { + pub name: Box, + pub id: Box, + pub sprite: Option>, + pub visible: bool, + pub is_ever_visible: RefCell, + pub x: f64, + pub y: f64, +} + impl RcVar { - pub fn new(ty: Type, initial_value: VarVal) -> HQResult { + pub fn new(ty: Type, initial_value: VarVal, monitor: Option) -> HQResult { Ok(Self(Rc::new(Variable { possible_types: RefCell::new(ty.or(var_val_type(&initial_value)?)), initial_value, id: Uuid::new_v4().to_string(), + monitor, }))) } @@ -36,6 +49,7 @@ impl RcVar { possible_types: RefCell::new(Type::none()), initial_value: VarVal::Bool(false), // arbitrary value id: Uuid::new_v4().to_string(), + monitor: None, })) } @@ -58,6 +72,11 @@ impl RcVar { pub fn id(&self) -> &str { &self.0.id } + + #[must_use] + pub fn monitor(&self) -> &Option { + &self.0.monitor + } } impl PartialEq for RcVar { @@ -128,11 +147,28 @@ pub type TargetVars = BTreeMap, Rc>; pub type TargetLists = BTreeMap, Rc>; -pub fn variables_from_target(target: &Sb3Target) -> HQResult { +pub fn variables_from_target(target: &Sb3Target, monitors: &[Sb3Monitor]) -> HQResult { target .variables .iter() .map(|(id, var_info)| { + let monitor = monitors + .iter() + .find(|monitor| monitor.id() == Some(id)) + .and_then(|monitor| { + Some(IrMonitor { + name: monitor + .params() + .map(|params| params.get("VARIABLE"))?? + .clone(), + sprite: monitor.sprite_name().cloned().flatten(), + visible: *monitor.visible()?, + x: *monitor.x()?, + y: *monitor.y()?, + is_ever_visible: RefCell::new(*monitor.visible()?), + id: id.clone(), + }) + }); Ok(( id.clone(), Rc::new(TargetVar { @@ -143,6 +179,7 @@ pub fn variables_from_target(target: &Sb3Target) -> HQResult { reason = "this field exists on all variants" )] var_info.get_1().unwrap().clone(), + monitor, )?, is_used: RefCell::new(false), }), From 41dab0a43eb50b6c9df9730cfa6d2869453a5112 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:13:11 +0000 Subject: [PATCH 13/46] consider that `project_json.monitors` might not exist --- playground/lib/project-runner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playground/lib/project-runner.js b/playground/lib/project-runner.js index b571e751..dde8027f 100644 --- a/playground/lib/project-runner.js +++ b/playground/lib/project-runner.js @@ -69,7 +69,7 @@ export class ProjectRunner extends EventTarget { this.#mouseDown = exports.mouseDown ?? { value: false }; this.#triggerSpriteClicked = exports.trigger_sprite_clicked; this.monitors = Object.fromEntries( - project_json.monitors.map((monitor) => { + project_json.monitors?.map?.((monitor) => { return [ monitor.id, { @@ -81,7 +81,7 @@ export class ProjectRunner extends EventTarget { sprite: monitor.spriteName, }, ]; - }), + }) ?? [] ); } From 9d3bce7403c00b0dd4dfcc05aa1e5c2f3b7d46ce Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:08:45 +0000 Subject: [PATCH 14/46] respect yielding condition when generating inlined next step (fixes #52) --- src/ir/blocks.rs | 89 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index a97f1ab7..d69c3438 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -423,21 +423,35 @@ fn procedure_argument( )]) } +/// Generates the next step to go to, based off of the block info, and the `NextBlocks` +/// passed to it. +/// +/// Returns a `MaybeInlinedStep` along with a `bool` indicating if the step should yield +/// first. fn generate_next_step( block_info: &BlockInfo, blocks: &BTreeMap, Block>, context: &StepContext, final_next_blocks: NextBlocks, flags: &WasmFlags, -) -> HQResult { - let (next_block, outer_next_blocks) = if let Some(ref next_block) = block_info.next { - (Some(NextBlock::ID(next_block.clone())), final_next_blocks) +) -> HQResult<(MaybeInlinedStep, bool)> { + let (next_block, yield_first, outer_next_blocks) = if let Some(ref next_block) = block_info.next + { + ( + Some(NextBlock::ID(next_block.clone())), + false, + final_next_blocks, + ) } else if let (Some(next_block_info), popped_next_blocks) = final_next_blocks.clone().pop_inner() { - (Some(next_block_info.block), popped_next_blocks) + ( + Some(next_block_info.block), + next_block_info.yield_first, + popped_next_blocks, + ) } else { - (None, final_next_blocks) + (None, false, final_next_blocks) }; let next_step = match next_block { Some(NextBlock::ID(id)) => { @@ -463,7 +477,7 @@ fn generate_next_step( false, )), }; - Ok(next_step) + Ok((next_step, yield_first)) } fn generate_next_step_inlined( @@ -473,10 +487,39 @@ fn generate_next_step_inlined( final_next_blocks: NextBlocks, flags: &WasmFlags, ) -> HQResult { - Ok( - match generate_next_step(block_info, blocks, context, final_next_blocks, flags)? { - MaybeInlinedStep::Inlined(step) => step, - MaybeInlinedStep::NonInlined(step_index) => { + let (next_step, yield_first) = + generate_next_step(block_info, blocks, context, final_next_blocks, flags)?; + Ok(match next_step { + MaybeInlinedStep::Inlined(step) => { + if yield_first { + let next_step_index = step + .try_borrow()? + .clone_to_non_inlined(&context.target().project())?; + Rc::new(RefCell::new(Step::new( + None, + context.clone(), + vec![IrOpcode::hq_yield(HqYieldFields { + mode: YieldMode::Schedule(next_step_index), + })], + context.target().project(), + false, + ))) + } else { + step + } + } + MaybeInlinedStep::NonInlined(step_index) => { + if yield_first { + Rc::new(RefCell::new(Step::new( + None, + context.clone(), + vec![IrOpcode::hq_yield(HqYieldFields { + mode: YieldMode::Schedule(step_index), + })], + context.target().project(), + false, + ))) + } else { let mut step = context .project()? .steps() @@ -488,14 +531,30 @@ fn generate_next_step_inlined( step.make_inlined(); Rc::new(RefCell::new(step)) } - MaybeInlinedStep::Undetermined(mut step) => { + } + MaybeInlinedStep::Undetermined(mut step) => { + if yield_first { + let step_index = step.clone_to_non_inlined(&context.target().project())?; + Rc::new(RefCell::new(Step::new( + None, + context.clone(), + vec![IrOpcode::hq_yield(HqYieldFields { + mode: YieldMode::Schedule(step_index), + })], + context.target().project(), + false, + ))) + } else { step.make_inlined(); Rc::new(RefCell::new(step)) } - }, - ) + } + }) } +/// Generates a `StepIndex` for the next step of the given block, based on the +/// block info and the given `NextSteps`. The generated step must not be inlined +/// at a later stage, as it may cause reference cycles. fn generate_next_step_non_inlined( block_info: &BlockInfo, blocks: &BTreeMap, Block>, @@ -503,7 +562,9 @@ fn generate_next_step_non_inlined( final_next_blocks: NextBlocks, flags: &WasmFlags, ) -> HQResult { - match generate_next_step(block_info, blocks, context, final_next_blocks, flags)? { + let (next_step, _yield_first) = + generate_next_step(block_info, blocks, context, final_next_blocks, flags)?; + match next_step { MaybeInlinedStep::Inlined(step) => step .try_borrow()? .clone_to_non_inlined(&context.target().project()), From 64770b63261de98eceeeca91b48f2dbe35699cd5 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:28:30 +0000 Subject: [PATCH 15/46] fixup proc calls in all nested steps (fixes #53) --- src/ir/project.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/ir/project.rs b/src/ir/project.rs index 1ee6937a..b454e79f 100644 --- a/src/ir/project.rs +++ b/src/ir/project.rs @@ -217,8 +217,9 @@ impl IrProject { for target in project.targets().try_borrow()?.values() { fixup_proc_types(target)?; } + let fixed_proc_calls = &mut BTreeSet::new(); for step in project.steps().try_borrow()?.iter() { - fixup_proc_calls(step)?; + fixup_proc_calls(step, fixed_proc_calls)?; } Ok(project) } @@ -337,15 +338,27 @@ fn fixup_proc_types(target: &Rc) -> HQResult<()> { } /// Pass variables into procedure calls, and read them on return -fn fixup_proc_calls(step: S) -> HQResult<()> +fn fixup_proc_calls(step: S, visited_steps: &mut BTreeSet>) -> HQResult<()> where S: Deref>, { + if visited_steps.contains(step.try_borrow()?.id()) { + return Ok(()); + } + + visited_steps.insert(step.try_borrow()?.id().into()); + let mut call_indices = vec![]; for (index, opcode) in step.try_borrow()?.opcodes().iter().enumerate() { if matches!(opcode, IrOpcode::procedures_call_warp(_)) { call_indices.push(index); } + + if let Some(inline_steps) = opcode.inline_steps() { + inline_steps.iter().try_for_each(|inline_step| { + fixup_proc_calls(Rc::clone(inline_step), visited_steps) + })?; + } } let globally_scoped_variables_num = step.try_borrow()?.globally_scoped_variables_num()?; From fd957068e8b3a1a12640e8735ab9cbd110417594 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:41:07 +0000 Subject: [PATCH 16/46] add test fixtures for #52 & #53 --- .../hq-52-nonyielding-nested-loop-compiles.sb3 | Bin 0 -> 2965 bytes .../hq-53-warped-proc-after-loop-compiles.sb3 | Bin 0 -> 3096 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/fixtures/execute/hq-52-nonyielding-nested-loop-compiles.sb3 create mode 100644 test/fixtures/execute/hq-53-warped-proc-after-loop-compiles.sb3 diff --git a/test/fixtures/execute/hq-52-nonyielding-nested-loop-compiles.sb3 b/test/fixtures/execute/hq-52-nonyielding-nested-loop-compiles.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..f9c352bed09a5fbe76f4923157b38d921f35f6ef GIT binary patch literal 2965 zcma);c{CJy8^_0FCfi7iEm;!TN@L7m62`t4USy|XkQighuE|)lr6JpdhOt(*v6Zf^ zh(v0#Wauh|2BERv@Ve*yYDPJJ0!@^E>DJobx^BInU>7WqyR63jhFc0Is+8 zTQ!0?uPU(v0CErjfamZR9~|h1^$b<^3kk#x*#`tN)cE$5*tW`h5ym80=S^*Y9EsP6 z?v*l)Gn|RcF)>Y7u$EPjb7UTLl*wo|vLv9-J2<|qnP_cIg?yQo%E0P1>pf?Qy?Adh zn@~#0m2I}lR23@+99Uu0VGG`0Ts(a4VzeVD;wOAbEzlybv;9xfB(+%sx-q zd>`x|-O|YC^XlgkIEezyH)ty^3k;;&F=Ict$K=m1Exvh6j+ZK73{U?@~t`_nq%}$WjMRhEYdBwqtWR^iprI9$DyQa%%%RBy9q6s^x2A60N!;RpEcJt>Y``QB17YI*vU`&K@md1`-`xi$1v%;*A=#1-VyZ- zajUaxHyaZP7xp6%)oWXT&Vl&EGbJA_E6U=Jd4xmkX!DBR5boc((|SX=7F*liB}Zy$ zXrWGCepxAp1Sfs(dg@D_nrw+cz4zr-6vj~IxLQes_1Z^1t_cmYtT{D4i(O|k7lk{R33)$akm_`nY7 z+BCU|SVU?2dJp3V7hl>6mvmoxC(z*MeY0s@JxPVpZmoN-+?5IY#Ay6c*Pn!^9Q2h! z&%$is%I)T5TTfL>-Tu&|G}W31S5)?Rp0!8j63!R`oI`i#pU7HsiaNkK)E9r|9?{UAcwGJ-3+%xA8>~Zso&Xp664g-Wpsu zJ!rzNlg+(%-!V|y;$2k*)}w-7&U9KvUDUCxaN)YAVLnUiEClQ|;w9~auU4~sdUeJ0 zTY=?`t0N+LQ&48ufUXg6C=s)PG zt6E*Dr)sQIf1Dn?Ud$Bj@axAgv#L#Ornkj_XfWam+A5or&SX;T5zd7c4{$HPPgJ0W zJTT!?e!u{KJ9t&p$50*{1o_0qC3DeKo%*vLPjaR%GM$9|+B)loUdcWRO|aN}>({(1 zZ1s<&k}}N4mVLNPULBUqVR?GNRgfwOFBno)#oNo<8v}>KVF)Y?>xsmAX&|mDhlKeA zSeo<3OF^fiQ^#A6zNkg?l$u^15oj_?u90q{>#%Q7WdNDu&! z%Kbl6D7dM`G16D3rIlhKj1XD$*07f$&sA!Qp?JdTh_YF^u^31c{(jA*pdq zO&YBomy@jBiYDk%e4R7SKH(SFRM=h7qETZwC&4hkEX0PJv2NrXl-?MMq)xdb6mJPn!t9@`}Idq>onn~oUvlL%lpazR{gYv z`a}Bx(*aY)ocEO5?-u=r1LanwO+%H-+pDZkL8H6F;XZG^fB3Z5zUY0ScB}H?^R=+i zvBBx66NH8M^U4NwltqOHQJHasNBiWBrtd3b;j;~`gLZ$+ZxN;>D@s}_e;;G)HBGx8 zH11!N@R|J@`b_e`kNj}?aOzjWXKSV9|os2AHJ z?{C(No+oP=HGHIuwr_;e{q2|Q;)vNIX5amx{3@NgE++Tk6|)Gr+mLpfl&aHtFr{li zP30HWY27=d{G-zI0Zdov)A%3V!v71t< zFI-nS=@_=ru2@+Au4yTIaU7mltQqt=@d-%{_BWUpkaTbBV`>{5X-yqW0ep6Ws(`$&qxk%anpr7}`b1ewcX z{C?Z<(`cX`PgRa|ri>kjM0)@2i#gx8?mbdH9h2^1Ae}PKV`%o4O`dzC<#Zyg;SoF3 zW+irWGHXqAX{Q{X`S}jVK7d#;SPZG(CUbtCGZAG$6r=>n-WVXY zi(7u8v}^zP^Or)dV4~oZjEA^)nT;`@#n^zsEr}mIS-JTEN;Uv`5Qis}Y$gOb|388M!#Gr{5Eza2pwGVQu?JZ*sd+2DSaBqZ;7GkHwed0)tIk>|$I+Wu=G6UG9rX zA^V$V!vp$yIU^2SEu;Y|ZiIx8?_q9gd_)PLY^M|BtALK>6GGdtm)l`74M1lGzHU1pZTXp;(2HDmw) zDLnu{d-d!6*vr+?-dEh!$IElf!o%ke8HNi{j>FOUk6P~bQTfn&;v;^r3|;w%_9Fqc z@;ZkqoBPTrfqcY8h($V(R7)F1H0YA}P>|uA7iQl61WEmz%8b#^3&M~Rh7)A=OA{)? z!SKE89N9Q0w1lF8P*oTbJD;nM-R*41hA!x1pRmkzZH2jZpjNd;HM}7ehGIi4u{1O{ z^?r;(Y~)yPq%dS7PEX|@^e?O~ul;E}*m=g>DgSfVuuZYpr1y~S11BTIpmL1I@NSk^ zA~g+k2;AxZZK0vT`gbXkbfxM$b3iXj^Aj#MVe_3>ync6danTQaw<3SdvI3<3 zyo4!9=z4pNTgb+n_S}5N+SAks=gBS(xwk&zJGK~TTkQT(c&r{TP~`CYuk zGIbG^S?8@og~yvk_tyEUaG0{%Sw6T$AJ49@EuAhYaOMdz+dU&0H%LEyOOYIvfD5M@ za_b9j0IG@FkX^&b7oA-1Ym3PzYl;k4J``8ZRdKknJ_vl`0lzVA+jB`=ecQ_Btia(d z=Nyyx)AYKHO2@i7yBho2UgmD-N^HK@%5Y#FUoH-RIg_{>1Lc`WT^!LS;8k^MNL1D% zxH(TKR-xmkq2glrRB6Yhb`(`j*5$f6olwJxI(PFGE#OyVbAU*@AJ|2d2P5a^n9o}bGp@Um%A|{Vl0MQj+R2k`5S8m z7^Ss@Aj17I+(H+{by^mSef(CTh-&O;+?&VG83<0ct}UIMIxEG(FGWSaI=*KlInLax zHZ}wB)#%c~t#Dh`nVt0Wv~B{{?g1~5QsRuw4PR`N`p|`HG~wBu9o*`drIE;EkW@da z!JJ+5Pm!6%==0kCM+)$b_HVbxh8ri3i>n_yPJ+K=mNTysO;w?;Cy(ixCb(Ml?|HN? zpEfCN`4;#u!e{mG9FW|0_p@qUP=?-XuMP8@iEkl3Y>DdjpzxgB4lR6j6s1Yno2l;J z?IB9C#PXxm8C=Cd61K|jqMeIUNSIPj7y0n5GDylnyIIkox0XkQ}%~O z;|9awS*d+Y?=%j9C7%^C^@CGJ{Ptntwo>PAqAK_0=5@ZrZ%i?YgoG4g3x_!js0-s2 zG)=7%?A{5~i^L*v7?Y*#I)0{;1qLYEDG(+Vib)#yDx8x4y5e3f6@Y`FN1|e;ZBp?L zqW8hKgfAis+-|#0PapXG<-GSORcJw~aMZhT4GnIMx8)|=uNmww+6^>mSV)qA13D`W zA)!?VcjWast@ILjP;8d~qyM>UPsbU6DOdOG-PQ14jlBa*Qd$!3AR#R!>Ez(#WCw%6 zB;bw`j`q@y4svi?aUXwY4}D#_C~oGtkT_y1MSl&vw*X~GU~1BdHXZ1$B+L@;W#~Ie zzVhNE0?nBU_8V14ZmK?3vc$1ON_*BdO-{Y?-L>RNsmqVlADxub!lk=BjMf(3_5G$y zTOcxO;@+oKG*XzIcNy7G<5u5ZMiYM2&u+E<2k9#a#H-D3$sh+rt|faxri}Qb1WRS3 z$F_N_i~YsaCV$>bX%GMqNAq7(2$-ycj4TWRlXjG~Lm-@F6sB55DLPYm5{d6*P^NZl3JyFFl$%9S?Ax z*!sM5-oEZ6Uvpeq*|+OIH9a~Xd>#8e>bAIgZT`CO>)?b4Y}G~fVbj+3bii_b>!{hE ztH;6hu*^QL*sWy6Ic#Cdtew*z|Vnq9f+NrY|(ELY4!k1&JF`5p$7yF1rn zwnNVqID?yH>DJrL{#<&J^}rH)$6a!Cb`uWC+c@-W9iW%T(luD-c}@>4pYB(LIVxCL z=V-Q>PfVCkY#!BL)^Q$ZUmVqO+|E|gtY65VYCrUybT{9ujfhNP*V%GsW|ZtwvC_uF zN|xb*&%o^_Ps{i+Bt##QD2Vr$KkxaOoJqmE>hZ%`$bivUZQ$l1HJiVVrM=n@yMw2U z0}lzwF%UMyblc}uA@HI%trvhP5`iNe=)yKnOWl?1R#d5;By$N1;x&GXL=>GC-;k3& zPiA_uT7;RwQUrBzK-pZ5kK5--#E^5Tux~r?bzDy8+dzc2<`YK{$jARj;e;CI21>hp zUssrS?r;QzGR95QeLlb#03uOB{T7YI5VDg zWPbR3AV66gLl57R*9by@7yg>vlrca^(V-rLc?*XWTn4?I1WGYuQ;csRA{2bpf^3D9uC=~eVLx= zA!-7cd_-x_oUOwO7I0SJlBPo0JCaH$i2yM zH}PbCcH~qq)d9)=C^4EB9-3Ibh`!x)!tga-^dJ@57VmV z+7R@5jA}6pwdT0VJC@yzS #IMNv(QBzGNps8DuM_6eq4od{S%S`EO^G0CM-)LIG z6rli<#>tL73!ScO< zIo^Zo7NDYC#AV{=PE}QI>06y9K51pe3CMVc-m@ScbhZpj)m@)la8#1ukdaCw zn7L=Mf<6?TDZ3y7b9G^Txr literal 0 HcmV?d00001 From 9b3e52a9726d008a86d30030391415c67ea139e5 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sat, 14 Feb 2026 23:22:41 +0000 Subject: [PATCH 17/46] implement control_stop all --- playground/components/ProjectPlayer.vue | 3 +- playground/lib/project-runner.js | 22 +++++---- src/instructions/control.rs | 1 + src/instructions/control/stop_all.rs | 45 +++++++++++++++++++ src/ir/blocks.rs | 2 +- .../turbowarp-integration-execute.test.mjs | 3 +- 6 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 src/instructions/control/stop_all.rs diff --git a/playground/components/ProjectPlayer.vue b/playground/components/ProjectPlayer.vue index 83e46dd9..a061faff 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -258,7 +258,8 @@ onMounted(async () => { try { loadingMsg.value = "instantiating project"; - runner = await ProjectRunner.init({ + runner = new ProjectRunner(); + await runner.init({ framerate: 30, turbo: turbo.value, wasm_bytes: wasmBytes, diff --git a/playground/lib/project-runner.js b/playground/lib/project-runner.js index dde8027f..e025eff5 100644 --- a/playground/lib/project-runner.js +++ b/playground/lib/project-runner.js @@ -42,7 +42,7 @@ export class ProjectRunner extends EventTarget { #triggerSpriteClicked; monitors; - constructor({ + #initProps({ renderer, timeout, framerate_wait, @@ -50,8 +50,6 @@ export class ProjectRunner extends EventTarget { exports, project_json, }) { - super(); - this.#sensing_answer = exports.sensing_answer; this.#mark_question_resolved_func = exports.mark_waiting_flag; this.#renderer = renderer; @@ -81,11 +79,11 @@ export class ProjectRunner extends EventTarget { sprite: monitor.spriteName, }, ]; - }) ?? [] + }) ?? [], ); } - static async init({ + async init({ framerate = 30, turbo, wasm_bytes, @@ -111,18 +109,17 @@ export class ProjectRunner extends EventTarget { assets, target_names, (question, struct) => { - // runner doesn't exist yet, but it will by the time the function is called - runner.dispatchEvent( + this.dispatchEvent( new CustomEvent("queueQuestion", { detail: { question, struct } }), ); }, (id, value) => { - runner.dispatchEvent( + this.dispatchEvent( new CustomEvent("updateVariableVal", { detail: { id, value } }), ); }, (id, visible) => { - runner.dispatchEvent( + this.dispatchEvent( new CustomEvent("updateVariableVisibility", { detail: { id, visible }, }), @@ -188,7 +185,7 @@ export class ProjectRunner extends EventTarget { console.info("synthetic error to expose wasm module to devtools:", error); } - const runner = new ProjectRunner({ + this.#initProps({ renderer, exports: instance.exports, timeout, @@ -197,7 +194,7 @@ export class ProjectRunner extends EventTarget { project_json, }); - return runner; + return this; } async run() { @@ -228,7 +225,8 @@ export class ProjectRunner extends EventTarget { } while ( Date.now() - thisTickStartTime < this.#framerate_wait * 0.8 && !this.turbo && - this.#requests_refresh.value === 0 + this.#requests_refresh.value === 0 && + this.#threads_count.value > 0 ); this.#requests_refresh.value = 0; this.#renderer.draw(); diff --git a/src/instructions/control.rs b/src/instructions/control.rs index 884a31ab..83af48a9 100644 --- a/src/instructions/control.rs +++ b/src/instructions/control.rs @@ -1,4 +1,5 @@ pub mod get_thread_timeout; pub mod if_else; pub mod r#loop; +pub mod stop_all; pub mod wait; diff --git a/src/instructions/control/stop_all.rs b/src/instructions/control/stop_all.rs new file mode 100644 index 00000000..3d3ad98a --- /dev/null +++ b/src/instructions/control/stop_all.rs @@ -0,0 +1,45 @@ +use wasm_encoder::{ConstExpr, HeapType}; + +use super::super::prelude::*; +use crate::wasm::{GlobalExportable, GlobalMutable, ThreadsTable}; + +pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { + let threads_count = func.registries().globals().register( + "threads_count".into(), + ( + ValType::I32, + ConstExpr::i32_const(0), + GlobalMutable(true), + GlobalExportable(true), + ), + )?; + + let threads_table = func.registries().tables().register::()?; + let thread_struct_type = func.registries().types().thread_struct_type()?; + + Ok(wasm![ + I32Const(0), + GlobalSet(threads_count), + I32Const(0), + RefNull(HeapType::Concrete(thread_struct_type)), + TableSize(threads_table), + TableFill(threads_table), + ]) +} + +pub fn acceptable_inputs() -> HQResult> { + Ok(Rc::from([])) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { + Ok(ReturnType::None) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +pub const fn const_fold( + _inputs: &[ConstFoldItem], + _state: &mut ConstFoldState, +) -> HQResult { + Ok(NotFoldable) +} diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index d69c3438..ec6f4cb3 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -2075,7 +2075,7 @@ fn from_normal_block( ) }; match operator.to_lowercase().as_str() { - "all" => hq_todo!("control_stop all"), + "all" => vec![IrOpcode::control_stop_all], "this script" => vec![IrOpcode::hq_yield(HqYieldFields { mode: if context.warp { YieldMode::Return diff --git a/test/integration/turbowarp-integration-execute.test.mjs b/test/integration/turbowarp-integration-execute.test.mjs index 13088fcb..eed78fb7 100644 --- a/test/integration/turbowarp-integration-execute.test.mjs +++ b/test/integration/turbowarp-integration-execute.test.mjs @@ -172,7 +172,8 @@ describe("Integration tests", () => { // todo: run wasm-opt if specified in flags? // Run the project and once all threads are complete check the results. - const runner = await ProjectRunner.init({ + const runner = new ProjectRunner(); + await runner.init({ wasm_bytes: project_wasm.wasm_bytes, target_names: project_wasm.target_names, project_json, From c9c63a4fec59791cf736852da5dd8d509148122d Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sun, 15 Feb 2026 18:57:29 +0000 Subject: [PATCH 18/46] remove unneeded info from `IrMonitor` --- src/instructions/data/visvariable.rs | 8 +++++--- src/ir.rs | 2 +- src/ir/variable.rs | 13 ------------- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/instructions/data/visvariable.rs b/src/instructions/data/visvariable.rs index c42e8921..908b722e 100644 --- a/src/instructions/data/visvariable.rs +++ b/src/instructions/data/visvariable.rs @@ -70,15 +70,17 @@ pub const fn const_fold( } crate::instructions_test!( - any_global; + test; data_visvariable; - t @ super::Fields { var: RefCell::new( crate::ir::RcVar::new( IrType::Any, crate::sb3::VarVal::Float(0.0), - None, + Some(crate::ir::IrMonitor { + id: "".into(), + is_ever_visible: RefCell::new(true,) + }), ).unwrap()) , visible: true diff --git a/src/ir.rs b/src/ir.rs index 7f60ef06..5a9de866 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -18,4 +18,4 @@ pub use step::{InlinedStep, MaybeInlinedStep, Step, StepIndex}; pub use target::Target; use thread::Thread; pub use types::{ReturnType, Type, base_types, var_val_instruction, var_val_type}; -pub use variable::{RcList, RcVar, used_vars}; +pub use variable::{IrMonitor, RcList, RcVar, used_vars}; diff --git a/src/ir/variable.rs b/src/ir/variable.rs index bc8b3281..9a60223e 100644 --- a/src/ir/variable.rs +++ b/src/ir/variable.rs @@ -23,13 +23,8 @@ pub struct RcVar(Rc); #[derive(Debug)] pub struct IrMonitor { - pub name: Box, pub id: Box, - pub sprite: Option>, - pub visible: bool, pub is_ever_visible: RefCell, - pub x: f64, - pub y: f64, } impl RcVar { @@ -157,14 +152,6 @@ pub fn variables_from_target(target: &Sb3Target, monitors: &[Sb3Monitor]) -> HQR .find(|monitor| monitor.id() == Some(id)) .and_then(|monitor| { Some(IrMonitor { - name: monitor - .params() - .map(|params| params.get("VARIABLE"))?? - .clone(), - sprite: monitor.sprite_name().cloned().flatten(), - visible: *monitor.visible()?, - x: *monitor.x()?, - y: *monitor.y()?, is_ever_visible: RefCell::new(*monitor.visible()?), id: id.clone(), }) From 5e9dc8ddeefe4da00d1f31d7f279d824a9399891 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sun, 15 Feb 2026 19:38:24 +0000 Subject: [PATCH 19/46] lt/gt: try to convert strings to numbers first --- src/instructions/operator/gt.rs | 140 ++++++++++++++++++++++++++++++-- src/instructions/operator/lt.rs | 109 +++++++++++++++++++++++-- 2 files changed, 232 insertions(+), 17 deletions(-) diff --git a/src/instructions/operator/gt.rs b/src/instructions/operator/gt.rs index 9b30d5e5..e8a8b4b5 100644 --- a/src/instructions/operator/gt.rs +++ b/src/instructions/operator/gt.rs @@ -28,7 +28,10 @@ pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult) -> HQResult) -> HQResult) -> HQResult) -> HQResult) -> HQResult) -> HQResult) -> HQResult) -> HQResult) -> HQResult) -> HQResult) -> HQResult) -> HQResult Date: Sun, 15 Feb 2026 19:49:31 +0000 Subject: [PATCH 20/46] consider in const folding that variables may be overwritten in loop bodies --- src/optimisation/const_folding.rs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/optimisation/const_folding.rs b/src/optimisation/const_folding.rs index 7b250cca..b7ceb138 100644 --- a/src/optimisation/const_folding.rs +++ b/src/optimisation/const_folding.rs @@ -54,6 +54,20 @@ pub struct ConstFoldState { pub vars: BTreeMap, ConstFoldItem>, } +impl ConstFoldState { + fn merge(&mut self, other: Self) { + for (var, _) in other.vars { + self.vars.insert( + var, + ConstFoldItem::Unknown { + possible_types: IrType::none(), // this is ok because the unknown value is never actually used + opcodes: Rc::from([]), + }, + ); + } + } +} + fn const_fold_step(step: S, state: &mut ConstFoldState) -> HQResult<()> where S: Deref>, @@ -89,12 +103,18 @@ where { let body_mut = Rc::new(Rc::unwrap_or_clone(body)); let condition_mut = Rc::new(Rc::unwrap_or_clone(condition)); - const_fold_step(Rc::clone(&body_mut), &mut ConstFoldState::default())?; - const_fold_step(Rc::clone(&condition_mut), &mut ConstFoldState::default())?; + let mut body_state = ConstFoldState::default(); + const_fold_step(Rc::clone(&body_mut), &mut body_state)?; + state.merge(body_state); + let mut condition_state = ConstFoldState::default(); + const_fold_step(Rc::clone(&condition_mut), &mut condition_state)?; + state.merge(condition_state); let first_condition_mut = first_condition .map(|first_cond_step| -> HQResult<_> { let first_cond_mut = Rc::new(Rc::unwrap_or_clone(first_cond_step)); - const_fold_step(Rc::clone(&first_cond_mut), &mut ConstFoldState::default())?; + let mut cond_state = ConstFoldState::default(); + const_fold_step(Rc::clone(&first_cond_mut), &mut cond_state)?; + state.merge(cond_state); Ok(first_cond_mut) }) From e6334eb8b5f8125b92aa0dc3443591f1977bddd1 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:58:44 +0000 Subject: [PATCH 21/46] implement control_for_each --- src/instructions.rs | 14 ++- src/instructions/control/loop.rs | 7 ++ src/ir/blocks.rs | 133 ++++++++++++++++++++++++++++- src/optimisation/const_folding.rs | 12 +++ src/optimisation/loop_unrolling.rs | 12 ++- src/optimisation/ssa.rs | 53 ++++++++++++ 6 files changed, 223 insertions(+), 8 deletions(-) diff --git a/src/instructions.rs b/src/instructions.rs index 2b9c0263..e4058d2b 100644 --- a/src/instructions.rs +++ b/src/instructions.rs @@ -166,12 +166,18 @@ impl IrOpcode { first_condition, condition, body, + pre_body, .. }) => Some( - [first_condition.as_mut(), Some(condition), Some(body)] - .into_iter() - .flatten() - .collect(), + [ + first_condition.as_mut(), + Some(condition), + pre_body.as_mut(), + Some(body), + ] + .into_iter() + .flatten() + .collect(), ), _ => None, } diff --git a/src/instructions/control/loop.rs b/src/instructions/control/loop.rs index 6c0336a3..e055219f 100644 --- a/src/instructions/control/loop.rs +++ b/src/instructions/control/loop.rs @@ -10,6 +10,7 @@ pub struct Fields { pub first_condition: Option>>, pub condition: Rc>, pub body: Rc>, + pub pre_body: Option>>, pub flip_if: bool, } @@ -41,6 +42,7 @@ pub fn wasm( condition, body, flip_if, + pre_body, }: &Fields, ) -> HQResult> { let inner_instructions = func.compile_inner_step(Rc::clone(body))?; @@ -50,6 +52,10 @@ pub fn wasm( .unwrap_or_else(|| Rc::clone(condition)), )?; let condition_instructions = func.compile_inner_step(Rc::clone(condition))?; + let pre_body_instructions = pre_body.as_ref().map_or_else( + || Ok(vec![]), + |post_step| func.compile_inner_step(Rc::clone(post_step)), + )?; Ok(wasm![Block(BlockType::Empty),] .into_iter() .chain(first_condition_instructions) @@ -58,6 +64,7 @@ pub fn wasm( } else { wasm![I32Eqz, BrIf(0), Loop(BlockType::Empty)] }) + .chain(pre_body_instructions) .chain(inner_instructions) .chain(condition_instructions) .chain(if *flip_if { diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index ec6f4cb3..9d81912f 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -276,7 +276,9 @@ pub fn input_names(block_info: &BlockInfo, context: &StepContext) -> HQResult vec!["DURATION"], - BlockOpcode::data_setvariableto | BlockOpcode::data_changevariableby => vec!["VALUE"], + BlockOpcode::data_setvariableto + | BlockOpcode::data_changevariableby + | BlockOpcode::control_for_each => vec!["VALUE"], BlockOpcode::operator_random => vec!["FROM", "TO"], BlockOpcode::pen_setPenColorParamTo => vec!["COLOR_PARAM", "VALUE"], BlockOpcode::control_if @@ -586,6 +588,7 @@ fn generate_loop( final_next_blocks: NextBlocks, first_condition_instructions: Option>, condition_instructions: Vec, + pre_body_instructions: Option>, flip_if: bool, setup_instructions: Vec, flags: &WasmFlags, @@ -649,12 +652,22 @@ fn generate_loop( false, ))) }); + let pre_body_step = pre_body_instructions.map(|instrs| { + Rc::new(RefCell::new(Step::new( + None, + context.clone(), + instrs, + context.target().project(), + false, + ))) + }); Ok(setup_instructions .into_iter() .chain(vec![IrOpcode::control_loop(ControlLoopFields { first_condition: first_condition_step, condition: condition_step, body: substack_step, + pre_body: pre_body_step, flip_if, })]) .collect()) @@ -684,6 +697,12 @@ fn generate_loop( branch_else: Rc::clone(if flip_if { &substack_step } else { &next_step }), })); let condition_step_index = project.new_owned_step(condition_step)?; + if let Some(pre_body_blocks) = pre_body_instructions { + substack_step + .try_borrow_mut()? + .opcodes_mut() + .extend(pre_body_blocks); + } if let Some(block) = substack_block { let substack_blocks = from_block( block, @@ -2172,6 +2191,7 @@ fn from_normal_block( final_next_blocks.clone(), first_condition_instructions, condition_instructions, + None, false, vec![], flags, @@ -2213,6 +2233,115 @@ fn from_normal_block( final_next_blocks.clone(), first_condition_instructions, condition_instructions, + None, + false, + setup_instructions, + flags, + )? + } + BlockOpcode::control_for_each => { + let sb3::Field::ValueId(_val, maybe_id) = + block_info.fields.get("VARIABLE").ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - missing field VARIABLE" + ) + })? + else { + hq_bad_proj!( + "invalid project.json - missing variable id for VARIABLE field" + ); + }; + let id = maybe_id.clone().ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - null variable id for VARIABLE field" + ) + })?; + let target = context.target(); + let variable = if let Some(var) = target.variables().get(&id) { + var.clone() + } else if let Some(var) = context + .target() + .project() + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .global_variables() + .get(&id) + { + var.clone() + } else { + hq_bad_proj!("variable not found") + }; + *variable.is_used.try_borrow_mut()? = true; + let counter = RcVar::new(IrType::Int, sb3::VarVal::Int(0), None)?; + let local = context.warp; + let condition_instructions = vec![ + IrOpcode::data_variable(DataVariableFields { + var: RefCell::new(counter.clone()), + local_read: RefCell::new(local), + }), + IrOpcode::hq_integer(HqIntegerFields(1)), + IrOpcode::operator_add, + IrOpcode::data_teevariable(DataTeevariableFields { + var: RefCell::new(counter.clone()), + local_read_write: RefCell::new(local), + }), + ] + .into_iter() + .chain(inputs( + block_info, + blocks, + context, + &context.target().project(), + flags, + )?) + .chain(vec![IrOpcode::operator_lt]) + .collect(); + let first_condition_instructions = Some( + vec![IrOpcode::data_variable(DataVariableFields { + var: RefCell::new(counter.clone()), + local_read: RefCell::new(local), + })] + .into_iter() + .chain(inputs( + block_info, + blocks, + context, + &context.target().project(), + flags, + )?) + .chain(vec![IrOpcode::operator_lt]) + .collect(), + ); + let setup_instructions = vec![ + IrOpcode::hq_drop, + IrOpcode::hq_integer(HqIntegerFields(0)), + IrOpcode::data_setvariableto(DataSetvariabletoFields { + var: RefCell::new(counter.clone()), + local_write: RefCell::new(local), + }), + ]; + let pre_body_instructions = Some(vec![ + IrOpcode::data_variable(DataVariableFields { + var: RefCell::new(counter), + local_read: RefCell::new(local), + }), + IrOpcode::hq_integer(HqIntegerFields(1)), + IrOpcode::operator_add, + IrOpcode::data_setvariableto(DataSetvariabletoFields { + var: RefCell::new(variable.var.clone()), + local_write: RefCell::new(false), + }), + ]); + generate_loop( + context.warp, + &mut should_break, + block_info, + blocks, + context, + final_next_blocks.clone(), + first_condition_instructions, + condition_instructions, + pre_body_instructions, false, setup_instructions, flags, @@ -2237,6 +2366,7 @@ fn from_normal_block( final_next_blocks.clone(), first_condition_instructions, condition_instructions, + None, true, setup_instructions, flags, @@ -2261,6 +2391,7 @@ fn from_normal_block( final_next_blocks.clone(), first_condition_instructions, condition_instructions, + None, false, setup_instructions, flags, diff --git a/src/optimisation/const_folding.rs b/src/optimisation/const_folding.rs index b7ceb138..894da8de 100644 --- a/src/optimisation/const_folding.rs +++ b/src/optimisation/const_folding.rs @@ -99,10 +99,21 @@ where first_condition, body, flip_if, + pre_body, }) = opcode { let body_mut = Rc::new(Rc::unwrap_or_clone(body)); let condition_mut = Rc::new(Rc::unwrap_or_clone(condition)); + let pre_body_mut = pre_body + .as_ref() + .map(|pre_body_real| -> HQResult<_> { + let pre_body_mut = Rc::new(Rc::unwrap_or_clone(Rc::clone(pre_body_real))); + let mut pre_body_state = ConstFoldState::default(); + const_fold_step(Rc::clone(&pre_body_mut), &mut pre_body_state)?; + state.merge(pre_body_state); + Ok(pre_body_mut) + }) + .transpose()?; let mut body_state = ConstFoldState::default(); const_fold_step(Rc::clone(&body_mut), &mut body_state)?; state.merge(body_state); @@ -125,6 +136,7 @@ where condition: condition_mut, first_condition: first_condition_mut, flip_if, + pre_body: pre_body_mut, }); } diff --git a/src/optimisation/loop_unrolling.rs b/src/optimisation/loop_unrolling.rs index 99773bde..924a1a2e 100644 --- a/src/optimisation/loop_unrolling.rs +++ b/src/optimisation/loop_unrolling.rs @@ -31,6 +31,7 @@ where condition, ref body, flip_if, + pre_body, }, ) in loops { @@ -51,15 +52,20 @@ where branch_if: Rc::new(RefCell::new(Step::new( None, step.try_borrow()?.context().clone(), - body.try_borrow()? - .opcodes() - .clone() + pre_body + .as_ref() + .map_or_else( + || -> HQResult<_> { Ok(vec![]) }, + |pb| Ok(pb.try_borrow()?.opcodes().clone()), + )? .into_iter() + .chain(body.try_borrow()?.opcodes().clone()) .chain([IrOpcode::control_loop(ControlLoopFields { first_condition: None, condition: condition.clone(), body: Rc::clone(body), flip_if, + pre_body: pre_body.clone(), })]) .collect(), step.try_borrow()?.project(), diff --git a/src/optimisation/ssa.rs b/src/optimisation/ssa.rs index 318cbee6..af413e4c 100644 --- a/src/optimisation/ssa.rs +++ b/src/optimisation/ssa.rs @@ -536,6 +536,7 @@ impl VarGraph { condition, body, flip_if, + pre_body, }) => { let new_var_map: BTreeMap<_, _> = step .try_borrow()? @@ -642,6 +643,31 @@ impl VarGraph { self.add_edge(first_cond_exit, header_node, EdgeType::Forward); *self.exit_node().borrow_mut() = header_node; + let pre_body_mut = pre_body + .as_ref() + .map(|pre_body_real| -> HQResult<_> { + let pre_body_mut = + Rc::new(Rc::unwrap_or_clone(Rc::clone(pre_body_real))); + let mut pre_body_variable_maps = variable_maps.clone(); + graphs.insert( + pre_body_mut.try_borrow()?.id().into(), + MaybeGraph::Started, + ); + self.visit_step( + Rc::clone(&pre_body_mut), + &mut pre_body_variable_maps, + graphs, + type_stack, + next_steps, + )?; + graphs.insert( + pre_body_mut.try_borrow()?.id().into(), + MaybeGraph::Inlined, + ); + Ok(pre_body_mut) + }) + .transpose()?; + let body_mut = Rc::new(Rc::unwrap_or_clone(Rc::clone(body))); let mut body_variable_maps = variable_maps.clone(); graphs.insert(body_mut.try_borrow()?.id().into(), MaybeGraph::Started); @@ -681,6 +707,7 @@ impl VarGraph { first_condition: Some(first_condition_mut), flip_if: *flip_if, condition: condition_mut, + pre_body: pre_body_mut, }), )); @@ -705,6 +732,31 @@ impl VarGraph { graphs.insert(condition_mut.try_borrow()?.id().into(), MaybeGraph::Inlined); type_stack.clear(); + let pre_body_mut = pre_body + .as_ref() + .map(|pre_body_real| -> HQResult<_> { + let pre_body_mut = + Rc::new(Rc::unwrap_or_clone(Rc::clone(pre_body_real))); + let mut pre_body_variable_maps = variable_maps.clone(); + graphs.insert( + pre_body_mut.try_borrow()?.id().into(), + MaybeGraph::Started, + ); + self.visit_step( + Rc::clone(&pre_body_mut), + &mut pre_body_variable_maps, + graphs, + type_stack, + next_steps, + )?; + graphs.insert( + pre_body_mut.try_borrow()?.id().into(), + MaybeGraph::Inlined, + ); + Ok(pre_body_mut) + }) + .transpose()?; + let body_mut = Rc::new(Rc::unwrap_or_clone(Rc::clone(body))); graphs.insert(body_mut.try_borrow()?.id().into(), MaybeGraph::Started); self.visit_step( @@ -733,6 +785,7 @@ impl VarGraph { body: body_mut, first_condition: None, condition: condition_mut, + pre_body: pre_body_mut, }), )); } From d7a6dd6a8dc36e911dd19b1875da239930a9ba84 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sun, 15 Feb 2026 21:01:14 +0000 Subject: [PATCH 22/46] consider that projects may have faield to compile when unmounting player --- playground/components/ProjectPlayer.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/components/ProjectPlayer.vue b/playground/components/ProjectPlayer.vue index a061faff..928f1c8d 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -155,7 +155,7 @@ onBeforeUnmount(() => { document.removeEventListener("mousemove", mouseMove); canvas.value.removeEventListener("mousedown", mouseDown); canvas.value.removeEventListener("mouseup", mouseUp); - runner.stop(); + runner?.stop?.(); unsetup(); }); From 2e1625a5921333f116d01efab8ad9b72d3602e56 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:21:33 +0000 Subject: [PATCH 23/46] add mousedown, mousex, mousey --- src/instructions/sensing.rs | 3 +++ src/instructions/sensing/mousedown.rs | 38 +++++++++++++++++++++++++++ src/instructions/sensing/mousex.rs | 38 +++++++++++++++++++++++++++ src/instructions/sensing/mousey.rs | 38 +++++++++++++++++++++++++++ src/ir/blocks.rs | 8 +++++- 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/instructions/sensing/mousedown.rs create mode 100644 src/instructions/sensing/mousex.rs create mode 100644 src/instructions/sensing/mousey.rs diff --git a/src/instructions/sensing.rs b/src/instructions/sensing.rs index 6ac15a6c..ae723488 100644 --- a/src/instructions/sensing.rs +++ b/src/instructions/sensing.rs @@ -1,5 +1,8 @@ pub mod answer; pub mod askandwait; pub mod dayssince2000; +pub mod mousedown; +pub mod mousex; +pub mod mousey; pub mod reset_timer; pub mod timer; diff --git a/src/instructions/sensing/mousedown.rs b/src/instructions/sensing/mousedown.rs new file mode 100644 index 00000000..f6c46e82 --- /dev/null +++ b/src/instructions/sensing/mousedown.rs @@ -0,0 +1,38 @@ +use wasm_encoder::ConstExpr; + +use super::super::prelude::*; +use crate::wasm::{GlobalExportable, GlobalMutable}; + +pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { + let global_index = func.registries().globals().register( + "mouseDown".into(), + ( + ValType::I32, + ConstExpr::i32_const(0), + GlobalMutable(true), + GlobalExportable(true), + ), + )?; + Ok(wasm![ + #LazyGlobalGet(global_index), + ]) +} + +pub fn acceptable_inputs() -> HQResult> { + Ok(Rc::from([])) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { + Ok(Singleton(IrType::Boolean)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +pub const fn const_fold( + _inputs: &[ConstFoldItem], + _state: &mut ConstFoldState, +) -> HQResult { + Ok(NotFoldable) +} + +crate::instructions_test! {tests; sensing_mousedown; ;} diff --git a/src/instructions/sensing/mousex.rs b/src/instructions/sensing/mousex.rs new file mode 100644 index 00000000..3b738091 --- /dev/null +++ b/src/instructions/sensing/mousex.rs @@ -0,0 +1,38 @@ +use wasm_encoder::ConstExpr; + +use super::super::prelude::*; +use crate::wasm::{GlobalExportable, GlobalMutable}; + +pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { + let global_index = func.registries().globals().register( + "mouseX".into(), + ( + ValType::F64, + ConstExpr::f64_const(0.0.into()), + GlobalMutable(true), + GlobalExportable(true), + ), + )?; + Ok(wasm![ + #LazyGlobalGet(global_index), + ]) +} + +pub fn acceptable_inputs() -> HQResult> { + Ok(Rc::from([])) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { + Ok(Singleton(IrType::FloatPos)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +pub const fn const_fold( + _inputs: &[ConstFoldItem], + _state: &mut ConstFoldState, +) -> HQResult { + Ok(NotFoldable) +} + +crate::instructions_test! {tests; sensing_mousex; ;} diff --git a/src/instructions/sensing/mousey.rs b/src/instructions/sensing/mousey.rs new file mode 100644 index 00000000..9272fbdc --- /dev/null +++ b/src/instructions/sensing/mousey.rs @@ -0,0 +1,38 @@ +use wasm_encoder::ConstExpr; + +use super::super::prelude::*; +use crate::wasm::{GlobalExportable, GlobalMutable}; + +pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { + let global_index = func.registries().globals().register( + "mouseY".into(), + ( + ValType::F64, + ConstExpr::f64_const(0.0.into()), + GlobalMutable(true), + GlobalExportable(true), + ), + )?; + Ok(wasm![ + #LazyGlobalGet(global_index), + ]) +} + +pub fn acceptable_inputs() -> HQResult> { + Ok(Rc::from([])) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { + Ok(Singleton(IrType::FloatPos)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +pub const fn const_fold( + _inputs: &[ConstFoldItem], + _state: &mut ConstFoldState, +) -> HQResult { + Ok(NotFoldable) +} + +crate::instructions_test! {tests; sensing_mousey; ;} diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index 9d81912f..0785d6b4 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -270,7 +270,10 @@ pub fn input_names(block_info: &BlockInfo, context: &StepContext) -> HQResult vec![], + | BlockOpcode::data_hidevariable + | BlockOpcode::sensing_mousex + | BlockOpcode::sensing_mousey + | BlockOpcode::sensing_mousedown => vec![], BlockOpcode::sensing_askandwait => vec!["QUESTION"], BlockOpcode::event_broadcast | BlockOpcode::event_broadcastandwait => { vec!["BROADCAST_INPUT"] @@ -1346,6 +1349,9 @@ fn from_normal_block( BlockOpcode::operator_letter_of => vec![IrOpcode::operator_letter_of], BlockOpcode::sensing_dayssince2000 => vec![IrOpcode::sensing_dayssince2000], BlockOpcode::sensing_timer => vec![IrOpcode::sensing_timer], + BlockOpcode::sensing_mousex => vec![IrOpcode::sensing_mousex], + BlockOpcode::sensing_mousey => vec![IrOpcode::sensing_mousey], + BlockOpcode::sensing_mousedown => vec![IrOpcode::sensing_mousedown], BlockOpcode::sensing_answer => vec![IrOpcode::sensing_answer], BlockOpcode::sensing_resettimer => vec![IrOpcode::sensing_reset_timer], BlockOpcode::operator_lt => vec![IrOpcode::operator_lt], From 5393844d3d645522db65326ffdaaf695feae4eab Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:23:11 +0000 Subject: [PATCH 24/46] implement operator_round --- src/instructions/operator.rs | 1 + src/instructions/operator/round.rs | 41 ++++++++++++++++++++++++++++++ src/ir/blocks.rs | 3 ++- 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/instructions/operator/round.rs diff --git a/src/instructions/operator.rs b/src/instructions/operator.rs index 9357401c..ce821e37 100644 --- a/src/instructions/operator.rs +++ b/src/instructions/operator.rs @@ -24,6 +24,7 @@ pub mod not; pub mod or; pub mod pow10; pub mod random; +pub mod round; pub mod sin; pub mod sqrt; pub mod subtract; diff --git a/src/instructions/operator/round.rs b/src/instructions/operator/round.rs new file mode 100644 index 00000000..5f2c297a --- /dev/null +++ b/src/instructions/operator/round.rs @@ -0,0 +1,41 @@ +use super::super::prelude::*; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 1); + let t1 = inputs[0]; + Ok(if IrType::QuasiInt.contains(t1) { + wasm![] + } else if IrType::Float.contains(t1) { + wasm![ + @nanreduce(t1), + F64Nearest, + ] + } else { + hq_bug!("bad input: {:?}", inputs) + }) +} + +pub fn acceptable_inputs() -> HQResult> { + Ok(Rc::from([IrType::Number])) +} + +pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { + hq_assert_eq!(inputs.len(), 1); + let t1 = inputs[0]; + Ok(Singleton(if IrType::QuasiInt.contains(t1) { + t1 + } else { + IrType::Float + })) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +pub const fn const_fold( + _inputs: &[ConstFoldItem], + _state: &mut ConstFoldState, +) -> HQResult { + Ok(NotFoldable) +} + +crate::instructions_test! {tests; operator_round; t } diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index 0785d6b4..c76ef0d9 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -229,7 +229,7 @@ pub fn input_names(block_info: &BlockInfo, context: &StepContext) -> HQResult vec!["NUM1", "NUM2"], - BlockOpcode::operator_mathop => vec!["NUM"], + BlockOpcode::operator_mathop | BlockOpcode::operator_round => vec!["NUM"], BlockOpcode::operator_lt | BlockOpcode::operator_gt | BlockOpcode::operator_equals @@ -1360,6 +1360,7 @@ fn from_normal_block( BlockOpcode::operator_not => vec![IrOpcode::operator_not], BlockOpcode::operator_and => vec![IrOpcode::operator_and], BlockOpcode::operator_or => vec![IrOpcode::operator_or], + BlockOpcode::operator_round => vec![IrOpcode::operator_round], BlockOpcode::operator_mathop => { let (sb3::Field::Value((Some(val),)) | sb3::Field::ValueId(Some(val), _)) = From 7536e2988e5005736dc2a29b5be286715ff7d145 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:24:16 +0000 Subject: [PATCH 25/46] allow calling nonexistent procedures as noop --- src/ir/blocks.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index c76ef0d9..08cf535d 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -302,7 +302,7 @@ pub fn input_names(block_info: &BlockInfo, context: &StepContext) -> HQResult { vec!["INDEX", "ITEM"] } - BlockOpcode::procedures_call => { + BlockOpcode::procedures_call => 'proc_block: { let serde_json::Value::String(proccode) = block_info .mutation .mutations @@ -312,7 +312,7 @@ pub fn input_names(block_info: &BlockInfo, context: &StepContext) -> HQResult { + BlockOpcode::procedures_call => 'proc_block: { let target = context.target(); let procs = target.procedures()?; let serde_json::Value::String(proccode) = block_info @@ -2417,9 +2417,9 @@ fn from_normal_block( else { hq_bad_proj!("non-string proccode on procedures_call") }; - let proc = procs.get(proccode.as_str()).ok_or_else(|| { - make_hq_bad_proj!("non-existant proccode on procedures_call") - })?; + let Some(proc) = procs.get(proccode.as_str()) else { + break 'proc_block vec![]; + }; let warp = context.warp || proc.always_warped(); if warp { proc.compile_warped(blocks, flags)?; From 8d9a61acef183db70184c26c5ae0203f8d5e27eb Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:47:51 +0000 Subject: [PATCH 26/46] avoid creating dummy steps in `generate_if_else` where possible --- src/ir/blocks.rs | 382 ++++++++++++++++++++++++----------------------- 1 file changed, 192 insertions(+), 190 deletions(-) diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index 08cf535d..747d000b 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -762,221 +762,223 @@ fn generate_if_else( // ) // .as_str(), // ); - let this_project = context.project()?; - let dummy_project = Rc::new(IrProject::new( - this_project.global_variables().clone(), - this_project.global_lists().clone(), - Box::from(this_project.broadcasts()), - 0, - vec![], - )); - let dummy_target = Rc::new(Target::new( - false, - context.target().variables().clone(), - context.target().lists().clone(), - Rc::downgrade(&dummy_project), - RefCell::new(context.target().procedures()?.clone()), - 0, - context.target().costumes().into(), - )); - dummy_project - .targets() - .try_borrow_mut() - .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? - .insert("".into(), Rc::clone(&dummy_target)); - let dummy_context = StepContext { - target: Rc::clone(&dummy_target), - ..context.clone() - }; - let dummy_if_step = Step::from_block( - if_block.0, - if_block.1.clone(), - blocks, - &dummy_context, - &Rc::downgrade(&dummy_project), - NextBlocks::new(false), - false, - flags, - )?; - let dummy_else_step = if let Some((else_block, else_block_id)) = maybe_else_block.clone() { - Step::from_block( - else_block, - else_block_id, + + // let if_step_yields = dummy_if_step.does_yield()?; + // let else_step_yields = dummy_else_step.does_yield()?; + // crate::log(format!("if yields: {if_step_yields}, else yields: {else_step_yields}").as_str()); + if !context.warp { + let this_project = context.project()?; + let dummy_project = Rc::new(IrProject::new( + this_project.global_variables().clone(), + this_project.global_lists().clone(), + Box::from(this_project.broadcasts()), + 0, + vec![], + )); + let dummy_target = Rc::new(Target::new( + false, + context.target().variables().clone(), + context.target().lists().clone(), + Rc::downgrade(&dummy_project), + RefCell::new(context.target().procedures()?.clone()), + 0, + context.target().costumes().into(), + )); + dummy_project + .targets() + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? + .insert("".into(), Rc::clone(&dummy_target)); + let dummy_context = StepContext { + target: Rc::clone(&dummy_target), + ..context.clone() + }; + let dummy_if_step = Step::from_block( + if_block.0, + if_block.1.clone(), blocks, &dummy_context, &Rc::downgrade(&dummy_project), NextBlocks::new(false), false, flags, - )? - } else { - Step::new_empty( - Rc::downgrade(&dummy_project), - false, - Rc::clone(&dummy_target), - ) - }; - // let if_step_yields = dummy_if_step.does_yield()?; - // let else_step_yields = dummy_else_step.does_yield()?; - // crate::log(format!("if yields: {if_step_yields}, else yields: {else_step_yields}").as_str()); - if !context.warp && (dummy_if_step.does_yield() || dummy_else_step.does_yield()) { - // TODO: ideally if only one branch yields then we'd duplicate the next step and put one - // version inline after the branch, and the other tagged on in the substep's NextBlocks - // as usual, to allow for extra variable type optimisations. - #[expect( - clippy::option_if_let_else, - reason = "map_or_else alternative is too complex" - )] - let (next_block, next_blocks) = if let Some(ref next_block) = block_info.next { - // crate::log("got next block from block_info.next"); - ( - Some(NextBlock::ID(next_block.clone())), - final_next_blocks.extend_with_inner(NextBlockInfo { - yield_first: false, - block: NextBlock::ID(next_block.clone()), - }), - ) - } else if let (Some(next_block_info), _) = final_next_blocks.clone().pop_inner() { - // crate::log("got next block from popping from final_next_blocks"); - (Some(next_block_info.block), final_next_blocks.clone()) - } else { - // crate::log("no next block found"); - ( - None, - final_next_blocks.clone(), // preserve termination behaviour - ) - }; - let final_if_step = { - Step::from_block( - if_block.0, - if_block.1, - blocks, - context, - &context.target().project(), - next_blocks.clone(), - false, - flags, - )? - // crate::log("recompiled if_step with correct next blocks"); - }; - let final_else_step = if let Some((else_block, else_block_id)) = maybe_else_block { + )?; + let dummy_else_step = if let Some((else_block, else_block_id)) = maybe_else_block.clone() { Step::from_block( else_block, else_block_id, blocks, - context, - &context.target().project(), - next_blocks, + &dummy_context, + &Rc::downgrade(&dummy_project), + NextBlocks::new(false), false, flags, )? - // crate::log("recompiled else step with correct next blocks"); } else { - let opcode = match next_block { - Some(NextBlock::ID(id)) => { - let next_block = blocks - .get(&id) - .ok_or_else(|| make_hq_bad_proj!("missing next block"))?; - // crate::log( - // format!("got NextBlock::Id({id:?}), creating step from_block").as_str(), - // ); - vec![IrOpcode::hq_yield(HqYieldFields { - mode: YieldMode::Inline(Rc::new(RefCell::new(Step::from_block( - next_block, - id.clone(), - blocks, - context, - &context.target().project(), - next_blocks, - false, - flags, - )?))), - })] - } - Some(NextBlock::Step(mut step)) => { - step.make_inlined(); - // crate::log(format!("got NextBlock::Step({:?})", rcstep.id()).as_str()); + Step::new_empty( + Rc::downgrade(&dummy_project), + false, + Rc::clone(&dummy_target), + ) + }; + if dummy_if_step.does_yield() || dummy_else_step.does_yield() { + // TODO: ideally if only one branch yields then we'd duplicate the next step and put one + // version inline after the branch, and the other tagged on in the substep's NextBlocks + // as usual, to allow for extra variable type optimisations. + #[expect( + clippy::option_if_let_else, + reason = "map_or_else alternative is too complex" + )] + let (next_block, next_blocks) = if let Some(ref next_block) = block_info.next { + // crate::log("got next block from block_info.next"); + ( + Some(NextBlock::ID(next_block.clone())), + final_next_blocks.extend_with_inner(NextBlockInfo { + yield_first: false, + block: NextBlock::ID(next_block.clone()), + }), + ) + } else if let (Some(next_block_info), _) = final_next_blocks.clone().pop_inner() { + // crate::log("got next block from popping from final_next_blocks"); + (Some(next_block_info.block), final_next_blocks.clone()) + } else { + // crate::log("no next block found"); + ( + None, + final_next_blocks.clone(), // preserve termination behaviour + ) + }; + let final_if_step = { + Step::from_block( + if_block.0, + if_block.1, + blocks, + context, + &context.target().project(), + next_blocks.clone(), + false, + flags, + )? + // crate::log("recompiled if_step with correct next blocks"); + }; + let final_else_step = if let Some((else_block, else_block_id)) = maybe_else_block { + Step::from_block( + else_block, + else_block_id, + blocks, + context, + &context.target().project(), + next_blocks, + false, + flags, + )? + // crate::log("recompiled else step with correct next blocks"); + } else { + let opcode = match next_block { + Some(NextBlock::ID(id)) => { + let next_block = blocks + .get(&id) + .ok_or_else(|| make_hq_bad_proj!("missing next block"))?; + // crate::log( + // format!("got NextBlock::Id({id:?}), creating step from_block").as_str(), + // ); + vec![IrOpcode::hq_yield(HqYieldFields { + mode: YieldMode::Inline(Rc::new(RefCell::new(Step::from_block( + next_block, + id.clone(), + blocks, + context, + &context.target().project(), + next_blocks, + false, + flags, + )?))), + })] + } + Some(NextBlock::Step(mut step)) => { + step.make_inlined(); + // crate::log(format!("got NextBlock::Step({:?})", rcstep.id()).as_str()); - vec![IrOpcode::hq_yield(HqYieldFields { - mode: YieldMode::Inline(Rc::new(RefCell::new(step))), - })] - } - Some(NextBlock::StepIndex(step_index)) => { - let mut step = context - .project()? - .steps() - .try_borrow()? - .get(step_index.0) - .ok_or_else(|| make_hq_bug!("step index out of bounds"))? - .try_borrow()? - .clone(); - step.make_inlined(); - vec![IrOpcode::hq_yield(HqYieldFields { - mode: YieldMode::Inline(Rc::new(RefCell::new(step))), - })] - } - None => { - // crate::log("no next block after if!"); - if next_blocks.terminating() { - // crate::log("terminating after if/else\n"); vec![IrOpcode::hq_yield(HqYieldFields { - mode: YieldMode::None, + mode: YieldMode::Inline(Rc::new(RefCell::new(step))), })] - } else { - // crate::log("not terminating, at end of if/else"); - vec![] } - } + Some(NextBlock::StepIndex(step_index)) => { + let mut step = context + .project()? + .steps() + .try_borrow()? + .get(step_index.0) + .ok_or_else(|| make_hq_bug!("step index out of bounds"))? + .try_borrow()? + .clone(); + step.make_inlined(); + vec![IrOpcode::hq_yield(HqYieldFields { + mode: YieldMode::Inline(Rc::new(RefCell::new(step))), + })] + } + None => { + // crate::log("no next block after if!"); + if next_blocks.terminating() { + // crate::log("terminating after if/else\n"); + vec![IrOpcode::hq_yield(HqYieldFields { + mode: YieldMode::None, + })] + } else { + // crate::log("not terminating, at end of if/else"); + vec![] + } + } + }; + Step::new( + None, + context.clone(), + opcode, + context.target().project(), + false, + ) }; - Step::new( - None, - context.clone(), - opcode, - context.target().project(), - false, - ) - }; - *should_break = true; - Ok(vec![IrOpcode::control_if_else(ControlIfElseFields { - branch_if: Rc::new(RefCell::new(final_if_step)), - branch_else: Rc::new(RefCell::new(final_else_step)), - })]) - } else { - let final_if_step = Step::from_block( - if_block.0, - if_block.1, + *should_break = true; + return Ok(vec![IrOpcode::control_if_else(ControlIfElseFields { + branch_if: Rc::new(RefCell::new(final_if_step)), + branch_else: Rc::new(RefCell::new(final_else_step)), + })]); + } + } + let final_if_step = Step::from_block( + if_block.0, + if_block.1, + blocks, + context, + &context.target().project(), + NextBlocks::new(false), + false, + flags, + )?; + let final_else_step = if let Some((else_block, else_block_id)) = maybe_else_block { + Step::from_block( + else_block, + else_block_id, blocks, context, &context.target().project(), NextBlocks::new(false), false, flags, - )?; - let final_else_step = if let Some((else_block, else_block_id)) = maybe_else_block { - Step::from_block( - else_block, - else_block_id, - blocks, - context, - &context.target().project(), - NextBlocks::new(false), - false, - flags, - )? - } else { - Step::new( - None, - context.clone(), - vec![], - context.target().project(), - false, - ) - }; - Ok(vec![IrOpcode::control_if_else(ControlIfElseFields { - branch_if: Rc::new(RefCell::new(final_if_step)), - branch_else: Rc::new(RefCell::new(final_else_step)), - })]) - } + )? + } else { + Step::new( + None, + context.clone(), + vec![], + context.target().project(), + false, + ) + }; + Ok(vec![IrOpcode::control_if_else(ControlIfElseFields { + branch_if: Rc::new(RefCell::new(final_if_step)), + branch_else: Rc::new(RefCell::new(final_else_step)), + })]) } fn generate_list_index_op( From 342a4d7fa33abdeeaa1be0c03d0c75ee8f557b8f Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:18:40 +0000 Subject: [PATCH 27/46] add costume name block --- src/instructions/looks.rs | 1 + src/instructions/looks/costumename.rs | 67 ++++++++++++++++++++++ src/instructions/tests.rs | 6 +- src/ir/blocks.rs | 2 +- src/registry.rs | 25 ++++++++ src/wasm/func.rs | 22 ++++++- src/wasm/project.rs | 82 ++++++++++++++++++++++++++- 7 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 src/instructions/looks/costumename.rs diff --git a/src/instructions/looks.rs b/src/instructions/looks.rs index 200dbc39..2f143123 100644 --- a/src/instructions/looks.rs +++ b/src/instructions/looks.rs @@ -1,4 +1,5 @@ pub mod backdropnumber; +pub mod costumename; pub mod costumenumber; pub mod say; pub mod setsizeto; diff --git a/src/instructions/looks/costumename.rs b/src/instructions/looks/costumename.rs new file mode 100644 index 00000000..266be5d5 --- /dev/null +++ b/src/instructions/looks/costumename.rs @@ -0,0 +1,67 @@ +use wasm_encoder::MemArg; + +use super::super::prelude::*; +use crate::wasm::registries::tables::TableOptions; +use crate::wasm::{StepTarget, mem_layout}; + +pub fn wasm(func: &StepFunc, _inputs: Rc<[IrType]>) -> HQResult> { + let wasm_target_index = match func.target() { + StepTarget::Sprite(index) => index, + StepTarget::Stage => 0, + }; + let offset = mem_layout::stage::BLOCK_SIZE + + wasm_target_index * mem_layout::sprite::BLOCK_SIZE + + mem_layout::sprite::COSTUME; + + let costume_names = func + .costume_names() + .get(func.target_index() as usize) + .ok_or_else(|| make_hq_bug!("target index out of bounds in costume names vec"))?; + + let costume_name_table = func.registries().tables().register_dyn( + format!("costume_names_{}", func.target_index()).into_boxed_str(), + TableOptions { + element_type: RefType::EXTERNREF, + min: costume_names.len() as u64, + max: Some(costume_names.len() as u64), + init: None, + export_name: None, + }, + )?; + + for costume_name in costume_names { + // make sure that strings are all registered so that the element segment can refer to globals that actually exist + func.registries() + .strings() + .register_default::(costume_name.clone())?; + } + + Ok(wasm![ + I32Const(0), + I32Load(MemArg { + offset: offset.into(), + align: 2, + memory_index: 0, + }), + TableGet(costume_name_table), + ]) +} + +pub fn acceptable_inputs() -> HQResult> { + Ok(Rc::from([])) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult { + Ok(ReturnType::Singleton(IrType::String)) +} + +pub const REQUESTS_SCREEN_REFRESH: bool = false; + +pub const fn const_fold( + _inputs: &[ConstFoldItem], + _state: &mut ConstFoldState, +) -> HQResult { + Ok(NotFoldable) +} + +// crate::instructions_test! {tests; looks_costumename; ; } diff --git a/src/instructions/tests.rs b/src/instructions/tests.rs index 58fbb6fc..3430966a 100644 --- a/src/instructions/tests.rs +++ b/src/instructions/tests.rs @@ -68,7 +68,7 @@ macro_rules! instructions_test { for ($($type_arg,)*) in types_iter(true) { let output_type_result = output_type(Rc::from([$($type_arg,)*]), $(&$fields)?); let registries = Rc::new(Registries::default()); - let step_func = StepFunc::new(Rc::clone(®istries), flags(), StepTarget::Sprite(0), 0); + let step_func = StepFunc::new(Rc::clone(®istries), flags(), StepTarget::Sprite(0), 0, Rc::new(vec![])); let wasm_result = wasm(&step_func, Rc::from([$($type_arg,)*]), $(&$fields)?); match (output_type_result.clone(), wasm_result.clone()) { (Err(..), Ok(..)) | (Ok(..), Err(..)) => panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)*), output_type_result, wasm_result), @@ -100,7 +100,7 @@ macro_rules! instructions_test { ReturnType::MultiValue(outputs) => outputs.iter().copied().map(WasmProject::ir_type_to_wasm).collect::>()?, ReturnType::None => vec![], }; - let step_func = StepFunc::new_with_types(params.into(), result.into(), Rc::clone(®istries), flags(), StepTarget::Sprite(0), 0); + let step_func = StepFunc::new_with_types(params.into(), result.into(), Rc::clone(®istries), flags(), StepTarget::Sprite(0), 0, Rc::new(vec![])); let Ok(wasm) = wasm(&step_func, Rc::from([$($type_arg,)*]), $(&$fields)?) else { println!("skipping failed wasm"); continue; @@ -142,7 +142,7 @@ macro_rules! instructions_test { ReturnType::None => vec![], }; println!("{result:?}"); - let step_func = StepFunc::new_with_types(params.into(), result.into(), Rc::clone(®istries), flags(), StepTarget::Sprite(0), 0); + let step_func = StepFunc::new_with_types(params.into(), result.into(), Rc::clone(®istries), flags(), StepTarget::Sprite(0), 0, Rc::new(vec![])); let wasm = match $crate::instructions::wrap_instruction(&step_func, Rc::from([$($type_arg,)*]), &$crate::instructions::IrOpcode::$opcode$(($fields))?) { Ok(a) => a, Err(e) => { diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index 747d000b..ffe0ec0d 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -2524,7 +2524,7 @@ fn from_normal_block( }; match &*number_name { "number" => vec![IrOpcode::looks_costumenumber], - "name" => hq_todo!("costume name"), + "name" => vec![IrOpcode::looks_costumename], _ => hq_bad_proj!("invalid value for NUMBER_NAME field"), } } diff --git a/src/registry.rs b/src/registry.rs index 289183d5..69f8312b 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -231,6 +231,7 @@ where self.0.registry() } + /// Registers a `NamedRegistryItem` using its key function and its `const VALUE` pub fn register(&self) -> HQResult where N: TryFrom, @@ -240,6 +241,28 @@ where self.0.register(R::name::(), T::VALUE) } + /// Registers a runtime key-value pair; just calls `register` on the underlying + /// `Registry` + pub fn register_dyn(&self, key: R::Key, value: R::Value) -> HQResult + where + N: TryFrom, + >::Error: fmt::Debug, + { + self.0.register(key, value) + } + + /// Override a runtime key-value pair with a runtime value; just calls + /// `register_override` on the underlying `Registry` + pub fn register_dyn_override(&self, key: R::Key, value: R::Value) -> HQResult + where + N: TryFrom, + >::Error: fmt::Debug, + { + self.0.register_override(key, value) + } + + /// Overrides a `NamedRegistryItem` entry using the override argument types + /// associated with the corresponding `NamedRegistryItemOverride` pub fn register_override(&self, override_arg: A) -> HQResult where N: TryFrom, @@ -250,6 +273,8 @@ where .register_override(R::name::(), T::r#override(override_arg)) } + /// Tries to override a `NamedRegistryItem` entry using the override argument + /// types associated with the corresponding `TryNamedRegistryItemOverride` pub fn try_register_override(&self, override_arg: A) -> HQResult where N: TryFrom, diff --git a/src/wasm/func.rs b/src/wasm/func.rs index dd0866b3..8678adde 100644 --- a/src/wasm/func.rs +++ b/src/wasm/func.rs @@ -228,6 +228,7 @@ pub struct StepFunc { target: StepTarget, // the actual target index, for interfacing with js target_index: u32, + costume_names: Rc>>>, } impl StepFunc { @@ -255,6 +256,10 @@ impl StepFunc { self.target_index } + pub const fn costume_names(&self) -> &Rc>>> { + &self.costume_names + } + /// creates a new step function, with one paramter #[must_use] pub fn new( @@ -262,6 +267,7 @@ impl StepFunc { flags: WasmFlags, target: StepTarget, target_index: u32, + costume_names: Rc>>>, ) -> Self { Self { locals: RefCell::new(vec![]), @@ -273,6 +279,7 @@ impl StepFunc { local_variables: RefCell::new(BTreeMap::default()), target, target_index, + costume_names, } } @@ -286,6 +293,7 @@ impl StepFunc { flags: WasmFlags, target: StepTarget, target_index: u32, + costume_names: Rc>>>, ) -> Self { Self { locals: RefCell::new(vec![]), @@ -297,6 +305,7 @@ impl StepFunc { local_variables: RefCell::new(BTreeMap::default()), target, target_index, + costume_names, } } @@ -407,6 +416,7 @@ impl StepFunc { steps: &Rc>>, registries: Rc, flags: WasmFlags, + costume_names: Rc>>>, ) -> HQResult { hq_assert!( step.try_borrow()?.used_non_inline(), @@ -454,9 +464,17 @@ impl StepFunc { } else { Box::from([]) }; - Self::new_with_types(params, outputs, registries, flags, target, target_index) + Self::new_with_types( + params, + outputs, + registries, + flags, + target, + target_index, + costume_names, + ) } else { - Self::new(registries, flags, target, target_index) + Self::new(registries, flags, target, target_index, costume_names) }; if let Some(ref proc_context) = step.try_borrow()?.context().proc_context && !step.try_borrow()?.context().warp diff --git a/src/wasm/project.rs b/src/wasm/project.rs index 6499ad1c..b962e831 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -28,6 +28,7 @@ pub struct WasmProject { events: BTreeMap>, registries: Rc, target_names: Vec>, + costume_names: Rc>>>, environment: ExternalEnvironment, } @@ -46,6 +47,7 @@ impl WasmProject { environment, registries: Rc::new(Registries::default()), target_names: vec![], + costume_names: Rc::new(vec![]), } } @@ -218,6 +220,61 @@ impl WasmProject { Rc::unwrap_or_clone(self.registries().types().clone()).finish(&mut types); + // TODO: make an elements registry to deal with this at the site of the block + for (table_index, table_name) in self + .registries() + .tables() + .registry() + .try_borrow()? + .keys() + .enumerate() + { + if table_name.starts_with("costume_names_") { + #[expect( + clippy::unwrap_used, + reason = "checked immediately before that this is a prefix of the table name" + )] + let target_index: u32 = table_name + .strip_prefix("costume_names_") + .unwrap() + .parse() + .map_err(|_| make_hq_bug!("couldn't parse target index from table name"))?; + let costume_names = self + .costume_names + .get(target_index as usize) + .ok_or_else(|| make_hq_bug!("target index out of bounds for costume names"))?; + let name_globals = costume_names + .iter() + .map(|costume_name| { + Ok(ConstExpr::global_get( + u32::try_from( + self.registries() + .strings() + .registry() + .try_borrow()? + .get_index_of(costume_name) + .ok_or_else(|| { + make_hq_bug!( + "couldn't find costume name string in strings registry" + ) + })?, + ) + .map_err(|_| make_hq_bug!("string index out of bounds"))?, + )) + }) + .collect::>>()?; + elements.active( + Some( + table_index + .try_into() + .map_err(|_| make_hq_bug!("table index out of bounds"))?, + ), + &ConstExpr::i32_const(0), + Elements::Expressions(RefType::EXTERNREF, Cow::Borrowed(&*name_globals)), + ); + } + } + self.registries() .tables() .clone() @@ -681,6 +738,20 @@ impl WasmProject { let steps = Rc::new(RefCell::new(Vec::new())); let registries = Rc::new(Registries::default()); let mut events: BTreeMap> = BTreeMap::default(); + let costume_names = Rc::new( + ir_project + .targets() + .try_borrow()? + .values() + .map(|target| { + target + .costumes() + .iter() + .map(|costume| costume.name.clone()) + .collect() + }) + .collect(), + ); // StepFunc::compile_step( // Rc::new(Step::new_empty( // Rc::downgrade(ir_project), @@ -701,7 +772,14 @@ impl WasmProject { // )?; // compile every step for (i, step) in ir_project.steps().try_borrow()?.iter().enumerate() { - StepFunc::compile_step(step, StepIndex(i), &steps, Rc::clone(®istries), flags)?; + StepFunc::compile_step( + step, + StepIndex(i), + &steps, + Rc::clone(®istries), + flags, + Rc::clone(&costume_names), + )?; } // add thread event handlers for them for thread in ir_project.threads().try_borrow()?.iter() { @@ -717,6 +795,7 @@ impl WasmProject { registries, environment: ExternalEnvironment::WebBrowser, target_names: ir_project.targets().try_borrow()?.keys().cloned().collect(), + costume_names, }) } } @@ -755,6 +834,7 @@ mod tests { environment: ExternalEnvironment::WebBrowser, registries, target_names: vec![], + costume_names: Rc::new(vec![]), }; let wasm_bytes = project.finish().unwrap().wasm_bytes; if let Err(err) = wasmparser::validate(&wasm_bytes) { From 5391b80b63423aa2f97a56152cb327734da0647c Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:19:07 +0000 Subject: [PATCH 28/46] compile project in webworker to avoid hanging the main thread --- playground/components/ProjectPlayer.vue | 43 +++++++++++++++++-------- playground/lib/compile-worker.js | 15 +++++++++ 2 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 playground/lib/compile-worker.js diff --git a/playground/components/ProjectPlayer.vue b/playground/components/ProjectPlayer.vue index 928f1c8d..0ae953c1 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -71,11 +71,6 @@