From 9f15783b95503315fb34695e9a4e41282ef940cd Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Fri, 27 Dec 2024 10:10:13 +0000 Subject: [PATCH 01/98] initial commit of rewrite --- Cargo.toml | 2 + src/error.rs | 12 + src/instructions.rs | 24 + src/instructions/operator.rs | 1 + src/instructions/operator.yaml | 49 + src/instructions/operator/add.rs | 52 + src/ir.rs | 2346 +----------------------------- src/ir/types.rs | 61 + src/lib.rs | 34 +- src/old-ir.rs | 2345 +++++++++++++++++++++++++++++ src/targets/mod.rs | 1 - src/{targets => wasm}/wasm.rs | 0 12 files changed, 2571 insertions(+), 2356 deletions(-) create mode 100644 src/instructions.rs create mode 100644 src/instructions/operator.rs create mode 100644 src/instructions/operator.yaml create mode 100644 src/instructions/operator/add.rs create mode 100644 src/ir/types.rs create mode 100644 src/old-ir.rs delete mode 100644 src/targets/mod.rs rename src/{targets => wasm}/wasm.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 78826379..ffedd876 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ hashers = "1.0.1" uuid = { version = "1.4.1", default-features = false, features = ["v4", "js"] } regex = "1.10.5" lazy-regex = "3.2.0" +bitmask-enum = "2.2.5" +itertools = { version = "0.13.0", default-features = false } #[dev-dependencies] #reqwest = { version = "0.11", features = ["blocking"] } diff --git a/src/error.rs b/src/error.rs index 78c8df70..29ff0da6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,8 @@ use alloc::string::String; use wasm_bindgen::JsValue; +pub type HQResult = Result; + #[derive(Debug)] // todo: get rid of this once all expects are gone pub struct HQError { pub err_type: HQErrorType, @@ -28,6 +30,16 @@ impl From for JsValue { #[macro_export] macro_rules! hq_todo { + () => {{ + use $crate::alloc::string::ToString; + return Err($crate::HQError { + err_type: $crate::HQErrorType::Unimplemented, + msg: "todo".to_string(), + file: file!().to_string(), + line: line!(), + column: column!() + }); + }}; ($($args:tt)+) => {{ use $crate::alloc::string::ToString; return Err($crate::HQError { diff --git a/src/instructions.rs b/src/instructions.rs new file mode 100644 index 00000000..d392fc3f --- /dev/null +++ b/src/instructions.rs @@ -0,0 +1,24 @@ +pub mod operator; + +#[allow(non_camel_case_types)] +pub enum IrOpcode { + operator_add +} + +#[macro_export] +macro_rules! instructions_test { + ($($type_arg:ident $(,)?)+) => { + #[cfg(test)] + pub mod tests { + use super::{instructions, output_type, IrType}; + #[test] + fn output_type_fails_when_instructions_fails() { + $(let $type_arg = IrType::flags().map(|(_, ty)| ty); + )+ + for ($($type_arg ,)+) in itertools::iproduct!($($type_arg,)+){ + println!("boop") + } + } + } + } +} \ No newline at end of file diff --git a/src/instructions/operator.rs b/src/instructions/operator.rs new file mode 100644 index 00000000..71ae505b --- /dev/null +++ b/src/instructions/operator.rs @@ -0,0 +1 @@ +pub mod add; \ No newline at end of file diff --git a/src/instructions/operator.yaml b/src/instructions/operator.yaml new file mode 100644 index 00000000..10ec9a94 --- /dev/null +++ b/src/instructions/operator.yaml @@ -0,0 +1,49 @@ +--- +- + opcode: operator_add + inputs: + - Number + - Number + output_depends: [QuasiInteger,] + output: > + if QuasiInteger.contains(I1) && QuasiInteger.contains(I2) { + if (I1|I2) & (BooleanTrue | IntPos) != 0 { IntPos } else { none() } + | if (I1|I2) & (BooleanTrue | IntNeg) != 0 { IntNeg } else { none() } + | if (I1 & (BooleanTrue | IntPos) != 0) && I2 & IntNeg != 0) || (I2 & (BooleanTrue | IntPos) != 0 && I1 & IntNeg != 0) || (IntZero & I1 != 0 && IntZero & I2 != 0)) { IntZero } else { none() } + } else { + if FloatPosInf & (I1|I2) != 0 { FloatPosInf } else { none() } + | if FloatNegInf & (I1|I2) != 0 { FloatNegInf } else { none() } + | if (FloatPosInf & I1 != 0 && FloatNegInf & I2 != 0) || (FloatPosInf & I2 != 0 && FloatNegInf & I2 != 0) { FloatNan } else { none() } + | if (FloatPosReal | IntPos) & (I1|I2) != 0 { if FloatPosFrac & (I1|I2) != 0 { FloatPosReal } else { FloatPosInt } } else { none() } + | if (FloatNegReal | IntNeg) & (I1|I2) != { if FloatNegFrac & (I1|I2) != 0 { FloatNegReal } else { FloatNegInt } } else { none() } + | if ((FloatPosReal | IntPos) & I1 != 0 && (FloatNegReal | IntNeg) & I2 != 0) || ((FloatPosReal | IntPos) & I1 != 0 && (FloatNegReal | IntNeg) & I2 != 0) || (FloatPosZero | IntZero) & (I1&I2) != 0 || (FloatPosZero & I1 != 0 && FloatNegZero & I2 != 0) || (FloatPosZero & I2 != 0 && FloatNegZero & I1 != 0) { FloatPosZero } else { none() } + | if FloatNegZero & (I1&I2) != 0 { FloatNegZero } else { none() } + } + wasm: | + #IF I2 & FloatNan != 0 + local.get $I1 + local.get $I1 + f64.ne + if + 0 + local.set $I1 + end + #ENDIF + #IF I2 & FloatNan != 0 + local.get $I2 + local.get $I2 + f64.ne + if + 0 + local.set $I2 + end + #ENDIF + #IF I1 & QuasiInteger != 0 && I1 & Float == 0 + local.get $I1 + + #ENDIF + local.get $I1 + local.get $I2 + + +... \ No newline at end of file diff --git a/src/instructions/operator/add.rs b/src/instructions/operator/add.rs new file mode 100644 index 00000000..6ff8190e --- /dev/null +++ b/src/instructions/operator/add.rs @@ -0,0 +1,52 @@ +use crate::ir::types::Type as IrType; +use crate::prelude::*; + +use wasm_encoder::Instruction; + +pub fn instructions(t1: IrType, t2: IrType) -> HQResult>> { + hq_todo!(); + if IrType::QuasiInt.contains(t1) { + if IrType::QuasiInt.contains(t2) { + hq_todo!() + } else if IrType::Float.contains(t2) { + hq_todo!() + } else { + hq_todo!() + } + } else if IrType::Float.contains(t2) { + hq_todo!() + } else { + hq_todo!() + } +} + +pub fn output_type(t1: IrType, t2: IrType) -> HQResult { + hq_todo!(); +} + +// (context (param $MEMORY_LOCATION i32) (result i32) +// (local $EXTERNREF ref.extern) +// (local $F64 f64) +// (local $I64 i64) +// (local $I32 i32) +// (local $I32_2 i32) +// (local $F64_2 i32) +// (inline $add_QuasiInteger_QuasiInteger (stack i64 i64) (result i64) +// i64.add) +// (inline $add_QuasiInteger_Float (stack i64 f64) (result f64) +// local.set $F64 +// f64.convert_i64_s +// local.get $F64 +// f64.add +// ) +// (inline $add_Float_QuasiInteger (stack f64 i64) (result f64) +// f64.convert_i64_s +// f64.add +// ) +// (inline $add_Float_Float (stack f64 f64) (result f64) +// f64.convert_i64_s +// f64.add +// ) +// ) + +crate::instructions_test!(t1, t2); \ No newline at end of file diff --git a/src/ir.rs b/src/ir.rs index 90a2ffa8..dd198c6d 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -1,2345 +1 @@ -// intermediate representation -use crate::log; -use crate::sb3::{ - Block, BlockArray, BlockArrayOrId, BlockOpcode, CostumeDataFormat, Field, Input, Sb3Project, - VarVal, VariableInfo, -}; -use crate::HQError; -use alloc::boxed::Box; -use alloc::collections::BTreeMap; -use alloc::rc::Rc; -use alloc::string::{String, ToString}; -use alloc::vec::Vec; -use core::cell::RefCell; -use core::fmt; -use core::hash::BuildHasherDefault; -use hashers::fnv::FNV1aHasher64; -use indexmap::IndexMap; -use lazy_regex::{lazy_regex, Lazy}; -use regex::Regex; -use uuid::Uuid; - -#[derive(Debug, Clone, PartialEq)] -pub struct IrCostume { - pub name: String, - pub data_format: CostumeDataFormat, - pub md5ext: String, -} - -pub type ProcMap = BTreeMap<(String, String), Procedure>; -pub type StepMap = IndexMap<(String, String), Step, BuildHasherDefault>; - -#[derive(Debug)] -pub struct IrProject { - pub threads: Vec, - pub vars: Rc>>, - pub target_names: Vec, - pub costumes: Vec>, - pub steps: StepMap, - pub procedures: ProcMap, // maps (target_id, proccode) to (target_id, step_top_block_id, warp) - pub sb3: Sb3Project, -} - -impl fmt::Display for IrProject { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{{\n\tthreads: {:?},\n\tvars: {:?},\n\t,target_names: {:?},\n\tcostumes: {:?},\n\tsteps: {:?}\n}}", - self.threads, - self.vars, - self.target_names, - self.costumes, - self.steps - ) - } -} - -impl TryFrom for IrProject { - type Error = HQError; - - fn try_from(sb3: Sb3Project) -> Result { - let vars: Rc>> = Rc::new(RefCell::new( - sb3.targets - .iter() - .flat_map(|target| { - target.variables.iter().map(|(id, info)| match info { - VariableInfo::LocalVar(name, val) => { - IrVar::new(id.clone(), name.clone(), val.clone(), false) - } - VariableInfo::CloudVar(name, val, is_cloud) => { - IrVar::new(id.clone(), name.clone(), val.clone(), *is_cloud) - } - }) - }) - .collect(), - )); - - let costumes: Vec> = sb3 - .targets - .iter() - .map(|target| { - target - .costumes - .iter() - .map(|costume| { - IrCostume { - name: costume.name.clone(), - data_format: costume.data_format, - md5ext: costume.md5ext.clone(), - //data: load_asset(costume.md5ext.as_str()), - } - }) - .collect() - }) - .collect(); - - let mut procedures = BTreeMap::new(); - - let mut steps: StepMap = Default::default(); - // insert a noop step so that these step indices match up with the step function indices in the generated wasm - // (step function 0 is a noop) - steps.insert( - ("".into(), "".into()), - Step::new( - vec![], - Rc::new(ThreadContext { - target_index: u32::MAX, - dbg: false, - vars: Rc::new(RefCell::new(vec![])), - target_num: sb3.targets.len(), - costumes: vec![], - proc: None, - }), - ), - ); - let mut threads: Vec = vec![]; - for (target_index, target) in sb3.targets.iter().enumerate() { - for (id, block) in - target - .blocks - .clone() - .iter() - .filter(|(_id, b)| match b.block_info() { - Some(block_info) => { - block_info.top_level - && matches!(block_info.opcode, BlockOpcode::event_whenflagclicked) - } - None => false, - }) - { - let context = Rc::new(ThreadContext { - target_index: target_index.try_into().map_err(|_| make_hq_bug!(""))?, - dbg: target.comments.clone().iter().any(|(_id, comment)| { - matches!(comment.block_id.clone(), Some(d) if &d == id) - && comment.text.clone() == *"hq-dbg" - }), - vars: Rc::clone(&vars), - target_num: sb3.targets.len(), - costumes: costumes.get(target_index).ok_or(make_hq_bug!(""))?.clone(), - proc: None, - }); - let thread = Thread::from_hat( - block.clone(), - target.blocks.clone(), - context, - &mut steps, - target.name.clone(), - &mut procedures, - )?; - threads.push(thread); - } - } - let target_names = sb3 - .targets - .iter() - .map(|t| t.name.clone()) - .collect::>(); - Ok(Self { - vars, - threads, - target_names, - steps, - costumes, - sb3, - procedures, - }) - } -} - -#[allow(non_camel_case_types)] -#[allow(non_snake_case)] -#[derive(Debug, Clone, PartialEq)] -pub enum IrOpcode { - boolean { - BOOL: bool, - }, - control_repeat, - control_repeat_until, - control_while, - control_for_each { - VARIABLE: String, - }, - control_forever, - control_wait, - control_wait_until, - control_stop { - STOP_OPTION: String, - }, - control_create_clone_of, - control_create_clone_of_menu { - CLONE_OPTOON: String, - }, - control_delete_this_clone, - control_get_counter, - control_incr_counter, - control_clear_counter, - control_all_at_once, - control_start_as_clone, - data_variable { - VARIABLE: String, - assume_type: Option, - }, - data_setvariableto { - VARIABLE: String, - assume_type: Option, - }, - data_teevariable { - VARIABLE: String, - assume_type: Option, - }, - data_hidevariable { - VARIABLE: String, - }, - data_showvariable { - VARIABLE: String, - }, - data_listcontents { - LIST: String, - }, - data_addtolist, - data_deleteoflist, - data_deletealloflist, - data_insertatlist, - data_replaceitemoflist, - data_itemoflist, - data_itemnumoflist, - data_lengthoflist, - data_listcontainsitem, - data_hidelist, - data_showlist, - event_broadcast, - event_broadcastandwait, - event_whenflagclicked, - event_whenkeypressed, - event_whenthisspriteclicked, - event_whentouchingobject, - event_whenstageclicked, - event_whenbackdropswitchesto, - event_whengreaterthan, - event_whenbroadcastreceived, - hq_cast(InputType, InputType), - hq_drop(usize), - hq_goto { - step: Option<(String, String)>, - does_yield: bool, - }, - hq_goto_if { - step: Option<(String, String)>, - does_yield: bool, - }, - hq_launch_procedure(Procedure), - looks_say, - looks_sayforsecs, - looks_think, - looks_thinkforsecs, - looks_show, - looks_hide, - looks_hideallsprites, - looks_switchcostumeto, - looks_switchbackdropto, - looks_switchbackdroptoandwait, - looks_nextcostume, - looks_nextbackdrop, - looks_changeeffectby, - looks_seteffectto, - looks_cleargraphiceffects, - looks_changesizeby, - looks_setsizeto, - looks_changestretchby, - looks_setstretchto, - looks_gotofrontback, - looks_goforwardbackwardlayers, - looks_size, - looks_costumenumbername, - looks_backdropnumbername, - looks_backdrops, - math_angle { - NUM: i32, - }, - math_integer { - NUM: i32, - }, - math_number { - NUM: f64, - }, - math_positive_number { - NUM: i32, - }, - math_whole_number { - NUM: i32, - }, - motion_movesteps, - motion_gotoxy, - motion_goto, - motion_turnright, - motion_turnleft, - motion_pointindirection, - motion_pointtowards, - motion_glidesecstoxy, - motion_glideto, - motion_ifonedgebounce, - motion_setrotationstyle, - motion_changexby, - motion_setx, - motion_changeyby, - motion_sety, - motion_xposition, - motion_yposition, - motion_direction, - motion_scroll_right, - motion_scroll_up, - motion_align_scene, - motion_xscroll, - motion_yscroll, - motion_pointtowards_menu, - operator_add, - operator_subtract, - operator_multiply, - operator_divide, - operator_lt, - operator_equals, - operator_gt, - operator_and, - operator_or, - operator_not, - operator_random, - operator_join, - operator_letter_of, - operator_length, - operator_contains, - operator_mod, - operator_round, - operator_mathop { - OPERATOR: String, - }, - pen_clear, - pen_stamp, - pen_penDown, - pen_penUp, - pen_setPenColorToColor, - pen_changePenColorParamBy, - pen_setPenColorParamTo, - pen_changePenSizeBy, - pen_setPenSizeTo, - pen_setPenShadeToNumber, - pen_changePenShadeBy, - pen_setPenHueToNumber, - pen_changePenHueBy, - argument_reporter_string_number, - argument_reporter_boolean, - sensing_touchingobject, - sensing_touchingcolor, - sensing_coloristouchingcolor, - sensing_distanceto, - sensing_distancetomenu, - sensing_timer, - sensing_resettimer, - sensing_of, - sensing_mousex, - sensing_mousey, - sensing_setdragmode, - sensing_mousedown, - sensing_keypressed, - sensing_current, - sensing_dayssince2000, - sensing_loudness, - sensing_loud, - sensing_askandwait, - sensing_answer, - sensing_username, - sensing_userid, - sensing_touchingobjectmenu, - sensing_keyoptions, - sensing_of_object_menu, - sound_play, - sound_playuntildone, - sound_stopallsounds, - sound_seteffectto, - sound_changeeffectby, - sound_cleareffects, - sound_sounds_menu, - sound_beats_menu, - sound_effects_menu, - sound_setvolumeto, - sound_changevolumeby, - sound_volume, - text { - TEXT: String, - }, - unknown_const { - val: IrVal, - }, -} - -#[derive(Clone)] -pub struct TypeStack(pub Rc>>, pub InputType); - -impl TypeStack { - pub fn new_some(prev: TypeStack) -> Rc>> { - Rc::new(RefCell::new(Some(prev))) - } - pub fn new(prev: Option) -> Rc>> { - Rc::new(RefCell::new(prev)) - } -} - -#[allow(clippy::len_without_is_empty)] -pub trait TypeStackImpl { - fn get(&self, i: usize) -> Rc>>; - fn len(&self) -> usize; -} - -impl TypeStackImpl for Rc>> { - fn get(&self, i: usize) -> Rc>> { - if i == 0 || self.borrow().is_none() { - Rc::clone(self) - } else { - self.borrow().clone().unwrap().0.get(i - 1) - } - } - - fn len(&self) -> usize { - if self.borrow().is_none() { - 0 - } else { - 1 + self.borrow().clone().unwrap().0.len() - } - } -} - -#[derive(Debug, Clone)] -pub struct IrBlock { - pub opcode: IrOpcode, - pub type_stack: Rc>>, -} - -impl IrBlock { - pub fn new_with_stack( - opcode: IrOpcode, - type_stack: Rc>>, - add_cast: &mut F, - ) -> Result - where - F: FnMut(usize, &InputType), - { - let expected_inputs = opcode.expected_inputs()?; - if type_stack.len() < expected_inputs.len() { - hq_bug!( - "expected {} inputs, got {} at {:?}", - expected_inputs.len(), - type_stack.len(), - opcode, - ); - } - let arity = expected_inputs.len(); - for i in 0..expected_inputs.len() { - let expected = expected_inputs.get(i).ok_or(make_hq_bug!(""))?; - let actual = &type_stack.get(arity - 1 - i).borrow().clone().unwrap().1; - if !expected.includes(actual) { - add_cast(arity - 1 - i, expected); - } - } - let output_stack = opcode.output(type_stack)?; - Ok(IrBlock { - opcode, - type_stack: output_stack, - }) - } - - pub fn new_with_stack_no_cast( - opcode: IrOpcode, - type_stack: Rc>>, - ) -> Result { - let expected_inputs = opcode.expected_inputs()?; - if type_stack.len() < expected_inputs.len() { - hq_bug!( - "expected {} inputs, got {}", - expected_inputs.len(), - type_stack.len() - ); - } - let arity = expected_inputs.len(); - for i in 0..expected_inputs.len() { - let expected = expected_inputs.get(i).ok_or(make_hq_bug!(""))?; - let actual = &type_stack - .get(arity - 1 - i) - .borrow() - .clone() - .ok_or(make_hq_bug!(""))? - .1; - - if !expected.includes(actual) { - hq_bug!( - "cast needed at input position {:}, {:?} -> {:?}, but no casts were expected; at {:?}", - i, - actual, - expected, - opcode, - ); - } - } - let output_stack = opcode.output(type_stack)?; - Ok(IrBlock { - opcode, - type_stack: output_stack, - }) - } - - pub fn does_request_redraw(&self) -> bool { - use IrOpcode::*; - matches!( - self.opcode(), - looks_say - | looks_think - | hq_goto { - does_yield: true, - .. - } - | motion_gotoxy - | pen_penDown - | pen_clear - | looks_switchcostumeto - | motion_turnleft - | motion_turnright - | looks_setsizeto - | looks_changesizeby - ) - } - pub fn is_hat(&self) -> bool { - use IrOpcode::*; - matches!(self.opcode, event_whenflagclicked) - } - pub fn opcode(&self) -> &IrOpcode { - &self.opcode - } - pub fn type_stack(&self) -> Rc>> { - Rc::clone(&self.type_stack) - } - - pub fn is_const(&self) -> bool { - use IrOpcode::*; - matches!( - self.opcode(), - math_number { .. } - | math_whole_number { .. } - | math_integer { .. } - | math_angle { .. } - | math_positive_number { .. } - | text { .. } - ) - } - - pub fn const_value(&self, value_stack: &mut Vec) -> Result, HQError> { - use IrOpcode::*; - //dbg!(value_stack.len()); - let arity = self.opcode().expected_inputs()?.len(); - if arity > value_stack.len() { - return Ok(None); - } - match self.opcode() { - math_number { NUM } => value_stack.push(IrVal::Float(*NUM)), - math_positive_number { NUM } - | math_integer { NUM } - | math_angle { NUM } - | math_whole_number { NUM } => value_stack.push(IrVal::Int(*NUM)), - text { TEXT } => value_stack.push(IrVal::String(TEXT.to_string())), - operator_add => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Float(num1 + num2)); - } - operator_subtract => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Float(num1 - num2)); - } - operator_multiply => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Float(num1 * num2)); - } - operator_divide => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Float(num1 / num2)); - } - operator_mod => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Float(num1 % num2)); - } - operator_lt => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Boolean(num1 < num2)); - } - operator_gt => { - let num2 = value_stack.pop().unwrap().to_f64(); - let num1 = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Boolean(num1 > num2)); - } - operator_and => { - let bool2 = value_stack.pop().unwrap().to_bool(); - let bool1 = value_stack.pop().unwrap().to_bool(); - value_stack.push(IrVal::Boolean(bool1 && bool2)); - } - operator_or => { - let bool2 = value_stack.pop().unwrap().to_bool(); - let bool1 = value_stack.pop().unwrap().to_bool(); - value_stack.push(IrVal::Boolean(bool1 || bool2)); - } - operator_join => { - let str2 = value_stack.pop().unwrap().to_string(); - let str1 = value_stack.pop().unwrap().to_string(); - value_stack.push(IrVal::String(str1 + &str2)); - } - operator_length => { - let string = value_stack.pop().unwrap().to_string(); - value_stack.push(IrVal::Int(string.len().try_into().unwrap())); - } - operator_not => { - let b = value_stack.pop().unwrap().to_bool(); - value_stack.push(IrVal::Boolean(!b)); - } - operator_round => { - let num = value_stack.pop().unwrap().to_f64(); - value_stack.push(IrVal::Int(num as i32)); - } - hq_cast(_from, to) => { - let val = value_stack.pop().unwrap(); - let ty = to.least_restrictive_concrete_type(); - match ty { - InputType::ConcreteInteger => value_stack.push(IrVal::Int(val.to_i32())), - InputType::Float => value_stack.push(IrVal::Float(val.to_f64())), - InputType::String => value_stack.push(IrVal::String(val.to_string())), - InputType::Boolean => value_stack.push(IrVal::Boolean(val.to_bool())), - InputType::Unknown => { - value_stack.push(IrVal::Unknown(Box::new(val))); - } - _ => hq_bug!("unexpected non-concrete type {:?}", ty), - }; - } - _ => return Ok(None), - }; - Ok(Some(())) - } -} - -/// represents an input (or output) type of a block. -/// output types shpuld always be exactly known, -/// so unions, or types that resolve to a union, -/// should never be used as output types -#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)] -pub enum InputType { - Any, - String, - Number, - Float, - Integer, - Boolean, - ConcreteInteger, - Unknown, - Union(Box, Box), -} - -impl InputType { - /// returns the base type that a type is aliased to, if present; - /// otherwise, returns a clone of itself - pub fn base_type(&self) -> InputType { - use InputType::*; - // unions of types should be represented with the least restrictive type first, - // so that casting chooses the less restrictive type to cast to - match self { - Any => Union( - Box::new(Union(Box::new(Unknown), Box::new(String))), - Box::new(Number), - ) - .base_type(), - Number => Union(Box::new(Float), Box::new(Integer)).base_type(), - Integer => Union(Box::new(ConcreteInteger), Box::new(Boolean)).base_type(), - Union(a, b) => Union(Box::new(a.base_type()), Box::new(b.base_type())), - _ => self.clone(), - } - } - - /// when a type is a union type, returns the first concrete type in that union - /// (this assumes that the least restrictive type is placed first in the union). - /// otherwise, returns itself. - pub fn least_restrictive_concrete_type(&self) -> InputType { - match self.base_type() { - InputType::Union(a, _) => a.least_restrictive_concrete_type(), - other => other, - } - } - - /// determines whether a type is a superyype of another type - pub fn includes(&self, other: &Self) -> bool { - if self.base_type() == other.base_type() { - true - } else if let InputType::Union(a, b) = self.base_type() { - a.includes(other) || b.includes(other) - } else { - false - } - } - - /// attempts to promote a type to one of the ones provided. - /// none of the provided types should overlap with each other, - /// so that there is no ambiguity over which type it should be promoted to. - /// if there are no types that can be prpmoted to, - /// an `Err(HQError)` will be returned. - pub fn loosen_to(&self, others: T) -> Result - where - T: IntoIterator, - T::IntoIter: ExactSizeIterator, - { - for other in others { - if other.includes(self) { - return Ok(other); - } - } - Err(make_hq_bug!( - "couldn't find any types that this type can be demoted to" - )) - } -} - -impl IrOpcode { - pub fn does_request_redraw(&self) -> bool { - use IrOpcode::*; - matches!(self, looks_say | looks_think) - } - - pub fn expected_inputs(&self) -> Result, HQError> { - use InputType::*; - use IrOpcode::*; - Ok(match self { - operator_add | operator_subtract | operator_multiply | operator_divide - | operator_mod | operator_random => vec![Number, Number], - operator_round | operator_mathop { .. } => vec![Number], - looks_say | looks_think => vec![Unknown], - data_setvariableto { - assume_type: None, .. - } - | data_teevariable { - assume_type: None, .. - } => vec![Unknown], - data_setvariableto { - assume_type: Some(ty), - .. - } - | data_teevariable { - assume_type: Some(ty), - .. - } => vec![ty.clone()], - math_integer { .. } - | math_angle { .. } - | math_whole_number { .. } - | math_positive_number { .. } - | math_number { .. } - | sensing_timer - | sensing_dayssince2000 - | looks_size - | data_variable { .. } - | text { .. } - | boolean { .. } - | unknown_const { .. } => vec![], - operator_lt | operator_gt => vec![Number, Number], - operator_equals => vec![Any, Any], - operator_and | operator_or => vec![Boolean, Boolean], - operator_not => vec![Boolean], - operator_join | operator_contains => vec![String, String], - operator_letter_of => vec![Number, String], - operator_length => vec![String], - hq_goto { .. } - | sensing_resettimer - | pen_clear - | pen_stamp - | pen_penDown - | pen_penUp => vec![], - hq_goto_if { .. } => vec![Boolean], - hq_drop(n) => vec![Any; *n], - hq_cast(from, _to) => vec![from.clone()], - pen_setPenColorToColor - | pen_changePenSizeBy - | pen_setPenSizeTo - | pen_setPenShadeToNumber - | pen_changePenShadeBy - | pen_setPenHueToNumber - | pen_changePenHueBy - | looks_setsizeto - | looks_changesizeby - | motion_turnleft - | motion_turnright => vec![Number], - // todo: looks_switchcostumeto waiting on generic monomorphisation to work properly - looks_switchcostumeto => vec![Any], - pen_changePenColorParamBy | pen_setPenColorParamTo => vec![String, Number], - motion_gotoxy => vec![Number, Number], - hq_launch_procedure(Procedure { arg_types, .. }) => arg_types.clone(), - _ => hq_todo!("{:?}", &self), - }) - } - - pub fn output( - &self, - type_stack: Rc>>, - ) -> Result>>, HQError> { - use InputType::*; - use IrOpcode::*; - let expected_inputs = self.expected_inputs()?; - if type_stack.len() < expected_inputs.len() { - hq_bug!( - "expected {} inputs, got {}", - expected_inputs.len(), - type_stack.len() - ); - } - let arity = expected_inputs.len(); - let get_input = |i| { - Ok(type_stack - .get(arity - 1 - i) - .borrow() - .clone() - .ok_or(make_hq_bug!(""))? - .1) - }; - let output = match self { - data_teevariable { .. } => Ok(Rc::clone(&type_stack)), - hq_cast(_from, to) => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(1)), - to.clone(), - ))), - operator_add | operator_subtract | operator_multiply | operator_random - | operator_mod => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(2)), - if Integer.includes(&get_input(0)?) && Integer.includes(&get_input(1)?) { - ConcreteInteger - } else { - Float - }, - ))), - operator_divide => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(2)), - Float, - ))), - looks_size | sensing_timer | sensing_dayssince2000 | math_number { .. } => Ok( - TypeStack::new_some(TypeStack(Rc::clone(&type_stack), Float)), - ), - data_variable { - assume_type: None, .. - } - | unknown_const { .. } => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack), - Unknown, - ))), - data_variable { - assume_type: Some(ty), - .. - } => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack), - ty.clone(), - ))), - math_integer { .. } - | math_angle { .. } - | math_whole_number { .. } - | math_positive_number { .. } => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack), - ConcreteInteger, - ))), - operator_round => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(1)), - ConcreteInteger, - ))), - operator_length => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(1)), - ConcreteInteger, - ))), - operator_mathop { OPERATOR } => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(1)), - match OPERATOR.as_str().to_uppercase().as_str() { - "CEILING" | "FLOOR" => ConcreteInteger, - _ => Float, - }, - ))), - text { .. } => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack), - String, - ))), - boolean { .. } => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack), - Boolean, - ))), - operator_join | operator_letter_of => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(2)), - String, - ))), - operator_contains | operator_and | operator_or | operator_gt | operator_lt - | operator_equals => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(2)), - Boolean, - ))), - operator_not => Ok(TypeStack::new_some(TypeStack( - Rc::clone(&type_stack.get(1)), - Boolean, - ))), - motion_gotoxy | pen_setPenColorParamTo => Ok(Rc::clone(&type_stack.get(2))), - data_setvariableto { .. } - | motion_turnleft - | motion_turnright - | looks_switchcostumeto - | looks_changesizeby - | looks_setsizeto - | looks_say - | looks_think - | pen_setPenColorToColor - | pen_changePenSizeBy - | pen_setPenSizeTo - | pen_setPenShadeToNumber - | pen_changePenShadeBy - | pen_setPenHueToNumber - | pen_changePenHueBy - | hq_goto_if { .. } => Ok(Rc::clone(&type_stack.get(1))), - hq_goto { .. } - | sensing_resettimer - | pen_clear - | pen_penUp - | pen_penDown - | pen_stamp => Ok(Rc::clone(&type_stack)), - hq_drop(n) => Ok(type_stack.get(*n)), - hq_launch_procedure(Procedure { arg_types, .. }) => Ok(type_stack.get(arg_types.len())), - _ => hq_todo!("{:?}", &self), - }; - output - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct IrVar { - id: String, - name: String, - initial_value: VarVal, - is_cloud: bool, -} - -impl IrVar { - pub fn new(id: String, name: String, initial_value: VarVal, is_cloud: bool) -> Self { - Self { - id, - name, - initial_value, - is_cloud, - } - } - pub fn id(&self) -> &String { - &self.id - } - pub fn name(&self) -> &String { - &self.name - } - pub fn initial_value(&self) -> &VarVal { - &self.initial_value - } - pub fn is_cloud(&self) -> &bool { - &self.is_cloud - } -} - -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] -pub enum ThreadStart { - GreenFlag, -} - -#[derive(Debug, Clone)] -pub struct Step { - pub opcodes: Vec, - pub context: Rc, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Procedure { - pub arg_types: Vec, - pub first_step: String, - pub target_id: String, - pub warp: bool, -} - -impl fmt::Debug for TypeStack { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let this = Rc::new(RefCell::new(Some(self.clone()))); - f.debug_list() - .entries( - (0..this.len()) - .rev() - .map(|i| this.get(this.len() - 1 - i).borrow().clone().unwrap().1), - ) - .finish() - } -} - -impl Step { - pub fn new(opcodes: Vec, context: Rc) -> Step { - Step { opcodes, context } - } - pub fn opcodes(&self) -> &Vec { - &self.opcodes - } - pub fn opcodes_mut(&mut self) -> &mut Vec { - &mut self.opcodes - } - pub fn context(&self) -> Rc { - Rc::clone(&self.context) - } - pub fn const_fold(&mut self) -> Result<(), HQError> { - let mut value_stack: Vec = vec![]; - let mut is_folding = false; - let mut fold_start = 0; - let mut to_splice/*: Vec<(Range, Vec)>*/ = vec![]; - for (i, opcode) in self.opcodes.iter().enumerate() { - if !is_folding { - if !opcode.is_const() { - continue; - } - is_folding = true; - fold_start = i; - } - let const_value = opcode.const_value(&mut value_stack)?; - if const_value.is_none() { - is_folding = false; - let mut type_stack = - Rc::clone(&self.opcodes.get(fold_start).unwrap().type_stack.get(1)); - for ty in value_stack.iter().map(|val| val.as_input_type()) { - let prev_type_stack = Rc::clone(&type_stack); - type_stack = TypeStack::new_some(TypeStack(prev_type_stack, ty)); - } - //dbg!(&value_stack); - let folded_blocks: Result, HQError> = value_stack - .iter() - .enumerate() - .map(|(j, val)| val.try_as_block(type_stack.get(value_stack.len() - j))) - .collect(); - to_splice.push((fold_start..i, folded_blocks?)); - value_stack = vec![]; - } - } - if is_folding { - let mut type_stack = - Rc::clone(&self.opcodes.get(fold_start).unwrap().type_stack.get(1)); - for ty in value_stack.iter().map(|val| val.as_input_type()) { - let prev_type_stack = Rc::clone(&type_stack); - type_stack = TypeStack::new_some(TypeStack(prev_type_stack, ty)); - } - let folded_blocks: Result, HQError> = value_stack - .iter() - .enumerate() - .map(|(j, val)| val.try_as_block(type_stack.get(value_stack.len() - j))) - .collect(); - to_splice.push((fold_start..self.opcodes().len(), folded_blocks?)); - } - for (range, replace) in to_splice.into_iter().rev() { - self.opcodes.splice(range, replace); - } - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum IrVal { - Int(i32), - Float(f64), - Boolean(bool), - String(String), - Unknown(Box), -} - -impl IrVal { - pub fn to_f64(self) -> f64 { - match self { - IrVal::Unknown(u) => u.to_f64(), - IrVal::Float(f) => f, - IrVal::Int(i) => i as f64, - IrVal::Boolean(b) => b as i32 as f64, - IrVal::String(s) => s.parse().unwrap_or(0.0), - } - } - pub fn to_i32(self) -> i32 { - match self { - IrVal::Unknown(u) => u.to_i32(), - IrVal::Float(f) => f as i32, - IrVal::Int(i) => i, - IrVal::Boolean(b) => b as i32, - IrVal::String(s) => s.parse().unwrap_or(0), - } - } - #[allow(clippy::inherent_to_string)] - pub fn to_string(self) -> String { - match self { - IrVal::Unknown(u) => u.to_string(), - IrVal::Float(f) => format!("{f}"), - IrVal::Int(i) => format!("{i}"), - IrVal::Boolean(b) => format!("{b}"), - IrVal::String(s) => s, - } - } - pub fn to_bool(self) -> bool { - match self { - IrVal::Unknown(u) => u.to_bool(), - IrVal::Boolean(b) => b, - _ => unreachable!(), - } - } - pub fn as_input_type(&self) -> InputType { - match self { - IrVal::Unknown(_) => InputType::Unknown, - IrVal::Float(_) => InputType::Float, - IrVal::Int(_) => InputType::ConcreteInteger, - IrVal::String(_) => InputType::String, - IrVal::Boolean(_) => InputType::Boolean, - } - } - pub fn try_as_block( - &self, - type_stack: Rc>>, - ) -> Result { - IrBlock::new_with_stack_no_cast( - match self { - IrVal::Int(num) => IrOpcode::math_integer { NUM: *num }, - IrVal::Float(num) => IrOpcode::math_number { NUM: *num }, - IrVal::String(text) => IrOpcode::text { TEXT: text.clone() }, - IrVal::Boolean(b) => IrOpcode::boolean { BOOL: *b }, - IrVal::Unknown(u) => IrOpcode::unknown_const { val: *u.clone() }, - }, - type_stack, - ) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct ThreadContext { - pub target_index: u32, - pub dbg: bool, - pub vars: Rc>>, // todo: fix variable id collisions between targets - pub target_num: usize, - pub costumes: Vec, - pub proc: Option, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Thread { - start: ThreadStart, - first_step: String, - target_id: String, -} - -static ARG_REGEX: Lazy = lazy_regex!(r#"[^\\]%[nbs]"#); - -fn arg_types_from_proccode(proccode: String) -> Result, HQError> { - // https://github.com/scratchfoundation/scratch-blocks/blob/abbfe93136fef57fdfb9a077198b0bc64726f012/blocks_vertical/procedures.js#L207-L215 - (*ARG_REGEX) - .find_iter(proccode.as_str()) - .map(|s| s.as_str().to_string().trim().to_string()) - .filter(|s| s.as_str().starts_with('%')) - .map(|s| s[..2].to_string()) - .map(|s| { - Ok(match s.as_str() { - "%n" => InputType::Number, - "%s" => InputType::String, - "%b" => InputType::Boolean, - other => hq_bug!("invalid proccode arg \"{other}\" found"), - }) - }) - .collect::, _>>() -} - -fn add_procedure( - target_id: String, - proccode: String, - expect_warp: bool, - blocks: &BTreeMap, - steps: &mut StepMap, - procedures: &mut ProcMap, - context: Rc, -) -> Result { - if let Some(procedure) = procedures.get(&(target_id.clone(), proccode.clone())) { - return Ok(procedure.clone()); - } - let arg_types = arg_types_from_proccode(proccode.clone())?; - let Some(prototype_block) = blocks.values().find(|block| { - let Some(info) = block.block_info() else { - return false; - }; - return info.opcode == BlockOpcode::procedures_prototype - && (match info.mutation.mutations.get("proccode") { - None => false, - Some(p) => { - if let serde_json::Value::String(ref s) = p { - log(s.as_str()); - *s == proccode - } else { - false - } - } - }); - }) else { - hq_bad_proj!("no prototype found for {proccode} in {target_id}") - }; - let Some(def_block) = blocks.get( - &prototype_block - .block_info() - .unwrap() - .parent - .clone() - .ok_or(make_hq_bad_proj!("prototype block without parent"))?, - ) else { - hq_bad_proj!("no definition found for {proccode} in {target_id}") - }; - if let Some(warp_val) = prototype_block - .block_info() - .unwrap() - .mutation - .mutations - .get("warp") - { - let warp = match warp_val { - serde_json::Value::Bool(w) => *w, - serde_json::Value::String(wstr) => match wstr.as_str() { - "true" => true, - "false" => false, - _ => hq_bad_proj!("unexpected string for warp mutation"), - }, - _ => hq_bad_proj!("bad type for warp mutation"), - }; - if warp != expect_warp { - hq_bad_proj!("proc call warp does not equal definition warp") - } - } else { - hq_bad_proj!("missing warp mutation on orocedures_dedinition") - } - let Some(ref next) = def_block.block_info().unwrap().next else { - hq_todo!("empty procedure definition") - }; - let procedure = Procedure { - arg_types, - first_step: next.clone(), - target_id: target_id.clone(), - warp: expect_warp, - }; - step_from_top_block( - next.clone(), - vec![], - blocks, - Rc::new(ThreadContext { - proc: Some(procedure.clone()), - dbg: false, - costumes: context.costumes.clone(), - target_index: context.target_index, - vars: Rc::clone(&context.vars), - target_num: context.target_num, - }), - steps, - target_id.clone(), - procedures, - )?; - procedures.insert((target_id.clone(), proccode.clone()), procedure.clone()); - Ok(procedure) -} - -trait IrBlockVec { - #[allow(clippy::too_many_arguments)] - fn add_block( - &mut self, - block_id: String, - blocks: &BTreeMap, - context: Rc, - last_nexts: Vec, - steps: &mut StepMap, - target_id: String, - procedures: &mut ProcMap, - ) -> Result<(), HQError>; - fn add_block_arr(&mut self, block_arr: &BlockArray) -> Result<(), HQError>; - fn add_inputs( - &mut self, - inputs: &BTreeMap, - blocks: &BTreeMap, - context: Rc, - steps: &mut StepMap, - target_id: String, - procedures: &mut ProcMap, - ) -> Result<(), HQError>; - fn get_type_stack(&self, i: Option) -> Rc>>; -} - -impl IrBlockVec for Vec { - fn add_inputs( - &mut self, - inputs: &BTreeMap, - blocks: &BTreeMap, - context: Rc, - steps: &mut StepMap, - target_id: String, - procedures: &mut ProcMap, - ) -> Result<(), HQError> { - for (name, input) in inputs { - if name.starts_with("SUBSTACK") { - continue; - } - match input { - Input::Shadow(_, maybe_block, _) | Input::NoShadow(_, maybe_block) => { - let Some(block) = maybe_block else { - hq_bad_proj!("block doesn't exist"); // is this a problem with the project, or is it a bug? - }; - match block { - BlockArrayOrId::Id(id) => { - self.add_block( - id.clone(), - blocks, - Rc::clone(&context), - vec![], - steps, - target_id.clone(), - procedures, - )?; - } - BlockArrayOrId::Array(arr) => { - self.add_block_arr(arr)?; - } - } - } - } - } - Ok(()) - } - fn add_block_arr(&mut self, block_arr: &BlockArray) -> Result<(), HQError> { - let prev_block = self.last(); - let type_stack = if let Some(block) = prev_block { - Rc::clone(&block.type_stack) - } else { - Rc::new(RefCell::new(None)) - }; - self.push(IrBlock::new_with_stack_no_cast( - match block_arr { - BlockArray::NumberOrAngle(ty, value) => match ty { - 4 => IrOpcode::math_number { NUM: *value }, - 5 => IrOpcode::math_positive_number { NUM: *value as i32 }, - 6 => IrOpcode::math_whole_number { NUM: *value as i32 }, - 7 => IrOpcode::math_integer { NUM: *value as i32 }, - 8 => IrOpcode::math_angle { NUM: *value as i32 }, - _ => hq_bad_proj!("bad project json (block array of type ({}, u32))", ty), - }, - BlockArray::ColorOrString(ty, value) => match ty { - 4 => IrOpcode::math_number { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, - 5 => IrOpcode::math_positive_number { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, - 6 => IrOpcode::math_whole_number { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, - 7 => IrOpcode::math_integer { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, - 8 => IrOpcode::math_angle { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, - 9 => hq_todo!(""), - 10 => IrOpcode::text { - TEXT: value.to_string(), - }, - _ => hq_bad_proj!("bad project json (block array of type ({}, string))", ty), - }, - BlockArray::Broadcast(ty, _name, id) => match ty { - 12 => IrOpcode::data_variable { - VARIABLE: id.to_string(), - assume_type: None, - }, - _ => hq_todo!(""), - }, - BlockArray::VariableOrList(ty, _name, id, _pos_x, _pos_y) => match ty { - 12 => IrOpcode::data_variable { - VARIABLE: id.to_string(), - assume_type: None, - }, - _ => hq_todo!(""), - }, - }, - type_stack, - )?); - Ok(()) - } - fn get_type_stack(&self, i: Option) -> Rc>> { - let prev_block = if let Some(j) = i { - self.get(j) - } else { - self.last() - }; - if let Some(block) = prev_block { - Rc::clone(&block.type_stack) - } else { - Rc::new(RefCell::new(None)) - } - } - fn add_block( - &mut self, - block_id: String, - blocks: &BTreeMap, - context: Rc, - last_nexts: Vec, - steps: &mut StepMap, - target_id: String, - procedures: &mut ProcMap, - ) -> Result<(), HQError> { - let block = blocks.get(&block_id).ok_or(make_hq_bug!(""))?; - match block { - Block::Normal { block_info, .. } => { - self.add_inputs( - &block_info.inputs, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - let ops: Vec<_> = match block_info.opcode { - BlockOpcode::motion_gotoxy => vec![IrOpcode::motion_gotoxy], - BlockOpcode::sensing_timer => vec![IrOpcode::sensing_timer], - BlockOpcode::sensing_dayssince2000 => vec![IrOpcode::sensing_dayssince2000], - BlockOpcode::sensing_resettimer => vec![IrOpcode::sensing_resettimer], - BlockOpcode::looks_say => vec![IrOpcode::looks_say], - BlockOpcode::looks_think => vec![IrOpcode::looks_think], - BlockOpcode::looks_show => vec![IrOpcode::looks_show], - BlockOpcode::looks_hide => vec![IrOpcode::looks_hide], - BlockOpcode::looks_hideallsprites => vec![IrOpcode::looks_hideallsprites], - BlockOpcode::looks_switchcostumeto => vec![IrOpcode::looks_switchcostumeto], - BlockOpcode::looks_costume => { - let val = match block_info.fields.get("COSTUME").ok_or( - make_hq_bad_proj!("invalid project.json - missing field COSTUME"), - )? { - Field::Value((val,)) => val, - Field::ValueId(val, _) => val, - }; - let VarVal::String(name) = val.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null costume name for COSTUME field" - ))? - else { - hq_bad_proj!( - "invalid project.json - COSTUME field is not of type String" - ); - }; - log(name.as_str()); - let index = context - .costumes - .iter() - .position(|costume| costume.name == name) - .ok_or(make_hq_bad_proj!("missing costume with name {}", name))?; - log(format!("{}", index).as_str()); - vec![IrOpcode::math_whole_number { NUM: index as i32 }] - } - BlockOpcode::looks_switchbackdropto => { - vec![IrOpcode::looks_switchbackdropto] - } - BlockOpcode::looks_switchbackdroptoandwait => { - vec![IrOpcode::looks_switchbackdroptoandwait] - } - BlockOpcode::looks_nextcostume => vec![IrOpcode::looks_nextcostume], - BlockOpcode::looks_nextbackdrop => vec![IrOpcode::looks_nextbackdrop], - BlockOpcode::looks_changeeffectby => vec![IrOpcode::looks_changeeffectby], - BlockOpcode::looks_seteffectto => vec![IrOpcode::looks_seteffectto], - BlockOpcode::looks_cleargraphiceffects => { - vec![IrOpcode::looks_cleargraphiceffects] - } - BlockOpcode::looks_changesizeby => vec![IrOpcode::looks_changesizeby], - BlockOpcode::looks_setsizeto => vec![IrOpcode::looks_setsizeto], - BlockOpcode::motion_turnleft => vec![IrOpcode::motion_turnleft], - BlockOpcode::looks_size => vec![IrOpcode::looks_size], - BlockOpcode::operator_add => vec![IrOpcode::operator_add], - BlockOpcode::operator_subtract => vec![IrOpcode::operator_subtract], - BlockOpcode::operator_multiply => vec![IrOpcode::operator_multiply], - BlockOpcode::operator_divide => vec![IrOpcode::operator_divide], - BlockOpcode::operator_mod => vec![IrOpcode::operator_mod], - BlockOpcode::operator_round => vec![IrOpcode::operator_round], - BlockOpcode::operator_lt => vec![IrOpcode::operator_lt], - BlockOpcode::operator_equals => vec![IrOpcode::operator_equals], - BlockOpcode::operator_gt => vec![IrOpcode::operator_gt], - BlockOpcode::operator_and => vec![IrOpcode::operator_and], - BlockOpcode::operator_or => vec![IrOpcode::operator_or], - BlockOpcode::operator_not => vec![IrOpcode::operator_not], - BlockOpcode::operator_random => vec![IrOpcode::operator_random], - BlockOpcode::operator_join => vec![IrOpcode::operator_join], - BlockOpcode::operator_letter_of => vec![IrOpcode::operator_letter_of], - BlockOpcode::operator_length => vec![IrOpcode::operator_length], - BlockOpcode::operator_contains => vec![IrOpcode::operator_contains], - BlockOpcode::pen_clear => vec![IrOpcode::pen_clear], - BlockOpcode::pen_stamp => vec![IrOpcode::pen_stamp], - BlockOpcode::pen_penDown => vec![IrOpcode::pen_penDown], - BlockOpcode::pen_penUp => vec![IrOpcode::pen_penUp], - BlockOpcode::pen_setPenColorToColor => { - vec![IrOpcode::pen_setPenColorToColor] - } - BlockOpcode::pen_changePenColorParamBy => { - vec![IrOpcode::pen_changePenColorParamBy] - } - BlockOpcode::pen_setPenColorParamTo => { - vec![IrOpcode::pen_setPenColorParamTo] - } - BlockOpcode::pen_changePenSizeBy => vec![IrOpcode::pen_changePenSizeBy], - BlockOpcode::pen_setPenSizeTo => vec![IrOpcode::pen_setPenSizeTo], - BlockOpcode::pen_setPenShadeToNumber => { - vec![IrOpcode::pen_setPenShadeToNumber] - } - BlockOpcode::pen_changePenShadeBy => vec![IrOpcode::pen_changePenShadeBy], - BlockOpcode::pen_setPenHueToNumber => vec![IrOpcode::pen_setPenHueToNumber], - BlockOpcode::pen_changePenHueBy => vec![IrOpcode::pen_changePenHueBy], - BlockOpcode::pen_menu_colorParam => { - let maybe_val = - match block_info - .fields - .get("colorParam") - .ok_or(make_hq_bad_proj!( - "invalid project.json - missing field colorParam" - ))? { - Field::Value((v,)) | Field::ValueId(v, _) => v, - }; - let val_varval = maybe_val.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null value for OPERATOR field" - ))?; - let VarVal::String(val) = val_varval else { - hq_bad_proj!( - "invalid project.json - expected colorParam field to be string" - ); - }; - vec![IrOpcode::text { TEXT: val }] - } - BlockOpcode::operator_mathop => { - let maybe_val = match block_info.fields.get("OPERATOR").ok_or( - make_hq_bad_proj!("invalid project.json - missing field OPERATOR"), - )? { - Field::Value((v,)) | Field::ValueId(v, _) => v, - }; - let val_varval = maybe_val.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null value for OPERATOR field" - ))?; - let VarVal::String(val) = val_varval else { - hq_bad_proj!( - "invalid project.json - expected OPERATOR field to be string" - ); - }; - vec![IrOpcode::operator_mathop { OPERATOR: val }] - } - BlockOpcode::data_variable => { - let Field::ValueId(_val, maybe_id) = - block_info.fields.get("VARIABLE").ok_or(make_hq_bad_proj!( - "invalid project.json - missing field VARIABLE" - ))? - else { - hq_bad_proj!( - "invalid project.json - missing variable id for VARIABLE field" - ); - }; - let id = maybe_id.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null variable id for VARIABLE field" - ))?; - vec![IrOpcode::data_variable { - VARIABLE: id, - assume_type: None, - }] - } - BlockOpcode::data_setvariableto => { - let Field::ValueId(_val, maybe_id) = - block_info.fields.get("VARIABLE").ok_or(make_hq_bad_proj!( - "invalid project.json - missing field VARIABLE" - ))? - else { - hq_bad_proj!( - "invalid project.json - missing variable id for VARIABLE field" - ); - }; - let id = maybe_id.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null variable id for VARIABLE field" - ))?; - vec![IrOpcode::data_setvariableto { - VARIABLE: id, - assume_type: None, - }] - } - BlockOpcode::data_changevariableby => { - let Field::ValueId(_val, maybe_id) = - block_info.fields.get("VARIABLE").ok_or(make_hq_bad_proj!( - "invalid project.json - missing field VARIABLE" - ))? - else { - hq_bad_proj!( - "invalid project.json - missing variable id for VARIABLE field" - ); - }; - let id = maybe_id.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null id for VARIABLE field" - ))?; - vec![ - IrOpcode::data_variable { - VARIABLE: id.to_string(), - assume_type: None, - }, - IrOpcode::operator_add, - IrOpcode::data_setvariableto { - VARIABLE: id, - assume_type: None, - }, - ] - } - BlockOpcode::control_if => { - let substack_id = if let BlockArrayOrId::Id(id) = block_info - .inputs - .get("SUBSTACK") - .ok_or(make_hq_bad_proj!("missing SUBSTACK input for control_if"))? - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - { - id - } else { - hq_bad_proj!("malformed SUBSTACK input") - }; - let mut new_nexts = last_nexts.clone(); - if let Some(ref next) = block_info.next { - new_nexts.push(next.clone()); - } - step_from_top_block( - substack_id.clone(), - new_nexts, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - step_from_top_block( - block_info.next.clone().ok_or(make_hq_bug!(""))?, - last_nexts, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - vec![ - IrOpcode::hq_goto_if { - step: Some((target_id.clone(), substack_id)), - does_yield: false, - }, - IrOpcode::hq_goto { - step: if block_info.next.is_some() { - Some(( - target_id, - block_info.next.clone().ok_or(make_hq_bug!(""))?, - )) - } else { - None - }, - does_yield: false, - }, - ] - } - BlockOpcode::control_if_else => { - let substack_id = if let BlockArrayOrId::Id(id) = block_info - .inputs - .get("SUBSTACK") - .ok_or(make_hq_bad_proj!("missing SUBSTACK input for control_if"))? - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - { - id - } else { - hq_bad_proj!("malformed SUBSTACK input") - }; - let substack2_id = if let BlockArrayOrId::Id(id) = block_info - .inputs - .get("SUBSTACK2") - .ok_or(make_hq_bad_proj!("missing SUBSTACK input for control_if"))? - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - { - id - } else { - hq_bad_proj!("malformed SUBSTACK2 input") - }; - let mut new_nexts = last_nexts; - if let Some(ref next) = block_info.next { - new_nexts.push(next.clone()); - } - step_from_top_block( - substack_id.clone(), - new_nexts.clone(), - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - step_from_top_block( - substack2_id.clone(), - new_nexts.clone(), - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - vec![ - IrOpcode::hq_goto_if { - step: Some((target_id.clone(), substack_id)), - does_yield: false, - }, - IrOpcode::hq_goto { - step: Some((target_id, substack2_id)), - does_yield: false, - }, - ] - } - BlockOpcode::control_repeat => { - let substack_id = if let BlockArrayOrId::Id(id) = block_info - .inputs - .get("SUBSTACK") - .ok_or(make_hq_bad_proj!("missing SUBSTACK input"))? - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - { - id - } else { - hq_bad_proj!("malformed SUBSTACK input") - }; - let next_step = match block_info.next.as_ref() { - Some(next) => Some((target_id.clone(), next.clone())), - None => last_nexts - .first() - .map(|next| (target_id.clone(), next.clone())), - }; - let condition_opcodes = vec![ - IrOpcode::hq_goto_if { - step: next_step.clone(), - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - IrOpcode::hq_goto { - step: Some((target_id.clone(), substack_id.clone())), - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - ]; - let looper_id = Uuid::new_v4().to_string(); - context.vars.borrow_mut().push(IrVar::new( - looper_id.clone(), - looper_id.clone(), - VarVal::Float(0.0), - false, - )); - if !steps.contains_key(&(target_id.clone(), looper_id.clone())) { - let type_stack = Rc::new(RefCell::new(None)); - let mut looper_opcodes = vec![IrBlock::new_with_stack_no_cast( - IrOpcode::data_variable { - VARIABLE: looper_id.clone(), - assume_type: Some(InputType::ConcreteInteger), - }, - type_stack, - )?]; - for op in [ - //IrOpcode::hq_cast(InputType::Unknown, InputType::Float), // todo: integer - IrOpcode::math_whole_number { NUM: 1 }, - IrOpcode::operator_subtract, - //IrOpcode::hq_cast(Float, Unknown), - IrOpcode::data_teevariable { - VARIABLE: looper_id.clone(), - assume_type: Some(InputType::ConcreteInteger), - }, - //IrOpcode::hq_cast(Unknown, Float), - IrOpcode::math_whole_number { NUM: 1 }, - IrOpcode::operator_lt, - ] - .into_iter() - { - looper_opcodes.push(IrBlock::new_with_stack_no_cast( - op, - Rc::clone( - &looper_opcodes.last().ok_or(make_hq_bug!(""))?.type_stack, - ), - )?); - } - for op in condition_opcodes.iter() { - looper_opcodes.push(IrBlock::new_with_stack_no_cast( - op.clone(), - Rc::clone( - &looper_opcodes.last().ok_or(make_hq_bug!(""))?.type_stack, - ), - )?); - } - //looper_opcodes.fixup_types()?; - steps.insert( - (target_id.clone(), looper_id.clone()), - Step::new(looper_opcodes, Rc::clone(&context)), - ); - } - if block_info.next.is_some() { - step_from_top_block( - block_info.next.clone().ok_or(make_hq_bug!(""))?, - last_nexts, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - } - step_from_top_block( - substack_id.clone(), - vec![looper_id.clone()], - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - let mut opcodes = vec![]; - opcodes.add_inputs( - &block_info.inputs, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - for op in [ - IrOpcode::math_whole_number { NUM: 1 }, - IrOpcode::operator_lt, - ] - .into_iter() - .chain(condition_opcodes.into_iter()) - { - opcodes.push(IrBlock::new_with_stack_no_cast( - op, - Rc::clone(&opcodes.last().ok_or(make_hq_bug!(""))?.type_stack), - )?); - } - steps.insert( - (target_id.clone(), block_id.clone()), - Step::new(opcodes.clone(), Rc::clone(&context)), - ); - vec![ - IrOpcode::operator_round, // this is correct, scratch rounds rather than floor - //IrOpcode::hq_cast(ConcreteInteger, Unknown), - IrOpcode::data_teevariable { - VARIABLE: looper_id, - assume_type: Some(InputType::ConcreteInteger), - }, - //IrOpcode::hq_cast(Unknown, Float), - IrOpcode::math_whole_number { NUM: 1 }, - IrOpcode::operator_lt, - IrOpcode::hq_goto_if { - step: next_step, - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - IrOpcode::hq_goto { - step: Some((target_id, substack_id)), - does_yield: false, - }, - ] - } - BlockOpcode::control_repeat_until => { - let substack_id = if let BlockArrayOrId::Id(id) = block_info - .inputs - .get("SUBSTACK") - .ok_or(make_hq_bad_proj!("missing SUBSTACK input"))? - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - { - id - } else { - hq_bad_proj!("malformed SUBSTACK input") - }; - let next_step = match block_info.next.as_ref() { - Some(next) => Some((target_id.clone(), next.clone())), - None => last_nexts - .first() - .map(|next| (target_id.clone(), next.clone())), - }; - let condition_opcodes = vec![ - IrOpcode::hq_goto_if { - step: next_step.clone(), - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - IrOpcode::hq_goto { - step: Some((target_id.clone(), substack_id.clone())), - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - ]; - let looper_id = Uuid::new_v4().to_string(); - if !steps.contains_key(&(target_id.clone(), looper_id.clone())) { - let mut looper_opcodes = vec![]; - looper_opcodes.add_inputs( - &block_info.inputs, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - for op in condition_opcodes.clone().into_iter() { - looper_opcodes.push(IrBlock::new_with_stack_no_cast( - op, - Rc::clone( - &looper_opcodes.last().ok_or(make_hq_bug!(""))?.type_stack, - ), - )?); - } - steps.insert( - (target_id.clone(), looper_id.clone()), - Step::new(looper_opcodes, Rc::clone(&context)), - ); - } - if block_info.next.is_some() { - step_from_top_block( - block_info.next.clone().ok_or(make_hq_bug!(""))?, - last_nexts, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - } - step_from_top_block( - substack_id.clone(), - vec![looper_id], - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - let mut opcodes = vec![]; - opcodes.add_inputs( - &block_info.inputs, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - for op in condition_opcodes.into_iter() { - opcodes.push(IrBlock::new_with_stack_no_cast( - op, - Rc::clone(&opcodes.last().ok_or(make_hq_bug!(""))?.type_stack), - )?); - } - steps.insert( - (target_id.clone(), block_id.clone()), - Step::new(opcodes.clone(), Rc::clone(&context)), - ); - vec![ - IrOpcode::hq_goto_if { - step: next_step, - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - IrOpcode::hq_goto { - step: Some((target_id, substack_id)), - does_yield: false, - }, - ] - } - BlockOpcode::control_forever => { - let substack_id = if let BlockArrayOrId::Id(id) = block_info - .inputs - .get("SUBSTACK") - .ok_or(make_hq_bad_proj!("missing SUBSTACK input for control_if"))? - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - { - id - } else { - hq_bad_proj!("malformed SUBSTACK input") - }; - let goto_opcode = IrOpcode::hq_goto { - step: Some((target_id.clone(), substack_id.clone())), - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }; - let looper_id = Uuid::new_v4().to_string(); - if !steps.contains_key(&(target_id.clone(), looper_id.clone())) { - let looper_opcodes = vec![IrBlock::new_with_stack_no_cast( - goto_opcode.clone(), - Rc::new(RefCell::new(None)), - )?]; - steps.insert( - (target_id.clone(), looper_id.clone()), - Step::new(looper_opcodes, Rc::clone(&context)), - ); - } - if let Some(next) = block_info.next.clone() { - step_from_top_block( - next, - last_nexts, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - } - step_from_top_block( - substack_id.clone(), - vec![looper_id], - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - let opcodes = vec![IrBlock::new_with_stack_no_cast( - goto_opcode, - Rc::new(RefCell::new(None)), - )?]; - steps.insert( - (target_id.clone(), block_id.clone()), - Step::new(opcodes.clone(), Rc::clone(&context)), - ); - vec![IrOpcode::hq_goto { - step: Some((target_id, substack_id)), - does_yield: false, - }] - } - BlockOpcode::procedures_call => { - let serde_json::Value::String(proccode) = - block_info.mutation.mutations.get("proccode").ok_or( - make_hq_bad_proj!("missing proccode mutation in procedures_call"), - )? - else { - hq_bad_proj!("non-string proccode mutation") - }; - let warp = match block_info.mutation.mutations.get("warp").ok_or( - make_hq_bad_proj!("missing warp mutation in procedures_call"), - )? { - serde_json::Value::Bool(w) => *w, - serde_json::Value::String(wstr) => match wstr.as_str() { - "true" => true, - "false" => false, - _ => hq_bad_proj!("unexpected string for warp mutation"), - }, - _ => hq_bad_proj!("bad type for warp mutation"), - }; - if !warp { - hq_todo!("non-warp procedure"); - } - let procedure = add_procedure( - target_id.clone(), - proccode.clone(), - warp, - blocks, - steps, - procedures, - Rc::clone(&context), - )?; - vec![IrOpcode::hq_launch_procedure(procedure.clone())] - } - ref other => hq_todo!("unknown block {:?}", other), - }; - - for op in ops.into_iter() { - let type_stack = self.get_type_stack(None); - let mut casts: BTreeMap = BTreeMap::new(); - let mut add_cast = |i, ty: &InputType| { - casts.insert(i, ty.clone()); - }; - let block = - IrBlock::new_with_stack(op.clone(), Rc::clone(&type_stack), &mut add_cast)?; - for (j, cast_type) in casts.iter() { - let cast_pos = self - .iter() - .rposition(|b| b.type_stack.len() == type_stack.len() - j) - .ok_or(make_hq_bug!(""))?; - let cast_stack = self.get_type_stack(Some(cast_pos)); - #[allow(clippy::let_and_return)] - let cast_block = IrBlock::new_with_stack_no_cast( - IrOpcode::hq_cast( - { - let from = cast_stack - .borrow() - .clone() - .ok_or(make_hq_bug!( - "tried to cast to {:?} from empty type stack at {:?}", - cast_type.clone(), - op - ))? - .1; - from - }, - cast_type.clone(), - ), - cast_stack, - )?; - self.insert(cast_pos + 1, cast_block); - } - self.push(block); - /*if let Some(ty) = casts.get(&(type_stack.len() - 1 - i) { - let this_prev_block = self.last(); - let this_type_stack = if let Some(block) = this_prev_block { - Rc::clone(&block.type_stack) - } else { - hq_bug!("tried to cast from an empty type stack") - //Rc::new(RefCell::new(None)) - }; - self.push(IrBlock::new_with_stack_no_cast( - IrOpcode::hq_cast({ let from = this_type_stack.borrow().clone().ok_or(make_hq_bug!("tried to cast to {:?} from empty type stack at {:?}", ty, op))?.1; from }, ty.clone()), - this_type_stack, - )?); - }*/ - } - } - Block::Special(a) => self.add_block_arr(a)?, - }; - Ok(()) - } -} - -pub fn step_from_top_block<'a>( - top_id: String, - mut last_nexts: Vec, - blocks: &BTreeMap, - context: Rc, - steps: &'a mut StepMap, - target_id: String, - procedures: &mut ProcMap, -) -> Result<&'a Step, HQError> { - if steps.contains_key(&(target_id.clone(), top_id.clone())) { - return steps.get(&(target_id, top_id)).ok_or(make_hq_bug!("")); - } - let mut ops: Vec = vec![]; - let mut next_block = blocks.get(&top_id).ok_or(make_hq_bug!(""))?; - let mut next_id = Some(top_id.clone()); - loop { - ops.add_block( - next_id.clone().ok_or(make_hq_bug!(""))?, - blocks, - Rc::clone(&context), - last_nexts.clone(), - steps, - target_id.clone(), - procedures, - )?; - if next_block - .block_info() - .ok_or(make_hq_bug!(""))? - .next - .is_none() - { - next_id = last_nexts.pop(); - } else { - next_id.clone_from(&next_block.block_info().ok_or(make_hq_bug!(""))?.next); - } - if ops.is_empty() { - hq_bug!("assertion failed: !ops.is_empty()") - }; - if matches!( - ops.last().ok_or(make_hq_bug!(""))?.opcode(), - IrOpcode::hq_goto { .. } - ) { - next_id = None; - } - if next_id.is_none() { - break; - } else if let Some(block) = blocks.get(&next_id.clone().ok_or(make_hq_bug!(""))?) { - next_block = block; - } else if steps.contains_key(&(target_id.clone(), next_id.clone().ok_or(make_hq_bug!(""))?)) - { - ops.push(IrBlock::new_with_stack_no_cast( - IrOpcode::hq_goto { - step: Some((target_id.clone(), next_id.clone().ok_or(make_hq_bug!(""))?)), - does_yield: false, - }, - Rc::clone(&ops.last().unwrap().type_stack), - )?); - next_id = None; - break; - } else { - hq_bad_proj!("invalid next_id"); - } - let Some(last_block) = ops.last() else { - unreachable!() - }; - if last_block.does_request_redraw() - && !context.proc.clone().is_some_and(|p| p.warp) - && !(*last_block.opcode() == IrOpcode::looks_say && context.dbg) - { - break; - } - } - //ops.fixup_types()?; - let mut step = Step::new(ops.clone(), Rc::clone(&context)); - let goto = if let Some(ref id) = next_id { - step_from_top_block( - id.clone(), - last_nexts, - blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?; - IrBlock::new_with_stack_no_cast( - IrOpcode::hq_goto { - step: Some((target_id.clone(), id.clone())), - does_yield: !context.proc.clone().is_some_and(|p| p.warp), - }, - Rc::clone(&step.opcodes().last().unwrap().type_stack), - )? - } else { - IrBlock::new_with_stack_no_cast( - IrOpcode::hq_goto { - step: None, - does_yield: false, - }, - Rc::clone(&step.opcodes().last().unwrap().type_stack), - )? - }; - step.opcodes_mut().push(goto); - steps.insert((target_id.clone(), top_id.clone()), step); - steps.get(&(target_id, top_id)).ok_or(make_hq_bug!("")) -} - -impl Thread { - pub fn new(start: ThreadStart, first_step: String, target_id: String) -> Thread { - Thread { - start, - first_step, - target_id, - } - } - pub fn start(&self) -> &ThreadStart { - &self.start - } - pub fn first_step(&self) -> &String { - &self.first_step - } - pub fn target_id(&self) -> &String { - &self.target_id - } - pub fn from_hat( - hat: Block, - blocks: BTreeMap, - context: Rc, - steps: &mut StepMap, - target_id: String, - procedures: &mut ProcMap, - ) -> Result { - let (first_step_id, _first_step) = if let Block::Normal { block_info, .. } = &hat { - if let Some(next_id) = &block_info.next { - ( - next_id.clone(), - step_from_top_block( - next_id.clone(), - vec![], - &blocks, - Rc::clone(&context), - steps, - target_id.clone(), - procedures, - )?, - ) - } else { - unreachable!(); - } - } else { - unreachable!(); - }; - let start_type = if let Block::Normal { block_info, .. } = &hat { - match block_info.opcode { - BlockOpcode::event_whenflagclicked => ThreadStart::GreenFlag, - _ => hq_todo!(""), - } - } else { - unreachable!() - }; - Ok(Self::new(start_type, first_step_id, target_id)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn create_ir() -> Result<(), HQError> { - use crate::sb3::Sb3Project; - use std::fs; - let proj: Sb3Project = fs::read_to_string("./project.json") - .expect("couldn't read hq-test.project.json") - .try_into()?; - let ir: IrProject = proj.try_into()?; - println!("{}", ir); - Ok(()) - } - - #[test] - fn const_fold() -> Result<(), HQError> { - use crate::sb3::Sb3Project; - use std::fs; - let proj: Sb3Project = fs::read_to_string("./project.json") - .expect("couldn't read hq-test.project.json") - .try_into()?; - let mut ir: IrProject = proj.try_into()?; - ir.const_fold()?; - println!("{}", ir); - Ok(()) - } - #[test] - fn opt() -> Result<(), HQError> { - use crate::sb3::Sb3Project; - use std::fs; - let proj: Sb3Project = fs::read_to_string("./project.json") - .expect("couldn't read hq-test.project.json") - .try_into()?; - let mut ir: IrProject = proj.try_into()?; - ir.optimise()?; - println!("{}", ir); - Ok(()) - } - - #[test] - fn input_types() { - use InputType::*; - assert!(Number.includes(&Float)); - assert!(Number.includes(&Integer)); - assert!(Number.includes(&ConcreteInteger)); - assert!(Number.includes(&Boolean)); - assert!(Any.includes(&Float)); - assert!(Any.includes(&Integer)); - assert!(Any.includes(&ConcreteInteger)); - assert!(Any.includes(&Boolean)); - assert!(Any.includes(&Number)); - assert!(Any.includes(&String)); - assert!(!String.includes(&Float)); - } -} +pub mod types; \ No newline at end of file diff --git a/src/ir/types.rs b/src/ir/types.rs new file mode 100644 index 00000000..97927f2a --- /dev/null +++ b/src/ir/types.rs @@ -0,0 +1,61 @@ +use bitmask_enum::bitmask; + +#[bitmask(u32)] +#[bitmask_config(vec_debug, flags_iter)] +pub enum Type { + IntZero, + IntPos, + IntNeg, + IntNonZero = Self::IntPos.or(Self::IntNeg).bits, + Int = Self::IntNonZero.or(Self::IntZero).bits, + + FloatPosZero, + FloatNegZero, + FloatZero = Self::FloatPosZero.or(Self::FloatNegZero).bits, + + FloatPosInt, + FloatPosFrac, + FloatPosReal = Self::FloatPosInt.or(Self::FloatPosFrac).bits, + + FloatNegInt, + FloatNegFrac, + FloatNegReal = Self::FloatNegInt.or(Self::FloatNegFrac).bits, + + FloatPosInf, + FloatNegInf, + FloatInf = Self::FloatPosInf.or(Self::FloatNegInf).bits, + + FloatNan, + + FloatPos = Self::FloatPosReal.or(Self::FloatPosInf).bits, + FloatNeg = Self::FloatNegReal.or(Self::FloatNegInf).bits, + + FloatPosWhole = Self::FloatPosInt.or(Self::FloatPosZero).bits, + FloatNegWhole = Self::FloatNegInt.or(Self::FloatNegZero).bits, + + FloatInt = Self::FloatPosWhole.or(Self::FloatNegWhole).bits, + FloatFrac = Self::FloatPosFrac.or(Self::FloatNegFrac).bits, + FloatReal = Self::FloatInt.or(Self::FloatFrac).bits, + FloatNotNan = Self::FloatReal.or(Self::FloatInf).bits, + Float = Self::FloatNotNan.or(Self::FloatNan).bits, + + BooleanTrue, + BooleanFalse, + Boolean = Self::BooleanTrue.or(Self::BooleanFalse).bits, + + QuasiInt = Self::Int.or(Self::Boolean).bits, + + Number = Self::QuasiInt.or(Self::Float).bits, + + StringNumber, // a string which can be interpreted as a non-nan number + StringBoolean, // "true" or "false" + StringNan, // some other string which can only be interpreted as NaN + String = Self::StringNumber.or(Self::StringBoolean).or(Self::StringNan).bits, + + QuasiBoolean = Self::Boolean.or(Self::StringBoolean).bits, + QuasiNumber = Self::Number.or(Self::StringNumber).bits, + + Any = Self::String.or(Self::Number).bits, + + Color, +} diff --git a/src/lib.rs b/src/lib.rs index 828f2aef..8627e575 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,13 +9,15 @@ use wasm_bindgen::prelude::*; #[macro_use] mod error; pub mod ir; -pub mod ir_opt; +// pub mod ir_opt; pub mod sb3; -pub mod targets; +// pub mod wasm; +#[macro_use] +pub mod instructions; -pub use error::{HQError, HQErrorType}; +pub use error::{HQError, HQErrorType, HQResult}; -use targets::wasm; +// use wasm::wasm; #[cfg(not(test))] #[wasm_bindgen(js_namespace=console)] @@ -28,9 +30,21 @@ pub fn log(s: &str) { println!("{s}") } -#[wasm_bindgen] -pub fn sb3_to_wasm(proj: &str) -> Result { - let mut ir_proj = ir::IrProject::try_from(sb3::Sb3Project::try_from(proj)?)?; - ir_proj.optimise()?; - ir_proj.try_into() -} +// #[wasm_bindgen] +// pub fn sb3_to_wasm(proj: &str) -> Result { +// let mut ir_proj = ir::IrProject::try_from(sb3::Sb3Project::try_from(proj)?)?; +// ir_proj.optimise()?; +// ir_proj.try_into() +// } + +/// commonly used _things_ which would be nice not to have to type out every time +pub mod prelude { + pub use crate::{HQError, HQResult}; + pub use alloc::boxed::Box; + pub use alloc::collections::BTreeMap; + pub use alloc::rc::Rc; + pub use alloc::string::{String, ToString}; + pub use alloc::vec::Vec; + pub use core::cell::RefCell; + pub use core::fmt; +} \ No newline at end of file diff --git a/src/old-ir.rs b/src/old-ir.rs new file mode 100644 index 00000000..90a2ffa8 --- /dev/null +++ b/src/old-ir.rs @@ -0,0 +1,2345 @@ +// intermediate representation +use crate::log; +use crate::sb3::{ + Block, BlockArray, BlockArrayOrId, BlockOpcode, CostumeDataFormat, Field, Input, Sb3Project, + VarVal, VariableInfo, +}; +use crate::HQError; +use alloc::boxed::Box; +use alloc::collections::BTreeMap; +use alloc::rc::Rc; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::cell::RefCell; +use core::fmt; +use core::hash::BuildHasherDefault; +use hashers::fnv::FNV1aHasher64; +use indexmap::IndexMap; +use lazy_regex::{lazy_regex, Lazy}; +use regex::Regex; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq)] +pub struct IrCostume { + pub name: String, + pub data_format: CostumeDataFormat, + pub md5ext: String, +} + +pub type ProcMap = BTreeMap<(String, String), Procedure>; +pub type StepMap = IndexMap<(String, String), Step, BuildHasherDefault>; + +#[derive(Debug)] +pub struct IrProject { + pub threads: Vec, + pub vars: Rc>>, + pub target_names: Vec, + pub costumes: Vec>, + pub steps: StepMap, + pub procedures: ProcMap, // maps (target_id, proccode) to (target_id, step_top_block_id, warp) + pub sb3: Sb3Project, +} + +impl fmt::Display for IrProject { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{{\n\tthreads: {:?},\n\tvars: {:?},\n\t,target_names: {:?},\n\tcostumes: {:?},\n\tsteps: {:?}\n}}", + self.threads, + self.vars, + self.target_names, + self.costumes, + self.steps + ) + } +} + +impl TryFrom for IrProject { + type Error = HQError; + + fn try_from(sb3: Sb3Project) -> Result { + let vars: Rc>> = Rc::new(RefCell::new( + sb3.targets + .iter() + .flat_map(|target| { + target.variables.iter().map(|(id, info)| match info { + VariableInfo::LocalVar(name, val) => { + IrVar::new(id.clone(), name.clone(), val.clone(), false) + } + VariableInfo::CloudVar(name, val, is_cloud) => { + IrVar::new(id.clone(), name.clone(), val.clone(), *is_cloud) + } + }) + }) + .collect(), + )); + + let costumes: Vec> = sb3 + .targets + .iter() + .map(|target| { + target + .costumes + .iter() + .map(|costume| { + IrCostume { + name: costume.name.clone(), + data_format: costume.data_format, + md5ext: costume.md5ext.clone(), + //data: load_asset(costume.md5ext.as_str()), + } + }) + .collect() + }) + .collect(); + + let mut procedures = BTreeMap::new(); + + let mut steps: StepMap = Default::default(); + // insert a noop step so that these step indices match up with the step function indices in the generated wasm + // (step function 0 is a noop) + steps.insert( + ("".into(), "".into()), + Step::new( + vec![], + Rc::new(ThreadContext { + target_index: u32::MAX, + dbg: false, + vars: Rc::new(RefCell::new(vec![])), + target_num: sb3.targets.len(), + costumes: vec![], + proc: None, + }), + ), + ); + let mut threads: Vec = vec![]; + for (target_index, target) in sb3.targets.iter().enumerate() { + for (id, block) in + target + .blocks + .clone() + .iter() + .filter(|(_id, b)| match b.block_info() { + Some(block_info) => { + block_info.top_level + && matches!(block_info.opcode, BlockOpcode::event_whenflagclicked) + } + None => false, + }) + { + let context = Rc::new(ThreadContext { + target_index: target_index.try_into().map_err(|_| make_hq_bug!(""))?, + dbg: target.comments.clone().iter().any(|(_id, comment)| { + matches!(comment.block_id.clone(), Some(d) if &d == id) + && comment.text.clone() == *"hq-dbg" + }), + vars: Rc::clone(&vars), + target_num: sb3.targets.len(), + costumes: costumes.get(target_index).ok_or(make_hq_bug!(""))?.clone(), + proc: None, + }); + let thread = Thread::from_hat( + block.clone(), + target.blocks.clone(), + context, + &mut steps, + target.name.clone(), + &mut procedures, + )?; + threads.push(thread); + } + } + let target_names = sb3 + .targets + .iter() + .map(|t| t.name.clone()) + .collect::>(); + Ok(Self { + vars, + threads, + target_names, + steps, + costumes, + sb3, + procedures, + }) + } +} + +#[allow(non_camel_case_types)] +#[allow(non_snake_case)] +#[derive(Debug, Clone, PartialEq)] +pub enum IrOpcode { + boolean { + BOOL: bool, + }, + control_repeat, + control_repeat_until, + control_while, + control_for_each { + VARIABLE: String, + }, + control_forever, + control_wait, + control_wait_until, + control_stop { + STOP_OPTION: String, + }, + control_create_clone_of, + control_create_clone_of_menu { + CLONE_OPTOON: String, + }, + control_delete_this_clone, + control_get_counter, + control_incr_counter, + control_clear_counter, + control_all_at_once, + control_start_as_clone, + data_variable { + VARIABLE: String, + assume_type: Option, + }, + data_setvariableto { + VARIABLE: String, + assume_type: Option, + }, + data_teevariable { + VARIABLE: String, + assume_type: Option, + }, + data_hidevariable { + VARIABLE: String, + }, + data_showvariable { + VARIABLE: String, + }, + data_listcontents { + LIST: String, + }, + data_addtolist, + data_deleteoflist, + data_deletealloflist, + data_insertatlist, + data_replaceitemoflist, + data_itemoflist, + data_itemnumoflist, + data_lengthoflist, + data_listcontainsitem, + data_hidelist, + data_showlist, + event_broadcast, + event_broadcastandwait, + event_whenflagclicked, + event_whenkeypressed, + event_whenthisspriteclicked, + event_whentouchingobject, + event_whenstageclicked, + event_whenbackdropswitchesto, + event_whengreaterthan, + event_whenbroadcastreceived, + hq_cast(InputType, InputType), + hq_drop(usize), + hq_goto { + step: Option<(String, String)>, + does_yield: bool, + }, + hq_goto_if { + step: Option<(String, String)>, + does_yield: bool, + }, + hq_launch_procedure(Procedure), + looks_say, + looks_sayforsecs, + looks_think, + looks_thinkforsecs, + looks_show, + looks_hide, + looks_hideallsprites, + looks_switchcostumeto, + looks_switchbackdropto, + looks_switchbackdroptoandwait, + looks_nextcostume, + looks_nextbackdrop, + looks_changeeffectby, + looks_seteffectto, + looks_cleargraphiceffects, + looks_changesizeby, + looks_setsizeto, + looks_changestretchby, + looks_setstretchto, + looks_gotofrontback, + looks_goforwardbackwardlayers, + looks_size, + looks_costumenumbername, + looks_backdropnumbername, + looks_backdrops, + math_angle { + NUM: i32, + }, + math_integer { + NUM: i32, + }, + math_number { + NUM: f64, + }, + math_positive_number { + NUM: i32, + }, + math_whole_number { + NUM: i32, + }, + motion_movesteps, + motion_gotoxy, + motion_goto, + motion_turnright, + motion_turnleft, + motion_pointindirection, + motion_pointtowards, + motion_glidesecstoxy, + motion_glideto, + motion_ifonedgebounce, + motion_setrotationstyle, + motion_changexby, + motion_setx, + motion_changeyby, + motion_sety, + motion_xposition, + motion_yposition, + motion_direction, + motion_scroll_right, + motion_scroll_up, + motion_align_scene, + motion_xscroll, + motion_yscroll, + motion_pointtowards_menu, + operator_add, + operator_subtract, + operator_multiply, + operator_divide, + operator_lt, + operator_equals, + operator_gt, + operator_and, + operator_or, + operator_not, + operator_random, + operator_join, + operator_letter_of, + operator_length, + operator_contains, + operator_mod, + operator_round, + operator_mathop { + OPERATOR: String, + }, + pen_clear, + pen_stamp, + pen_penDown, + pen_penUp, + pen_setPenColorToColor, + pen_changePenColorParamBy, + pen_setPenColorParamTo, + pen_changePenSizeBy, + pen_setPenSizeTo, + pen_setPenShadeToNumber, + pen_changePenShadeBy, + pen_setPenHueToNumber, + pen_changePenHueBy, + argument_reporter_string_number, + argument_reporter_boolean, + sensing_touchingobject, + sensing_touchingcolor, + sensing_coloristouchingcolor, + sensing_distanceto, + sensing_distancetomenu, + sensing_timer, + sensing_resettimer, + sensing_of, + sensing_mousex, + sensing_mousey, + sensing_setdragmode, + sensing_mousedown, + sensing_keypressed, + sensing_current, + sensing_dayssince2000, + sensing_loudness, + sensing_loud, + sensing_askandwait, + sensing_answer, + sensing_username, + sensing_userid, + sensing_touchingobjectmenu, + sensing_keyoptions, + sensing_of_object_menu, + sound_play, + sound_playuntildone, + sound_stopallsounds, + sound_seteffectto, + sound_changeeffectby, + sound_cleareffects, + sound_sounds_menu, + sound_beats_menu, + sound_effects_menu, + sound_setvolumeto, + sound_changevolumeby, + sound_volume, + text { + TEXT: String, + }, + unknown_const { + val: IrVal, + }, +} + +#[derive(Clone)] +pub struct TypeStack(pub Rc>>, pub InputType); + +impl TypeStack { + pub fn new_some(prev: TypeStack) -> Rc>> { + Rc::new(RefCell::new(Some(prev))) + } + pub fn new(prev: Option) -> Rc>> { + Rc::new(RefCell::new(prev)) + } +} + +#[allow(clippy::len_without_is_empty)] +pub trait TypeStackImpl { + fn get(&self, i: usize) -> Rc>>; + fn len(&self) -> usize; +} + +impl TypeStackImpl for Rc>> { + fn get(&self, i: usize) -> Rc>> { + if i == 0 || self.borrow().is_none() { + Rc::clone(self) + } else { + self.borrow().clone().unwrap().0.get(i - 1) + } + } + + fn len(&self) -> usize { + if self.borrow().is_none() { + 0 + } else { + 1 + self.borrow().clone().unwrap().0.len() + } + } +} + +#[derive(Debug, Clone)] +pub struct IrBlock { + pub opcode: IrOpcode, + pub type_stack: Rc>>, +} + +impl IrBlock { + pub fn new_with_stack( + opcode: IrOpcode, + type_stack: Rc>>, + add_cast: &mut F, + ) -> Result + where + F: FnMut(usize, &InputType), + { + let expected_inputs = opcode.expected_inputs()?; + if type_stack.len() < expected_inputs.len() { + hq_bug!( + "expected {} inputs, got {} at {:?}", + expected_inputs.len(), + type_stack.len(), + opcode, + ); + } + let arity = expected_inputs.len(); + for i in 0..expected_inputs.len() { + let expected = expected_inputs.get(i).ok_or(make_hq_bug!(""))?; + let actual = &type_stack.get(arity - 1 - i).borrow().clone().unwrap().1; + if !expected.includes(actual) { + add_cast(arity - 1 - i, expected); + } + } + let output_stack = opcode.output(type_stack)?; + Ok(IrBlock { + opcode, + type_stack: output_stack, + }) + } + + pub fn new_with_stack_no_cast( + opcode: IrOpcode, + type_stack: Rc>>, + ) -> Result { + let expected_inputs = opcode.expected_inputs()?; + if type_stack.len() < expected_inputs.len() { + hq_bug!( + "expected {} inputs, got {}", + expected_inputs.len(), + type_stack.len() + ); + } + let arity = expected_inputs.len(); + for i in 0..expected_inputs.len() { + let expected = expected_inputs.get(i).ok_or(make_hq_bug!(""))?; + let actual = &type_stack + .get(arity - 1 - i) + .borrow() + .clone() + .ok_or(make_hq_bug!(""))? + .1; + + if !expected.includes(actual) { + hq_bug!( + "cast needed at input position {:}, {:?} -> {:?}, but no casts were expected; at {:?}", + i, + actual, + expected, + opcode, + ); + } + } + let output_stack = opcode.output(type_stack)?; + Ok(IrBlock { + opcode, + type_stack: output_stack, + }) + } + + pub fn does_request_redraw(&self) -> bool { + use IrOpcode::*; + matches!( + self.opcode(), + looks_say + | looks_think + | hq_goto { + does_yield: true, + .. + } + | motion_gotoxy + | pen_penDown + | pen_clear + | looks_switchcostumeto + | motion_turnleft + | motion_turnright + | looks_setsizeto + | looks_changesizeby + ) + } + pub fn is_hat(&self) -> bool { + use IrOpcode::*; + matches!(self.opcode, event_whenflagclicked) + } + pub fn opcode(&self) -> &IrOpcode { + &self.opcode + } + pub fn type_stack(&self) -> Rc>> { + Rc::clone(&self.type_stack) + } + + pub fn is_const(&self) -> bool { + use IrOpcode::*; + matches!( + self.opcode(), + math_number { .. } + | math_whole_number { .. } + | math_integer { .. } + | math_angle { .. } + | math_positive_number { .. } + | text { .. } + ) + } + + pub fn const_value(&self, value_stack: &mut Vec) -> Result, HQError> { + use IrOpcode::*; + //dbg!(value_stack.len()); + let arity = self.opcode().expected_inputs()?.len(); + if arity > value_stack.len() { + return Ok(None); + } + match self.opcode() { + math_number { NUM } => value_stack.push(IrVal::Float(*NUM)), + math_positive_number { NUM } + | math_integer { NUM } + | math_angle { NUM } + | math_whole_number { NUM } => value_stack.push(IrVal::Int(*NUM)), + text { TEXT } => value_stack.push(IrVal::String(TEXT.to_string())), + operator_add => { + let num2 = value_stack.pop().unwrap().to_f64(); + let num1 = value_stack.pop().unwrap().to_f64(); + value_stack.push(IrVal::Float(num1 + num2)); + } + operator_subtract => { + let num2 = value_stack.pop().unwrap().to_f64(); + let num1 = value_stack.pop().unwrap().to_f64(); + value_stack.push(IrVal::Float(num1 - num2)); + } + operator_multiply => { + let num2 = value_stack.pop().unwrap().to_f64(); + let num1 = value_stack.pop().unwrap().to_f64(); + value_stack.push(IrVal::Float(num1 * num2)); + } + operator_divide => { + let num2 = value_stack.pop().unwrap().to_f64(); + let num1 = value_stack.pop().unwrap().to_f64(); + value_stack.push(IrVal::Float(num1 / num2)); + } + operator_mod => { + let num2 = value_stack.pop().unwrap().to_f64(); + let num1 = value_stack.pop().unwrap().to_f64(); + value_stack.push(IrVal::Float(num1 % num2)); + } + operator_lt => { + let num2 = value_stack.pop().unwrap().to_f64(); + let num1 = value_stack.pop().unwrap().to_f64(); + value_stack.push(IrVal::Boolean(num1 < num2)); + } + operator_gt => { + let num2 = value_stack.pop().unwrap().to_f64(); + let num1 = value_stack.pop().unwrap().to_f64(); + value_stack.push(IrVal::Boolean(num1 > num2)); + } + operator_and => { + let bool2 = value_stack.pop().unwrap().to_bool(); + let bool1 = value_stack.pop().unwrap().to_bool(); + value_stack.push(IrVal::Boolean(bool1 && bool2)); + } + operator_or => { + let bool2 = value_stack.pop().unwrap().to_bool(); + let bool1 = value_stack.pop().unwrap().to_bool(); + value_stack.push(IrVal::Boolean(bool1 || bool2)); + } + operator_join => { + let str2 = value_stack.pop().unwrap().to_string(); + let str1 = value_stack.pop().unwrap().to_string(); + value_stack.push(IrVal::String(str1 + &str2)); + } + operator_length => { + let string = value_stack.pop().unwrap().to_string(); + value_stack.push(IrVal::Int(string.len().try_into().unwrap())); + } + operator_not => { + let b = value_stack.pop().unwrap().to_bool(); + value_stack.push(IrVal::Boolean(!b)); + } + operator_round => { + let num = value_stack.pop().unwrap().to_f64(); + value_stack.push(IrVal::Int(num as i32)); + } + hq_cast(_from, to) => { + let val = value_stack.pop().unwrap(); + let ty = to.least_restrictive_concrete_type(); + match ty { + InputType::ConcreteInteger => value_stack.push(IrVal::Int(val.to_i32())), + InputType::Float => value_stack.push(IrVal::Float(val.to_f64())), + InputType::String => value_stack.push(IrVal::String(val.to_string())), + InputType::Boolean => value_stack.push(IrVal::Boolean(val.to_bool())), + InputType::Unknown => { + value_stack.push(IrVal::Unknown(Box::new(val))); + } + _ => hq_bug!("unexpected non-concrete type {:?}", ty), + }; + } + _ => return Ok(None), + }; + Ok(Some(())) + } +} + +/// represents an input (or output) type of a block. +/// output types shpuld always be exactly known, +/// so unions, or types that resolve to a union, +/// should never be used as output types +#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)] +pub enum InputType { + Any, + String, + Number, + Float, + Integer, + Boolean, + ConcreteInteger, + Unknown, + Union(Box, Box), +} + +impl InputType { + /// returns the base type that a type is aliased to, if present; + /// otherwise, returns a clone of itself + pub fn base_type(&self) -> InputType { + use InputType::*; + // unions of types should be represented with the least restrictive type first, + // so that casting chooses the less restrictive type to cast to + match self { + Any => Union( + Box::new(Union(Box::new(Unknown), Box::new(String))), + Box::new(Number), + ) + .base_type(), + Number => Union(Box::new(Float), Box::new(Integer)).base_type(), + Integer => Union(Box::new(ConcreteInteger), Box::new(Boolean)).base_type(), + Union(a, b) => Union(Box::new(a.base_type()), Box::new(b.base_type())), + _ => self.clone(), + } + } + + /// when a type is a union type, returns the first concrete type in that union + /// (this assumes that the least restrictive type is placed first in the union). + /// otherwise, returns itself. + pub fn least_restrictive_concrete_type(&self) -> InputType { + match self.base_type() { + InputType::Union(a, _) => a.least_restrictive_concrete_type(), + other => other, + } + } + + /// determines whether a type is a superyype of another type + pub fn includes(&self, other: &Self) -> bool { + if self.base_type() == other.base_type() { + true + } else if let InputType::Union(a, b) = self.base_type() { + a.includes(other) || b.includes(other) + } else { + false + } + } + + /// attempts to promote a type to one of the ones provided. + /// none of the provided types should overlap with each other, + /// so that there is no ambiguity over which type it should be promoted to. + /// if there are no types that can be prpmoted to, + /// an `Err(HQError)` will be returned. + pub fn loosen_to(&self, others: T) -> Result + where + T: IntoIterator, + T::IntoIter: ExactSizeIterator, + { + for other in others { + if other.includes(self) { + return Ok(other); + } + } + Err(make_hq_bug!( + "couldn't find any types that this type can be demoted to" + )) + } +} + +impl IrOpcode { + pub fn does_request_redraw(&self) -> bool { + use IrOpcode::*; + matches!(self, looks_say | looks_think) + } + + pub fn expected_inputs(&self) -> Result, HQError> { + use InputType::*; + use IrOpcode::*; + Ok(match self { + operator_add | operator_subtract | operator_multiply | operator_divide + | operator_mod | operator_random => vec![Number, Number], + operator_round | operator_mathop { .. } => vec![Number], + looks_say | looks_think => vec![Unknown], + data_setvariableto { + assume_type: None, .. + } + | data_teevariable { + assume_type: None, .. + } => vec![Unknown], + data_setvariableto { + assume_type: Some(ty), + .. + } + | data_teevariable { + assume_type: Some(ty), + .. + } => vec![ty.clone()], + math_integer { .. } + | math_angle { .. } + | math_whole_number { .. } + | math_positive_number { .. } + | math_number { .. } + | sensing_timer + | sensing_dayssince2000 + | looks_size + | data_variable { .. } + | text { .. } + | boolean { .. } + | unknown_const { .. } => vec![], + operator_lt | operator_gt => vec![Number, Number], + operator_equals => vec![Any, Any], + operator_and | operator_or => vec![Boolean, Boolean], + operator_not => vec![Boolean], + operator_join | operator_contains => vec![String, String], + operator_letter_of => vec![Number, String], + operator_length => vec![String], + hq_goto { .. } + | sensing_resettimer + | pen_clear + | pen_stamp + | pen_penDown + | pen_penUp => vec![], + hq_goto_if { .. } => vec![Boolean], + hq_drop(n) => vec![Any; *n], + hq_cast(from, _to) => vec![from.clone()], + pen_setPenColorToColor + | pen_changePenSizeBy + | pen_setPenSizeTo + | pen_setPenShadeToNumber + | pen_changePenShadeBy + | pen_setPenHueToNumber + | pen_changePenHueBy + | looks_setsizeto + | looks_changesizeby + | motion_turnleft + | motion_turnright => vec![Number], + // todo: looks_switchcostumeto waiting on generic monomorphisation to work properly + looks_switchcostumeto => vec![Any], + pen_changePenColorParamBy | pen_setPenColorParamTo => vec![String, Number], + motion_gotoxy => vec![Number, Number], + hq_launch_procedure(Procedure { arg_types, .. }) => arg_types.clone(), + _ => hq_todo!("{:?}", &self), + }) + } + + pub fn output( + &self, + type_stack: Rc>>, + ) -> Result>>, HQError> { + use InputType::*; + use IrOpcode::*; + let expected_inputs = self.expected_inputs()?; + if type_stack.len() < expected_inputs.len() { + hq_bug!( + "expected {} inputs, got {}", + expected_inputs.len(), + type_stack.len() + ); + } + let arity = expected_inputs.len(); + let get_input = |i| { + Ok(type_stack + .get(arity - 1 - i) + .borrow() + .clone() + .ok_or(make_hq_bug!(""))? + .1) + }; + let output = match self { + data_teevariable { .. } => Ok(Rc::clone(&type_stack)), + hq_cast(_from, to) => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack.get(1)), + to.clone(), + ))), + operator_add | operator_subtract | operator_multiply | operator_random + | operator_mod => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack.get(2)), + if Integer.includes(&get_input(0)?) && Integer.includes(&get_input(1)?) { + ConcreteInteger + } else { + Float + }, + ))), + operator_divide => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack.get(2)), + Float, + ))), + looks_size | sensing_timer | sensing_dayssince2000 | math_number { .. } => Ok( + TypeStack::new_some(TypeStack(Rc::clone(&type_stack), Float)), + ), + data_variable { + assume_type: None, .. + } + | unknown_const { .. } => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack), + Unknown, + ))), + data_variable { + assume_type: Some(ty), + .. + } => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack), + ty.clone(), + ))), + math_integer { .. } + | math_angle { .. } + | math_whole_number { .. } + | math_positive_number { .. } => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack), + ConcreteInteger, + ))), + operator_round => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack.get(1)), + ConcreteInteger, + ))), + operator_length => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack.get(1)), + ConcreteInteger, + ))), + operator_mathop { OPERATOR } => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack.get(1)), + match OPERATOR.as_str().to_uppercase().as_str() { + "CEILING" | "FLOOR" => ConcreteInteger, + _ => Float, + }, + ))), + text { .. } => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack), + String, + ))), + boolean { .. } => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack), + Boolean, + ))), + operator_join | operator_letter_of => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack.get(2)), + String, + ))), + operator_contains | operator_and | operator_or | operator_gt | operator_lt + | operator_equals => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack.get(2)), + Boolean, + ))), + operator_not => Ok(TypeStack::new_some(TypeStack( + Rc::clone(&type_stack.get(1)), + Boolean, + ))), + motion_gotoxy | pen_setPenColorParamTo => Ok(Rc::clone(&type_stack.get(2))), + data_setvariableto { .. } + | motion_turnleft + | motion_turnright + | looks_switchcostumeto + | looks_changesizeby + | looks_setsizeto + | looks_say + | looks_think + | pen_setPenColorToColor + | pen_changePenSizeBy + | pen_setPenSizeTo + | pen_setPenShadeToNumber + | pen_changePenShadeBy + | pen_setPenHueToNumber + | pen_changePenHueBy + | hq_goto_if { .. } => Ok(Rc::clone(&type_stack.get(1))), + hq_goto { .. } + | sensing_resettimer + | pen_clear + | pen_penUp + | pen_penDown + | pen_stamp => Ok(Rc::clone(&type_stack)), + hq_drop(n) => Ok(type_stack.get(*n)), + hq_launch_procedure(Procedure { arg_types, .. }) => Ok(type_stack.get(arg_types.len())), + _ => hq_todo!("{:?}", &self), + }; + output + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct IrVar { + id: String, + name: String, + initial_value: VarVal, + is_cloud: bool, +} + +impl IrVar { + pub fn new(id: String, name: String, initial_value: VarVal, is_cloud: bool) -> Self { + Self { + id, + name, + initial_value, + is_cloud, + } + } + pub fn id(&self) -> &String { + &self.id + } + pub fn name(&self) -> &String { + &self.name + } + pub fn initial_value(&self) -> &VarVal { + &self.initial_value + } + pub fn is_cloud(&self) -> &bool { + &self.is_cloud + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub enum ThreadStart { + GreenFlag, +} + +#[derive(Debug, Clone)] +pub struct Step { + pub opcodes: Vec, + pub context: Rc, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Procedure { + pub arg_types: Vec, + pub first_step: String, + pub target_id: String, + pub warp: bool, +} + +impl fmt::Debug for TypeStack { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let this = Rc::new(RefCell::new(Some(self.clone()))); + f.debug_list() + .entries( + (0..this.len()) + .rev() + .map(|i| this.get(this.len() - 1 - i).borrow().clone().unwrap().1), + ) + .finish() + } +} + +impl Step { + pub fn new(opcodes: Vec, context: Rc) -> Step { + Step { opcodes, context } + } + pub fn opcodes(&self) -> &Vec { + &self.opcodes + } + pub fn opcodes_mut(&mut self) -> &mut Vec { + &mut self.opcodes + } + pub fn context(&self) -> Rc { + Rc::clone(&self.context) + } + pub fn const_fold(&mut self) -> Result<(), HQError> { + let mut value_stack: Vec = vec![]; + let mut is_folding = false; + let mut fold_start = 0; + let mut to_splice/*: Vec<(Range, Vec)>*/ = vec![]; + for (i, opcode) in self.opcodes.iter().enumerate() { + if !is_folding { + if !opcode.is_const() { + continue; + } + is_folding = true; + fold_start = i; + } + let const_value = opcode.const_value(&mut value_stack)?; + if const_value.is_none() { + is_folding = false; + let mut type_stack = + Rc::clone(&self.opcodes.get(fold_start).unwrap().type_stack.get(1)); + for ty in value_stack.iter().map(|val| val.as_input_type()) { + let prev_type_stack = Rc::clone(&type_stack); + type_stack = TypeStack::new_some(TypeStack(prev_type_stack, ty)); + } + //dbg!(&value_stack); + let folded_blocks: Result, HQError> = value_stack + .iter() + .enumerate() + .map(|(j, val)| val.try_as_block(type_stack.get(value_stack.len() - j))) + .collect(); + to_splice.push((fold_start..i, folded_blocks?)); + value_stack = vec![]; + } + } + if is_folding { + let mut type_stack = + Rc::clone(&self.opcodes.get(fold_start).unwrap().type_stack.get(1)); + for ty in value_stack.iter().map(|val| val.as_input_type()) { + let prev_type_stack = Rc::clone(&type_stack); + type_stack = TypeStack::new_some(TypeStack(prev_type_stack, ty)); + } + let folded_blocks: Result, HQError> = value_stack + .iter() + .enumerate() + .map(|(j, val)| val.try_as_block(type_stack.get(value_stack.len() - j))) + .collect(); + to_splice.push((fold_start..self.opcodes().len(), folded_blocks?)); + } + for (range, replace) in to_splice.into_iter().rev() { + self.opcodes.splice(range, replace); + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum IrVal { + Int(i32), + Float(f64), + Boolean(bool), + String(String), + Unknown(Box), +} + +impl IrVal { + pub fn to_f64(self) -> f64 { + match self { + IrVal::Unknown(u) => u.to_f64(), + IrVal::Float(f) => f, + IrVal::Int(i) => i as f64, + IrVal::Boolean(b) => b as i32 as f64, + IrVal::String(s) => s.parse().unwrap_or(0.0), + } + } + pub fn to_i32(self) -> i32 { + match self { + IrVal::Unknown(u) => u.to_i32(), + IrVal::Float(f) => f as i32, + IrVal::Int(i) => i, + IrVal::Boolean(b) => b as i32, + IrVal::String(s) => s.parse().unwrap_or(0), + } + } + #[allow(clippy::inherent_to_string)] + pub fn to_string(self) -> String { + match self { + IrVal::Unknown(u) => u.to_string(), + IrVal::Float(f) => format!("{f}"), + IrVal::Int(i) => format!("{i}"), + IrVal::Boolean(b) => format!("{b}"), + IrVal::String(s) => s, + } + } + pub fn to_bool(self) -> bool { + match self { + IrVal::Unknown(u) => u.to_bool(), + IrVal::Boolean(b) => b, + _ => unreachable!(), + } + } + pub fn as_input_type(&self) -> InputType { + match self { + IrVal::Unknown(_) => InputType::Unknown, + IrVal::Float(_) => InputType::Float, + IrVal::Int(_) => InputType::ConcreteInteger, + IrVal::String(_) => InputType::String, + IrVal::Boolean(_) => InputType::Boolean, + } + } + pub fn try_as_block( + &self, + type_stack: Rc>>, + ) -> Result { + IrBlock::new_with_stack_no_cast( + match self { + IrVal::Int(num) => IrOpcode::math_integer { NUM: *num }, + IrVal::Float(num) => IrOpcode::math_number { NUM: *num }, + IrVal::String(text) => IrOpcode::text { TEXT: text.clone() }, + IrVal::Boolean(b) => IrOpcode::boolean { BOOL: *b }, + IrVal::Unknown(u) => IrOpcode::unknown_const { val: *u.clone() }, + }, + type_stack, + ) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ThreadContext { + pub target_index: u32, + pub dbg: bool, + pub vars: Rc>>, // todo: fix variable id collisions between targets + pub target_num: usize, + pub costumes: Vec, + pub proc: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Thread { + start: ThreadStart, + first_step: String, + target_id: String, +} + +static ARG_REGEX: Lazy = lazy_regex!(r#"[^\\]%[nbs]"#); + +fn arg_types_from_proccode(proccode: String) -> Result, HQError> { + // https://github.com/scratchfoundation/scratch-blocks/blob/abbfe93136fef57fdfb9a077198b0bc64726f012/blocks_vertical/procedures.js#L207-L215 + (*ARG_REGEX) + .find_iter(proccode.as_str()) + .map(|s| s.as_str().to_string().trim().to_string()) + .filter(|s| s.as_str().starts_with('%')) + .map(|s| s[..2].to_string()) + .map(|s| { + Ok(match s.as_str() { + "%n" => InputType::Number, + "%s" => InputType::String, + "%b" => InputType::Boolean, + other => hq_bug!("invalid proccode arg \"{other}\" found"), + }) + }) + .collect::, _>>() +} + +fn add_procedure( + target_id: String, + proccode: String, + expect_warp: bool, + blocks: &BTreeMap, + steps: &mut StepMap, + procedures: &mut ProcMap, + context: Rc, +) -> Result { + if let Some(procedure) = procedures.get(&(target_id.clone(), proccode.clone())) { + return Ok(procedure.clone()); + } + let arg_types = arg_types_from_proccode(proccode.clone())?; + let Some(prototype_block) = blocks.values().find(|block| { + let Some(info) = block.block_info() else { + return false; + }; + return info.opcode == BlockOpcode::procedures_prototype + && (match info.mutation.mutations.get("proccode") { + None => false, + Some(p) => { + if let serde_json::Value::String(ref s) = p { + log(s.as_str()); + *s == proccode + } else { + false + } + } + }); + }) else { + hq_bad_proj!("no prototype found for {proccode} in {target_id}") + }; + let Some(def_block) = blocks.get( + &prototype_block + .block_info() + .unwrap() + .parent + .clone() + .ok_or(make_hq_bad_proj!("prototype block without parent"))?, + ) else { + hq_bad_proj!("no definition found for {proccode} in {target_id}") + }; + if let Some(warp_val) = prototype_block + .block_info() + .unwrap() + .mutation + .mutations + .get("warp") + { + let warp = match warp_val { + serde_json::Value::Bool(w) => *w, + serde_json::Value::String(wstr) => match wstr.as_str() { + "true" => true, + "false" => false, + _ => hq_bad_proj!("unexpected string for warp mutation"), + }, + _ => hq_bad_proj!("bad type for warp mutation"), + }; + if warp != expect_warp { + hq_bad_proj!("proc call warp does not equal definition warp") + } + } else { + hq_bad_proj!("missing warp mutation on orocedures_dedinition") + } + let Some(ref next) = def_block.block_info().unwrap().next else { + hq_todo!("empty procedure definition") + }; + let procedure = Procedure { + arg_types, + first_step: next.clone(), + target_id: target_id.clone(), + warp: expect_warp, + }; + step_from_top_block( + next.clone(), + vec![], + blocks, + Rc::new(ThreadContext { + proc: Some(procedure.clone()), + dbg: false, + costumes: context.costumes.clone(), + target_index: context.target_index, + vars: Rc::clone(&context.vars), + target_num: context.target_num, + }), + steps, + target_id.clone(), + procedures, + )?; + procedures.insert((target_id.clone(), proccode.clone()), procedure.clone()); + Ok(procedure) +} + +trait IrBlockVec { + #[allow(clippy::too_many_arguments)] + fn add_block( + &mut self, + block_id: String, + blocks: &BTreeMap, + context: Rc, + last_nexts: Vec, + steps: &mut StepMap, + target_id: String, + procedures: &mut ProcMap, + ) -> Result<(), HQError>; + fn add_block_arr(&mut self, block_arr: &BlockArray) -> Result<(), HQError>; + fn add_inputs( + &mut self, + inputs: &BTreeMap, + blocks: &BTreeMap, + context: Rc, + steps: &mut StepMap, + target_id: String, + procedures: &mut ProcMap, + ) -> Result<(), HQError>; + fn get_type_stack(&self, i: Option) -> Rc>>; +} + +impl IrBlockVec for Vec { + fn add_inputs( + &mut self, + inputs: &BTreeMap, + blocks: &BTreeMap, + context: Rc, + steps: &mut StepMap, + target_id: String, + procedures: &mut ProcMap, + ) -> Result<(), HQError> { + for (name, input) in inputs { + if name.starts_with("SUBSTACK") { + continue; + } + match input { + Input::Shadow(_, maybe_block, _) | Input::NoShadow(_, maybe_block) => { + let Some(block) = maybe_block else { + hq_bad_proj!("block doesn't exist"); // is this a problem with the project, or is it a bug? + }; + match block { + BlockArrayOrId::Id(id) => { + self.add_block( + id.clone(), + blocks, + Rc::clone(&context), + vec![], + steps, + target_id.clone(), + procedures, + )?; + } + BlockArrayOrId::Array(arr) => { + self.add_block_arr(arr)?; + } + } + } + } + } + Ok(()) + } + fn add_block_arr(&mut self, block_arr: &BlockArray) -> Result<(), HQError> { + let prev_block = self.last(); + let type_stack = if let Some(block) = prev_block { + Rc::clone(&block.type_stack) + } else { + Rc::new(RefCell::new(None)) + }; + self.push(IrBlock::new_with_stack_no_cast( + match block_arr { + BlockArray::NumberOrAngle(ty, value) => match ty { + 4 => IrOpcode::math_number { NUM: *value }, + 5 => IrOpcode::math_positive_number { NUM: *value as i32 }, + 6 => IrOpcode::math_whole_number { NUM: *value as i32 }, + 7 => IrOpcode::math_integer { NUM: *value as i32 }, + 8 => IrOpcode::math_angle { NUM: *value as i32 }, + _ => hq_bad_proj!("bad project json (block array of type ({}, u32))", ty), + }, + BlockArray::ColorOrString(ty, value) => match ty { + 4 => IrOpcode::math_number { + NUM: value.parse().map_err(|_| make_hq_bug!(""))?, + }, + 5 => IrOpcode::math_positive_number { + NUM: value.parse().map_err(|_| make_hq_bug!(""))?, + }, + 6 => IrOpcode::math_whole_number { + NUM: value.parse().map_err(|_| make_hq_bug!(""))?, + }, + 7 => IrOpcode::math_integer { + NUM: value.parse().map_err(|_| make_hq_bug!(""))?, + }, + 8 => IrOpcode::math_angle { + NUM: value.parse().map_err(|_| make_hq_bug!(""))?, + }, + 9 => hq_todo!(""), + 10 => IrOpcode::text { + TEXT: value.to_string(), + }, + _ => hq_bad_proj!("bad project json (block array of type ({}, string))", ty), + }, + BlockArray::Broadcast(ty, _name, id) => match ty { + 12 => IrOpcode::data_variable { + VARIABLE: id.to_string(), + assume_type: None, + }, + _ => hq_todo!(""), + }, + BlockArray::VariableOrList(ty, _name, id, _pos_x, _pos_y) => match ty { + 12 => IrOpcode::data_variable { + VARIABLE: id.to_string(), + assume_type: None, + }, + _ => hq_todo!(""), + }, + }, + type_stack, + )?); + Ok(()) + } + fn get_type_stack(&self, i: Option) -> Rc>> { + let prev_block = if let Some(j) = i { + self.get(j) + } else { + self.last() + }; + if let Some(block) = prev_block { + Rc::clone(&block.type_stack) + } else { + Rc::new(RefCell::new(None)) + } + } + fn add_block( + &mut self, + block_id: String, + blocks: &BTreeMap, + context: Rc, + last_nexts: Vec, + steps: &mut StepMap, + target_id: String, + procedures: &mut ProcMap, + ) -> Result<(), HQError> { + let block = blocks.get(&block_id).ok_or(make_hq_bug!(""))?; + match block { + Block::Normal { block_info, .. } => { + self.add_inputs( + &block_info.inputs, + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + let ops: Vec<_> = match block_info.opcode { + BlockOpcode::motion_gotoxy => vec![IrOpcode::motion_gotoxy], + BlockOpcode::sensing_timer => vec![IrOpcode::sensing_timer], + BlockOpcode::sensing_dayssince2000 => vec![IrOpcode::sensing_dayssince2000], + BlockOpcode::sensing_resettimer => vec![IrOpcode::sensing_resettimer], + BlockOpcode::looks_say => vec![IrOpcode::looks_say], + BlockOpcode::looks_think => vec![IrOpcode::looks_think], + BlockOpcode::looks_show => vec![IrOpcode::looks_show], + BlockOpcode::looks_hide => vec![IrOpcode::looks_hide], + BlockOpcode::looks_hideallsprites => vec![IrOpcode::looks_hideallsprites], + BlockOpcode::looks_switchcostumeto => vec![IrOpcode::looks_switchcostumeto], + BlockOpcode::looks_costume => { + let val = match block_info.fields.get("COSTUME").ok_or( + make_hq_bad_proj!("invalid project.json - missing field COSTUME"), + )? { + Field::Value((val,)) => val, + Field::ValueId(val, _) => val, + }; + let VarVal::String(name) = val.clone().ok_or(make_hq_bad_proj!( + "invalid project.json - null costume name for COSTUME field" + ))? + else { + hq_bad_proj!( + "invalid project.json - COSTUME field is not of type String" + ); + }; + log(name.as_str()); + let index = context + .costumes + .iter() + .position(|costume| costume.name == name) + .ok_or(make_hq_bad_proj!("missing costume with name {}", name))?; + log(format!("{}", index).as_str()); + vec![IrOpcode::math_whole_number { NUM: index as i32 }] + } + BlockOpcode::looks_switchbackdropto => { + vec![IrOpcode::looks_switchbackdropto] + } + BlockOpcode::looks_switchbackdroptoandwait => { + vec![IrOpcode::looks_switchbackdroptoandwait] + } + BlockOpcode::looks_nextcostume => vec![IrOpcode::looks_nextcostume], + BlockOpcode::looks_nextbackdrop => vec![IrOpcode::looks_nextbackdrop], + BlockOpcode::looks_changeeffectby => vec![IrOpcode::looks_changeeffectby], + BlockOpcode::looks_seteffectto => vec![IrOpcode::looks_seteffectto], + BlockOpcode::looks_cleargraphiceffects => { + vec![IrOpcode::looks_cleargraphiceffects] + } + BlockOpcode::looks_changesizeby => vec![IrOpcode::looks_changesizeby], + BlockOpcode::looks_setsizeto => vec![IrOpcode::looks_setsizeto], + BlockOpcode::motion_turnleft => vec![IrOpcode::motion_turnleft], + BlockOpcode::looks_size => vec![IrOpcode::looks_size], + BlockOpcode::operator_add => vec![IrOpcode::operator_add], + BlockOpcode::operator_subtract => vec![IrOpcode::operator_subtract], + BlockOpcode::operator_multiply => vec![IrOpcode::operator_multiply], + BlockOpcode::operator_divide => vec![IrOpcode::operator_divide], + BlockOpcode::operator_mod => vec![IrOpcode::operator_mod], + BlockOpcode::operator_round => vec![IrOpcode::operator_round], + BlockOpcode::operator_lt => vec![IrOpcode::operator_lt], + BlockOpcode::operator_equals => vec![IrOpcode::operator_equals], + BlockOpcode::operator_gt => vec![IrOpcode::operator_gt], + BlockOpcode::operator_and => vec![IrOpcode::operator_and], + BlockOpcode::operator_or => vec![IrOpcode::operator_or], + BlockOpcode::operator_not => vec![IrOpcode::operator_not], + BlockOpcode::operator_random => vec![IrOpcode::operator_random], + BlockOpcode::operator_join => vec![IrOpcode::operator_join], + BlockOpcode::operator_letter_of => vec![IrOpcode::operator_letter_of], + BlockOpcode::operator_length => vec![IrOpcode::operator_length], + BlockOpcode::operator_contains => vec![IrOpcode::operator_contains], + BlockOpcode::pen_clear => vec![IrOpcode::pen_clear], + BlockOpcode::pen_stamp => vec![IrOpcode::pen_stamp], + BlockOpcode::pen_penDown => vec![IrOpcode::pen_penDown], + BlockOpcode::pen_penUp => vec![IrOpcode::pen_penUp], + BlockOpcode::pen_setPenColorToColor => { + vec![IrOpcode::pen_setPenColorToColor] + } + BlockOpcode::pen_changePenColorParamBy => { + vec![IrOpcode::pen_changePenColorParamBy] + } + BlockOpcode::pen_setPenColorParamTo => { + vec![IrOpcode::pen_setPenColorParamTo] + } + BlockOpcode::pen_changePenSizeBy => vec![IrOpcode::pen_changePenSizeBy], + BlockOpcode::pen_setPenSizeTo => vec![IrOpcode::pen_setPenSizeTo], + BlockOpcode::pen_setPenShadeToNumber => { + vec![IrOpcode::pen_setPenShadeToNumber] + } + BlockOpcode::pen_changePenShadeBy => vec![IrOpcode::pen_changePenShadeBy], + BlockOpcode::pen_setPenHueToNumber => vec![IrOpcode::pen_setPenHueToNumber], + BlockOpcode::pen_changePenHueBy => vec![IrOpcode::pen_changePenHueBy], + BlockOpcode::pen_menu_colorParam => { + let maybe_val = + match block_info + .fields + .get("colorParam") + .ok_or(make_hq_bad_proj!( + "invalid project.json - missing field colorParam" + ))? { + Field::Value((v,)) | Field::ValueId(v, _) => v, + }; + let val_varval = maybe_val.clone().ok_or(make_hq_bad_proj!( + "invalid project.json - null value for OPERATOR field" + ))?; + let VarVal::String(val) = val_varval else { + hq_bad_proj!( + "invalid project.json - expected colorParam field to be string" + ); + }; + vec![IrOpcode::text { TEXT: val }] + } + BlockOpcode::operator_mathop => { + let maybe_val = match block_info.fields.get("OPERATOR").ok_or( + make_hq_bad_proj!("invalid project.json - missing field OPERATOR"), + )? { + Field::Value((v,)) | Field::ValueId(v, _) => v, + }; + let val_varval = maybe_val.clone().ok_or(make_hq_bad_proj!( + "invalid project.json - null value for OPERATOR field" + ))?; + let VarVal::String(val) = val_varval else { + hq_bad_proj!( + "invalid project.json - expected OPERATOR field to be string" + ); + }; + vec![IrOpcode::operator_mathop { OPERATOR: val }] + } + BlockOpcode::data_variable => { + let Field::ValueId(_val, maybe_id) = + block_info.fields.get("VARIABLE").ok_or(make_hq_bad_proj!( + "invalid project.json - missing field VARIABLE" + ))? + else { + hq_bad_proj!( + "invalid project.json - missing variable id for VARIABLE field" + ); + }; + let id = maybe_id.clone().ok_or(make_hq_bad_proj!( + "invalid project.json - null variable id for VARIABLE field" + ))?; + vec![IrOpcode::data_variable { + VARIABLE: id, + assume_type: None, + }] + } + BlockOpcode::data_setvariableto => { + let Field::ValueId(_val, maybe_id) = + block_info.fields.get("VARIABLE").ok_or(make_hq_bad_proj!( + "invalid project.json - missing field VARIABLE" + ))? + else { + hq_bad_proj!( + "invalid project.json - missing variable id for VARIABLE field" + ); + }; + let id = maybe_id.clone().ok_or(make_hq_bad_proj!( + "invalid project.json - null variable id for VARIABLE field" + ))?; + vec![IrOpcode::data_setvariableto { + VARIABLE: id, + assume_type: None, + }] + } + BlockOpcode::data_changevariableby => { + let Field::ValueId(_val, maybe_id) = + block_info.fields.get("VARIABLE").ok_or(make_hq_bad_proj!( + "invalid project.json - missing field VARIABLE" + ))? + else { + hq_bad_proj!( + "invalid project.json - missing variable id for VARIABLE field" + ); + }; + let id = maybe_id.clone().ok_or(make_hq_bad_proj!( + "invalid project.json - null id for VARIABLE field" + ))?; + vec![ + IrOpcode::data_variable { + VARIABLE: id.to_string(), + assume_type: None, + }, + IrOpcode::operator_add, + IrOpcode::data_setvariableto { + VARIABLE: id, + assume_type: None, + }, + ] + } + BlockOpcode::control_if => { + let substack_id = if let BlockArrayOrId::Id(id) = block_info + .inputs + .get("SUBSTACK") + .ok_or(make_hq_bad_proj!("missing SUBSTACK input for control_if"))? + .get_1() + .ok_or(make_hq_bug!(""))? + .clone() + .ok_or(make_hq_bug!(""))? + { + id + } else { + hq_bad_proj!("malformed SUBSTACK input") + }; + let mut new_nexts = last_nexts.clone(); + if let Some(ref next) = block_info.next { + new_nexts.push(next.clone()); + } + step_from_top_block( + substack_id.clone(), + new_nexts, + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + step_from_top_block( + block_info.next.clone().ok_or(make_hq_bug!(""))?, + last_nexts, + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + vec![ + IrOpcode::hq_goto_if { + step: Some((target_id.clone(), substack_id)), + does_yield: false, + }, + IrOpcode::hq_goto { + step: if block_info.next.is_some() { + Some(( + target_id, + block_info.next.clone().ok_or(make_hq_bug!(""))?, + )) + } else { + None + }, + does_yield: false, + }, + ] + } + BlockOpcode::control_if_else => { + let substack_id = if let BlockArrayOrId::Id(id) = block_info + .inputs + .get("SUBSTACK") + .ok_or(make_hq_bad_proj!("missing SUBSTACK input for control_if"))? + .get_1() + .ok_or(make_hq_bug!(""))? + .clone() + .ok_or(make_hq_bug!(""))? + { + id + } else { + hq_bad_proj!("malformed SUBSTACK input") + }; + let substack2_id = if let BlockArrayOrId::Id(id) = block_info + .inputs + .get("SUBSTACK2") + .ok_or(make_hq_bad_proj!("missing SUBSTACK input for control_if"))? + .get_1() + .ok_or(make_hq_bug!(""))? + .clone() + .ok_or(make_hq_bug!(""))? + { + id + } else { + hq_bad_proj!("malformed SUBSTACK2 input") + }; + let mut new_nexts = last_nexts; + if let Some(ref next) = block_info.next { + new_nexts.push(next.clone()); + } + step_from_top_block( + substack_id.clone(), + new_nexts.clone(), + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + step_from_top_block( + substack2_id.clone(), + new_nexts.clone(), + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + vec![ + IrOpcode::hq_goto_if { + step: Some((target_id.clone(), substack_id)), + does_yield: false, + }, + IrOpcode::hq_goto { + step: Some((target_id, substack2_id)), + does_yield: false, + }, + ] + } + BlockOpcode::control_repeat => { + let substack_id = if let BlockArrayOrId::Id(id) = block_info + .inputs + .get("SUBSTACK") + .ok_or(make_hq_bad_proj!("missing SUBSTACK input"))? + .get_1() + .ok_or(make_hq_bug!(""))? + .clone() + .ok_or(make_hq_bug!(""))? + { + id + } else { + hq_bad_proj!("malformed SUBSTACK input") + }; + let next_step = match block_info.next.as_ref() { + Some(next) => Some((target_id.clone(), next.clone())), + None => last_nexts + .first() + .map(|next| (target_id.clone(), next.clone())), + }; + let condition_opcodes = vec![ + IrOpcode::hq_goto_if { + step: next_step.clone(), + does_yield: !context.proc.clone().is_some_and(|p| p.warp), + }, + IrOpcode::hq_goto { + step: Some((target_id.clone(), substack_id.clone())), + does_yield: !context.proc.clone().is_some_and(|p| p.warp), + }, + ]; + let looper_id = Uuid::new_v4().to_string(); + context.vars.borrow_mut().push(IrVar::new( + looper_id.clone(), + looper_id.clone(), + VarVal::Float(0.0), + false, + )); + if !steps.contains_key(&(target_id.clone(), looper_id.clone())) { + let type_stack = Rc::new(RefCell::new(None)); + let mut looper_opcodes = vec![IrBlock::new_with_stack_no_cast( + IrOpcode::data_variable { + VARIABLE: looper_id.clone(), + assume_type: Some(InputType::ConcreteInteger), + }, + type_stack, + )?]; + for op in [ + //IrOpcode::hq_cast(InputType::Unknown, InputType::Float), // todo: integer + IrOpcode::math_whole_number { NUM: 1 }, + IrOpcode::operator_subtract, + //IrOpcode::hq_cast(Float, Unknown), + IrOpcode::data_teevariable { + VARIABLE: looper_id.clone(), + assume_type: Some(InputType::ConcreteInteger), + }, + //IrOpcode::hq_cast(Unknown, Float), + IrOpcode::math_whole_number { NUM: 1 }, + IrOpcode::operator_lt, + ] + .into_iter() + { + looper_opcodes.push(IrBlock::new_with_stack_no_cast( + op, + Rc::clone( + &looper_opcodes.last().ok_or(make_hq_bug!(""))?.type_stack, + ), + )?); + } + for op in condition_opcodes.iter() { + looper_opcodes.push(IrBlock::new_with_stack_no_cast( + op.clone(), + Rc::clone( + &looper_opcodes.last().ok_or(make_hq_bug!(""))?.type_stack, + ), + )?); + } + //looper_opcodes.fixup_types()?; + steps.insert( + (target_id.clone(), looper_id.clone()), + Step::new(looper_opcodes, Rc::clone(&context)), + ); + } + if block_info.next.is_some() { + step_from_top_block( + block_info.next.clone().ok_or(make_hq_bug!(""))?, + last_nexts, + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + } + step_from_top_block( + substack_id.clone(), + vec![looper_id.clone()], + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + let mut opcodes = vec![]; + opcodes.add_inputs( + &block_info.inputs, + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + for op in [ + IrOpcode::math_whole_number { NUM: 1 }, + IrOpcode::operator_lt, + ] + .into_iter() + .chain(condition_opcodes.into_iter()) + { + opcodes.push(IrBlock::new_with_stack_no_cast( + op, + Rc::clone(&opcodes.last().ok_or(make_hq_bug!(""))?.type_stack), + )?); + } + steps.insert( + (target_id.clone(), block_id.clone()), + Step::new(opcodes.clone(), Rc::clone(&context)), + ); + vec![ + IrOpcode::operator_round, // this is correct, scratch rounds rather than floor + //IrOpcode::hq_cast(ConcreteInteger, Unknown), + IrOpcode::data_teevariable { + VARIABLE: looper_id, + assume_type: Some(InputType::ConcreteInteger), + }, + //IrOpcode::hq_cast(Unknown, Float), + IrOpcode::math_whole_number { NUM: 1 }, + IrOpcode::operator_lt, + IrOpcode::hq_goto_if { + step: next_step, + does_yield: !context.proc.clone().is_some_and(|p| p.warp), + }, + IrOpcode::hq_goto { + step: Some((target_id, substack_id)), + does_yield: false, + }, + ] + } + BlockOpcode::control_repeat_until => { + let substack_id = if let BlockArrayOrId::Id(id) = block_info + .inputs + .get("SUBSTACK") + .ok_or(make_hq_bad_proj!("missing SUBSTACK input"))? + .get_1() + .ok_or(make_hq_bug!(""))? + .clone() + .ok_or(make_hq_bug!(""))? + { + id + } else { + hq_bad_proj!("malformed SUBSTACK input") + }; + let next_step = match block_info.next.as_ref() { + Some(next) => Some((target_id.clone(), next.clone())), + None => last_nexts + .first() + .map(|next| (target_id.clone(), next.clone())), + }; + let condition_opcodes = vec![ + IrOpcode::hq_goto_if { + step: next_step.clone(), + does_yield: !context.proc.clone().is_some_and(|p| p.warp), + }, + IrOpcode::hq_goto { + step: Some((target_id.clone(), substack_id.clone())), + does_yield: !context.proc.clone().is_some_and(|p| p.warp), + }, + ]; + let looper_id = Uuid::new_v4().to_string(); + if !steps.contains_key(&(target_id.clone(), looper_id.clone())) { + let mut looper_opcodes = vec![]; + looper_opcodes.add_inputs( + &block_info.inputs, + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + for op in condition_opcodes.clone().into_iter() { + looper_opcodes.push(IrBlock::new_with_stack_no_cast( + op, + Rc::clone( + &looper_opcodes.last().ok_or(make_hq_bug!(""))?.type_stack, + ), + )?); + } + steps.insert( + (target_id.clone(), looper_id.clone()), + Step::new(looper_opcodes, Rc::clone(&context)), + ); + } + if block_info.next.is_some() { + step_from_top_block( + block_info.next.clone().ok_or(make_hq_bug!(""))?, + last_nexts, + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + } + step_from_top_block( + substack_id.clone(), + vec![looper_id], + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + let mut opcodes = vec![]; + opcodes.add_inputs( + &block_info.inputs, + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + for op in condition_opcodes.into_iter() { + opcodes.push(IrBlock::new_with_stack_no_cast( + op, + Rc::clone(&opcodes.last().ok_or(make_hq_bug!(""))?.type_stack), + )?); + } + steps.insert( + (target_id.clone(), block_id.clone()), + Step::new(opcodes.clone(), Rc::clone(&context)), + ); + vec![ + IrOpcode::hq_goto_if { + step: next_step, + does_yield: !context.proc.clone().is_some_and(|p| p.warp), + }, + IrOpcode::hq_goto { + step: Some((target_id, substack_id)), + does_yield: false, + }, + ] + } + BlockOpcode::control_forever => { + let substack_id = if let BlockArrayOrId::Id(id) = block_info + .inputs + .get("SUBSTACK") + .ok_or(make_hq_bad_proj!("missing SUBSTACK input for control_if"))? + .get_1() + .ok_or(make_hq_bug!(""))? + .clone() + .ok_or(make_hq_bug!(""))? + { + id + } else { + hq_bad_proj!("malformed SUBSTACK input") + }; + let goto_opcode = IrOpcode::hq_goto { + step: Some((target_id.clone(), substack_id.clone())), + does_yield: !context.proc.clone().is_some_and(|p| p.warp), + }; + let looper_id = Uuid::new_v4().to_string(); + if !steps.contains_key(&(target_id.clone(), looper_id.clone())) { + let looper_opcodes = vec![IrBlock::new_with_stack_no_cast( + goto_opcode.clone(), + Rc::new(RefCell::new(None)), + )?]; + steps.insert( + (target_id.clone(), looper_id.clone()), + Step::new(looper_opcodes, Rc::clone(&context)), + ); + } + if let Some(next) = block_info.next.clone() { + step_from_top_block( + next, + last_nexts, + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + } + step_from_top_block( + substack_id.clone(), + vec![looper_id], + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + let opcodes = vec![IrBlock::new_with_stack_no_cast( + goto_opcode, + Rc::new(RefCell::new(None)), + )?]; + steps.insert( + (target_id.clone(), block_id.clone()), + Step::new(opcodes.clone(), Rc::clone(&context)), + ); + vec![IrOpcode::hq_goto { + step: Some((target_id, substack_id)), + does_yield: false, + }] + } + BlockOpcode::procedures_call => { + let serde_json::Value::String(proccode) = + block_info.mutation.mutations.get("proccode").ok_or( + make_hq_bad_proj!("missing proccode mutation in procedures_call"), + )? + else { + hq_bad_proj!("non-string proccode mutation") + }; + let warp = match block_info.mutation.mutations.get("warp").ok_or( + make_hq_bad_proj!("missing warp mutation in procedures_call"), + )? { + serde_json::Value::Bool(w) => *w, + serde_json::Value::String(wstr) => match wstr.as_str() { + "true" => true, + "false" => false, + _ => hq_bad_proj!("unexpected string for warp mutation"), + }, + _ => hq_bad_proj!("bad type for warp mutation"), + }; + if !warp { + hq_todo!("non-warp procedure"); + } + let procedure = add_procedure( + target_id.clone(), + proccode.clone(), + warp, + blocks, + steps, + procedures, + Rc::clone(&context), + )?; + vec![IrOpcode::hq_launch_procedure(procedure.clone())] + } + ref other => hq_todo!("unknown block {:?}", other), + }; + + for op in ops.into_iter() { + let type_stack = self.get_type_stack(None); + let mut casts: BTreeMap = BTreeMap::new(); + let mut add_cast = |i, ty: &InputType| { + casts.insert(i, ty.clone()); + }; + let block = + IrBlock::new_with_stack(op.clone(), Rc::clone(&type_stack), &mut add_cast)?; + for (j, cast_type) in casts.iter() { + let cast_pos = self + .iter() + .rposition(|b| b.type_stack.len() == type_stack.len() - j) + .ok_or(make_hq_bug!(""))?; + let cast_stack = self.get_type_stack(Some(cast_pos)); + #[allow(clippy::let_and_return)] + let cast_block = IrBlock::new_with_stack_no_cast( + IrOpcode::hq_cast( + { + let from = cast_stack + .borrow() + .clone() + .ok_or(make_hq_bug!( + "tried to cast to {:?} from empty type stack at {:?}", + cast_type.clone(), + op + ))? + .1; + from + }, + cast_type.clone(), + ), + cast_stack, + )?; + self.insert(cast_pos + 1, cast_block); + } + self.push(block); + /*if let Some(ty) = casts.get(&(type_stack.len() - 1 - i) { + let this_prev_block = self.last(); + let this_type_stack = if let Some(block) = this_prev_block { + Rc::clone(&block.type_stack) + } else { + hq_bug!("tried to cast from an empty type stack") + //Rc::new(RefCell::new(None)) + }; + self.push(IrBlock::new_with_stack_no_cast( + IrOpcode::hq_cast({ let from = this_type_stack.borrow().clone().ok_or(make_hq_bug!("tried to cast to {:?} from empty type stack at {:?}", ty, op))?.1; from }, ty.clone()), + this_type_stack, + )?); + }*/ + } + } + Block::Special(a) => self.add_block_arr(a)?, + }; + Ok(()) + } +} + +pub fn step_from_top_block<'a>( + top_id: String, + mut last_nexts: Vec, + blocks: &BTreeMap, + context: Rc, + steps: &'a mut StepMap, + target_id: String, + procedures: &mut ProcMap, +) -> Result<&'a Step, HQError> { + if steps.contains_key(&(target_id.clone(), top_id.clone())) { + return steps.get(&(target_id, top_id)).ok_or(make_hq_bug!("")); + } + let mut ops: Vec = vec![]; + let mut next_block = blocks.get(&top_id).ok_or(make_hq_bug!(""))?; + let mut next_id = Some(top_id.clone()); + loop { + ops.add_block( + next_id.clone().ok_or(make_hq_bug!(""))?, + blocks, + Rc::clone(&context), + last_nexts.clone(), + steps, + target_id.clone(), + procedures, + )?; + if next_block + .block_info() + .ok_or(make_hq_bug!(""))? + .next + .is_none() + { + next_id = last_nexts.pop(); + } else { + next_id.clone_from(&next_block.block_info().ok_or(make_hq_bug!(""))?.next); + } + if ops.is_empty() { + hq_bug!("assertion failed: !ops.is_empty()") + }; + if matches!( + ops.last().ok_or(make_hq_bug!(""))?.opcode(), + IrOpcode::hq_goto { .. } + ) { + next_id = None; + } + if next_id.is_none() { + break; + } else if let Some(block) = blocks.get(&next_id.clone().ok_or(make_hq_bug!(""))?) { + next_block = block; + } else if steps.contains_key(&(target_id.clone(), next_id.clone().ok_or(make_hq_bug!(""))?)) + { + ops.push(IrBlock::new_with_stack_no_cast( + IrOpcode::hq_goto { + step: Some((target_id.clone(), next_id.clone().ok_or(make_hq_bug!(""))?)), + does_yield: false, + }, + Rc::clone(&ops.last().unwrap().type_stack), + )?); + next_id = None; + break; + } else { + hq_bad_proj!("invalid next_id"); + } + let Some(last_block) = ops.last() else { + unreachable!() + }; + if last_block.does_request_redraw() + && !context.proc.clone().is_some_and(|p| p.warp) + && !(*last_block.opcode() == IrOpcode::looks_say && context.dbg) + { + break; + } + } + //ops.fixup_types()?; + let mut step = Step::new(ops.clone(), Rc::clone(&context)); + let goto = if let Some(ref id) = next_id { + step_from_top_block( + id.clone(), + last_nexts, + blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?; + IrBlock::new_with_stack_no_cast( + IrOpcode::hq_goto { + step: Some((target_id.clone(), id.clone())), + does_yield: !context.proc.clone().is_some_and(|p| p.warp), + }, + Rc::clone(&step.opcodes().last().unwrap().type_stack), + )? + } else { + IrBlock::new_with_stack_no_cast( + IrOpcode::hq_goto { + step: None, + does_yield: false, + }, + Rc::clone(&step.opcodes().last().unwrap().type_stack), + )? + }; + step.opcodes_mut().push(goto); + steps.insert((target_id.clone(), top_id.clone()), step); + steps.get(&(target_id, top_id)).ok_or(make_hq_bug!("")) +} + +impl Thread { + pub fn new(start: ThreadStart, first_step: String, target_id: String) -> Thread { + Thread { + start, + first_step, + target_id, + } + } + pub fn start(&self) -> &ThreadStart { + &self.start + } + pub fn first_step(&self) -> &String { + &self.first_step + } + pub fn target_id(&self) -> &String { + &self.target_id + } + pub fn from_hat( + hat: Block, + blocks: BTreeMap, + context: Rc, + steps: &mut StepMap, + target_id: String, + procedures: &mut ProcMap, + ) -> Result { + let (first_step_id, _first_step) = if let Block::Normal { block_info, .. } = &hat { + if let Some(next_id) = &block_info.next { + ( + next_id.clone(), + step_from_top_block( + next_id.clone(), + vec![], + &blocks, + Rc::clone(&context), + steps, + target_id.clone(), + procedures, + )?, + ) + } else { + unreachable!(); + } + } else { + unreachable!(); + }; + let start_type = if let Block::Normal { block_info, .. } = &hat { + match block_info.opcode { + BlockOpcode::event_whenflagclicked => ThreadStart::GreenFlag, + _ => hq_todo!(""), + } + } else { + unreachable!() + }; + Ok(Self::new(start_type, first_step_id, target_id)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_ir() -> Result<(), HQError> { + use crate::sb3::Sb3Project; + use std::fs; + let proj: Sb3Project = fs::read_to_string("./project.json") + .expect("couldn't read hq-test.project.json") + .try_into()?; + let ir: IrProject = proj.try_into()?; + println!("{}", ir); + Ok(()) + } + + #[test] + fn const_fold() -> Result<(), HQError> { + use crate::sb3::Sb3Project; + use std::fs; + let proj: Sb3Project = fs::read_to_string("./project.json") + .expect("couldn't read hq-test.project.json") + .try_into()?; + let mut ir: IrProject = proj.try_into()?; + ir.const_fold()?; + println!("{}", ir); + Ok(()) + } + #[test] + fn opt() -> Result<(), HQError> { + use crate::sb3::Sb3Project; + use std::fs; + let proj: Sb3Project = fs::read_to_string("./project.json") + .expect("couldn't read hq-test.project.json") + .try_into()?; + let mut ir: IrProject = proj.try_into()?; + ir.optimise()?; + println!("{}", ir); + Ok(()) + } + + #[test] + fn input_types() { + use InputType::*; + assert!(Number.includes(&Float)); + assert!(Number.includes(&Integer)); + assert!(Number.includes(&ConcreteInteger)); + assert!(Number.includes(&Boolean)); + assert!(Any.includes(&Float)); + assert!(Any.includes(&Integer)); + assert!(Any.includes(&ConcreteInteger)); + assert!(Any.includes(&Boolean)); + assert!(Any.includes(&Number)); + assert!(Any.includes(&String)); + assert!(!String.includes(&Float)); + } +} diff --git a/src/targets/mod.rs b/src/targets/mod.rs deleted file mode 100644 index ce1d9f82..00000000 --- a/src/targets/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod wasm; diff --git a/src/targets/wasm.rs b/src/wasm/wasm.rs similarity index 100% rename from src/targets/wasm.rs rename to src/wasm/wasm.rs From 4737e34c8481da0fa0dce327849eb9603a4e4faa Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sat, 28 Dec 2024 14:17:09 +0000 Subject: [PATCH 02/98] commit number 2: full testing support for validity of wasm instructions --- Cargo.toml | 3 +- build.sh | 30 ++++----- src/error.rs | 4 +- src/instructions.rs | 98 ++++++++++++++++++++++++--- src/instructions/operator.rs | 2 +- src/instructions/operator/add.rs | 75 ++++++++++----------- src/ir.rs | 2 +- src/ir/types.rs | 9 ++- src/lib.rs | 5 +- src/{wasm/wasm.rs => old-wasm.rs} | 0 src/wasm.rs | 106 ++++++++++++++++++++++++++++++ 11 files changed, 262 insertions(+), 72 deletions(-) rename src/{wasm/wasm.rs => old-wasm.rs} (100%) create mode 100644 src/wasm.rs diff --git a/Cargo.toml b/Cargo.toml index ffedd876..1baaebb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,8 @@ publish = false serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } enum-field-getter = { path = "enum-field-getter" } -wasm-encoder = "0.214.0" wasm-bindgen = "0.2.92" +wasm-encoder = "0.222.0" indexmap = { version = "2.0.0", default-features = false } hashers = "1.0.1" uuid = { version = "1.4.1", default-features = false, features = ["v4", "js"] } @@ -19,6 +19,7 @@ bitmask-enum = "2.2.5" itertools = { version = "0.13.0", default-features = false } #[dev-dependencies] +wasmparser = "0.222.0" #reqwest = { version = "0.11", features = ["blocking"] } [lib] diff --git a/build.sh b/build.sh index e0355a8b..439ea8c8 100755 --- a/build.sh +++ b/build.sh @@ -18,13 +18,11 @@ usage() echo " -d build for development" echo " -p build for production" echo " -V build the website with vite" - echo " -v do not build the website with vite" echo " -W build wasm" - echo " -w do not build wasm" echo " -o do not run wasm-opt" echo " -O run wasm-opt" echo " -s run wasm-opt with -Os" - echo " -z run wasm-opt with -Os" + echo " -z run wasm-opt with -Oz" exit 1 } @@ -41,20 +39,16 @@ set_variable() fi } -unset PROD VITE WASM -QUIET=1 -while getopts 'dpwvoWVOhi' c +unset VITE WASM PROD; +QUIET=1; while getopts 'dpwvoWVszhi' c do case $c in d) set_variable PROD 0 ;; p) set_variable PROD 1 ;; - v) set_variable VITE 0 ;; - w) set_variable WASM 0 ;; V) set_variable VITE 1 ;; W) set_variable WASM 1 ;; o) set_variable WOPT 0 ;; - O) set_variable WOPT 1 ;; s) set_variable WOPT 1 ;; z) set_variable WOPT 2 ;; i) unset QUIET ;; @@ -62,12 +56,14 @@ do esac done -[ -z $PROD ] && usage -[ -z $VITE ] && usage -[ -z $WASM ] && usage +[ -z $WASM ] && set_variable WASM 0; +[ -z $VITE ] && set_variable VITE 0; + +[ -z $PROD ] && usage; + if [ -z $WOPT ]; then if [ $PROD = "1" ]; then - set_variable WOPT 1; + set_variable WOPT 2; else set_variable WOPT 0; fi @@ -88,15 +84,15 @@ if [ $WASM = "1" ]; then fi fi if [ $WOPT = "1" ]; then - echo running wasm-opt... - wasm-opt -Oz js/hyperquark_bg.wasm -o js/hyperquark_bg.wasm + echo running wasm-opt -Os... wasm-opt -Os -g js/hyperquark_bg.wasm -o js/hyperquark_bg.wasm fi if [ $WOPT = "2" ]; then - echo running wasm-opt... + echo running wasm-opt -Oz... wasm-opt -Oz -g js/hyperquark_bg.wasm -o js/hyperquark_bg.wasm fi if [ $VITE = "1" ]; then echo running npm build... npm run build -fi \ No newline at end of file +fi +echo finished! \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 29ff0da6..3e43b0ba 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,7 +3,7 @@ use wasm_bindgen::JsValue; pub type HQResult = Result; -#[derive(Debug)] // todo: get rid of this once all expects are gone +#[derive(Clone, Debug)] // todo: get rid of this once all expects are gone pub struct HQError { pub err_type: HQErrorType, pub msg: String, @@ -11,7 +11,7 @@ pub struct HQError { pub line: u32, pub column: u32, } -#[derive(Debug)] // todo: get rid of this once all expects are gone +#[derive(Clone, Debug, PartialEq)] // todo: get rid of this once all expects are gone pub enum HQErrorType { MalformedProject, InternalError, diff --git a/src/instructions.rs b/src/instructions.rs index d392fc3f..427c294f 100644 --- a/src/instructions.rs +++ b/src/instructions.rs @@ -2,7 +2,7 @@ pub mod operator; #[allow(non_camel_case_types)] pub enum IrOpcode { - operator_add + operator_add, } #[macro_export] @@ -10,15 +10,97 @@ macro_rules! instructions_test { ($($type_arg:ident $(,)?)+) => { #[cfg(test)] pub mod tests { - use super::{instructions, output_type, IrType}; + use super::{wasm, output_type, acceptable_inputs, IrType, HQError, HQResult}; + + macro_rules! ident_as_irtype { + ( $_:ident ) => { IrType }; + } + + fn types_iter() -> impl Iterator { + $(let $type_arg = IrType::flags().map(|(_, ty)| *ty).collect::>();)+ + itertools::iproduct!($($type_arg,)+).filter(|($($type_arg,)+)| { + for (i, input) in [$($type_arg,)+].into_iter().enumerate() { + // invalid input types should be handled by a wrapper function somewhere + // so we won't test those here. + if !acceptable_inputs()[i].contains(*input) { + return false; + } + } + true + }) + } + #[test] - fn output_type_fails_when_instructions_fails() { - $(let $type_arg = IrType::flags().map(|(_, ty)| ty); - )+ - for ($($type_arg ,)+) in itertools::iproduct!($($type_arg,)+){ - println!("boop") + fn output_type_fails_when_wasm_fails() { + // we need to collect this iterator into a Vec because it doesn't implement clone for some reason, + // which makes itertools angry + for ($($type_arg,)+) in types_iter() { + let output_type_result = output_type($($type_arg,)+); + let wasm_result = wasm(&$crate::wasm::StepFunc::new(), $($type_arg,)+); + match (output_type_result.clone(), wasm_result.clone()) { + (Err(..), Ok(..)) | (Ok(..), Err(..)) => panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)+), output_type_result, wasm_result), + (Err(HQError { err_type: e1, .. }), Err(HQError { err_type: e2, .. })) => { + if e1 != e2 { + panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)+), output_type_result, wasm_result); + } + } + _ => continue, + } } } + + #[test] + fn wasm_output_type_matches_expected_output_type() -> HQResult<()> { + use wasm_encoder::{ + CodeSection, FunctionSection, Instruction, Module, TypeSection, + }; + + for ($($type_arg,)+) in types_iter() { + let output_type = match output_type($($type_arg,)+) { + Ok(a) => a, + Err(_) => { + println!("skipping failed output_type"); + continue; + } + }; + let step_func = $crate::wasm::StepFunc::new_with_param_count([$($type_arg,)+].len())?; + let wasm = match wasm(&step_func, $($type_arg,)+) { + Ok(a) => a, + Err(_) => { + println!("skipping failed wasm"); + continue; + } + }; + println!("not skipping for types: {:?}", ($($type_arg,)+)); + for (i, _) in [$($type_arg,)+].iter().enumerate() { + step_func.add_instructions([Instruction::LocalGet(i.try_into().unwrap())]) + } + step_func.add_instructions(wasm); + let func = step_func.finish(); + + let mut module = Module::new(); + + let mut types = TypeSection::new(); + let params = [$($type_arg,)+].into_iter().map(|ty| $crate::wasm::ir_type_to_wasm(ty)).collect::>>()?; + let results = [$crate::wasm::ir_type_to_wasm(output_type)?]; + types.ty().function(params, results); + module.section(&types); + + let mut functions = FunctionSection::new(); + let type_index = 0; + functions.function(type_index); + module.section(&functions); + + let mut codes = CodeSection::new(); + codes.function(&func); + module.section(&codes); + + let wasm_bytes = module.finish(); + + wasmparser::validate(&wasm_bytes).map_err(|err| make_hq_bug!("invalid wasm module with types {:?}. Original error message: {}", ($($type_arg,)+), err.message()))?; + } + Ok(()) + } } } -} \ No newline at end of file +} diff --git a/src/instructions/operator.rs b/src/instructions/operator.rs index 71ae505b..cced7b48 100644 --- a/src/instructions/operator.rs +++ b/src/instructions/operator.rs @@ -1 +1 @@ -pub mod add; \ No newline at end of file +pub mod add; diff --git a/src/instructions/operator/add.rs b/src/instructions/operator/add.rs index 6ff8190e..acd4fc22 100644 --- a/src/instructions/operator/add.rs +++ b/src/instructions/operator/add.rs @@ -1,52 +1,53 @@ use crate::ir::types::Type as IrType; use crate::prelude::*; +use crate::wasm::StepFunc; +use wasm_encoder::{Instruction, ValType}; -use wasm_encoder::Instruction; - -pub fn instructions(t1: IrType, t2: IrType) -> HQResult>> { - hq_todo!(); - if IrType::QuasiInt.contains(t1) { +pub fn wasm(func: &StepFunc, t1: IrType, t2: IrType) -> HQResult>> { + Ok(if IrType::QuasiInt.contains(t1) { if IrType::QuasiInt.contains(t2) { - hq_todo!() + vec![Instruction::I64Add] } else if IrType::Float.contains(t2) { - hq_todo!() + let f64_local = func.get_local(ValType::F64, 1)?; + vec![ + Instruction::LocalSet(f64_local), + Instruction::F64ConvertI64S, + Instruction::LocalGet(f64_local), + Instruction::F64Add, + ] } else { hq_todo!() } - } else if IrType::Float.contains(t2) { - hq_todo!() + } else if IrType::Float.contains(t1) { + if IrType::Float.contains(t2) { + vec![Instruction::F64Add] + } else if IrType::QuasiInt.contains(t2) { + vec![Instruction::F64ConvertI64S, Instruction::F64Add] + } else { + hq_todo!(); + } } else { hq_todo!() - } + }) } -pub fn output_type(t1: IrType, t2: IrType) -> HQResult { - hq_todo!(); +#[allow(non_upper_case_globals)] +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Number, IrType::Number]) } -// (context (param $MEMORY_LOCATION i32) (result i32) -// (local $EXTERNREF ref.extern) -// (local $F64 f64) -// (local $I64 i64) -// (local $I32 i32) -// (local $I32_2 i32) -// (local $F64_2 i32) -// (inline $add_QuasiInteger_QuasiInteger (stack i64 i64) (result i64) -// i64.add) -// (inline $add_QuasiInteger_Float (stack i64 f64) (result f64) -// local.set $F64 -// f64.convert_i64_s -// local.get $F64 -// f64.add -// ) -// (inline $add_Float_QuasiInteger (stack f64 i64) (result f64) -// f64.convert_i64_s -// f64.add -// ) -// (inline $add_Float_Float (stack f64 f64) (result f64) -// f64.convert_i64_s -// f64.add -// ) -// ) +// TODO: nan +pub fn output_type(t1: IrType, t2: IrType) -> HQResult { + Ok(if IrType::QuasiInt.contains(t1.or(t2)) { + IrType::QuasiInt + } else if (IrType::QuasiInt.contains(t1) && IrType::Float.contains(t2)) + || (IrType::QuasiInt.contains(t2) && IrType::Float.contains(t1)) + || IrType::Float.contains(t1.or(t2)) + { + IrType::Float + } else { + hq_todo!() //IrType::Number + }) +} -crate::instructions_test!(t1, t2); \ No newline at end of file +crate::instructions_test!(t1, t2); diff --git a/src/ir.rs b/src/ir.rs index dd198c6d..cd408564 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -1 +1 @@ -pub mod types; \ No newline at end of file +pub mod types; diff --git a/src/ir/types.rs b/src/ir/types.rs index 97927f2a..f4d93f9c 100644 --- a/src/ir/types.rs +++ b/src/ir/types.rs @@ -47,10 +47,13 @@ pub enum Type { Number = Self::QuasiInt.or(Self::Float).bits, - StringNumber, // a string which can be interpreted as a non-nan number + StringNumber, // a string which can be interpreted as a non-nan number StringBoolean, // "true" or "false" - StringNan, // some other string which can only be interpreted as NaN - String = Self::StringNumber.or(Self::StringBoolean).or(Self::StringNan).bits, + StringNan, // some other string which can only be interpreted as NaN + String = Self::StringNumber + .or(Self::StringBoolean) + .or(Self::StringNan) + .bits, QuasiBoolean = Self::Boolean.or(Self::StringBoolean).bits, QuasiNumber = Self::Number.or(Self::StringNumber).bits, diff --git a/src/lib.rs b/src/lib.rs index 8627e575..87c91da1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ mod error; pub mod ir; // pub mod ir_opt; pub mod sb3; -// pub mod wasm; +pub mod wasm; #[macro_use] pub mod instructions; @@ -27,6 +27,7 @@ extern "C" { #[cfg(test)] pub fn log(s: &str) { + let a = ir::types::Type::flags().map(|(_, ty)| *ty); println!("{s}") } @@ -47,4 +48,4 @@ pub mod prelude { pub use alloc::vec::Vec; pub use core::cell::RefCell; pub use core::fmt; -} \ No newline at end of file +} diff --git a/src/wasm/wasm.rs b/src/old-wasm.rs similarity index 100% rename from src/wasm/wasm.rs rename to src/old-wasm.rs diff --git a/src/wasm.rs b/src/wasm.rs new file mode 100644 index 00000000..fd443010 --- /dev/null +++ b/src/wasm.rs @@ -0,0 +1,106 @@ +use crate::ir::types::Type as IrType; +use crate::prelude::*; +use wasm_encoder::{Function, Instruction, ValType}; + +pub mod locals { + pub const MEM_LOCATION: u32 = 0; + pub const EXTERNREF: u32 = 1; + pub const F64: u32 = 2; + pub const I64: u32 = 3; + pub const I32: u32 = 4; + pub const I32_2: u32 = 5; + pub const F64_2: u32 = 6; +} + +pub struct StepFunc { + locals: RefCell>, + instructions: RefCell>>, + param_count: u32, +} + +impl StepFunc { + pub fn new() -> Self { + StepFunc { + locals: RefCell::new(vec![]), + instructions: RefCell::new(vec![]), + param_count: 1, + } + } + + pub fn new_with_param_count(count: usize) -> HQResult { + Ok(StepFunc { + locals: RefCell::new(vec![]), + instructions: RefCell::new(vec![]), + param_count: u32::try_from(count) + .map_err(|_| make_hq_bug!("param count out of bounds"))?, + }) + } + + pub fn get_local(&self, val_type: ValType, count: u32) -> HQResult { + let existing_count = self + .locals + .borrow() + .iter() + .filter(|ty| **ty == val_type) + .count(); + Ok(u32::try_from(if existing_count < (count as usize) { + { + self.locals + .borrow_mut() + .extend([val_type].repeat(count as usize - existing_count)); + } + self.locals.borrow().len() - 1 + } else { + self.locals + .borrow() + .iter() + .rposition(|ty| *ty == val_type) + .unwrap() + }) + .map_err(|_| make_hq_bug!("local index was out of bounds"))? + + self.param_count) + } + + pub fn add_instructions(&self, instructions: impl IntoIterator>) { + self.instructions.borrow_mut().extend(instructions); + } + + pub fn finish(self) -> Function { + let mut func = Function::new_with_locals_types(self.locals.take()); + for instruction in self.instructions.take() { + func.instruction(&instruction); + } + func.instruction(&Instruction::End); + func + } +} + +impl Default for StepFunc { + fn default() -> Self { + Self::new() + } +} + +pub fn ir_type_to_wasm(ir_type: IrType) -> HQResult { + Ok(if IrType::Float.contains(ir_type) { + ValType::F64 + } else if IrType::QuasiInt.contains(ir_type) { + ValType::I64 + } else { + hq_todo!() + }) +} + +pub struct WasmProject { + step_funcs: RefCell>, +} + +impl WasmProject { + pub fn add_step_func(&self, step_func: StepFunc) { + self.step_funcs.borrow_mut().push(step_func); + } + + pub fn finish(self) -> HQResult> { + hq_todo!(); + } +} From f8df9bc1a3cb03f617610fa09fded971b95fa16b Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sat, 28 Dec 2024 16:22:00 +0000 Subject: [PATCH 03/98] commit 3: auto-generate opcode enum & related functions --- build.rs | 90 ++++++++++++++++++++++++++++++++ src/instructions.rs | 29 ++++++---- src/instructions/operator/add.rs | 8 ++- src/ir.rs | 6 +++ src/ir/irgen.rs | 3 ++ src/ir/types.rs | 1 + src/lib.rs | 4 +- src/sb3.rs | 39 ++++++++++++-- src/wasm.rs | 78 +++++++++++++++++++-------- 9 files changed, 218 insertions(+), 40 deletions(-) create mode 100644 build.rs create mode 100644 src/ir/irgen.rs diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..d3b5c7ce --- /dev/null +++ b/build.rs @@ -0,0 +1,90 @@ +use std::env; +use std::fs; +use std::path::Path; + +// I hate to admit this, but much of this was written by chatgpt to speed things up +// and to allow me to continue to procrastinate about learning how to do i/o stuff in rust. + +fn main() { + println!("cargo::rerun-if-changed=src/instructions/**/*"); + + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("ir-opcodes.rs"); + + // Define the base directory to search for files. + let base_dir = "src/instructions"; + + // Collect file paths + let mut paths = Vec::new(); + visit_dirs(Path::new(base_dir), &mut |entry| { + if let Some(ext) = entry.path().extension() { + if ext == "rs" { + if let Ok(relative_path) = entry.path().strip_prefix(base_dir) { + let components: Vec<_> = relative_path + .components() + .filter_map(|comp| comp.as_os_str().to_str()) + .collect(); + + if components.len() == 2 { + paths.push(( + format!("{}::{}", components[0], components[1].trim_end_matches(".rs")), + format!("{}_{}", components[0], components[1].trim_end_matches(".rs")), + )); + } + } + } + } + }); + + fs::write( + &dest_path, + format!( + "/// A list of all instructions. + #[allow(non_camel_case_types)] + pub enum IrOpcode {{ + {} + }} + + /// maps an opcode to its acceptable input types + pub fn acceptable_inputs(opcode: IrOpcode) -> Rc<[crate::ir::Type]> {{ + match opcode {{ + {} + }} + }} + + /// maps an opcode to its WASM instructions + pub fn wasm(opcode: IrOpcode, step_func: &crate::wasm::StepFunc, inputs: Rc<[crate::ir::Type]>) -> HQResult>> {{ + match opcode {{ + {} + }} + }} + + /// maps an opcode to its output type + pub fn output_type(opcode: IrOpcode, inputs: Rc<[crate::ir::Type]>) -> HQResult {{ + match opcode {{ + {} + }} + }} + ", + paths.iter().map(|(_, id)| id.clone()).collect::>().join(", "), + paths.iter().map(|(path, id)| format!("IrOpcode::{} => {}::acceptable_inputs(),", id, path)).collect::>().join("\n"), + paths.iter().map(|(path, id)| format!("IrOpcode::{} => {}::wasm(step_func, inputs),", id, path)).collect::>().join("\n"), + paths.iter().map(|(path, id)| format!("IrOpcode::{} => {}::output_type(inputs),", id, path)).collect::>().join("\n"), + )) + .unwrap(); +} + +fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&fs::DirEntry)) { + if dir.is_dir() { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + visit_dirs(&path, cb); + } else { + cb(&entry); + } + } + } + } +} diff --git a/src/instructions.rs b/src/instructions.rs index 427c294f..22d34d45 100644 --- a/src/instructions.rs +++ b/src/instructions.rs @@ -1,16 +1,21 @@ +//! Contains information about instructions (roughly anaologus to blocks), +//! including input type validation, output type mapping, and WASM generation. + +use crate::prelude::*; + pub mod operator; -#[allow(non_camel_case_types)] -pub enum IrOpcode { - operator_add, -} +include!(concat!(env!("OUT_DIR"), "/ir-opcodes.rs")); +/// generates unit tests for instructions files #[macro_export] macro_rules! instructions_test { ($($type_arg:ident $(,)?)+) => { #[cfg(test)] pub mod tests { - use super::{wasm, output_type, acceptable_inputs, IrType, HQError, HQResult}; + use super::{wasm, output_type, acceptable_inputs}; + use $crate::prelude::*; + use $crate::ir::Type as IrType; macro_rules! ident_as_irtype { ( $_:ident ) => { IrType }; @@ -35,8 +40,8 @@ macro_rules! instructions_test { // we need to collect this iterator into a Vec because it doesn't implement clone for some reason, // which makes itertools angry for ($($type_arg,)+) in types_iter() { - let output_type_result = output_type($($type_arg,)+); - let wasm_result = wasm(&$crate::wasm::StepFunc::new(), $($type_arg,)+); + let output_type_result = output_type(Rc::new([$($type_arg,)+])); + let wasm_result = wasm(&$crate::wasm::StepFunc::new(), Rc::new([$($type_arg,)+])); match (output_type_result.clone(), wasm_result.clone()) { (Err(..), Ok(..)) | (Ok(..), Err(..)) => panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)+), output_type_result, wasm_result), (Err(HQError { err_type: e1, .. }), Err(HQError { err_type: e2, .. })) => { @@ -56,7 +61,7 @@ macro_rules! instructions_test { }; for ($($type_arg,)+) in types_iter() { - let output_type = match output_type($($type_arg,)+) { + let output_type = match output_type(Rc::new([$($type_arg,)+])) { Ok(a) => a, Err(_) => { println!("skipping failed output_type"); @@ -64,7 +69,7 @@ macro_rules! instructions_test { } }; let step_func = $crate::wasm::StepFunc::new_with_param_count([$($type_arg,)+].len())?; - let wasm = match wasm(&step_func, $($type_arg,)+) { + let wasm = match wasm(&step_func, Rc::new([$($type_arg,)+])) { Ok(a) => a, Err(_) => { println!("skipping failed wasm"); @@ -78,11 +83,13 @@ macro_rules! instructions_test { step_func.add_instructions(wasm); let func = step_func.finish(); + let wasm_proj = $crate::wasm::WasmProject::new(Default::default()); + let mut module = Module::new(); let mut types = TypeSection::new(); - let params = [$($type_arg,)+].into_iter().map(|ty| $crate::wasm::ir_type_to_wasm(ty)).collect::>>()?; - let results = [$crate::wasm::ir_type_to_wasm(output_type)?]; + let params = [$($type_arg,)+].into_iter().map(|ty| wasm_proj.ir_type_to_wasm(ty)).collect::>>()?; + let results = [wasm_proj.ir_type_to_wasm(output_type)?]; types.ty().function(params, results); module.section(&types); diff --git a/src/instructions/operator/add.rs b/src/instructions/operator/add.rs index acd4fc22..7a36683f 100644 --- a/src/instructions/operator/add.rs +++ b/src/instructions/operator/add.rs @@ -3,7 +3,9 @@ use crate::prelude::*; use crate::wasm::StepFunc; use wasm_encoder::{Instruction, ValType}; -pub fn wasm(func: &StepFunc, t1: IrType, t2: IrType) -> HQResult>> { +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult>> { + let t1 = inputs[0]; + let t2 = inputs[1]; Ok(if IrType::QuasiInt.contains(t1) { if IrType::QuasiInt.contains(t2) { vec![Instruction::I64Add] @@ -37,7 +39,9 @@ pub fn acceptable_inputs() -> Rc<[IrType]> { } // TODO: nan -pub fn output_type(t1: IrType, t2: IrType) -> HQResult { +pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { + let t1 = inputs[0]; + let t2 = inputs[1]; Ok(if IrType::QuasiInt.contains(t1.or(t2)) { IrType::QuasiInt } else if (IrType::QuasiInt.contains(t1) && IrType::Float.contains(t2)) diff --git a/src/ir.rs b/src/ir.rs index cd408564..209160e9 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -1 +1,7 @@ pub mod types; +pub mod irgen; + +#[doc(inline)] +pub use types::Type; +#[doc(inline)] +pub use irgen::IrProject; \ No newline at end of file diff --git a/src/ir/irgen.rs b/src/ir/irgen.rs new file mode 100644 index 00000000..05b3b4bd --- /dev/null +++ b/src/ir/irgen.rs @@ -0,0 +1,3 @@ +pub struct IrProject { + +} \ No newline at end of file diff --git a/src/ir/types.rs b/src/ir/types.rs index f4d93f9c..edb0f048 100644 --- a/src/ir/types.rs +++ b/src/ir/types.rs @@ -1,5 +1,6 @@ use bitmask_enum::bitmask; +/// a bitmask of possible IR types #[bitmask(u32)] #[bitmask_config(vec_debug, flags_iter)] pub enum Type { diff --git a/src/lib.rs b/src/lib.rs index 87c91da1..0c4e83ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ #![cfg_attr(not(test), no_std)] +#![doc(html_logo_url = "https://hyperquark.github.io/hyperquark/logo.png")] +#![doc(html_favicon_url = "https://hyperquark.github.io/hyperquark/favicon.ico")] #[macro_use] extern crate alloc; @@ -15,6 +17,7 @@ pub mod wasm; #[macro_use] pub mod instructions; +#[doc(inline)] pub use error::{HQError, HQErrorType, HQResult}; // use wasm::wasm; @@ -27,7 +30,6 @@ extern "C" { #[cfg(test)] pub fn log(s: &str) { - let a = ir::types::Type::flags().map(|(_, ty)| *ty); println!("{s}") } diff --git a/src/sb3.rs b/src/sb3.rs index 51811857..04f52dd9 100644 --- a/src/sb3.rs +++ b/src/sb3.rs @@ -1,3 +1,8 @@ +//! 1-1 representation of `project.json` or `sprite.json` files +//! in the `sb3` format. `sb` or `sb2` files must be converted first; +//! `sb3` files must be unzipped first. See +//! for a loose informal specification. + use crate::error::HQError; use alloc::collections::BTreeMap; use alloc::string::String; @@ -6,6 +11,7 @@ use enum_field_getter::EnumFieldGetter; use serde::{Deserialize, Serialize}; use serde_json::Value; +/// A scratch project #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Sb3Project { pub targets: Vec, @@ -14,6 +20,7 @@ pub struct Sb3Project { pub meta: Meta, } +/// A comment, possibly attached to a block #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Comment { @@ -26,6 +33,9 @@ pub struct Comment { pub text: String, } +/// A possible block opcode, encompassing the default block pallette, the pen extension, +/// and a few hidden but non-obsolete blocks. A block being listed here does not imply that +/// it is supported by HyperQuark. #[allow(non_camel_case_types)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub enum BlockOpcode { @@ -201,6 +211,7 @@ pub enum BlockOpcode { // other, } +/// A scratch block - either special or not #[derive(Serialize, Deserialize, Debug, Clone, EnumFieldGetter)] #[serde(untagged)] pub enum Block { @@ -216,15 +227,18 @@ pub enum Block { Special(BlockArray), } +/// A special representation of a scratch block. #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum BlockArray { NumberOrAngle(u32, f64), ColorOrString(u32, String), - Broadcast(u32, String, String), // might also be variable or list if not top level? + /// This might also represent a variable or list if the block is not top-level, in theory + Broadcast(u32, String, String), VariableOrList(u32, String, String, f64, f64), } +/// Either a block array or a block id #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum BlockArrayOrId { @@ -232,6 +246,7 @@ pub enum BlockArrayOrId { Array(BlockArray), } +/// Possible inputs (round or predicate) in a block #[derive(Serialize, Deserialize, Debug, Clone, EnumFieldGetter)] #[serde(untagged)] pub enum Input { @@ -239,6 +254,7 @@ pub enum Input { NoShadow(u32, Option), } +/// Possible fields (rectangular) in a block #[derive(Serialize, Deserialize, Debug, Clone, EnumFieldGetter)] #[serde(untagged)] pub enum Field { @@ -246,10 +262,13 @@ pub enum Field { ValueId(Option, Option), } +/// Represents a mutation on a block. See #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Mutation { + /// ignored - should always be "mutation" pub tag_name: String, + /// ignored - should always be [] #[serde(default)] pub children: Vec<()>, #[serde(flatten)] @@ -266,6 +285,7 @@ impl Default for Mutation { } } +/// Represents a non-special block #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct BlockInfo { @@ -280,6 +300,7 @@ pub struct BlockInfo { pub mutation: Mutation, } +/// the data format of a costume #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] #[allow(non_camel_case_types)] pub enum CostumeDataFormat { @@ -291,6 +312,7 @@ pub enum CostumeDataFormat { gif, } +/// A costume #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Costume { @@ -304,17 +326,19 @@ pub struct Costume { pub rotation_center_y: f64, } +/// A sound #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Sound { pub asset_id: String, pub name: String, pub md5ext: String, - pub data_format: String, + pub data_format: String, // TODO: enumerate pub rate: f64, pub sample_count: f64, } +/// The (default) value of a variable #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(untagged)] pub enum VarVal { @@ -323,6 +347,7 @@ pub enum VarVal { String(String), } +/// Represents a variable #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(untagged)] pub enum VariableInfo { @@ -330,6 +355,7 @@ pub enum VariableInfo { LocalVar(String, VarVal), } +/// A target (sprite or stage) #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Target { @@ -372,6 +398,7 @@ pub struct Target { pub unknown: BTreeMap, } +/// The value of a list monitor #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum ListMonitorValue { @@ -379,13 +406,15 @@ pub enum ListMonitorValue { String(String), } +/// A monitor #[derive(Serialize, Deserialize, Debug, Clone, EnumFieldGetter)] #[serde(untagged)] pub enum Monitor { #[serde(rename_all = "camelCase")] ListMonitor { id: String, - mode: String, // The name of the monitor's mode: "default", "large", "slider", or "list" - should be "list" + /// The name of the monitor's mode: "default", "large", "slider", or "list" - should be "list" + mode: String, opcode: String, params: BTreeMap, sprite_name: Option, @@ -399,7 +428,8 @@ pub enum Monitor { #[serde(rename_all = "camelCase")] VarMonitor { id: String, - mode: String, // The name of the monitor's mode: "default", "large", "slider", or "list". + /// The name of the monitor's mode: "default", "large", "slider", or "list". + mode: String, opcode: String, params: BTreeMap, sprite_name: Option, @@ -415,6 +445,7 @@ pub enum Monitor { }, } +/// metadata about a scratch project - only included here for completeness #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Meta { pub semver: String, diff --git a/src/wasm.rs b/src/wasm.rs index fd443010..aa50ace9 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -2,16 +2,7 @@ use crate::ir::types::Type as IrType; use crate::prelude::*; use wasm_encoder::{Function, Instruction, ValType}; -pub mod locals { - pub const MEM_LOCATION: u32 = 0; - pub const EXTERNREF: u32 = 1; - pub const F64: u32 = 2; - pub const I64: u32 = 3; - pub const I32: u32 = 4; - pub const I32_2: u32 = 5; - pub const F64_2: u32 = 6; -} - +/// representation of a step's function pub struct StepFunc { locals: RefCell>, instructions: RefCell>>, @@ -19,6 +10,7 @@ pub struct StepFunc { } impl StepFunc { + /// creates a new step function, with one paramter pub fn new() -> Self { StepFunc { locals: RefCell::new(vec![]), @@ -26,7 +18,9 @@ impl StepFunc { param_count: 1, } } - + + /// creates a new step function with the specified amount of paramters. + /// currently only used in testing to validate types pub fn new_with_param_count(count: usize) -> HQResult { Ok(StepFunc { locals: RefCell::new(vec![]), @@ -36,21 +30,24 @@ impl StepFunc { }) } - pub fn get_local(&self, val_type: ValType, count: u32) -> HQResult { + /// Returns the index of the `n`th local of the specified type in this function, + /// adding some if necessary + pub fn get_local(&self, val_type: ValType, n: u32) -> HQResult { let existing_count = self .locals .borrow() .iter() .filter(|ty| **ty == val_type) .count(); - Ok(u32::try_from(if existing_count < (count as usize) { + Ok(u32::try_from(if existing_count < (n as usize) { { self.locals .borrow_mut() - .extend([val_type].repeat(count as usize - existing_count)); + .extend([val_type].repeat(n as usize - existing_count)); } self.locals.borrow().len() - 1 } else { + // TODO: return nth rather than last self.locals .borrow() .iter() @@ -65,6 +62,7 @@ impl StepFunc { self.instructions.borrow_mut().extend(instructions); } + /// Takes ownership of the function and returns the backing `wasm_encoder` `Function` pub fn finish(self) -> Function { let mut func = Function::new_with_locals_types(self.locals.take()); for instruction in self.instructions.take() { @@ -81,21 +79,57 @@ impl Default for StepFunc { } } -pub fn ir_type_to_wasm(ir_type: IrType) -> HQResult { - Ok(if IrType::Float.contains(ir_type) { - ValType::F64 - } else if IrType::QuasiInt.contains(ir_type) { - ValType::I64 - } else { - hq_todo!() - }) +#[non_exhaustive] +pub enum WasmStringType { + ExternRef, + JsString, +} + +impl Default for WasmStringType { + fn default() -> Self { + Self::ExternRef + } +} + +/// compilation flags +#[non_exhaustive] +#[derive(Default)] +pub struct WasmFlags { + pub string_type: WasmStringType, } pub struct WasmProject { + flags: WasmFlags, step_funcs: RefCell>, } impl WasmProject { + pub fn new(flags: WasmFlags) -> Self { + WasmProject { + flags, + step_funcs: RefCell::new(vec![]), + } + } + + pub fn flags(&self) -> &WasmFlags { + &self.flags + } + + /// maps a broad IR type to a WASM type + pub fn ir_type_to_wasm(&self, ir_type: IrType) -> HQResult { + Ok(if IrType::Float.contains(ir_type) { + ValType::F64 + } else if IrType::QuasiInt.contains(ir_type) { + ValType::I64 + } else if IrType::String.contains(ir_type) { + ValType::EXTERNREF + } else if IrType::Color.contains(ir_type) { + hq_todo!();//ValType::V128 // f32x4 + } else { + ValType::I64 // NaN boxed value... let's worry about colors later + }) + } + pub fn add_step_func(&self, step_func: StepFunc) { self.step_funcs.borrow_mut().push(step_func); } From 7946cad48a344ce05851c448a7bbffe0c06edcdf Mon Sep 17 00:00:00 2001 From: Pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Wed, 1 Jan 2025 16:05:01 +0000 Subject: [PATCH 04/98] basic looks_say, js type checking, and Other Things --- Cargo.toml | 10 +- build.rs | 18 ++- src/error.rs | 2 + src/instructions.rs | 111 +------------ src/instructions/looks.rs | 1 + src/instructions/looks/say.rs | 40 +++++ src/instructions/looks/say_float.ts | 3 + src/instructions/looks/say_int.ts | 3 + src/instructions/operator/add.rs | 7 +- src/instructions/utilities.rs | 233 ++++++++++++++++++++++++++++ src/ir.rs | 6 +- src/ir/irgen.rs | 4 +- src/lib.rs | 11 +- src/wasm.rs | 151 ++---------------- src/wasm/external.rs | 57 +++++++ src/wasm/flags.rs | 18 +++ src/wasm/func.rs | 96 ++++++++++++ src/wasm/project.rs | 48 ++++++ src/wasm/type_registry.rs | 37 +++++ 19 files changed, 591 insertions(+), 265 deletions(-) create mode 100644 src/instructions/looks.rs create mode 100644 src/instructions/looks/say.rs create mode 100644 src/instructions/looks/say_float.ts create mode 100644 src/instructions/looks/say_int.ts create mode 100644 src/instructions/utilities.rs create mode 100644 src/wasm/external.rs create mode 100644 src/wasm/flags.rs create mode 100644 src/wasm/func.rs create mode 100644 src/wasm/project.rs create mode 100644 src/wasm/type_registry.rs diff --git a/Cargo.toml b/Cargo.toml index 1baaebb8..8b13a4ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ publish = false serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } enum-field-getter = { path = "enum-field-getter" } -wasm-bindgen = "0.2.92" wasm-encoder = "0.222.0" indexmap = { version = "2.0.0", default-features = false } hashers = "1.0.1" @@ -17,11 +16,18 @@ regex = "1.10.5" lazy-regex = "3.2.0" bitmask-enum = "2.2.5" itertools = { version = "0.13.0", default-features = false } +split_exact = "1.1.0" + +[target.'cfg(target_family = "wasm")'.dependencies] +wasm-bindgen = "0.2.92" -#[dev-dependencies] +[dev-dependencies] wasmparser = "0.222.0" #reqwest = { version = "0.11", features = ["blocking"] } +[target.'cfg(not(target_family = "wasm"))'.dev-dependencies] +ezno = { git = "https://github.com/hyperquark/ezno.git", branch = "wasm-bindgen-version-patch" } + [lib] crate-type = ["cdylib", "rlib"] diff --git a/build.rs b/build.rs index d3b5c7ce..b19540c3 100644 --- a/build.rs +++ b/build.rs @@ -6,7 +6,7 @@ use std::path::Path; // and to allow me to continue to procrastinate about learning how to do i/o stuff in rust. fn main() { - println!("cargo::rerun-if-changed=src/instructions/**/*"); + println!("cargo::rerun-if-changed=src/instructions/**/*.rs"); let out_dir = env::var_os("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("ir-opcodes.rs"); @@ -25,10 +25,18 @@ fn main() { .filter_map(|comp| comp.as_os_str().to_str()) .collect(); - if components.len() == 2 { + if components.len() == 2 && components[1].ends_with(".rs") { paths.push(( - format!("{}::{}", components[0], components[1].trim_end_matches(".rs")), - format!("{}_{}", components[0], components[1].trim_end_matches(".rs")), + format!( + "{}::{}", + components[0], + components[1].trim_end_matches(".rs") + ), + format!( + "{}_{}", + components[0], + components[1].trim_end_matches(".rs") + ), )); } } @@ -60,7 +68,7 @@ fn main() { }} /// maps an opcode to its output type - pub fn output_type(opcode: IrOpcode, inputs: Rc<[crate::ir::Type]>) -> HQResult {{ + pub fn output_type(opcode: IrOpcode, inputs: Rc<[crate::ir::Type]>) -> HQResult> {{ match opcode {{ {} }} diff --git a/src/error.rs b/src/error.rs index 3e43b0ba..1fd0d84e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,5 @@ use alloc::string::String; +#[cfg(target_family = "wasm")] use wasm_bindgen::JsValue; pub type HQResult = Result; @@ -18,6 +19,7 @@ pub enum HQErrorType { Unimplemented, } +#[cfg(target_family = "wasm")] impl From for JsValue { fn from(val: HQError) -> JsValue { JsValue::from_str(match val.err_type { diff --git a/src/instructions.rs b/src/instructions.rs index 22d34d45..eede4df4 100644 --- a/src/instructions.rs +++ b/src/instructions.rs @@ -3,111 +3,10 @@ use crate::prelude::*; -pub mod operator; +mod looks; +mod operator; +#[macro_use] +mod utilities; +pub use utilities::{file_block_category, file_block_name, file_opcode}; include!(concat!(env!("OUT_DIR"), "/ir-opcodes.rs")); - -/// generates unit tests for instructions files -#[macro_export] -macro_rules! instructions_test { - ($($type_arg:ident $(,)?)+) => { - #[cfg(test)] - pub mod tests { - use super::{wasm, output_type, acceptable_inputs}; - use $crate::prelude::*; - use $crate::ir::Type as IrType; - - macro_rules! ident_as_irtype { - ( $_:ident ) => { IrType }; - } - - fn types_iter() -> impl Iterator { - $(let $type_arg = IrType::flags().map(|(_, ty)| *ty).collect::>();)+ - itertools::iproduct!($($type_arg,)+).filter(|($($type_arg,)+)| { - for (i, input) in [$($type_arg,)+].into_iter().enumerate() { - // invalid input types should be handled by a wrapper function somewhere - // so we won't test those here. - if !acceptable_inputs()[i].contains(*input) { - return false; - } - } - true - }) - } - - #[test] - fn output_type_fails_when_wasm_fails() { - // we need to collect this iterator into a Vec because it doesn't implement clone for some reason, - // which makes itertools angry - for ($($type_arg,)+) in types_iter() { - let output_type_result = output_type(Rc::new([$($type_arg,)+])); - let wasm_result = wasm(&$crate::wasm::StepFunc::new(), Rc::new([$($type_arg,)+])); - match (output_type_result.clone(), wasm_result.clone()) { - (Err(..), Ok(..)) | (Ok(..), Err(..)) => panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)+), output_type_result, wasm_result), - (Err(HQError { err_type: e1, .. }), Err(HQError { err_type: e2, .. })) => { - if e1 != e2 { - panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)+), output_type_result, wasm_result); - } - } - _ => continue, - } - } - } - - #[test] - fn wasm_output_type_matches_expected_output_type() -> HQResult<()> { - use wasm_encoder::{ - CodeSection, FunctionSection, Instruction, Module, TypeSection, - }; - - for ($($type_arg,)+) in types_iter() { - let output_type = match output_type(Rc::new([$($type_arg,)+])) { - Ok(a) => a, - Err(_) => { - println!("skipping failed output_type"); - continue; - } - }; - let step_func = $crate::wasm::StepFunc::new_with_param_count([$($type_arg,)+].len())?; - let wasm = match wasm(&step_func, Rc::new([$($type_arg,)+])) { - Ok(a) => a, - Err(_) => { - println!("skipping failed wasm"); - continue; - } - }; - println!("not skipping for types: {:?}", ($($type_arg,)+)); - for (i, _) in [$($type_arg,)+].iter().enumerate() { - step_func.add_instructions([Instruction::LocalGet(i.try_into().unwrap())]) - } - step_func.add_instructions(wasm); - let func = step_func.finish(); - - let wasm_proj = $crate::wasm::WasmProject::new(Default::default()); - - let mut module = Module::new(); - - let mut types = TypeSection::new(); - let params = [$($type_arg,)+].into_iter().map(|ty| wasm_proj.ir_type_to_wasm(ty)).collect::>>()?; - let results = [wasm_proj.ir_type_to_wasm(output_type)?]; - types.ty().function(params, results); - module.section(&types); - - let mut functions = FunctionSection::new(); - let type_index = 0; - functions.function(type_index); - module.section(&functions); - - let mut codes = CodeSection::new(); - codes.function(&func); - module.section(&codes); - - let wasm_bytes = module.finish(); - - wasmparser::validate(&wasm_bytes).map_err(|err| make_hq_bug!("invalid wasm module with types {:?}. Original error message: {}", ($($type_arg,)+), err.message()))?; - } - Ok(()) - } - } - } -} diff --git a/src/instructions/looks.rs b/src/instructions/looks.rs new file mode 100644 index 00000000..51213c67 --- /dev/null +++ b/src/instructions/looks.rs @@ -0,0 +1 @@ +pub mod say; diff --git a/src/instructions/looks/say.rs b/src/instructions/looks/say.rs new file mode 100644 index 00000000..4fe97266 --- /dev/null +++ b/src/instructions/looks/say.rs @@ -0,0 +1,40 @@ +use crate::instructions::{file_block_category, file_block_name}; +use crate::ir::types::Type as IrType; +use crate::prelude::*; +use crate::wasm::StepFunc; +use wasm_encoder::{Instruction, ValType}; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult>> { + Ok(if IrType::QuasiInt.contains(inputs[0]) { + let func_index = func.external_functions().function_index( + "looks", + "say_int", + vec![ValType::I64], + vec![], + )?; + vec![Instruction::Call(func_index)] + } else if IrType::Float.contains(inputs[0]) { + let func_index = func.external_functions().function_index( + "looks", + "say_float", + vec![ValType::F64], + vec![], + )?; + vec![Instruction::Call(func_index)] + } else { + hq_todo!() + }) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Any]) +} + +pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { + if !(IrType::QuasiInt.contains(inputs[0]) || IrType::Float.contains(inputs[0])) { + hq_todo!() + } + Ok(None) +} + +crate::instructions_test!(t); diff --git a/src/instructions/looks/say_float.ts b/src/instructions/looks/say_float.ts new file mode 100644 index 00000000..a495d6c1 --- /dev/null +++ b/src/instructions/looks/say_float.ts @@ -0,0 +1,3 @@ +function say_float(data: number): void { + console.log(data); +} \ No newline at end of file diff --git a/src/instructions/looks/say_int.ts b/src/instructions/looks/say_int.ts new file mode 100644 index 00000000..e6072adf --- /dev/null +++ b/src/instructions/looks/say_int.ts @@ -0,0 +1,3 @@ +function say_int(data: number): void { + console.log(data); +} \ No newline at end of file diff --git a/src/instructions/operator/add.rs b/src/instructions/operator/add.rs index 7a36683f..06f7e952 100644 --- a/src/instructions/operator/add.rs +++ b/src/instructions/operator/add.rs @@ -33,16 +33,15 @@ pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult Rc<[IrType]> { Rc::new([IrType::Number, IrType::Number]) } // TODO: nan -pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { +pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { let t1 = inputs[0]; let t2 = inputs[1]; - Ok(if IrType::QuasiInt.contains(t1.or(t2)) { + Ok(Some(if IrType::QuasiInt.contains(t1.or(t2)) { IrType::QuasiInt } else if (IrType::QuasiInt.contains(t1) && IrType::Float.contains(t2)) || (IrType::QuasiInt.contains(t2) && IrType::Float.contains(t1)) @@ -51,7 +50,7 @@ pub fn output_type(inputs: Rc<[IrType]>) -> HQResult { IrType::Float } else { hq_todo!() //IrType::Number - }) + })) } crate::instructions_test!(t1, t2); diff --git a/src/instructions/utilities.rs b/src/instructions/utilities.rs new file mode 100644 index 00000000..c3a0988b --- /dev/null +++ b/src/instructions/utilities.rs @@ -0,0 +1,233 @@ +use crate::prelude::*; + +mod file_opcode { + //! instruction module paths look something like + //! hyperquark::instructions::category::block + //! so if we split it by ':', we end up with 7 chunks + + use crate::prelude::*; + use split_exact::SplitExact; + + pub fn file_block_category(path: &'static str) -> &'static str { + path.split_exact::<7>(|c| c == ':')[4].unwrap() + } + + pub fn file_block_name(path: &'static str) -> &'static str { + path.split_exact::<7>(|c| c == ':')[6].unwrap() + } + + pub fn file_opcode(path: &'static str) -> String { + format!("{}_{}", file_block_category(path), file_block_name(path)) + } +} +pub use file_opcode::*; + +#[cfg(test)] +pub mod tests { + #[test] + fn file_block_category() { + assert_eq!(super::file_block_category(module_path!()), "utilities"); + } + + #[test] + fn file_block_name() { + assert_eq!(super::file_block_name(module_path!()), "tests"); + } + + #[test] + fn file_opcode() { + assert_eq!(super::file_opcode(module_path!()), "utilities_tests"); + } +} + +/// generates unit tests for instructions files +#[macro_export] +macro_rules! instructions_test { + ($($type_arg:ident $(,)?)+) => { + #[cfg(test)] + pub mod tests { + use super::{wasm, output_type, acceptable_inputs}; + use $crate::prelude::*; + use $crate::ir::Type as IrType; + use wasm_encoder::ValType; + + macro_rules! ident_as_irtype { + ( $_:ident ) => { IrType }; + } + + fn types_iter() -> impl Iterator { + // we need to collect this iterator into a Vec because it doesn't implement clone for some reason, + // which makes itertools angry + $(let $type_arg = IrType::flags().map(|(_, ty)| *ty).collect::>();)+ + itertools::iproduct!($($type_arg,)+).filter(|($($type_arg,)+)| { + for (i, input) in [$($type_arg,)+].into_iter().enumerate() { + // invalid input types should be handled by a wrapper function somewhere + // so we won't test those here. + if !acceptable_inputs()[i].contains(*input) { + return false; + } + } + true + }) + } + + #[test] + fn output_type_fails_when_wasm_fails() { + use $crate::wasm::{StepFunc, TypeRegistry, ExternalFunctionMap}; + for ($($type_arg,)+) in types_iter() { + let output_type_result = output_type(Rc::new([$($type_arg,)+])); + let type_registry = Rc::new(TypeRegistry::new()); + let external_functions = Rc::new(ExternalFunctionMap::new()); + let step_func = StepFunc::new_with_param_count([$($type_arg,)+].len(), type_registry.clone(), external_functions.clone()).unwrap(); + let wasm_result = wasm(&step_func, Rc::new([$($type_arg,)+])); + match (output_type_result.clone(), wasm_result.clone()) { + (Err(..), Ok(..)) | (Ok(..), Err(..)) => panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)+), output_type_result, wasm_result), + (Err(HQError { err_type: e1, .. }), Err(HQError { err_type: e2, .. })) => { + if e1 != e2 { + panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)+), output_type_result, wasm_result); + } + } + _ => continue, + } + } + } + + #[test] + fn wasm_output_type_matches_expected_output_type() -> HQResult<()> { + use wasm_encoder::{ + CodeSection, FunctionSection, ImportSection, Instruction, Module, TypeSection, + }; + use $crate::wasm::{StepFunc, TypeRegistry, ExternalFunctionMap}; + use $crate::prelude::Rc; + + for ($($type_arg,)+) in types_iter() { + let output_type = match output_type(Rc::new([$($type_arg,)+])) { + Ok(a) => a, + Err(_) => { + println!("skipping failed output_type"); + continue; + } + }; + let type_registry = Rc::new(TypeRegistry::new()); + let external_functions = Rc::new(ExternalFunctionMap::new()); + let step_func = StepFunc::new_with_param_count([$($type_arg,)+].len(), type_registry.clone(), external_functions.clone())?; + let wasm = match wasm(&step_func, Rc::new([$($type_arg,)+])) { + Ok(a) => a, + Err(_) => { + println!("skipping failed wasm"); + continue; + } + }; + for (i, _) in [$($type_arg,)+].iter().enumerate() { + step_func.add_instructions([Instruction::LocalGet(i.try_into().unwrap())]) + } + step_func.add_instructions(wasm); + let func = step_func.finish(); + + let wasm_proj = $crate::wasm::WasmProject::new(Default::default(), crate::wasm::ExternalEnvironment::WebBrowser); + + let mut module = Module::new(); + + let mut imports = ImportSection::new(); + + Rc::unwrap_or_clone(external_functions).finish(&mut imports, type_registry.clone())?; + + let mut types = TypeSection::new(); + let params = [$($type_arg,)+].into_iter().map(|ty| wasm_proj.ir_type_to_wasm(ty)).collect::>>()?; + let results = match output_type { + Some(output) => vec![wasm_proj.ir_type_to_wasm(output)?], + None => vec![], + }; + let step_type_index = type_registry.type_index(params, results)?; + Rc::unwrap_or_clone(type_registry).finish(&mut types); + module.section(&types); + + module.section(&imports); + + let mut functions = FunctionSection::new(); + functions.function(step_type_index); + module.section(&functions); + + let mut codes = CodeSection::new(); + codes.function(&func); + module.section(&codes); + + let wasm_bytes = module.finish(); + + wasmparser::validate(&wasm_bytes).map_err(|err| make_hq_bug!("invalid wasm module with types {:?}. Original error message: {}", ($($type_arg,)+), err.message()))?; + } + Ok(()) + } + + fn wasm_to_js_type(ty: ValType) -> &'static str { + match ty { + ValType::I64 | ValType::F64 => "number", + _ => todo!("unknown js type for wasm type {:?}", ty) + } + } + + #[test] + fn js_functions_match_declared_types() { + use $crate::wasm::{ExternalFunctionMap, StepFunc, TypeRegistry}; + use ezno_lib::{check as check_js, Diagnostic}; + use std::path::{Path, PathBuf}; + use std::fs; + + for ($($type_arg,)+) in types_iter() { + let type_registry = Rc::new(TypeRegistry::new()); + let external_functions = Rc::new(ExternalFunctionMap::new()); + let step_func = StepFunc::new(type_registry.clone(), external_functions.clone()); + if wasm(&step_func, Rc::new([$($type_arg,)+])).is_err() { + println!("skipping failed wasm"); + continue; + }; + for ((module, name), (params, results)) in external_functions.get_map().borrow().iter() { + assert!(results.len() < 1, "external function {}::{} registered as returning multiple results", module, name); + let out = if results.len() == 0 { + "void" + } else { + wasm_to_js_type(results[1]) + }; + let ins = [$(stringify!($type_arg),)+].into_iter().enumerate().map(|(i, t)| { + format!( + "{}: {}", + t, + wasm_to_js_type( + $crate::wasm::WasmProject::new( + Default::default(), + crate::wasm::ExternalEnvironment::WebBrowser + ).ir_type_to_wasm([$($type_arg,)+][i]).unwrap() + ) + ) + }).collect::>().join(", "); + let diagnostics = check_js( + vec![PathBuf::from(format!("src/instructions/{}/{}.ts", module, name))], + &|path: &Path| { + let func_string = fs::read_to_string(path).ok()?; + let test_string = format!("function _({ins}): {out} {{ + {func} + return {name}{ts}; + }}", ins=ins, out=out, func=func_string, name=name, ts=stringify!(($($type_arg,)+))); + println!("{}", test_string.clone()); + Some(test_string.as_str().as_bytes().into_iter().map(|&u| u).collect::>()) + }, + None, + Default::default(), + ) + .diagnostics; + if diagnostics.contains_error() { + let reasons = diagnostics.into_iter().map(|d| { + match d { + Diagnostic::Global { reason, .. } => reason, + Diagnostic::Position { reason, .. } => reason, + Diagnostic::PositionWithAdditionalLabels { reason, .. } => reason, + } + }).collect::>().join("; "); + panic!("js for external function {}::{} is not type-safe; reason(s): {}", module, name, reasons); + } + } + } + } + } + } +} diff --git a/src/ir.rs b/src/ir.rs index 209160e9..3a695266 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -1,7 +1,7 @@ -pub mod types; pub mod irgen; +pub mod types; #[doc(inline)] -pub use types::Type; +pub use irgen::IrProject; #[doc(inline)] -pub use irgen::IrProject; \ No newline at end of file +pub use types::Type; diff --git a/src/ir/irgen.rs b/src/ir/irgen.rs index 05b3b4bd..5a66b036 100644 --- a/src/ir/irgen.rs +++ b/src/ir/irgen.rs @@ -1,3 +1 @@ -pub struct IrProject { - -} \ No newline at end of file +pub struct IrProject {} diff --git a/src/lib.rs b/src/lib.rs index 0c4e83ff..4b653832 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ extern crate alloc; extern crate enum_field_getter; +#[cfg(target_family = "wasm")] use wasm_bindgen::prelude::*; #[macro_use] @@ -22,8 +23,8 @@ pub use error::{HQError, HQErrorType, HQResult}; // use wasm::wasm; -#[cfg(not(test))] -#[wasm_bindgen(js_namespace=console)] +#[cfg(target_family = "wasm")] +#[cfg_attr(target_family = "wasm", wasm_bindgen(js_namespace=console))] extern "C" { pub fn log(s: &str); } @@ -50,4 +51,10 @@ pub mod prelude { pub use alloc::vec::Vec; pub use core::cell::RefCell; pub use core::fmt; + + use core::hash::BuildHasherDefault; + use hashers::fnv::FNV1aHasher64; + use indexmap; + pub type IndexMap = indexmap::IndexMap>; + pub type IndexSet = indexmap::IndexSet>; } diff --git a/src/wasm.rs b/src/wasm.rs index aa50ace9..a7f32459 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -1,140 +1,11 @@ -use crate::ir::types::Type as IrType; -use crate::prelude::*; -use wasm_encoder::{Function, Instruction, ValType}; - -/// representation of a step's function -pub struct StepFunc { - locals: RefCell>, - instructions: RefCell>>, - param_count: u32, -} - -impl StepFunc { - /// creates a new step function, with one paramter - pub fn new() -> Self { - StepFunc { - locals: RefCell::new(vec![]), - instructions: RefCell::new(vec![]), - param_count: 1, - } - } - - /// creates a new step function with the specified amount of paramters. - /// currently only used in testing to validate types - pub fn new_with_param_count(count: usize) -> HQResult { - Ok(StepFunc { - locals: RefCell::new(vec![]), - instructions: RefCell::new(vec![]), - param_count: u32::try_from(count) - .map_err(|_| make_hq_bug!("param count out of bounds"))?, - }) - } - - /// Returns the index of the `n`th local of the specified type in this function, - /// adding some if necessary - pub fn get_local(&self, val_type: ValType, n: u32) -> HQResult { - let existing_count = self - .locals - .borrow() - .iter() - .filter(|ty| **ty == val_type) - .count(); - Ok(u32::try_from(if existing_count < (n as usize) { - { - self.locals - .borrow_mut() - .extend([val_type].repeat(n as usize - existing_count)); - } - self.locals.borrow().len() - 1 - } else { - // TODO: return nth rather than last - self.locals - .borrow() - .iter() - .rposition(|ty| *ty == val_type) - .unwrap() - }) - .map_err(|_| make_hq_bug!("local index was out of bounds"))? - + self.param_count) - } - - pub fn add_instructions(&self, instructions: impl IntoIterator>) { - self.instructions.borrow_mut().extend(instructions); - } - - /// Takes ownership of the function and returns the backing `wasm_encoder` `Function` - pub fn finish(self) -> Function { - let mut func = Function::new_with_locals_types(self.locals.take()); - for instruction in self.instructions.take() { - func.instruction(&instruction); - } - func.instruction(&Instruction::End); - func - } -} - -impl Default for StepFunc { - fn default() -> Self { - Self::new() - } -} - -#[non_exhaustive] -pub enum WasmStringType { - ExternRef, - JsString, -} - -impl Default for WasmStringType { - fn default() -> Self { - Self::ExternRef - } -} - -/// compilation flags -#[non_exhaustive] -#[derive(Default)] -pub struct WasmFlags { - pub string_type: WasmStringType, -} - -pub struct WasmProject { - flags: WasmFlags, - step_funcs: RefCell>, -} - -impl WasmProject { - pub fn new(flags: WasmFlags) -> Self { - WasmProject { - flags, - step_funcs: RefCell::new(vec![]), - } - } - - pub fn flags(&self) -> &WasmFlags { - &self.flags - } - - /// maps a broad IR type to a WASM type - pub fn ir_type_to_wasm(&self, ir_type: IrType) -> HQResult { - Ok(if IrType::Float.contains(ir_type) { - ValType::F64 - } else if IrType::QuasiInt.contains(ir_type) { - ValType::I64 - } else if IrType::String.contains(ir_type) { - ValType::EXTERNREF - } else if IrType::Color.contains(ir_type) { - hq_todo!();//ValType::V128 // f32x4 - } else { - ValType::I64 // NaN boxed value... let's worry about colors later - }) - } - - pub fn add_step_func(&self, step_func: StepFunc) { - self.step_funcs.borrow_mut().push(step_func); - } - - pub fn finish(self) -> HQResult> { - hq_todo!(); - } -} +mod external; +mod flags; +mod func; +mod project; +mod type_registry; + +pub use external::{ExternalEnvironment, ExternalFunctionMap}; +pub use flags::WasmFlags; +pub use func::StepFunc; +pub use project::WasmProject; +pub use type_registry::TypeRegistry; diff --git a/src/wasm/external.rs b/src/wasm/external.rs new file mode 100644 index 00000000..ffd7a57e --- /dev/null +++ b/src/wasm/external.rs @@ -0,0 +1,57 @@ +use super::TypeRegistry; +use crate::prelude::*; +use wasm_encoder::{EntityType, ImportSection, ValType}; + +pub type FunctionMap = IndexMap<(&'static str, &'static str), (Vec, Vec)>; + +#[derive(Clone, Default)] +pub struct ExternalFunctionMap(RefCell); + +impl ExternalFunctionMap { + pub fn new() -> Self { + ExternalFunctionMap(RefCell::new(Default::default())) + } + + pub(crate) fn get_map(&self) -> &RefCell { + &self.0 + } + + /// get the index of the specified function, inserting it if it doesn't exist in the map already. + /// Doesn't check if the provided params/results types match what's already there. + pub fn function_index( + &self, + module: &'static str, + name: &'static str, + params: Vec, + results: Vec, + ) -> HQResult { + self.get_map() + .borrow_mut() + .entry((module, name)) + .or_insert((params, results)); + u32::try_from( + self.get_map() + .borrow() + .get_index_of(&(module, name)) + .ok_or(make_hq_bug!("couldn't find entry in ExternalFunctionMap"))?, + ) + .map_err(|_| make_hq_bug!("external function index out of bounds")) + } + + pub fn finish( + self, + imports: &mut ImportSection, + type_registry: Rc, + ) -> HQResult<()> { + for ((module, name), (params, results)) in self.get_map().take() { + let type_index = type_registry.type_index(params, results)?; + imports.import(module, name, EntityType::Function(type_index)); + } + Ok(()) + } +} + +#[non_exhaustive] +pub enum ExternalEnvironment { + WebBrowser, +} diff --git a/src/wasm/flags.rs b/src/wasm/flags.rs new file mode 100644 index 00000000..0d183c07 --- /dev/null +++ b/src/wasm/flags.rs @@ -0,0 +1,18 @@ +#[non_exhaustive] +pub enum WasmStringType { + ExternRef, + JsString, +} + +impl Default for WasmStringType { + fn default() -> Self { + Self::ExternRef + } +} + +/// compilation flags +#[non_exhaustive] +#[derive(Default)] +pub struct WasmFlags { + pub string_type: WasmStringType, +} diff --git a/src/wasm/func.rs b/src/wasm/func.rs new file mode 100644 index 00000000..2eeab52a --- /dev/null +++ b/src/wasm/func.rs @@ -0,0 +1,96 @@ +use super::{ExternalFunctionMap, TypeRegistry}; +use crate::ir::Type as IrType; +use crate::prelude::*; +use wasm_encoder::{Function, Instruction, ValType}; + +/// representation of a step's function +pub struct StepFunc { + locals: RefCell>, + instructions: RefCell>>, + param_count: u32, + type_registry: Rc, + external_functions: Rc, +} + +impl StepFunc { + pub fn type_registry(&self) -> Rc { + self.type_registry.clone() + } + + pub fn external_functions(&self) -> Rc { + self.external_functions.clone() + } + + /// creates a new step function, with one paramter + pub fn new( + type_registry: Rc, + external_functions: Rc, + ) -> Self { + StepFunc { + locals: RefCell::new(vec![]), + instructions: RefCell::new(vec![]), + param_count: 1, + type_registry, + external_functions, + } + } + + /// creates a new step function with the specified amount of paramters. + /// currently only used in testing to validate types + pub fn new_with_param_count( + count: usize, + type_registry: Rc, + external_functions: Rc, + ) -> HQResult { + Ok(StepFunc { + locals: RefCell::new(vec![]), + instructions: RefCell::new(vec![]), + param_count: u32::try_from(count) + .map_err(|_| make_hq_bug!("param count out of bounds"))?, + type_registry, + external_functions, + }) + } + + /// Returns the index of the `n`th local of the specified type in this function, + /// adding some if necessary + pub fn get_local(&self, val_type: ValType, n: u32) -> HQResult { + let existing_count = self + .locals + .borrow() + .iter() + .filter(|ty| **ty == val_type) + .count(); + Ok(u32::try_from(if existing_count < (n as usize) { + { + self.locals + .borrow_mut() + .extend([val_type].repeat(n as usize - existing_count)); + } + self.locals.borrow().len() - 1 + } else { + // TODO: return nth rather than last + self.locals + .borrow() + .iter() + .rposition(|ty| *ty == val_type) + .unwrap() + }) + .map_err(|_| make_hq_bug!("local index was out of bounds"))? + + self.param_count) + } + + pub fn add_instructions(&self, instructions: impl IntoIterator>) { + self.instructions.borrow_mut().extend(instructions); + } + + /// Takes ownership of the function and returns the backing `wasm_encoder` `Function` + pub fn finish(self) -> Function { + let mut func = Function::new_with_locals_types(self.locals.take()); + for instruction in self.instructions.take() { + func.instruction(&instruction); + } + func.instruction(&Instruction::End); + func + } +} diff --git a/src/wasm/project.rs b/src/wasm/project.rs new file mode 100644 index 00000000..1399650c --- /dev/null +++ b/src/wasm/project.rs @@ -0,0 +1,48 @@ +use super::ExternalEnvironment; +use crate::ir::Type as IrType; +use crate::prelude::*; +use crate::wasm::{StepFunc, WasmFlags}; +use wasm_encoder::ValType; + +pub struct WasmProject { + flags: WasmFlags, + step_funcs: RefCell>, + environment: ExternalEnvironment, +} + +impl WasmProject { + pub fn new(flags: WasmFlags, environment: ExternalEnvironment) -> Self { + WasmProject { + flags, + step_funcs: RefCell::new(vec![]), + environment, + } + } + + pub fn flags(&self) -> &WasmFlags { + &self.flags + } + + /// maps a broad IR type to a WASM type + pub fn ir_type_to_wasm(&self, ir_type: IrType) -> HQResult { + Ok(if IrType::Float.contains(ir_type) { + ValType::F64 + } else if IrType::QuasiInt.contains(ir_type) { + ValType::I64 + } else if IrType::String.contains(ir_type) { + ValType::EXTERNREF + } else if IrType::Color.contains(ir_type) { + hq_todo!(); //ValType::V128 // f32x4 + } else { + ValType::I64 // NaN boxed value... let's worry about colors later + }) + } + + pub fn add_step_func(&self, step_func: StepFunc) { + self.step_funcs.borrow_mut().push(step_func); + } + + pub fn finish(self) -> HQResult> { + hq_todo!(); + } +} diff --git a/src/wasm/type_registry.rs b/src/wasm/type_registry.rs new file mode 100644 index 00000000..0e534f2d --- /dev/null +++ b/src/wasm/type_registry.rs @@ -0,0 +1,37 @@ +use crate::prelude::*; +use wasm_encoder::{TypeSection, ValType}; + +pub type TypeSet = IndexSet<(Vec, Vec)>; + +#[derive(Clone, Default)] +pub struct TypeRegistry(RefCell); + +impl TypeRegistry { + pub fn new() -> Self { + TypeRegistry(RefCell::new(Default::default())) + } + + pub(crate) fn get_set(&self) -> &RefCell { + &self.0 + } + + /// get the index of the specified type, inserting it if it doesn't exist in the set already. + pub fn type_index(&self, params: Vec, results: Vec) -> HQResult { + self.get_set() + .borrow_mut() + .insert((params.clone(), results.clone())); + u32::try_from( + self.get_set() + .borrow() + .get_index_of(&(params, results)) + .ok_or(make_hq_bug!("couldn't find entry in TypeRegistry"))?, + ) + .map_err(|_| make_hq_bug!("type index out of bounds")) + } + + pub fn finish(self, types: &mut TypeSection) { + for (params, results) in self.get_set().take() { + types.ty().function(params, results); + } + } +} From 0146d53cf1c4b92591bb62d4bbf20100b4653033 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Thu, 2 Jan 2025 15:56:47 +0000 Subject: [PATCH 05/98] start implementing IR --- Cargo.toml | 3 + build.rs | 101 +++++++++++----- src/instructions.rs | 1 + src/instructions/looks/say.rs | 7 +- src/instructions/math.rs | 1 + src/instructions/math/number.rs | 26 ++++ src/instructions/operator/add.rs | 7 +- src/instructions/utilities.rs | 94 +++++++------- src/ir.rs | 19 ++- src/ir/blocks.rs | 58 +++++++++ src/ir/context.rs | 11 ++ src/ir/event.rs | 4 + src/ir/irgen.rs | 1 - src/ir/proc.rs | 202 +++++++++++++++++++++++++++++++ src/ir/project.rs | 14 +++ src/ir/step.rs | 20 +++ src/ir/thread.rs | 51 ++++++++ src/ir/types.rs | 3 + src/lib.rs | 1 + src/sb3.rs | 4 +- src/wasm/func.rs | 63 +++++++++- 21 files changed, 601 insertions(+), 90 deletions(-) create mode 100644 src/instructions/math.rs create mode 100644 src/instructions/math/number.rs create mode 100644 src/ir/blocks.rs create mode 100644 src/ir/context.rs create mode 100644 src/ir/event.rs delete mode 100644 src/ir/irgen.rs create mode 100644 src/ir/proc.rs create mode 100644 src/ir/project.rs create mode 100644 src/ir/step.rs create mode 100644 src/ir/thread.rs diff --git a/Cargo.toml b/Cargo.toml index 8b13a4ee..a330b12d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,3 +34,6 @@ crate-type = ["cdylib", "rlib"] [profile.release] lto = true opt-level = "z" + +[build-dependencies] +convert_case = "0.6.0" diff --git a/build.rs b/build.rs index b19540c3..163888d8 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,11 @@ +use convert_case::{Case, Casing}; use std::env; use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; -// I hate to admit this, but much of this was written by chatgpt to speed things up +// I hate to admit this, but a fair bit of this file was written by chatgpt to speed things up // and to allow me to continue to procrastinate about learning how to do i/o stuff in rust. +// But I did write some of it! fn main() { println!("cargo::rerun-if-changed=src/instructions/**/*.rs"); @@ -26,17 +28,21 @@ fn main() { .collect(); if components.len() == 2 && components[1].ends_with(".rs") { + let category = components[0]; + let opcode = components[1].trim_end_matches(".rs"); + println!("src/instructions/{category}/{opcode}.rs"); + let contents = fs::read_to_string( + format!("src/instructions/{category}/{opcode}.rs") + ) + .unwrap(); + let fields = contents.contains("pub struct Fields"); + let fields_name = + format!("{}_{}_fields", category, opcode).to_case(Case::Pascal); paths.push(( - format!( - "{}::{}", - components[0], - components[1].trim_end_matches(".rs") - ), - format!( - "{}_{}", - components[0], - components[1].trim_end_matches(".rs") - ), + format!("{}::{}", category, opcode), + format!("{}_{}", category, opcode,), + fields, + fields_name, )); } } @@ -49,35 +55,68 @@ fn main() { format!( "/// A list of all instructions. #[allow(non_camel_case_types)] + #[derive(Clone, Debug)] pub enum IrOpcode {{ {} }} - /// maps an opcode to its acceptable input types - pub fn acceptable_inputs(opcode: IrOpcode) -> Rc<[crate::ir::Type]> {{ - match opcode {{ - {} + impl IrOpcode {{ + /// maps an opcode to its acceptable input types + pub fn acceptable_inputs(&self) -> Rc<[crate::ir::Type]> {{ + match self {{ + {} + }} }} - }} - /// maps an opcode to its WASM instructions - pub fn wasm(opcode: IrOpcode, step_func: &crate::wasm::StepFunc, inputs: Rc<[crate::ir::Type]>) -> HQResult>> {{ - match opcode {{ - {} + /// maps an opcode to its WASM instructions + pub fn wasm(&self, step_func: &crate::wasm::StepFunc, inputs: Rc<[crate::ir::Type]>) -> HQResult>> {{ + match self {{ + {} + }} }} - }} - - /// maps an opcode to its output type - pub fn output_type(opcode: IrOpcode, inputs: Rc<[crate::ir::Type]>) -> HQResult> {{ - match opcode {{ - {} + + /// maps an opcode to its output type + pub fn output_type(&self, inputs: Rc<[crate::ir::Type]>) -> HQResult> {{ + match self {{ + {} + }} }} }} + + {} ", - paths.iter().map(|(_, id)| id.clone()).collect::>().join(", "), - paths.iter().map(|(path, id)| format!("IrOpcode::{} => {}::acceptable_inputs(),", id, path)).collect::>().join("\n"), - paths.iter().map(|(path, id)| format!("IrOpcode::{} => {}::wasm(step_func, inputs),", id, path)).collect::>().join("\n"), - paths.iter().map(|(path, id)| format!("IrOpcode::{} => {}::output_type(inputs),", id, path)).collect::>().join("\n"), + paths.iter().map(|(_, id, fields, fields_name)| { + if *fields { + format!("{}({})", id.clone(), fields_name.clone()) + } else { + id.clone() + } + }).collect::>().join(", "), + paths.iter().map(|(path, id, fields, _)| { + if *fields { + format!("IrOpcode::{}(_) => {}::acceptable_inputs(),", id, path) + } else { + format!("IrOpcode::{} => {}::acceptable_inputs(),", id, path) + } + }).collect::>().join("\n"), + paths.iter().map(|(path, id, fields, _)| { + if *fields { + format!("IrOpcode::{}(fields) => {}::wasm(step_func, inputs, fields),", id, path) + } else { + format!("IrOpcode::{} => {}::wasm(step_func, inputs),", id, path) + } + }).collect::>().join("\n"), + paths.iter().map(|(path, id, fields, _)| { + if *fields { + format!("IrOpcode::{}(_) => {}::output_type(inputs),", id, path) + } else { + format!("IrOpcode::{} => {}::output_type(inputs),", id, path) + } + }).collect::>().join("\n"), + paths.iter().filter(|(_, _, fields, _)| *fields) + .map(|(path, id, _, fields_name)| + format!("pub use {}::Fields as {};", path, fields_name) + ).collect::>().join("\n"), )) .unwrap(); } diff --git a/src/instructions.rs b/src/instructions.rs index eede4df4..37961ba7 100644 --- a/src/instructions.rs +++ b/src/instructions.rs @@ -5,6 +5,7 @@ use crate::prelude::*; mod looks; mod operator; +mod math; #[macro_use] mod utilities; pub use utilities::{file_block_category, file_block_name, file_opcode}; diff --git a/src/instructions/looks/say.rs b/src/instructions/looks/say.rs index 4fe97266..579b4b02 100644 --- a/src/instructions/looks/say.rs +++ b/src/instructions/looks/say.rs @@ -1,5 +1,5 @@ use crate::instructions::{file_block_category, file_block_name}; -use crate::ir::types::Type as IrType; +use crate::ir::Type as IrType; use crate::prelude::*; use crate::wasm::StepFunc; use wasm_encoder::{Instruction, ValType}; @@ -37,4 +37,7 @@ pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { Ok(None) } -crate::instructions_test!(t); +#[cfg(test)] +mod tests { + crate::instructions_test!{test; t} +} diff --git a/src/instructions/math.rs b/src/instructions/math.rs new file mode 100644 index 00000000..f621f685 --- /dev/null +++ b/src/instructions/math.rs @@ -0,0 +1 @@ +pub mod number; \ No newline at end of file diff --git a/src/instructions/math/number.rs b/src/instructions/math/number.rs new file mode 100644 index 00000000..0e50bbce --- /dev/null +++ b/src/instructions/math/number.rs @@ -0,0 +1,26 @@ +use crate::ir::Type as IrType; +use crate::prelude::*; +use crate::wasm::StepFunc; +use wasm_encoder::{Instruction, ValType}; + +#[derive(Clone, Copy, Debug)] +pub struct Fields(pub f64); + +pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>, fields: &Fields) -> HQResult>> { + Ok(vec![Instruction::F64Const(fields.0)]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([]) +} + +pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { + Ok(Some(IrType::Float)) +} + +#[cfg(test)] +mod tests { + use super::Fields; + crate::instructions_test!{_0;; super::Fields(0.0)} + crate::instructions_test!{_1;; super::Fields(1.0)} +} \ No newline at end of file diff --git a/src/instructions/operator/add.rs b/src/instructions/operator/add.rs index 06f7e952..f52afce7 100644 --- a/src/instructions/operator/add.rs +++ b/src/instructions/operator/add.rs @@ -1,4 +1,4 @@ -use crate::ir::types::Type as IrType; +use crate::ir::Type as IrType; use crate::prelude::*; use crate::wasm::StepFunc; use wasm_encoder::{Instruction, ValType}; @@ -53,4 +53,7 @@ pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { })) } -crate::instructions_test!(t1, t2); +#[cfg(test)] +mod tests { + crate::instructions_test!{test; t1, t2} +} diff --git a/src/instructions/utilities.rs b/src/instructions/utilities.rs index c3a0988b..76bfe882 100644 --- a/src/instructions/utilities.rs +++ b/src/instructions/utilities.rs @@ -40,51 +40,55 @@ pub mod tests { } } -/// generates unit tests for instructions files +/// generates unit tests for instructions files. Takes a optional comma-separated list of arbitrary identifiers +/// corresponding to the number of inputs the block takes, optionally followed by a semicolon and an expression +/// for a sensible default for any fields; if multiple field values need to be tested, the macro can be repeated. +/// Must be used *inside* a `mod test` block, after `instructions_test_setup!(...)`. #[macro_export] macro_rules! instructions_test { - ($($type_arg:ident $(,)?)+) => { - #[cfg(test)] - pub mod tests { - use super::{wasm, output_type, acceptable_inputs}; - use $crate::prelude::*; - use $crate::ir::Type as IrType; - use wasm_encoder::ValType; - - macro_rules! ident_as_irtype { - ( $_:ident ) => { IrType }; - } + {$module:ident; $($type_arg:ident $(,)?)* $(; $fields:expr)?} => { + mod $module { + use super::super::{wasm, output_type, acceptable_inputs}; + use $crate::prelude::*; + use $crate::ir::Type as IrType; + use wasm_encoder::ValType; + + macro_rules! ident_as_irtype { + ( $_:ident ) => { IrType }; + } - fn types_iter() -> impl Iterator { - // we need to collect this iterator into a Vec because it doesn't implement clone for some reason, - // which makes itertools angry - $(let $type_arg = IrType::flags().map(|(_, ty)| *ty).collect::>();)+ - itertools::iproduct!($($type_arg,)+).filter(|($($type_arg,)+)| { - for (i, input) in [$($type_arg,)+].into_iter().enumerate() { - // invalid input types should be handled by a wrapper function somewhere - // so we won't test those here. - if !acceptable_inputs()[i].contains(*input) { - return false; - } + fn types_iter() -> impl Iterator { + // we need to collect this iterator into a Vec because it doesn't implement clone for some reason, + // which makes itertools angry + $(let $type_arg = IrType::flags().map(|(_, ty)| *ty).collect::>();)* + itertools::iproduct!($($type_arg,)*).filter(|($($type_arg,)*)| { + let types: &[&IrType] = &[$($type_arg,)*]; + for (i, input) in (*types).into_iter().enumerate() { + // invalid input types should be handled by a wrapper function somewhere + // so we won't test those here. + if !acceptable_inputs()[i].contains(**input) { + return false; } - true - }) - } + } + true + }) + } #[test] fn output_type_fails_when_wasm_fails() { use $crate::wasm::{StepFunc, TypeRegistry, ExternalFunctionMap}; - for ($($type_arg,)+) in types_iter() { - let output_type_result = output_type(Rc::new([$($type_arg,)+])); + for ($($type_arg,)*) in types_iter() { + let output_type_result = output_type(Rc::new([$($type_arg,)*])); let type_registry = Rc::new(TypeRegistry::new()); let external_functions = Rc::new(ExternalFunctionMap::new()); - let step_func = StepFunc::new_with_param_count([$($type_arg,)+].len(), type_registry.clone(), external_functions.clone()).unwrap(); - let wasm_result = wasm(&step_func, Rc::new([$($type_arg,)+])); + let types: &[IrType] = &[$($type_arg,)*]; + let step_func = StepFunc::new_with_param_count(types.len(), type_registry.clone(), external_functions.clone()).unwrap(); + let wasm_result = wasm(&step_func, Rc::new([$($type_arg,)*]), $(&$fields)?); match (output_type_result.clone(), wasm_result.clone()) { - (Err(..), Ok(..)) | (Ok(..), Err(..)) => panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)+), output_type_result, wasm_result), + (Err(..), Ok(..)) | (Ok(..), Err(..)) => panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)*), output_type_result, wasm_result), (Err(HQError { err_type: e1, .. }), Err(HQError { err_type: e2, .. })) => { if e1 != e2 { - panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)+), output_type_result, wasm_result); + panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)*), output_type_result, wasm_result); } } _ => continue, @@ -100,8 +104,8 @@ macro_rules! instructions_test { use $crate::wasm::{StepFunc, TypeRegistry, ExternalFunctionMap}; use $crate::prelude::Rc; - for ($($type_arg,)+) in types_iter() { - let output_type = match output_type(Rc::new([$($type_arg,)+])) { + for ($($type_arg,)*) in types_iter() { + let output_type = match output_type(Rc::new([$($type_arg,)*])) { Ok(a) => a, Err(_) => { println!("skipping failed output_type"); @@ -110,15 +114,16 @@ macro_rules! instructions_test { }; let type_registry = Rc::new(TypeRegistry::new()); let external_functions = Rc::new(ExternalFunctionMap::new()); - let step_func = StepFunc::new_with_param_count([$($type_arg,)+].len(), type_registry.clone(), external_functions.clone())?; - let wasm = match wasm(&step_func, Rc::new([$($type_arg,)+])) { + let types: &[IrType] = &[$($type_arg,)*]; + let step_func = StepFunc::new_with_param_count(types.len(), type_registry.clone(), external_functions.clone())?; + let wasm = match wasm(&step_func, Rc::new([$($type_arg,)*]), $(&$fields)?) { Ok(a) => a, Err(_) => { println!("skipping failed wasm"); continue; } }; - for (i, _) in [$($type_arg,)+].iter().enumerate() { + for (i, _) in types.iter().enumerate() { step_func.add_instructions([Instruction::LocalGet(i.try_into().unwrap())]) } step_func.add_instructions(wasm); @@ -133,7 +138,7 @@ macro_rules! instructions_test { Rc::unwrap_or_clone(external_functions).finish(&mut imports, type_registry.clone())?; let mut types = TypeSection::new(); - let params = [$($type_arg,)+].into_iter().map(|ty| wasm_proj.ir_type_to_wasm(ty)).collect::>>()?; + let params = [$($type_arg,)*].into_iter().map(|ty| wasm_proj.ir_type_to_wasm(ty)).collect::>>()?; let results = match output_type { Some(output) => vec![wasm_proj.ir_type_to_wasm(output)?], None => vec![], @@ -154,7 +159,7 @@ macro_rules! instructions_test { let wasm_bytes = module.finish(); - wasmparser::validate(&wasm_bytes).map_err(|err| make_hq_bug!("invalid wasm module with types {:?}. Original error message: {}", ($($type_arg,)+), err.message()))?; + wasmparser::validate(&wasm_bytes).map_err(|err| make_hq_bug!("invalid wasm module with types {:?}. Original error message: {}", ($($type_arg,)*), err.message()))?; } Ok(()) } @@ -173,11 +178,11 @@ macro_rules! instructions_test { use std::path::{Path, PathBuf}; use std::fs; - for ($($type_arg,)+) in types_iter() { + for ($($type_arg,)*) in types_iter() { let type_registry = Rc::new(TypeRegistry::new()); let external_functions = Rc::new(ExternalFunctionMap::new()); let step_func = StepFunc::new(type_registry.clone(), external_functions.clone()); - if wasm(&step_func, Rc::new([$($type_arg,)+])).is_err() { + if wasm(&step_func, Rc::new([$($type_arg,)*]), $(&$fields)?).is_err() { println!("skipping failed wasm"); continue; }; @@ -188,7 +193,8 @@ macro_rules! instructions_test { } else { wasm_to_js_type(results[1]) }; - let ins = [$(stringify!($type_arg),)+].into_iter().enumerate().map(|(i, t)| { + let type_strs: &[&str] = &[$(stringify!($type_arg),)*]; + let ins = type_strs.into_iter().enumerate().map(|(i, t)| { format!( "{}: {}", t, @@ -196,7 +202,7 @@ macro_rules! instructions_test { $crate::wasm::WasmProject::new( Default::default(), crate::wasm::ExternalEnvironment::WebBrowser - ).ir_type_to_wasm([$($type_arg,)+][i]).unwrap() + ).ir_type_to_wasm([$($type_arg,)*][i]).unwrap() ) ) }).collect::>().join(", "); @@ -207,7 +213,7 @@ macro_rules! instructions_test { let test_string = format!("function _({ins}): {out} {{ {func} return {name}{ts}; - }}", ins=ins, out=out, func=func_string, name=name, ts=stringify!(($($type_arg,)+))); + }}", ins=ins, out=out, func=func_string, name=name, ts=stringify!(($($type_arg,)*))); println!("{}", test_string.clone()); Some(test_string.as_str().as_bytes().into_iter().map(|&u| u).collect::>()) }, diff --git a/src/ir.rs b/src/ir.rs index 3a695266..725d9cd9 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -1,7 +1,16 @@ -pub mod irgen; -pub mod types; +mod blocks; +mod context; +mod event; +mod proc; +mod project; +mod step; +mod thread; +mod types; -#[doc(inline)] -pub use irgen::IrProject; -#[doc(inline)] +pub use context::{TargetContext, StepContext}; +pub use event::Event; +pub use proc::{Proc, ProcedureContext, ProcMap, ProcRegistry}; +pub use project::IrProject; +pub use step::Step; +pub use thread::Thread; pub use types::Type; diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs new file mode 100644 index 00000000..40ad7ef0 --- /dev/null +++ b/src/ir/blocks.rs @@ -0,0 +1,58 @@ +use crate::instructions::{IrOpcode, MathNumberFields}; +use crate::prelude::*; +use crate::sb3::{Block, BlockArray, BlockInfo, BlockMap, BlockOpcode}; + +impl BlockOpcode { + fn from_block(block: &Block, blocks: &BlockMap) -> HQResult { + match block { + Block::Normal { block_info, .. } => BlockOpcode::from_normal_block(block_info, blocks), + Block::Special(block_array) => BlockOpcode::from_special_block(block_array, blocks), + } + } + + fn from_normal_block(block_info: &BlockInfo, blocks: &BlockMap) -> HQResult { + hq_todo!() + } + + fn from_special_block(block_array: &BlockArray, blocks: &BlockMap) -> HQResult { + Ok(match block_array { + BlockArray::NumberOrAngle(ty, value) => match ty { + 4 => IrOpcode::math_number(MathNumberFields(*value)), + //5 => IrOpcode::math_positive_number { NUM: *value as i32 }, + //6 => IrOpcode::math_whole_number { NUM: *value as i32 }, + //7 => IrOpcode::math_integer { NUM: *value as i32 }, + //8 => IrOpcode::math_angle { NUM: *value as i32 }, + _ => hq_bad_proj!("bad project json (block array of type ({}, f64))", ty), + }, + BlockArray::ColorOrString(ty, value) => match ty { + /*4 => IrOpcode::math_number { + NUM: value.parse().map_err(|_| make_hq_bug!(""))?, + },*/ + /*5 => IrOpcode::math_positive_number { + NUM: value.parse().map_err(|_| make_hq_bug!(""))?, + }, + 6 => IrOpcode::math_whole_number { + NUM: value.parse().map_err(|_| make_hq_bug!(""))?, + }, + 7 => IrOpcode::math_integer { + NUM: value.parse().map_err(|_| make_hq_bug!(""))?, + }, + 8 => IrOpcode::math_angle { + NUM: value.parse().map_err(|_| make_hq_bug!(""))?, + }, + 9 => hq_todo!(""), + 10 => IrOpcode::text { + TEXT: value.to_string(), + },*/ + _ => hq_bad_proj!("bad project json (block array of type ({}, string))", ty), + }, + BlockArray::Broadcast(ty, _name, id) | BlockArray::VariableOrList(ty, _name, id, _, _) => match ty { + /*12 => IrOpcode::data_variable { + VARIABLE: id.to_string(), + assume_type: None, + },*/ + _ => hq_todo!(""), + }, + }) + } +} diff --git a/src/ir/context.rs b/src/ir/context.rs new file mode 100644 index 00000000..bf0e792b --- /dev/null +++ b/src/ir/context.rs @@ -0,0 +1,11 @@ +use super::ProcedureContext; + +#[derive(Debug, Clone, Copy)] +pub struct TargetContext { + pub target: u32, +} +#[derive(Debug, Clone)] +pub struct StepContext { + pub target_context: TargetContext, + pub proc_context: Option, +} \ No newline at end of file diff --git a/src/ir/event.rs b/src/ir/event.rs new file mode 100644 index 00000000..2287176f --- /dev/null +++ b/src/ir/event.rs @@ -0,0 +1,4 @@ +#[derive(Copy, Clone)] +pub enum Event { + FlagCLicked, +} \ No newline at end of file diff --git a/src/ir/irgen.rs b/src/ir/irgen.rs deleted file mode 100644 index 5a66b036..00000000 --- a/src/ir/irgen.rs +++ /dev/null @@ -1 +0,0 @@ -pub struct IrProject {} diff --git a/src/ir/proc.rs b/src/ir/proc.rs new file mode 100644 index 00000000..e33a0d70 --- /dev/null +++ b/src/ir/proc.rs @@ -0,0 +1,202 @@ +use super::{Step, StepContext, TargetContext, Type as IrType}; +use crate::prelude::*; +use crate::sb3::{Block, BlockMap, BlockOpcode}; +use lazy_regex::{lazy_regex, Lazy}; +use regex::Regex; + +#[derive(Clone, Debug)] +pub struct ProcedureContext { + arg_ids: Box<[String]>, + arg_types: Box<[IrType]>, + warp: bool, + target_context: Box, +} + +impl ProcedureContext { + pub fn arg_ids(&self) -> &[String] { + self.arg_ids.borrow() + } + + pub fn arg_types(&self) -> &[IrType] { + self.arg_types.borrow() + } + + pub fn warp(&self) -> bool { + self.warp + } +} + +#[derive(Clone)] +pub struct Proc { + first_step: Box, + context: ProcedureContext, +} + +impl Proc { + pub fn first_step(&self) -> &Step { + self.first_step.borrow() + } + + pub fn context(&self) -> &ProcedureContext { + &self.context + } +} + +static ARG_REGEX: Lazy = lazy_regex!(r#"[^\\]%[nbs]"#); + +fn arg_types_from_proccode(proccode: String) -> Result, HQError> { + // https://github.com/scratchfoundation/scratch-blocks/blob/abbfe93136fef57fdfb9a077198b0bc64726f012/blocks_vertical/procedures.js#L207-L215 + (*ARG_REGEX) + .find_iter(proccode.as_str()) + .map(|s| s.as_str().to_string().trim().to_string()) + .filter(|s| s.as_str().starts_with('%')) + .map(|s| s[..2].to_string()) + .map(|s| { + Ok(match s.as_str() { + "%n" => IrType::Number, + "%s" => IrType::String, + "%b" => IrType::Boolean, + other => hq_bug!("invalid proccode arg \"{other}\" found"), + }) + }) + .collect() +} + +impl Proc { + pub fn from_proccode( + proccode: String, + blocks: &BlockMap, + target_context: TargetContext, + expect_warp: bool, + ) -> HQResult { + let arg_types = arg_types_from_proccode(proccode.clone())?; + let Some(prototype_block) = blocks.values().find(|block| { + let Some(info) = block.block_info() else { + return false; + }; + info.opcode == BlockOpcode::procedures_prototype + && (match info.mutation.mutations.get("proccode") { + None => false, + Some(p) => { + if let serde_json::Value::String(ref s) = p { + *s == proccode + } else { + false + } + } + }) + }) else { + hq_bad_proj!("no prototype found for {proccode} in") + }; + let Some(def_block) = blocks.get( + &prototype_block + .block_info() + .unwrap() + .parent + .clone() + .ok_or(make_hq_bad_proj!("prototype block without parent"))?, + ) else { + hq_bad_proj!("no definition found for {proccode}") + }; + if let Some(warp_val) = prototype_block + .block_info() + .unwrap() + .mutation + .mutations + .get("warp") + { + let warp = match warp_val { + serde_json::Value::Bool(w) => *w, + serde_json::Value::String(wstr) => match wstr.as_str() { + "true" => true, + "false" => false, + _ => hq_bad_proj!("unexpected string for warp mutation for {proccode}"), + }, + _ => hq_bad_proj!("bad type for warp mutation for {proccode}"), + }; + if warp != expect_warp { + hq_bad_proj!("proc call warp does not equal definition warp for {proccode}") + } + } else { + hq_bad_proj!("missing warp mutation on procedures_definition for {proccode}") + }; + let arg_ids = match prototype_block + .block_info() + .unwrap() + .mutation + .mutations + .get("argumentids") + .ok_or(make_hq_bad_proj!( + "missing argumentids mutation for {proccode}" + ))? { + serde_json::Value::Array(values) => values + .iter() + .map(|val| match val { + serde_json::Value::String(s) => Ok(s.clone()), + _ => hq_bad_proj!("non-string argumentids member in {proccode}"), + }) + .collect::>>()?, + _ => hq_bad_proj!("non-array argumentids for {proccode}"), + }; + let context = ProcedureContext { + warp: expect_warp, + arg_types, + arg_ids, + target_context: Box::new(target_context), + }; + let step_context = StepContext { + target_context, + proc_context: Some(context.clone()), + }; + let first_step = match &def_block.block_info().unwrap().next { + Some(next_id) => Step::from_block( + blocks + .get(next_id) + .ok_or(make_hq_bad_proj!("specified next block does not exist"))?, + blocks, + step_context, + )?, + None => Step::new(step_context, Box::new([])), + }; + Ok(Proc { + first_step: Box::new(first_step), + context + }) + } +} + +pub type ProcMap = IndexMap>; + +#[derive(Clone, Default)] +pub struct ProcRegistry(RefCell); + +impl ProcRegistry { + pub fn new() -> Self { + ProcRegistry(RefCell::new(Default::default())) + } + + pub(crate) fn get_map(&self) -> &RefCell { + &self.0 + } + + /// get the `Proc` for the specified proccode, creating it if it doesn't already exist + pub fn proc( + &self, + proccode: String, + blocks: &BlockMap, + target_context: TargetContext, + expect_warp: bool, + ) -> HQResult> { + Ok(Rc::clone( + self.get_map() + .borrow_mut() + .entry(proccode.clone()) + .or_insert(Rc::new(Proc::from_proccode( + proccode, + blocks, + target_context, + expect_warp, + )?)), + )) + } +} diff --git a/src/ir/project.rs b/src/ir/project.rs new file mode 100644 index 00000000..866b4881 --- /dev/null +++ b/src/ir/project.rs @@ -0,0 +1,14 @@ +use crate::prelude::*; +use crate::sb3::Sb3Project; + +pub struct IrProject { + +} + +impl TryFrom for IrProject { + type Error = HQError; + + fn try_from(sb3: Sb3Project) -> HQResult { + hq_todo!() + } +} \ No newline at end of file diff --git a/src/ir/step.rs b/src/ir/step.rs new file mode 100644 index 00000000..daf64bc4 --- /dev/null +++ b/src/ir/step.rs @@ -0,0 +1,20 @@ +use super::StepContext; +use crate::instructions::IrOpcode; +use crate::prelude::*; +use crate::sb3::{Block, BlockMap, BlockOpcode}; + +#[derive(Clone)] +pub struct Step { + context: StepContext, + opcodes: Box<[IrOpcode]>, +} + +impl Step { + pub fn new(context: StepContext, opcodes: Box<[IrOpcode]>) -> Self { + Step { context, opcodes } + } + + pub fn from_block(block: &Block, blocks: &BlockMap, context: StepContext) -> HQResult { + hq_todo!() + } +} diff --git a/src/ir/thread.rs b/src/ir/thread.rs new file mode 100644 index 00000000..bbdf3283 --- /dev/null +++ b/src/ir/thread.rs @@ -0,0 +1,51 @@ +use super::{Event, Step, StepContext, TargetContext}; +use crate::prelude::*; +use crate::sb3::{Block, BlockMap, BlockOpcode}; + +#[derive(Clone)] +pub struct Thread { + event: Event, + first_step: Box, +} + +impl Thread { + pub fn event(&self) -> Event { + self.event + } + + pub fn first_step(&self) -> &Step { + self.first_step.borrow() + } + + /// tries to construct a thread from a top-level block. + /// Returns Ok(None) if the top-level block is not a valid event or if there is no next block. + pub fn try_from_top_block(block: &Block, blocks: &BlockMap, target_context: TargetContext) -> HQResult> { + let block_info = block + .block_info() + .ok_or(make_hq_bug!("top-level block is a special block"))?; + let event = match block_info.opcode { + BlockOpcode::event_whenflagclicked => Event::FlagCLicked, + BlockOpcode::event_whenbackdropswitchesto + | BlockOpcode::event_whenbroadcastreceived + | BlockOpcode::event_whengreaterthan + | BlockOpcode::event_whenkeypressed + | BlockOpcode::event_whenstageclicked + | BlockOpcode::event_whenthisspriteclicked + | BlockOpcode::event_whentouchingobject => { + hq_todo!("unimplemented event {:?}", block_info.opcode) + } + _ => return Ok(None), + }; + let next = blocks.get(match &block_info.next { + Some(next) => next, + None => return Ok(None), + }).ok_or(make_hq_bug!("block not found in BlockMap"))?; + Ok(Some(Thread { + event, + first_step: Box::new(Step::from_block(next, blocks, StepContext { + target_context, + proc_context: None, + })?) + })) + } +} diff --git a/src/ir/types.rs b/src/ir/types.rs index edb0f048..8f3ec654 100644 --- a/src/ir/types.rs +++ b/src/ir/types.rs @@ -1,3 +1,4 @@ +use crate::prelude::*; use bitmask_enum::bitmask; /// a bitmask of possible IR types @@ -63,3 +64,5 @@ pub enum Type { Color, } + +pub type TypeStack = Vec; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 4b653832..cd053a70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,7 @@ pub mod prelude { pub use alloc::rc::Rc; pub use alloc::string::{String, ToString}; pub use alloc::vec::Vec; + pub use core::borrow::Borrow; pub use core::cell::RefCell; pub use core::fmt; diff --git a/src/sb3.rs b/src/sb3.rs index 04f52dd9..117a2fe9 100644 --- a/src/sb3.rs +++ b/src/sb3.rs @@ -355,6 +355,8 @@ pub enum VariableInfo { LocalVar(String, VarVal), } +pub type BlockMap = BTreeMap; + /// A target (sprite or stage) #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] @@ -365,7 +367,7 @@ pub struct Target { pub lists: BTreeMap)>, #[serde(default)] pub broadcasts: BTreeMap, - pub blocks: BTreeMap, + pub blocks: BlockMap, pub comments: BTreeMap, pub current_costume: u32, pub costumes: Vec, diff --git a/src/wasm/func.rs b/src/wasm/func.rs index 2eeab52a..2fb0c6d4 100644 --- a/src/wasm/func.rs +++ b/src/wasm/func.rs @@ -53,8 +53,11 @@ impl StepFunc { } /// Returns the index of the `n`th local of the specified type in this function, - /// adding some if necessary + /// adding some if necessary. `n` should be greater than 0. pub fn get_local(&self, val_type: ValType, n: u32) -> HQResult { + if n == 0 { + hq_bug!("can't have a 0 amount of locals; n should be >0 for get_local") + } let existing_count = self .locals .borrow() @@ -69,12 +72,17 @@ impl StepFunc { } self.locals.borrow().len() - 1 } else { - // TODO: return nth rather than last self.locals .borrow() .iter() - .rposition(|ty| *ty == val_type) - .unwrap() + .enumerate() + .filter(|(_, ty)| **ty == val_type) + .map(|(i, _)| i) + .nth(n as usize - 1) + .ok_or(make_hq_bug!( + "couldn't find nth local of type {:?}", + val_type + ))? }) .map_err(|_| make_hq_bug!("local index was out of bounds"))? + self.param_count) @@ -94,3 +102,50 @@ impl StepFunc { func } } + +#[cfg(test)] +mod tests { + use super::StepFunc; + use wasm_encoder::ValType; + + #[test] + fn get_local_works_with_valid_inputs_with_1_param() { + let func = StepFunc::new(Default::default(), Default::default()); + assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 1); + + assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 1); + + assert_eq!(func.get_local(ValType::F64, 1).unwrap(), 2); + assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 1); + + assert_eq!(func.get_local(ValType::I32, 2).unwrap(), 3); + assert_eq!(func.get_local(ValType::F64, 1).unwrap(), 2); + assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 1); + + assert_eq!(func.get_local(ValType::F64, 4).unwrap(), 6); + } + + #[test] + fn get_local_fails_when_n_is_0() { + let func = StepFunc::new(Default::default(), Default::default()); + assert!(func.get_local(ValType::I32, 0).is_err()); + } + + #[test] + fn get_local_works_with_valid_inputs_with_3_params() { + let func = + StepFunc::new_with_param_count(3, Default::default(), Default::default()).unwrap(); + assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 3); + + assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 3); + + assert_eq!(func.get_local(ValType::F64, 1).unwrap(), 4); + assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 3); + + assert_eq!(func.get_local(ValType::I32, 2).unwrap(), 5); + assert_eq!(func.get_local(ValType::F64, 1).unwrap(), 4); + assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 3); + + assert_eq!(func.get_local(ValType::F64, 4).unwrap(), 8); + } +} From 5462a0c1370b54a7ab8bef612f26225631b98c6b Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Fri, 3 Jan 2025 12:09:23 +0000 Subject: [PATCH 06/98] format and add remaining primitive number blocks --- build.rs | 20 ++++---- src/instructions.rs | 2 +- src/instructions/looks/say.rs | 32 ++++++------- src/instructions/math.rs | 5 +- src/instructions/math/integer.rs | 30 ++++++++++++ src/instructions/math/number.rs | 29 ++++++++---- src/instructions/math/positive_number.rs | 33 +++++++++++++ src/instructions/math/whole_number.rs | 30 ++++++++++++ src/instructions/operator/add.rs | 5 +- src/instructions/utilities.rs | 36 +++++++------- src/ir.rs | 6 +-- src/ir/blocks.rs | 60 ++++++++++++------------ src/ir/context.rs | 2 +- src/ir/event.rs | 2 +- src/ir/proc.rs | 18 +++---- src/ir/project.rs | 8 ++-- src/ir/step.rs | 12 ++++- src/ir/thread.rs | 30 ++++++++---- src/ir/types.rs | 2 +- src/wasm/external.rs | 1 + src/wasm/func.rs | 1 - src/wasm/project.rs | 4 ++ 22 files changed, 243 insertions(+), 125 deletions(-) create mode 100644 src/instructions/math/integer.rs create mode 100644 src/instructions/math/positive_number.rs create mode 100644 src/instructions/math/whole_number.rs diff --git a/build.rs b/build.rs index 163888d8..32cb8f96 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,7 @@ use convert_case::{Case, Casing}; use std::env; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; // I hate to admit this, but a fair bit of this file was written by chatgpt to speed things up // and to allow me to continue to procrastinate about learning how to do i/o stuff in rust. @@ -31,10 +31,9 @@ fn main() { let category = components[0]; let opcode = components[1].trim_end_matches(".rs"); println!("src/instructions/{category}/{opcode}.rs"); - let contents = fs::read_to_string( - format!("src/instructions/{category}/{opcode}.rs") - ) - .unwrap(); + let contents = + fs::read_to_string(format!("src/instructions/{category}/{opcode}.rs")) + .unwrap(); let fields = contents.contains("pub struct Fields"); let fields_name = format!("{}_{}_fields", category, opcode).to_case(Case::Pascal); @@ -82,8 +81,11 @@ fn main() { }} }} }} - - {} + pub mod fields {{ + use super::*; + {} + }} + pub use fields::*; ", paths.iter().map(|(_, id, fields, fields_name)| { if *fields { @@ -108,13 +110,13 @@ fn main() { }).collect::>().join("\n"), paths.iter().map(|(path, id, fields, _)| { if *fields { - format!("IrOpcode::{}(_) => {}::output_type(inputs),", id, path) + format!("IrOpcode::{}(fields) => {}::output_type(inputs, fields),", id, path) } else { format!("IrOpcode::{} => {}::output_type(inputs),", id, path) } }).collect::>().join("\n"), paths.iter().filter(|(_, _, fields, _)| *fields) - .map(|(path, id, _, fields_name)| + .map(|(path, _, _, fields_name)| format!("pub use {}::Fields as {};", path, fields_name) ).collect::>().join("\n"), )) diff --git a/src/instructions.rs b/src/instructions.rs index 37961ba7..eadb4dd4 100644 --- a/src/instructions.rs +++ b/src/instructions.rs @@ -4,8 +4,8 @@ use crate::prelude::*; mod looks; -mod operator; mod math; +mod operator; #[macro_use] mod utilities; pub use utilities::{file_block_category, file_block_name, file_opcode}; diff --git a/src/instructions/looks/say.rs b/src/instructions/looks/say.rs index 579b4b02..7c21f1ce 100644 --- a/src/instructions/looks/say.rs +++ b/src/instructions/looks/say.rs @@ -1,4 +1,3 @@ -use crate::instructions::{file_block_category, file_block_name}; use crate::ir::Type as IrType; use crate::prelude::*; use crate::wasm::StepFunc; @@ -6,21 +5,21 @@ use wasm_encoder::{Instruction, ValType}; pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult>> { Ok(if IrType::QuasiInt.contains(inputs[0]) { - let func_index = func.external_functions().function_index( - "looks", - "say_int", - vec![ValType::I64], - vec![], - )?; - vec![Instruction::Call(func_index)] + let func_index = func.external_functions().function_index( + "looks", + "say_int", + vec![ValType::I64], + vec![], + )?; + vec![Instruction::Call(func_index)] } else if IrType::Float.contains(inputs[0]) { let func_index = func.external_functions().function_index( - "looks", - "say_float", - vec![ValType::F64], - vec![], - )?; - vec![Instruction::Call(func_index)] + "looks", + "say_float", + vec![ValType::F64], + vec![], + )?; + vec![Instruction::Call(func_index)] } else { hq_todo!() }) @@ -37,7 +36,4 @@ pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { Ok(None) } -#[cfg(test)] -mod tests { - crate::instructions_test!{test; t} -} +crate::instructions_test! {tests; t} diff --git a/src/instructions/math.rs b/src/instructions/math.rs index f621f685..6564b130 100644 --- a/src/instructions/math.rs +++ b/src/instructions/math.rs @@ -1 +1,4 @@ -pub mod number; \ No newline at end of file +pub mod integer; +pub mod number; +pub mod positive_number; +pub mod whole_number; diff --git a/src/instructions/math/integer.rs b/src/instructions/math/integer.rs new file mode 100644 index 00000000..379850df --- /dev/null +++ b/src/instructions/math/integer.rs @@ -0,0 +1,30 @@ +use crate::ir::Type as IrType; +use crate::prelude::*; +use crate::wasm::StepFunc; +use wasm_encoder::Instruction; + +#[derive(Clone, Copy, Debug)] +pub struct Fields(pub i64); + +pub fn wasm( + _func: &StepFunc, + _inputs: Rc<[IrType]>, + fields: &Fields, +) -> HQResult>> { + Ok(vec![Instruction::I64Const(fields.0)]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, &Fields(val): &Fields) -> HQResult> { + Ok(Some(match val { + 0 => IrType::IntZero, + pos if pos > 0 => IrType::IntPos, + neg if neg < 0 => IrType::IntNeg, + _ => unreachable!(), + })) +} + +crate::instructions_test! {tests;; super::Fields(0)} diff --git a/src/instructions/math/number.rs b/src/instructions/math/number.rs index 0e50bbce..4bd460ee 100644 --- a/src/instructions/math/number.rs +++ b/src/instructions/math/number.rs @@ -1,12 +1,16 @@ use crate::ir::Type as IrType; use crate::prelude::*; use crate::wasm::StepFunc; -use wasm_encoder::{Instruction, ValType}; +use wasm_encoder::Instruction; #[derive(Clone, Copy, Debug)] pub struct Fields(pub f64); -pub fn wasm(_func: &StepFunc, _inputs: Rc<[IrType]>, fields: &Fields) -> HQResult>> { +pub fn wasm( + _func: &StepFunc, + _inputs: Rc<[IrType]>, + fields: &Fields, +) -> HQResult>> { Ok(vec![Instruction::F64Const(fields.0)]) } @@ -14,13 +18,18 @@ pub fn acceptable_inputs() -> Rc<[IrType]> { Rc::new([]) } -pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { - Ok(Some(IrType::Float)) +pub fn output_type(_inputs: Rc<[IrType]>, &Fields(val): &Fields) -> HQResult> { + Ok(Some(match val { + 0.0 => IrType::FloatZero, + f64::INFINITY => IrType::FloatPosInf, + f64::NEG_INFINITY => IrType::FloatNegInf, + nan if f64::is_nan(nan) => IrType::FloatNan, + int if int % 1.0 == 0.0 && int > 0.0 => IrType::FloatPosInt, + int if int % 1.0 == 0.0 && int < 0.0 => IrType::FloatNegInt, + frac if frac > 0.0 => IrType::FloatPosFrac, + frac if frac < 0.0 => IrType::FloatNegFrac, + _ => unreachable!(), + })) } -#[cfg(test)] -mod tests { - use super::Fields; - crate::instructions_test!{_0;; super::Fields(0.0)} - crate::instructions_test!{_1;; super::Fields(1.0)} -} \ No newline at end of file +crate::instructions_test! {tests;; super::Fields(0.0)} diff --git a/src/instructions/math/positive_number.rs b/src/instructions/math/positive_number.rs new file mode 100644 index 00000000..eaa8e60b --- /dev/null +++ b/src/instructions/math/positive_number.rs @@ -0,0 +1,33 @@ +use crate::ir::Type as IrType; +use crate::prelude::*; +use crate::wasm::StepFunc; +use wasm_encoder::Instruction; + +#[derive(Clone, Copy, Debug)] +pub struct Fields(pub f64); + +pub fn wasm( + _func: &StepFunc, + _inputs: Rc<[IrType]>, + fields: &Fields, +) -> HQResult>> { + Ok(vec![Instruction::F64Const(fields.0)]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, &Fields(val): &Fields) -> HQResult> { + Ok(Some(match val { + 0.0 => IrType::FloatZero, + f64::INFINITY => IrType::FloatPosInf, + nan if f64::is_nan(nan) => IrType::FloatNan, + int if int % 1.0 == 0.0 && int > 0.0 => IrType::FloatPosInt, + frac if frac > 0.0 => IrType::FloatPosFrac, + neg if neg < 0.0 => hq_bad_proj!("negative number in math_positive_number"), + _ => unreachable!(), + })) +} + +crate::instructions_test! {tests;; super::Fields(0.0)} diff --git a/src/instructions/math/whole_number.rs b/src/instructions/math/whole_number.rs new file mode 100644 index 00000000..d2364bc7 --- /dev/null +++ b/src/instructions/math/whole_number.rs @@ -0,0 +1,30 @@ +use crate::ir::Type as IrType; +use crate::prelude::*; +use crate::wasm::StepFunc; +use wasm_encoder::Instruction; + +#[derive(Clone, Copy, Debug)] +pub struct Fields(pub i64); + +pub fn wasm( + _func: &StepFunc, + _inputs: Rc<[IrType]>, + fields: &Fields, +) -> HQResult>> { + Ok(vec![Instruction::I64Const(fields.0)]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, &Fields(val): &Fields) -> HQResult> { + Ok(Some(match val { + 0 => IrType::IntZero, + pos if pos > 0 => IrType::IntPos, + neg if neg < 0 => hq_bad_proj!("negative number in math_whole_number"), + _ => unreachable!(), + })) +} + +crate::instructions_test! {tests;; super::Fields(0)} diff --git a/src/instructions/operator/add.rs b/src/instructions/operator/add.rs index f52afce7..bf399a63 100644 --- a/src/instructions/operator/add.rs +++ b/src/instructions/operator/add.rs @@ -53,7 +53,4 @@ pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { })) } -#[cfg(test)] -mod tests { - crate::instructions_test!{test; t1, t2} -} +crate::instructions_test! {tests; t1, t2} diff --git a/src/instructions/utilities.rs b/src/instructions/utilities.rs index 76bfe882..c50a1a98 100644 --- a/src/instructions/utilities.rs +++ b/src/instructions/utilities.rs @@ -1,5 +1,3 @@ -use crate::prelude::*; - mod file_opcode { //! instruction module paths look something like //! hyperquark::instructions::category::block @@ -40,19 +38,20 @@ pub mod tests { } } -/// generates unit tests for instructions files. Takes a optional comma-separated list of arbitrary identifiers +/// generates unit tests for instructions files. Takes a module name, followed by a semicolon, followed by an optional comma-separated list of arbitrary identifiers /// corresponding to the number of inputs the block takes, optionally followed by a semicolon and an expression /// for a sensible default for any fields; if multiple field values need to be tested, the macro can be repeated. -/// Must be used *inside* a `mod test` block, after `instructions_test_setup!(...)`. #[macro_export] macro_rules! instructions_test { {$module:ident; $($type_arg:ident $(,)?)* $(; $fields:expr)?} => { + #[cfg(test)] mod $module { - use super::super::{wasm, output_type, acceptable_inputs}; + use super::{wasm, output_type, acceptable_inputs}; use $crate::prelude::*; use $crate::ir::Type as IrType; use wasm_encoder::ValType; + #[allow(unused)] macro_rules! ident_as_irtype { ( $_:ident ) => { IrType }; } @@ -78,7 +77,7 @@ macro_rules! instructions_test { fn output_type_fails_when_wasm_fails() { use $crate::wasm::{StepFunc, TypeRegistry, ExternalFunctionMap}; for ($($type_arg,)*) in types_iter() { - let output_type_result = output_type(Rc::new([$($type_arg,)*])); + let output_type_result = output_type(Rc::new([$($type_arg,)*]), $(&$fields)?); let type_registry = Rc::new(TypeRegistry::new()); let external_functions = Rc::new(ExternalFunctionMap::new()); let types: &[IrType] = &[$($type_arg,)*]; @@ -105,7 +104,7 @@ macro_rules! instructions_test { use $crate::prelude::Rc; for ($($type_arg,)*) in types_iter() { - let output_type = match output_type(Rc::new([$($type_arg,)*])) { + let output_type = match output_type(Rc::new([$($type_arg,)*]), $(&$fields)?) { Ok(a) => a, Err(_) => { println!("skipping failed output_type"); @@ -129,7 +128,7 @@ macro_rules! instructions_test { step_func.add_instructions(wasm); let func = step_func.finish(); - let wasm_proj = $crate::wasm::WasmProject::new(Default::default(), crate::wasm::ExternalEnvironment::WebBrowser); + let wasm_proj = $crate::wasm::WasmProject::new(Default::default(), $crate::wasm::ExternalEnvironment::WebBrowser); let mut module = Module::new(); @@ -166,7 +165,9 @@ macro_rules! instructions_test { fn wasm_to_js_type(ty: ValType) -> &'static str { match ty { - ValType::I64 | ValType::F64 => "number", + ValType::I64 => "Integer", + ValType::F64 => "number", + ValType::EXTERNREF => "string", _ => todo!("unknown js type for wasm type {:?}", ty) } } @@ -193,18 +194,13 @@ macro_rules! instructions_test { } else { wasm_to_js_type(results[1]) }; - let type_strs: &[&str] = &[$(stringify!($type_arg),)*]; - let ins = type_strs.into_iter().enumerate().map(|(i, t)| { + let arg_idents: Vec = params.iter().enumerate().map(|(i, _)| format!("_{i}")).collect(); + let ins = arg_idents.iter().enumerate().map(|(i, ident)| { format!( "{}: {}", - t, - wasm_to_js_type( - $crate::wasm::WasmProject::new( - Default::default(), - crate::wasm::ExternalEnvironment::WebBrowser - ).ir_type_to_wasm([$($type_arg,)*][i]).unwrap() + ident, + wasm_to_js_type(*params.get(i).unwrap()) ) - ) }).collect::>().join(", "); let diagnostics = check_js( vec![PathBuf::from(format!("src/instructions/{}/{}.ts", module, name))], @@ -212,8 +208,8 @@ macro_rules! instructions_test { let func_string = fs::read_to_string(path).ok()?; let test_string = format!("function _({ins}): {out} {{ {func} - return {name}{ts}; - }}", ins=ins, out=out, func=func_string, name=name, ts=stringify!(($($type_arg,)*))); + return {name}({ts}); + }}", ins=ins, out=out, func=func_string, name=name, ts=arg_idents.join(", ")); println!("{}", test_string.clone()); Some(test_string.as_str().as_bytes().into_iter().map(|&u| u).collect::>()) }, diff --git a/src/ir.rs b/src/ir.rs index 725d9cd9..a3c40137 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -7,10 +7,10 @@ mod step; mod thread; mod types; -pub use context::{TargetContext, StepContext}; +pub use context::{StepContext, TargetContext}; pub use event::Event; -pub use proc::{Proc, ProcedureContext, ProcMap, ProcRegistry}; +pub use proc::{Proc, ProcMap, ProcRegistry, ProcedureContext}; pub use project::IrProject; pub use step::Step; pub use thread::Thread; -pub use types::Type; +pub use types::{Type, TypeStack}; diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index 40ad7ef0..7288fbdc 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -1,52 +1,54 @@ -use crate::instructions::{IrOpcode, MathNumberFields}; +use crate::instructions::{fields, IrOpcode}; use crate::prelude::*; use crate::sb3::{Block, BlockArray, BlockInfo, BlockMap, BlockOpcode}; +use fields::*; impl BlockOpcode { - fn from_block(block: &Block, blocks: &BlockMap) -> HQResult { + pub fn from_block(block: &Block, blocks: &BlockMap) -> HQResult { match block { Block::Normal { block_info, .. } => BlockOpcode::from_normal_block(block_info, blocks), - Block::Special(block_array) => BlockOpcode::from_special_block(block_array, blocks), + Block::Special(block_array) => BlockOpcode::from_special_block(block_array), } } - fn from_normal_block(block_info: &BlockInfo, blocks: &BlockMap) -> HQResult { - hq_todo!() + fn from_normal_block(block_info: &BlockInfo, _blocks: &BlockMap) -> HQResult { + Ok(match &block_info.opcode { + BlockOpcode::operator_add => IrOpcode::operator_add, + BlockOpcode::looks_say => IrOpcode::looks_say, + other => hq_todo!("unimplemented block: {:?}", other), + }) } - fn from_special_block(block_array: &BlockArray, blocks: &BlockMap) -> HQResult { + fn from_special_block(block_array: &BlockArray) -> HQResult { Ok(match block_array { BlockArray::NumberOrAngle(ty, value) => match ty { - 4 => IrOpcode::math_number(MathNumberFields(*value)), - //5 => IrOpcode::math_positive_number { NUM: *value as i32 }, - //6 => IrOpcode::math_whole_number { NUM: *value as i32 }, - //7 => IrOpcode::math_integer { NUM: *value as i32 }, - //8 => IrOpcode::math_angle { NUM: *value as i32 }, + 4 | 8 => IrOpcode::math_number(MathNumberFields(*value)), + 5 => IrOpcode::math_positive_number(MathPositiveNumberFields(*value)), + 6 => IrOpcode::math_whole_number(MathWholeNumberFields(*value as i64)), + 7 => IrOpcode::math_integer(MathIntegerFields(*value as i64)), _ => hq_bad_proj!("bad project json (block array of type ({}, f64))", ty), }, BlockArray::ColorOrString(ty, value) => match ty { - /*4 => IrOpcode::math_number { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - },*/ - /*5 => IrOpcode::math_positive_number { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, - 6 => IrOpcode::math_whole_number { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, - 7 => IrOpcode::math_integer { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, - 8 => IrOpcode::math_angle { - NUM: value.parse().map_err(|_| make_hq_bug!(""))?, - }, + 4 | 8 => IrOpcode::math_number(MathNumberFields( + value.parse().map_err(|_| make_hq_bug!(""))?, + )), + 5 => IrOpcode::math_positive_number(MathPositiveNumberFields( + value.parse().map_err(|_| make_hq_bug!(""))?, + )), + 6 => IrOpcode::math_whole_number(MathWholeNumberFields( + value.parse().map_err(|_| make_hq_bug!(""))?, + )), + 7 => IrOpcode::math_integer(MathIntegerFields( + value.parse().map_err(|_| make_hq_bug!(""))?, + )), 9 => hq_todo!(""), - 10 => IrOpcode::text { - TEXT: value.to_string(), + 10 => hq_todo!(), /*IrOpcode::text { + TEXT: value.to_string(), },*/ _ => hq_bad_proj!("bad project json (block array of type ({}, string))", ty), }, - BlockArray::Broadcast(ty, _name, id) | BlockArray::VariableOrList(ty, _name, id, _, _) => match ty { + BlockArray::Broadcast(ty, _name, _id) + | BlockArray::VariableOrList(ty, _name, _id, _, _) => match ty { /*12 => IrOpcode::data_variable { VARIABLE: id.to_string(), assume_type: None, diff --git a/src/ir/context.rs b/src/ir/context.rs index bf0e792b..a2edbc65 100644 --- a/src/ir/context.rs +++ b/src/ir/context.rs @@ -8,4 +8,4 @@ pub struct TargetContext { pub struct StepContext { pub target_context: TargetContext, pub proc_context: Option, -} \ No newline at end of file +} diff --git a/src/ir/event.rs b/src/ir/event.rs index 2287176f..8c538bc8 100644 --- a/src/ir/event.rs +++ b/src/ir/event.rs @@ -1,4 +1,4 @@ #[derive(Copy, Clone)] pub enum Event { FlagCLicked, -} \ No newline at end of file +} diff --git a/src/ir/proc.rs b/src/ir/proc.rs index e33a0d70..8e62306c 100644 --- a/src/ir/proc.rs +++ b/src/ir/proc.rs @@ -1,6 +1,6 @@ use super::{Step, StepContext, TargetContext, Type as IrType}; use crate::prelude::*; -use crate::sb3::{Block, BlockMap, BlockOpcode}; +use crate::sb3::{BlockMap, BlockOpcode}; use lazy_regex::{lazy_regex, Lazy}; use regex::Regex; @@ -24,6 +24,10 @@ impl ProcedureContext { pub fn warp(&self) -> bool { self.warp } + + pub fn target_context(&self) -> &TargetContext { + self.target_context.borrow() + } } #[derive(Clone)] @@ -76,14 +80,10 @@ impl Proc { }; info.opcode == BlockOpcode::procedures_prototype && (match info.mutation.mutations.get("proccode") { - None => false, - Some(p) => { - if let serde_json::Value::String(ref s) = p { - *s == proccode - } else { - false - } + Some(serde_json::Value::String(ref s)) => { + *s == proccode } + _ => false }) }) else { hq_bad_proj!("no prototype found for {proccode} in") @@ -160,7 +160,7 @@ impl Proc { }; Ok(Proc { first_step: Box::new(first_step), - context + context, }) } } diff --git a/src/ir/project.rs b/src/ir/project.rs index 866b4881..a7733f4e 100644 --- a/src/ir/project.rs +++ b/src/ir/project.rs @@ -1,14 +1,12 @@ use crate::prelude::*; use crate::sb3::Sb3Project; -pub struct IrProject { - -} +pub struct IrProject {} impl TryFrom for IrProject { type Error = HQError; - fn try_from(sb3: Sb3Project) -> HQResult { + fn try_from(_sb3: Sb3Project) -> HQResult { hq_todo!() } -} \ No newline at end of file +} diff --git a/src/ir/step.rs b/src/ir/step.rs index daf64bc4..da07b292 100644 --- a/src/ir/step.rs +++ b/src/ir/step.rs @@ -1,7 +1,7 @@ use super::StepContext; use crate::instructions::IrOpcode; use crate::prelude::*; -use crate::sb3::{Block, BlockMap, BlockOpcode}; +use crate::sb3::{Block, BlockMap}; #[derive(Clone)] pub struct Step { @@ -10,11 +10,19 @@ pub struct Step { } impl Step { + pub fn context(&self) -> &StepContext { + &self.context + } + + pub fn opcodes(&self) -> &[IrOpcode] { + self.opcodes.borrow() + } + pub fn new(context: StepContext, opcodes: Box<[IrOpcode]>) -> Self { Step { context, opcodes } } - pub fn from_block(block: &Block, blocks: &BlockMap, context: StepContext) -> HQResult { + pub fn from_block(_block: &Block, _blocks: &BlockMap, _context: StepContext) -> HQResult { hq_todo!() } } diff --git a/src/ir/thread.rs b/src/ir/thread.rs index bbdf3283..5fad9d2f 100644 --- a/src/ir/thread.rs +++ b/src/ir/thread.rs @@ -17,9 +17,13 @@ impl Thread { self.first_step.borrow() } - /// tries to construct a thread from a top-level block. + /// tries to construct a thread from a top-level block. /// Returns Ok(None) if the top-level block is not a valid event or if there is no next block. - pub fn try_from_top_block(block: &Block, blocks: &BlockMap, target_context: TargetContext) -> HQResult> { + pub fn try_from_top_block( + block: &Block, + blocks: &BlockMap, + target_context: TargetContext, + ) -> HQResult> { let block_info = block .block_info() .ok_or(make_hq_bug!("top-level block is a special block"))?; @@ -36,16 +40,22 @@ impl Thread { } _ => return Ok(None), }; - let next = blocks.get(match &block_info.next { - Some(next) => next, - None => return Ok(None), - }).ok_or(make_hq_bug!("block not found in BlockMap"))?; + let next = blocks + .get(match &block_info.next { + Some(next) => next, + None => return Ok(None), + }) + .ok_or(make_hq_bug!("block not found in BlockMap"))?; Ok(Some(Thread { event, - first_step: Box::new(Step::from_block(next, blocks, StepContext { - target_context, - proc_context: None, - })?) + first_step: Box::new(Step::from_block( + next, + blocks, + StepContext { + target_context, + proc_context: None, + }, + )?), })) } } diff --git a/src/ir/types.rs b/src/ir/types.rs index 8f3ec654..5b4fe499 100644 --- a/src/ir/types.rs +++ b/src/ir/types.rs @@ -65,4 +65,4 @@ pub enum Type { Color, } -pub type TypeStack = Vec; \ No newline at end of file +pub type TypeStack = Vec; diff --git a/src/wasm/external.rs b/src/wasm/external.rs index ffd7a57e..fb847739 100644 --- a/src/wasm/external.rs +++ b/src/wasm/external.rs @@ -51,6 +51,7 @@ impl ExternalFunctionMap { } } +#[derive(Debug, Copy, Clone)] #[non_exhaustive] pub enum ExternalEnvironment { WebBrowser, diff --git a/src/wasm/func.rs b/src/wasm/func.rs index 2fb0c6d4..bea68bfa 100644 --- a/src/wasm/func.rs +++ b/src/wasm/func.rs @@ -1,5 +1,4 @@ use super::{ExternalFunctionMap, TypeRegistry}; -use crate::ir::Type as IrType; use crate::prelude::*; use wasm_encoder::{Function, Instruction, ValType}; diff --git a/src/wasm/project.rs b/src/wasm/project.rs index 1399650c..9f5345bf 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -23,6 +23,10 @@ impl WasmProject { &self.flags } + pub fn environment(&self) -> ExternalEnvironment { + self.environment + } + /// maps a broad IR type to a WASM type pub fn ir_type_to_wasm(&self, ir_type: IrType) -> HQResult { Ok(if IrType::Float.contains(ir_type) { From 75fe2a1ddb20b48f99e900c679192713059d2387 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:22:47 +0000 Subject: [PATCH 07/98] getting closer --- src/error.rs | 42 ++++++------- src/instructions/operator/add.rs | 2 +- src/ir.rs | 4 +- src/ir/blocks.rs | 101 +++++++++++++++--------------- src/ir/context.rs | 9 +-- src/ir/proc.rs | 40 ++++++------ src/ir/project.rs | 36 ++++++++++- src/ir/step.rs | 16 ++++- src/ir/target.rs | 21 +++++++ src/ir/thread.rs | 12 +++- src/sb3.rs | 103 +++++++++++++++---------------- src/wasm/project.rs | 32 +++++++--- 12 files changed, 243 insertions(+), 175 deletions(-) create mode 100644 src/ir/target.rs diff --git a/src/error.rs b/src/error.rs index 1fd0d84e..b574d6db 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use alloc::string::String; +use alloc::boxed::Box; #[cfg(target_family = "wasm")] use wasm_bindgen::JsValue; @@ -7,8 +7,8 @@ pub type HQResult = Result; #[derive(Clone, Debug)] // todo: get rid of this once all expects are gone pub struct HQError { pub err_type: HQErrorType, - pub msg: String, - pub file: String, + pub msg: Box, + pub file: Box, pub line: u32, pub column: u32, } @@ -33,21 +33,19 @@ impl From for JsValue { #[macro_export] macro_rules! hq_todo { () => {{ - use $crate::alloc::string::ToString; return Err($crate::HQError { err_type: $crate::HQErrorType::Unimplemented, - msg: "todo".to_string(), - file: file!().to_string(), + msg: "todo".into(), + file: file!().into(), line: line!(), column: column!() }); }}; ($($args:tt)+) => {{ - use $crate::alloc::string::ToString; return Err($crate::HQError { err_type: $crate::HQErrorType::Unimplemented, - msg: format!("{}", format_args!($($args)*)), - file: file!().to_string(), + msg: format!("{}", format_args!($($args)*)).into(), + file: file!().into(), line: line!(), column: column!() }); @@ -57,11 +55,10 @@ macro_rules! hq_todo { #[macro_export] macro_rules! hq_bug { ($($args:tt)+) => {{ - use $crate::alloc::string::ToString; return Err($crate::HQError { err_type: $crate::HQErrorType::InternalError, - msg: format!("{}", format_args!($($args)*)), - file: file!().to_string(), + msg: format!("{}", format_args!($($args)*)).into(), + file: file!().into(), line: line!(), column: column!() }); @@ -71,11 +68,10 @@ macro_rules! hq_bug { #[macro_export] macro_rules! hq_bad_proj { ($($args:tt)+) => {{ - use $crate::alloc::string::ToString; return Err($crate::HQError { err_type: $crate::HQErrorType::MalformedProject, - msg: format!("{}", format_args!($($args)*)), - file: file!().to_string(), + msg: format!("{}", format_args!($($args)*)).into(), + file: file!().into(), line: line!(), column: column!() }); @@ -85,11 +81,11 @@ macro_rules! hq_bad_proj { #[macro_export] macro_rules! make_hq_todo { ($($args:tt)+) => {{ - use $crate::alloc::string::ToString; + use $crate::alloc::Box::ToBox; $crate::HQError { err_type: $crate::HQErrorType::Unimplemented, - msg: format!("{}", format_args!($($args)*)), - file: file!().to_string(), + msg: format!("{}", format_args!($($args)*)).into(), + file: file!().into(), line: line!(), column: column!() } @@ -99,11 +95,10 @@ macro_rules! make_hq_todo { #[macro_export] macro_rules! make_hq_bug { ($($args:tt)+) => {{ - use $crate::alloc::string::ToString; $crate::HQError { err_type: $crate::HQErrorType::InternalError, - msg: format!("{}", format_args!($($args)*)), - file: file!().to_string(), + msg: format!("{}", format_args!($($args)*)).into(), + file: file!().into(), line: line!(), column: column!() } @@ -113,11 +108,10 @@ macro_rules! make_hq_bug { #[macro_export] macro_rules! make_hq_bad_proj { ($($args:tt)+) => {{ - use $crate::alloc::string::ToString; $crate::HQError { err_type: $crate::HQErrorType::MalformedProject, - msg: format!("{}", format_args!($($args)*)), - file: file!().to_string(), + msg: format!("{}", format_args!($($args)*)).into(), + file: file!().into(), line: line!(), column: column!() } diff --git a/src/instructions/operator/add.rs b/src/instructions/operator/add.rs index bf399a63..6f5e3c73 100644 --- a/src/instructions/operator/add.rs +++ b/src/instructions/operator/add.rs @@ -26,7 +26,7 @@ pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult HQResult { - match block { - Block::Normal { block_info, .. } => BlockOpcode::from_normal_block(block_info, blocks), - Block::Special(block_array) => BlockOpcode::from_special_block(block_array), - } - } +pub fn from_block(block: &Block, blocks: &BlockMap) -> HQResult> { + Ok(match block { + Block::Normal { block_info, .. } => from_normal_block(block_info, blocks)?, + Block::Special(block_array) => Box::new([from_special_block(block_array)?]), + }) +} - fn from_normal_block(block_info: &BlockInfo, _blocks: &BlockMap) -> HQResult { - Ok(match &block_info.opcode { - BlockOpcode::operator_add => IrOpcode::operator_add, - BlockOpcode::looks_say => IrOpcode::looks_say, - other => hq_todo!("unimplemented block: {:?}", other), - }) +fn from_normal_block(block_info: &BlockInfo, _blocks: &BlockMap) -> HQResult> { + Ok(match &block_info.opcode { + BlockOpcode::operator_add => [IrOpcode::operator_add].into_iter(), + BlockOpcode::looks_say => [IrOpcode::looks_say].into_iter(), + other => hq_todo!("unimplemented block: {:?}", other), } + .collect()) +} - fn from_special_block(block_array: &BlockArray) -> HQResult { - Ok(match block_array { - BlockArray::NumberOrAngle(ty, value) => match ty { - 4 | 8 => IrOpcode::math_number(MathNumberFields(*value)), - 5 => IrOpcode::math_positive_number(MathPositiveNumberFields(*value)), - 6 => IrOpcode::math_whole_number(MathWholeNumberFields(*value as i64)), - 7 => IrOpcode::math_integer(MathIntegerFields(*value as i64)), - _ => hq_bad_proj!("bad project json (block array of type ({}, f64))", ty), - }, - BlockArray::ColorOrString(ty, value) => match ty { - 4 | 8 => IrOpcode::math_number(MathNumberFields( - value.parse().map_err(|_| make_hq_bug!(""))?, - )), - 5 => IrOpcode::math_positive_number(MathPositiveNumberFields( - value.parse().map_err(|_| make_hq_bug!(""))?, - )), - 6 => IrOpcode::math_whole_number(MathWholeNumberFields( - value.parse().map_err(|_| make_hq_bug!(""))?, - )), - 7 => IrOpcode::math_integer(MathIntegerFields( - value.parse().map_err(|_| make_hq_bug!(""))?, - )), - 9 => hq_todo!(""), - 10 => hq_todo!(), /*IrOpcode::text { - TEXT: value.to_string(), - },*/ - _ => hq_bad_proj!("bad project json (block array of type ({}, string))", ty), - }, - BlockArray::Broadcast(ty, _name, _id) - | BlockArray::VariableOrList(ty, _name, _id, _, _) => match ty { - /*12 => IrOpcode::data_variable { - VARIABLE: id.to_string(), - assume_type: None, - },*/ - _ => hq_todo!(""), - }, - }) - } +fn from_special_block(block_array: &BlockArray) -> HQResult { + Ok(match block_array { + BlockArray::NumberOrAngle(ty, value) => match ty { + 4 | 8 => IrOpcode::math_number(MathNumberFields(*value)), + 5 => IrOpcode::math_positive_number(MathPositiveNumberFields(*value)), + 6 => IrOpcode::math_whole_number(MathWholeNumberFields(*value as i64)), + 7 => IrOpcode::math_integer(MathIntegerFields(*value as i64)), + _ => hq_bad_proj!("bad project json (block array of type ({}, f64))", ty), + }, + BlockArray::ColorOrString(ty, value) => match ty { + 4 | 8 => IrOpcode::math_number(MathNumberFields( + value.parse().map_err(|_| make_hq_bug!(""))?, + )), + 5 => IrOpcode::math_positive_number(MathPositiveNumberFields( + value.parse().map_err(|_| make_hq_bug!(""))?, + )), + 6 => IrOpcode::math_whole_number(MathWholeNumberFields( + value.parse().map_err(|_| make_hq_bug!(""))?, + )), + 7 => IrOpcode::math_integer(MathIntegerFields( + value.parse().map_err(|_| make_hq_bug!(""))?, + )), + 9 => hq_todo!(""), + 10 => hq_todo!(), /*IrOpcode::text { + TEXT: value.to_string(), + },*/ + _ => hq_bad_proj!("bad project json (block array of type ({}, string))", ty), + }, + BlockArray::Broadcast(ty, _name, _id) + | BlockArray::VariableOrList(ty, _name, _id, _, _) => match ty { + /*12 => IrOpcode::data_variable { + VARIABLE: id.to_string(), + assume_type: None, + },*/ + _ => hq_todo!(""), + }, + }) } diff --git a/src/ir/context.rs b/src/ir/context.rs index a2edbc65..28292f7f 100644 --- a/src/ir/context.rs +++ b/src/ir/context.rs @@ -1,11 +1,8 @@ -use super::ProcedureContext; +use super::{ProcedureContext, Target}; +use crate::prelude::*; -#[derive(Debug, Clone, Copy)] -pub struct TargetContext { - pub target: u32, -} #[derive(Debug, Clone)] pub struct StepContext { - pub target_context: TargetContext, + pub target: Rc, pub proc_context: Option, } diff --git a/src/ir/proc.rs b/src/ir/proc.rs index 8e62306c..4842aa9f 100644 --- a/src/ir/proc.rs +++ b/src/ir/proc.rs @@ -1,4 +1,4 @@ -use super::{Step, StepContext, TargetContext, Type as IrType}; +use super::{Step, StepContext, Target, Type as IrType}; use crate::prelude::*; use crate::sb3::{BlockMap, BlockOpcode}; use lazy_regex::{lazy_regex, Lazy}; @@ -6,14 +6,14 @@ use regex::Regex; #[derive(Clone, Debug)] pub struct ProcedureContext { - arg_ids: Box<[String]>, + arg_ids: Box<[Box]>, arg_types: Box<[IrType]>, warp: bool, - target_context: Box, + target: Rc, } impl ProcedureContext { - pub fn arg_ids(&self) -> &[String] { + pub fn arg_ids(&self) -> &[Box] { self.arg_ids.borrow() } @@ -25,8 +25,8 @@ impl ProcedureContext { self.warp } - pub fn target_context(&self) -> &TargetContext { - self.target_context.borrow() + pub fn target_context(&self) -> &Target { + self.target.borrow() } } @@ -48,10 +48,10 @@ impl Proc { static ARG_REGEX: Lazy = lazy_regex!(r#"[^\\]%[nbs]"#); -fn arg_types_from_proccode(proccode: String) -> Result, HQError> { +fn arg_types_from_proccode(proccode: Box) -> Result, HQError> { // https://github.com/scratchfoundation/scratch-blocks/blob/abbfe93136fef57fdfb9a077198b0bc64726f012/blocks_vertical/procedures.js#L207-L215 (*ARG_REGEX) - .find_iter(proccode.as_str()) + .find_iter(&*proccode) .map(|s| s.as_str().to_string().trim().to_string()) .filter(|s| s.as_str().starts_with('%')) .map(|s| s[..2].to_string()) @@ -68,9 +68,9 @@ fn arg_types_from_proccode(proccode: String) -> Result, HQError> { impl Proc { pub fn from_proccode( - proccode: String, + proccode: Box, blocks: &BlockMap, - target_context: TargetContext, + target: Rc, expect_warp: bool, ) -> HQResult { let arg_types = arg_types_from_proccode(proccode.clone())?; @@ -80,10 +80,8 @@ impl Proc { }; info.opcode == BlockOpcode::procedures_prototype && (match info.mutation.mutations.get("proccode") { - Some(serde_json::Value::String(ref s)) => { - *s == proccode - } - _ => false + Some(serde_json::Value::String(ref s)) => **s == *proccode, + _ => false, }) }) else { hq_bad_proj!("no prototype found for {proccode} in") @@ -132,7 +130,7 @@ impl Proc { serde_json::Value::Array(values) => values .iter() .map(|val| match val { - serde_json::Value::String(s) => Ok(s.clone()), + serde_json::Value::String(s) => Ok(Into::>::into(s.clone())), _ => hq_bad_proj!("non-string argumentids member in {proccode}"), }) .collect::>>()?, @@ -142,10 +140,10 @@ impl Proc { warp: expect_warp, arg_types, arg_ids, - target_context: Box::new(target_context), + target: Rc::clone(&target), }; let step_context = StepContext { - target_context, + target, proc_context: Some(context.clone()), }; let first_step = match &def_block.block_info().unwrap().next { @@ -165,7 +163,7 @@ impl Proc { } } -pub type ProcMap = IndexMap>; +pub type ProcMap = IndexMap, Rc>; #[derive(Clone, Default)] pub struct ProcRegistry(RefCell); @@ -182,9 +180,9 @@ impl ProcRegistry { /// get the `Proc` for the specified proccode, creating it if it doesn't already exist pub fn proc( &self, - proccode: String, + proccode: Box, blocks: &BlockMap, - target_context: TargetContext, + target: Rc, expect_warp: bool, ) -> HQResult> { Ok(Rc::clone( @@ -194,7 +192,7 @@ impl ProcRegistry { .or_insert(Rc::new(Proc::from_proccode( proccode, blocks, - target_context, + target, expect_warp, )?)), )) diff --git a/src/ir/project.rs b/src/ir/project.rs index a7733f4e..11c02fb9 100644 --- a/src/ir/project.rs +++ b/src/ir/project.rs @@ -1,12 +1,42 @@ +use super::{Target, Thread}; use crate::prelude::*; use crate::sb3::Sb3Project; -pub struct IrProject {} +pub struct IrProject { + threads: Box<[Thread]>, +} + +impl IrProject { + pub fn threads(&self) -> &[Thread] { + self.threads.borrow() + } +} impl TryFrom for IrProject { type Error = HQError; - fn try_from(_sb3: Sb3Project) -> HQResult { - hq_todo!() + fn try_from(sb3: Sb3Project) -> HQResult { + let threads = sb3 + .targets + .iter() + .map(|target| { + let ir_target = Rc::new(Target::new(target.name.clone(), target.is_stage)); + let blocks = &target.blocks; + blocks + .values() + .filter_map(|block| { + let thread = + Thread::try_from_top_block(block, blocks, Rc::clone(&ir_target)) + .transpose()?; + Some(thread) + }) + .collect::>>() + }) + .collect::>>()? + .iter() + .flatten() + .cloned() + .collect::>(); + Ok(IrProject { threads }) } } diff --git a/src/ir/step.rs b/src/ir/step.rs index da07b292..8df89316 100644 --- a/src/ir/step.rs +++ b/src/ir/step.rs @@ -1,3 +1,4 @@ +use super::blocks; use super::StepContext; use crate::instructions::IrOpcode; use crate::prelude::*; @@ -7,6 +8,7 @@ use crate::sb3::{Block, BlockMap}; pub struct Step { context: StepContext, opcodes: Box<[IrOpcode]>, + inlined: RefCell } impl Step { @@ -18,11 +20,19 @@ impl Step { self.opcodes.borrow() } + pub fn inlined(&self) -> bool { + *self.inlined.borrow() + } + + pub fn set_inlined(&self, inlined: bool) { + *self.inlined.borrow_mut() = inlined; + } + pub fn new(context: StepContext, opcodes: Box<[IrOpcode]>) -> Self { - Step { context, opcodes } + Step { context, opcodes, inlined: RefCell::new(false) } } - pub fn from_block(_block: &Block, _blocks: &BlockMap, _context: StepContext) -> HQResult { - hq_todo!() + pub fn from_block(block: &Block, blocks: &BlockMap, context: StepContext) -> HQResult { + Ok(Step::new(context, blocks::from_block(block, blocks)?)) } } diff --git a/src/ir/target.rs b/src/ir/target.rs new file mode 100644 index 00000000..189a8fd4 --- /dev/null +++ b/src/ir/target.rs @@ -0,0 +1,21 @@ +use crate::prelude::*; + +#[derive(Debug, Clone)] +pub struct Target { + name: Box, + is_stage: bool, +} + +impl Target { + pub fn name(&self) -> &str { + self.name.borrow() + } + + pub fn is_stage(&self) -> bool { + self.is_stage + } + + pub fn new(name: Box, is_stage: bool) -> Self { + Target { name, is_stage } + } +} diff --git a/src/ir/thread.rs b/src/ir/thread.rs index 5fad9d2f..f7f5d48f 100644 --- a/src/ir/thread.rs +++ b/src/ir/thread.rs @@ -1,4 +1,4 @@ -use super::{Event, Step, StepContext, TargetContext}; +use super::{Event, Step, StepContext, Target}; use crate::prelude::*; use crate::sb3::{Block, BlockMap, BlockOpcode}; @@ -6,6 +6,7 @@ use crate::sb3::{Block, BlockMap, BlockOpcode}; pub struct Thread { event: Event, first_step: Box, + target: Rc, } impl Thread { @@ -17,12 +18,16 @@ impl Thread { self.first_step.borrow() } + pub fn target(&self) -> Rc { + Rc::clone(&self.target) + } + /// tries to construct a thread from a top-level block. /// Returns Ok(None) if the top-level block is not a valid event or if there is no next block. pub fn try_from_top_block( block: &Block, blocks: &BlockMap, - target_context: TargetContext, + target: Rc, ) -> HQResult> { let block_info = block .block_info() @@ -52,10 +57,11 @@ impl Thread { next, blocks, StepContext { - target_context, + target: Rc::clone(&target), proc_context: None, }, )?), + target, })) } } diff --git a/src/sb3.rs b/src/sb3.rs index 117a2fe9..bdc7e6cc 100644 --- a/src/sb3.rs +++ b/src/sb3.rs @@ -3,10 +3,7 @@ //! `sb3` files must be unzipped first. See //! for a loose informal specification. -use crate::error::HQError; -use alloc::collections::BTreeMap; -use alloc::string::String; -use alloc::vec::Vec; +use crate::prelude::*; use enum_field_getter::EnumFieldGetter; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -16,7 +13,7 @@ use serde_json::Value; pub struct Sb3Project { pub targets: Vec, pub monitors: Vec, - pub extensions: Vec, + pub extensions: Vec>, pub meta: Meta, } @@ -24,13 +21,13 @@ pub struct Sb3Project { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Comment { - pub block_id: Option, + pub block_id: Option>, pub x: Option, pub y: Option, pub width: f64, pub height: f64, pub minimized: bool, - pub text: String, + pub text: Box, } /// A possible block opcode, encompassing the default block pallette, the pen extension, @@ -232,17 +229,17 @@ pub enum Block { #[serde(untagged)] pub enum BlockArray { NumberOrAngle(u32, f64), - ColorOrString(u32, String), + ColorOrString(u32, Box), /// This might also represent a variable or list if the block is not top-level, in theory - Broadcast(u32, String, String), - VariableOrList(u32, String, String, f64, f64), + Broadcast(u32, Box, Box), + VariableOrList(u32, Box, Box, f64, f64), } /// Either a block array or a block id #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum BlockArrayOrId { - Id(String), + Id(Box), Array(BlockArray), } @@ -259,7 +256,7 @@ pub enum Input { #[serde(untagged)] pub enum Field { Value((Option,)), - ValueId(Option, Option), + ValueId(Option, Option>), } /// Represents a mutation on a block. See @@ -267,18 +264,18 @@ pub enum Field { #[serde(rename_all = "camelCase")] pub struct Mutation { /// ignored - should always be "mutation" - pub tag_name: String, + pub tag_name: Box, /// ignored - should always be [] #[serde(default)] pub children: Vec<()>, #[serde(flatten)] - pub mutations: BTreeMap, + pub mutations: BTreeMap, Value>, } impl Default for Mutation { fn default() -> Self { Mutation { - tag_name: String::from("mutation"), + tag_name: "mutation".into(), children: Default::default(), mutations: BTreeMap::new(), } @@ -290,10 +287,10 @@ impl Default for Mutation { #[serde(rename_all = "camelCase")] pub struct BlockInfo { pub opcode: BlockOpcode, - pub next: Option, - pub parent: Option, - pub inputs: BTreeMap, - pub fields: BTreeMap, + pub next: Option>, + pub parent: Option>, + pub inputs: BTreeMap, Input>, + pub fields: BTreeMap, Field>, pub shadow: bool, pub top_level: bool, #[serde(default)] @@ -316,9 +313,9 @@ pub enum CostumeDataFormat { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Costume { - pub asset_id: String, - pub name: String, - pub md5ext: String, + pub asset_id: Box, + pub name: Box, + pub md5ext: Box, pub data_format: CostumeDataFormat, #[serde(default)] pub bitmap_resolution: f64, @@ -330,10 +327,10 @@ pub struct Costume { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Sound { - pub asset_id: String, - pub name: String, - pub md5ext: String, - pub data_format: String, // TODO: enumerate + pub asset_id: Box, + pub name: Box, + pub md5ext: Box, + pub data_format: Box, // TODO: enumerate pub rate: f64, pub sample_count: f64, } @@ -344,31 +341,31 @@ pub struct Sound { pub enum VarVal { Float(f64), Bool(bool), - String(String), + String(Box), } /// Represents a variable #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(untagged)] pub enum VariableInfo { - CloudVar(String, VarVal, bool), - LocalVar(String, VarVal), + CloudVar(Box, VarVal, bool), + LocalVar(Box, VarVal), } -pub type BlockMap = BTreeMap; +pub type BlockMap = BTreeMap, Block>; /// A target (sprite or stage) #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Target { pub is_stage: bool, - pub name: String, - pub variables: BTreeMap, - pub lists: BTreeMap)>, + pub name: Box, + pub variables: BTreeMap, VariableInfo>, + pub lists: BTreeMap, (Box, Vec)>, #[serde(default)] - pub broadcasts: BTreeMap, + pub broadcasts: BTreeMap, Box>, pub blocks: BlockMap, - pub comments: BTreeMap, + pub comments: BTreeMap, Comment>, pub current_costume: u32, pub costumes: Vec, pub sounds: Vec, @@ -377,11 +374,11 @@ pub struct Target { #[serde(default)] pub tempo: f64, #[serde(default)] - pub video_state: Option, + pub video_state: Option>, #[serde(default)] pub video_transparency: f64, #[serde(default)] - pub text_to_speech_language: Option, + pub text_to_speech_language: Option>, #[serde(default)] pub visible: bool, #[serde(default)] @@ -395,9 +392,9 @@ pub struct Target { #[serde(default)] pub draggable: bool, #[serde(default)] - pub rotation_style: String, + pub rotation_style: Box, #[serde(flatten)] - pub unknown: BTreeMap, + pub unknown: BTreeMap, Value>, } /// The value of a list monitor @@ -405,7 +402,7 @@ pub struct Target { #[serde(untagged)] pub enum ListMonitorValue { List(Vec), - String(String), + String(Box), } /// A monitor @@ -414,12 +411,12 @@ pub enum ListMonitorValue { pub enum Monitor { #[serde(rename_all = "camelCase")] ListMonitor { - id: String, + id: Box, /// The name of the monitor's mode: "default", "large", "slider", or "list" - should be "list" - mode: String, - opcode: String, - params: BTreeMap, - sprite_name: Option, + mode: Box, + opcode: Box, + params: BTreeMap, Box>, + sprite_name: Option>, width: f64, height: f64, x: f64, @@ -429,12 +426,12 @@ pub enum Monitor { }, #[serde(rename_all = "camelCase")] VarMonitor { - id: String, + id: Box, /// The name of the monitor's mode: "default", "large", "slider", or "list". - mode: String, - opcode: String, - params: BTreeMap, - sprite_name: Option, + mode: Box, + opcode: Box, + params: BTreeMap, Box>, + sprite_name: Option>, value: VarVal, width: f64, height: f64, @@ -450,9 +447,9 @@ pub enum Monitor { /// metadata about a scratch project - only included here for completeness #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Meta { - pub semver: String, - pub vm: String, - pub agent: String, + pub semver: Box, + pub vm: Box, + pub agent: Box, } impl TryFrom for Sb3Project { diff --git a/src/wasm/project.rs b/src/wasm/project.rs index 9f5345bf..068d7ac6 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -1,12 +1,14 @@ use super::ExternalEnvironment; -use crate::ir::Type as IrType; +use crate::ir::{IrProject, Type as IrType}; use crate::prelude::*; use crate::wasm::{StepFunc, WasmFlags}; use wasm_encoder::ValType; +/// A respresntation of a WASM representation of a project. Cannot be created directly; +/// use `TryFrom`. pub struct WasmProject { flags: WasmFlags, - step_funcs: RefCell>, + step_funcs: Box<[StepFunc]>, environment: ExternalEnvironment, } @@ -14,7 +16,7 @@ impl WasmProject { pub fn new(flags: WasmFlags, environment: ExternalEnvironment) -> Self { WasmProject { flags, - step_funcs: RefCell::new(vec![]), + step_funcs: Box::new([]), environment, } } @@ -27,6 +29,10 @@ impl WasmProject { self.environment } + pub fn step_funcs(&self) -> &[StepFunc] { + self.step_funcs.borrow() + } + /// maps a broad IR type to a WASM type pub fn ir_type_to_wasm(&self, ir_type: IrType) -> HQResult { Ok(if IrType::Float.contains(ir_type) { @@ -36,17 +42,25 @@ impl WasmProject { } else if IrType::String.contains(ir_type) { ValType::EXTERNREF } else if IrType::Color.contains(ir_type) { - hq_todo!(); //ValType::V128 // f32x4 + hq_todo!() //ValType::V128 // f32x4 } else { ValType::I64 // NaN boxed value... let's worry about colors later }) } - pub fn add_step_func(&self, step_func: StepFunc) { - self.step_funcs.borrow_mut().push(step_func); - } - pub fn finish(self) -> HQResult> { - hq_todo!(); + hq_todo!() } } + +impl TryFrom for WasmProject { + type Error = HQError; + + fn try_from(ir_project: IrProject) -> HQResult { + let mut steps: Vec = vec![]; + for thread in ir_project.threads() { + + } + hq_todo!() + } +} \ No newline at end of file From 02290b0b361e721ee0a51a27c7ab85f5d7f99e12 Mon Sep 17 00:00:00 2001 From: Pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sat, 4 Jan 2025 10:08:05 +0000 Subject: [PATCH 08/98] try to avoid circular strong references --- src/ir.rs | 2 +- src/ir/context.rs | 2 +- src/ir/event.rs | 2 +- src/ir/proc.rs | 31 +++++++------ src/ir/project.rs | 50 +++++++++++++++++---- src/ir/step.rs | 103 ++++++++++++++++++++++++++++++++++++++++---- src/ir/target.rs | 2 +- src/ir/thread.rs | 47 +++++++++++--------- src/lib.rs | 4 +- src/wasm/project.rs | 6 +-- 10 files changed, 188 insertions(+), 61 deletions(-) diff --git a/src/ir.rs b/src/ir.rs index 5cdb76b5..2ac10663 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -12,7 +12,7 @@ pub use context::StepContext; pub use event::Event; pub use proc::{Proc, ProcMap, ProcRegistry, ProcedureContext}; pub use project::IrProject; -pub use step::Step; +pub use step::{Step, RcStep}; pub use target::Target; pub use thread::Thread; pub use types::{Type, TypeStack}; diff --git a/src/ir/context.rs b/src/ir/context.rs index 28292f7f..a89e958d 100644 --- a/src/ir/context.rs +++ b/src/ir/context.rs @@ -3,6 +3,6 @@ use crate::prelude::*; #[derive(Debug, Clone)] pub struct StepContext { - pub target: Rc, + pub target: Weak, pub proc_context: Option, } diff --git a/src/ir/event.rs b/src/ir/event.rs index 8c538bc8..0dae81a8 100644 --- a/src/ir/event.rs +++ b/src/ir/event.rs @@ -1,4 +1,4 @@ -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub enum Event { FlagCLicked, } diff --git a/src/ir/proc.rs b/src/ir/proc.rs index 4842aa9f..e0587ab3 100644 --- a/src/ir/proc.rs +++ b/src/ir/proc.rs @@ -1,4 +1,4 @@ -use super::{Step, StepContext, Target, Type as IrType}; +use super::{IrProject, RcStep, Step, StepContext, Target, Type as IrType}; use crate::prelude::*; use crate::sb3::{BlockMap, BlockOpcode}; use lazy_regex::{lazy_regex, Lazy}; @@ -9,7 +9,7 @@ pub struct ProcedureContext { arg_ids: Box<[Box]>, arg_types: Box<[IrType]>, warp: bool, - target: Rc, + target: Weak, } impl ProcedureContext { @@ -25,20 +25,20 @@ impl ProcedureContext { self.warp } - pub fn target_context(&self) -> &Target { - self.target.borrow() + pub fn target(&self) -> Weak { + Weak::clone(&self.target) } } #[derive(Clone)] pub struct Proc { - first_step: Box, + first_step: RcStep, context: ProcedureContext, } impl Proc { - pub fn first_step(&self) -> &Step { - self.first_step.borrow() + pub fn first_step(&self) -> &RcStep { + &self.first_step } pub fn context(&self) -> &ProcedureContext { @@ -51,7 +51,7 @@ static ARG_REGEX: Lazy = lazy_regex!(r#"[^\\]%[nbs]"#); fn arg_types_from_proccode(proccode: Box) -> Result, HQError> { // https://github.com/scratchfoundation/scratch-blocks/blob/abbfe93136fef57fdfb9a077198b0bc64726f012/blocks_vertical/procedures.js#L207-L215 (*ARG_REGEX) - .find_iter(&*proccode) + .find_iter(&proccode) .map(|s| s.as_str().to_string().trim().to_string()) .filter(|s| s.as_str().starts_with('%')) .map(|s| s[..2].to_string()) @@ -70,8 +70,9 @@ impl Proc { pub fn from_proccode( proccode: Box, blocks: &BlockMap, - target: Rc, + target: Weak, expect_warp: bool, + project: Weak, ) -> HQResult { let arg_types = arg_types_from_proccode(proccode.clone())?; let Some(prototype_block) = blocks.values().find(|block| { @@ -140,7 +141,7 @@ impl Proc { warp: expect_warp, arg_types, arg_ids, - target: Rc::clone(&target), + target: Weak::clone(&target), }; let step_context = StepContext { target, @@ -151,13 +152,15 @@ impl Proc { blocks .get(next_id) .ok_or(make_hq_bad_proj!("specified next block does not exist"))?, + next_id.clone(), blocks, step_context, + project, )?, - None => Step::new(step_context, Box::new([])), + None => RcStep::new(Rc::new(Step::new(None, step_context, Box::new([]), project))), }; Ok(Proc { - first_step: Box::new(first_step), + first_step, context, }) } @@ -182,8 +185,9 @@ impl ProcRegistry { &self, proccode: Box, blocks: &BlockMap, - target: Rc, + target: Weak, expect_warp: bool, + project: Weak, ) -> HQResult> { Ok(Rc::clone( self.get_map() @@ -194,6 +198,7 @@ impl ProcRegistry { blocks, target, expect_warp, + project, )?)), )) } diff --git a/src/ir/project.rs b/src/ir/project.rs index 11c02fb9..058d6b1b 100644 --- a/src/ir/project.rs +++ b/src/ir/project.rs @@ -1,21 +1,48 @@ -use super::{Target, Thread}; +use super::{Step, Target, Thread}; use crate::prelude::*; use crate::sb3::Sb3Project; +pub type StepSet = IndexSet>; + +#[derive(Clone, Debug)] pub struct IrProject { - threads: Box<[Thread]>, + threads: RefCell>, + steps: RefCell, + inlined_steps: RefCell, } impl IrProject { - pub fn threads(&self) -> &[Thread] { - self.threads.borrow() + pub fn threads(&self) -> &RefCell> { + &self.threads + } + + pub fn steps(&self) -> &RefCell { + &self.steps + } + + pub fn inlined_steps(&self) -> &RefCell { + &self.inlined_steps + } + + pub fn new() -> Self { + IrProject { + threads: RefCell::new(Box::new([])), + steps: RefCell::new(Default::default()), + inlined_steps: RefCell::new(Default::default()), + } + } + + pub fn register_step(&self, step: Rc) { + self.steps().borrow_mut().insert(step); } } -impl TryFrom for IrProject { +impl TryFrom for Rc { type Error = HQError; fn try_from(sb3: Sb3Project) -> HQResult { + let project = Rc::new(IrProject::new()); + let threads = sb3 .targets .iter() @@ -25,9 +52,13 @@ impl TryFrom for IrProject { blocks .values() .filter_map(|block| { - let thread = - Thread::try_from_top_block(block, blocks, Rc::clone(&ir_target)) - .transpose()?; + let thread = Thread::try_from_top_block( + block, + blocks, + Rc::downgrade(&ir_target), + Rc::downgrade(&project), + ) + .transpose()?; Some(thread) }) .collect::>>() @@ -37,6 +68,7 @@ impl TryFrom for IrProject { .flatten() .cloned() .collect::>(); - Ok(IrProject { threads }) + *project.threads.borrow_mut() = threads; + Ok(project) } } diff --git a/src/ir/step.rs b/src/ir/step.rs index 8df89316..e2b8f88b 100644 --- a/src/ir/step.rs +++ b/src/ir/step.rs @@ -1,16 +1,29 @@ use super::blocks; -use super::StepContext; +use super::{IrProject, StepContext}; use crate::instructions::IrOpcode; use crate::prelude::*; use crate::sb3::{Block, BlockMap}; +use uuid::Uuid; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Step { context: StepContext, opcodes: Box<[IrOpcode]>, - inlined: RefCell + /// is this step inlined? if not, its own function should be produced + inlined: RefCell, + /// used for `Hash`. Should be obtained from a block in the `Step` where possible. + id: Box, + project: Weak, } +impl PartialEq for Step { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Step {} + impl Step { pub fn context(&self) -> &StepContext { &self.context @@ -24,15 +37,87 @@ impl Step { *self.inlined.borrow() } - pub fn set_inlined(&self, inlined: bool) { - *self.inlined.borrow_mut() = inlined; + pub fn project(&self) -> Weak { + Weak::clone(&self.project) + } + + pub fn new( + id: Option>, + context: StepContext, + opcodes: Box<[IrOpcode]>, + project: Weak, + ) -> Self { + Step { + id: id.unwrap_or_else(|| Uuid::new_v4().to_string().into()), + context, + opcodes, + inlined: RefCell::new(false), + project, + } + } + + pub fn from_block( + block: &Block, + block_id: Box, + blocks: &BlockMap, + context: StepContext, + project: Weak, + ) -> HQResult { + let step = Rc::new(Step::new( + Some(block_id), + context, + blocks::from_block(block, blocks)?, + Weak::clone(&project), + )); + project + .upgrade() + .ok_or(make_hq_bug!("couldn't upgrade Weak"))? + .steps() + .borrow_mut() + .insert(Rc::clone(&step)); + Ok(RcStep(step)) + } +} + +impl core::hash::Hash for Step { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +#[derive(Clone, Debug)] +pub struct RcStep(Rc); + +impl RcStep { + pub fn new(rc: Rc) -> Self { + RcStep(rc) } - pub fn new(context: StepContext, opcodes: Box<[IrOpcode]>) -> Self { - Step { context, opcodes, inlined: RefCell::new(false) } + pub fn get_rc(&self) -> Rc { + Rc::clone(&self.0) } - pub fn from_block(block: &Block, blocks: &BlockMap, context: StepContext) -> HQResult { - Ok(Step::new(context, blocks::from_block(block, blocks)?)) + pub fn make_inlined(&self) -> HQResult<()> { + if *self.0.inlined.borrow() { + return Ok(()); + }; + *self.0.inlined.borrow_mut() = true; + self.0 + .project + .upgrade() + .ok_or(make_hq_bug!("couldn't upgrade Weak"))? + .inlined_steps() + .borrow_mut() + .insert( + self.0 + .project + .upgrade() + .ok_or(make_hq_bug!("couldn't upgrade Weak"))? + .steps() + .borrow_mut() + .swap_take(&self.0) + .ok_or(make_hq_bug!("step not in project's StepMap"))?, + ); + Ok(()) } } diff --git a/src/ir/target.rs b/src/ir/target.rs index 189a8fd4..f8a62624 100644 --- a/src/ir/target.rs +++ b/src/ir/target.rs @@ -1,6 +1,6 @@ use crate::prelude::*; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Target { name: Box, is_stage: bool, diff --git a/src/ir/thread.rs b/src/ir/thread.rs index f7f5d48f..30258059 100644 --- a/src/ir/thread.rs +++ b/src/ir/thread.rs @@ -1,12 +1,12 @@ -use super::{Event, Step, StepContext, Target}; +use super::{Event, IrProject, RcStep, Step, StepContext, Target}; use crate::prelude::*; use crate::sb3::{Block, BlockMap, BlockOpcode}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Thread { event: Event, - first_step: Box, - target: Rc, + first_step: RcStep, + target: Weak, } impl Thread { @@ -14,12 +14,12 @@ impl Thread { self.event } - pub fn first_step(&self) -> &Step { - self.first_step.borrow() + pub fn first_step(&self) -> &RcStep { + &self.first_step } - pub fn target(&self) -> Rc { - Rc::clone(&self.target) + pub fn target(&self) -> Weak { + Weak::clone(&self.target) } /// tries to construct a thread from a top-level block. @@ -27,7 +27,8 @@ impl Thread { pub fn try_from_top_block( block: &Block, blocks: &BlockMap, - target: Rc, + target: Weak, + project: Weak, ) -> HQResult> { let block_info = block .block_info() @@ -45,22 +46,26 @@ impl Thread { } _ => return Ok(None), }; + let next_id = match &block_info.next { + Some(next) => next, + None => return Ok(None), + }; let next = blocks - .get(match &block_info.next { - Some(next) => next, - None => return Ok(None), - }) + .get(next_id) .ok_or(make_hq_bug!("block not found in BlockMap"))?; + let first_step = Step::from_block( + next, + next_id.clone(), + blocks, + StepContext { + target: Weak::clone(&target), + proc_context: None, + }, + Weak::clone(&project), + )?; Ok(Some(Thread { event, - first_step: Box::new(Step::from_block( - next, - blocks, - StepContext { - target: Rc::clone(&target), - proc_context: None, - }, - )?), + first_step, target, })) } diff --git a/src/lib.rs b/src/lib.rs index cd053a70..67bc078c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,8 @@ #![doc(html_logo_url = "https://hyperquark.github.io/hyperquark/logo.png")] #![doc(html_favicon_url = "https://hyperquark.github.io/hyperquark/favicon.ico")] +#![allow(clippy::new_without_default)] + #[macro_use] extern crate alloc; extern crate enum_field_getter; @@ -46,7 +48,7 @@ pub mod prelude { pub use crate::{HQError, HQResult}; pub use alloc::boxed::Box; pub use alloc::collections::BTreeMap; - pub use alloc::rc::Rc; + pub use alloc::rc::{Rc, Weak}; pub use alloc::string::{String, ToString}; pub use alloc::vec::Vec; pub use core::borrow::Borrow; diff --git a/src/wasm/project.rs b/src/wasm/project.rs index 068d7ac6..b89b42d5 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -58,9 +58,7 @@ impl TryFrom for WasmProject { fn try_from(ir_project: IrProject) -> HQResult { let mut steps: Vec = vec![]; - for thread in ir_project.threads() { - - } + for thread in ir_project.threads().borrow().iter() {} hq_todo!() } -} \ No newline at end of file +} From b2671aead1f564a2c19ca254df20936a8ff9adc4 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sat, 4 Jan 2025 17:27:03 +0000 Subject: [PATCH 09/98] mostly finish putting everything together --- README.md | 1 + src/instructions.rs | 2 +- src/instructions/hq.rs | 2 + .../{math/number.rs => hq/float.rs} | 0 src/instructions/{math => hq}/integer.rs | 0 src/instructions/math.rs | 4 - src/instructions/math/positive_number.rs | 33 ------- src/instructions/math/whole_number.rs | 30 ------ src/instructions/utilities.rs | 34 +++---- src/ir.rs | 2 +- src/ir/blocks.rs | 76 ++++++++++---- src/ir/proc.rs | 7 +- src/lib.rs | 43 ++++---- src/wasm/func.rs | 84 ++++++++++------ src/wasm/project.rs | 98 +++++++++++++++++-- 15 files changed, 243 insertions(+), 173 deletions(-) create mode 100644 src/instructions/hq.rs rename src/instructions/{math/number.rs => hq/float.rs} (100%) rename src/instructions/{math => hq}/integer.rs (100%) delete mode 100644 src/instructions/math.rs delete mode 100644 src/instructions/math/positive_number.rs delete mode 100644 src/instructions/math/whole_number.rs diff --git a/README.md b/README.md index cf4740df..99bf676e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Compile scratch projects to WASM - [Rust](https://rust-lang.org) (v1.65.0 or later) - the `wasm32-unknown-unknown` target (`rustup target add wasm32-unknown-unknown`) - wasm-bindgen-cli (`cargo install -f wasm-bindgen-cli`) +- ezno (`cargo install ezno`) - wasm-opt (install binaryen using whatever package manager you use) ## Building diff --git a/src/instructions.rs b/src/instructions.rs index eadb4dd4..3f2c75c5 100644 --- a/src/instructions.rs +++ b/src/instructions.rs @@ -3,8 +3,8 @@ use crate::prelude::*; +mod hq; mod looks; -mod math; mod operator; #[macro_use] mod utilities; diff --git a/src/instructions/hq.rs b/src/instructions/hq.rs new file mode 100644 index 00000000..9a134f09 --- /dev/null +++ b/src/instructions/hq.rs @@ -0,0 +1,2 @@ +pub mod float; +pub mod integer; diff --git a/src/instructions/math/number.rs b/src/instructions/hq/float.rs similarity index 100% rename from src/instructions/math/number.rs rename to src/instructions/hq/float.rs diff --git a/src/instructions/math/integer.rs b/src/instructions/hq/integer.rs similarity index 100% rename from src/instructions/math/integer.rs rename to src/instructions/hq/integer.rs diff --git a/src/instructions/math.rs b/src/instructions/math.rs deleted file mode 100644 index 6564b130..00000000 --- a/src/instructions/math.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod integer; -pub mod number; -pub mod positive_number; -pub mod whole_number; diff --git a/src/instructions/math/positive_number.rs b/src/instructions/math/positive_number.rs deleted file mode 100644 index eaa8e60b..00000000 --- a/src/instructions/math/positive_number.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::ir::Type as IrType; -use crate::prelude::*; -use crate::wasm::StepFunc; -use wasm_encoder::Instruction; - -#[derive(Clone, Copy, Debug)] -pub struct Fields(pub f64); - -pub fn wasm( - _func: &StepFunc, - _inputs: Rc<[IrType]>, - fields: &Fields, -) -> HQResult>> { - Ok(vec![Instruction::F64Const(fields.0)]) -} - -pub fn acceptable_inputs() -> Rc<[IrType]> { - Rc::new([]) -} - -pub fn output_type(_inputs: Rc<[IrType]>, &Fields(val): &Fields) -> HQResult> { - Ok(Some(match val { - 0.0 => IrType::FloatZero, - f64::INFINITY => IrType::FloatPosInf, - nan if f64::is_nan(nan) => IrType::FloatNan, - int if int % 1.0 == 0.0 && int > 0.0 => IrType::FloatPosInt, - frac if frac > 0.0 => IrType::FloatPosFrac, - neg if neg < 0.0 => hq_bad_proj!("negative number in math_positive_number"), - _ => unreachable!(), - })) -} - -crate::instructions_test! {tests;; super::Fields(0.0)} diff --git a/src/instructions/math/whole_number.rs b/src/instructions/math/whole_number.rs deleted file mode 100644 index d2364bc7..00000000 --- a/src/instructions/math/whole_number.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::ir::Type as IrType; -use crate::prelude::*; -use crate::wasm::StepFunc; -use wasm_encoder::Instruction; - -#[derive(Clone, Copy, Debug)] -pub struct Fields(pub i64); - -pub fn wasm( - _func: &StepFunc, - _inputs: Rc<[IrType]>, - fields: &Fields, -) -> HQResult>> { - Ok(vec![Instruction::I64Const(fields.0)]) -} - -pub fn acceptable_inputs() -> Rc<[IrType]> { - Rc::new([]) -} - -pub fn output_type(_inputs: Rc<[IrType]>, &Fields(val): &Fields) -> HQResult> { - Ok(Some(match val { - 0 => IrType::IntZero, - pos if pos > 0 => IrType::IntPos, - neg if neg < 0 => hq_bad_proj!("negative number in math_whole_number"), - _ => unreachable!(), - })) -} - -crate::instructions_test! {tests;; super::Fields(0)} diff --git a/src/instructions/utilities.rs b/src/instructions/utilities.rs index c50a1a98..68a2589b 100644 --- a/src/instructions/utilities.rs +++ b/src/instructions/utilities.rs @@ -81,7 +81,7 @@ macro_rules! instructions_test { let type_registry = Rc::new(TypeRegistry::new()); let external_functions = Rc::new(ExternalFunctionMap::new()); let types: &[IrType] = &[$($type_arg,)*]; - let step_func = StepFunc::new_with_param_count(types.len(), type_registry.clone(), external_functions.clone()).unwrap(); + let step_func = StepFunc::new(type_registry.clone(), external_functions.clone()); let wasm_result = wasm(&step_func, Rc::new([$($type_arg,)*]), $(&$fields)?); match (output_type_result.clone(), wasm_result.clone()) { (Err(..), Ok(..)) | (Ok(..), Err(..)) => panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)*), output_type_result, wasm_result), @@ -113,8 +113,14 @@ macro_rules! instructions_test { }; let type_registry = Rc::new(TypeRegistry::new()); let external_functions = Rc::new(ExternalFunctionMap::new()); + let wasm_proj = $crate::wasm::WasmProject::new(Default::default(), $crate::wasm::ExternalEnvironment::WebBrowser); let types: &[IrType] = &[$($type_arg,)*]; - let step_func = StepFunc::new_with_param_count(types.len(), type_registry.clone(), external_functions.clone())?; + let params = [$($type_arg,)*].into_iter().map(|ty| wasm_proj.ir_type_to_wasm(ty)).collect::>>()?; + let result = match output_type { + Some(output) => Some(wasm_proj.ir_type_to_wasm(output)?), + None => None, + }; + let step_func = StepFunc::new_with_types(params.into(), result, Rc::clone(&type_registry), external_functions.clone())?; let wasm = match wasm(&step_func, Rc::new([$($type_arg,)*]), $(&$fields)?) { Ok(a) => a, Err(_) => { @@ -126,34 +132,22 @@ macro_rules! instructions_test { step_func.add_instructions([Instruction::LocalGet(i.try_into().unwrap())]) } step_func.add_instructions(wasm); - let func = step_func.finish(); - - let wasm_proj = $crate::wasm::WasmProject::new(Default::default(), $crate::wasm::ExternalEnvironment::WebBrowser); let mut module = Module::new(); let mut imports = ImportSection::new(); + let mut types = TypeSection::new(); + let mut functions = FunctionSection::new(); + let mut codes = CodeSection::new(); - Rc::unwrap_or_clone(external_functions).finish(&mut imports, type_registry.clone())?; - let mut types = TypeSection::new(); - let params = [$($type_arg,)*].into_iter().map(|ty| wasm_proj.ir_type_to_wasm(ty)).collect::>>()?; - let results = match output_type { - Some(output) => vec![wasm_proj.ir_type_to_wasm(output)?], - None => vec![], - }; - let step_type_index = type_registry.type_index(params, results)?; + Rc::unwrap_or_clone(external_functions).finish(&mut imports, type_registry.clone())?; + step_func.finish(&mut functions, &mut codes)?; Rc::unwrap_or_clone(type_registry).finish(&mut types); - module.section(&types); + module.section(&types); module.section(&imports); - - let mut functions = FunctionSection::new(); - functions.function(step_type_index); module.section(&functions); - - let mut codes = CodeSection::new(); - codes.function(&func); module.section(&codes); let wasm_bytes = module.finish(); diff --git a/src/ir.rs b/src/ir.rs index 2ac10663..91c5d19b 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -12,7 +12,7 @@ pub use context::StepContext; pub use event::Event; pub use proc::{Proc, ProcMap, ProcRegistry, ProcedureContext}; pub use project::IrProject; -pub use step::{Step, RcStep}; +pub use step::{RcStep, Step}; pub use target::Target; pub use thread::Thread; pub use types::{Type, TypeStack}; diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index f1f406ed..4888309d 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -1,6 +1,6 @@ use crate::instructions::{fields, IrOpcode}; use crate::prelude::*; -use crate::sb3::{Block, BlockArray, BlockInfo, BlockMap, BlockOpcode}; +use crate::sb3::{Block, BlockArray, BlockArrayOrId, BlockInfo, BlockMap, BlockOpcode, Input}; use fields::*; pub fn from_block(block: &Block, blocks: &BlockMap) -> HQResult> { @@ -10,35 +10,71 @@ pub fn from_block(block: &Block, blocks: &BlockMap) -> HQResult> }) } -fn from_normal_block(block_info: &BlockInfo, _blocks: &BlockMap) -> HQResult> { - Ok(match &block_info.opcode { - BlockOpcode::operator_add => [IrOpcode::operator_add].into_iter(), - BlockOpcode::looks_say => [IrOpcode::looks_say].into_iter(), - other => hq_todo!("unimplemented block: {:?}", other), +pub fn input_names(opcode: BlockOpcode) -> HQResult> { + Ok(match opcode { + BlockOpcode::looks_say => vec!["MESSAGE"], + BlockOpcode::operator_add => vec!["NUM1", "NUM2"], + other => hq_todo!("unimplemented input_names for {:?}", other), } + .into_iter() + .map(String::from) .collect()) } +pub fn inputs(block_info: &BlockInfo, blocks: &BlockMap) -> HQResult> { + Ok(input_names(block_info.opcode.clone())? + .into_iter() + .map(|name| -> HQResult> { + match block_info + .inputs + .get((*name).into()) + .ok_or(make_hq_bad_proj!("missing input {}", name))? + { + Input::NoShadow(_, Some(block)) | Input::Shadow(_, Some(block), _) => match block { + BlockArrayOrId::Array(arr) => Ok(vec![from_special_block(arr)?]), + BlockArrayOrId::Id(id) => match blocks + .get(id) + .ok_or(make_hq_bad_proj!("block for input {} doesn't exist", name))? + { + Block::Normal { block_info, .. } => { + Ok(from_normal_block(block_info, blocks)?.into()) + } + Block::Special(block_array) => Ok(vec![from_special_block(block_array)?]), + }, + }, + _ => hq_bad_proj!("missing input block for {}", name), + } + }) + .collect::>>()? + .iter() + .flatten() + .cloned() + .collect()) +} + +fn from_normal_block(block_info: &BlockInfo, blocks: &BlockMap) -> HQResult> { + Ok(inputs(block_info, blocks)? + .into_iter() + .chain(match &block_info.opcode { + BlockOpcode::operator_add => [IrOpcode::operator_add].into_iter(), + BlockOpcode::looks_say => [IrOpcode::looks_say].into_iter(), + other => hq_todo!("unimplemented block: {:?}", other), + }) + .collect()) +} + fn from_special_block(block_array: &BlockArray) -> HQResult { Ok(match block_array { BlockArray::NumberOrAngle(ty, value) => match ty { - 4 | 8 => IrOpcode::math_number(MathNumberFields(*value)), - 5 => IrOpcode::math_positive_number(MathPositiveNumberFields(*value)), - 6 => IrOpcode::math_whole_number(MathWholeNumberFields(*value as i64)), - 7 => IrOpcode::math_integer(MathIntegerFields(*value as i64)), + 4 | 5 | 8 => IrOpcode::hq_float(HqFloatFields(*value)), + 6 | 7 => IrOpcode::hq_integer(HqIntegerFields(*value as i64)), _ => hq_bad_proj!("bad project json (block array of type ({}, f64))", ty), }, BlockArray::ColorOrString(ty, value) => match ty { - 4 | 8 => IrOpcode::math_number(MathNumberFields( - value.parse().map_err(|_| make_hq_bug!(""))?, - )), - 5 => IrOpcode::math_positive_number(MathPositiveNumberFields( - value.parse().map_err(|_| make_hq_bug!(""))?, - )), - 6 => IrOpcode::math_whole_number(MathWholeNumberFields( - value.parse().map_err(|_| make_hq_bug!(""))?, - )), - 7 => IrOpcode::math_integer(MathIntegerFields( + 4 | 5 | 8 => { + IrOpcode::hq_float(HqFloatFields(value.parse().map_err(|_| make_hq_bug!(""))?)) + } + 6 | 7 => IrOpcode::hq_integer(HqIntegerFields( value.parse().map_err(|_| make_hq_bug!(""))?, )), 9 => hq_todo!(""), diff --git a/src/ir/proc.rs b/src/ir/proc.rs index e0587ab3..07eceaf1 100644 --- a/src/ir/proc.rs +++ b/src/ir/proc.rs @@ -157,7 +157,12 @@ impl Proc { step_context, project, )?, - None => RcStep::new(Rc::new(Step::new(None, step_context, Box::new([]), project))), + None => RcStep::new(Rc::new(Step::new( + None, + step_context, + Box::new([]), + project, + ))), }; Ok(Proc { first_step, diff --git a/src/lib.rs b/src/lib.rs index 67bc078c..e5e29abd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ #![cfg_attr(not(test), no_std)] #![doc(html_logo_url = "https://hyperquark.github.io/hyperquark/logo.png")] #![doc(html_favicon_url = "https://hyperquark.github.io/hyperquark/favicon.ico")] - #![allow(clippy::new_without_default)] #[macro_use] @@ -23,26 +22,6 @@ pub mod instructions; #[doc(inline)] pub use error::{HQError, HQErrorType, HQResult}; -// use wasm::wasm; - -#[cfg(target_family = "wasm")] -#[cfg_attr(target_family = "wasm", wasm_bindgen(js_namespace=console))] -extern "C" { - pub fn log(s: &str); -} - -#[cfg(test)] -pub fn log(s: &str) { - println!("{s}") -} - -// #[wasm_bindgen] -// pub fn sb3_to_wasm(proj: &str) -> Result { -// let mut ir_proj = ir::IrProject::try_from(sb3::Sb3Project::try_from(proj)?)?; -// ir_proj.optimise()?; -// ir_proj.try_into() -// } - /// commonly used _things_ which would be nice not to have to type out every time pub mod prelude { pub use crate::{HQError, HQResult}; @@ -61,3 +40,25 @@ pub mod prelude { pub type IndexMap = indexmap::IndexMap>; pub type IndexSet = indexmap::IndexSet>; } + +use prelude::*; + +// use wasm::wasm; + +#[cfg(target_family = "wasm")] +#[cfg_attr(target_family = "wasm", wasm_bindgen(js_namespace=console))] +extern "C" { + pub fn log(s: &str); +} + +#[cfg(test)] +pub fn log(s: &str) { + println!("{s}") +} + +#[cfg_attr(target_family = "wasm", wasm_bindgen)] +pub fn sb3_to_wasm(proj: &str) -> HQResult> { + let sb3_proj = sb3::Sb3Project::try_from(proj)?; + let ir_proj: Rc = sb3_proj.try_into()?; + wasm::WasmProject::try_from(ir_proj)?.finish() +} diff --git a/src/wasm/func.rs b/src/wasm/func.rs index bea68bfa..5ddc5ec9 100644 --- a/src/wasm/func.rs +++ b/src/wasm/func.rs @@ -1,12 +1,14 @@ use super::{ExternalFunctionMap, TypeRegistry}; use crate::prelude::*; -use wasm_encoder::{Function, Instruction, ValType}; +use wasm_encoder::{CodeSection, Function, FunctionSection, Instruction, ValType}; /// representation of a step's function +#[derive(Clone)] pub struct StepFunc { locals: RefCell>, instructions: RefCell>>, - param_count: u32, + params: Box<[ValType]>, + output: Option, type_registry: Rc, external_functions: Rc, } @@ -28,7 +30,8 @@ impl StepFunc { StepFunc { locals: RefCell::new(vec![]), instructions: RefCell::new(vec![]), - param_count: 1, + params: Box::new([ValType::I32]), + output: None, type_registry, external_functions, } @@ -36,16 +39,17 @@ impl StepFunc { /// creates a new step function with the specified amount of paramters. /// currently only used in testing to validate types - pub fn new_with_param_count( - count: usize, + pub fn new_with_types( + params: Box<[ValType]>, + output: Option, type_registry: Rc, external_functions: Rc, ) -> HQResult { Ok(StepFunc { locals: RefCell::new(vec![]), instructions: RefCell::new(vec![]), - param_count: u32::try_from(count) - .map_err(|_| make_hq_bug!("param count out of bounds"))?, + params, + output, type_registry, external_functions, }) @@ -63,28 +67,29 @@ impl StepFunc { .iter() .filter(|ty| **ty == val_type) .count(); - Ok(u32::try_from(if existing_count < (n as usize) { - { + u32::try_from( + if existing_count < (n as usize) { + { + self.locals + .borrow_mut() + .extend([val_type].repeat(n as usize - existing_count)); + } + self.locals.borrow().len() - 1 + } else { self.locals - .borrow_mut() - .extend([val_type].repeat(n as usize - existing_count)); - } - self.locals.borrow().len() - 1 - } else { - self.locals - .borrow() - .iter() - .enumerate() - .filter(|(_, ty)| **ty == val_type) - .map(|(i, _)| i) - .nth(n as usize - 1) - .ok_or(make_hq_bug!( - "couldn't find nth local of type {:?}", - val_type - ))? - }) - .map_err(|_| make_hq_bug!("local index was out of bounds"))? - + self.param_count) + .borrow() + .iter() + .enumerate() + .filter(|(_, ty)| **ty == val_type) + .map(|(i, _)| i) + .nth(n as usize - 1) + .ok_or(make_hq_bug!( + "couldn't find nth local of type {:?}", + val_type + ))? + } + self.params.len(), + ) + .map_err(|_| make_hq_bug!("local index was out of bounds")) } pub fn add_instructions(&self, instructions: impl IntoIterator>) { @@ -92,13 +97,23 @@ impl StepFunc { } /// Takes ownership of the function and returns the backing `wasm_encoder` `Function` - pub fn finish(self) -> Function { + pub fn finish(self, funcs: &mut FunctionSection, code: &mut CodeSection) -> HQResult<()> { let mut func = Function::new_with_locals_types(self.locals.take()); for instruction in self.instructions.take() { func.instruction(&instruction); } func.instruction(&Instruction::End); - func + let type_index = self.type_registry.type_index( + self.params.into(), + if let Some(output) = self.output { + vec![output] + } else { + vec![] + }, + )?; + funcs.function(type_index); + code.function(&func); + Ok(()) } } @@ -132,8 +147,13 @@ mod tests { #[test] fn get_local_works_with_valid_inputs_with_3_params() { - let func = - StepFunc::new_with_param_count(3, Default::default(), Default::default()).unwrap(); + let func = StepFunc::new_with_types( + [ValType::I32, ValType::F64, ValType::EXTERNREF].into(), // these are just arbitrary types + None, + Default::default(), + Default::default(), + ) + .unwrap(); assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 3); assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 3); diff --git a/src/wasm/project.rs b/src/wasm/project.rs index b89b42d5..fb17cc15 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -1,14 +1,16 @@ -use super::ExternalEnvironment; -use crate::ir::{IrProject, Type as IrType}; +use super::{ExternalEnvironment, ExternalFunctionMap}; +use crate::ir::{IrProject, Step, Type as IrType}; use crate::prelude::*; -use crate::wasm::{StepFunc, WasmFlags}; -use wasm_encoder::ValType; +use crate::wasm::{StepFunc, TypeRegistry, WasmFlags}; +use wasm_encoder::{CodeSection, FunctionSection, ImportSection, Module, TypeSection, ValType}; /// A respresntation of a WASM representation of a project. Cannot be created directly; /// use `TryFrom`. pub struct WasmProject { flags: WasmFlags, step_funcs: Box<[StepFunc]>, + type_registry: Rc, + external_functions: Rc, environment: ExternalEnvironment, } @@ -18,9 +20,19 @@ impl WasmProject { flags, step_funcs: Box::new([]), environment, + type_registry: Rc::new(TypeRegistry::new()), + external_functions: Rc::new(ExternalFunctionMap::new()), } } + pub fn type_registry(&self) -> Rc { + Rc::clone(&self.type_registry) + } + + pub fn external_functions(&self) -> Rc { + Rc::clone(&self.external_functions) + } + pub fn flags(&self) -> &WasmFlags { &self.flags } @@ -49,16 +61,82 @@ impl WasmProject { } pub fn finish(self) -> HQResult> { - hq_todo!() + let mut module = Module::new(); + + let mut imports = ImportSection::new(); + let mut types = TypeSection::new(); + let mut functions = FunctionSection::new(); + let mut codes = CodeSection::new(); + + Rc::unwrap_or_clone(self.external_functions()) + .finish(&mut imports, self.type_registry())?; + for step_func in self.step_funcs().iter().cloned() { + step_func.finish(&mut functions, &mut codes)?; + } + Rc::unwrap_or_clone(self.type_registry()).finish(&mut types); + + module.section(&types); + module.section(&imports); + module.section(&functions); + module.section(&codes); + + let wasm_bytes = module.finish(); + + Ok(wasm_bytes) + } +} + +fn compile_step( + step: Rc, + steps: &RefCell, StepFunc>>, + type_registry: Rc, + external_funcs: Rc, +) -> HQResult<()> { + if steps.borrow().contains_key(&step) { + return Ok(()); } + let step_func = StepFunc::new(type_registry, external_funcs); + let mut instrs = vec![]; + let mut type_stack = vec![]; + for opcode in step.opcodes() { + let inputs = type_stack + .splice( + (type_stack.len() - 1 - opcode.acceptable_inputs().len()).., + [], + ) + .collect(); + instrs.append(&mut opcode.wasm(&step_func, Rc::clone(&inputs))?); + if let Some(output) = opcode.output_type(inputs)? { + type_stack.push(output); + } + } + step_func.add_instructions(instrs); + steps.borrow_mut().insert(step, step_func); + Ok(()) } -impl TryFrom for WasmProject { +impl TryFrom> for WasmProject { type Error = HQError; - fn try_from(ir_project: IrProject) -> HQResult { - let mut steps: Vec = vec![]; - for thread in ir_project.threads().borrow().iter() {} - hq_todo!() + fn try_from(ir_project: Rc) -> HQResult { + let steps: RefCell, StepFunc>> = Default::default(); + let type_registry = Rc::new(TypeRegistry::new()); + let external_functions = Rc::new(ExternalFunctionMap::new()); + for thread in ir_project.threads().borrow().iter() { + let step = thread.first_step().get_rc(); + compile_step( + step, + &steps, + Rc::clone(&type_registry), + Rc::clone(&external_functions), + )?; + } + Ok(WasmProject { + flags: Default::default(), + step_funcs: steps.take().values().cloned().collect(), + type_registry, + external_functions, + environment: ExternalEnvironment::WebBrowser, + }) } } From c7f072957d415ba1094fe3b9c6923291112d7860 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sat, 4 Jan 2025 17:53:28 +0000 Subject: [PATCH 10/98] make things work(ish) in the playground --- playground/components/ProjectPlayer.vue | 21 ++++++++++++++++----- src/instructions/utilities.rs | 1 - src/lib.rs | 4 ++-- src/wasm/project.rs | 2 +- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/playground/components/ProjectPlayer.vue b/playground/components/ProjectPlayer.vue index d6fb9cf8..8b7d672d 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -19,7 +19,7 @@ - \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index b574d6db..f8309233 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,4 @@ use alloc::boxed::Box; -#[cfg(target_family = "wasm")] use wasm_bindgen::JsValue; pub type HQResult = Result; @@ -19,7 +18,6 @@ pub enum HQErrorType { Unimplemented, } -#[cfg(target_family = "wasm")] impl From for JsValue { fn from(val: HQError) -> JsValue { JsValue::from_str(match val.err_type { diff --git a/src/instructions.rs b/src/instructions.rs index 3f2c75c5..8a30989b 100644 --- a/src/instructions.rs +++ b/src/instructions.rs @@ -7,7 +7,6 @@ mod hq; mod looks; mod operator; #[macro_use] -mod utilities; -pub use utilities::{file_block_category, file_block_name, file_opcode}; +mod tests; include!(concat!(env!("OUT_DIR"), "/ir-opcodes.rs")); diff --git a/src/instructions/hq.rs b/src/instructions/hq.rs index 0a5574bb..6cec2c5e 100644 --- a/src/instructions/hq.rs +++ b/src/instructions/hq.rs @@ -1,3 +1,4 @@ pub mod _yield; pub mod float; pub mod integer; +pub mod text; diff --git a/src/instructions/hq/_yield.rs b/src/instructions/hq/_yield.rs index 6b734d7e..376fcb2f 100644 --- a/src/instructions/hq/_yield.rs +++ b/src/instructions/hq/_yield.rs @@ -70,4 +70,4 @@ pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult, &Fields(val): &Fields) -> HQResult, &Fields(val): &Fields) -> HQResult); + +pub fn wasm( + func: &StepFunc, + _inputs: Rc<[IrType]>, + fields: &Fields, +) -> HQResult>> { + let string_idx = func + .registries() + .strings() + .register_default(fields.0.clone())?; + Ok(vec![ + Instruction::I32Const(string_idx), + Instruction::TableGet( + func.registries() + .tables() + .register("strings".into(), (RefType::EXTERNREF, 0))?, + ), + ]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, Fields(val): &Fields) -> HQResult> { + Ok(Some(match &**val { + bool if bool.to_lowercase() == "true" || bool.to_lowercase() == "false" => { + IrType::StringBoolean + } + num if num.parse::().is_ok() => { + if num.parse::().unwrap().is_nan() { + IrType::StringNan + } else { + IrType::StringNumber + } + } + _ => IrType::StringNan, + })) +} + +crate::instructions_test! {tests;@ super::Fields("hello, world!".into())} diff --git a/src/instructions/looks/say.rs b/src/instructions/looks/say.rs index a96b9604..9b67bca4 100644 --- a/src/instructions/looks/say.rs +++ b/src/instructions/looks/say.rs @@ -5,20 +5,16 @@ use wasm_encoder::{Instruction, ValType}; pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult>> { Ok(if IrType::QuasiInt.contains(inputs[0]) { - let func_index = func.external_functions().function_index( - "looks", - "say_int", - vec![ValType::I32], - vec![], - )?; + let func_index = func + .registries() + .external_functions() + .register(("looks", "say_int"), (vec![ValType::I32], vec![]))?; vec![Instruction::Call(func_index)] } else if IrType::Float.contains(inputs[0]) { - let func_index = func.external_functions().function_index( - "looks", - "say_float", - vec![ValType::F64], - vec![], - )?; + let func_index = func + .registries() + .external_functions() + .register(("looks", "say_float"), (vec![ValType::F64], vec![]))?; vec![Instruction::Call(func_index)] } else { hq_todo!() diff --git a/src/instructions/operator/add.rs b/src/instructions/operator/add.rs index 7538d709..0995d245 100644 --- a/src/instructions/operator/add.rs +++ b/src/instructions/operator/add.rs @@ -53,4 +53,4 @@ pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { })) } -crate::instructions_test! {tests; t1, t2} +crate::instructions_test! {tests; t1, t2 ;} diff --git a/src/instructions/utilities.rs b/src/instructions/tests.rs similarity index 78% rename from src/instructions/utilities.rs rename to src/instructions/tests.rs index 3c389339..6b7b3b52 100644 --- a/src/instructions/utilities.rs +++ b/src/instructions/tests.rs @@ -1,51 +1,18 @@ -mod file_opcode { - //! instruction module paths look something like - //! hyperquark::instructions::category::block - //! so if we split it by ':', we end up with 7 chunks - - use crate::prelude::*; - use split_exact::SplitExact; - - pub fn file_block_category(path: &'static str) -> &'static str { - path.split_exact::<7>(|c| c == ':')[4].unwrap() - } - - pub fn file_block_name(path: &'static str) -> &'static str { - path.split_exact::<7>(|c| c == ':')[6].unwrap() - } - - pub fn file_opcode(path: &'static str) -> String { - format!("{}_{}", file_block_category(path), file_block_name(path)) - } -} -pub use file_opcode::*; - -#[cfg(test)] -pub mod tests { - #[test] - fn file_block_category() { - assert_eq!(super::file_block_category(module_path!()), "utilities"); - } - - #[test] - fn file_block_name() { - assert_eq!(super::file_block_name(module_path!()), "tests"); - } - - #[test] - fn file_opcode() { - assert_eq!(super::file_opcode(module_path!()), "utilities_tests"); - } -} - /// generates unit tests for instructions files. Takes a module name, followed by a semicolon, followed by an optional comma-separated list of arbitrary identifiers /// corresponding to the number of inputs the block takes, optionally followed by a semicolon and an expression /// for a sensible default for any fields; if multiple field values need to be tested, the macro can be repeated. #[macro_export] macro_rules! instructions_test { - {$module:ident; $($type_arg:ident $(,)?)* $(; $fields:expr)?} => { + {$module:ident; $($type_arg:ident $(,)?)* $(@$fields:expr)? $(;)?} => { + $crate::instructions_test!{$module; $($type_arg,)* $(@$fields)? ; Default::default()} + }; + {$module:ident; $($type_arg:ident $(,)?)* $(@ $fields:expr)? ; $flags:expr} => { #[cfg(test)] mod $module { + fn flags() -> $crate::wasm::WasmFlags { + $flags + } + use super::{wasm, output_type, acceptable_inputs}; use $crate::prelude::*; use $crate::ir::Type as IrType; @@ -75,12 +42,11 @@ macro_rules! instructions_test { #[test] fn output_type_fails_when_wasm_fails() { - use $crate::wasm::{StepFunc, TypeRegistry, ExternalFunctionMap}; + use $crate::wasm::{StepFunc, Registries}; for ($($type_arg,)*) in types_iter() { let output_type_result = output_type(Rc::new([$($type_arg,)*]), $(&$fields)?); - let type_registry = Rc::new(TypeRegistry::new()); - let external_functions = Rc::new(ExternalFunctionMap::new()); - let step_func = StepFunc::new(type_registry.clone(), external_functions.clone()); + let registries = Rc::new(Registries::default()); + let step_func = StepFunc::new(Rc::clone(®istries), flags()); let wasm_result = wasm(&step_func, Rc::new([$($type_arg,)*]), $(&$fields)?); match (output_type_result.clone(), wasm_result.clone()) { (Err(..), Ok(..)) | (Ok(..), Err(..)) => panic!("output_type result doesn't match wasm result for type(s) {:?}:\noutput_type: {:?},\nwasm: {:?}", ($($type_arg,)*), output_type_result, wasm_result), @@ -97,9 +63,9 @@ macro_rules! instructions_test { #[test] fn wasm_output_type_matches_expected_output_type() -> HQResult<()> { use wasm_encoder::{ - CodeSection, FunctionSection, ImportSection, Instruction, Module, TypeSection, MemorySection, MemoryType + CodeSection, FunctionSection, ImportSection, Instruction, Module, TableSection, TypeSection, MemorySection, MemoryType }; - use $crate::wasm::{StepFunc, TypeRegistry, ExternalFunctionMap}; + use $crate::wasm::{StepFunc, Registries}; use $crate::prelude::Rc; for ($($type_arg,)*) in types_iter() { @@ -110,8 +76,7 @@ macro_rules! instructions_test { continue; } }; - let type_registry = Rc::new(TypeRegistry::new()); - let external_functions = Rc::new(ExternalFunctionMap::new()); + let registries = Rc::new(Registries::default()); let wasm_proj = $crate::wasm::WasmProject::new(Default::default(), $crate::wasm::ExternalEnvironment::WebBrowser); let types: &[IrType] = &[$($type_arg,)*]; let params = [Ok(ValType::I32)].into_iter().chain([$($type_arg,)*].into_iter().map(|ty| wasm_proj.ir_type_to_wasm(ty))).collect::>>()?; @@ -119,7 +84,7 @@ macro_rules! instructions_test { Some(output) => Some(wasm_proj.ir_type_to_wasm(output)?), None => None, }; - let step_func = StepFunc::new_with_types(params.into(), result, Rc::clone(&type_registry), external_functions.clone())?; + let step_func = StepFunc::new_with_types(params.into(), result, Rc::clone(®istries), flags())?; let wasm = match wasm(&step_func, Rc::new([$($type_arg,)*]), $(&$fields)?) { Ok(a) => a, Err(_) => { @@ -136,6 +101,7 @@ macro_rules! instructions_test { let mut imports = ImportSection::new(); let mut types = TypeSection::new(); + let mut tables = TableSection::new(); let mut functions = FunctionSection::new(); let mut codes = CodeSection::new(); let mut memories = MemorySection::new(); @@ -148,13 +114,15 @@ macro_rules! instructions_test { page_size_log2: None, }); - Rc::unwrap_or_clone(external_functions).finish(&mut imports, type_registry.clone())?; + registries.external_functions().clone().finish(&mut imports, registries.types())?; step_func.finish(&mut functions, &mut codes)?; - Rc::unwrap_or_clone(type_registry).finish(&mut types); + registries.types().clone().finish(&mut types); + registries.tables().clone().finish(& mut tables); module.section(&types); module.section(&imports); module.section(&functions); + module.section(&tables); module.section(&memories); module.section(&codes); @@ -176,20 +144,19 @@ macro_rules! instructions_test { #[test] fn js_functions_match_declared_types() { - use $crate::wasm::{ExternalFunctionMap, StepFunc, TypeRegistry}; + use $crate::wasm::{Registries, StepFunc,}; use ezno_lib::{check as check_js, Diagnostic}; use std::path::{Path, PathBuf}; use std::fs; for ($($type_arg,)*) in types_iter() { - let type_registry = Rc::new(TypeRegistry::new()); - let external_functions = Rc::new(ExternalFunctionMap::new()); - let step_func = StepFunc::new(type_registry.clone(), external_functions.clone()); + let registries = Rc::new(Registries::default()); + let step_func = StepFunc::new(Rc::clone(®istries), flags()); if wasm(&step_func, Rc::new([$($type_arg,)*]), $(&$fields)?).is_err() { println!("skipping failed wasm"); continue; }; - for ((module, name), (params, results)) in external_functions.get_map().borrow().iter() { + for ((module, name), (params, results)) in registries.external_functions().registry().borrow().iter() { assert!(results.len() < 1, "external function {}::{} registered as returning multiple results", module, name); let out = if results.len() == 0 { "void" diff --git a/src/ir.rs b/src/ir.rs index 91c5d19b..c6af05c0 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -8,11 +8,11 @@ mod target; mod thread; mod types; -pub use context::StepContext; -pub use event::Event; -pub use proc::{Proc, ProcMap, ProcRegistry, ProcedureContext}; -pub use project::IrProject; -pub use step::{RcStep, Step}; -pub use target::Target; -pub use thread::Thread; -pub use types::{Type, TypeStack}; +use context::StepContext; +pub(crate) use event::Event; +use proc::{Proc, ProcRegistry, ProcedureContext}; +pub(crate) use project::IrProject; +pub(crate) use step::{RcStep, Step}; +use target::Target; +use thread::Thread; +pub(crate) use types::{Type, TypeStack}; diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index 9aed30f0..ef2e13cf 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -3,6 +3,8 @@ use crate::prelude::*; use crate::sb3::{Block, BlockArray, BlockArrayOrId, BlockInfo, BlockMap, BlockOpcode, Input}; use fields::*; +// TODO: insert casts in relevant places + pub fn from_block(block: &Block, blocks: &BlockMap) -> HQResult> { Ok(match block { Block::Normal { block_info, .. } => { diff --git a/src/ir/proc.rs b/src/ir/proc.rs index 07eceaf1..d7bc7112 100644 --- a/src/ir/proc.rs +++ b/src/ir/proc.rs @@ -1,5 +1,6 @@ use super::{IrProject, RcStep, Step, StepContext, Target, Type as IrType}; use crate::prelude::*; +use crate::registry::MapRegistry; use crate::sb3::{BlockMap, BlockOpcode}; use lazy_regex::{lazy_regex, Lazy}; use regex::Regex; @@ -171,20 +172,9 @@ impl Proc { } } -pub type ProcMap = IndexMap, Rc>; - -#[derive(Clone, Default)] -pub struct ProcRegistry(RefCell); +pub type ProcRegistry = MapRegistry, Rc>; impl ProcRegistry { - pub fn new() -> Self { - ProcRegistry(RefCell::new(Default::default())) - } - - pub(crate) fn get_map(&self) -> &RefCell { - &self.0 - } - /// get the `Proc` for the specified proccode, creating it if it doesn't already exist pub fn proc( &self, @@ -194,17 +184,24 @@ impl ProcRegistry { expect_warp: bool, project: Weak, ) -> HQResult> { + let idx = self.register( + proccode.clone(), + Rc::new(Proc::from_proccode( + proccode, + blocks, + target, + expect_warp, + project, + )?), + )?; Ok(Rc::clone( - self.get_map() - .borrow_mut() - .entry(proccode.clone()) - .or_insert(Rc::new(Proc::from_proccode( - proccode, - blocks, - target, - expect_warp, - project, - )?)), + self.registry() + .borrow() + .get_index(idx) + .ok_or(make_hq_bug!( + "recently inserted proc not found in ProcRegistry" + ))? + .1, )) } } diff --git a/src/lib.rs b/src/lib.rs index c0708637..eeb49c3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,23 +7,25 @@ extern crate alloc; extern crate enum_field_getter; -#[cfg(target_family = "wasm")] use wasm_bindgen::prelude::*; #[macro_use] mod error; -pub mod ir; +mod ir; // pub mod ir_opt; -pub mod sb3; -pub mod wasm; +mod sb3; +mod wasm; #[macro_use] -pub mod instructions; +mod instructions; #[doc(inline)] pub use error::{HQError, HQErrorType, HQResult}; +mod registry; + /// commonly used _things_ which would be nice not to have to type out every time pub mod prelude { + pub use crate::registry::{Registry, RegistryDefault}; pub use crate::{HQError, HQResult}; pub use alloc::boxed::Box; pub use alloc::collections::BTreeMap; @@ -46,7 +48,7 @@ use prelude::*; // use wasm::wasm; #[cfg(target_family = "wasm")] -#[cfg_attr(target_family = "wasm", wasm_bindgen(js_namespace=console))] +#[wasm_bindgen(js_namespace=console)] extern "C" { pub fn log(s: &str); } @@ -56,11 +58,10 @@ pub fn log(s: &str) { println!("{s}") } -#[cfg_attr(target_family = "wasm", wasm_bindgen)] -pub fn sb3_to_wasm(proj: &str) -> HQResult> { +#[cfg(feature = "compiler")] +#[wasm_bindgen] +pub fn sb3_to_wasm(proj: &str, flags: wasm::WasmFlags) -> HQResult { let sb3_proj = sb3::Sb3Project::try_from(proj)?; let ir_proj: Rc = sb3_proj.try_into()?; - Ok(wasm::WasmProject::try_from(ir_proj)? - .finish()? - .into_boxed_slice()) + wasm::WasmProject::from_ir(ir_proj, flags)?.finish() } diff --git a/src/registry.rs b/src/registry.rs new file mode 100644 index 00000000..136e55a6 --- /dev/null +++ b/src/registry.rs @@ -0,0 +1,96 @@ +use crate::prelude::*; +use core::hash::Hash; + +#[derive(Clone)] +pub struct MapRegistry(RefCell>) +where + K: Hash + Eq + Clone; + +// deriving Default doesn't work if V: !Default, so we implement it manually +impl Default for MapRegistry +where + K: Hash + Eq + Clone, +{ + fn default() -> MapRegistry { + MapRegistry(RefCell::new(Default::default())) + } +} + +impl Registry for MapRegistry +where + K: Hash + Eq + Clone, +{ + type Key = K; + type Value = V; + + fn registry(&self) -> &RefCell> { + &self.0 + } +} + +#[derive(Clone)] +pub struct SetRegistry(RefCell>) +where + K: Hash + Eq + Clone; + +impl Default for SetRegistry +where + K: Hash + Eq + Clone, +{ + fn default() -> SetRegistry { + SetRegistry(RefCell::new(Default::default())) + } +} + +impl Registry for SetRegistry +where + K: Hash + Eq + Clone, +{ + type Key = K; + type Value = (); + + fn registry(&self) -> &RefCell> { + &self.0 + } +} + +pub trait Registry: Sized { + const IS_SET: bool = false; + + type Key: Hash + Clone + Eq; + type Value; + + fn registry(&self) -> &RefCell>; + + /// get the index of the specified item, inserting it if it doesn't exist in the map already. + /// Doesn't check if the provided value matches what's already there. This is generic because + /// various different numeric types are needed in different places, so it's easiest to encapsulate + /// the casting logic in here. + fn register(&self, key: Self::Key, value: Self::Value) -> HQResult + where + N: TryFrom, + { + self.registry() + .borrow_mut() + .entry(key.clone()) + .or_insert(value); + N::try_from( + self.registry() + .borrow() + .get_index_of(&key) + .ok_or(make_hq_bug!("couldn't find entry in Registry"))?, + ) + .map_err(|_| make_hq_bug!("registry item index out of bounds")) + } +} + +pub trait RegistryDefault: Registry { + fn register_default(&self, key: Self::Key) -> HQResult + where + N: TryFrom, + { + self.register(key, Default::default()) + } +} + +impl RegistryDefault for R where R: Registry {} diff --git a/src/wasm.rs b/src/wasm.rs index 56af5de3..ffc718c0 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -2,10 +2,16 @@ mod external; mod flags; mod func; mod project; +mod registries; +mod strings; +mod tables; mod type_registry; -pub use external::{ExternalEnvironment, ExternalFunctionMap}; +pub(crate) use external::{ExternalEnvironment, ExternalFunctionRegistry}; pub use flags::WasmFlags; -pub use func::StepFunc; -pub use project::{byte_offset, WasmProject}; -pub use type_registry::TypeRegistry; +pub(crate) use func::StepFunc; +pub(crate) use project::{byte_offset, FinishedWasm, WasmProject}; +pub(crate) use registries::Registries; +pub(crate) use strings::StringRegistry; +pub(crate) use tables::TableRegistry; +pub(crate) use type_registry::TypeRegistry; diff --git a/src/wasm/external.rs b/src/wasm/external.rs index fb847739..e258ca0c 100644 --- a/src/wasm/external.rs +++ b/src/wasm/external.rs @@ -1,50 +1,15 @@ use super::TypeRegistry; use crate::prelude::*; +use crate::registry::MapRegistry; use wasm_encoder::{EntityType, ImportSection, ValType}; -pub type FunctionMap = IndexMap<(&'static str, &'static str), (Vec, Vec)>; +pub type ExternalFunctionRegistry = + MapRegistry<(&'static str, &'static str), (Vec, Vec)>; -#[derive(Clone, Default)] -pub struct ExternalFunctionMap(RefCell); - -impl ExternalFunctionMap { - pub fn new() -> Self { - ExternalFunctionMap(RefCell::new(Default::default())) - } - - pub(crate) fn get_map(&self) -> &RefCell { - &self.0 - } - - /// get the index of the specified function, inserting it if it doesn't exist in the map already. - /// Doesn't check if the provided params/results types match what's already there. - pub fn function_index( - &self, - module: &'static str, - name: &'static str, - params: Vec, - results: Vec, - ) -> HQResult { - self.get_map() - .borrow_mut() - .entry((module, name)) - .or_insert((params, results)); - u32::try_from( - self.get_map() - .borrow() - .get_index_of(&(module, name)) - .ok_or(make_hq_bug!("couldn't find entry in ExternalFunctionMap"))?, - ) - .map_err(|_| make_hq_bug!("external function index out of bounds")) - } - - pub fn finish( - self, - imports: &mut ImportSection, - type_registry: Rc, - ) -> HQResult<()> { - for ((module, name), (params, results)) in self.get_map().take() { - let type_index = type_registry.type_index(params, results)?; +impl ExternalFunctionRegistry { + pub fn finish(self, imports: &mut ImportSection, type_registry: &TypeRegistry) -> HQResult<()> { + for ((module, name), (params, results)) in self.registry().take() { + let type_index = type_registry.register_default((params, results))?; imports.import(module, name, EntityType::Function(type_index)); } Ok(()) diff --git a/src/wasm/flags.rs b/src/wasm/flags.rs index 0d183c07..7077bd25 100644 --- a/src/wasm/flags.rs +++ b/src/wasm/flags.rs @@ -1,7 +1,14 @@ +use crate::prelude::*; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + #[non_exhaustive] +#[derive(Copy, Clone, Serialize, Deserialize)] +#[wasm_bindgen] pub enum WasmStringType { + /// externref strings - this will automatically use the JS string builtins proposal if available ExternRef, - JsString, + Manual, } impl Default for WasmStringType { @@ -12,7 +19,28 @@ impl Default for WasmStringType { /// compilation flags #[non_exhaustive] -#[derive(Default)] +#[derive(Default, Copy, Clone, Serialize, Deserialize)] +#[wasm_bindgen] pub struct WasmFlags { pub string_type: WasmStringType, } + +#[wasm_bindgen] +impl WasmFlags { + #[wasm_bindgen] + pub fn from_js(js: JsValue) -> HQResult { + serde_wasm_bindgen::from_value(js) + .map_err(|_| make_hq_bug!("couldn't convert JsValue to WasmFlags")) + } + + #[wasm_bindgen] + pub fn to_js(&self) -> HQResult { + serde_wasm_bindgen::to_value(&self) + .map_err(|_| make_hq_bug!("couldn't convert WasmFlags to JsValue")) + } + + #[wasm_bindgen(constructor)] + pub fn new() -> WasmFlags { + Default::default() + } +} diff --git a/src/wasm/func.rs b/src/wasm/func.rs index 740289b0..c6fc2126 100644 --- a/src/wasm/func.rs +++ b/src/wasm/func.rs @@ -1,4 +1,4 @@ -use super::{ExternalFunctionMap, TypeRegistry}; +use super::{Registries, WasmFlags}; use crate::prelude::*; use wasm_encoder::{CodeSection, Function, FunctionSection, Instruction, ValType}; @@ -9,31 +9,28 @@ pub struct StepFunc { instructions: RefCell>>, params: Box<[ValType]>, output: Option, - type_registry: Rc, - external_functions: Rc, + registries: Rc, + flags: WasmFlags, } impl StepFunc { - pub fn type_registry(&self) -> Rc { - self.type_registry.clone() + pub fn registries(&self) -> Rc { + Rc::clone(&self.registries) } - pub fn external_functions(&self) -> Rc { - self.external_functions.clone() + pub fn flags(&self) -> WasmFlags { + self.flags } /// creates a new step function, with one paramter - pub fn new( - type_registry: Rc, - external_functions: Rc, - ) -> Self { + pub fn new(registries: Rc, flags: WasmFlags) -> Self { StepFunc { locals: RefCell::new(vec![]), instructions: RefCell::new(vec![]), params: Box::new([ValType::I32]), output: Some(ValType::I32), - type_registry, - external_functions, + registries, + flags, } } @@ -42,16 +39,16 @@ impl StepFunc { pub fn new_with_types( params: Box<[ValType]>, output: Option, - type_registry: Rc, - external_functions: Rc, + registries: Rc, + flags: WasmFlags, ) -> HQResult { Ok(StepFunc { locals: RefCell::new(vec![]), instructions: RefCell::new(vec![]), params, output, - type_registry, - external_functions, + registries, + flags, }) } @@ -103,14 +100,14 @@ impl StepFunc { func.instruction(&instruction); } func.instruction(&Instruction::End); - let type_index = self.type_registry.type_index( + let type_index = self.registries().types().register_default(( self.params.into(), if let Some(output) = self.output { vec![output] } else { vec![] }, - )?; + ))?; funcs.function(type_index); code.function(&func); Ok(()) diff --git a/src/wasm/project.rs b/src/wasm/project.rs index fcfd96d7..baf7142f 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -1,7 +1,8 @@ -use super::{ExternalEnvironment, ExternalFunctionMap}; +use super::{ExternalEnvironment, Registries}; use crate::ir::{Event, IrProject, Step, Type as IrType}; use crate::prelude::*; -use crate::wasm::{StepFunc, TypeRegistry, WasmFlags}; +use crate::wasm::{StepFunc, WasmFlags}; +use wasm_bindgen::prelude::*; use wasm_encoder::{ BlockType as WasmBlockType, CodeSection, ConstExpr, DataCountSection, DataSection, ElementSection, Elements, ExportKind, ExportSection, Function, FunctionSection, ImportSection, @@ -15,11 +16,6 @@ pub mod byte_offset { pub const THREADS: i32 = 8; } -mod table_indices { - pub const STEP_FUNCS: u32 = 0; - pub const STRINGS: u32 = 1; -} - /// A respresentation of a WASM representation of a project. Cannot be created directly; /// use `TryFrom`. pub struct WasmProject { @@ -28,8 +24,7 @@ pub struct WasmProject { /// maps an event to a list of *step_func* indices (NOT function indices) which are /// triggered by that event. events: BTreeMap>, - type_registry: Rc, - external_functions: Rc, + registries: Rc, environment: ExternalEnvironment, } @@ -40,21 +35,12 @@ impl WasmProject { step_funcs: Box::new([]), events: Default::default(), environment, - type_registry: Rc::new(TypeRegistry::new()), - external_functions: Rc::new(ExternalFunctionMap::new()), + registries: Rc::new(Registries::default()), } } - pub fn type_registry(&self) -> Rc { - Rc::clone(&self.type_registry) - } - - pub fn external_functions(&self) -> Rc { - Rc::clone(&self.external_functions) - } - - pub fn flags(&self) -> &WasmFlags { - &self.flags + pub fn registries(&self) -> Rc { + Rc::clone(&self.registries) } pub fn environment(&self) -> ExternalEnvironment { @@ -80,7 +66,7 @@ impl WasmProject { }) } - pub fn finish(self) -> HQResult> { + pub fn finish(self) -> HQResult { let mut module = Module::new(); let mut memories = MemorySection::new(); @@ -93,23 +79,6 @@ impl WasmProject { let mut elements = ElementSection::new(); let mut data = DataSection::new(); - tables.table(TableType { - element_type: RefType::FUNCREF, - minimum: self - .step_funcs() - .len() - .try_into() - .map_err(|_| make_hq_bug!("step_funcs length out of bounds"))?, - maximum: Some( - self.step_funcs() - .len() - .try_into() - .map_err(|_| make_hq_bug!("step_funcs length out of bounds"))?, - ), - table64: false, - shared: false, - }); - memories.memory(MemoryType { minimum: 1, maximum: None, @@ -123,15 +92,25 @@ impl WasmProject { .map_err(|_| make_hq_bug!("step_funcs length out of bounds"))? + self.imported_func_count()?)) .collect::>(); + let step_func_table_idx = self.registries().tables().register( + "step_funcs".into(), + ( + RefType::FUNCREF, + u64::try_from(step_indices.len()) + .map_err(|_| make_hq_bug!("step indices length out of bounds"))?, + ), + )?; let step_func_indices = Elements::Functions(step_indices.into()); elements.active( - Some(table_indices::STEP_FUNCS), + Some(step_func_table_idx), &ConstExpr::i32_const(0), step_func_indices, ); - Rc::unwrap_or_clone(self.external_functions()) - .finish(&mut imports, self.type_registry())?; + self.registries() + .external_functions() + .clone() + .finish(&mut imports, self.registries().types())?; for step_func in self.step_funcs().iter().cloned() { step_func.finish(&mut functions, &mut codes)?; } @@ -142,7 +121,9 @@ impl WasmProject { self.unreachable_dbg_func(&mut functions, &mut codes, &mut exports)?; - Rc::unwrap_or_clone(self.type_registry()).finish(&mut types); + self.registries().types().clone().finish(&mut types); + + self.registries().tables().clone().finish(&mut tables); let data_count = DataCountSection { count: data.len() }; @@ -164,12 +145,16 @@ impl WasmProject { let wasm_bytes = module.finish(); - Ok(wasm_bytes) + Ok(FinishedWasm { + wasm_bytes: wasm_bytes.into_boxed_slice(), + strings: self.registries().strings().clone().finish(), + }) } fn imported_func_count(&self) -> HQResult { - self.external_functions() - .get_map() + self.registries() + .external_functions() + .registry() .borrow() .len() .try_into() @@ -179,13 +164,13 @@ impl WasmProject { fn compile_step( step: Rc, steps: &RefCell, StepFunc>>, - type_registry: Rc, - external_funcs: Rc, + registries: Rc, + flags: WasmFlags, ) -> HQResult<()> { if steps.borrow().contains_key(&step) { return Ok(()); } - let step_func = StepFunc::new(type_registry, external_funcs); + let step_func = StepFunc::new(registries, flags); let mut instrs = vec![]; let mut type_stack = vec![]; for opcode in step.opcodes() { @@ -212,7 +197,11 @@ impl WasmProject { func.instruction(&Instruction::Unreachable); func.instruction(&Instruction::End); codes.function(&func); - functions.function(self.type_registry().type_index(vec![], vec![])?); + functions.function( + self.registries() + .types() + .register_default((vec![], vec![]))?, + ); exports.export( "unreachable_dbg", ExportKind::Func, @@ -299,7 +288,11 @@ impl WasmProject { func.instruction(&instruction); } - funcs.function(self.type_registry().type_index(vec![], vec![])?); + funcs.function( + self.registries() + .types() + .register_default((vec![], vec![]))?, + ); codes.function(&func); exports.export( export_name, @@ -377,9 +370,13 @@ impl WasmProject { }), Instruction::CallIndirect { type_index: self - .type_registry() - .type_index(vec![ValType::I32], vec![ValType::I32])?, - table_index: table_indices::STEP_FUNCS, + .registries() + .types() + .register_default((vec![ValType::I32], vec![ValType::I32]))?, + table_index: self + .registries() + .tables() + .register("step_funcs".into(), (RefType::FUNCREF, 0))?, }, Instruction::If(WasmBlockType::Empty), Instruction::LocalGet(0), @@ -402,7 +399,11 @@ impl WasmProject { tick_func.instruction(&instr); } tick_func.instruction(&Instruction::End); - funcs.function(self.type_registry().type_index(vec![], vec![])?); + funcs.function( + self.registries() + .types() + .register_default((vec![], vec![]))?, + ); codes.function(&tick_func); exports.export( "tick", @@ -411,30 +412,20 @@ impl WasmProject { ); Ok(()) } -} - -impl TryFrom> for WasmProject { - type Error = HQError; - fn try_from(ir_project: Rc) -> HQResult { + pub fn from_ir(ir_project: Rc, flags: WasmFlags) -> HQResult { let steps: RefCell, StepFunc>> = Default::default(); - let type_registry = Rc::new(TypeRegistry::new()); - let external_functions = Rc::new(ExternalFunctionMap::new()); + let registries = Rc::new(Registries::default()); let mut events: BTreeMap> = Default::default(); WasmProject::compile_step( Rc::new(Step::new_empty()), &steps, - Rc::clone(&type_registry), - Rc::clone(&external_functions), + Rc::clone(®istries), + flags, )?; for thread in ir_project.threads().borrow().iter() { let step = thread.first_step().get_rc(); - WasmProject::compile_step( - step, - &steps, - Rc::clone(&type_registry), - Rc::clone(&external_functions), - )?; + WasmProject::compile_step(step, &steps, Rc::clone(®istries), flags)?; events.entry(thread.event()).or_default().push( u32::try_from( ir_project @@ -450,47 +441,53 @@ impl TryFrom> for WasmProject { ); } Ok(WasmProject { - flags: Default::default(), + flags, step_funcs: steps.take().values().cloned().collect(), events, - type_registry, - external_functions, + registries, environment: ExternalEnvironment::WebBrowser, }) } } +#[wasm_bindgen] +#[derive(Clone)] +pub struct FinishedWasm { + #[wasm_bindgen(getter_with_clone)] + pub wasm_bytes: Box<[u8]>, + #[wasm_bindgen(getter_with_clone)] + pub strings: Vec, +} + #[cfg(test)] mod tests { use wasm_encoder::Instruction; - use super::WasmProject; + use super::{Registries, WasmProject}; use crate::ir::Event; use crate::prelude::*; - use crate::wasm::{ExternalEnvironment, ExternalFunctionMap, StepFunc, TypeRegistry}; + use crate::wasm::{ExternalEnvironment, StepFunc}; #[test] fn empty_project_is_valid_wasm() { let proj = WasmProject::new(Default::default(), ExternalEnvironment::WebBrowser); - let wasm_bytes = proj.finish().unwrap(); + let wasm_bytes = proj.finish().unwrap().wasm_bytes; wasmparser::validate(&wasm_bytes).unwrap(); } #[test] fn project_with_one_empty_step_is_valid_wasm() { - let types = Rc::new(TypeRegistry::new()); - let external_funcs = Rc::new(ExternalFunctionMap::new()); - let step_func = StepFunc::new(Rc::clone(&types), Rc::clone(&external_funcs)); + let registries = Rc::new(Registries::default()); + let step_func = StepFunc::new(Rc::clone(®istries), Default::default()); step_func.add_instructions(vec![Instruction::I32Const(0)]); // this is handled by compile_step in a non-test environment let project = WasmProject { flags: Default::default(), step_funcs: Box::new([step_func]), events: BTreeMap::from_iter(vec![(Event::FlagCLicked, vec![0])].into_iter()), environment: ExternalEnvironment::WebBrowser, - type_registry: types, - external_functions: external_funcs, + registries, }; - let wasm_bytes = project.finish().unwrap(); + let wasm_bytes = project.finish().unwrap().wasm_bytes; wasmparser::validate(&wasm_bytes).unwrap(); } } diff --git a/src/wasm/registries.rs b/src/wasm/registries.rs new file mode 100644 index 00000000..cab6ff85 --- /dev/null +++ b/src/wasm/registries.rs @@ -0,0 +1,27 @@ +use super::{ExternalFunctionRegistry, StringRegistry, TableRegistry, TypeRegistry}; + +#[derive(Default)] +pub struct Registries { + strings: StringRegistry, + external_functions: ExternalFunctionRegistry, + types: TypeRegistry, + tables: TableRegistry, +} + +impl Registries { + pub fn strings(&self) -> &StringRegistry { + &self.strings + } + + pub fn external_functions(&self) -> &ExternalFunctionRegistry { + &self.external_functions + } + + pub fn types(&self) -> &TypeRegistry { + &self.types + } + + pub fn tables(&self) -> &TableRegistry { + &self.tables + } +} diff --git a/src/wasm/strings.rs b/src/wasm/strings.rs new file mode 100644 index 00000000..685a2b1a --- /dev/null +++ b/src/wasm/strings.rs @@ -0,0 +1,15 @@ +use crate::prelude::*; +use crate::registry::SetRegistry; + +pub type StringRegistry = SetRegistry>; + +impl StringRegistry { + pub fn finish(self) -> Vec { + self.registry() + .take() + .keys() + .cloned() + .map(|s| s.into_string()) + .collect() + } +} diff --git a/src/wasm/tables.rs b/src/wasm/tables.rs new file mode 100644 index 00000000..c82b2895 --- /dev/null +++ b/src/wasm/tables.rs @@ -0,0 +1,20 @@ +use crate::prelude::*; +use crate::registry::MapRegistry; +use wasm_encoder::{RefType, TableSection, TableType}; + +pub type TableRegistry = MapRegistry, (RefType, u64)>; + +impl TableRegistry { + pub fn finish(self, tables: &mut TableSection) { + for &(element_type, min) in self.registry().take().values() { + // TODO: allow specifying min/max table size when registering, or after registering + tables.table(TableType { + element_type, + minimum: min, + maximum: None, + table64: false, + shared: false, + }); + } + } +} diff --git a/src/wasm/type_registry.rs b/src/wasm/type_registry.rs index 0e534f2d..646391cc 100644 --- a/src/wasm/type_registry.rs +++ b/src/wasm/type_registry.rs @@ -1,36 +1,12 @@ use crate::prelude::*; +use crate::registry::SetRegistry; use wasm_encoder::{TypeSection, ValType}; -pub type TypeSet = IndexSet<(Vec, Vec)>; - -#[derive(Clone, Default)] -pub struct TypeRegistry(RefCell); +pub type TypeRegistry = SetRegistry<(Vec, Vec)>; impl TypeRegistry { - pub fn new() -> Self { - TypeRegistry(RefCell::new(Default::default())) - } - - pub(crate) fn get_set(&self) -> &RefCell { - &self.0 - } - - /// get the index of the specified type, inserting it if it doesn't exist in the set already. - pub fn type_index(&self, params: Vec, results: Vec) -> HQResult { - self.get_set() - .borrow_mut() - .insert((params.clone(), results.clone())); - u32::try_from( - self.get_set() - .borrow() - .get_index_of(&(params, results)) - .ok_or(make_hq_bug!("couldn't find entry in TypeRegistry"))?, - ) - .map_err(|_| make_hq_bug!("type index out of bounds")) - } - pub fn finish(self, types: &mut TypeSection) { - for (params, results) in self.get_set().take() { + for (params, results) in self.registry().take().keys().cloned() { types.ty().function(params, results); } } From 9c3f55d40809e4fdb748b01b29740166af59e876 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sun, 19 Jan 2025 14:36:51 +0000 Subject: [PATCH 15/98] start implementing casts --- Cargo.toml | 3 +- js/cast/float2string.ts | 3 + js/cast/int2string.ts | 3 + js/cast/string2float.ts | 3 + js/ecma.d.ts | 4529 ++++++++++++++++++++++++++++++ src/instructions/hq.rs | 1 + src/instructions/hq/_yield.rs | 2 +- src/instructions/hq/cast.rs | 164 ++ src/instructions/operator/add.rs | 2 +- src/instructions/tests.rs | 31 +- src/ir/blocks.rs | 7 +- src/wasm/func.rs | 92 +- src/wasm/project.rs | 2 +- 13 files changed, 4729 insertions(+), 113 deletions(-) create mode 100644 js/cast/float2string.ts create mode 100644 js/cast/int2string.ts create mode 100644 js/cast/string2float.ts create mode 100644 js/ecma.d.ts create mode 100644 src/instructions/hq/cast.rs diff --git a/Cargo.toml b/Cargo.toml index 7fa5c8fe..b79c4483 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,8 @@ wasmparser = "0.222.0" #reqwest = { version = "0.11", features = ["blocking"] } [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] -ezno = { git = "https://github.com/hyperquark/ezno.git", branch = "wasm-bindgen-version-patch" } +#ezno = { git = "https://github.com/hyperquark/ezno.git", branch = "wasm-bindgen-version-patch" } +ezno-checker = { git = "https://github.com/hyperquark/ezno.git", branch = "wasm-bindgen-version-patch" } [lib] crate-type = ["cdylib", "rlib"] diff --git a/js/cast/float2string.ts b/js/cast/float2string.ts new file mode 100644 index 00000000..eeb19af5 --- /dev/null +++ b/js/cast/float2string.ts @@ -0,0 +1,3 @@ +export function float2string(s: number): string { + return s.toString(); +} \ No newline at end of file diff --git a/js/cast/int2string.ts b/js/cast/int2string.ts new file mode 100644 index 00000000..b33b27e6 --- /dev/null +++ b/js/cast/int2string.ts @@ -0,0 +1,3 @@ +export function int2string(s: number): string { + return s.toString(); +} \ No newline at end of file diff --git a/js/cast/string2float.ts b/js/cast/string2float.ts new file mode 100644 index 00000000..1bc9e404 --- /dev/null +++ b/js/cast/string2float.ts @@ -0,0 +1,3 @@ +export function string2float(s: string): number { + return parseFloat(s); +} \ No newline at end of file diff --git a/js/ecma.d.ts b/js/ecma.d.ts new file mode 100644 index 00000000..516d2f5b --- /dev/null +++ b/js/ecma.d.ts @@ -0,0 +1,4529 @@ +// https://github.com/kaleidawave/ezno/blob/main/src/playground/comparison/full.lib.d.ts + +///////////////////////////// +/// ECMAScript APIs +///////////////////////////// + +declare var NaN: number; +declare var Infinity: number; + +/** + * Evaluates JavaScript code and executes it. + * @param x A String value that contains valid JavaScript code. + */ +declare function eval(x: string): any; + +/** + * Converts a string to an integer. + * @param string A string to convert into a number. + * @param radix A value between 2 and 36 that specifies the base of the number in `string`. + * If this argument is not supplied, strings with a prefix of '0x' are considered hexadecimal. + * All other strings are considered decimal. + */ +declare function parseInt(string: string, radix?: number): number; + +/** + * Converts a string to a floating-point number. + * @param string A string that contains a floating-point number. + */ +declare function parseFloat(string: string): number; + +/** + * Returns a Boolean value that indicates whether a value is the reserved value NaN (not a number). + * @param number A numeric value. + */ +declare function isNaN(number: number): boolean; + +/** + * Determines whether a supplied number is finite. + * @param number Any numeric value. + */ +declare function isFinite(number: number): boolean; + +/** + * Gets the unencoded version of an encoded Uniform Resource Identifier (URI). + * @param encodedURI A value representing an encoded URI. + */ +declare function decodeURI(encodedURI: string): string; + +/** + * Gets the unencoded version of an encoded component of a Uniform Resource Identifier (URI). + * @param encodedURIComponent A value representing an encoded URI component. + */ +declare function decodeURIComponent(encodedURIComponent: string): string; + +/** + * Encodes a text string as a valid Uniform Resource Identifier (URI) + * @param uri A value representing an unencoded URI. + */ +declare function encodeURI(uri: string): string; + +/** + * Encodes a text string as a valid component of a Uniform Resource Identifier (URI). + * @param uriComponent A value representing an unencoded URI component. + */ +declare function encodeURIComponent(uriComponent: string | number | boolean): string; + +/** + * Computes a new string in which certain characters have been replaced by a hexadecimal escape sequence. + * @deprecated A legacy feature for browser compatibility + * @param string A string value + */ +declare function escape(string: string): string; + +/** + * Computes a new string in which hexadecimal escape sequences are replaced with the character that it represents. + * @deprecated A legacy feature for browser compatibility + * @param string A string value + */ +declare function unescape(string: string): string; + +interface Symbol { + /** Returns a string representation of an object. */ + toString(): string; + + /** Returns the primitive value of the specified object. */ + valueOf(): symbol; +} + +declare type PropertyKey = string | number | symbol; + +interface PropertyDescriptor { + configurable?: boolean; + enumerable?: boolean; + value?: any; + writable?: boolean; + get?(): any; + set?(v: any): void; +} + +interface PropertyDescriptorMap { + [key: PropertyKey]: PropertyDescriptor; +} + +interface Object { + /** The initial value of Object.prototype.constructor is the standard built-in Object constructor. */ + constructor: Function; + + /** Returns a string representation of an object. */ + toString(): string; + + /** Returns a date converted to a string using the current locale. */ + toLocaleString(): string; + + /** Returns the primitive value of the specified object. */ + valueOf(): Object; + + /** + * Determines whether an object has a property with the specified name. + * @param v A property name. + */ + hasOwnProperty(v: PropertyKey): boolean; + + /** + * Determines whether an object exists in another object's prototype chain. + * @param v Another object whose prototype chain is to be checked. + */ + isPrototypeOf(v: Object): boolean; + + /** + * Determines whether a specified property is enumerable. + * @param v A property name. + */ + propertyIsEnumerable(v: PropertyKey): boolean; +} + +interface ObjectConstructor { + new (value?: any): Object; + (): any; + (value: any): any; + + /** A reference to the prototype for a class of objects. */ + readonly prototype: Object; + + /** + * Returns the prototype of an object. + * @param o The object that references the prototype. + */ + getPrototypeOf(o: any): any; + + /** + * Gets the own property descriptor of the specified object. + * An own property descriptor is one that is defined directly on the object and is not inherited from the object's prototype. + * @param o Object that contains the property. + * @param p Name of the property. + */ + getOwnPropertyDescriptor(o: any, p: PropertyKey): PropertyDescriptor | undefined; + + /** + * Returns the names of the own properties of an object. The own properties of an object are those that are defined directly + * on that object, and are not inherited from the object's prototype. The properties of an object include both fields (objects) and functions. + * @param o Object that contains the own properties. + */ + getOwnPropertyNames(o: any): string[]; + + /** + * Creates an object that has the specified prototype or that has null prototype. + * @param o Object to use as a prototype. May be null. + */ + create(o: object | null): any; + + /** + * Creates an object that has the specified prototype, and that optionally contains specified properties. + * @param o Object to use as a prototype. May be null + * @param properties JavaScript object that contains one or more property descriptors. + */ + create(o: object | null, properties: PropertyDescriptorMap & ThisType): any; + + /** + * Adds a property to an object, or modifies attributes of an existing property. + * @param o Object on which to add or modify the property. This can be a native JavaScript object (that is, a user-defined object or a built in object) or a DOM object. + * @param p The property name. + * @param attributes Descriptor for the property. It can be for a data property or an accessor property. + */ + defineProperty(o: T, p: PropertyKey, attributes: PropertyDescriptor & ThisType): T; + + /** + * Adds one or more properties to an object, and/or modifies attributes of existing properties. + * @param o Object on which to add or modify the properties. This can be a native JavaScript object or a DOM object. + * @param properties JavaScript object that contains one or more descriptor objects. Each descriptor object describes a data property or an accessor property. + */ + defineProperties(o: T, properties: PropertyDescriptorMap & ThisType): T; + + /** + * Prevents the modification of attributes of existing properties, and prevents the addition of new properties. + * @param o Object on which to lock the attributes. + */ + seal(o: T): T; + + /** + * Prevents the modification of existing property attributes and values, and prevents the addition of new properties. + * @param f Object on which to lock the attributes. + */ + freeze(f: T): T; + + /** + * Prevents the modification of existing property attributes and values, and prevents the addition of new properties. + * @param o Object on which to lock the attributes. + */ + freeze(o: T): Readonly; + + /** + * Prevents the modification of existing property attributes and values, and prevents the addition of new properties. + * @param o Object on which to lock the attributes. + */ + freeze(o: T): Readonly; + + /** + * Prevents the addition of new properties to an object. + * @param o Object to make non-extensible. + */ + preventExtensions(o: T): T; + + /** + * Returns true if existing property attributes cannot be modified in an object and new properties cannot be added to the object. + * @param o Object to test. + */ + isSealed(o: any): boolean; + + /** + * Returns true if existing property attributes and values cannot be modified in an object, and new properties cannot be added to the object. + * @param o Object to test. + */ + isFrozen(o: any): boolean; + + /** + * Returns a value that indicates whether new properties can be added to an object. + * @param o Object to test. + */ + isExtensible(o: any): boolean; + + /** + * Returns the names of the enumerable string properties and methods of an object. + * @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object. + */ + keys(o: object): string[]; +} + +/** + * Provides functionality common to all JavaScript objects. + */ +declare var Object: ObjectConstructor; + +/** + * Creates a new function. + */ +interface Function { + /** + * Calls the function, substituting the specified object for the this value of the function, and the specified array for the arguments of the function. + * @param thisArg The object to be used as the this object. + * @param argArray A set of arguments to be passed to the function. + */ + apply(this: Function, thisArg: any, argArray?: any): any; + + /** + * Calls a method of an object, substituting another object for the current object. + * @param thisArg The object to be used as the current object. + * @param argArray A list of arguments to be passed to the method. + */ + call(this: Function, thisArg: any, ...argArray: any[]): any; + + /** + * For a given function, creates a bound function that has the same body as the original function. + * The this object of the bound function is associated with the specified object, and has the specified initial parameters. + * @param thisArg An object to which the this keyword can refer inside the new function. + * @param argArray A list of arguments to be passed to the new function. + */ + bind(this: Function, thisArg: any, ...argArray: any[]): any; + + /** Returns a string representation of a function. */ + toString(): string; + + prototype: any; + readonly length: number; + + // Non-standard extensions + arguments: any; + caller: Function; +} + +interface FunctionConstructor { + /** + * Creates a new function. + * @param args A list of arguments the function accepts. + */ + new (...args: string[]): Function; + (...args: string[]): Function; + readonly prototype: Function; +} + +declare var Function: FunctionConstructor; + +/** + * Extracts the type of the 'this' parameter of a function type, or 'unknown' if the function type has no 'this' parameter. + */ +type ThisParameterType = T extends (this: infer U, ...args: never) => any ? U : unknown; + +/** + * Removes the 'this' parameter from a function type. + */ +type OmitThisParameter = unknown extends ThisParameterType ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T; + +interface CallableFunction extends Function { + /** + * Calls the function with the specified object as the this value and the elements of specified array as the arguments. + * @param thisArg The object to be used as the this object. + */ + apply(this: (this: T) => R, thisArg: T): R; + + /** + * Calls the function with the specified object as the this value and the elements of specified array as the arguments. + * @param thisArg The object to be used as the this object. + * @param args An array of argument values to be passed to the function. + */ + apply(this: (this: T, ...args: A) => R, thisArg: T, args: A): R; + + /** + * Calls the function with the specified object as the this value and the specified rest arguments as the arguments. + * @param thisArg The object to be used as the this object. + * @param args Argument values to be passed to the function. + */ + call(this: (this: T, ...args: A) => R, thisArg: T, ...args: A): R; + + /** + * For a given function, creates a bound function that has the same body as the original function. + * The this object of the bound function is associated with the specified object, and has the specified initial parameters. + * @param thisArg The object to be used as the this object. + */ + bind(this: T, thisArg: ThisParameterType): OmitThisParameter; + + /** + * For a given function, creates a bound function that has the same body as the original function. + * The this object of the bound function is associated with the specified object, and has the specified initial parameters. + * @param thisArg The object to be used as the this object. + * @param args Arguments to bind to the parameters of the function. + */ + bind(this: (this: T, ...args: [...A, ...B]) => R, thisArg: T, ...args: A): (...args: B) => R; +} + +interface NewableFunction extends Function { + /** + * Calls the function with the specified object as the this value and the elements of specified array as the arguments. + * @param thisArg The object to be used as the this object. + */ + apply(this: new () => T, thisArg: T): void; + /** + * Calls the function with the specified object as the this value and the elements of specified array as the arguments. + * @param thisArg The object to be used as the this object. + * @param args An array of argument values to be passed to the function. + */ + apply(this: new (...args: A) => T, thisArg: T, args: A): void; + + /** + * Calls the function with the specified object as the this value and the specified rest arguments as the arguments. + * @param thisArg The object to be used as the this object. + * @param args Argument values to be passed to the function. + */ + call(this: new (...args: A) => T, thisArg: T, ...args: A): void; + + /** + * For a given function, creates a bound function that has the same body as the original function. + * The this object of the bound function is associated with the specified object, and has the specified initial parameters. + * @param thisArg The object to be used as the this object. + */ + bind(this: T, thisArg: any): T; + + /** + * For a given function, creates a bound function that has the same body as the original function. + * The this object of the bound function is associated with the specified object, and has the specified initial parameters. + * @param thisArg The object to be used as the this object. + * @param args Arguments to bind to the parameters of the function. + */ + bind(this: new (...args: [...A, ...B]) => R, thisArg: any, ...args: A): new (...args: B) => R; +} + +interface IArguments { + [index: number]: any; + length: number; + callee: Function; +} + +interface String { + /** Returns a string representation of a string. */ + toString(): string; + + /** + * Returns the character at the specified index. + * @param pos The zero-based index of the desired character. + */ + charAt(pos: number): string; + + /** + * Returns the Unicode value of the character at the specified location. + * @param index The zero-based index of the desired character. If there is no character at the specified index, NaN is returned. + */ + charCodeAt(index: number): number; + + /** + * Returns a string that contains the concatenation of two or more strings. + * @param strings The strings to append to the end of the string. + */ + concat(...strings: string[]): string; + + /** + * Returns the position of the first occurrence of a substring. + * @param searchString The substring to search for in the string + * @param position The index at which to begin searching the String object. If omitted, search starts at the beginning of the string. + */ + indexOf(searchString: string, position?: number): number; + + /** + * Returns the last occurrence of a substring in the string. + * @param searchString The substring to search for. + * @param position The index at which to begin searching. If omitted, the search begins at the end of the string. + */ + lastIndexOf(searchString: string, position?: number): number; + + /** + * Determines whether two strings are equivalent in the current locale. + * @param that String to compare to target string + */ + localeCompare(that: string): number; + + /** + * Matches a string with a regular expression, and returns an array containing the results of that search. + * @param regexp A variable name or string literal containing the regular expression pattern and flags. + */ + match(regexp: string | RegExp): RegExpMatchArray | null; + + /** + * Replaces text in a string, using a regular expression or search string. + * @param searchValue A string or regular expression to search for. + * @param replaceValue A string containing the text to replace. When the {@linkcode searchValue} is a `RegExp`, all matches are replaced if the `g` flag is set (or only those matches at the beginning, if the `y` flag is also present). Otherwise, only the first match of {@linkcode searchValue} is replaced. + */ + replace(searchValue: string | RegExp, replaceValue: string): string; + + /** + * Replaces text in a string, using a regular expression or search string. + * @param searchValue A string to search for. + * @param replacer A function that returns the replacement text. + */ + replace(searchValue: string | RegExp, replacer: (substring: string, ...args: any[]) => string): string; + + /** + * Finds the first substring match in a regular expression search. + * @param regexp The regular expression pattern and applicable flags. + */ + search(regexp: string | RegExp): number; + + /** + * Returns a section of a string. + * @param start The index to the beginning of the specified portion of stringObj. + * @param end The index to the end of the specified portion of stringObj. The substring includes the characters up to, but not including, the character indicated by end. + * If this value is not specified, the substring continues to the end of stringObj. + */ + slice(start?: number, end?: number): string; + + /** + * Split a string into substrings using the specified separator and return them as an array. + * @param separator A string that identifies character or characters to use in separating the string. If omitted, a single-element array containing the entire string is returned. + * @param limit A value used to limit the number of elements returned in the array. + */ + split(separator: string | RegExp, limit?: number): string[]; + + /** + * Returns the substring at the specified location within a String object. + * @param start The zero-based index number indicating the beginning of the substring. + * @param end Zero-based index number indicating the end of the substring. The substring includes the characters up to, but not including, the character indicated by end. + * If end is omitted, the characters from start through the end of the original string are returned. + */ + substring(start: number, end?: number): string; + + /** Converts all the alphabetic characters in a string to lowercase. */ + toLowerCase(): string; + + /** Converts all alphabetic characters to lowercase, taking into account the host environment's current locale. */ + toLocaleLowerCase(locales?: string | string[]): string; + + /** Converts all the alphabetic characters in a string to uppercase. */ + toUpperCase(): string; + + /** Returns a string where all alphabetic characters have been converted to uppercase, taking into account the host environment's current locale. */ + toLocaleUpperCase(locales?: string | string[]): string; + + /** Removes the leading and trailing white space and line terminator characters from a string. */ + trim(): string; + + /** Returns the length of a String object. */ + readonly length: number; + + // IE extensions + /** + * Gets a substring beginning at the specified location and having the specified length. + * @deprecated A legacy feature for browser compatibility + * @param from The starting position of the desired substring. The index of the first character in the string is zero. + * @param length The number of characters to include in the returned substring. + */ + substr(from: number, length?: number): string; + + /** Returns the primitive value of the specified object. */ + valueOf(): string; + + readonly [index: number]: string; +} + +interface StringConstructor { + new (value?: any): String; + (value?: any): string; + readonly prototype: String; + fromCharCode(...codes: number[]): string; +} + +/** + * Allows manipulation and formatting of text strings and determination and location of substrings within strings. + */ +declare var String: StringConstructor; + +interface Boolean { + /** Returns the primitive value of the specified object. */ + valueOf(): boolean; +} + +interface BooleanConstructor { + new (value?: any): Boolean; + (value?: T): boolean; + readonly prototype: Boolean; +} + +declare var Boolean: BooleanConstructor; + +interface Number { + /** + * Returns a string representation of an object. + * @param radix Specifies a radix for converting numeric values to strings. This value is only used for numbers. + */ + toString(radix?: number): string; + + /** + * Returns a string representing a number in fixed-point notation. + * @param fractionDigits Number of digits after the decimal point. Must be in the range 0 - 20, inclusive. + */ + toFixed(fractionDigits?: number): string; + + /** + * Returns a string containing a number represented in exponential notation. + * @param fractionDigits Number of digits after the decimal point. Must be in the range 0 - 20, inclusive. + */ + toExponential(fractionDigits?: number): string; + + /** + * Returns a string containing a number represented either in exponential or fixed-point notation with a specified number of digits. + * @param precision Number of significant digits. Must be in the range 1 - 21, inclusive. + */ + toPrecision(precision?: number): string; + + /** Returns the primitive value of the specified object. */ + valueOf(): number; +} + +interface NumberConstructor { + new (value?: any): Number; + (value?: any): number; + readonly prototype: Number; + + /** The largest number that can be represented in JavaScript. Equal to approximately 1.79E+308. */ + readonly MAX_VALUE: number; + + /** The closest number to zero that can be represented in JavaScript. Equal to approximately 5.00E-324. */ + readonly MIN_VALUE: number; + + /** + * A value that is not a number. + * In equality comparisons, NaN does not equal any value, including itself. To test whether a value is equivalent to NaN, use the isNaN function. + */ + readonly NaN: number; + + /** + * A value that is less than the largest negative number that can be represented in JavaScript. + * JavaScript displays NEGATIVE_INFINITY values as -infinity. + */ + readonly NEGATIVE_INFINITY: number; + + /** + * A value greater than the largest number that can be represented in JavaScript. + * JavaScript displays POSITIVE_INFINITY values as infinity. + */ + readonly POSITIVE_INFINITY: number; +} + +/** An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers. */ +declare var Number: NumberConstructor; + +interface TemplateStringsArray extends ReadonlyArray { + readonly raw: readonly string[]; +} + +/** + * The type of `import.meta`. + * + * If you need to declare that a given property exists on `import.meta`, + * this type may be augmented via interface merging. + */ +interface ImportMeta { +} + +/** + * The type for the optional second argument to `import()`. + * + * If your host environment supports additional options, this type may be + * augmented via interface merging. + */ +interface ImportCallOptions { + /** @deprecated*/ assert?: ImportAssertions; + with?: ImportAttributes; +} + +/** + * The type for the `assert` property of the optional second argument to `import()`. + */ +interface ImportAssertions { + [key: string]: string; +} + +/** + * The type for the `with` property of the optional second argument to `import()`. + */ +interface ImportAttributes { + [key: string]: string; +} + +interface Math { + /** The mathematical constant e. This is Euler's number, the base of natural logarithms. */ + readonly E: number; + /** The natural logarithm of 10. */ + readonly LN10: number; + /** The natural logarithm of 2. */ + readonly LN2: number; + /** The base-2 logarithm of e. */ + readonly LOG2E: number; + /** The base-10 logarithm of e. */ + readonly LOG10E: number; + /** Pi. This is the ratio of the circumference of a circle to its diameter. */ + readonly PI: number; + /** The square root of 0.5, or, equivalently, one divided by the square root of 2. */ + readonly SQRT1_2: number; + /** The square root of 2. */ + readonly SQRT2: number; + /** + * Returns the absolute value of a number (the value without regard to whether it is positive or negative). + * For example, the absolute value of -5 is the same as the absolute value of 5. + * @param x A numeric expression for which the absolute value is needed. + */ + abs(x: number): number; + /** + * Returns the arc cosine (or inverse cosine) of a number. + * @param x A numeric expression. + */ + acos(x: number): number; + /** + * Returns the arcsine of a number. + * @param x A numeric expression. + */ + asin(x: number): number; + /** + * Returns the arctangent of a number. + * @param x A numeric expression for which the arctangent is needed. + */ + atan(x: number): number; + /** + * Returns the angle (in radians) from the X axis to a point. + * @param y A numeric expression representing the cartesian y-coordinate. + * @param x A numeric expression representing the cartesian x-coordinate. + */ + atan2(y: number, x: number): number; + /** + * Returns the smallest integer greater than or equal to its numeric argument. + * @param x A numeric expression. + */ + ceil(x: number): number; + /** + * Returns the cosine of a number. + * @param x A numeric expression that contains an angle measured in radians. + */ + cos(x: number): number; + /** + * Returns e (the base of natural logarithms) raised to a power. + * @param x A numeric expression representing the power of e. + */ + exp(x: number): number; + /** + * Returns the greatest integer less than or equal to its numeric argument. + * @param x A numeric expression. + */ + floor(x: number): number; + /** + * Returns the natural logarithm (base e) of a number. + * @param x A numeric expression. + */ + log(x: number): number; + /** + * Returns the larger of a set of supplied numeric expressions. + * @param values Numeric expressions to be evaluated. + */ + max(...values: number[]): number; + /** + * Returns the smaller of a set of supplied numeric expressions. + * @param values Numeric expressions to be evaluated. + */ + min(...values: number[]): number; + /** + * Returns the value of a base expression taken to a specified power. + * @param x The base value of the expression. + * @param y The exponent value of the expression. + */ + pow(x: number, y: number): number; + /** Returns a pseudorandom number between 0 and 1. */ + random(): number; + /** + * Returns a supplied numeric expression rounded to the nearest integer. + * @param x The value to be rounded to the nearest integer. + */ + round(x: number): number; + /** + * Returns the sine of a number. + * @param x A numeric expression that contains an angle measured in radians. + */ + sin(x: number): number; + /** + * Returns the square root of a number. + * @param x A numeric expression. + */ + sqrt(x: number): number; + /** + * Returns the tangent of a number. + * @param x A numeric expression that contains an angle measured in radians. + */ + tan(x: number): number; +} +/** An intrinsic object that provides basic mathematics functionality and constants. */ +declare var Math: Math; + +/** Enables basic storage and retrieval of dates and times. */ +interface Date { + /** Returns a string representation of a date. The format of the string depends on the locale. */ + toString(): string; + /** Returns a date as a string value. */ + toDateString(): string; + /** Returns a time as a string value. */ + toTimeString(): string; + /** Returns a value as a string value appropriate to the host environment's current locale. */ + toLocaleString(): string; + /** Returns a date as a string value appropriate to the host environment's current locale. */ + toLocaleDateString(): string; + /** Returns a time as a string value appropriate to the host environment's current locale. */ + toLocaleTimeString(): string; + /** Returns the stored time value in milliseconds since midnight, January 1, 1970 UTC. */ + valueOf(): number; + /** Returns the stored time value in milliseconds since midnight, January 1, 1970 UTC. */ + getTime(): number; + /** Gets the year, using local time. */ + getFullYear(): number; + /** Gets the year using Universal Coordinated Time (UTC). */ + getUTCFullYear(): number; + /** Gets the month, using local time. */ + getMonth(): number; + /** Gets the month of a Date object using Universal Coordinated Time (UTC). */ + getUTCMonth(): number; + /** Gets the day-of-the-month, using local time. */ + getDate(): number; + /** Gets the day-of-the-month, using Universal Coordinated Time (UTC). */ + getUTCDate(): number; + /** Gets the day of the week, using local time. */ + getDay(): number; + /** Gets the day of the week using Universal Coordinated Time (UTC). */ + getUTCDay(): number; + /** Gets the hours in a date, using local time. */ + getHours(): number; + /** Gets the hours value in a Date object using Universal Coordinated Time (UTC). */ + getUTCHours(): number; + /** Gets the minutes of a Date object, using local time. */ + getMinutes(): number; + /** Gets the minutes of a Date object using Universal Coordinated Time (UTC). */ + getUTCMinutes(): number; + /** Gets the seconds of a Date object, using local time. */ + getSeconds(): number; + /** Gets the seconds of a Date object using Universal Coordinated Time (UTC). */ + getUTCSeconds(): number; + /** Gets the milliseconds of a Date, using local time. */ + getMilliseconds(): number; + /** Gets the milliseconds of a Date object using Universal Coordinated Time (UTC). */ + getUTCMilliseconds(): number; + /** Gets the difference in minutes between the time on the local computer and Universal Coordinated Time (UTC). */ + getTimezoneOffset(): number; + /** + * Sets the date and time value in the Date object. + * @param time A numeric value representing the number of elapsed milliseconds since midnight, January 1, 1970 GMT. + */ + setTime(time: number): number; + /** + * Sets the milliseconds value in the Date object using local time. + * @param ms A numeric value equal to the millisecond value. + */ + setMilliseconds(ms: number): number; + /** + * Sets the milliseconds value in the Date object using Universal Coordinated Time (UTC). + * @param ms A numeric value equal to the millisecond value. + */ + setUTCMilliseconds(ms: number): number; + + /** + * Sets the seconds value in the Date object using local time. + * @param sec A numeric value equal to the seconds value. + * @param ms A numeric value equal to the milliseconds value. + */ + setSeconds(sec: number, ms?: number): number; + /** + * Sets the seconds value in the Date object using Universal Coordinated Time (UTC). + * @param sec A numeric value equal to the seconds value. + * @param ms A numeric value equal to the milliseconds value. + */ + setUTCSeconds(sec: number, ms?: number): number; + /** + * Sets the minutes value in the Date object using local time. + * @param min A numeric value equal to the minutes value. + * @param sec A numeric value equal to the seconds value. + * @param ms A numeric value equal to the milliseconds value. + */ + setMinutes(min: number, sec?: number, ms?: number): number; + /** + * Sets the minutes value in the Date object using Universal Coordinated Time (UTC). + * @param min A numeric value equal to the minutes value. + * @param sec A numeric value equal to the seconds value. + * @param ms A numeric value equal to the milliseconds value. + */ + setUTCMinutes(min: number, sec?: number, ms?: number): number; + /** + * Sets the hour value in the Date object using local time. + * @param hours A numeric value equal to the hours value. + * @param min A numeric value equal to the minutes value. + * @param sec A numeric value equal to the seconds value. + * @param ms A numeric value equal to the milliseconds value. + */ + setHours(hours: number, min?: number, sec?: number, ms?: number): number; + /** + * Sets the hours value in the Date object using Universal Coordinated Time (UTC). + * @param hours A numeric value equal to the hours value. + * @param min A numeric value equal to the minutes value. + * @param sec A numeric value equal to the seconds value. + * @param ms A numeric value equal to the milliseconds value. + */ + setUTCHours(hours: number, min?: number, sec?: number, ms?: number): number; + /** + * Sets the numeric day-of-the-month value of the Date object using local time. + * @param date A numeric value equal to the day of the month. + */ + setDate(date: number): number; + /** + * Sets the numeric day of the month in the Date object using Universal Coordinated Time (UTC). + * @param date A numeric value equal to the day of the month. + */ + setUTCDate(date: number): number; + /** + * Sets the month value in the Date object using local time. + * @param month A numeric value equal to the month. The value for January is 0, and other month values follow consecutively. + * @param date A numeric value representing the day of the month. If this value is not supplied, the value from a call to the getDate method is used. + */ + setMonth(month: number, date?: number): number; + /** + * Sets the month value in the Date object using Universal Coordinated Time (UTC). + * @param month A numeric value equal to the month. The value for January is 0, and other month values follow consecutively. + * @param date A numeric value representing the day of the month. If it is not supplied, the value from a call to the getUTCDate method is used. + */ + setUTCMonth(month: number, date?: number): number; + /** + * Sets the year of the Date object using local time. + * @param year A numeric value for the year. + * @param month A zero-based numeric value for the month (0 for January, 11 for December). Must be specified if numDate is specified. + * @param date A numeric value equal for the day of the month. + */ + setFullYear(year: number, month?: number, date?: number): number; + /** + * Sets the year value in the Date object using Universal Coordinated Time (UTC). + * @param year A numeric value equal to the year. + * @param month A numeric value equal to the month. The value for January is 0, and other month values follow consecutively. Must be supplied if numDate is supplied. + * @param date A numeric value equal to the day of the month. + */ + setUTCFullYear(year: number, month?: number, date?: number): number; + /** Returns a date converted to a string using Universal Coordinated Time (UTC). */ + toUTCString(): string; + /** Returns a date as a string value in ISO format. */ + toISOString(): string; + /** Used by the JSON.stringify method to enable the transformation of an object's data for JavaScript Object Notation (JSON) serialization. */ + toJSON(key?: any): string; +} + +interface DateConstructor { + new (): Date; + new (value: number | string): Date; + /** + * Creates a new Date. + * @param year The full year designation is required for cross-century date accuracy. If year is between 0 and 99 is used, then year is assumed to be 1900 + year. + * @param monthIndex The month as a number between 0 and 11 (January to December). + * @param date The date as a number between 1 and 31. + * @param hours Must be supplied if minutes is supplied. A number from 0 to 23 (midnight to 11pm) that specifies the hour. + * @param minutes Must be supplied if seconds is supplied. A number from 0 to 59 that specifies the minutes. + * @param seconds Must be supplied if milliseconds is supplied. A number from 0 to 59 that specifies the seconds. + * @param ms A number from 0 to 999 that specifies the milliseconds. + */ + new (year: number, monthIndex: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): Date; + (): string; + readonly prototype: Date; + /** + * Parses a string containing a date, and returns the number of milliseconds between that date and midnight, January 1, 1970. + * @param s A date string + */ + parse(s: string): number; + /** + * Returns the number of milliseconds between midnight, January 1, 1970 Universal Coordinated Time (UTC) (or GMT) and the specified date. + * @param year The full year designation is required for cross-century date accuracy. If year is between 0 and 99 is used, then year is assumed to be 1900 + year. + * @param monthIndex The month as a number between 0 and 11 (January to December). + * @param date The date as a number between 1 and 31. + * @param hours Must be supplied if minutes is supplied. A number from 0 to 23 (midnight to 11pm) that specifies the hour. + * @param minutes Must be supplied if seconds is supplied. A number from 0 to 59 that specifies the minutes. + * @param seconds Must be supplied if milliseconds is supplied. A number from 0 to 59 that specifies the seconds. + * @param ms A number from 0 to 999 that specifies the milliseconds. + */ + UTC(year: number, monthIndex: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): number; + /** Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC). */ + now(): number; +} + +declare var Date: DateConstructor; + +interface RegExpMatchArray extends Array { + /** + * The index of the search at which the result was found. + */ + index?: number; + /** + * A copy of the search string. + */ + input?: string; + /** + * The first match. This will always be present because `null` will be returned if there are no matches. + */ + 0: string; +} + +interface RegExpExecArray extends Array { + /** + * The index of the search at which the result was found. + */ + index: number; + /** + * A copy of the search string. + */ + input: string; + /** + * The first match. This will always be present because `null` will be returned if there are no matches. + */ + 0: string; +} + +interface RegExp { + /** + * Executes a search on a string using a regular expression pattern, and returns an array containing the results of that search. + * @param string The String object or string literal on which to perform the search. + */ + exec(string: string): RegExpExecArray | null; + + /** + * Returns a Boolean value that indicates whether or not a pattern exists in a searched string. + * @param string String on which to perform the search. + */ + test(string: string): boolean; + + /** Returns a copy of the text of the regular expression pattern. Read-only. The regExp argument is a Regular expression object. It can be a variable name or a literal. */ + readonly source: string; + + /** Returns a Boolean value indicating the state of the global flag (g) used with a regular expression. Default is false. Read-only. */ + readonly global: boolean; + + /** Returns a Boolean value indicating the state of the ignoreCase flag (i) used with a regular expression. Default is false. Read-only. */ + readonly ignoreCase: boolean; + + /** Returns a Boolean value indicating the state of the multiline flag (m) used with a regular expression. Default is false. Read-only. */ + readonly multiline: boolean; + + lastIndex: number; + + // Non-standard extensions + /** @deprecated A legacy feature for browser compatibility */ + compile(pattern: string, flags?: string): this; +} + +interface RegExpConstructor { + new (pattern: RegExp | string): RegExp; + new (pattern: string, flags?: string): RegExp; + (pattern: RegExp | string): RegExp; + (pattern: string, flags?: string): RegExp; + readonly "prototype": RegExp; + + // Non-standard extensions + /** @deprecated A legacy feature for browser compatibility */ + "$1": string; + /** @deprecated A legacy feature for browser compatibility */ + "$2": string; + /** @deprecated A legacy feature for browser compatibility */ + "$3": string; + /** @deprecated A legacy feature for browser compatibility */ + "$4": string; + /** @deprecated A legacy feature for browser compatibility */ + "$5": string; + /** @deprecated A legacy feature for browser compatibility */ + "$6": string; + /** @deprecated A legacy feature for browser compatibility */ + "$7": string; + /** @deprecated A legacy feature for browser compatibility */ + "$8": string; + /** @deprecated A legacy feature for browser compatibility */ + "$9": string; + /** @deprecated A legacy feature for browser compatibility */ + "input": string; + /** @deprecated A legacy feature for browser compatibility */ + "$_": string; + /** @deprecated A legacy feature for browser compatibility */ + "lastMatch": string; + /** @deprecated A legacy feature for browser compatibility */ + "$&": string; + /** @deprecated A legacy feature for browser compatibility */ + "lastParen": string; + /** @deprecated A legacy feature for browser compatibility */ + "$+": string; + /** @deprecated A legacy feature for browser compatibility */ + "leftContext": string; + /** @deprecated A legacy feature for browser compatibility */ + "$`": string; + /** @deprecated A legacy feature for browser compatibility */ + "rightContext": string; + /** @deprecated A legacy feature for browser compatibility */ + "$'": string; +} + +declare var RegExp: RegExpConstructor; + +interface Error { + name: string; + message: string; + stack?: string; +} + +interface ErrorConstructor { + new (message?: string): Error; + (message?: string): Error; + readonly prototype: Error; +} + +declare var Error: ErrorConstructor; + +interface EvalError extends Error { +} + +interface EvalErrorConstructor extends ErrorConstructor { + new (message?: string): EvalError; + (message?: string): EvalError; + readonly prototype: EvalError; +} + +declare var EvalError: EvalErrorConstructor; + +interface RangeError extends Error { +} + +interface RangeErrorConstructor extends ErrorConstructor { + new (message?: string): RangeError; + (message?: string): RangeError; + readonly prototype: RangeError; +} + +declare var RangeError: RangeErrorConstructor; + +interface ReferenceError extends Error { +} + +interface ReferenceErrorConstructor extends ErrorConstructor { + new (message?: string): ReferenceError; + (message?: string): ReferenceError; + readonly prototype: ReferenceError; +} + +declare var ReferenceError: ReferenceErrorConstructor; + +interface SyntaxError extends Error { +} + +interface SyntaxErrorConstructor extends ErrorConstructor { + new (message?: string): SyntaxError; + (message?: string): SyntaxError; + readonly prototype: SyntaxError; +} + +declare var SyntaxError: SyntaxErrorConstructor; + +interface TypeError extends Error { +} + +interface TypeErrorConstructor extends ErrorConstructor { + new (message?: string): TypeError; + (message?: string): TypeError; + readonly prototype: TypeError; +} + +declare var TypeError: TypeErrorConstructor; + +interface URIError extends Error { +} + +interface URIErrorConstructor extends ErrorConstructor { + new (message?: string): URIError; + (message?: string): URIError; + readonly prototype: URIError; +} + +declare var URIError: URIErrorConstructor; + +interface JSON { + /** + * Converts a JavaScript Object Notation (JSON) string into an object. + * @param text A valid JSON string. + * @param reviver A function that transforms the results. This function is called for each member of the object. + * If a member contains nested objects, the nested objects are transformed before the parent object is. + */ + parse(text: string, reviver?: (this: any, key: string, value: any) => any): any; + /** + * Converts a JavaScript value to a JavaScript Object Notation (JSON) string. + * @param value A JavaScript value, usually an object or array, to be converted. + * @param replacer A function that transforms the results. + * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. + */ + stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string; + /** + * Converts a JavaScript value to a JavaScript Object Notation (JSON) string. + * @param value A JavaScript value, usually an object or array, to be converted. + * @param replacer An array of strings and numbers that acts as an approved list for selecting the object properties that will be stringified. + * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. + */ + stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string; +} + +/** + * An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format. + */ +declare var JSON: JSON; + +///////////////////////////// +/// ECMAScript Array API (specially handled by compiler) +///////////////////////////// + +interface ReadonlyArray { + /** + * Gets the length of the array. This is a number one higher than the highest element defined in an array. + */ + readonly length: number; + /** + * Returns a string representation of an array. + */ + toString(): string; + /** + * Returns a string representation of an array. The elements are converted to string using their toLocaleString methods. + */ + toLocaleString(): string; + /** + * Combines two or more arrays. + * @param items Additional items to add to the end of array1. + */ + concat(...items: ConcatArray[]): T[]; + /** + * Combines two or more arrays. + * @param items Additional items to add to the end of array1. + */ + concat(...items: (T | ConcatArray)[]): T[]; + /** + * Adds all the elements of an array separated by the specified separator string. + * @param separator A string used to separate one element of an array from the next in the resulting String. If omitted, the array elements are separated with a comma. + */ + join(separator?: string): string; + /** + * Returns a section of an array. + * @param start The beginning of the specified portion of the array. + * @param end The end of the specified portion of the array. This is exclusive of the element at the index 'end'. + */ + slice(start?: number, end?: number): T[]; + /** + * Returns the index of the first occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the search starts at index 0. + */ + indexOf(searchElement: T, fromIndex?: number): number; + /** + * Returns the index of the last occurrence of a specified value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the search starts at the last index in the array. + */ + lastIndexOf(searchElement: T, fromIndex?: number): number; + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + every(predicate: (value: T, index: number, array: readonly T[]) => value is S, thisArg?: any): this is readonly S[]; + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + every(predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any): boolean; + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param predicate A function that accepts up to three arguments. The some method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + some(predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any): boolean; + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: T, index: number, array: readonly T[]) => void, thisArg?: any): void; + /** + * Calls a defined callback function on each element of an array, and returns an array that contains the results. + * @param callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value. + */ + map(callbackfn: (value: T, index: number, array: readonly T[]) => U, thisArg?: any): U[]; + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value. + */ + filter(predicate: (value: T, index: number, array: readonly T[]) => value is S, thisArg?: any): S[]; + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value. + */ + filter(predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any): T[]; + /** + * Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value. + */ + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T): T; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T, initialValue: T): T; + /** + * Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value. + */ + reduce(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, initialValue: U): U; + /** + * Calls the specified callback function for all the elements in an array, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value. + */ + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T): T; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T, initialValue: T): T; + /** + * Calls the specified callback function for all the elements in an array, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value. + */ + reduceRight(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, initialValue: U): U; + + readonly [n: number]: T; +} + +interface ConcatArray { + readonly length: number; + readonly [n: number]: T; + join(separator?: string): string; + slice(start?: number, end?: number): T[]; +} + +interface Array { + /** + * Gets or sets the length of the array. This is a number one higher than the highest index in the array. + */ + length: number; + /** + * Returns a string representation of an array. + */ + toString(): string; + /** + * Returns a string representation of an array. The elements are converted to string using their toLocaleString methods. + */ + toLocaleString(): string; + /** + * Removes the last element from an array and returns it. + * If the array is empty, undefined is returned and the array is not modified. + */ + pop(): T | undefined; + /** + * Appends new elements to the end of an array, and returns the new length of the array. + * @param items New elements to add to the array. + */ + push(...items: T[]): number; + /** + * Combines two or more arrays. + * This method returns a new array without modifying any existing arrays. + * @param items Additional arrays and/or items to add to the end of the array. + */ + concat(...items: ConcatArray[]): T[]; + /** + * Combines two or more arrays. + * This method returns a new array without modifying any existing arrays. + * @param items Additional arrays and/or items to add to the end of the array. + */ + concat(...items: (T | ConcatArray)[]): T[]; + /** + * Adds all the elements of an array into a string, separated by the specified separator string. + * @param separator A string used to separate one element of the array from the next in the resulting string. If omitted, the array elements are separated with a comma. + */ + join(separator?: string): string; + /** + * Reverses the elements in an array in place. + * This method mutates the array and returns a reference to the same array. + */ + reverse(): T[]; + /** + * Removes the first element from an array and returns it. + * If the array is empty, undefined is returned and the array is not modified. + */ + shift(): T | undefined; + /** + * Returns a copy of a section of an array. + * For both start and end, a negative index can be used to indicate an offset from the end of the array. + * For example, -2 refers to the second to last element of the array. + * @param start The beginning index of the specified portion of the array. + * If start is undefined, then the slice begins at index 0. + * @param end The end index of the specified portion of the array. This is exclusive of the element at the index 'end'. + * If end is undefined, then the slice extends to the end of the array. + */ + slice(start?: number, end?: number): T[]; + /** + * Sorts an array in place. + * This method mutates the array and returns a reference to the same array. + * @param compareFn Function used to determine the order of the elements. It is expected to return + * a negative value if the first argument is less than the second argument, zero if they're equal, and a positive + * value otherwise. If omitted, the elements are sorted in ascending, ASCII character order. + * ```ts + * [11,2,22,1].sort((a, b) => a - b) + * ``` + */ + sort(compareFn?: (a: T, b: T) => number): this; + /** + * Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements. + * @param start The zero-based location in the array from which to start removing elements. + * @param deleteCount The number of elements to remove. + * @returns An array containing the elements that were deleted. + */ + splice(start: number, deleteCount?: number): T[]; + /** + * Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements. + * @param start The zero-based location in the array from which to start removing elements. + * @param deleteCount The number of elements to remove. + * @param items Elements to insert into the array in place of the deleted elements. + * @returns An array containing the elements that were deleted. + */ + splice(start: number, deleteCount: number, ...items: T[]): T[]; + /** + * Inserts new elements at the start of an array, and returns the new length of the array. + * @param items Elements to insert at the start of the array. + */ + unshift(...items: T[]): number; + /** + * Returns the index of the first occurrence of a value in an array, or -1 if it is not present. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the search starts at index 0. + */ + indexOf(searchElement: T, fromIndex?: number): number; + /** + * Returns the index of the last occurrence of a specified value in an array, or -1 if it is not present. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin searching backward. If fromIndex is omitted, the search starts at the last index in the array. + */ + lastIndexOf(searchElement: T, fromIndex?: number): number; + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + every(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): this is S[]; + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + every(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean; + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param predicate A function that accepts up to three arguments. The some method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + some(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean; + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void; + /** + * Calls a defined callback function on each element of an array, and returns an array that contains the results. + * @param callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value. + */ + map(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value. + */ + filter(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[]; + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value. + */ + filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[]; + /** + * Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value. + */ + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; + /** + * Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value. + */ + reduce(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; + /** + * Calls the specified callback function for all the elements in an array, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value. + */ + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; + /** + * Calls the specified callback function for all the elements in an array, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value. + */ + reduceRight(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; + + [n: number]: T; +} + +interface ArrayConstructor { + new (arrayLength?: number): any[]; + new (arrayLength: number): T[]; + new (...items: T[]): T[]; + (arrayLength?: number): any[]; + (arrayLength: number): T[]; + (...items: T[]): T[]; + isArray(arg: any): arg is any[]; + readonly prototype: any[]; +} + +declare var Array: ArrayConstructor; + +interface TypedPropertyDescriptor { + enumerable?: boolean; + configurable?: boolean; + writable?: boolean; + value?: T; + get?: () => T; + set?: (value: T) => void; +} + +declare type PromiseConstructorLike = new (executor: (resolve: (value: T | PromiseLike) => void, reject: (reason?: any) => void) => void) => PromiseLike; + +interface PromiseLike { + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): PromiseLike; +} + +/** + * Represents the completion of an asynchronous operation + */ +interface Promise { + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise; + + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise; +} + +/** + * Recursively unwraps the "awaited type" of a type. Non-promise "thenables" should resolve to `never`. This emulates the behavior of `await`. + */ +type Awaited = T extends null | undefined ? T : // special case for `null | undefined` when not in `--strictNullChecks` mode + T extends object & { then(onfulfilled: infer F, ...args: infer _): any; } ? // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped + F extends ((value: infer V, ...args: infer _) => any) ? // if the argument to `then` is callable, extracts the first argument + Awaited : // recursively unwrap the value + never : // the argument to `then` was not callable + T; // non-object or non-thenable + +interface ArrayLike { + readonly length: number; + readonly [n: number]: T; +} + +/** + * Make all properties in T optional + */ +type Partial = { + [P in keyof T]?: T[P]; +}; + +/** + * Make all properties in T required + */ +type Required = { + [P in keyof T]-?: T[P]; +}; + +/** + * Make all properties in T readonly + */ +type Readonly = { + readonly [P in keyof T]: T[P]; +}; + +/** + * From T, pick a set of properties whose keys are in the union K + */ +type Pick = { + [P in K]: T[P]; +}; + +/** + * Construct a type with a set of properties K of type T + */ +type Record = { + [P in K]: T; +}; + +/** + * Exclude from T those types that are assignable to U + */ +type Exclude = T extends U ? never : T; + +/** + * Extract from T those types that are assignable to U + */ +type Extract = T extends U ? T : never; + +/** + * Construct a type with the properties of T except for those in type K. + */ +type Omit = Pick>; + +/** + * Exclude null and undefined from T + */ +type NonNullable = T & {}; + +/** + * Obtain the parameters of a function type in a tuple + */ +type Parameters any> = T extends (...args: infer P) => any ? P : never; + +/** + * Obtain the parameters of a constructor function type in a tuple + */ +type ConstructorParameters any> = T extends abstract new (...args: infer P) => any ? P : never; + +/** + * Obtain the return type of a function type + */ +type ReturnType any> = T extends (...args: any) => infer R ? R : any; + +/** + * Obtain the return type of a constructor function type + */ +type InstanceType any> = T extends abstract new (...args: any) => infer R ? R : any; + +/** + * Convert string literal type to uppercase + */ +type Uppercase = intrinsic; + +/** + * Convert string literal type to lowercase + */ +type Lowercase = intrinsic; + +/** + * Convert first character of string literal type to uppercase + */ +type Capitalize = intrinsic; + +/** + * Convert first character of string literal type to lowercase + */ +type Uncapitalize = intrinsic; + +/** + * Marker for contextual 'this' type + */ +interface ThisType {} + +/** + * Stores types to be used with WeakSet, WeakMap, WeakRef, and FinalizationRegistry + */ +interface WeakKeyTypes { + object: object; +} + +type WeakKey = WeakKeyTypes[keyof WeakKeyTypes]; + +/** + * Represents a raw buffer of binary data, which is used to store data for the + * different typed arrays. ArrayBuffers cannot be read from or written to directly, + * but can be passed to a typed array or DataView Object to interpret the raw + * buffer as needed. + */ +interface ArrayBuffer { + /** + * Read-only. The length of the ArrayBuffer (in bytes). + */ + readonly byteLength: number; + + /** + * Returns a section of an ArrayBuffer. + */ + slice(begin: number, end?: number): ArrayBuffer; +} + +/** + * Allowed ArrayBuffer types for the buffer of an ArrayBufferView and related Typed Arrays. + */ +interface ArrayBufferTypes { + ArrayBuffer: ArrayBuffer; +} +type ArrayBufferLike = ArrayBufferTypes[keyof ArrayBufferTypes]; + +interface ArrayBufferConstructor { + readonly prototype: ArrayBuffer; + new (byteLength: number): ArrayBuffer; + isView(arg: any): arg is ArrayBufferView; +} +declare var ArrayBuffer: ArrayBufferConstructor; + +interface ArrayBufferView { + /** + * The ArrayBuffer instance referenced by the array. + */ + buffer: ArrayBufferLike; + + /** + * The length in bytes of the array. + */ + byteLength: number; + + /** + * The offset in bytes of the array. + */ + byteOffset: number; +} + +interface DataView { + readonly buffer: ArrayBuffer; + readonly byteLength: number; + readonly byteOffset: number; + /** + * Gets the Float32 value at the specified byte offset from the start of the view. There is + * no alignment constraint; multi-byte values may be fetched from any offset. + * @param byteOffset The place in the buffer at which the value should be retrieved. + * @param littleEndian If false or undefined, a big-endian value should be read. + */ + getFloat32(byteOffset: number, littleEndian?: boolean): number; + + /** + * Gets the Float64 value at the specified byte offset from the start of the view. There is + * no alignment constraint; multi-byte values may be fetched from any offset. + * @param byteOffset The place in the buffer at which the value should be retrieved. + * @param littleEndian If false or undefined, a big-endian value should be read. + */ + getFloat64(byteOffset: number, littleEndian?: boolean): number; + + /** + * Gets the Int8 value at the specified byte offset from the start of the view. There is + * no alignment constraint; multi-byte values may be fetched from any offset. + * @param byteOffset The place in the buffer at which the value should be retrieved. + */ + getInt8(byteOffset: number): number; + + /** + * Gets the Int16 value at the specified byte offset from the start of the view. There is + * no alignment constraint; multi-byte values may be fetched from any offset. + * @param byteOffset The place in the buffer at which the value should be retrieved. + * @param littleEndian If false or undefined, a big-endian value should be read. + */ + getInt16(byteOffset: number, littleEndian?: boolean): number; + /** + * Gets the Int32 value at the specified byte offset from the start of the view. There is + * no alignment constraint; multi-byte values may be fetched from any offset. + * @param byteOffset The place in the buffer at which the value should be retrieved. + * @param littleEndian If false or undefined, a big-endian value should be read. + */ + getInt32(byteOffset: number, littleEndian?: boolean): number; + + /** + * Gets the Uint8 value at the specified byte offset from the start of the view. There is + * no alignment constraint; multi-byte values may be fetched from any offset. + * @param byteOffset The place in the buffer at which the value should be retrieved. + */ + getUint8(byteOffset: number): number; + + /** + * Gets the Uint16 value at the specified byte offset from the start of the view. There is + * no alignment constraint; multi-byte values may be fetched from any offset. + * @param byteOffset The place in the buffer at which the value should be retrieved. + * @param littleEndian If false or undefined, a big-endian value should be read. + */ + getUint16(byteOffset: number, littleEndian?: boolean): number; + + /** + * Gets the Uint32 value at the specified byte offset from the start of the view. There is + * no alignment constraint; multi-byte values may be fetched from any offset. + * @param byteOffset The place in the buffer at which the value should be retrieved. + * @param littleEndian If false or undefined, a big-endian value should be read. + */ + getUint32(byteOffset: number, littleEndian?: boolean): number; + + /** + * Stores an Float32 value at the specified byte offset from the start of the view. + * @param byteOffset The place in the buffer at which the value should be set. + * @param value The value to set. + * @param littleEndian If false or undefined, a big-endian value should be written. + */ + setFloat32(byteOffset: number, value: number, littleEndian?: boolean): void; + + /** + * Stores an Float64 value at the specified byte offset from the start of the view. + * @param byteOffset The place in the buffer at which the value should be set. + * @param value The value to set. + * @param littleEndian If false or undefined, a big-endian value should be written. + */ + setFloat64(byteOffset: number, value: number, littleEndian?: boolean): void; + + /** + * Stores an Int8 value at the specified byte offset from the start of the view. + * @param byteOffset The place in the buffer at which the value should be set. + * @param value The value to set. + */ + setInt8(byteOffset: number, value: number): void; + + /** + * Stores an Int16 value at the specified byte offset from the start of the view. + * @param byteOffset The place in the buffer at which the value should be set. + * @param value The value to set. + * @param littleEndian If false or undefined, a big-endian value should be written. + */ + setInt16(byteOffset: number, value: number, littleEndian?: boolean): void; + + /** + * Stores an Int32 value at the specified byte offset from the start of the view. + * @param byteOffset The place in the buffer at which the value should be set. + * @param value The value to set. + * @param littleEndian If false or undefined, a big-endian value should be written. + */ + setInt32(byteOffset: number, value: number, littleEndian?: boolean): void; + + /** + * Stores an Uint8 value at the specified byte offset from the start of the view. + * @param byteOffset The place in the buffer at which the value should be set. + * @param value The value to set. + */ + setUint8(byteOffset: number, value: number): void; + + /** + * Stores an Uint16 value at the specified byte offset from the start of the view. + * @param byteOffset The place in the buffer at which the value should be set. + * @param value The value to set. + * @param littleEndian If false or undefined, a big-endian value should be written. + */ + setUint16(byteOffset: number, value: number, littleEndian?: boolean): void; + + /** + * Stores an Uint32 value at the specified byte offset from the start of the view. + * @param byteOffset The place in the buffer at which the value should be set. + * @param value The value to set. + * @param littleEndian If false or undefined, a big-endian value should be written. + */ + setUint32(byteOffset: number, value: number, littleEndian?: boolean): void; +} + +interface DataViewConstructor { + readonly prototype: DataView; + new (buffer: ArrayBufferLike & { BYTES_PER_ELEMENT?: never; }, byteOffset?: number, byteLength?: number): DataView; +} +declare var DataView: DataViewConstructor; + +/** + * A typed array of 8-bit integer values. The contents are initialized to 0. If the requested + * number of bytes could not be allocated an exception is raised. + */ +interface Int8Array { + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * The ArrayBuffer instance referenced by the array. + */ + readonly buffer: ArrayBufferLike; + + /** + * The length in bytes of the array. + */ + readonly byteLength: number; + + /** + * The offset in bytes of the array. + */ + readonly byteOffset: number; + + /** + * Returns the this object after copying a section of the array identified by start and end + * to the same array starting at position target + * @param target If target is negative, it is treated as length+target where length is the + * length of the array. + * @param start If start is negative, it is treated as length+start. If end is negative, it + * is treated as length+end. + * @param end If not specified, length of the this object is used as its default value. + */ + copyWithin(target: number, start: number, end?: number): this; + + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + every(predicate: (value: number, index: number, array: Int8Array) => unknown, thisArg?: any): boolean; + + /** + * Changes all array elements from `start` to `end` index to a static `value` and returns the modified array + * @param value value to fill array section with + * @param start index to start filling the array at. If start is negative, it is treated as + * length+start where length is the length of the array. + * @param end index to stop filling the array at. If end is negative, it is treated as + * length+end. + */ + fill(value: number, start?: number, end?: number): this; + + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls + * the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + filter(predicate: (value: number, index: number, array: Int8Array) => any, thisArg?: any): Int8Array; + + /** + * Returns the value of the first element in the array where predicate is true, and undefined + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, find + * immediately returns that element value. Otherwise, find returns undefined. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + find(predicate: (value: number, index: number, obj: Int8Array) => boolean, thisArg?: any): number | undefined; + + /** + * Returns the index of the first element in the array where predicate is true, and -1 + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, + * findIndex immediately returns that element index. Otherwise, findIndex returns -1. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + findIndex(predicate: (value: number, index: number, obj: Int8Array) => boolean, thisArg?: any): number; + + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: number, index: number, array: Int8Array) => void, thisArg?: any): void; + + /** + * Returns the index of the first occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + indexOf(searchElement: number, fromIndex?: number): number; + + /** + * Adds all the elements of an array separated by the specified separator string. + * @param separator A string used to separate one element of an array from the next in the + * resulting String. If omitted, the array elements are separated with a comma. + */ + join(separator?: string): string; + + /** + * Returns the index of the last occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + lastIndexOf(searchElement: number, fromIndex?: number): number; + + /** + * The length of the array. + */ + readonly length: number; + + /** + * Calls a defined callback function on each element of an array, and returns an array that + * contains the results. + * @param callbackfn A function that accepts up to three arguments. The map method calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + map(callbackfn: (value: number, index: number, array: Int8Array) => number, thisArg?: any): Int8Array; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int8Array) => number): number; + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int8Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Int8Array) => U, initialValue: U): U; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an + * argument instead of an array value. + */ + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int8Array) => number): number; + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int8Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduceRight(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Int8Array) => U, initialValue: U): U; + + /** + * Reverses the elements in an Array. + */ + reverse(): Int8Array; + + /** + * Sets a value or an array of values. + * @param array A typed or untyped array of values to set. + * @param offset The index in the current array at which the values are to be written. + */ + set(array: ArrayLike, offset?: number): void; + + /** + * Returns a section of an array. + * @param start The beginning of the specified portion of the array. + * @param end The end of the specified portion of the array. This is exclusive of the element at the index 'end'. + */ + slice(start?: number, end?: number): Int8Array; + + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param predicate A function that accepts up to three arguments. The some method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + some(predicate: (value: number, index: number, array: Int8Array) => unknown, thisArg?: any): boolean; + + /** + * Sorts an array. + * @param compareFn Function used to determine the order of the elements. It is expected to return + * a negative value if first argument is less than second argument, zero if they're equal and a positive + * value otherwise. If omitted, the elements are sorted in ascending order. + * ```ts + * [11,2,22,1].sort((a, b) => a - b) + * ``` + */ + sort(compareFn?: (a: number, b: number) => number): this; + + /** + * Gets a new Int8Array view of the ArrayBuffer store for this array, referencing the elements + * at begin, inclusive, up to end, exclusive. + * @param begin The index of the beginning of the array. + * @param end The index of the end of the array. + */ + subarray(begin?: number, end?: number): Int8Array; + + /** + * Converts a number to a string by using the current locale. + */ + toLocaleString(): string; + + /** + * Returns a string representation of an array. + */ + toString(): string; + + /** Returns the primitive value of the specified object. */ + valueOf(): Int8Array; + + [index: number]: number; +} +interface Int8ArrayConstructor { + readonly prototype: Int8Array; + new (length: number): Int8Array; + new (array: ArrayLike | ArrayBufferLike): Int8Array; + new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): Int8Array; + + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * Returns a new array from a set of elements. + * @param items A set of elements to include in the new array object. + */ + of(...items: number[]): Int8Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + */ + from(arrayLike: ArrayLike): Int8Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + * @param mapfn A mapping function to call on every element of the array. + * @param thisArg Value of 'this' used to invoke the mapfn. + */ + from(arrayLike: ArrayLike, mapfn: (v: T, k: number) => number, thisArg?: any): Int8Array; +} +declare var Int8Array: Int8ArrayConstructor; + +/** + * A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the + * requested number of bytes could not be allocated an exception is raised. + */ +interface Uint8Array { + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * The ArrayBuffer instance referenced by the array. + */ + readonly buffer: ArrayBufferLike; + + /** + * The length in bytes of the array. + */ + readonly byteLength: number; + + /** + * The offset in bytes of the array. + */ + readonly byteOffset: number; + + /** + * Returns the this object after copying a section of the array identified by start and end + * to the same array starting at position target + * @param target If target is negative, it is treated as length+target where length is the + * length of the array. + * @param start If start is negative, it is treated as length+start. If end is negative, it + * is treated as length+end. + * @param end If not specified, length of the this object is used as its default value. + */ + copyWithin(target: number, start: number, end?: number): this; + + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + every(predicate: (value: number, index: number, array: Uint8Array) => unknown, thisArg?: any): boolean; + + /** + * Changes all array elements from `start` to `end` index to a static `value` and returns the modified array + * @param value value to fill array section with + * @param start index to start filling the array at. If start is negative, it is treated as + * length+start where length is the length of the array. + * @param end index to stop filling the array at. If end is negative, it is treated as + * length+end. + */ + fill(value: number, start?: number, end?: number): this; + + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls + * the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + filter(predicate: (value: number, index: number, array: Uint8Array) => any, thisArg?: any): Uint8Array; + + /** + * Returns the value of the first element in the array where predicate is true, and undefined + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, find + * immediately returns that element value. Otherwise, find returns undefined. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + find(predicate: (value: number, index: number, obj: Uint8Array) => boolean, thisArg?: any): number | undefined; + + /** + * Returns the index of the first element in the array where predicate is true, and -1 + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, + * findIndex immediately returns that element index. Otherwise, findIndex returns -1. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + findIndex(predicate: (value: number, index: number, obj: Uint8Array) => boolean, thisArg?: any): number; + + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: number, index: number, array: Uint8Array) => void, thisArg?: any): void; + + /** + * Returns the index of the first occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + indexOf(searchElement: number, fromIndex?: number): number; + + /** + * Adds all the elements of an array separated by the specified separator string. + * @param separator A string used to separate one element of an array from the next in the + * resulting String. If omitted, the array elements are separated with a comma. + */ + join(separator?: string): string; + + /** + * Returns the index of the last occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + lastIndexOf(searchElement: number, fromIndex?: number): number; + + /** + * The length of the array. + */ + readonly length: number; + + /** + * Calls a defined callback function on each element of an array, and returns an array that + * contains the results. + * @param callbackfn A function that accepts up to three arguments. The map method calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + map(callbackfn: (value: number, index: number, array: Uint8Array) => number, thisArg?: any): Uint8Array; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8Array) => number): number; + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Uint8Array) => U, initialValue: U): U; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an + * argument instead of an array value. + */ + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8Array) => number): number; + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduceRight(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Uint8Array) => U, initialValue: U): U; + + /** + * Reverses the elements in an Array. + */ + reverse(): Uint8Array; + + /** + * Sets a value or an array of values. + * @param array A typed or untyped array of values to set. + * @param offset The index in the current array at which the values are to be written. + */ + set(array: ArrayLike, offset?: number): void; + + /** + * Returns a section of an array. + * @param start The beginning of the specified portion of the array. + * @param end The end of the specified portion of the array. This is exclusive of the element at the index 'end'. + */ + slice(start?: number, end?: number): Uint8Array; + + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param predicate A function that accepts up to three arguments. The some method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + some(predicate: (value: number, index: number, array: Uint8Array) => unknown, thisArg?: any): boolean; + + /** + * Sorts an array. + * @param compareFn Function used to determine the order of the elements. It is expected to return + * a negative value if first argument is less than second argument, zero if they're equal and a positive + * value otherwise. If omitted, the elements are sorted in ascending order. + * ```ts + * [11,2,22,1].sort((a, b) => a - b) + * ``` + */ + sort(compareFn?: (a: number, b: number) => number): this; + + /** + * Gets a new Uint8Array view of the ArrayBuffer store for this array, referencing the elements + * at begin, inclusive, up to end, exclusive. + * @param begin The index of the beginning of the array. + * @param end The index of the end of the array. + */ + subarray(begin?: number, end?: number): Uint8Array; + + /** + * Converts a number to a string by using the current locale. + */ + toLocaleString(): string; + + /** + * Returns a string representation of an array. + */ + toString(): string; + + /** Returns the primitive value of the specified object. */ + valueOf(): Uint8Array; + + [index: number]: number; +} + +interface Uint8ArrayConstructor { + readonly prototype: Uint8Array; + new (length: number): Uint8Array; + new (array: ArrayLike | ArrayBufferLike): Uint8Array; + new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): Uint8Array; + + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * Returns a new array from a set of elements. + * @param items A set of elements to include in the new array object. + */ + of(...items: number[]): Uint8Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + */ + from(arrayLike: ArrayLike): Uint8Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + * @param mapfn A mapping function to call on every element of the array. + * @param thisArg Value of 'this' used to invoke the mapfn. + */ + from(arrayLike: ArrayLike, mapfn: (v: T, k: number) => number, thisArg?: any): Uint8Array; +} +declare var Uint8Array: Uint8ArrayConstructor; + +/** + * A typed array of 8-bit unsigned integer (clamped) values. The contents are initialized to 0. + * If the requested number of bytes could not be allocated an exception is raised. + */ +interface Uint8ClampedArray { + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * The ArrayBuffer instance referenced by the array. + */ + readonly buffer: ArrayBufferLike; + + /** + * The length in bytes of the array. + */ + readonly byteLength: number; + + /** + * The offset in bytes of the array. + */ + readonly byteOffset: number; + + /** + * Returns the this object after copying a section of the array identified by start and end + * to the same array starting at position target + * @param target If target is negative, it is treated as length+target where length is the + * length of the array. + * @param start If start is negative, it is treated as length+start. If end is negative, it + * is treated as length+end. + * @param end If not specified, length of the this object is used as its default value. + */ + copyWithin(target: number, start: number, end?: number): this; + + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + every(predicate: (value: number, index: number, array: Uint8ClampedArray) => unknown, thisArg?: any): boolean; + + /** + * Changes all array elements from `start` to `end` index to a static `value` and returns the modified array + * @param value value to fill array section with + * @param start index to start filling the array at. If start is negative, it is treated as + * length+start where length is the length of the array. + * @param end index to stop filling the array at. If end is negative, it is treated as + * length+end. + */ + fill(value: number, start?: number, end?: number): this; + + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls + * the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + filter(predicate: (value: number, index: number, array: Uint8ClampedArray) => any, thisArg?: any): Uint8ClampedArray; + + /** + * Returns the value of the first element in the array where predicate is true, and undefined + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, find + * immediately returns that element value. Otherwise, find returns undefined. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + find(predicate: (value: number, index: number, obj: Uint8ClampedArray) => boolean, thisArg?: any): number | undefined; + + /** + * Returns the index of the first element in the array where predicate is true, and -1 + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, + * findIndex immediately returns that element index. Otherwise, findIndex returns -1. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + findIndex(predicate: (value: number, index: number, obj: Uint8ClampedArray) => boolean, thisArg?: any): number; + + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: number, index: number, array: Uint8ClampedArray) => void, thisArg?: any): void; + + /** + * Returns the index of the first occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + indexOf(searchElement: number, fromIndex?: number): number; + + /** + * Adds all the elements of an array separated by the specified separator string. + * @param separator A string used to separate one element of an array from the next in the + * resulting String. If omitted, the array elements are separated with a comma. + */ + join(separator?: string): string; + + /** + * Returns the index of the last occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + lastIndexOf(searchElement: number, fromIndex?: number): number; + + /** + * The length of the array. + */ + readonly length: number; + + /** + * Calls a defined callback function on each element of an array, and returns an array that + * contains the results. + * @param callbackfn A function that accepts up to three arguments. The map method calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + map(callbackfn: (value: number, index: number, array: Uint8ClampedArray) => number, thisArg?: any): Uint8ClampedArray; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8ClampedArray) => number): number; + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8ClampedArray) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Uint8ClampedArray) => U, initialValue: U): U; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an + * argument instead of an array value. + */ + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8ClampedArray) => number): number; + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8ClampedArray) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduceRight(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Uint8ClampedArray) => U, initialValue: U): U; + + /** + * Reverses the elements in an Array. + */ + reverse(): Uint8ClampedArray; + + /** + * Sets a value or an array of values. + * @param array A typed or untyped array of values to set. + * @param offset The index in the current array at which the values are to be written. + */ + set(array: ArrayLike, offset?: number): void; + + /** + * Returns a section of an array. + * @param start The beginning of the specified portion of the array. + * @param end The end of the specified portion of the array. This is exclusive of the element at the index 'end'. + */ + slice(start?: number, end?: number): Uint8ClampedArray; + + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param predicate A function that accepts up to three arguments. The some method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + some(predicate: (value: number, index: number, array: Uint8ClampedArray) => unknown, thisArg?: any): boolean; + + /** + * Sorts an array. + * @param compareFn Function used to determine the order of the elements. It is expected to return + * a negative value if first argument is less than second argument, zero if they're equal and a positive + * value otherwise. If omitted, the elements are sorted in ascending order. + * ```ts + * [11,2,22,1].sort((a, b) => a - b) + * ``` + */ + sort(compareFn?: (a: number, b: number) => number): this; + + /** + * Gets a new Uint8ClampedArray view of the ArrayBuffer store for this array, referencing the elements + * at begin, inclusive, up to end, exclusive. + * @param begin The index of the beginning of the array. + * @param end The index of the end of the array. + */ + subarray(begin?: number, end?: number): Uint8ClampedArray; + + /** + * Converts a number to a string by using the current locale. + */ + toLocaleString(): string; + + /** + * Returns a string representation of an array. + */ + toString(): string; + + /** Returns the primitive value of the specified object. */ + valueOf(): Uint8ClampedArray; + + [index: number]: number; +} + +interface Uint8ClampedArrayConstructor { + readonly prototype: Uint8ClampedArray; + new (length: number): Uint8ClampedArray; + new (array: ArrayLike | ArrayBufferLike): Uint8ClampedArray; + new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): Uint8ClampedArray; + + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * Returns a new array from a set of elements. + * @param items A set of elements to include in the new array object. + */ + of(...items: number[]): Uint8ClampedArray; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + */ + from(arrayLike: ArrayLike): Uint8ClampedArray; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + * @param mapfn A mapping function to call on every element of the array. + * @param thisArg Value of 'this' used to invoke the mapfn. + */ + from(arrayLike: ArrayLike, mapfn: (v: T, k: number) => number, thisArg?: any): Uint8ClampedArray; +} +declare var Uint8ClampedArray: Uint8ClampedArrayConstructor; + +/** + * A typed array of 16-bit signed integer values. The contents are initialized to 0. If the + * requested number of bytes could not be allocated an exception is raised. + */ +interface Int16Array { + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * The ArrayBuffer instance referenced by the array. + */ + readonly buffer: ArrayBufferLike; + + /** + * The length in bytes of the array. + */ + readonly byteLength: number; + + /** + * The offset in bytes of the array. + */ + readonly byteOffset: number; + + /** + * Returns the this object after copying a section of the array identified by start and end + * to the same array starting at position target + * @param target If target is negative, it is treated as length+target where length is the + * length of the array. + * @param start If start is negative, it is treated as length+start. If end is negative, it + * is treated as length+end. + * @param end If not specified, length of the this object is used as its default value. + */ + copyWithin(target: number, start: number, end?: number): this; + + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + every(predicate: (value: number, index: number, array: Int16Array) => unknown, thisArg?: any): boolean; + + /** + * Changes all array elements from `start` to `end` index to a static `value` and returns the modified array + * @param value value to fill array section with + * @param start index to start filling the array at. If start is negative, it is treated as + * length+start where length is the length of the array. + * @param end index to stop filling the array at. If end is negative, it is treated as + * length+end. + */ + fill(value: number, start?: number, end?: number): this; + + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls + * the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + filter(predicate: (value: number, index: number, array: Int16Array) => any, thisArg?: any): Int16Array; + + /** + * Returns the value of the first element in the array where predicate is true, and undefined + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, find + * immediately returns that element value. Otherwise, find returns undefined. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + find(predicate: (value: number, index: number, obj: Int16Array) => boolean, thisArg?: any): number | undefined; + + /** + * Returns the index of the first element in the array where predicate is true, and -1 + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, + * findIndex immediately returns that element index. Otherwise, findIndex returns -1. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + findIndex(predicate: (value: number, index: number, obj: Int16Array) => boolean, thisArg?: any): number; + + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: number, index: number, array: Int16Array) => void, thisArg?: any): void; + /** + * Returns the index of the first occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + indexOf(searchElement: number, fromIndex?: number): number; + + /** + * Adds all the elements of an array separated by the specified separator string. + * @param separator A string used to separate one element of an array from the next in the + * resulting String. If omitted, the array elements are separated with a comma. + */ + join(separator?: string): string; + + /** + * Returns the index of the last occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + lastIndexOf(searchElement: number, fromIndex?: number): number; + + /** + * The length of the array. + */ + readonly length: number; + + /** + * Calls a defined callback function on each element of an array, and returns an array that + * contains the results. + * @param callbackfn A function that accepts up to three arguments. The map method calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + map(callbackfn: (value: number, index: number, array: Int16Array) => number, thisArg?: any): Int16Array; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int16Array) => number): number; + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int16Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Int16Array) => U, initialValue: U): U; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an + * argument instead of an array value. + */ + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int16Array) => number): number; + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int16Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduceRight(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Int16Array) => U, initialValue: U): U; + + /** + * Reverses the elements in an Array. + */ + reverse(): Int16Array; + + /** + * Sets a value or an array of values. + * @param array A typed or untyped array of values to set. + * @param offset The index in the current array at which the values are to be written. + */ + set(array: ArrayLike, offset?: number): void; + + /** + * Returns a section of an array. + * @param start The beginning of the specified portion of the array. + * @param end The end of the specified portion of the array. This is exclusive of the element at the index 'end'. + */ + slice(start?: number, end?: number): Int16Array; + + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param predicate A function that accepts up to three arguments. The some method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + some(predicate: (value: number, index: number, array: Int16Array) => unknown, thisArg?: any): boolean; + + /** + * Sorts an array. + * @param compareFn Function used to determine the order of the elements. It is expected to return + * a negative value if first argument is less than second argument, zero if they're equal and a positive + * value otherwise. If omitted, the elements are sorted in ascending order. + * ```ts + * [11,2,22,1].sort((a, b) => a - b) + * ``` + */ + sort(compareFn?: (a: number, b: number) => number): this; + + /** + * Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements + * at begin, inclusive, up to end, exclusive. + * @param begin The index of the beginning of the array. + * @param end The index of the end of the array. + */ + subarray(begin?: number, end?: number): Int16Array; + + /** + * Converts a number to a string by using the current locale. + */ + toLocaleString(): string; + + /** + * Returns a string representation of an array. + */ + toString(): string; + + /** Returns the primitive value of the specified object. */ + valueOf(): Int16Array; + + [index: number]: number; +} + +interface Int16ArrayConstructor { + readonly prototype: Int16Array; + new (length: number): Int16Array; + new (array: ArrayLike | ArrayBufferLike): Int16Array; + new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): Int16Array; + + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * Returns a new array from a set of elements. + * @param items A set of elements to include in the new array object. + */ + of(...items: number[]): Int16Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + */ + from(arrayLike: ArrayLike): Int16Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + * @param mapfn A mapping function to call on every element of the array. + * @param thisArg Value of 'this' used to invoke the mapfn. + */ + from(arrayLike: ArrayLike, mapfn: (v: T, k: number) => number, thisArg?: any): Int16Array; +} +declare var Int16Array: Int16ArrayConstructor; + +/** + * A typed array of 16-bit unsigned integer values. The contents are initialized to 0. If the + * requested number of bytes could not be allocated an exception is raised. + */ +interface Uint16Array { + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * The ArrayBuffer instance referenced by the array. + */ + readonly buffer: ArrayBufferLike; + + /** + * The length in bytes of the array. + */ + readonly byteLength: number; + + /** + * The offset in bytes of the array. + */ + readonly byteOffset: number; + + /** + * Returns the this object after copying a section of the array identified by start and end + * to the same array starting at position target + * @param target If target is negative, it is treated as length+target where length is the + * length of the array. + * @param start If start is negative, it is treated as length+start. If end is negative, it + * is treated as length+end. + * @param end If not specified, length of the this object is used as its default value. + */ + copyWithin(target: number, start: number, end?: number): this; + + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + every(predicate: (value: number, index: number, array: Uint16Array) => unknown, thisArg?: any): boolean; + + /** + * Changes all array elements from `start` to `end` index to a static `value` and returns the modified array + * @param value value to fill array section with + * @param start index to start filling the array at. If start is negative, it is treated as + * length+start where length is the length of the array. + * @param end index to stop filling the array at. If end is negative, it is treated as + * length+end. + */ + fill(value: number, start?: number, end?: number): this; + + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls + * the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + filter(predicate: (value: number, index: number, array: Uint16Array) => any, thisArg?: any): Uint16Array; + + /** + * Returns the value of the first element in the array where predicate is true, and undefined + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, find + * immediately returns that element value. Otherwise, find returns undefined. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + find(predicate: (value: number, index: number, obj: Uint16Array) => boolean, thisArg?: any): number | undefined; + + /** + * Returns the index of the first element in the array where predicate is true, and -1 + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, + * findIndex immediately returns that element index. Otherwise, findIndex returns -1. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + findIndex(predicate: (value: number, index: number, obj: Uint16Array) => boolean, thisArg?: any): number; + + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: number, index: number, array: Uint16Array) => void, thisArg?: any): void; + + /** + * Returns the index of the first occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + indexOf(searchElement: number, fromIndex?: number): number; + + /** + * Adds all the elements of an array separated by the specified separator string. + * @param separator A string used to separate one element of an array from the next in the + * resulting String. If omitted, the array elements are separated with a comma. + */ + join(separator?: string): string; + + /** + * Returns the index of the last occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + lastIndexOf(searchElement: number, fromIndex?: number): number; + + /** + * The length of the array. + */ + readonly length: number; + + /** + * Calls a defined callback function on each element of an array, and returns an array that + * contains the results. + * @param callbackfn A function that accepts up to three arguments. The map method calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + map(callbackfn: (value: number, index: number, array: Uint16Array) => number, thisArg?: any): Uint16Array; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint16Array) => number): number; + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint16Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Uint16Array) => U, initialValue: U): U; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an + * argument instead of an array value. + */ + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint16Array) => number): number; + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint16Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduceRight(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Uint16Array) => U, initialValue: U): U; + + /** + * Reverses the elements in an Array. + */ + reverse(): Uint16Array; + + /** + * Sets a value or an array of values. + * @param array A typed or untyped array of values to set. + * @param offset The index in the current array at which the values are to be written. + */ + set(array: ArrayLike, offset?: number): void; + + /** + * Returns a section of an array. + * @param start The beginning of the specified portion of the array. + * @param end The end of the specified portion of the array. This is exclusive of the element at the index 'end'. + */ + slice(start?: number, end?: number): Uint16Array; + + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param predicate A function that accepts up to three arguments. The some method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + some(predicate: (value: number, index: number, array: Uint16Array) => unknown, thisArg?: any): boolean; + + /** + * Sorts an array. + * @param compareFn Function used to determine the order of the elements. It is expected to return + * a negative value if first argument is less than second argument, zero if they're equal and a positive + * value otherwise. If omitted, the elements are sorted in ascending order. + * ```ts + * [11,2,22,1].sort((a, b) => a - b) + * ``` + */ + sort(compareFn?: (a: number, b: number) => number): this; + + /** + * Gets a new Uint16Array view of the ArrayBuffer store for this array, referencing the elements + * at begin, inclusive, up to end, exclusive. + * @param begin The index of the beginning of the array. + * @param end The index of the end of the array. + */ + subarray(begin?: number, end?: number): Uint16Array; + + /** + * Converts a number to a string by using the current locale. + */ + toLocaleString(): string; + + /** + * Returns a string representation of an array. + */ + toString(): string; + + /** Returns the primitive value of the specified object. */ + valueOf(): Uint16Array; + + [index: number]: number; +} + +interface Uint16ArrayConstructor { + readonly prototype: Uint16Array; + new (length: number): Uint16Array; + new (array: ArrayLike | ArrayBufferLike): Uint16Array; + new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): Uint16Array; + + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * Returns a new array from a set of elements. + * @param items A set of elements to include in the new array object. + */ + of(...items: number[]): Uint16Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + */ + from(arrayLike: ArrayLike): Uint16Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + * @param mapfn A mapping function to call on every element of the array. + * @param thisArg Value of 'this' used to invoke the mapfn. + */ + from(arrayLike: ArrayLike, mapfn: (v: T, k: number) => number, thisArg?: any): Uint16Array; +} +declare var Uint16Array: Uint16ArrayConstructor; +/** + * A typed array of 32-bit signed integer values. The contents are initialized to 0. If the + * requested number of bytes could not be allocated an exception is raised. + */ +interface Int32Array { + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * The ArrayBuffer instance referenced by the array. + */ + readonly buffer: ArrayBufferLike; + + /** + * The length in bytes of the array. + */ + readonly byteLength: number; + + /** + * The offset in bytes of the array. + */ + readonly byteOffset: number; + + /** + * Returns the this object after copying a section of the array identified by start and end + * to the same array starting at position target + * @param target If target is negative, it is treated as length+target where length is the + * length of the array. + * @param start If start is negative, it is treated as length+start. If end is negative, it + * is treated as length+end. + * @param end If not specified, length of the this object is used as its default value. + */ + copyWithin(target: number, start: number, end?: number): this; + + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + every(predicate: (value: number, index: number, array: Int32Array) => unknown, thisArg?: any): boolean; + + /** + * Changes all array elements from `start` to `end` index to a static `value` and returns the modified array + * @param value value to fill array section with + * @param start index to start filling the array at. If start is negative, it is treated as + * length+start where length is the length of the array. + * @param end index to stop filling the array at. If end is negative, it is treated as + * length+end. + */ + fill(value: number, start?: number, end?: number): this; + + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls + * the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + filter(predicate: (value: number, index: number, array: Int32Array) => any, thisArg?: any): Int32Array; + + /** + * Returns the value of the first element in the array where predicate is true, and undefined + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, find + * immediately returns that element value. Otherwise, find returns undefined. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + find(predicate: (value: number, index: number, obj: Int32Array) => boolean, thisArg?: any): number | undefined; + + /** + * Returns the index of the first element in the array where predicate is true, and -1 + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, + * findIndex immediately returns that element index. Otherwise, findIndex returns -1. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + findIndex(predicate: (value: number, index: number, obj: Int32Array) => boolean, thisArg?: any): number; + + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: number, index: number, array: Int32Array) => void, thisArg?: any): void; + + /** + * Returns the index of the first occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + indexOf(searchElement: number, fromIndex?: number): number; + + /** + * Adds all the elements of an array separated by the specified separator string. + * @param separator A string used to separate one element of an array from the next in the + * resulting String. If omitted, the array elements are separated with a comma. + */ + join(separator?: string): string; + + /** + * Returns the index of the last occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + lastIndexOf(searchElement: number, fromIndex?: number): number; + + /** + * The length of the array. + */ + readonly length: number; + + /** + * Calls a defined callback function on each element of an array, and returns an array that + * contains the results. + * @param callbackfn A function that accepts up to three arguments. The map method calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + map(callbackfn: (value: number, index: number, array: Int32Array) => number, thisArg?: any): Int32Array; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int32Array) => number): number; + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int32Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Int32Array) => U, initialValue: U): U; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an + * argument instead of an array value. + */ + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int32Array) => number): number; + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int32Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduceRight(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Int32Array) => U, initialValue: U): U; + + /** + * Reverses the elements in an Array. + */ + reverse(): Int32Array; + + /** + * Sets a value or an array of values. + * @param array A typed or untyped array of values to set. + * @param offset The index in the current array at which the values are to be written. + */ + set(array: ArrayLike, offset?: number): void; + + /** + * Returns a section of an array. + * @param start The beginning of the specified portion of the array. + * @param end The end of the specified portion of the array. This is exclusive of the element at the index 'end'. + */ + slice(start?: number, end?: number): Int32Array; + + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param predicate A function that accepts up to three arguments. The some method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + some(predicate: (value: number, index: number, array: Int32Array) => unknown, thisArg?: any): boolean; + + /** + * Sorts an array. + * @param compareFn Function used to determine the order of the elements. It is expected to return + * a negative value if first argument is less than second argument, zero if they're equal and a positive + * value otherwise. If omitted, the elements are sorted in ascending order. + * ```ts + * [11,2,22,1].sort((a, b) => a - b) + * ``` + */ + sort(compareFn?: (a: number, b: number) => number): this; + + /** + * Gets a new Int32Array view of the ArrayBuffer store for this array, referencing the elements + * at begin, inclusive, up to end, exclusive. + * @param begin The index of the beginning of the array. + * @param end The index of the end of the array. + */ + subarray(begin?: number, end?: number): Int32Array; + + /** + * Converts a number to a string by using the current locale. + */ + toLocaleString(): string; + + /** + * Returns a string representation of an array. + */ + toString(): string; + + /** Returns the primitive value of the specified object. */ + valueOf(): Int32Array; + + [index: number]: number; +} + +interface Int32ArrayConstructor { + readonly prototype: Int32Array; + new (length: number): Int32Array; + new (array: ArrayLike | ArrayBufferLike): Int32Array; + new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): Int32Array; + + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * Returns a new array from a set of elements. + * @param items A set of elements to include in the new array object. + */ + of(...items: number[]): Int32Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + */ + from(arrayLike: ArrayLike): Int32Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + * @param mapfn A mapping function to call on every element of the array. + * @param thisArg Value of 'this' used to invoke the mapfn. + */ + from(arrayLike: ArrayLike, mapfn: (v: T, k: number) => number, thisArg?: any): Int32Array; +} +declare var Int32Array: Int32ArrayConstructor; + +/** + * A typed array of 32-bit unsigned integer values. The contents are initialized to 0. If the + * requested number of bytes could not be allocated an exception is raised. + */ +interface Uint32Array { + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * The ArrayBuffer instance referenced by the array. + */ + readonly buffer: ArrayBufferLike; + + /** + * The length in bytes of the array. + */ + readonly byteLength: number; + + /** + * The offset in bytes of the array. + */ + readonly byteOffset: number; + + /** + * Returns the this object after copying a section of the array identified by start and end + * to the same array starting at position target + * @param target If target is negative, it is treated as length+target where length is the + * length of the array. + * @param start If start is negative, it is treated as length+start. If end is negative, it + * is treated as length+end. + * @param end If not specified, length of the this object is used as its default value. + */ + copyWithin(target: number, start: number, end?: number): this; + + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + every(predicate: (value: number, index: number, array: Uint32Array) => unknown, thisArg?: any): boolean; + + /** + * Changes all array elements from `start` to `end` index to a static `value` and returns the modified array + * @param value value to fill array section with + * @param start index to start filling the array at. If start is negative, it is treated as + * length+start where length is the length of the array. + * @param end index to stop filling the array at. If end is negative, it is treated as + * length+end. + */ + fill(value: number, start?: number, end?: number): this; + + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls + * the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + filter(predicate: (value: number, index: number, array: Uint32Array) => any, thisArg?: any): Uint32Array; + + /** + * Returns the value of the first element in the array where predicate is true, and undefined + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, find + * immediately returns that element value. Otherwise, find returns undefined. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + find(predicate: (value: number, index: number, obj: Uint32Array) => boolean, thisArg?: any): number | undefined; + + /** + * Returns the index of the first element in the array where predicate is true, and -1 + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, + * findIndex immediately returns that element index. Otherwise, findIndex returns -1. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + findIndex(predicate: (value: number, index: number, obj: Uint32Array) => boolean, thisArg?: any): number; + + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: number, index: number, array: Uint32Array) => void, thisArg?: any): void; + /** + * Returns the index of the first occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + indexOf(searchElement: number, fromIndex?: number): number; + + /** + * Adds all the elements of an array separated by the specified separator string. + * @param separator A string used to separate one element of an array from the next in the + * resulting String. If omitted, the array elements are separated with a comma. + */ + join(separator?: string): string; + + /** + * Returns the index of the last occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + lastIndexOf(searchElement: number, fromIndex?: number): number; + + /** + * The length of the array. + */ + readonly length: number; + + /** + * Calls a defined callback function on each element of an array, and returns an array that + * contains the results. + * @param callbackfn A function that accepts up to three arguments. The map method calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + map(callbackfn: (value: number, index: number, array: Uint32Array) => number, thisArg?: any): Uint32Array; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint32Array) => number): number; + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint32Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Uint32Array) => U, initialValue: U): U; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an + * argument instead of an array value. + */ + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint32Array) => number): number; + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint32Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduceRight(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Uint32Array) => U, initialValue: U): U; + + /** + * Reverses the elements in an Array. + */ + reverse(): Uint32Array; + + /** + * Sets a value or an array of values. + * @param array A typed or untyped array of values to set. + * @param offset The index in the current array at which the values are to be written. + */ + set(array: ArrayLike, offset?: number): void; + + /** + * Returns a section of an array. + * @param start The beginning of the specified portion of the array. + * @param end The end of the specified portion of the array. This is exclusive of the element at the index 'end'. + */ + slice(start?: number, end?: number): Uint32Array; + + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param predicate A function that accepts up to three arguments. The some method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + some(predicate: (value: number, index: number, array: Uint32Array) => unknown, thisArg?: any): boolean; + + /** + * Sorts an array. + * @param compareFn Function used to determine the order of the elements. It is expected to return + * a negative value if first argument is less than second argument, zero if they're equal and a positive + * value otherwise. If omitted, the elements are sorted in ascending order. + * ```ts + * [11,2,22,1].sort((a, b) => a - b) + * ``` + */ + sort(compareFn?: (a: number, b: number) => number): this; + + /** + * Gets a new Uint32Array view of the ArrayBuffer store for this array, referencing the elements + * at begin, inclusive, up to end, exclusive. + * @param begin The index of the beginning of the array. + * @param end The index of the end of the array. + */ + subarray(begin?: number, end?: number): Uint32Array; + + /** + * Converts a number to a string by using the current locale. + */ + toLocaleString(): string; + + /** + * Returns a string representation of an array. + */ + toString(): string; + + /** Returns the primitive value of the specified object. */ + valueOf(): Uint32Array; + + [index: number]: number; +} + +interface Uint32ArrayConstructor { + readonly prototype: Uint32Array; + new (length: number): Uint32Array; + new (array: ArrayLike | ArrayBufferLike): Uint32Array; + new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): Uint32Array; + + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * Returns a new array from a set of elements. + * @param items A set of elements to include in the new array object. + */ + of(...items: number[]): Uint32Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + */ + from(arrayLike: ArrayLike): Uint32Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + * @param mapfn A mapping function to call on every element of the array. + * @param thisArg Value of 'this' used to invoke the mapfn. + */ + from(arrayLike: ArrayLike, mapfn: (v: T, k: number) => number, thisArg?: any): Uint32Array; +} +declare var Uint32Array: Uint32ArrayConstructor; + +/** + * A typed array of 32-bit float values. The contents are initialized to 0. If the requested number + * of bytes could not be allocated an exception is raised. + */ +interface Float32Array { + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * The ArrayBuffer instance referenced by the array. + */ + readonly buffer: ArrayBufferLike; + + /** + * The length in bytes of the array. + */ + readonly byteLength: number; + + /** + * The offset in bytes of the array. + */ + readonly byteOffset: number; + + /** + * Returns the this object after copying a section of the array identified by start and end + * to the same array starting at position target + * @param target If target is negative, it is treated as length+target where length is the + * length of the array. + * @param start If start is negative, it is treated as length+start. If end is negative, it + * is treated as length+end. + * @param end If not specified, length of the this object is used as its default value. + */ + copyWithin(target: number, start: number, end?: number): this; + + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + every(predicate: (value: number, index: number, array: Float32Array) => unknown, thisArg?: any): boolean; + + /** + * Changes all array elements from `start` to `end` index to a static `value` and returns the modified array + * @param value value to fill array section with + * @param start index to start filling the array at. If start is negative, it is treated as + * length+start where length is the length of the array. + * @param end index to stop filling the array at. If end is negative, it is treated as + * length+end. + */ + fill(value: number, start?: number, end?: number): this; + + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls + * the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + filter(predicate: (value: number, index: number, array: Float32Array) => any, thisArg?: any): Float32Array; + + /** + * Returns the value of the first element in the array where predicate is true, and undefined + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, find + * immediately returns that element value. Otherwise, find returns undefined. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + find(predicate: (value: number, index: number, obj: Float32Array) => boolean, thisArg?: any): number | undefined; + + /** + * Returns the index of the first element in the array where predicate is true, and -1 + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, + * findIndex immediately returns that element index. Otherwise, findIndex returns -1. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + findIndex(predicate: (value: number, index: number, obj: Float32Array) => boolean, thisArg?: any): number; + + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: number, index: number, array: Float32Array) => void, thisArg?: any): void; + + /** + * Returns the index of the first occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + indexOf(searchElement: number, fromIndex?: number): number; + + /** + * Adds all the elements of an array separated by the specified separator string. + * @param separator A string used to separate one element of an array from the next in the + * resulting String. If omitted, the array elements are separated with a comma. + */ + join(separator?: string): string; + + /** + * Returns the index of the last occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + lastIndexOf(searchElement: number, fromIndex?: number): number; + + /** + * The length of the array. + */ + readonly length: number; + + /** + * Calls a defined callback function on each element of an array, and returns an array that + * contains the results. + * @param callbackfn A function that accepts up to three arguments. The map method calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + map(callbackfn: (value: number, index: number, array: Float32Array) => number, thisArg?: any): Float32Array; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Float32Array) => number): number; + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Float32Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Float32Array) => U, initialValue: U): U; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an + * argument instead of an array value. + */ + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Float32Array) => number): number; + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Float32Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduceRight(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Float32Array) => U, initialValue: U): U; + + /** + * Reverses the elements in an Array. + */ + reverse(): Float32Array; + + /** + * Sets a value or an array of values. + * @param array A typed or untyped array of values to set. + * @param offset The index in the current array at which the values are to be written. + */ + set(array: ArrayLike, offset?: number): void; + + /** + * Returns a section of an array. + * @param start The beginning of the specified portion of the array. + * @param end The end of the specified portion of the array. This is exclusive of the element at the index 'end'. + */ + slice(start?: number, end?: number): Float32Array; + + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param predicate A function that accepts up to three arguments. The some method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + some(predicate: (value: number, index: number, array: Float32Array) => unknown, thisArg?: any): boolean; + + /** + * Sorts an array. + * @param compareFn Function used to determine the order of the elements. It is expected to return + * a negative value if first argument is less than second argument, zero if they're equal and a positive + * value otherwise. If omitted, the elements are sorted in ascending order. + * ```ts + * [11,2,22,1].sort((a, b) => a - b) + * ``` + */ + sort(compareFn?: (a: number, b: number) => number): this; + + /** + * Gets a new Float32Array view of the ArrayBuffer store for this array, referencing the elements + * at begin, inclusive, up to end, exclusive. + * @param begin The index of the beginning of the array. + * @param end The index of the end of the array. + */ + subarray(begin?: number, end?: number): Float32Array; + + /** + * Converts a number to a string by using the current locale. + */ + toLocaleString(): string; + + /** + * Returns a string representation of an array. + */ + toString(): string; + + /** Returns the primitive value of the specified object. */ + valueOf(): Float32Array; + + [index: number]: number; +} + +interface Float32ArrayConstructor { + readonly prototype: Float32Array; + new (length: number): Float32Array; + new (array: ArrayLike | ArrayBufferLike): Float32Array; + new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): Float32Array; + + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * Returns a new array from a set of elements. + * @param items A set of elements to include in the new array object. + */ + of(...items: number[]): Float32Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + */ + from(arrayLike: ArrayLike): Float32Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + * @param mapfn A mapping function to call on every element of the array. + * @param thisArg Value of 'this' used to invoke the mapfn. + */ + from(arrayLike: ArrayLike, mapfn: (v: T, k: number) => number, thisArg?: any): Float32Array; +} +declare var Float32Array: Float32ArrayConstructor; + +/** + * A typed array of 64-bit float values. The contents are initialized to 0. If the requested + * number of bytes could not be allocated an exception is raised. + */ +interface Float64Array { + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * The ArrayBuffer instance referenced by the array. + */ + readonly buffer: ArrayBufferLike; + + /** + * The length in bytes of the array. + */ + readonly byteLength: number; + + /** + * The offset in bytes of the array. + */ + readonly byteOffset: number; + + /** + * Returns the this object after copying a section of the array identified by start and end + * to the same array starting at position target + * @param target If target is negative, it is treated as length+target where length is the + * length of the array. + * @param start If start is negative, it is treated as length+start. If end is negative, it + * is treated as length+end. + * @param end If not specified, length of the this object is used as its default value. + */ + copyWithin(target: number, start: number, end?: number): this; + + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + every(predicate: (value: number, index: number, array: Float64Array) => unknown, thisArg?: any): boolean; + + /** + * Changes all array elements from `start` to `end` index to a static `value` and returns the modified array + * @param value value to fill array section with + * @param start index to start filling the array at. If start is negative, it is treated as + * length+start where length is the length of the array. + * @param end index to stop filling the array at. If end is negative, it is treated as + * length+end. + */ + fill(value: number, start?: number, end?: number): this; + + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls + * the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + filter(predicate: (value: number, index: number, array: Float64Array) => any, thisArg?: any): Float64Array; + + /** + * Returns the value of the first element in the array where predicate is true, and undefined + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, find + * immediately returns that element value. Otherwise, find returns undefined. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + find(predicate: (value: number, index: number, obj: Float64Array) => boolean, thisArg?: any): number | undefined; + + /** + * Returns the index of the first element in the array where predicate is true, and -1 + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, + * findIndex immediately returns that element index. Otherwise, findIndex returns -1. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + findIndex(predicate: (value: number, index: number, obj: Float64Array) => boolean, thisArg?: any): number; + + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: number, index: number, array: Float64Array) => void, thisArg?: any): void; + + /** + * Returns the index of the first occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + indexOf(searchElement: number, fromIndex?: number): number; + + /** + * Adds all the elements of an array separated by the specified separator string. + * @param separator A string used to separate one element of an array from the next in the + * resulting String. If omitted, the array elements are separated with a comma. + */ + join(separator?: string): string; + + /** + * Returns the index of the last occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + lastIndexOf(searchElement: number, fromIndex?: number): number; + + /** + * The length of the array. + */ + readonly length: number; + + /** + * Calls a defined callback function on each element of an array, and returns an array that + * contains the results. + * @param callbackfn A function that accepts up to three arguments. The map method calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + map(callbackfn: (value: number, index: number, array: Float64Array) => number, thisArg?: any): Float64Array; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Float64Array) => number): number; + reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Float64Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Float64Array) => U, initialValue: U): U; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an + * argument instead of an array value. + */ + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Float64Array) => number): number; + reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Float64Array) => number, initialValue: number): number; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduceRight(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Float64Array) => U, initialValue: U): U; + + /** + * Reverses the elements in an Array. + */ + reverse(): Float64Array; + + /** + * Sets a value or an array of values. + * @param array A typed or untyped array of values to set. + * @param offset The index in the current array at which the values are to be written. + */ + set(array: ArrayLike, offset?: number): void; + + /** + * Returns a section of an array. + * @param start The beginning of the specified portion of the array. + * @param end The end of the specified portion of the array. This is exclusive of the element at the index 'end'. + */ + slice(start?: number, end?: number): Float64Array; + + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param predicate A function that accepts up to three arguments. The some method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + some(predicate: (value: number, index: number, array: Float64Array) => unknown, thisArg?: any): boolean; + + /** + * Sorts an array. + * @param compareFn Function used to determine the order of the elements. It is expected to return + * a negative value if first argument is less than second argument, zero if they're equal and a positive + * value otherwise. If omitted, the elements are sorted in ascending order. + * ```ts + * [11,2,22,1].sort((a, b) => a - b) + * ``` + */ + sort(compareFn?: (a: number, b: number) => number): this; + + /** + * Gets a new Float64Array view of the ArrayBuffer store for this array, referencing the elements + * at begin, inclusive, up to end, exclusive. + * @param begin The index of the beginning of the array. + * @param end The index of the end of the array. + */ + subarray(begin?: number, end?: number): Float64Array; + + /** + * Converts a number to a string by using the current locale. + */ + toLocaleString(): string; + + /** + * Returns a string representation of an array. + */ + toString(): string; + + /** Returns the primitive value of the specified object. */ + valueOf(): Float64Array; + + [index: number]: number; +} + +interface Float64ArrayConstructor { + readonly prototype: Float64Array; + new (length: number): Float64Array; + new (array: ArrayLike | ArrayBufferLike): Float64Array; + new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): Float64Array; + + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * Returns a new array from a set of elements. + * @param items A set of elements to include in the new array object. + */ + of(...items: number[]): Float64Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + */ + from(arrayLike: ArrayLike): Float64Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like or iterable object to convert to an array. + * @param mapfn A mapping function to call on every element of the array. + * @param thisArg Value of 'this' used to invoke the mapfn. + */ + from(arrayLike: ArrayLike, mapfn: (v: T, k: number) => number, thisArg?: any): Float64Array; +} +declare var Float64Array: Float64ArrayConstructor; + +///////////////////////////// +/// ECMAScript Internationalization API +///////////////////////////// + +declare namespace Intl { + interface CollatorOptions { + usage?: "sort" | "search" | undefined; + localeMatcher?: "lookup" | "best fit" | undefined; + numeric?: boolean | undefined; + caseFirst?: "upper" | "lower" | "false" | undefined; + sensitivity?: "base" | "accent" | "case" | "variant" | undefined; + collation?: "big5han" | "compat" | "dict" | "direct" | "ducet" | "emoji" | "eor" | "gb2312" | "phonebk" | "phonetic" | "pinyin" | "reformed" | "searchjl" | "stroke" | "trad" | "unihan" | "zhuyin" | undefined; + ignorePunctuation?: boolean | undefined; + } + + interface ResolvedCollatorOptions { + locale: string; + usage: string; + sensitivity: string; + ignorePunctuation: boolean; + collation: string; + caseFirst: string; + numeric: boolean; + } + + interface Collator { + compare(x: string, y: string): number; + resolvedOptions(): ResolvedCollatorOptions; + } + var Collator: { + new (locales?: string | string[], options?: CollatorOptions): Collator; + (locales?: string | string[], options?: CollatorOptions): Collator; + supportedLocalesOf(locales: string | string[], options?: CollatorOptions): string[]; + }; + + interface NumberFormatOptions { + localeMatcher?: string | undefined; + style?: string | undefined; + currency?: string | undefined; + currencySign?: string | undefined; + useGrouping?: boolean | undefined; + minimumIntegerDigits?: number | undefined; + minimumFractionDigits?: number | undefined; + maximumFractionDigits?: number | undefined; + minimumSignificantDigits?: number | undefined; + maximumSignificantDigits?: number | undefined; + } + + interface ResolvedNumberFormatOptions { + locale: string; + numberingSystem: string; + style: string; + currency?: string; + minimumIntegerDigits: number; + minimumFractionDigits: number; + maximumFractionDigits: number; + minimumSignificantDigits?: number; + maximumSignificantDigits?: number; + useGrouping: boolean; + } + + interface NumberFormat { + format(value: number): string; + resolvedOptions(): ResolvedNumberFormatOptions; + } + var NumberFormat: { + new (locales?: string | string[], options?: NumberFormatOptions): NumberFormat; + (locales?: string | string[], options?: NumberFormatOptions): NumberFormat; + supportedLocalesOf(locales: string | string[], options?: NumberFormatOptions): string[]; + readonly prototype: NumberFormat; + }; + + interface DateTimeFormatOptions { + localeMatcher?: "best fit" | "lookup" | undefined; + weekday?: "long" | "short" | "narrow" | undefined; + era?: "long" | "short" | "narrow" | undefined; + year?: "numeric" | "2-digit" | undefined; + month?: "numeric" | "2-digit" | "long" | "short" | "narrow" | undefined; + day?: "numeric" | "2-digit" | undefined; + hour?: "numeric" | "2-digit" | undefined; + minute?: "numeric" | "2-digit" | undefined; + second?: "numeric" | "2-digit" | undefined; + timeZoneName?: "short" | "long" | "shortOffset" | "longOffset" | "shortGeneric" | "longGeneric" | undefined; + formatMatcher?: "best fit" | "basic" | undefined; + hour12?: boolean | undefined; + timeZone?: string | undefined; + } + + interface ResolvedDateTimeFormatOptions { + locale: string; + calendar: string; + numberingSystem: string; + timeZone: string; + hour12?: boolean; + weekday?: string; + era?: string; + year?: string; + month?: string; + day?: string; + hour?: string; + minute?: string; + second?: string; + timeZoneName?: string; + } + + interface DateTimeFormat { + format(date?: Date | number): string; + resolvedOptions(): ResolvedDateTimeFormatOptions; + } + var DateTimeFormat: { + new (locales?: string | string[], options?: DateTimeFormatOptions): DateTimeFormat; + (locales?: string | string[], options?: DateTimeFormatOptions): DateTimeFormat; + supportedLocalesOf(locales: string | string[], options?: DateTimeFormatOptions): string[]; + readonly prototype: DateTimeFormat; + }; +} + +interface String { + /** + * Determines whether two strings are equivalent in the current or specified locale. + * @param that String to compare to target string + * @param locales A locale string or array of locale strings that contain one or more language or locale tags. If you include more than one locale string, list them in descending order of priority so that the first entry is the preferred locale. If you omit this parameter, the default locale of the JavaScript runtime is used. This parameter must conform to BCP 47 standards; see the Intl.Collator object for details. + * @param options An object that contains one or more properties that specify comparison options. see the Intl.Collator object for details. + */ + localeCompare(that: string, locales?: string | string[], options?: Intl.CollatorOptions): number; +} + +interface Number { + /** + * Converts a number to a string by using the current or specified locale. + * @param locales A locale string or array of locale strings that contain one or more language or locale tags. If you include more than one locale string, list them in descending order of priority so that the first entry is the preferred locale. If you omit this parameter, the default locale of the JavaScript runtime is used. + * @param options An object that contains one or more properties that specify comparison options. + */ + toLocaleString(locales?: string | string[], options?: Intl.NumberFormatOptions): string; +} + +interface Date { + /** + * Converts a date and time to a string by using the current or specified locale. + * @param locales A locale string or array of locale strings that contain one or more language or locale tags. If you include more than one locale string, list them in descending order of priority so that the first entry is the preferred locale. If you omit this parameter, the default locale of the JavaScript runtime is used. + * @param options An object that contains one or more properties that specify comparison options. + */ + toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; + /** + * Converts a date to a string by using the current or specified locale. + * @param locales A locale string or array of locale strings that contain one or more language or locale tags. If you include more than one locale string, list them in descending order of priority so that the first entry is the preferred locale. If you omit this parameter, the default locale of the JavaScript runtime is used. + * @param options An object that contains one or more properties that specify comparison options. + */ + toLocaleDateString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; + + /** + * Converts a time to a string by using the current or specified locale. + * @param locales A locale string or array of locale strings that contain one or more language or locale tags. If you include more than one locale string, list them in descending order of priority so that the first entry is the preferred locale. If you omit this parameter, the default locale of the JavaScript runtime is used. + * @param options An object that contains one or more properties that specify comparison options. + */ + toLocaleTimeString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; +} \ No newline at end of file diff --git a/src/instructions/hq.rs b/src/instructions/hq.rs index 6cec2c5e..0d5b680e 100644 --- a/src/instructions/hq.rs +++ b/src/instructions/hq.rs @@ -1,4 +1,5 @@ pub mod _yield; +pub mod cast; pub mod float; pub mod integer; pub mod text; diff --git a/src/instructions/hq/_yield.rs b/src/instructions/hq/_yield.rs index 376fcb2f..abf6ae0a 100644 --- a/src/instructions/hq/_yield.rs +++ b/src/instructions/hq/_yield.rs @@ -70,4 +70,4 @@ pub fn output_type(_inputs: Rc<[IrType]>, _fields: &Fields) -> HQResult, + &Fields(to): &Fields, +) -> HQResult>> { + let from = inputs[0]; + // float needs to be the last input type we check, as I don't think there's a direct way of checking + // if a value is *not* boxed + let base_types = [IrType::QuasiInt, IrType::String, IrType::Float]; + let possible_input_types = base_types + .into_iter() + .filter(|&ty| from.intersects(ty)) + .map(|ty| Ok((ty, cast_instructions(ty, to, func)?))) + .collect::>>()?; + Ok(match possible_input_types.len() { + 0 => hq_bug!("empty input type for hq_cast"), + 1 => possible_input_types[0].1.clone(), + _ => { + let result_type = WasmProject::ir_type_to_wasm(to)?; + let box_local = func.local(ValType::I64)?; + let possible_types_num = possible_input_types.len() - 1; + [LocalSet(box_local)] + .into_iter() + .chain( + possible_input_types + .into_iter() + .enumerate() + .map(|(i, (ty, instrs))| { + [ + if i == 0 { + match ty { + IrType::QuasiInt => vec![ + LocalGet(box_local), + I64Const(BOXED_INT_PATTERN), + I64And, + I64Const(BOXED_INT_PATTERN), + I64Eq, + If(BlockType::Result(result_type)), + ], + IrType::String => vec![ + LocalGet(box_local), + I64Const(BOXED_STRING_PATTERN), + I64And, + I64Const(BOXED_STRING_PATTERN), + I64Eq, + If(BlockType::Result(result_type)), + ], + // float guaranteed to be last so no need to check + _ => unreachable!(), + } + } else if i == possible_types_num { + vec![Else] + } else { + match ty { + IrType::Float => vec![Else], // float guaranteed to be last so no need to check + IrType::QuasiInt => vec![ + Else, + LocalGet(box_local), + I64Const(BOXED_INT_PATTERN), + I64And, + I64Const(BOXED_INT_PATTERN), + I64Eq, + If(BlockType::Result(result_type)), + ], + IrType::String => vec![ + Else, + LocalGet(box_local), + I64Const(BOXED_STRING_PATTERN), + I64And, + I64Const(BOXED_STRING_PATTERN), + I64Eq, + If(BlockType::Result(result_type)), + ], + _ => unreachable!(), + } + }, + vec![LocalGet(box_local)], + instrs.clone(), + ] + .into_iter() + .flatten() + }) + .flatten(), + ) + .chain(std::iter::repeat_n(Instruction::End, possible_types_num)) + .collect::>() + } + }) +} + +fn cast_instructions( + from: IrType, + to: IrType, + func: &StepFunc, +) -> HQResult>> { + // `to` and `from` are guaranteed to be a base type (i.e. float, int/bool or string) + Ok(if IrType::Number.contains(to) { + // if casting to a number, we always cast to a float. for now. + // I suppose this doesn't really make sense if we're casting to a bool, + // but we should never be casting anything to a bool because in general you can't + // put round blocks in predicate inputs. + // TODO: consider the exception () + if IrType::Float.contains(from) { + vec![] + } else if IrType::QuasiInt.contains(from) { + vec![F64ConvertI32S] + } else if IrType::String.contains(from) { + let func_index = func.registries().external_functions().register( + ("cast", "string2float"), + (vec![ValType::EXTERNREF], vec![ValType::F64]), + )?; + vec![Call(func_index)] + } else { + hq_todo!("bad cast: {:?} -> number", to) + } + } else if IrType::String.contains(to) { + if IrType::Float.contains(from) { + let func_index = func.registries().external_functions().register( + ("cast", "float2string"), + (vec![ValType::F64], vec![ValType::EXTERNREF]), + )?; + vec![Call(func_index)] + } else if IrType::QuasiInt.contains(from) { + let func_index = func.registries().external_functions().register( + ("cast", "int2string"), + (vec![ValType::I32], vec![ValType::EXTERNREF]), + )?; + vec![Call(func_index)] + } else if IrType::String.contains(from) { + vec![] + } else { + hq_todo!("bad cast: {:?} -> number", to) + } + } else { + hq_todo!("unimplemented cast: {:?} -> {:?}", to, from) + }) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::Number.or(IrType::String).or(IrType::Boolean)]) +} + +pub fn output_type(_inputs: Rc<[IrType]>, &Fields(to): &Fields) -> HQResult> { + Ok(Some(to)) +} + +crate::instructions_test! {float; t @ super::Fields(IrType::Float)} +crate::instructions_test! {string; t @ super::Fields(IrType::String)} diff --git a/src/instructions/operator/add.rs b/src/instructions/operator/add.rs index 0995d245..72716fb6 100644 --- a/src/instructions/operator/add.rs +++ b/src/instructions/operator/add.rs @@ -10,7 +10,7 @@ pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult HQResult<()> { - use wasm_encoder::{ - CodeSection, FunctionSection, ImportSection, Instruction, Module, TableSection, TypeSection, MemorySection, MemoryType - }; - use $crate::wasm::{StepFunc, Registries}; - use $crate::prelude::Rc; - for ($($type_arg,)*) in types_iter() { let output_type = match output_type(Rc::new([$($type_arg,)*]), $(&$fields)?) { Ok(a) => a, @@ -77,11 +73,11 @@ macro_rules! instructions_test { } }; let registries = Rc::new(Registries::default()); - let wasm_proj = $crate::wasm::WasmProject::new(Default::default(), $crate::wasm::ExternalEnvironment::WebBrowser); + let wasm_proj = WasmProject::new(Default::default(), ExternalEnvironment::WebBrowser); let types: &[IrType] = &[$($type_arg,)*]; - let params = [Ok(ValType::I32)].into_iter().chain([$($type_arg,)*].into_iter().map(|ty| wasm_proj.ir_type_to_wasm(ty))).collect::>>()?; + let params = [Ok(ValType::I32)].into_iter().chain([$($type_arg,)*].into_iter().map(|ty| WasmProject::ir_type_to_wasm(ty))).collect::>>()?; let result = match output_type { - Some(output) => Some(wasm_proj.ir_type_to_wasm(output)?), + Some(output) => Some(WasmProject::ir_type_to_wasm(output)?), None => None, }; let step_func = StepFunc::new_with_types(params.into(), result, Rc::clone(®istries), flags())?; @@ -144,8 +140,7 @@ macro_rules! instructions_test { #[test] fn js_functions_match_declared_types() { - use $crate::wasm::{Registries, StepFunc,}; - use ezno_lib::{check as check_js, Diagnostic}; + use ezno_checker::{check_project as check_js, Diagnostic, INTERNAL_DEFINITION_FILE_PATH as ts_defs}; use std::path::{Path, PathBuf}; use std::fs; @@ -157,11 +152,11 @@ macro_rules! instructions_test { continue; }; for ((module, name), (params, results)) in registries.external_functions().registry().borrow().iter() { - assert!(results.len() < 1, "external function {}::{} registered as returning multiple results", module, name); + assert!(results.len() <= 1, "external function {}::{} registered as returning multiple results", module, name); let out = if results.len() == 0 { "void" } else { - wasm_to_js_type(results[1]) + wasm_to_js_type(results[0]) }; let arg_idents: Vec = params.iter().enumerate().map(|(i, _)| format!("_{i}")).collect(); let ins = arg_idents.iter().enumerate().map(|(i, ident)| { @@ -171,8 +166,9 @@ macro_rules! instructions_test { wasm_to_js_type(*params.get(i).unwrap()) ) }).collect::>().join(", "); - let diagnostics = check_js( + let diagnostics = check_js::<_, ezno_checker::synthesis::EznoParser>( vec![PathBuf::from(format!("js/{}/{}.ts", module, name))], + vec![ts_defs.into()], &|path: &Path| { let func_string = fs::read_to_string(path).ok()?; let test_string = format!(" @@ -184,8 +180,9 @@ function _({ins}): {out} {{ println!("{}", test_string.clone()); Some(test_string.as_str().as_bytes().into_iter().map(|&u| u).collect::>()) }, - None, Default::default(), + (), + None, ) .diagnostics; if diagnostics.contains_error() { diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index ef2e13cf..dfb553bc 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -1,7 +1,6 @@ -use crate::instructions::{fields, IrOpcode}; +use crate::instructions::{fields::*, IrOpcode}; use crate::prelude::*; use crate::sb3::{Block, BlockArray, BlockArrayOrId, BlockInfo, BlockMap, BlockOpcode, Input}; -use fields::*; // TODO: insert casts in relevant places @@ -102,9 +101,7 @@ fn from_special_block(block_array: &BlockArray) -> HQResult { value.parse().map_err(|_| make_hq_bug!(""))?, )), 9 => hq_todo!(""), - 10 => hq_todo!(), /*IrOpcode::text { - TEXT: value.to_string(), - },*/ + 10 => IrOpcode::hq_text(HqTextFields(value.clone())), _ => hq_bad_proj!("bad project json (block array of type ({}, string))", ty), }, BlockArray::Broadcast(ty, _name, _id) diff --git a/src/wasm/func.rs b/src/wasm/func.rs index c6fc2126..e94dee31 100644 --- a/src/wasm/func.rs +++ b/src/wasm/func.rs @@ -52,41 +52,11 @@ impl StepFunc { }) } - /// Returns the index of the `n`th local of the specified type in this function, - /// adding some if necessary. `n` should be greater than 0. - pub fn get_local(&self, val_type: ValType, n: u32) -> HQResult { - if n == 0 { - hq_bug!("can't have a 0 amount of locals; n should be >0 for get_local") - } - let existing_count = self - .locals - .borrow() - .iter() - .filter(|ty| **ty == val_type) - .count(); - u32::try_from( - if existing_count < (n as usize) { - { - self.locals - .borrow_mut() - .extend([val_type].repeat(n as usize - existing_count)); - } - self.locals.borrow().len() - 1 - } else { - self.locals - .borrow() - .iter() - .enumerate() - .filter(|(_, ty)| **ty == val_type) - .map(|(i, _)| i) - .nth(n as usize - 1) - .ok_or(make_hq_bug!( - "couldn't find nth local of type {:?}", - val_type - ))? - } + self.params.len(), - ) - .map_err(|_| make_hq_bug!("local index was out of bounds")) + /// Registers a new local in this function, and returns its index + pub fn local(&self, val_type: ValType) -> HQResult { + self.locals.borrow_mut().push(val_type); + u32::try_from(self.locals.borrow().len() + self.params.len() - 1) + .map_err(|_| make_hq_bug!("local index was out of bounds")) } pub fn add_instructions(&self, instructions: impl IntoIterator>) { @@ -113,55 +83,3 @@ impl StepFunc { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::StepFunc; - use wasm_encoder::ValType; - - #[test] - fn get_local_works_with_valid_inputs_with_1_param() { - let func = StepFunc::new(Default::default(), Default::default()); - assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 1); - - assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 1); - - assert_eq!(func.get_local(ValType::F64, 1).unwrap(), 2); - assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 1); - - assert_eq!(func.get_local(ValType::I32, 2).unwrap(), 3); - assert_eq!(func.get_local(ValType::F64, 1).unwrap(), 2); - assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 1); - - assert_eq!(func.get_local(ValType::F64, 4).unwrap(), 6); - } - - #[test] - fn get_local_fails_when_n_is_0() { - let func = StepFunc::new(Default::default(), Default::default()); - assert!(func.get_local(ValType::I32, 0).is_err()); - } - - #[test] - fn get_local_works_with_valid_inputs_with_3_params() { - let func = StepFunc::new_with_types( - [ValType::I32, ValType::F64, ValType::EXTERNREF].into(), // these are just arbitrary types - None, - Default::default(), - Default::default(), - ) - .unwrap(); - assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 3); - - assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 3); - - assert_eq!(func.get_local(ValType::F64, 1).unwrap(), 4); - assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 3); - - assert_eq!(func.get_local(ValType::I32, 2).unwrap(), 5); - assert_eq!(func.get_local(ValType::F64, 1).unwrap(), 4); - assert_eq!(func.get_local(ValType::I32, 1).unwrap(), 3); - - assert_eq!(func.get_local(ValType::F64, 4).unwrap(), 8); - } -} diff --git a/src/wasm/project.rs b/src/wasm/project.rs index baf7142f..26152c6c 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -52,7 +52,7 @@ impl WasmProject { } /// maps a broad IR type to a WASM type - pub fn ir_type_to_wasm(&self, ir_type: IrType) -> HQResult { + pub fn ir_type_to_wasm(ir_type: IrType) -> HQResult { Ok(if IrType::Float.contains(ir_type) { ValType::F64 } else if IrType::QuasiInt.contains(ir_type) { From 3b22e821036e46d8e469e9cda5c43d434a6cd932 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sat, 25 Jan 2025 14:44:42 +0000 Subject: [PATCH 16/98] (hopefully) fix casts --- src/instructions/hq/cast.rs | 40 ++++++++++++++++++++++++++----------- src/instructions/tests.rs | 2 +- src/wasm/project.rs | 8 ++++++++ 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/instructions/hq/cast.rs b/src/instructions/hq/cast.rs index 946d21b5..a56b787a 100644 --- a/src/instructions/hq/cast.rs +++ b/src/instructions/hq/cast.rs @@ -1,10 +1,8 @@ -use core::result; - use crate::ir::{Step, Type as IrType}; use crate::prelude::*; use crate::wasm::{byte_offset, StepFunc, WasmProject}; use wasm_encoder::Instruction::{self, *}; -use wasm_encoder::{BlockType, MemArg, ValType}; +use wasm_encoder::{BlockType, MemArg, RefType, ValType}; #[derive(Clone, Debug)] pub struct Fields(pub IrType); @@ -23,26 +21,26 @@ pub fn wasm( // float needs to be the last input type we check, as I don't think there's a direct way of checking // if a value is *not* boxed let base_types = [IrType::QuasiInt, IrType::String, IrType::Float]; - let possible_input_types = base_types + let casts = base_types .into_iter() .filter(|&ty| from.intersects(ty)) .map(|ty| Ok((ty, cast_instructions(ty, to, func)?))) .collect::>>()?; - Ok(match possible_input_types.len() { + Ok(match casts.len() { 0 => hq_bug!("empty input type for hq_cast"), - 1 => possible_input_types[0].1.clone(), + 1 => casts[0].1.clone(), _ => { let result_type = WasmProject::ir_type_to_wasm(to)?; let box_local = func.local(ValType::I64)?; - let possible_types_num = possible_input_types.len() - 1; + let possible_types_num = casts.len(); [LocalSet(box_local)] .into_iter() .chain( - possible_input_types + casts .into_iter() .enumerate() .map(|(i, (ty, instrs))| { - [ + Ok([ if i == 0 { match ty { IrType::QuasiInt => vec![ @@ -64,7 +62,7 @@ pub fn wasm( // float guaranteed to be last so no need to check _ => unreachable!(), } - } else if i == possible_types_num { + } else if i == possible_types_num - 1 { vec![Else] } else { match ty { @@ -91,14 +89,32 @@ pub fn wasm( } }, vec![LocalGet(box_local)], + if IrType::QuasiInt.contains(ty) { + vec![I32WrapI64] + } else if IrType::Float.contains(ty) { + vec![F64ReinterpretI64] + } else if IrType::String.contains(ty) { + let table_index = func + .registries() + .tables() + .register("strings".into(), (RefType::EXTERNREF, 0))?; + vec![I32WrapI64, TableGet(table_index)] + } else { + vec![] + }, instrs.clone(), ] .into_iter() - .flatten() + .flatten()) }) + .collect::>>()? + .into_iter() .flatten(), ) - .chain(std::iter::repeat_n(Instruction::End, possible_types_num)) + .chain(std::iter::repeat_n( + Instruction::End, + possible_types_num - 1, // the last else doesn't need an additional `end` instruction + )) .collect::>() } }) diff --git a/src/instructions/tests.rs b/src/instructions/tests.rs index b22f5fbf..0080eb5c 100644 --- a/src/instructions/tests.rs +++ b/src/instructions/tests.rs @@ -73,7 +73,6 @@ macro_rules! instructions_test { } }; let registries = Rc::new(Registries::default()); - let wasm_proj = WasmProject::new(Default::default(), ExternalEnvironment::WebBrowser); let types: &[IrType] = &[$($type_arg,)*]; let params = [Ok(ValType::I32)].into_iter().chain([$($type_arg,)*].into_iter().map(|ty| WasmProject::ir_type_to_wasm(ty))).collect::>>()?; let result = match output_type { @@ -88,6 +87,7 @@ macro_rules! instructions_test { continue; } }; + println!("{wasm:?}"); for (i, _) in types.iter().enumerate() { step_func.add_instructions([Instruction::LocalGet((i + 1).try_into().unwrap())]) } diff --git a/src/wasm/project.rs b/src/wasm/project.rs index 26152c6c..ce028a2a 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -107,6 +107,14 @@ impl WasmProject { step_func_indices, ); + self.registries().tables().register::( + "strings".into(), + ( + RefType::EXTERNREF, + 0, // filled in by js (for now...); TODO: use js string imports + ), + )?; + self.registries() .external_functions() .clone() From 7c2e1c6de310c508f4f46329ee430af62e6532b2 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:38:40 +0000 Subject: [PATCH 17/98] update casting system to work properly --- Cargo.toml | 2 +- build.rs | 8 +- build.sh | 12 +- js/imports.ts | 8 +- js/operator/join.ts | 3 + playground/lib/project-runner.js | 1 + src/error.rs | 78 +++++++++ src/instructions.rs | 3 + src/instructions/hq/cast.rs | 230 +++++++++---------------- src/instructions/input_switcher.rs | 258 +++++++++++++++++++++++++++++ src/instructions/looks/say.rs | 12 +- src/instructions/operator.rs | 1 + src/instructions/operator.yaml | 49 ------ src/instructions/operator/add.rs | 4 +- src/instructions/operator/join.rs | 26 +++ src/instructions/tests.rs | 19 ++- src/ir/blocks.rs | 36 +++- src/ir/proc.rs | 17 +- src/ir/project.rs | 7 +- src/ir/step.rs | 27 +-- src/ir/target.rs | 4 +- src/ir/types.rs | 21 +++ src/ir_opt.rs | 8 +- src/lib.rs | 1 + src/old-ir.rs | 122 +++++++------- src/old-wasm.rs | 16 +- src/registry.rs | 6 +- src/sb3.rs | 10 +- src/wasm/func.rs | 12 +- src/wasm/project.rs | 54 +++--- src/wasm/registries.rs | 1 + src/wasm/tables.rs | 8 +- 32 files changed, 700 insertions(+), 364 deletions(-) create mode 100644 js/operator/join.ts create mode 100644 src/instructions/input_switcher.rs delete mode 100644 src/instructions/operator.yaml create mode 100644 src/instructions/operator/join.rs diff --git a/Cargo.toml b/Cargo.toml index b79c4483..872b1f95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ uuid = { version = "1.4.1", default-features = false, features = ["v4", "js"] } regex = "1.10.5" lazy-regex = "3.2.0" bitmask-enum = "2.2.5" -itertools = { version = "0.13.0", default-features = false } +itertools = { version = "0.13.0", default-features = false, features = ["use_alloc"] } split_exact = "1.1.0" wasm-bindgen = "0.2.92" serde-wasm-bindgen = "0.6.5" diff --git a/build.rs b/build.rs index fd862771..a8bec93a 100644 --- a/build.rs +++ b/build.rs @@ -88,7 +88,7 @@ export const imports = {{ .iter() .map(|dir| { format!( - "{dir}: {{ {} }},", + "{dir}: {{ {} }}", ts_paths .iter() .filter(|(d, _)| d == dir) @@ -116,21 +116,21 @@ pub enum IrOpcode {{ impl IrOpcode {{ /// maps an opcode to its acceptable input types - pub fn acceptable_inputs(&self) -> Rc<[crate::ir::Type]> {{ + pub fn acceptable_inputs(&self) -> Rc<[crate::ir::Type]> {{ match self {{ {} }} }} /// maps an opcode to its WASM instructions - pub fn wasm(&self, step_func: &crate::wasm::StepFunc, inputs: Rc<[crate::ir::Type]>) -> HQResult>> {{ + pub fn wasm(&self, step_func: &crate::wasm::StepFunc, inputs: Rc<[crate::ir::Type]>) -> HQResult>> {{ match self {{ {} }} }} /// maps an opcode to its output type - pub fn output_type(&self, inputs: Rc<[crate::ir::Type]>) -> HQResult> {{ + pub fn output_type(&self, inputs: Rc<[crate::ir::Type]>) -> HQResult> {{ match self {{ {} }} diff --git a/build.sh b/build.sh index 6a03db53..5153c1e9 100755 --- a/build.sh +++ b/build.sh @@ -20,9 +20,9 @@ usage() echo " -V build the website with vite" echo " -W build wasm" echo " -o do not run wasm-opt" - echo " -O run wasm-opt" echo " -s run wasm-opt with -Os" echo " -z run wasm-opt with -Oz" + echo " -v verbose output" exit 1 } @@ -41,7 +41,7 @@ set_variable() unset VITE WASM PROD; QUIET=1; -while getopts 'dpwvoWVszhi' c +while getopts 'dpVWoszvh' c do case $c in d) set_variable PROD 0 ;; @@ -51,7 +51,7 @@ do o) set_variable WOPT 0 ;; s) set_variable WOPT 1 ;; z) set_variable WOPT 2 ;; - i) unset QUIET ;; + v) unset QUIET ;; h|?) usage ;; esac done @@ -94,11 +94,13 @@ if [ $WASM = "1" ]; then fi if [ $WOPT = "1" ]; then echo running wasm-opt -Os... - wasm-opt -Os -g js/hyperquark_bg.wasm -o js/hyperquark_bg.wasm + wasm-opt -Os -g js/compiler/hyperquark_bg.wasm -o js/compiler/hyperquark_bg.wasm + wasm-opt -Os -g js/no-compiler/hyperquark_bg.wasm -o js/no-compiler/hyperquark_bg.wasm fi if [ $WOPT = "2" ]; then echo running wasm-opt -Oz... - wasm-opt -Oz -g js/hyperquark_bg.wasm -o js/hyperquark_bg.wasm + wasm-opt -Oz -g js/compiler/hyperquark_bg.wasm -o js/compiler/hyperquark_bg.wasm + wasm-opt -Oz -g js/no-compiler/hyperquark_bg.wasm -o js/no-compiler/hyperquark_bg.wasm fi if [ $VITE = "1" ]; then echo running npm build... diff --git a/js/imports.ts b/js/imports.ts index 218bdc3a..71513e9d 100644 --- a/js/imports.ts +++ b/js/imports.ts @@ -1,8 +1,14 @@ +import { join } from './operator/join.ts'; +import { string2float } from './cast/string2float.ts'; +import { int2string } from './cast/int2string.ts'; +import { float2string } from './cast/float2string.ts'; import { say_int } from './looks/say_int.ts'; import { say_string } from './looks/say_string.ts'; import { say_float } from './looks/say_float.ts'; export const imports = { - looks: { say_int, say_string, say_float }, + operator: { join }, + cast: { string2float, int2string, float2string }, + looks: { say_int, say_string, say_float } }; \ No newline at end of file diff --git a/js/operator/join.ts b/js/operator/join.ts new file mode 100644 index 00000000..9aa31267 --- /dev/null +++ b/js/operator/join.ts @@ -0,0 +1,3 @@ +export function join(left: string, right: string): string { + return left.concat(right); +} \ No newline at end of file diff --git a/playground/lib/project-runner.js b/playground/lib/project-runner.js index 93ffdeb2..9ae7ef23 100644 --- a/playground/lib/project-runner.js +++ b/playground/lib/project-runner.js @@ -115,6 +115,7 @@ export default async ( upc, unreachable_dbg } = instance.exports; + strings.grow(Object.entries(string_consts).length); for (const [i, str] of Object.entries(string_consts || {})) { // @ts-ignore strings.set(i, str); diff --git a/src/error.rs b/src/error.rs index f8309233..3874f216 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +use core::cell::{BorrowError, BorrowMutError}; + use alloc::boxed::Box; use wasm_bindgen::JsValue; @@ -28,6 +30,30 @@ impl From for JsValue { } } +impl From for HQError { + fn from(e: BorrowError) -> Self { + HQError { + err_type: HQErrorType::InternalError, + msg: "couldn't borrow cell".into(), + file: file!().into(), + line: line!(), + column: column!(), + } + } +} + +impl From for HQError { + fn from(e: BorrowMutError) -> Self { + HQError { + err_type: HQErrorType::InternalError, + msg: "couldn't mutably borrow cell".into(), + file: file!().into(), + line: line!(), + column: column!(), + } + } +} + #[macro_export] macro_rules! hq_todo { () => {{ @@ -63,6 +89,58 @@ macro_rules! hq_bug { }}; } +#[macro_export] +macro_rules! hq_assert { + ($expr:expr) => {{ + if !($expr) { + return Err($crate::HQError { + err_type: $crate::HQErrorType::InternalError, + msg: format!("Assertion failed: {}", stringify!($expr)).into(), + file: file!().into(), + line: line!(), + column: column!() + }); + } + }}; + ($expr:expr, $($args:tt)+) => {{ + if !($expr) { + return Err($crate::HQError { + err_type: $crate::HQErrorType::InternalError, + msg: format!("Assertion failed: {}\nMessage: {}", stringify!($expr), format_args!($($args)*)).into(), + file: file!().into(), + line: line!(), + column: column!() + }); + } + }}; +} + +#[macro_export] +macro_rules! hq_assert_eq { + ($l:expr, $r:expr) => {{ + if $l != $r { + return Err($crate::HQError { + err_type: $crate::HQErrorType::InternalError, + msg: format!("Assertion failed: {} == {}\nLeft: {}\nRight: {}", stringify!($l), stringify!($r), $l, $r).into(), + file: file!().into(), + line: line!(), + column: column!() + }); + } + }}; + ($l:expr, $r:expr, $($args:tt)+) => {{ + if $l != $r { + return Err($crate::HQError { + err_type: $crate::HQErrorType::InternalError, + msg: format!("Assertion failed: {}\nLeft: {}\nRight: {}\nMessage: {}", stringify!($l), stringify!($r), $l, $r, format_args!($($args)*)).into(), + file: file!().into(), + line: line!(), + column: column!() + }); + } + }}; +} + #[macro_export] macro_rules! hq_bad_proj { ($($args:tt)+) => {{ diff --git a/src/instructions.rs b/src/instructions.rs index 8a30989b..8af83e4a 100644 --- a/src/instructions.rs +++ b/src/instructions.rs @@ -10,3 +10,6 @@ mod operator; mod tests; include!(concat!(env!("OUT_DIR"), "/ir-opcodes.rs")); + +mod input_switcher; +pub use input_switcher::wrap_instruction; diff --git a/src/instructions/hq/cast.rs b/src/instructions/hq/cast.rs index a56b787a..f9aed079 100644 --- a/src/instructions/hq/cast.rs +++ b/src/instructions/hq/cast.rs @@ -1,16 +1,36 @@ -use crate::ir::{Step, Type as IrType}; +use crate::ir::Type as IrType; use crate::prelude::*; -use crate::wasm::{byte_offset, StepFunc, WasmProject}; +use crate::wasm::StepFunc; use wasm_encoder::Instruction::{self, *}; -use wasm_encoder::{BlockType, MemArg, RefType, ValType}; +use wasm_encoder::ValType; #[derive(Clone, Debug)] pub struct Fields(pub IrType); -/// Canonical NaN + bit 33, + string pointer in bits 1-32 -const BOXED_STRING_PATTERN: i64 = 0x7FF80001 << 32; -/// Canonical NaN + bit 33, + i32 in bits 1-32 -const BOXED_INT_PATTERN: i64 = 0x7ff80002 << 32; +fn best_cast_candidate(from: IrType, to: IrType) -> HQResult { + let to_base_types = to.base_types().collect::>(); + hq_assert!(!to_base_types.is_empty()); + let Some(from_base) = from.base_type() else { + hq_bug!("from type has no base type") + }; + Ok(if to_base_types.contains(&&from_base) { + from_base + } else { + let mut candidates = vec![]; + for preference in match from_base { + IrType::QuasiInt => &[IrType::Float, IrType::String] as &[IrType], + IrType::Float => &[IrType::String] as &[IrType], + IrType::String => &[IrType::Float] as &[IrType], + _ => unreachable!(), + } { + if to_base_types.contains(&preference) { + candidates.push(preference); + } + } + hq_assert!(!candidates.is_empty()); + *candidates[0] + }) +} pub fn wasm( func: &StepFunc, @@ -18,153 +38,45 @@ pub fn wasm( &Fields(to): &Fields, ) -> HQResult>> { let from = inputs[0]; - // float needs to be the last input type we check, as I don't think there's a direct way of checking - // if a value is *not* boxed - let base_types = [IrType::QuasiInt, IrType::String, IrType::Float]; - let casts = base_types - .into_iter() - .filter(|&ty| from.intersects(ty)) - .map(|ty| Ok((ty, cast_instructions(ty, to, func)?))) - .collect::>>()?; - Ok(match casts.len() { - 0 => hq_bug!("empty input type for hq_cast"), - 1 => casts[0].1.clone(), - _ => { - let result_type = WasmProject::ir_type_to_wasm(to)?; - let box_local = func.local(ValType::I64)?; - let possible_types_num = casts.len(); - [LocalSet(box_local)] - .into_iter() - .chain( - casts - .into_iter() - .enumerate() - .map(|(i, (ty, instrs))| { - Ok([ - if i == 0 { - match ty { - IrType::QuasiInt => vec![ - LocalGet(box_local), - I64Const(BOXED_INT_PATTERN), - I64And, - I64Const(BOXED_INT_PATTERN), - I64Eq, - If(BlockType::Result(result_type)), - ], - IrType::String => vec![ - LocalGet(box_local), - I64Const(BOXED_STRING_PATTERN), - I64And, - I64Const(BOXED_STRING_PATTERN), - I64Eq, - If(BlockType::Result(result_type)), - ], - // float guaranteed to be last so no need to check - _ => unreachable!(), - } - } else if i == possible_types_num - 1 { - vec![Else] - } else { - match ty { - IrType::Float => vec![Else], // float guaranteed to be last so no need to check - IrType::QuasiInt => vec![ - Else, - LocalGet(box_local), - I64Const(BOXED_INT_PATTERN), - I64And, - I64Const(BOXED_INT_PATTERN), - I64Eq, - If(BlockType::Result(result_type)), - ], - IrType::String => vec![ - Else, - LocalGet(box_local), - I64Const(BOXED_STRING_PATTERN), - I64And, - I64Const(BOXED_STRING_PATTERN), - I64Eq, - If(BlockType::Result(result_type)), - ], - _ => unreachable!(), - } - }, - vec![LocalGet(box_local)], - if IrType::QuasiInt.contains(ty) { - vec![I32WrapI64] - } else if IrType::Float.contains(ty) { - vec![F64ReinterpretI64] - } else if IrType::String.contains(ty) { - let table_index = func - .registries() - .tables() - .register("strings".into(), (RefType::EXTERNREF, 0))?; - vec![I32WrapI64, TableGet(table_index)] - } else { - vec![] - }, - instrs.clone(), - ] - .into_iter() - .flatten()) - }) - .collect::>>()? - .into_iter() - .flatten(), - ) - .chain(std::iter::repeat_n( - Instruction::End, - possible_types_num - 1, // the last else doesn't need an additional `end` instruction - )) - .collect::>() - } - }) -} -fn cast_instructions( - from: IrType, - to: IrType, - func: &StepFunc, -) -> HQResult>> { - // `to` and `from` are guaranteed to be a base type (i.e. float, int/bool or string) - Ok(if IrType::Number.contains(to) { - // if casting to a number, we always cast to a float. for now. - // I suppose this doesn't really make sense if we're casting to a bool, - // but we should never be casting anything to a bool because in general you can't - // put round blocks in predicate inputs. - // TODO: consider the exception () - if IrType::Float.contains(from) { - vec![] - } else if IrType::QuasiInt.contains(from) { - vec![F64ConvertI32S] - } else if IrType::String.contains(from) { - let func_index = func.registries().external_functions().register( - ("cast", "string2float"), - (vec![ValType::EXTERNREF], vec![ValType::F64]), - )?; - vec![Call(func_index)] - } else { - hq_todo!("bad cast: {:?} -> number", to) - } - } else if IrType::String.contains(to) { - if IrType::Float.contains(from) { - let func_index = func.registries().external_functions().register( - ("cast", "float2string"), - (vec![ValType::F64], vec![ValType::EXTERNREF]), - )?; - vec![Call(func_index)] - } else if IrType::QuasiInt.contains(from) { - let func_index = func.registries().external_functions().register( - ("cast", "int2string"), - (vec![ValType::I32], vec![ValType::EXTERNREF]), - )?; - vec![Call(func_index)] - } else if IrType::String.contains(from) { - vec![] - } else { - hq_todo!("bad cast: {:?} -> number", to) - } - } else { - hq_todo!("unimplemented cast: {:?} -> {:?}", to, from) + let target = best_cast_candidate(from, to)?; + + let Some(from_base) = from.base_type() else { + hq_bug!("from type has no base type") + }; + + Ok(match target { + IrType::Float => match from_base { + IrType::Float => vec![], + IrType::QuasiInt => vec![F64ConvertI32S], + IrType::String => { + let func_index = func.registries().external_functions().register( + ("cast", "string2float"), + (vec![ValType::EXTERNREF], vec![ValType::F64]), + )?; + vec![Call(func_index)] + } + _ => hq_todo!("bad cast: {:?} -> float", from_base), + }, + IrType::String => match from_base { + IrType::Float => { + let func_index = func.registries().external_functions().register( + ("cast", "float2string"), + (vec![ValType::F64], vec![ValType::EXTERNREF]), + )?; + vec![Call(func_index)] + } + IrType::QuasiInt => { + let func_index = func.registries().external_functions().register( + ("cast", "int2string"), + (vec![ValType::I32], vec![ValType::EXTERNREF]), + )?; + vec![Call(func_index)] + } + IrType::String => vec![], + _ => hq_todo!("bad cast: {:?} -> string", from_base), + }, + _ => hq_todo!("unimplemented cast: {:?} -> {:?}", from_base, target), }) } @@ -172,8 +84,16 @@ pub fn acceptable_inputs() -> Rc<[IrType]> { Rc::new([IrType::Number.or(IrType::String).or(IrType::Boolean)]) } -pub fn output_type(_inputs: Rc<[IrType]>, &Fields(to): &Fields) -> HQResult> { - Ok(Some(to)) +pub fn output_type(inputs: Rc<[IrType]>, &Fields(to): &Fields) -> HQResult> { + Ok(Some( + inputs[0] + .base_types() + .map(|&from| best_cast_candidate(from, to)) + .collect::>>()? + .into_iter() + .reduce(|acc, el| acc.or(el)) + .ok_or(make_hq_bug!("input was empty"))?, + )) } crate::instructions_test! {float; t @ super::Fields(IrType::Float)} diff --git a/src/instructions/input_switcher.rs b/src/instructions/input_switcher.rs new file mode 100644 index 00000000..4adca183 --- /dev/null +++ b/src/instructions/input_switcher.rs @@ -0,0 +1,258 @@ +//! Provides the logic for having boxed input types to blocks + +use super::IrOpcode; +use crate::prelude::*; +use crate::wasm::WasmProject; +use crate::{ir::Type as IrType, wasm::StepFunc}; +use itertools::Itertools; +use wasm_encoder::Instruction::{self, *}; +use wasm_encoder::{BlockType, RefType, ValType}; + +/// Canonical NaN + bit 33, + string pointer in bits 1-32 +const BOXED_STRING_PATTERN: i64 = 0x7FF80001 << 32; +/// Canonical NaN + bit 33, + i32 in bits 1-32 +const BOXED_INT_PATTERN: i64 = 0x7ff80002 << 32; + +/// generates branches (or not, if an input is not boxed) for a list of remaining input types. +/// This sort of recursion makes me feel distinctly uneasy; I'm just waiting for a stack +/// overflow. +/// TODO: tail-recursify/loopify? +fn generate_branches( + func: &StepFunc, + processed_inputs: &[IrType], + remaining_inputs: &[(Box<[IrType]>, u32)], // u32 is local index + opcode: &IrOpcode, + output_type: Option, +) -> HQResult>> { + if remaining_inputs.is_empty() { + hq_assert!(processed_inputs.iter().all(|ty| ty.is_base_type())); + let mut wasm = opcode.wasm(func, Rc::from(processed_inputs))?; + // if the overall output is boxed, but this particular branch produces an unboxed result + // (which i think all branches probably should?), box it. + // TODO: split this into another function somewhere? it seems like this should + // be useful somewhere else as well + if let Some(this_output) = opcode.output_type(Rc::from(processed_inputs))? { + if this_output.is_base_type() + && !output_type + .ok_or(make_hq_bug!("expected no output type but got one"))? + .is_base_type() + { + wasm.append(&mut match this_output.base_type().unwrap() { + IrType::Float => vec![I64ReinterpretF64], + IrType::QuasiInt => vec![I64ExtendI32S, I64Const(BOXED_INT_PATTERN), I64And], + IrType::String => { + let table_index = func + .registries() + .tables() + .register("strings".into(), (RefType::EXTERNREF, 0))?; + let externref_local = func.local(ValType::EXTERNREF)?; + vec![ + LocalSet(externref_local), + TableSize(table_index), + I32Const(1), + TableGrow(table_index), + LocalGet(externref_local), + TableSet(table_index), + I64ExtendI32S, + I64Const(BOXED_STRING_PATTERN), + I64And, + ] + } + _ => unreachable!(), + }); + } + } + return Ok(wasm); + } + let (curr_input, local_idx) = &remaining_inputs[0]; + let local_idx = *local_idx; // variable shadowing feels evil but hey it works + let mut wasm = vec![LocalGet(local_idx)]; + Ok(if curr_input.len() == 1 { + let mut processed_inputs = processed_inputs.to_vec(); + processed_inputs.push(curr_input[0]); + wasm.append(&mut generate_branches( + func, + &processed_inputs, + &remaining_inputs[1..], + opcode, + output_type, + )?); + wasm + } else { + let if_block_type = if let Some(out_ty) = output_type { + BlockType::Result(WasmProject::ir_type_to_wasm(out_ty)?) + } else { + BlockType::Empty + }; + let possible_types_num = curr_input.len(); + for (i, ty) in curr_input.iter().enumerate() { + wasm.append(&mut if i == 0 { + match *ty { + IrType::QuasiInt => vec![ + LocalGet(local_idx), + I64Const(BOXED_INT_PATTERN), + I64And, + I64Const(BOXED_INT_PATTERN), + I64Eq, + If(if_block_type), + LocalGet(local_idx), + I32WrapI64, + ], + IrType::String => { + let table_index = func + .registries() + .tables() + .register("strings".into(), (RefType::EXTERNREF, 0))?; + vec![ + LocalGet(local_idx), + I64Const(BOXED_STRING_PATTERN), + I64And, + I64Const(BOXED_STRING_PATTERN), + I64Eq, + If(if_block_type), + LocalGet(local_idx), + I32WrapI64, + TableGet(table_index), + ] + } + // float guaranteed to be last so no need to check + _ => unreachable!(), + } + } else if i == possible_types_num - 1 { + match *ty { + IrType::Float => vec![Else, LocalGet(local_idx), F64ReinterpretI64], // float guaranteed to be last so no need to check + IrType::QuasiInt => vec![Else, LocalGet(local_idx), I32WrapI64], + IrType::String => { + let table_index = func + .registries() + .tables() + .register("strings".into(), (RefType::EXTERNREF, 0))?; + vec![Else, LocalGet(local_idx), I32WrapI64, TableGet(table_index)] + } + _ => unreachable!(), + } + } else { + match *ty { + // float guaranteed to be last so no need to check + IrType::Float => vec![Else, LocalGet(local_idx), F64ReinterpretI64], + IrType::QuasiInt => vec![ + Else, + LocalGet(local_idx), + I64Const(BOXED_INT_PATTERN), + I64And, + I64Const(BOXED_INT_PATTERN), + I64Eq, + If(if_block_type), + LocalGet(local_idx), + I32WrapI64, + ], + IrType::String => { + let table_index = func + .registries() + .tables() + .register("strings".into(), (RefType::EXTERNREF, 0))?; + vec![ + Else, + LocalGet(local_idx), + I64Const(BOXED_STRING_PATTERN), + I64And, + I64Const(BOXED_STRING_PATTERN), + I64Eq, + If(if_block_type), + LocalGet(local_idx), + I32WrapI64, + TableGet(table_index), + ] + } + _ => unreachable!(), + } + }); + let mut processed_inputs = processed_inputs.to_vec(); + processed_inputs.push(curr_input[0]); + wasm.append(&mut generate_branches( + func, + &processed_inputs, + &remaining_inputs[1..], + opcode, + output_type, + )?) + } + wasm.extend(core::iter::repeat_n( + Instruction::End, + possible_types_num - 1, // the last else doesn't need an additional `end` instruction + )); + wasm + }) +} + +pub fn wrap_instruction( + func: &StepFunc, + inputs: Rc<[IrType]>, + opcode: IrOpcode, +) -> HQResult>> { + crate::log( + format!( + "wrap_instruction. inputs: {:?}, opcode: {:?}", + inputs, opcode + ) + .as_str(), + ); + + let output = opcode.output_type(Rc::clone(&inputs))?; + + hq_assert!(inputs.len() == opcode.acceptable_inputs().len()); + + // iterator of possible base types for each input + let mut base_types = + // check for float last of all, because I don't think there's an easy way of checking + // if something is *not* a canonical NaN with extra bits + core::iter::repeat([IrType::QuasiInt, IrType::String, IrType::Float].into_iter()) + .take(inputs.len()) + .enumerate() + .map(|(i, tys)| { + tys.filter(|ty| inputs[i].intersects(*ty)) + .collect::>() + }).collect::>(); + + // sanity check; we have at least one possible input type for each input + hq_assert!( + !base_types.iter().any(|tys| tys.len() == 0), + "empty input type for block {:?}", + opcode + ); + + let locals = base_types + .iter() + .map(|tys| { + if tys.len() == 1 { + func.local(WasmProject::ir_type_to_wasm(tys[0])?) + } else { + func.local(ValType::I64) + } + }) + .collect::>>()?; + + // for now, chuck each input into a local + // TODO: change this so that only the inputs following the first boxed input are local-ised + let mut wasm = locals + .iter() + .rev() + .cloned() + .map(LocalSet) + .collect::>(); + + wasm.append(&mut generate_branches( + func, + &[], + base_types + .into_iter() + .zip_eq(locals.iter().cloned()) + .collect::>() + .as_slice(), + &opcode, + output, + )?); + Ok(wasm) +} + +// TODO: test that wasm compiles correctly. How? Not sure yet. diff --git a/src/instructions/looks/say.rs b/src/instructions/looks/say.rs index 9b67bca4..0a1f59ce 100644 --- a/src/instructions/looks/say.rs +++ b/src/instructions/looks/say.rs @@ -16,18 +16,24 @@ pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult Rc<[IrType]> { - Rc::new([IrType::Any]) + Rc::new([IrType::String.or(IrType::Number)]) } pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { - if !(IrType::QuasiInt.contains(inputs[0]) || IrType::Float.contains(inputs[0])) { - hq_todo!() + if !(IrType::Number.or(IrType::String).contains(inputs[0])) { + hq_todo!("unimplemented input type: {:?}", inputs) } Ok(None) } diff --git a/src/instructions/operator.rs b/src/instructions/operator.rs index cced7b48..7dce9ee6 100644 --- a/src/instructions/operator.rs +++ b/src/instructions/operator.rs @@ -1 +1,2 @@ pub mod add; +pub mod join; diff --git a/src/instructions/operator.yaml b/src/instructions/operator.yaml deleted file mode 100644 index 10ec9a94..00000000 --- a/src/instructions/operator.yaml +++ /dev/null @@ -1,49 +0,0 @@ ---- -- - opcode: operator_add - inputs: - - Number - - Number - output_depends: [QuasiInteger,] - output: > - if QuasiInteger.contains(I1) && QuasiInteger.contains(I2) { - if (I1|I2) & (BooleanTrue | IntPos) != 0 { IntPos } else { none() } - | if (I1|I2) & (BooleanTrue | IntNeg) != 0 { IntNeg } else { none() } - | if (I1 & (BooleanTrue | IntPos) != 0) && I2 & IntNeg != 0) || (I2 & (BooleanTrue | IntPos) != 0 && I1 & IntNeg != 0) || (IntZero & I1 != 0 && IntZero & I2 != 0)) { IntZero } else { none() } - } else { - if FloatPosInf & (I1|I2) != 0 { FloatPosInf } else { none() } - | if FloatNegInf & (I1|I2) != 0 { FloatNegInf } else { none() } - | if (FloatPosInf & I1 != 0 && FloatNegInf & I2 != 0) || (FloatPosInf & I2 != 0 && FloatNegInf & I2 != 0) { FloatNan } else { none() } - | if (FloatPosReal | IntPos) & (I1|I2) != 0 { if FloatPosFrac & (I1|I2) != 0 { FloatPosReal } else { FloatPosInt } } else { none() } - | if (FloatNegReal | IntNeg) & (I1|I2) != { if FloatNegFrac & (I1|I2) != 0 { FloatNegReal } else { FloatNegInt } } else { none() } - | if ((FloatPosReal | IntPos) & I1 != 0 && (FloatNegReal | IntNeg) & I2 != 0) || ((FloatPosReal | IntPos) & I1 != 0 && (FloatNegReal | IntNeg) & I2 != 0) || (FloatPosZero | IntZero) & (I1&I2) != 0 || (FloatPosZero & I1 != 0 && FloatNegZero & I2 != 0) || (FloatPosZero & I2 != 0 && FloatNegZero & I1 != 0) { FloatPosZero } else { none() } - | if FloatNegZero & (I1&I2) != 0 { FloatNegZero } else { none() } - } - wasm: | - #IF I2 & FloatNan != 0 - local.get $I1 - local.get $I1 - f64.ne - if - 0 - local.set $I1 - end - #ENDIF - #IF I2 & FloatNan != 0 - local.get $I2 - local.get $I2 - f64.ne - if - 0 - local.set $I2 - end - #ENDIF - #IF I1 & QuasiInteger != 0 && I1 & Float == 0 - local.get $I1 - - #ENDIF - local.get $I1 - local.get $I2 - - -... \ No newline at end of file diff --git a/src/instructions/operator/add.rs b/src/instructions/operator/add.rs index 72716fb6..3a2f572a 100644 --- a/src/instructions/operator/add.rs +++ b/src/instructions/operator/add.rs @@ -4,6 +4,7 @@ use crate::wasm::StepFunc; use wasm_encoder::{Instruction, ValType}; pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult>> { + hq_assert_eq!(inputs.len(), 2); let t1 = inputs[0]; let t2 = inputs[1]; Ok(if IrType::QuasiInt.contains(t1) { @@ -38,6 +39,7 @@ pub fn acceptable_inputs() -> Rc<[IrType]> { } // TODO: nan + pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { let t1 = inputs[0]; let t2 = inputs[1]; @@ -49,7 +51,7 @@ pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { { IrType::Float } else { - hq_todo!() //IrType::Number + IrType::Number })) } diff --git a/src/instructions/operator/join.rs b/src/instructions/operator/join.rs new file mode 100644 index 00000000..1f377881 --- /dev/null +++ b/src/instructions/operator/join.rs @@ -0,0 +1,26 @@ +use crate::ir::Type as IrType; +use crate::prelude::*; +use crate::wasm::StepFunc; +use wasm_encoder::{Instruction, ValType}; + +pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult>> { + hq_assert_eq!(inputs.len(), 2); + let func_index = func.registries().external_functions().register( + ("operator", "join"), + ( + vec![ValType::EXTERNREF, ValType::EXTERNREF], + vec![ValType::EXTERNREF], + ), + )?; + Ok(vec![Instruction::Call(func_index)]) +} + +pub fn acceptable_inputs() -> Rc<[IrType]> { + Rc::new([IrType::String, IrType::String]) +} + +pub fn output_type(_inputs: Rc<[IrType]>) -> HQResult> { + Ok(Some(IrType::String)) +} + +crate::instructions_test! {tests; t1, t2 ;} diff --git a/src/instructions/tests.rs b/src/instructions/tests.rs index 0080eb5c..7aa6015f 100644 --- a/src/instructions/tests.rs +++ b/src/instructions/tests.rs @@ -17,9 +17,9 @@ macro_rules! instructions_test { use $crate::prelude::*; use $crate::ir::Type as IrType; use wasm_encoder::{ - CodeSection, FunctionSection, ImportSection, Instruction, Module, TableSection, TypeSection, MemorySection, MemoryType, ValType, + CodeSection, ExportSection, FunctionSection, ImportSection, Instruction, Module, TableSection, TypeSection, MemorySection, MemoryType, ValType, }; - use $crate::wasm::{StepFunc, Registries, WasmProject, ExternalEnvironment}; + use $crate::wasm::{StepFunc, Registries, WasmProject}; #[allow(unused)] macro_rules! ident_as_irtype { @@ -35,9 +35,15 @@ macro_rules! instructions_test { for (i, input) in (*types).into_iter().enumerate() { // invalid input types should be handled by a wrapper function somewhere // so we won't test those here. + // TODO: are they actually handled elsewhere? if !acceptable_inputs()[i].contains(**input) { return false; } + // again, non-base types should be handled and unboxed by a wrapper function + // contained in src/instructions/input_switcher.rs + if !input.is_base_type() { + return false; + } } true }) @@ -89,9 +95,9 @@ macro_rules! instructions_test { }; println!("{wasm:?}"); for (i, _) in types.iter().enumerate() { - step_func.add_instructions([Instruction::LocalGet((i + 1).try_into().unwrap())]) + step_func.add_instructions([Instruction::LocalGet((i + 1).try_into().unwrap())])? } - step_func.add_instructions(wasm); + step_func.add_instructions(wasm)?; let mut module = Module::new(); @@ -101,6 +107,7 @@ macro_rules! instructions_test { let mut functions = FunctionSection::new(); let mut codes = CodeSection::new(); let mut memories = MemorySection::new(); + let mut exports = ExportSection::new(); memories.memory(MemoryType { minimum: 1, @@ -113,7 +120,7 @@ macro_rules! instructions_test { registries.external_functions().clone().finish(&mut imports, registries.types())?; step_func.finish(&mut functions, &mut codes)?; registries.types().clone().finish(&mut types); - registries.tables().clone().finish(& mut tables); + registries.tables().clone().finish(& mut tables, &mut exports); module.section(&types); module.section(&imports); @@ -151,7 +158,7 @@ macro_rules! instructions_test { println!("skipping failed wasm"); continue; }; - for ((module, name), (params, results)) in registries.external_functions().registry().borrow().iter() { + for ((module, name), (params, results)) in registries.external_functions().registry().try_borrow().unwrap().iter() { assert!(results.len() <= 1, "external function {}::{} registered as returning multiple results", module, name); let out = if results.len() == 0 { "void" diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index dfb553bc..151f7f2a 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -1,11 +1,37 @@ use crate::instructions::{fields::*, IrOpcode}; +use crate::ir::{Type as IrType, TypeStack}; use crate::prelude::*; use crate::sb3::{Block, BlockArray, BlockArrayOrId, BlockInfo, BlockMap, BlockOpcode, Input}; -// TODO: insert casts in relevant places +fn insert_casts(mut blocks: Vec) -> HQResult> { + let mut type_stack: Vec<(IrType, usize)> = vec![]; // a vector of types, and where they came from + let mut casts: Vec<(usize, IrType)> = vec![]; // a vector of cast targets, and where they're needed + for (i, block) in blocks.iter().enumerate() { + let expected_inputs = block.acceptable_inputs(); + if type_stack.len() < expected_inputs.len() { + hq_bug!("didn't have enough inputs on the type stack") + } + let actual_inputs: Vec<_> = type_stack + .splice((type_stack.len() - expected_inputs.len()).., []) + .collect(); + for (&expected, actual) in core::iter::zip(expected_inputs.iter(), actual_inputs) { + if !expected.contains(actual.0) { + casts.push((actual.1, expected)); + } + } + // TODO: make this more specific by using the actual input types post-cast + if let Some(output) = block.output_type(expected_inputs)? { + type_stack.push((output, i)); + } + } + for (pos, ty) in casts { + blocks.insert(pos + 1, IrOpcode::hq_cast(HqCastFields(ty))); + } + Ok(blocks) +} -pub fn from_block(block: &Block, blocks: &BlockMap) -> HQResult> { - Ok(match block { +pub fn from_block(block: &Block, blocks: &BlockMap) -> HQResult> { + insert_casts(match block { Block::Normal { block_info, .. } => { if let Some(next_id) = &block_info.next { from_normal_block(block_info, blocks)? @@ -29,7 +55,7 @@ pub fn from_block(block: &Block, blocks: &BlockMap) -> HQResult> .collect() } } - Block::Special(block_array) => Box::new([from_special_block(block_array)?]), + Block::Special(block_array) => vec![from_special_block(block_array)?], }) } @@ -37,6 +63,7 @@ pub fn input_names(opcode: BlockOpcode) -> HQResult> { Ok(match opcode { BlockOpcode::looks_say => vec!["MESSAGE"], BlockOpcode::operator_add => vec!["NUM1", "NUM2"], + BlockOpcode::operator_join => vec!["STRING1", "STRING2"], other => hq_todo!("unimplemented input_names for {:?}", other), } .into_iter() @@ -81,6 +108,7 @@ fn from_normal_block(block_info: &BlockInfo, blocks: &BlockMap) -> HQResult [IrOpcode::operator_add].into_iter(), BlockOpcode::looks_say => [IrOpcode::looks_say].into_iter(), + BlockOpcode::operator_join => [IrOpcode::operator_join].into_iter(), other => hq_todo!("unimplemented block: {:?}", other), }) .collect()) diff --git a/src/ir/proc.rs b/src/ir/proc.rs index d7bc7112..a0f52c88 100644 --- a/src/ir/proc.rs +++ b/src/ir/proc.rs @@ -14,12 +14,12 @@ pub struct ProcedureContext { } impl ProcedureContext { - pub fn arg_ids(&self) -> &[Box] { - self.arg_ids.borrow() + pub fn arg_ids(&self) -> &Box<[Box]> { + &self.arg_ids } - pub fn arg_types(&self) -> &[IrType] { - self.arg_types.borrow() + pub fn arg_types(&self) -> &Box<[IrType]> { + &self.arg_types } pub fn warp(&self) -> bool { @@ -158,12 +158,7 @@ impl Proc { step_context, project, )?, - None => RcStep::new(Rc::new(Step::new( - None, - step_context, - Box::new([]), - project, - ))), + None => RcStep::new(Rc::new(Step::new(None, step_context, vec![], project))), }; Ok(Proc { first_step, @@ -196,7 +191,7 @@ impl ProcRegistry { )?; Ok(Rc::clone( self.registry() - .borrow() + .try_borrow()? .get_index(idx) .ok_or(make_hq_bug!( "recently inserted proc not found in ProcRegistry" diff --git a/src/ir/project.rs b/src/ir/project.rs index 058d6b1b..9cc75f2c 100644 --- a/src/ir/project.rs +++ b/src/ir/project.rs @@ -32,8 +32,9 @@ impl IrProject { } } - pub fn register_step(&self, step: Rc) { - self.steps().borrow_mut().insert(step); + pub fn register_step(&self, step: Rc) -> HQResult<()> { + self.steps().try_borrow_mut()?.insert(step); + Ok(()) } } @@ -68,7 +69,7 @@ impl TryFrom for Rc { .flatten() .cloned() .collect::>(); - *project.threads.borrow_mut() = threads; + *project.threads.try_borrow_mut()? = threads; Ok(project) } } diff --git a/src/ir/step.rs b/src/ir/step.rs index b1e050bb..de4126cd 100644 --- a/src/ir/step.rs +++ b/src/ir/step.rs @@ -8,7 +8,7 @@ use uuid::Uuid; #[derive(Clone, Debug)] pub struct Step { context: StepContext, - opcodes: Box<[IrOpcode]>, + opcodes: Vec, /// is this step inlined? if not, its own function should be produced inlined: RefCell, /// used for `Hash`. Should be obtained from a block in the `Step` where possible. @@ -29,12 +29,12 @@ impl Step { &self.context } - pub fn opcodes(&self) -> &[IrOpcode] { - self.opcodes.borrow() + pub fn opcodes(&self) -> &Vec { + &self.opcodes } - pub fn inlined(&self) -> bool { - *self.inlined.borrow() + pub fn inlined(&self) -> &RefCell { + &self.inlined } pub fn project(&self) -> Weak { @@ -44,7 +44,7 @@ impl Step { pub fn new( id: Option>, context: StepContext, - opcodes: Box<[IrOpcode]>, + opcodes: Vec, project: Weak, ) -> Self { Step { @@ -63,9 +63,9 @@ impl Step { target: Weak::new(), proc_context: None, }, - opcodes: Box::new([IrOpcode::hq_integer(crate::instructions::HqIntegerFields( + opcodes: vec![IrOpcode::hq_integer(crate::instructions::HqIntegerFields( 0, - ))]), + ))], inlined: RefCell::new(false), project: Weak::new(), } @@ -88,8 +88,9 @@ impl Step { .upgrade() .ok_or(make_hq_bug!("couldn't upgrade Weak"))? .steps() - .borrow_mut() + .try_borrow_mut()? .insert(Rc::clone(&step)); + crate::log(format!("{:?}", step).as_str()); Ok(RcStep(step)) } } @@ -113,23 +114,23 @@ impl RcStep { } pub fn make_inlined(&self) -> HQResult<()> { - if *self.0.inlined.borrow() { + if *self.0.inlined.try_borrow()? { return Ok(()); }; - *self.0.inlined.borrow_mut() = true; + *self.0.inlined.try_borrow_mut()? = true; self.0 .project .upgrade() .ok_or(make_hq_bug!("couldn't upgrade Weak"))? .inlined_steps() - .borrow_mut() + .try_borrow_mut()? .insert( self.0 .project .upgrade() .ok_or(make_hq_bug!("couldn't upgrade Weak"))? .steps() - .borrow_mut() + .try_borrow_mut()? .swap_take(&self.0) .ok_or(make_hq_bug!("step not in project's StepMap"))?, ); diff --git a/src/ir/target.rs b/src/ir/target.rs index f8a62624..1fa2b6db 100644 --- a/src/ir/target.rs +++ b/src/ir/target.rs @@ -7,8 +7,8 @@ pub struct Target { } impl Target { - pub fn name(&self) -> &str { - self.name.borrow() + pub fn name(&self) -> &Box { + &self.name } pub fn is_stage(&self) -> bool { diff --git a/src/ir/types.rs b/src/ir/types.rs index 5b4fe499..6dbdfd83 100644 --- a/src/ir/types.rs +++ b/src/ir/types.rs @@ -64,5 +64,26 @@ pub enum Type { Color, } +impl Type { + pub const BASE_TYPES: [Type; 3] = [Type::String, Type::QuasiInt, Type::Float]; + + pub fn is_base_type(&self) -> bool { + Type::BASE_TYPES.iter().any(|ty| ty.contains(*self)) + } + + pub fn base_type(&self) -> Option { + if !self.is_base_type() { + return None; + } + Type::BASE_TYPES + .iter() + .cloned() + .find(|&ty| ty.contains(*self)) + } + + pub fn base_types(&self) -> impl Iterator { + Type::BASE_TYPES.iter().filter(|ty| ty.intersects(*self)) + } +} pub type TypeStack = Vec; diff --git a/src/ir_opt.rs b/src/ir_opt.rs index fdb45ae6..4ae51180 100644 --- a/src/ir_opt.rs +++ b/src/ir_opt.rs @@ -8,12 +8,12 @@ use alloc::vec::Vec; use core::cell::RefCell; impl IrProject { - pub fn optimise(&mut self) -> Result<(), HQError> { + pub fn optimise(&mut self) -> Result<(), HQError> { self.const_fold()?; self.variable_types()?; Ok(()) } - pub fn variable_types(&mut self) -> Result<(), HQError> { + pub fn variable_types(&mut self) -> Result<(), HQError> { let mut var_map: BTreeMap> = BTreeMap::new(); #[allow(clippy::type_complexity)] let mut block_swaps: BTreeMap< @@ -124,7 +124,7 @@ impl IrProject { }, _ => &previous_block .type_stack() - .borrow() + .try_borrow()? .clone() .ok_or(make_hq_bug!("unexpected empty type stack"))? .1 @@ -191,7 +191,7 @@ impl IrProject { } Ok(()) } - pub fn const_fold(&mut self) -> Result<(), HQError> { + pub fn const_fold(&mut self) -> Result<(), HQError> { for step in self.steps.values_mut() { step.const_fold()?; } diff --git a/src/lib.rs b/src/lib.rs index eeb49c3d..78b7bc59 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,6 +50,7 @@ use prelude::*; #[cfg(target_family = "wasm")] #[wasm_bindgen(js_namespace=console)] extern "C" { + pub fn log(s: &str); } diff --git a/src/old-ir.rs b/src/old-ir.rs index 90a2ffa8..8b6575ba 100644 --- a/src/old-ir.rs +++ b/src/old-ir.rs @@ -13,7 +13,7 @@ use alloc::vec::Vec; use core::cell::RefCell; use core::fmt; use core::hash::BuildHasherDefault; -use hashers::fnv::FNV1aHasher64; +use hashers:: fnv::FNV1aHasher64; use indexmap::IndexMap; use lazy_regex::{lazy_regex, Lazy}; use regex::Regex; @@ -41,7 +41,7 @@ pub struct IrProject { } impl fmt::Display for IrProject { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{{\n\tthreads: {:?},\n\tvars: {:?},\n\t,target_names: {:?},\n\tcostumes: {:?},\n\tsteps: {:?}\n}}", self.threads, self.vars, @@ -55,7 +55,7 @@ impl fmt::Display for IrProject { impl TryFrom for IrProject { type Error = HQError; - fn try_from(sb3: Sb3Project) -> Result { + fn try_from(sb3: Sb3Project) -> Result { let vars: Rc>> = Rc::new(RefCell::new( sb3.targets .iter() @@ -393,22 +393,22 @@ pub enum IrOpcode { pub struct TypeStack(pub Rc>>, pub InputType); impl TypeStack { - pub fn new_some(prev: TypeStack) -> Rc>> { + pub fn new_some(prev: TypeStack) -> Rc>> { Rc::new(RefCell::new(Some(prev))) } - pub fn new(prev: Option) -> Rc>> { + pub fn new(prev: Option) -> Rc>> { Rc::new(RefCell::new(prev)) } } #[allow(clippy::len_without_is_empty)] pub trait TypeStackImpl { - fn get(&self, i: usize) -> Rc>>; - fn len(&self) -> usize; + fn get(&self, i: usize) -> Rc>>; + fn len(&self) -> usize; } impl TypeStackImpl for Rc>> { - fn get(&self, i: usize) -> Rc>> { + fn get(&self, i: usize) -> Rc>> { if i == 0 || self.borrow().is_none() { Rc::clone(self) } else { @@ -416,7 +416,7 @@ impl TypeStackImpl for Rc>> { } } - fn len(&self) -> usize { + fn len(&self) -> usize { if self.borrow().is_none() { 0 } else { @@ -432,7 +432,7 @@ pub struct IrBlock { } impl IrBlock { - pub fn new_with_stack( + pub fn new_with_stack( opcode: IrOpcode, type_stack: Rc>>, add_cast: &mut F, @@ -464,7 +464,7 @@ impl IrBlock { }) } - pub fn new_with_stack_no_cast( + pub fn new_with_stack_no_cast( opcode: IrOpcode, type_stack: Rc>>, ) -> Result { @@ -503,7 +503,7 @@ impl IrBlock { }) } - pub fn does_request_redraw(&self) -> bool { + pub fn does_request_redraw(&self) -> bool { use IrOpcode::*; matches!( self.opcode(), @@ -523,18 +523,18 @@ impl IrBlock { | looks_changesizeby ) } - pub fn is_hat(&self) -> bool { + pub fn is_hat(&self) -> bool { use IrOpcode::*; matches!(self.opcode, event_whenflagclicked) } - pub fn opcode(&self) -> &IrOpcode { + pub fn opcode(&self) -> &IrOpcode { &self.opcode } - pub fn type_stack(&self) -> Rc>> { + pub fn type_stack(&self) -> Rc>> { Rc::clone(&self.type_stack) } - pub fn is_const(&self) -> bool { + pub fn is_const(&self) -> bool { use IrOpcode::*; matches!( self.opcode(), @@ -547,7 +547,7 @@ impl IrBlock { ) } - pub fn const_value(&self, value_stack: &mut Vec) -> Result, HQError> { + pub fn const_value(&self, value_stack: &mut Vec) -> Result, HQError> { use IrOpcode::*; //dbg!(value_stack.len()); let arity = self.opcode().expected_inputs()?.len(); @@ -663,7 +663,7 @@ pub enum InputType { impl InputType { /// returns the base type that a type is aliased to, if present; /// otherwise, returns a clone of itself - pub fn base_type(&self) -> InputType { + pub fn base_type(&self) -> InputType { use InputType::*; // unions of types should be represented with the least restrictive type first, // so that casting chooses the less restrictive type to cast to @@ -683,7 +683,7 @@ impl InputType { /// when a type is a union type, returns the first concrete type in that union /// (this assumes that the least restrictive type is placed first in the union). /// otherwise, returns itself. - pub fn least_restrictive_concrete_type(&self) -> InputType { + pub fn least_restrictive_concrete_type(&self) -> InputType { match self.base_type() { InputType::Union(a, _) => a.least_restrictive_concrete_type(), other => other, @@ -691,7 +691,7 @@ impl InputType { } /// determines whether a type is a superyype of another type - pub fn includes(&self, other: &Self) -> bool { + pub fn includes(&self, other: &Self) -> bool { if self.base_type() == other.base_type() { true } else if let InputType::Union(a, b) = self.base_type() { @@ -706,7 +706,7 @@ impl InputType { /// so that there is no ambiguity over which type it should be promoted to. /// if there are no types that can be prpmoted to, /// an `Err(HQError)` will be returned. - pub fn loosen_to(&self, others: T) -> Result + pub fn loosen_to(&self, others: T) -> Result where T: IntoIterator, T::IntoIter: ExactSizeIterator, @@ -723,12 +723,12 @@ impl InputType { } impl IrOpcode { - pub fn does_request_redraw(&self) -> bool { + pub fn does_request_redraw(&self) -> bool { use IrOpcode::*; matches!(self, looks_say | looks_think) } - pub fn expected_inputs(&self) -> Result, HQError> { + pub fn expected_inputs(&self) -> Result, HQError> { use InputType::*; use IrOpcode::*; Ok(match self { @@ -798,7 +798,7 @@ impl IrOpcode { }) } - pub fn output( + pub fn output( &self, type_stack: Rc>>, ) -> Result>>, HQError> { @@ -940,7 +940,7 @@ pub struct IrVar { } impl IrVar { - pub fn new(id: String, name: String, initial_value: VarVal, is_cloud: bool) -> Self { + pub fn new(id: String, name: String, initial_value: VarVal, is_cloud: bool) -> Self { Self { id, name, @@ -948,16 +948,16 @@ impl IrVar { is_cloud, } } - pub fn id(&self) -> &String { + pub fn id(&self) -> &String { &self.id } - pub fn name(&self) -> &String { + pub fn name(&self) -> &String { &self.name } - pub fn initial_value(&self) -> &VarVal { + pub fn initial_value(&self) -> &VarVal { &self.initial_value } - pub fn is_cloud(&self) -> &bool { + pub fn is_cloud(&self) -> &bool { &self.is_cloud } } @@ -982,7 +982,7 @@ pub struct Procedure { } impl fmt::Debug for TypeStack { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let this = Rc::new(RefCell::new(Some(self.clone()))); f.debug_list() .entries( @@ -995,19 +995,19 @@ impl fmt::Debug for TypeStack { } impl Step { - pub fn new(opcodes: Vec, context: Rc) -> Step { + pub fn new(opcodes: Vec, context: Rc) -> Step { Step { opcodes, context } } - pub fn opcodes(&self) -> &Vec { + pub fn opcodes(&self) -> &Vec { &self.opcodes } - pub fn opcodes_mut(&mut self) -> &mut Vec { + pub fn opcodes_mut(&mut self) -> &mut Vec { &mut self.opcodes } - pub fn context(&self) -> Rc { + pub fn context(&self) -> Rc { Rc::clone(&self.context) } - pub fn const_fold(&mut self) -> Result<(), HQError> { + pub fn const_fold(&mut self) -> Result<(), HQError> { let mut value_stack: Vec = vec![]; let mut is_folding = false; let mut fold_start = 0; @@ -1070,7 +1070,7 @@ pub enum IrVal { } impl IrVal { - pub fn to_f64(self) -> f64 { + pub fn to_f64(self) -> f64 { match self { IrVal::Unknown(u) => u.to_f64(), IrVal::Float(f) => f, @@ -1079,7 +1079,7 @@ impl IrVal { IrVal::String(s) => s.parse().unwrap_or(0.0), } } - pub fn to_i32(self) -> i32 { + pub fn to_i32(self) -> i32 { match self { IrVal::Unknown(u) => u.to_i32(), IrVal::Float(f) => f as i32, @@ -1089,7 +1089,7 @@ impl IrVal { } } #[allow(clippy::inherent_to_string)] - pub fn to_string(self) -> String { + pub fn to_string(self) -> String { match self { IrVal::Unknown(u) => u.to_string(), IrVal::Float(f) => format!("{f}"), @@ -1098,14 +1098,14 @@ impl IrVal { IrVal::String(s) => s, } } - pub fn to_bool(self) -> bool { + pub fn to_bool(self) -> bool { match self { IrVal::Unknown(u) => u.to_bool(), IrVal::Boolean(b) => b, _ => unreachable!(), } } - pub fn as_input_type(&self) -> InputType { + pub fn as_input_type(&self) -> InputType { match self { IrVal::Unknown(_) => InputType::Unknown, IrVal::Float(_) => InputType::Float, @@ -1114,7 +1114,7 @@ impl IrVal { IrVal::Boolean(_) => InputType::Boolean, } } - pub fn try_as_block( + pub fn try_as_block( &self, type_stack: Rc>>, ) -> Result { @@ -1150,7 +1150,7 @@ pub struct Thread { static ARG_REGEX: Lazy = lazy_regex!(r#"[^\\]%[nbs]"#); -fn arg_types_from_proccode(proccode: String) -> Result, HQError> { + fn arg_types_from_proccode(proccode: String) -> Result, HQError> { // https://github.com/scratchfoundation/scratch-blocks/blob/abbfe93136fef57fdfb9a077198b0bc64726f012/blocks_vertical/procedures.js#L207-L215 (*ARG_REGEX) .find_iter(proccode.as_str()) @@ -1168,7 +1168,7 @@ fn arg_types_from_proccode(proccode: String) -> Result, HQError> .collect::, _>>() } -fn add_procedure( + fn add_procedure( target_id: String, proccode: String, expect_warp: bool, @@ -1263,7 +1263,7 @@ fn add_procedure( trait IrBlockVec { #[allow(clippy::too_many_arguments)] - fn add_block( + fn add_block( &mut self, block_id: String, blocks: &BTreeMap, @@ -1273,8 +1273,8 @@ trait IrBlockVec { target_id: String, procedures: &mut ProcMap, ) -> Result<(), HQError>; - fn add_block_arr(&mut self, block_arr: &BlockArray) -> Result<(), HQError>; - fn add_inputs( + fn add_block_arr(&mut self, block_arr: &BlockArray) -> Result<(), HQError>; + fn add_inputs( &mut self, inputs: &BTreeMap, blocks: &BTreeMap, @@ -1283,11 +1283,11 @@ trait IrBlockVec { target_id: String, procedures: &mut ProcMap, ) -> Result<(), HQError>; - fn get_type_stack(&self, i: Option) -> Rc>>; + fn get_type_stack(&self, i: Option) -> Rc>>; } impl IrBlockVec for Vec { - fn add_inputs( + fn add_inputs( &mut self, inputs: &BTreeMap, blocks: &BTreeMap, @@ -1326,7 +1326,7 @@ impl IrBlockVec for Vec { } Ok(()) } - fn add_block_arr(&mut self, block_arr: &BlockArray) -> Result<(), HQError> { + fn add_block_arr(&mut self, block_arr: &BlockArray) -> Result<(), HQError> { let prev_block = self.last(); let type_stack = if let Some(block) = prev_block { Rc::clone(&block.type_stack) @@ -1384,7 +1384,7 @@ impl IrBlockVec for Vec { )?); Ok(()) } - fn get_type_stack(&self, i: Option) -> Rc>> { + fn get_type_stack(&self, i: Option) -> Rc>> { let prev_block = if let Some(j) = i { self.get(j) } else { @@ -1396,7 +1396,7 @@ impl IrBlockVec for Vec { Rc::new(RefCell::new(None)) } } - fn add_block( + fn add_block( &mut self, block_id: String, blocks: &BTreeMap, @@ -2124,7 +2124,7 @@ impl IrBlockVec for Vec { } } -pub fn step_from_top_block<'a>( + pub fn step_from_top_block<'a>( top_id: String, mut last_nexts: Vec, blocks: &BTreeMap, @@ -2230,23 +2230,23 @@ pub fn step_from_top_block<'a>( } impl Thread { - pub fn new(start: ThreadStart, first_step: String, target_id: String) -> Thread { + pub fn new(start: ThreadStart, first_step: String, target_id: String) -> Thread { Thread { start, first_step, target_id, } } - pub fn start(&self) -> &ThreadStart { + pub fn start(&self) -> &ThreadStart { &self.start } - pub fn first_step(&self) -> &String { + pub fn first_step(&self) -> &String { &self.first_step } - pub fn target_id(&self) -> &String { + pub fn target_id(&self) -> &String { &self.target_id } - pub fn from_hat( + pub fn from_hat( hat: Block, blocks: BTreeMap, context: Rc, @@ -2291,7 +2291,7 @@ mod tests { use super::*; #[test] - fn create_ir() -> Result<(), HQError> { + fn create_ir() -> Result<(), HQError> { use crate::sb3::Sb3Project; use std::fs; let proj: Sb3Project = fs::read_to_string("./project.json") @@ -2303,7 +2303,7 @@ mod tests { } #[test] - fn const_fold() -> Result<(), HQError> { + fn const_fold() -> Result<(), HQError> { use crate::sb3::Sb3Project; use std::fs; let proj: Sb3Project = fs::read_to_string("./project.json") @@ -2315,7 +2315,7 @@ mod tests { Ok(()) } #[test] - fn opt() -> Result<(), HQError> { + fn opt() -> Result<(), HQError> { use crate::sb3::Sb3Project; use std::fs; let proj: Sb3Project = fs::read_to_string("./project.json") @@ -2328,7 +2328,7 @@ mod tests { } #[test] - fn input_types() { + fn input_types() { use InputType::*; assert!(Number.includes(&Float)); assert!(Number.includes(&Integer)); diff --git a/src/old-wasm.rs b/src/old-wasm.rs index c7020526..971b25d2 100644 --- a/src/old-wasm.rs +++ b/src/old-wasm.rs @@ -19,7 +19,7 @@ use wasm_encoder::{ MemoryType, Module, RefType, TableSection, TableType, TypeSection, ValType, }; -fn instructions( + fn instructions( op: &IrBlock, context: Rc, string_consts: &mut Vec, @@ -1371,7 +1371,7 @@ fn instructions( } pub trait CompileToWasm { - fn compile_wasm( + fn compile_wasm( &self, step_funcs: &mut IndexMap< Option<(String, String)>, @@ -1384,7 +1384,7 @@ pub trait CompileToWasm { } impl CompileToWasm for (&(String, String), &Step) { - fn compile_wasm( + fn compile_wasm( &self, step_funcs: &mut IndexMap< Option<(String, String)>, @@ -1451,13 +1451,13 @@ pub struct WasmProject { /* #[wasm_bindgen] impl WasmProject { - pub fn wasm_bytes(&self) -> &Vec { + pub fn wasm_bytes(&self) -> &Vec { &self.wasm_bytes } - pub fn string_consts(&self) -> &Vec { + pub fn string_consts(&self) -> &Vec { &self.string_consts } - pub fn target_names(&self) -> &Vec { + pub fn target_names(&self) -> &Vec { &self.target_names } }*/ @@ -1639,7 +1639,7 @@ pub const BUILTIN_GLOBALS: u32 = 3; impl TryFrom for WasmProject { type Error = HQError; - fn try_from(project: IrProject) -> Result { + fn try_from(project: IrProject) -> Result { let mut module = Module::new(); let mut imports = ImportSection::new(); @@ -2858,7 +2858,7 @@ mod tests { use std::process::{Command, Stdio}; #[test] - fn make_wasm() -> Result<(), HQError> { + fn make_wasm() -> Result<(), HQError> { use crate::sb3::Sb3Project; use std::fs; let proj: Sb3Project = fs::read_to_string("./benchmark (3.1).json") diff --git a/src/registry.rs b/src/registry.rs index 136e55a6..672bddaf 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -69,14 +69,15 @@ pub trait Registry: Sized { fn register(&self, key: Self::Key, value: Self::Value) -> HQResult where N: TryFrom, + >::Error: alloc::fmt::Debug, { self.registry() - .borrow_mut() + .try_borrow_mut()? .entry(key.clone()) .or_insert(value); N::try_from( self.registry() - .borrow() + .try_borrow()? .get_index_of(&key) .ok_or(make_hq_bug!("couldn't find entry in Registry"))?, ) @@ -88,6 +89,7 @@ pub trait RegistryDefault: Registry { fn register_default(&self, key: Self::Key) -> HQResult where N: TryFrom, + >::Error: alloc::fmt::Debug, { self.register(key, Default::default()) } diff --git a/src/sb3.rs b/src/sb3.rs index bdc7e6cc..44156ff7 100644 --- a/src/sb3.rs +++ b/src/sb3.rs @@ -494,7 +494,7 @@ impl TryFrom<&str> for Sb3Project { pub mod tests { use super::*; - pub fn test_project_id(id: &str) -> String { + pub fn test_project_id(id: &str) -> String { use std::time::{SystemTime, UNIX_EPOCH}; println!("https://api.scratch.mit.edu/projects/{:}/", id); let token_val = serde_json::from_str::( @@ -530,7 +530,7 @@ pub mod tests { } #[test] - fn paper_minecraft() { + fn paper_minecraft() { let resp = self::test_project_id("10128407"); let j: Sb3Project = resp.try_into().unwrap(); dbg!(j); @@ -557,7 +557,7 @@ pub mod tests { } #[test] - fn level_eaten() { + fn level_eaten() { let resp = self::test_project_id("704676520"); let j: Sb3Project = resp.try_into().unwrap(); dbg!(j); @@ -584,7 +584,7 @@ pub mod tests { } #[test] - fn hq_test_project() { + fn hq_test_project() { let resp = self::test_project_id("771449498"); dbg!(&resp); let j: Sb3Project = resp.try_into().unwrap(); @@ -592,7 +592,7 @@ pub mod tests { } #[test] - fn default_project() { + fn default_project() { let resp = self::test_project_id("510186917"); let j: Sb3Project = resp.try_into().unwrap(); dbg!(j); diff --git a/src/wasm/func.rs b/src/wasm/func.rs index e94dee31..2aa76e37 100644 --- a/src/wasm/func.rs +++ b/src/wasm/func.rs @@ -54,13 +54,17 @@ impl StepFunc { /// Registers a new local in this function, and returns its index pub fn local(&self, val_type: ValType) -> HQResult { - self.locals.borrow_mut().push(val_type); - u32::try_from(self.locals.borrow().len() + self.params.len() - 1) + self.locals.try_borrow_mut()?.push(val_type); + u32::try_from(self.locals.try_borrow()?.len() + self.params.len() - 1) .map_err(|_| make_hq_bug!("local index was out of bounds")) } - pub fn add_instructions(&self, instructions: impl IntoIterator>) { - self.instructions.borrow_mut().extend(instructions); + pub fn add_instructions( + &self, + instructions: impl IntoIterator>, + ) -> HQResult<()> { + self.instructions.try_borrow_mut()?.extend(instructions); + Ok(()) } /// Takes ownership of the function and returns the backing `wasm_encoder` `Function` diff --git a/src/wasm/project.rs b/src/wasm/project.rs index ce028a2a..e2469cea 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -1,4 +1,5 @@ use super::{ExternalEnvironment, Registries}; +use crate::instructions::wrap_instruction; use crate::ir::{Event, IrProject, Step, Type as IrType}; use crate::prelude::*; use crate::wasm::{StepFunc, WasmFlags}; @@ -47,8 +48,8 @@ impl WasmProject { self.environment } - pub fn step_funcs(&self) -> &[StepFunc] { - self.step_funcs.borrow() + pub fn step_funcs(&self) -> &Box<[StepFunc]> { + &self.step_funcs } /// maps a broad IR type to a WASM type @@ -60,7 +61,7 @@ impl WasmProject { } else if IrType::String.contains(ir_type) { ValType::EXTERNREF } else if IrType::Color.contains(ir_type) { - hq_todo!() //ValType::V128 // f32x4 + hq_todo!() //ValType::V128 // f32x4? } else { ValType::I64 // NaN boxed value... let's worry about colors later }) @@ -107,11 +108,17 @@ impl WasmProject { step_func_indices, ); + let strings = self.registries().strings().clone().finish(); + self.registries().tables().register::( "strings".into(), ( RefType::EXTERNREF, - 0, // filled in by js (for now...); TODO: use js string imports + strings + .len() + .try_into() + .map_err(|_| make_hq_bug!("strings length out of bounds"))?, + // TODO: use js string imports for preknown strings ), )?; @@ -131,7 +138,10 @@ impl WasmProject { self.registries().types().clone().finish(&mut types); - self.registries().tables().clone().finish(&mut tables); + self.registries() + .tables() + .clone() + .finish(&mut tables, &mut exports); let data_count = DataCountSection { count: data.len() }; @@ -155,7 +165,7 @@ impl WasmProject { Ok(FinishedWasm { wasm_bytes: wasm_bytes.into_boxed_slice(), - strings: self.registries().strings().clone().finish(), + strings, }) } @@ -163,7 +173,7 @@ impl WasmProject { self.registries() .external_functions() .registry() - .borrow() + .try_borrow()? .len() .try_into() .map_err(|_| make_hq_bug!("external function map len out of bounds")) @@ -175,7 +185,7 @@ impl WasmProject { registries: Rc, flags: WasmFlags, ) -> HQResult<()> { - if steps.borrow().contains_key(&step) { + if steps.try_borrow()?.contains_key(&step) { return Ok(()); } let step_func = StepFunc::new(registries, flags); @@ -185,13 +195,17 @@ impl WasmProject { let inputs = type_stack .splice((type_stack.len() - opcode.acceptable_inputs().len()).., []) .collect(); - instrs.append(&mut opcode.wasm(&step_func, Rc::clone(&inputs))?); + instrs.append(&mut wrap_instruction( + &step_func, + Rc::clone(&inputs), + opcode.clone(), + )?); if let Some(output) = opcode.output_type(inputs)? { type_stack.push(output); } } - step_func.add_instructions(instrs); - steps.borrow_mut().insert(step, step_func); + step_func.add_instructions(instrs)?; + steps.try_borrow_mut()?.insert(step, step_func); Ok(()) } @@ -247,7 +261,7 @@ impl WasmProject { Instruction::I32Load(MemArg { offset: byte_offset::THREAD_NUM .try_into() - .map_err(|_| make_hq_bug!("THREAD_NUM out of bounds"))?, + .map_err(|_| make_hq_bug!("thread num byte offset out of bounds"))?, align: 2, memory_index: 0, }), // [i32] @@ -258,7 +272,7 @@ impl WasmProject { Instruction::I32Const(0), // offset in data segment; [i32, i32] Instruction::I32Const( i32::try_from(indices.len()) - .map_err(|_| make_hq_bug!("start_type count out of bounds"))? + .map_err(|_| make_hq_bug!("indices lenout of bounds"))? * WasmProject::THREAD_BYTE_LEN, ), // segment length; [i32, i32, i32] Instruction::MemoryInit { @@ -271,7 +285,7 @@ impl WasmProject { Instruction::I32Load(MemArg { offset: byte_offset::THREAD_NUM .try_into() - .map_err(|_| make_hq_bug!("THREAD_NUM out of bounds"))?, + .map_err(|_| make_hq_bug!("thread num byte offset out of bounds"))?, align: 2, memory_index: 0, }), // [i32, i32] @@ -285,7 +299,7 @@ impl WasmProject { Instruction::I32Store(MemArg { offset: byte_offset::THREAD_NUM .try_into() - .map_err(|_| make_hq_bug!("THREAD_NUM out of bounds"))?, + .map_err(|_| make_hq_bug!("thread num byte offset out of bounds"))?, align: 2, memory_index: 0, }), // [] @@ -367,7 +381,7 @@ impl WasmProject { Instruction::LocalGet(0), Instruction::I32Load(MemArg { offset: /*(byte_offset::VARS as usize - + VAR_INFO_LEN as usize * project.vars.borrow().len() + + VAR_INFO_LEN as usize * project.vars.try_borrow()?.len() + usize::try_from(SPRITE_INFO_LEN).map_err(|_| make_hq_bug!(""))? * (project.target_names.len() - 1)) .try_into() @@ -431,14 +445,14 @@ impl WasmProject { Rc::clone(®istries), flags, )?; - for thread in ir_project.threads().borrow().iter() { + for thread in ir_project.threads().try_borrow()?.iter() { let step = thread.first_step().get_rc(); WasmProject::compile_step(step, &steps, Rc::clone(®istries), flags)?; events.entry(thread.event()).or_default().push( u32::try_from( ir_project .steps() - .borrow() + .try_borrow()? .get_index_of(&thread.first_step().get_rc()) .ok_or(make_hq_bug!( "Thread's first_step wasn't found in Thread::steps()" @@ -487,7 +501,9 @@ mod tests { fn project_with_one_empty_step_is_valid_wasm() { let registries = Rc::new(Registries::default()); let step_func = StepFunc::new(Rc::clone(®istries), Default::default()); - step_func.add_instructions(vec![Instruction::I32Const(0)]); // this is handled by compile_step in a non-test environment + step_func + .add_instructions(vec![Instruction::I32Const(0)]) + .unwrap(); // this is handled by compile_step in a non-test environment let project = WasmProject { flags: Default::default(), step_funcs: Box::new([step_func]), diff --git a/src/wasm/registries.rs b/src/wasm/registries.rs index cab6ff85..d555ac47 100644 --- a/src/wasm/registries.rs +++ b/src/wasm/registries.rs @@ -1,4 +1,5 @@ use super::{ExternalFunctionRegistry, StringRegistry, TableRegistry, TypeRegistry}; +use crate::prelude::*; #[derive(Default)] pub struct Registries { diff --git a/src/wasm/tables.rs b/src/wasm/tables.rs index c82b2895..c7d81055 100644 --- a/src/wasm/tables.rs +++ b/src/wasm/tables.rs @@ -1,12 +1,14 @@ use crate::prelude::*; use crate::registry::MapRegistry; -use wasm_encoder::{RefType, TableSection, TableType}; +use wasm_encoder::{ExportKind, ExportSection, RefType, TableSection, TableType}; pub type TableRegistry = MapRegistry, (RefType, u64)>; impl TableRegistry { - pub fn finish(self, tables: &mut TableSection) { - for &(element_type, min) in self.registry().take().values() { + pub fn finish(self, tables: &mut TableSection, exports: &mut ExportSection) { + for (key, (element_type, min)) in self.registry().take() { + // TODO: allow choosing whether to export a table or not? + exports.export(&key, ExportKind::Table, tables.len()); // TODO: allow specifying min/max table size when registering, or after registering tables.table(TableType { element_type, From f61bdb61413090e25764dc46f8bce0e43cda6f9d Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:43:25 +0000 Subject: [PATCH 18/98] add instructions on adding new blocks --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index f4862657..acebf265 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,16 @@ The build script has additonal configuration options; run `./build.sh -h` for in If you experience runtime stack overflow errors in debug mode, try using the `-O` option to enable wasm-opt. +## Adding a new block + +To add a new block named `category_opcode`: +- create `src/instructions/category/opcode.rs` with the relevant `pub` functions +- - ensure to add relevant `instructions_test!`s +- add `pub mod opcode;` to `src/instructions/category.rs`, creating the file if needed +- - if you're creating the category file, add `mod category;` to `src/instructions.rs` +- add the block to `from_normal_block` in `src/ir/blocks.rs` +- add the block's input names to `input_names` in `src/ir/blocks.rs` + ## generared WASM module memory layout | name | number of bytes | optional? | description | From d140faf3a8a20f5761c89559085bd57ff00a17c2 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:01:49 +0000 Subject: [PATCH 19/98] CI overhaul --- .github/workflows/ci.yaml | 47 +++++++++++++++++++++++++++++---------- README.md | 1 + 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index db6e9fcf..24311bf1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,4 +1,4 @@ -on: [push, workflow_dispatch] +on: [push, workflow_dispatch, pull_request] name: CI @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install nightly toolchain with clippy available uses: actions-rs/toolchain@v1 @@ -48,7 +48,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install nightly toolchain with rustfmt available uses: actions-rs/toolchain@v1 @@ -67,11 +67,14 @@ jobs: buildwasm: name: Build WASM & website - if: github.ref_name == 'main' runs-on: ubuntu-latest + env: + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + permissions: + contents: write steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install nightly toolchain uses: actions-rs/toolchain@v1 @@ -87,6 +90,12 @@ jobs: command: install args: -f wasm-bindgen-cli + - name: Install cargo-outdir + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-outdir + - name: Install binaryen run: sudo apt-get install binaryen @@ -96,17 +105,31 @@ jobs: node-version: "20.x" - name: Run npm install - run: npm install + run: | + npm install + npm i -g vite - name: Build - run: chmod +x build.sh && ./build.sh -pWV + run: | + chmod +x build.sh && ./build.sh -Wpo # wasm-opt breaks things currently so don't optimise + vite build --base=/hyperquark/$BRANCH_NAME/ + + - name: Move files to tmp + run: mv ./playground/dist /tmp/hq-dist + + - name: checkout gh-pages + uses: actions/checkout@v4 + with: + ref: gh-pages - - name: Move files for gh pages - run: mv ./playground/dist docs && cp docs/index.html docs/404.html + - name: move file to gh-pages + run: | + rm -rf ./$BRANCH_NAME + mv /tmp/hq-dist ./$BRANCH_NAME + #mv ./main/* ./ - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v5 with: branch: gh-pages - create_branch: true - push_options: '--force' + push_options: '--force-with-lease' diff --git a/README.md b/README.md index acebf265..d03c12bd 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Compile scratch projects to WASM - the `wasm32-unknown-unknown` target (`rustup target add wasm32-unknown-unknown`) - wasm-bindgen-cli (`cargo install -f wasm-bindgen-cli`) - wasm-opt (install binaryen using whatever package manager you use) +- `cargo-outdir` (`cargo install cargo-outdir`) ## Building From 753e2a2a974e8536cb8fe10cff0421375cf0d64d Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Tue, 18 Feb 2025 11:50:19 +0000 Subject: [PATCH 20/98] auto-track implemented blocks --- build.sh | 1 + js/imports.ts | 6 +++--- js/opcodes.js | 1 + opcodes.mjs | 5 +++++ playground/views/HomeView.vue | 28 +++++++++++++++++++++++++++- 5 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 js/opcodes.js create mode 100644 opcodes.mjs diff --git a/build.sh b/build.sh index 5153c1e9..04b1a96b 100755 --- a/build.sh +++ b/build.sh @@ -91,6 +91,7 @@ if [ $WASM = "1" ]; then wasm-bindgen target/wasm32-unknown-unknown/debug/hyperquark.wasm --out-dir=js/no-compiler fi mv $(cargo outdir --no-names --quiet)/imports.ts js/imports.ts + node opcodes.mjs fi if [ $WOPT = "1" ]; then echo running wasm-opt -Os... diff --git a/js/imports.ts b/js/imports.ts index 71513e9d..ccde9d23 100644 --- a/js/imports.ts +++ b/js/imports.ts @@ -7,8 +7,8 @@ import { say_int } from './looks/say_int.ts'; import { say_string } from './looks/say_string.ts'; import { say_float } from './looks/say_float.ts'; export const imports = { - operator: { join }, - cast: { string2float, int2string, float2string }, - looks: { say_int, say_string, say_float } + cast: { string2float, int2string, float2string }, + looks: { say_int, say_string, say_float }, + operator: { join } }; \ No newline at end of file diff --git a/js/opcodes.js b/js/opcodes.js new file mode 100644 index 00000000..ea764cd4 --- /dev/null +++ b/js/opcodes.js @@ -0,0 +1 @@ +export const opcodes = ["looks_say","operator_add","operator_join"]; \ No newline at end of file diff --git a/opcodes.mjs b/opcodes.mjs new file mode 100644 index 00000000..27deca13 --- /dev/null +++ b/opcodes.mjs @@ -0,0 +1,5 @@ +import { readFile, writeFile } from 'node:fs/promises'; + +let blocksRs = (await readFile('./src/ir/blocks.rs', 'utf8')); +const opcodes = [...new Set(blocksRs.match(/BlockOpcode::[a-z_]+? /g).map(op => op.replace('BlockOpcode::', '').trim()))]; +await writeFile('./js/opcodes.js', `export const opcodes = ${JSON.stringify(opcodes)};`); \ No newline at end of file diff --git a/playground/views/HomeView.vue b/playground/views/HomeView.vue index fe1f3d88..10059ef6 100644 --- a/playground/views/HomeView.vue +++ b/playground/views/HomeView.vue @@ -20,10 +20,18 @@ import ProjectInput from '../components/ProjectInput.vue' {{ project.name }} by {{ project.author }} +
+

Currently implemented blocks:

+
    +
  • + {{ opcode }} +
  • +
\ No newline at end of file diff --git a/playground/lib/project-runner.js b/playground/lib/project-runner.js index a0a0ba49..28f3689d 100644 --- a/playground/lib/project-runner.js +++ b/playground/lib/project-runner.js @@ -19,21 +19,11 @@ function createSkin(renderer, type, layer, ...params) { } const spriteInfoLen = 80; +let _setup = false; -// @ts-ignore -export default async ( - { framerate = 30, turbo, renderer, wasm_bytes, target_names, string_consts, project_json, assets } = { - framerate: 30, turbo: false, - } -) => { - if (debugModeStore.debug) window.open(URL.createObjectURL(new Blob([wasm_bytes], { type: 'application/wasm' }))); - const framerate_wait = Math.round(1000 / framerate); - let assert; - let exit; - let browser = false; - let output_div; - let text_div; - +function setup(renderer, project_json, assets, target_names) { + if (_setup) return; + _setup = true; renderer.getDrawable = id => renderer._allDrawables[id]; renderer.getSkin = id => renderer._allSkins[id]; renderer.createSkin = (type, layer, ...params) => createSkin(renderer, type, layer, ...params); @@ -55,7 +45,6 @@ export default async ( renderer.setLayerGroupOrdering(["background", "video", "pen", "sprite"]); //window.open(URL.createObjectURL(new Blob([wasm_bytes], { type: "octet/stream" }))); const pen_skin = createSkin(renderer, "pen", "pen")[0]; - let strings_tbl; const target_skins = project_json.targets.map((target, index) => { const realCostume = target.costumes[target.currentCostume]; @@ -73,6 +62,27 @@ export default async ( console.log(target_skins) sharedSetup(target_names, renderer); +} + +// @ts-ignore +export default async ( + { framerate = 30, turbo, renderer, wasm_bytes, target_names, string_consts, project_json, assets } = { + framerate: 30, turbo: false, + } +) => { + if (debugModeStore.debug) window.open(URL.createObjectURL(new Blob([wasm_bytes], { type: 'application/wasm' }))); + const framerate_wait = Math.round(1000 / framerate); + let assert; + let exit; + let browser = false; + let output_div; + let text_div; + + setup(renderer, project_json, assets, target_names); + + console.log('green flag setup complete') + + let strings_tbl; let updatePenColor; let start_time = 0; diff --git a/playground/lib/settings.js b/playground/lib/settings.js index 955b8bc8..a7b98562 100644 --- a/playground/lib/settings.js +++ b/playground/lib/settings.js @@ -9,10 +9,12 @@ window.defaultSettings = defaultSettings; // TODO: can this be automated somehow? const settings_type = { string_type: WasmStringType, + wasm_opt: "boolean", } const settings_descriptions = { - string_type: "How strings should be represented internally." + string_type: "How strings should be represented internally.", + wasm_opt: "Should we try to optimise generated WASM modules using wasm-opt?" } function settingsInfoFromType(type) { @@ -22,7 +24,7 @@ function settingsInfoFromType(type) { options: Object.keys(type).filter(key => typeof key === 'string' && !/\d+/.test(key)), enum_obj: type } - } else if (type === Boolean) { + } else if (type === "boolean") { return { type: "checkbox" } diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index 8e5e19aa..53e69c05 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -722,8 +722,8 @@ fn from_normal_block( BlockOpcode::control_repeat_until => { let condition_instructions = inputs(block_info, blocks, context, context.project()?)?; - let first_condition_instructions = Some(vec![]); - let setup_instructions = vec![]; + let first_condition_instructions = None; + let setup_instructions = vec![IrOpcode::hq_drop]; generate_loop( context.warp, &mut should_break, diff --git a/src/wasm/flags.rs b/src/wasm/flags.rs index f3edfcb5..10dd7fd1 100644 --- a/src/wasm/flags.rs +++ b/src/wasm/flags.rs @@ -19,10 +19,20 @@ impl Default for WasmStringType { /// compilation flags #[non_exhaustive] -#[derive(Default, Copy, Clone, Serialize, Deserialize)] +#[derive(Copy, Clone, Serialize, Deserialize)] #[wasm_bindgen] pub struct WasmFlags { pub string_type: WasmStringType, + pub wasm_opt: bool, +} + +impl Default for WasmFlags { + fn default() -> WasmFlags { + WasmFlags { + wasm_opt: true, + string_type: Default::default(), + } + } } #[wasm_bindgen] From b6ae18bbaa75d78a82d1549084787d5dd3509bad Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:14:40 +0100 Subject: [PATCH 90/98] fix warped control_repeat_until --- js/shared.ts | 1 + src/instructions/control/loop.rs | 14 ++++++++++++-- src/ir/blocks.rs | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/js/shared.ts b/js/shared.ts index 4c584757..fc3b0a8c 100644 --- a/js/shared.ts +++ b/js/shared.ts @@ -6,6 +6,7 @@ let _renderer; export function setup(new_target_names: Array, renderer: object) { _target_names = new_target_names; _target_bubbles = _target_names.map(_ => null); + console.log(_target_names, _target_bubbles) _renderer = renderer; _setup = true; } diff --git a/src/instructions/control/loop.rs b/src/instructions/control/loop.rs index ae4c628c..71cbb13f 100644 --- a/src/instructions/control/loop.rs +++ b/src/instructions/control/loop.rs @@ -9,6 +9,7 @@ pub struct Fields { pub first_condition: Option>, pub condition: Rc, pub body: Rc, + pub flip_if: bool, } pub fn wasm( @@ -18,6 +19,7 @@ pub fn wasm( first_condition, condition, body, + flip_if, }: &Fields, ) -> HQResult> { let inner_instructions = func.compile_inner_step(Rc::clone(body))?; @@ -28,10 +30,18 @@ pub fn wasm( Ok(wasm![Block(BlockType::Empty),] .into_iter() .chain(first_condition_instructions) - .chain(wasm![I32Eqz, BrIf(0), Loop(BlockType::Empty)]) + .chain(if *flip_if { + wasm![BrIf(0), Loop(BlockType::Empty)] + } else { + wasm![I32Eqz, BrIf(0), Loop(BlockType::Empty)] + }) .chain(inner_instructions) .chain(condition_instructions) - .chain(wasm![BrIf(0), End, End]) + .chain(if *flip_if { + wasm![I32Eqz, BrIf(0), End, End] + } else { + wasm![BrIf(0), End, End] + }) .collect()) } diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index 53e69c05..4f64da9b 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -389,6 +389,7 @@ fn generate_loop( first_condition: first_condition_step, condition: condition_step, body: substack_step, + flip_if, })]) .collect()) } From 55c712b6f1898ce5df7e4cd808e2812ff7e75316 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:46:12 +0100 Subject: [PATCH 91/98] don't use colons in directory names allows for hq tp be developed on more restrictive filesystems (such as the SD card on my phone) that don't like colons --- build.rs | 7 ++++++- js/{wasm:js-string => wasm-js-string}/concat.ts | 0 js/{wasm:js-string => wasm-js-string}/length.ts | 0 js/{wasm:js-string => wasm-js-string}/substring.ts | 0 src/instructions/tests.rs | 7 ++++++- 5 files changed, 12 insertions(+), 2 deletions(-) rename js/{wasm:js-string => wasm-js-string}/concat.ts (100%) rename js/{wasm:js-string => wasm-js-string}/length.ts (100%) rename js/{wasm:js-string => wasm-js-string}/substring.ts (100%) diff --git a/build.rs b/build.rs index 188dc5b2..9f115144 100644 --- a/build.rs +++ b/build.rs @@ -95,7 +95,12 @@ export const imports = {{ .iter() .map(|dir| { format!( - "\"{dir}\": {{ {} }}", + "\"{}\": {{ {} }}", + if dir == "wasm-js-string" { + "wasm:js-string" + } else { + dir + }, ts_paths .iter() .filter(|(d, _)| d == dir) diff --git a/js/wasm:js-string/concat.ts b/js/wasm-js-string/concat.ts similarity index 100% rename from js/wasm:js-string/concat.ts rename to js/wasm-js-string/concat.ts diff --git a/js/wasm:js-string/length.ts b/js/wasm-js-string/length.ts similarity index 100% rename from js/wasm:js-string/length.ts rename to js/wasm-js-string/length.ts diff --git a/js/wasm:js-string/substring.ts b/js/wasm-js-string/substring.ts similarity index 100% rename from js/wasm:js-string/substring.ts rename to js/wasm-js-string/substring.ts diff --git a/src/instructions/tests.rs b/src/instructions/tests.rs index aece6f5c..6aeca9a2 100644 --- a/src/instructions/tests.rs +++ b/src/instructions/tests.rs @@ -265,7 +265,12 @@ macro_rules! instructions_test { wasm_to_js_type(*params.get(i).unwrap()) ) }).collect::>().join(", "); - let path_buf = PathBuf::from(format!("js/{}/{}.ts", module, name)); + let module_path = if *module == "wasm:js-string" { + "wasm-js-string" + } else { + module + }; + let path_buf = PathBuf::from(format!("js/{}/{}.ts", module_path, name)); let diagnostics = check_js::<_, ezno_checker::synthesis::EznoParser>( vec![path_buf.clone()], vec![ts_defs.into()], From a32d96f3691fb80489c7e62a1c1f8b0058fe0c06 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Wed, 23 Apr 2025 17:21:42 +0100 Subject: [PATCH 92/98] move more flag logic to rust --- playground/lib/settings.js | 30 ++++++++++++------------------ src/wasm/flags.rs | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/playground/lib/settings.js b/playground/lib/settings.js index a7b98562..456eb462 100644 --- a/playground/lib/settings.js +++ b/playground/lib/settings.js @@ -1,3 +1,4 @@ +import * as hyperquarkExports from '../../js/no-compiler/hyperquark.js'; import { WasmFlags, WasmStringType } from '../../js/no-compiler/hyperquark.js'; export { WasmFlags }; @@ -5,28 +6,18 @@ const defaultSettings = new WasmFlags(); const defaultSettingsObj = defaultSettings.to_js(); window.defaultSettings = defaultSettings; - -// TODO: can this be automated somehow? -const settings_type = { - string_type: WasmStringType, - wasm_opt: "boolean", -} - -const settings_descriptions = { - string_type: "How strings should be represented internally.", - wasm_opt: "Should we try to optimise generated WASM modules using wasm-opt?" -} +window.hyperquarkExports = hyperquarkExports; function settingsInfoFromType(type) { - if (typeof type === "object") { + if (type === "boolean") { return { - type: "radio", - options: Object.keys(type).filter(key => typeof key === 'string' && !/\d+/.test(key)), - enum_obj: type + type: "checkbox" } - } else if (type === "boolean") { + } else if (type in hyperquarkExports) { return { - type: "checkbox" + type: "radio", + options: Object.keys(hyperquarkExports[type]).filter(key => typeof key === 'string' && !/\d+/.test(key)), + enum_obj: hyperquarkExports[type], } } else { return null; @@ -36,7 +27,10 @@ function settingsInfoFromType(type) { export const settingsInfo = Object.fromEntries(Object.entries(Object.getOwnPropertyDescriptors(WasmFlags.prototype)) .filter(([_, descriptor]) => typeof descriptor.get === 'function') .map(([key, _]) => key) - .map(key => [key, { ...settingsInfoFromType(settings_type[key]), description: settings_descriptions[key] }])); + .map(key => [key, { + ...settingsInfoFromType(WasmFlags.flag_type(key)), + description: WasmFlags.flag_descriptor(key) + }])); /** * @returns {WasmFlags} diff --git a/src/wasm/flags.rs b/src/wasm/flags.rs index 10dd7fd1..c3941bcb 100644 --- a/src/wasm/flags.rs +++ b/src/wasm/flags.rs @@ -54,4 +54,22 @@ impl WasmFlags { pub fn new() -> WasmFlags { Default::default() } + + #[wasm_bindgen] + pub fn flag_descriptor(flag: &str) -> String { + match flag { + "string_type" => "How strings should be represented internally.", + "wasm_opt" => "Should we try to optimise generated WASM modules using wasm-opt?", + _ => "", + }.into() + } + + #[wasm_bindgen] + pub fn flag_type(flag: &str) -> String { + match flag { + "string_type" => stringify!(WasmStringType), + "wasm_opt" => "boolean", + _ => "", + }.into() + } } From 7e8a61d3ae70be350896a7919d634a8fa43471d0 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:15:17 +0100 Subject: [PATCH 93/98] run wasm-opt in deploy with latest binaryen --- .github/workflows/deploy.yml | 3 +- playground/lib/settings.js | 11 ++-- playground/views/Settings.vue | 2 +- src/wasm/flags.rs | 108 ++++++++++++++++++++++++++++------ 4 files changed, 101 insertions(+), 23 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 279f35b0..99666212 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,10 +46,11 @@ jobs: run: | npm install npm i -g vite + npm i -g binaryen@nightly - name: Build run: | - chmod +x build.sh && ./build.sh -Wpo # wasm-opt breaks things currently so don't optimise + chmod +x build.sh && ./build.sh -Wpz vite build --base=/hyperquark/$BRANCH_NAME/ - name: Move files to tmp diff --git a/playground/lib/settings.js b/playground/lib/settings.js index 456eb462..50f417ac 100644 --- a/playground/lib/settings.js +++ b/playground/lib/settings.js @@ -27,10 +27,13 @@ function settingsInfoFromType(type) { export const settingsInfo = Object.fromEntries(Object.entries(Object.getOwnPropertyDescriptors(WasmFlags.prototype)) .filter(([_, descriptor]) => typeof descriptor.get === 'function') .map(([key, _]) => key) - .map(key => [key, { - ...settingsInfoFromType(WasmFlags.flag_type(key)), - description: WasmFlags.flag_descriptor(key) - }])); + .map(key => { + let flag_info = WasmFlags.flag_info(key).to_js() + return [key, { + ...flag_info, + ...settingsInfoFromType(flag_info.ty) + }] + })); /** * @returns {WasmFlags} diff --git a/playground/views/Settings.vue b/playground/views/Settings.vue index 5973d7ca..ab059b46 100644 --- a/playground/views/Settings.vue +++ b/playground/views/Settings.vue @@ -5,7 +5,7 @@
-

{{ id }}

+

{{ settingsInfo[id].name }}

{{ settingsInfo[id].description }}

diff --git a/src/wasm/flags.rs b/src/wasm/flags.rs index c3941bcb..cac65909 100644 --- a/src/wasm/flags.rs +++ b/src/wasm/flags.rs @@ -2,13 +2,12 @@ use crate::prelude::*; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; -#[non_exhaustive] #[derive(Copy, Clone, Serialize, Deserialize)] #[wasm_bindgen] pub enum WasmStringType { /// externref strings - this will automatically use the JS string builtins proposal if available ExternRef, - Manual, + //Manual, } impl Default for WasmStringType { @@ -18,7 +17,6 @@ impl Default for WasmStringType { } /// compilation flags -#[non_exhaustive] #[derive(Copy, Clone, Serialize, Deserialize)] #[wasm_bindgen] pub struct WasmFlags { @@ -35,6 +33,82 @@ impl Default for WasmFlags { } } +#[derive(Clone, Serialize, Deserialize)] +#[wasm_bindgen] +pub enum WasmFeature { + TypedFunctionReferences, +} + +#[derive(Clone, Serialize, Deserialize)] +#[wasm_bindgen] +pub struct FlagInfo { + /// a human-readable name for the flag + name: String, + description: String, + ty: String, + /// which WASM features does this flag rely on? + wasm_features: Vec, +} + +#[wasm_bindgen] +impl FlagInfo { + fn new() -> Self { + FlagInfo { + name: "".into(), + description: "".into(), + ty: "".into(), + wasm_features: vec![], + } + } + + fn with_name(mut self, name: &str) -> Self { + self.name = name.to_string(); + self + } + + fn with_description(mut self, description: &str) -> Self { + self.description = description.to_string(); + self + } + + fn with_ty(mut self, ty: &str) -> Self { + self.ty = ty.to_string(); + self + } + + fn with_wasm_features(mut self, wasm_features: Vec) -> Self { + self.wasm_features = wasm_features; + self + } + + #[wasm_bindgen] + pub fn name(&self) -> String { + self.name.clone() + } + + #[wasm_bindgen] + pub fn description(&self) -> String { + self.description.clone() + } + + #[wasm_bindgen] + pub fn ty(&self) -> String { + self.ty.clone() + } + + #[wasm_bindgen] + pub fn wasm_features(&self) -> Vec { + self.wasm_features.clone() + } + + #[allow(clippy::wrong_self_convention)] + #[wasm_bindgen] + pub fn to_js(&self) -> HQResult { + serde_wasm_bindgen::to_value(&self) + .map_err(|_| make_hq_bug!("couldn't convert FlagInfo to JsValue")) + } +} + #[wasm_bindgen] impl WasmFlags { #[wasm_bindgen] @@ -56,20 +130,20 @@ impl WasmFlags { } #[wasm_bindgen] - pub fn flag_descriptor(flag: &str) -> String { + pub fn flag_info(flag: &str) -> FlagInfo { match flag { - "string_type" => "How strings should be represented internally.", - "wasm_opt" => "Should we try to optimise generated WASM modules using wasm-opt?", - _ => "", - }.into() - } - - #[wasm_bindgen] - pub fn flag_type(flag: &str) -> String { - match flag { - "string_type" => stringify!(WasmStringType), - "wasm_opt" => "boolean", - _ => "", - }.into() + "string_type" => FlagInfo::new() + .with_name("Internal string representation") + .with_description( + "ExternRef - uses JavaScript strings with JS string builtins where available.", + ) + .with_ty(stringify!(WasmStringType)), + "wasm_opt" => FlagInfo::new() + .with_name("WASM optimisation") + .with_description("Should we try to optimise generated WASM modules using wasm-opt?") + .with_ty("boolean"), + _ => FlagInfo::new(), + } + .into() } } From 43a19493d62ae43af0616ad3341ea2187252aa60 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Mon, 28 Apr 2025 15:56:31 +0100 Subject: [PATCH 94/98] actually use call_ref when using typed_function references --- src/wasm/project.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/wasm/project.rs b/src/wasm/project.rs index 922b94bf..bed4a800 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -351,10 +351,8 @@ impl WasmProject { Loop(WasmBlockType::Empty), LocalGet(0), LocalGet(0), - CallIndirect { - type_index: step_func_ty, - table_index: threads_table_index, - }, + TableGet(threads_table_index), + CallRef(step_func_ty), LocalGet(0), I32Const(1), I32Add, From 0b6587fad0b30e4c278db0e8719b5ffe9da3329f Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Fri, 2 May 2025 15:54:03 +0100 Subject: [PATCH 95/98] add option to use old call_indirect scheduler, and fix binaryen better --- .github/workflows/deploy.yml | 2 + package.json | 3 +- playground/assets/base.css | 6 +- playground/assets/main.css | 2 +- playground/components/ProjectPlayer.vue | 22 +-- playground/lib/project-runner.js | 7 + playground/lib/settings.js | 45 ++++- playground/router/index.js | 6 +- playground/views/ProjectFileView.vue | 32 ++-- playground/views/Settings.vue | 107 ++++++++++-- src/instructions/hq/text.rs | 17 +- src/instructions/hq/yield.rs | 117 +++++++++---- src/instructions/input_switcher.rs | 42 +++-- src/instructions/tests.rs | 4 +- src/lib.rs | 2 + src/registry.rs | 19 ++ src/wasm.rs | 4 +- src/wasm/flags.rs | 144 ++++++++++------ src/wasm/func.rs | 14 ++ src/wasm/project.rs | 219 +++++++++++++++++------- src/wasm/tables.rs | 25 ++- wasm-gen/src/lib.rs | 7 +- 22 files changed, 632 insertions(+), 214 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 99666212..ff2306ea 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -49,6 +49,8 @@ jobs: npm i -g binaryen@nightly - name: Build + env: + VITE_HASH_HISTORY: true run: | chmod +x build.sh && ./build.sh -Wpz vite build --base=/hyperquark/$BRANCH_NAME/ diff --git a/package.json b/package.json index 32ffa71a..347d087e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "scratch-sb1-converter": "^0.2.7", "vite-plugin-wasm": "^3.2.2", "vue": "^3.3.4", - "vue-router": "^4.2.2" + "vue-router": "^4.2.2", + "wasm-feature-detect": "^1.8.0" }, "devDependencies": { "@vitejs/plugin-vue": "^4.2.3", diff --git a/playground/assets/base.css b/playground/assets/base.css index d3de42ec..c6aba862 100644 --- a/playground/assets/base.css +++ b/playground/assets/base.css @@ -50,6 +50,10 @@ } } +:root { + --transition-time: 0.4s; +} + *, *::before, *::after { @@ -70,4 +74,4 @@ body { text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -} +} \ No newline at end of file diff --git a/playground/assets/main.css b/playground/assets/main.css index 63e8285d..c5a08b50 100644 --- a/playground/assets/main.css +++ b/playground/assets/main.css @@ -15,7 +15,7 @@ a, .green { text-decoration: none; color: hsl(39.3,100%,37%); - transition: 0.4s; + transition: var(--transition-time); } @media (hover: hover) { diff --git a/playground/components/ProjectPlayer.vue b/playground/components/ProjectPlayer.vue index c6e69576..a28abca1 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -18,7 +18,6 @@ \ No newline at end of file diff --git a/playground/views/Settings.vue b/playground/views/Settings.vue index ab059b46..9f6a0ee2 100644 --- a/playground/views/Settings.vue +++ b/playground/views/Settings.vue @@ -3,35 +3,76 @@

Compiler settings

-
-
-

{{ settingsInfo[id].name }}

-

{{ settingsInfo[id].description }}

-
-
- - - {{ option }}
-
+ +

Currently used WASM features:

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

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

+
+

+
+ + + {{ option }}
+
+
\ No newline at end of file diff --git a/src/instructions/hq/text.rs b/src/instructions/hq/text.rs index cbcaa8e8..22ef836e 100644 --- a/src/instructions/hq/text.rs +++ b/src/instructions/hq/text.rs @@ -1,3 +1,5 @@ +use crate::wasm::TableOptions; + use super::super::prelude::*; use wasm_encoder::RefType; @@ -15,11 +17,16 @@ pub fn wasm( .register_default(fields.0.clone())?; Ok(wasm![ I32Const(string_idx), - TableGet( - func.registries() - .tables() - .register("strings".into(), (RefType::EXTERNREF, 0, None))?, - ), + TableGet(func.registries().tables().register( + "strings".into(), + TableOptions { + element_type: RefType::EXTERNREF, + min: 0, + max: None, + // this default gets fixed up in src/wasm/tables.rs + init: None, + } + )?,), ]) } diff --git a/src/instructions/hq/yield.rs b/src/instructions/hq/yield.rs index 2e7981ba..3f7f9ae4 100644 --- a/src/instructions/hq/yield.rs +++ b/src/instructions/hq/yield.rs @@ -1,7 +1,8 @@ use super::super::prelude::*; use crate::ir::Step; -use crate::wasm::{GlobalExportable, GlobalMutable, StepFunc}; -use wasm_encoder::{ConstExpr, HeapType}; +use crate::wasm::TableOptions; +use crate::wasm::{flags::Scheduler, GlobalExportable, GlobalMutable, StepFunc}; +use wasm_encoder::{ConstExpr, HeapType, MemArg}; #[derive(Clone, Debug)] pub enum YieldMode { @@ -42,18 +43,6 @@ pub fn wasm( .registries() .types() .register_default((vec![ValType::I32], vec![]))?; - let threads_table = func.registries().tables().register( - "threads".into(), - ( - RefType { - nullable: false, - heap_type: HeapType::Concrete(step_func_ty), - }, - 0, - // this default gets fixed up in src/wasm/tables.rs - None, - ), - )?; let threads_count = func.registries().globals().register( "threads_count".into(), @@ -66,27 +55,95 @@ pub fn wasm( )?; Ok(match yield_mode { - YieldMode::None => wasm![ - LocalGet(0), - GlobalGet(noop_global), - TableSet(threads_table), - GlobalGet(threads_count), - I32Const(1), - I32Sub, - GlobalSet(threads_count), - Return - ], + YieldMode::None => match func.flags().scheduler { + Scheduler::CallIndirect => { + // Write a special value (e.g. 0 for noop) to linear memory for this thread + wasm![ + LocalGet(0), // thread index + I32Const(4), + I32Mul, + I32Const(0), // 0 = noop step index + // store at address (thread_index * 4) + I32Store(MemArg { + offset: 0, + align: 2, + memory_index: 0, + }), + // GlobalGet(threads_count), + // I32Const(1), + // I32Sub, + // GlobalSet(threads_count), + Return + ] + } + Scheduler::TypedFuncRef => { + let threads_table = func.registries().tables().register( + "threads".into(), + TableOptions { + element_type: RefType { + nullable: false, + heap_type: HeapType::Concrete(step_func_ty), + }, + min: 0, + max: None, + // this default gets fixed up in src/wasm/tables.rs + init: None, + }, + )?; + wasm![ + LocalGet(0), + GlobalGet(noop_global), + TableSet(threads_table), + GlobalGet(threads_count), + I32Const(1), + I32Sub, + GlobalSet(threads_count), + Return + ] + } + }, YieldMode::Inline(step) => func.compile_inner_step(Rc::clone(step))?, YieldMode::Schedule(weak_step) => { let step = Weak::upgrade(weak_step).ok_or(make_hq_bug!("couldn't upgrade Weak"))?; step.make_used_non_inline()?; - wasm![ - LocalGet(0), - #LazyStepRef(Weak::clone(weak_step)), - TableSet(threads_table), - Return - ] + match func.flags().scheduler { + Scheduler::CallIndirect => { + wasm![ + LocalGet(0), // thread index + I32Const(4), + I32Mul, + #LazyStepIndex(Weak::clone(weak_step)), + I32Store(MemArg { + offset: 0, + align: 2, + memory_index: 0, + }), + Return + ] + } + Scheduler::TypedFuncRef => { + let threads_table = func.registries().tables().register( + "threads".into(), + TableOptions { + element_type: RefType { + nullable: false, + heap_type: HeapType::Concrete(step_func_ty), + }, + min: 0, + max: None, + // this default gets fixed up in src/wasm/tables.rs + init: None, + }, + )?; + wasm![ + LocalGet(0), + #LazyStepRef(Weak::clone(weak_step)), + TableSet(threads_table), + Return + ] + } + } } _ => hq_todo!(), }) diff --git a/src/instructions/input_switcher.rs b/src/instructions/input_switcher.rs index 47f51801..8e10d2ae 100644 --- a/src/instructions/input_switcher.rs +++ b/src/instructions/input_switcher.rs @@ -5,6 +5,7 @@ use super::HqCastFields; use super::IrOpcode; use crate::wasm::GlobalExportable; use crate::wasm::GlobalMutable; +use crate::wasm::TableOptions; use crate::wasm::WasmProject; use crate::{ir::Type as IrType, wasm::StepFunc}; use itertools::Itertools; @@ -88,10 +89,16 @@ fn generate_branches( I32WrapI64, ], IrType::String => { - let table_index = func - .registries() - .tables() - .register("strings".into(), (RefType::EXTERNREF, 0, None))?; + let table_index = func.registries().tables().register( + "strings".into(), + TableOptions { + element_type: RefType::EXTERNREF, + min: 0, + // TODO: use js string imports for preknown strings + max: None, + init: None, + }, + )?; wasm![ I64Const(BOXED_STRING_PATTERN), I64And, @@ -111,10 +118,16 @@ fn generate_branches( IrType::Float => wasm![Else, LocalGet(local_idx), F64ReinterpretI64], // float guaranteed to be last so no need to check IrType::QuasiInt => wasm![Else, LocalGet(local_idx), I32WrapI64], IrType::String => { - let table_index = func - .registries() - .tables() - .register("strings".into(), (RefType::EXTERNREF, 0, None))?; + let table_index = func.registries().tables().register( + "strings".into(), + TableOptions { + element_type: RefType::EXTERNREF, + min: 0, + // TODO: use js string imports for preknown strings + max: None, + init: None, + }, + )?; wasm![Else, LocalGet(local_idx), I32WrapI64, TableGet(table_index)] } _ => unreachable!(), @@ -135,10 +148,15 @@ fn generate_branches( I32WrapI64, ], IrType::String => { - let table_index = func - .registries() - .tables() - .register("strings".into(), (RefType::EXTERNREF, 0, None))?; + let table_index = func.registries().tables().register( + "strings".into(), + TableOptions { + element_type: RefType::EXTERNREF, + min: 0, + max: None, + init: None, + }, + )?; wasm![ Else, LocalGet(local_idx), diff --git a/src/instructions/tests.rs b/src/instructions/tests.rs index 6aeca9a2..f3afdbc8 100644 --- a/src/instructions/tests.rs +++ b/src/instructions/tests.rs @@ -15,7 +15,7 @@ #[macro_export] macro_rules! instructions_test { {$module:ident; $opcode:ident; $($type_arg:ident $(,)?)* $(@$fields:expr)? $(;)?} => { - $crate::instructions_test!{$module; $opcode; $($type_arg,)* $(@$fields)? ; Default::default()} + $crate::instructions_test!{$module; $opcode; $($type_arg,)* $(@$fields)? ; WasmFlags::new(all_wasm_features())} }; {$module:ident; $opcode:ident; $($type_arg:ident $(,)?)* $(@ $fields:expr)? ; $flags:expr} => { #[cfg(test)] @@ -31,7 +31,7 @@ macro_rules! instructions_test { CodeSection, ExportSection, FunctionSection, GlobalSection, HeapType, ImportSection, Module, RefType, TableSection, TypeSection, MemorySection, MemoryType, ValType, }; - use $crate::wasm::{StepFunc, Registries, WasmProject}; + use $crate::wasm::{flags::all_wasm_features, StepFunc, Registries, WasmProject, WasmFlags}; #[allow(unused)] macro_rules! ident_as_irtype { diff --git a/src/lib.rs b/src/lib.rs index 6c80471e..fc315a9f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,8 @@ pub mod prelude { use indexmap; pub type IndexMap = indexmap::IndexMap>; pub type IndexSet = indexmap::IndexSet>; + + pub use itertools::Itertools; } use prelude::*; diff --git a/src/registry.rs b/src/registry.rs index 49daf754..5e84e397 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -84,6 +84,25 @@ pub trait Registry: Sized { ) .map_err(|_| make_hq_bug!("registry item index out of bounds")) } + + fn register_override(&self, key: Self::Key, value: Self::Value) -> HQResult + where + N: TryFrom, + >::Error: alloc::fmt::Debug, + { + self.registry() + .try_borrow_mut() + .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? + .entry(key.clone()) + .insert_entry(value); + N::try_from( + self.registry() + .try_borrow()? + .get_index_of(&key) + .ok_or(make_hq_bug!("couldn't find entry in Registry"))?, + ) + .map_err(|_| make_hq_bug!("registry item index out of bounds")) + } } pub trait RegistryDefault: Registry { diff --git a/src/wasm.rs b/src/wasm.rs index 103defd5..59b24c4d 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -1,5 +1,5 @@ mod external; -mod flags; +pub mod flags; mod func; mod globals; mod project; @@ -18,6 +18,6 @@ pub(crate) use globals::{ pub(crate) use project::{FinishedWasm, WasmProject}; pub(crate) use registries::Registries; pub(crate) use strings::StringRegistry; -pub(crate) use tables::TableRegistry; +pub(crate) use tables::{TableOptions, TableRegistry}; pub(crate) use type_registry::TypeRegistry; pub(crate) use variable::VariableRegistry; diff --git a/src/wasm/flags.rs b/src/wasm/flags.rs index cac65909..80b225d5 100644 --- a/src/wasm/flags.rs +++ b/src/wasm/flags.rs @@ -5,49 +5,60 @@ use wasm_bindgen::prelude::*; #[derive(Copy, Clone, Serialize, Deserialize)] #[wasm_bindgen] pub enum WasmStringType { - /// externref strings - this will automatically use the JS string builtins proposal if available ExternRef, + JsStringBuiltins, //Manual, } -impl Default for WasmStringType { - fn default() -> Self { - Self::ExternRef - } -} - -/// compilation flags -#[derive(Copy, Clone, Serialize, Deserialize)] +#[derive(Copy, Clone, Serialize, Deserialize, PartialEq)] #[wasm_bindgen] -pub struct WasmFlags { - pub string_type: WasmStringType, - pub wasm_opt: bool, +pub enum WasmOpt { + On, + Off, } -impl Default for WasmFlags { - fn default() -> WasmFlags { - WasmFlags { - wasm_opt: true, - string_type: Default::default(), - } - } +#[derive(Copy, Clone, Serialize, Deserialize, PartialEq)] +#[wasm_bindgen] +pub enum Scheduler { + TypedFuncRef, + CallIndirect, } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] #[wasm_bindgen] pub enum WasmFeature { + ReferenceTypes, TypedFunctionReferences, + JSStringBuiltins, } -#[derive(Clone, Serialize, Deserialize)] #[wasm_bindgen] +pub fn all_wasm_features() -> Vec { + use WasmFeature::*; + vec![ReferenceTypes, TypedFunctionReferences, JSStringBuiltins] +} + +// no &self because wasm_bidgen doesn't like it +#[wasm_bindgen] +pub fn wasm_feature_detect_name(feat: WasmFeature) -> String { + use WasmFeature::*; + match feat { + ReferenceTypes => "referenceTypes", + TypedFunctionReferences => "typedFunctionReferences", + JSStringBuiltins => "jsStringBuiltins", + } + .into() +} + +#[derive(Clone, Serialize, Deserialize)] +#[wasm_bindgen(getter_with_clone)] pub struct FlagInfo { /// a human-readable name for the flag - name: String, - description: String, - ty: String, + pub name: String, + pub description: String, + pub ty: String, /// which WASM features does this flag rely on? - wasm_features: Vec, + wasm_features: BTreeMap>, } #[wasm_bindgen] @@ -57,7 +68,7 @@ impl FlagInfo { name: "".into(), description: "".into(), ty: "".into(), - wasm_features: vec![], + wasm_features: Default::default(), } } @@ -76,29 +87,14 @@ impl FlagInfo { self } - fn with_wasm_features(mut self, wasm_features: Vec) -> Self { + fn with_wasm_features(mut self, wasm_features: BTreeMap>) -> Self { self.wasm_features = wasm_features; self } #[wasm_bindgen] - pub fn name(&self) -> String { - self.name.clone() - } - - #[wasm_bindgen] - pub fn description(&self) -> String { - self.description.clone() - } - - #[wasm_bindgen] - pub fn ty(&self) -> String { - self.ty.clone() - } - - #[wasm_bindgen] - pub fn wasm_features(&self) -> Vec { - self.wasm_features.clone() + pub fn wasm_features(&self, flag: String) -> Option> { + self.wasm_features.get(&flag).cloned() } #[allow(clippy::wrong_self_convention)] @@ -109,6 +105,29 @@ impl FlagInfo { } } +macro_rules! stringmap { + ($($k:ident : $v:expr),+ $(,)?) => {{ + BTreeMap::from([$((String::from(stringify!($k)), $v),)+]) + }} +} + +/// stringifies the name of a type whilst ensuring that the type is valid +macro_rules! ty_str { + ($ty:ty) => {{ + let _ = core::any::TypeId::of::<$ty>(); // forces the type to be valid + stringify!($ty) + }}; +} + +/// compilation flags +#[derive(Copy, Clone, Serialize, Deserialize)] +#[wasm_bindgen] +pub struct WasmFlags { + pub string_type: WasmStringType, + pub wasm_opt: WasmOpt, + pub scheduler: Scheduler, +} + #[wasm_bindgen] impl WasmFlags { #[wasm_bindgen] @@ -125,8 +144,17 @@ impl WasmFlags { } #[wasm_bindgen(constructor)] - pub fn new() -> WasmFlags { - Default::default() + pub fn new(wasm_features: Vec) -> WasmFlags { + crate::log(format!("{:?}", wasm_features).as_str()); + WasmFlags { + wasm_opt: WasmOpt::On, + string_type: if wasm_features.contains(&WasmFeature::JSStringBuiltins) { + WasmStringType::JsStringBuiltins + } else { + WasmStringType::ExternRef + }, + scheduler: Scheduler::CallIndirect, + } } #[wasm_bindgen] @@ -135,15 +163,29 @@ impl WasmFlags { "string_type" => FlagInfo::new() .with_name("Internal string representation") .with_description( - "ExternRef - uses JavaScript strings with JS string builtins where available.", + "ExternRef - uses JavaScript strings.\ +
\ + JsStringBuiltins (recommended) - uses JavaScript strings with the JS String Builtins proposal", ) - .with_ty(stringify!(WasmStringType)), + .with_ty(ty_str!(WasmStringType)) + .with_wasm_features(stringmap! { + ExternRef : vec![WasmFeature::ReferenceTypes], + JsStringBuiltins : vec![WasmFeature::ReferenceTypes, WasmFeature::JSStringBuiltins], + }), "wasm_opt" => FlagInfo::new() .with_name("WASM optimisation") .with_description("Should we try to optimise generated WASM modules using wasm-opt?") - .with_ty("boolean"), - _ => FlagInfo::new(), + .with_ty(ty_str!(WasmOpt)), + "scheduler" => FlagInfo::new() + .with_name("Scheduler") + .with_description("TypedFuncRef - uses typed function references to eliminate runtime checks.\ +
\ + CallIndirect - stores function indices, then uses CallIndirect to call them.") + .with_ty(ty_str!(Scheduler)) + .with_wasm_features(stringmap! { + TypedFuncRef : vec![WasmFeature::TypedFunctionReferences] + }), + _ => FlagInfo::new().with_name(format!("unknown setting '{flag}'").as_str()) } - .into() } } diff --git a/src/wasm/func.rs b/src/wasm/func.rs index 20f49970..3cbd4953 100644 --- a/src/wasm/func.rs +++ b/src/wasm/func.rs @@ -11,6 +11,7 @@ use wasm_encoder::{ pub enum Instruction { Immediate(wasm_encoder::Instruction<'static>), LazyStepRef(Weak), + LazyStepIndex(Weak), LazyWarpedProcCall(Rc), } @@ -35,6 +36,19 @@ impl Instruction { .map_err(|_| make_hq_bug!("step index out of bounds"))?; WInstruction::RefFunc(imported_func_count + step_index) } + Instruction::LazyStepIndex(step) => { + let step_index: i32 = steps + .try_borrow()? + .get_index_of( + &step + .upgrade() + .ok_or(make_hq_bug!("couldn't upgrade Weak"))?, + ) + .ok_or(make_hq_bug!("couldn't find step in step map"))? + .try_into() + .map_err(|_| make_hq_bug!("step index out of bounds"))?; + WInstruction::I32Const(step_index) + } Instruction::LazyWarpedProcCall(proc) => { let PartialStep::Finished(ref step) = *proc.warped_first_step()? else { hq_bug!("tried to use uncompiled warped procedure step") diff --git a/src/wasm/project.rs b/src/wasm/project.rs index bed4a800..33cd64f5 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -1,4 +1,5 @@ -use super::{ExternalEnvironment, GlobalExportable, GlobalMutable, Registries}; +use super::flags::Scheduler; +use super::{ExternalEnvironment, GlobalExportable, GlobalMutable, Registries, TableOptions}; use crate::ir::{Event, IrProject, Step, Type as IrType}; use crate::prelude::*; use crate::wasm::{StepFunc, WasmFlags}; @@ -7,7 +8,7 @@ use wasm_bindgen::prelude::*; use wasm_encoder::{ BlockType as WasmBlockType, CodeSection, ConstExpr, ElementSection, Elements, ExportKind, ExportSection, Function, FunctionSection, GlobalSection, HeapType, ImportSection, Instruction, - MemorySection, MemoryType, Module, RefType, TableSection, TypeSection, ValType, + MemArg, MemorySection, MemoryType, Module, RefType, TableSection, TypeSection, ValType, }; use wasm_gen::wasm; @@ -96,17 +97,18 @@ impl WasmProject { let strings = self.registries().strings().clone().finish(); - self.registries().tables().register::( + self.registries().tables().register_override::( "strings".into(), - ( - RefType::EXTERNREF, - strings + TableOptions { + element_type: RefType::EXTERNREF, + min: strings .len() .try_into() .map_err(|_| make_hq_bug!("strings length out of bounds"))?, // TODO: use js string imports for preknown strings - None, - ), + max: None, + init: None, + }, )?; self.registries() @@ -128,6 +130,37 @@ impl WasmProject { self.unreachable_dbg_func(&mut functions, &mut codes, &mut exports)?; + match self.flags.scheduler { + Scheduler::CallIndirect => { + let step_count = self.steps().try_borrow()?.len() as u64; + // use register_override just in case we've accidentally defined the threads table elsewhere + let steps_table_index = self.registries().tables().register_override( + "steps".into(), + TableOptions { + element_type: RefType::FUNCREF, + min: step_count, + max: Some(step_count), + init: None, + }, + )?; + let func_indices: Vec = (0..step_count) + .map(|i| self.imported_func_count().unwrap() + i as u32) + .collect(); + elements.active( + Some(steps_table_index), + &ConstExpr::i32_const(0), + Elements::Functions(func_indices.into()), + ); + } + Scheduler::TypedFuncRef => { + elements.declared(Elements::Functions( + (self.imported_func_count()? + ..functions.len() + self.imported_func_count()? - 2) + .collect(), + )); + } + } + self.registries().types().clone().finish(&mut types); self.registries() @@ -135,11 +168,6 @@ impl WasmProject { .clone() .finish(&imports, &mut tables, &mut exports); - elements.declared(Elements::Functions( - (self.imported_func_count()?..functions.len() + self.imported_func_count()? - 2) - .collect(), - )); - exports.export("memory", ExportKind::Memory, 0); exports.export("noop", ExportKind::Func, self.imported_func_count()?); @@ -218,19 +246,39 @@ impl WasmProject { .registries() .types() .register_default((vec![ValType::I32], vec![]))?; + match self.flags.scheduler { + Scheduler::TypedFuncRef => self.registries().tables().register( + "threads".into(), + TableOptions { + element_type: RefType { + nullable: false, + heap_type: HeapType::Concrete(step_func_ty), + }, + min: 0, + max: None, + // default to noop, just so the module validates. + init: Some(ConstExpr::ref_func(self.imported_func_count()?)), + }), + Scheduler::CallIndirect => hq_bug!("tried to access threads_table_index outside of `WasmProject::Finish` when the scheduler is not TypedFuncRef") + } + } - self.registries().tables().register( - "threads".into(), - ( - RefType { - nullable: false, - heap_type: HeapType::Concrete(step_func_ty), - }, - 0, - // default to noop, just so the module validates. - Some(ConstExpr::ref_func(self.imported_func_count()?)), - ), - ) + fn steps_table_index(&self) -> HQResult + where + N: TryFrom, + >::Error: alloc::fmt::Debug, + { + match self.flags.scheduler { + Scheduler::CallIndirect => self.registries().tables().register( + "steps".into(), + TableOptions { + element_type: RefType::FUNCREF, + min: 0, + max: None, + init: None + }), + Scheduler::TypedFuncRef => hq_bug!("tried to access steps_table_index outside of `WasmProject::Finish` when the scheduler is not CallIndirect") + } } fn finish_event( @@ -243,8 +291,6 @@ impl WasmProject { ) -> HQResult<()> { let mut func = Function::new(vec![]); - let threads_table_index = self.threads_table_index()?; - let threads_count = self.registries().globals().register( "threads_count".into(), ( @@ -257,7 +303,8 @@ impl WasmProject { let instrs = indices .iter() - .map(|i| { + .enumerate() + .map(|(position, &i)| { crate::log( format!( "event step idx: {}; func idx: {}", @@ -266,12 +313,28 @@ impl WasmProject { ) .as_str(), ); - Ok(wasm![ - RefFunc(i + self.imported_func_count()?), - I32Const(1), - TableGrow(threads_table_index), - Drop, - ]) + Ok(match self.flags.scheduler { + Scheduler::TypedFuncRef => wasm![ + RefFunc(i + self.imported_func_count()?), + I32Const(1), + TableGrow(self.threads_table_index()?), + Drop, + ], + Scheduler::CallIndirect => wasm![ + GlobalGet(threads_count), + I32Const(4), + I32Mul, + I32Const( + i.try_into() + .map_err(|_| make_hq_bug!("step index out of bounds"))? + ), + I32Store(MemArg { + offset: position as u64 * 4, + align: 2, + memory_index: 0, + }), + ], + }) }) .flatten_ok() .collect::>>()?; @@ -341,27 +404,67 @@ impl WasmProject { .types() .register_default((vec![ValType::I32], vec![]))?; - let threads_table_index = self.threads_table_index()?; - - let instructions = wasm![ - TableSize(threads_table_index), - LocalTee(1), - I32Eqz, - BrIf(0), - Loop(WasmBlockType::Empty), - LocalGet(0), - LocalGet(0), - TableGet(threads_table_index), - CallRef(step_func_ty), - LocalGet(0), - I32Const(1), - I32Add, - LocalTee(0), - LocalGet(1), - I32LtS, - BrIf(0), - End, - ]; + let threads_count = self.registries().globals().register( + "threads_count".into(), + ( + ValType::I32, + ConstExpr::i32_const(0), + GlobalMutable(true), + GlobalExportable(true), + ), + )?; + + let instructions = match self.flags.scheduler { + crate::wasm::flags::Scheduler::CallIndirect => wasm![ + // For call_indirect: read step indices from linear memory and call_indirect + GlobalGet(threads_count), + LocalTee(1), + I32Eqz, + BrIf(0), + Loop(WasmBlockType::Empty), + LocalGet(0), + LocalGet(0), // thread index + I32Const(4), // 4 bytes per index (i32) + I32Mul, + I32Load(MemArg { + offset: 0, + align: 2, + memory_index: 0, + }), // load step index from memory + CallIndirect { + type_index: step_func_ty, + table_index: self.steps_table_index()?, + }, + LocalGet(0), + I32Const(1), + I32Add, + LocalTee(0), + LocalGet(1), + I32LtS, + BrIf(0), + End, + ], + _ => wasm![ + // Default: typed function references (call_ref) + TableSize(self.threads_table_index()?), + LocalTee(1), + I32Eqz, + BrIf(0), + Loop(WasmBlockType::Empty), + LocalGet(0), + LocalGet(0), + TableGet(self.threads_table_index()?), + CallRef(step_func_ty), + LocalGet(0), + I32Const(1), + I32Add, + LocalTee(0), + LocalGet(1), + I32LtS, + BrIf(0), + End, + ], + }; for instr in instructions { tick_func.instruction(&instr.eval(self.steps(), self.imported_func_count()?)?); } @@ -450,7 +553,7 @@ mod tests { use super::{Registries, WasmProject}; use crate::ir::Step; use crate::prelude::*; - use crate::wasm::{ExternalEnvironment, StepFunc}; + use crate::wasm::{flags::all_wasm_features, ExternalEnvironment, StepFunc, WasmFlags}; #[test] fn empty_project_is_valid_wasm() { @@ -460,11 +563,11 @@ mod tests { Rc::new(Step::new_empty()), Rc::clone(&steps), Rc::clone(®istries), - Default::default(), + WasmFlags::new(all_wasm_features()), ) .unwrap(); let project = WasmProject { - flags: Default::default(), + flags: WasmFlags::new(all_wasm_features()), steps, events: BTreeMap::new(), environment: ExternalEnvironment::WebBrowser, diff --git a/src/wasm/tables.rs b/src/wasm/tables.rs index 08d7910f..2580886e 100644 --- a/src/wasm/tables.rs +++ b/src/wasm/tables.rs @@ -4,7 +4,15 @@ use wasm_encoder::{ ConstExpr, ExportKind, ExportSection, ImportSection, RefType, TableSection, TableType, }; -pub type TableRegistry = MapRegistry, (RefType, u64, Option)>; +#[derive(Clone, Debug)] +pub struct TableOptions { + pub element_type: RefType, + pub min: u64, + pub max: Option, + pub init: Option, +} + +pub type TableRegistry = MapRegistry, TableOptions>; impl TableRegistry { pub fn finish( @@ -13,7 +21,16 @@ impl TableRegistry { tables: &mut TableSection, exports: &mut ExportSection, ) { - for (key, (element_type, min, init)) in self.registry().take() { + for ( + key, + TableOptions { + element_type, + min, + max, + init, + }, + ) in self.registry().take() + { // TODO: allow choosing whether to export a table or not? exports.export(&key, ExportKind::Table, tables.len()); let init = match &*key { @@ -25,7 +42,7 @@ impl TableRegistry { TableType { element_type, minimum: min, - maximum: None, + maximum: max, table64: false, shared: false, }, @@ -35,7 +52,7 @@ impl TableRegistry { tables.table(TableType { element_type, minimum: min, - maximum: None, + maximum: max, table64: false, shared: false, }); diff --git a/wasm-gen/src/lib.rs b/wasm-gen/src/lib.rs index 91aa836f..32cb3d33 100644 --- a/wasm-gen/src/lib.rs +++ b/wasm-gen/src/lib.rs @@ -260,7 +260,12 @@ pub fn wasm(input: TokenStream) -> TokenStream { let __strings_table_index: u32 = func .registries() .tables() - .register("strings".into(), (RefType::EXTERNREF, 0, None))?; + .register("strings".into(), crate::wasm::TableOptions { + element_type: RefType::EXTERNREF, + min: 0, + max: None, + init: None, + })?; #(#conditions) else * else { unreachable!() } From 03000d1ccea80481ab5962a6512907fdc49bcf18 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sat, 3 May 2025 10:35:33 +0100 Subject: [PATCH 96/98] fix binaryen dependency name i didn't notice this locally because i didn't run `npm i` after changing package.json :( --- 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 a28abca1..308d6220 100644 --- a/playground/components/ProjectPlayer.vue +++ b/playground/components/ProjectPlayer.vue @@ -69,7 +69,7 @@ try { try { console.log('loading binaryen...'); console.log(getSettings().to_js().scheduler) - let binaryen = (await import('binaryen-with-extras')).default; // only load binaryen if it's used. + let binaryen = (await import('binaryen')).default; // only load binaryen if it's used. console.log('optimising using wasm-opt...') errorStage.value = "optimising"; errorMode.value = "A warning"; From 0b32a4a82f9eab72d9381a18079fbce4a228019a Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sat, 3 May 2025 12:37:58 +0100 Subject: [PATCH 97/98] add rust-toolchain.toml --- rust-toolchain.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 rust-toolchain.toml diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..271800cb --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file From c52ee5d53b08b80667ab39bc15c59712a87cab87 Mon Sep 17 00:00:00 2001 From: pufferfish101007 <50246616+pufferfish101007@users.noreply.github.com> Date: Sat, 3 May 2025 12:41:30 +0100 Subject: [PATCH 98/98] mega lint --- .gitignore | 3 +- build.rs | 27 +- clippy.toml | 4 + src/error.rs | 30 +- src/instructions.rs | 17 +- src/instructions/control/if_else.rs | 4 +- src/instructions/control/loop.rs | 12 +- src/instructions/data/setvariableto.rs | 7 +- src/instructions/data/teevariable.rs | 7 +- src/instructions/data/variable.rs | 7 +- src/instructions/hq/cast.rs | 10 +- src/instructions/hq/float.rs | 5 + src/instructions/hq/integer.rs | 5 + src/instructions/hq/text.rs | 10 +- src/instructions/hq/yield.rs | 8 +- src/instructions/input_switcher.rs | 264 +++--- src/instructions/looks/say.rs | 8 +- src/instructions/looks/think.rs | 8 +- src/instructions/operator/add.rs | 1 + src/instructions/operator/divide.rs | 1 + src/instructions/operator/equals.rs | 1 + src/instructions/operator/gt.rs | 8 +- src/instructions/operator/multiply.rs | 1 + src/instructions/operator/subtract.rs | 5 +- src/instructions/procedures/argument.rs | 2 +- src/instructions/tests.rs | 46 +- src/ir.rs | 13 +- src/ir/blocks.rs | 1147 +++++++++++++---------- src/ir/context.rs | 2 +- src/ir/proc.rs | 94 +- src/ir/project.rs | 28 +- src/ir/step.rs | 52 +- src/ir/target.rs | 10 +- src/ir/thread.rs | 20 +- src/ir/types.rs | 51 +- src/ir/variable.rs | 9 +- src/lib.rs | 43 +- src/optimisation.rs | 4 +- src/optimisation/var_types.rs | 2 +- src/registry.rs | 20 +- src/sb3.rs | 19 +- src/wasm.rs | 20 +- src/wasm/flags.rs | 53 +- src/wasm/func.rs | 76 +- src/wasm/globals.rs | 8 +- src/wasm/project.rs | 74 +- src/wasm/registries.rs | 22 +- src/wasm/strings.rs | 2 +- src/wasm/tables.rs | 4 +- src/wasm/variable.rs | 6 +- 50 files changed, 1240 insertions(+), 1040 deletions(-) create mode 100644 clippy.toml diff --git a/.gitignore b/.gitignore index ec52615e..e64f1a39 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ /node_modules/ /package-lock.json /playground/dist/ -/.vite/ \ No newline at end of file +/.vite/ +/.vscode/ \ No newline at end of file diff --git a/build.rs b/build.rs index 9f115144..15699880 100644 --- a/build.rs +++ b/build.rs @@ -36,7 +36,7 @@ fn main() { .unwrap(); let fields = contents.contains("pub struct Fields"); let fields_name = - format!("{}_{}_fields", category, opcode).to_case(Case::Pascal); + format!("{category}_{opcode}_fields").to_case(Case::Pascal); paths.push(( format!( "{}::{}", @@ -46,7 +46,7 @@ fn main() { _ => opcode.to_string(), } ), - format!("{}_{}", category, opcode,), + format!("{category}_{opcode}",), fields, fields_name, )); @@ -120,7 +120,7 @@ export const imports = {{ format!( " /// A list of all instructions. -#[allow(non_camel_case_types)] +#[expect(non_camel_case_types, reason = \"block opcode are snake_case\")] #[derive(Clone, Debug)] pub enum IrOpcode {{ {} @@ -156,7 +156,10 @@ impl IrOpcode {{ }} }} pub mod fields {{ + #![expect(clippy::wildcard_imports, reason = \"we don't know what we need to import\")] + use super::*; + {} }} pub use fields::*; @@ -170,35 +173,35 @@ pub use fields::*; }).collect::>().join(",\n\t"), paths.iter().map(|(path, id, fields, _)| { if *fields { - format!("IrOpcode::{}(fields) => {}::acceptable_inputs(fields),", id, path) + format!("Self::{id}(fields) => {path}::acceptable_inputs(fields),") } else { - format!("IrOpcode::{} => {}::acceptable_inputs(),", id, path) + format!("Self::{id} => {path}::acceptable_inputs(),") } }).collect::>().join("\n\t\t\t"), paths.iter().map(|(path, id, fields, _)| { if *fields { - format!("IrOpcode::{}(fields) => {}::wasm(step_func, inputs, fields),", id, path) + format!("Self::{id}(fields) => {path}::wasm(step_func, inputs, fields),") } else { - format!("IrOpcode::{} => {}::wasm(step_func, inputs),", id, path) + format!("Self::{id} => {path}::wasm(step_func, inputs),") } }).collect::>().join("\n\t\t\t"), paths.iter().map(|(path, id, fields, _)| { if *fields { - format!("IrOpcode::{}(fields) => {}::output_type(inputs, fields),", id, path) + format!("Self::{id}(fields) => {path}::output_type(inputs, fields),") } else { - format!("IrOpcode::{} => {}::output_type(inputs),", id, path) + format!("Self::{id} => {path}::output_type(inputs),") } }).collect::>().join("\n\t\t\t"), paths.iter().map(|(path, id, fields, _)| { if *fields { - format!("IrOpcode::{}(_) => {}::REQUESTS_SCREEN_REFRESH,", id, path) + format!("Self::{id}(_) => {path}::REQUESTS_SCREEN_REFRESH,") } else { - format!("IrOpcode::{} => {}::REQUESTS_SCREEN_REFRESH,", id, path) + format!("Self::{id} => {path}::REQUESTS_SCREEN_REFRESH,") } }).collect::>().join("\n\t\t\t"), paths.iter().filter(|(_, _, fields, _)| *fields) .map(|(path, _, _, fields_name)| - format!("pub use {}::Fields as {};", path, fields_name) + format!("pub use {path}::Fields as {fields_name};") ).collect::>().join("\n\t"), )) .unwrap(); diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..88c73d01 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,4 @@ +allowed-duplicate-crates = ["syn"] +too-many-lines-threshold = 150 +allow-panic-in-tests = true +allow-unwrap-in-tests = true \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 86ea0321..9e01d1a5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,7 +13,7 @@ pub struct HQError { pub line: u32, pub column: u32, } -#[derive(Clone, Debug, PartialEq)] // todo: get rid of this once all expects are gone +#[derive(Clone, Debug, PartialEq, Eq)] // todo: get rid of this once all expects are gone pub enum HQErrorType { MalformedProject, InternalError, @@ -21,8 +21,8 @@ pub enum HQErrorType { } impl From for JsValue { - fn from(val: HQError) -> JsValue { - JsValue::from_str(match val.err_type { + fn from(val: HQError) -> Self { + Self::from_str(match val.err_type { HQErrorType::Unimplemented => format!("todo: {}
at {}:{}:{}
this is a bug or missing feature that is known and will be fixed or implemented in a future update", val.msg, val.file, val.line, val.column), HQErrorType::InternalError => format!("error: {}
at {}:{}:{}
this is probably a bug with HyperQuark itself. Please report this bug, with this error message, at https://github.com/hyperquark/hyperquark/issues/new", val.msg, val.file, val.line, val.column), HQErrorType::MalformedProject => format!("error: {}
at {}:{}:{}
this is probably a problem with the project itself, but if it works in vanilla scratch then this is a bug; please report it, by creating an issue at https://github.com/hyperquark/hyperquark/issues/new, including this error message", val.msg, val.file, val.line, val.column), @@ -32,7 +32,7 @@ impl From for JsValue { impl From for HQError { fn from(_e: BorrowError) -> Self { - HQError { + Self { err_type: HQErrorType::InternalError, msg: "couldn't borrow cell".into(), file: file!().into(), @@ -44,7 +44,7 @@ impl From for HQError { impl From for HQError { fn from(_e: BorrowMutError) -> Self { - HQError { + Self { err_type: HQErrorType::InternalError, msg: "couldn't mutably borrow cell".into(), file: file!().into(), @@ -55,6 +55,7 @@ impl From for HQError { } #[macro_export] +#[clippy::format_args] macro_rules! hq_todo { () => {{ return Err($crate::HQError { @@ -77,6 +78,7 @@ macro_rules! hq_todo { } #[macro_export] +#[clippy::format_args] macro_rules! hq_bug { ($($args:tt)+) => {{ return Err($crate::HQError { @@ -90,6 +92,7 @@ macro_rules! hq_bug { } #[macro_export] +#[clippy::format_args] macro_rules! hq_assert { ($expr:expr) => {{ if !($expr) { @@ -100,7 +103,8 @@ macro_rules! hq_assert { line: line!(), column: column!() }); - } + }; + assert!($expr); }}; ($expr:expr, $($args:tt)+) => {{ if !($expr) { @@ -111,11 +115,13 @@ macro_rules! hq_assert { line: line!(), column: column!() }); - } + }; + assert!($expr); }}; } #[macro_export] +#[clippy::format_args] macro_rules! hq_assert_eq { ($l:expr, $r:expr) => {{ if $l != $r { @@ -126,7 +132,8 @@ macro_rules! hq_assert_eq { line: line!(), column: column!() }); - } + }; + assert_eq!($l, $r); }}; ($l:expr, $r:expr, $($args:tt)+) => {{ if $l != $r { @@ -137,11 +144,13 @@ macro_rules! hq_assert_eq { line: line!(), column: column!() }); - } + }; + assert_eq!($l, $r); }}; } #[macro_export] +#[clippy::format_args] macro_rules! hq_bad_proj { ($($args:tt)+) => {{ return Err($crate::HQError { @@ -155,6 +164,7 @@ macro_rules! hq_bad_proj { } #[macro_export] +#[clippy::format_args] macro_rules! make_hq_todo { ($($args:tt)+) => {{ use $crate::alloc::Box::ToBox; @@ -169,6 +179,7 @@ macro_rules! make_hq_todo { } #[macro_export] +#[clippy::format_args] macro_rules! make_hq_bug { ($($args:tt)+) => {{ $crate::HQError { @@ -182,6 +193,7 @@ macro_rules! make_hq_bug { } #[macro_export] +#[clippy::format_args] macro_rules! make_hq_bad_proj { ($($args:tt)+) => {{ $crate::HQError { diff --git a/src/instructions.rs b/src/instructions.rs index 2b639e52..0ac402c2 100644 --- a/src/instructions.rs +++ b/src/instructions.rs @@ -1,6 +1,15 @@ //! Contains information about instructions (roughly anaologus to blocks), //! including input type validation, output type mapping, and WASM generation. +#![allow( + clippy::unnecessary_wraps, + reason = "many functions here needlessly return `Result`s in order to keep type signatures consistent" +)] +#![allow( + clippy::needless_pass_by_value, + reason = "there are so many `Rc`s here which I don't want to change" +)] + use crate::prelude::*; mod control; @@ -22,14 +31,14 @@ pub use input_switcher::wrap_instruction; pub use hq::r#yield::YieldMode; mod prelude { - pub(crate) use crate::ir::Type as IrType; + pub use crate::ir::Type as IrType; pub use crate::prelude::*; - pub(crate) use crate::wasm::{InternalInstruction, StepFunc}; + pub use crate::wasm::{InternalInstruction, StepFunc}; pub use wasm_encoder::{RefType, ValType}; pub use wasm_gen::wasm; /// Canonical NaN + bit 33, + string pointer in bits 1-32 - pub const BOXED_STRING_PATTERN: i64 = 0x7FF80001 << 32; + pub const BOXED_STRING_PATTERN: i64 = 0x7FF8_0001 << 32; /// Canonical NaN + bit 33, + i32 in bits 1-32 - pub const BOXED_INT_PATTERN: i64 = 0x7ff80002 << 32; + pub const BOXED_INT_PATTERN: i64 = 0x7ff8_0002 << 32; } diff --git a/src/instructions/control/if_else.rs b/src/instructions/control/if_else.rs index e19ad1a9..c55ea827 100644 --- a/src/instructions/control/if_else.rs +++ b/src/instructions/control/if_else.rs @@ -16,8 +16,8 @@ pub fn wasm( branch_else, }: &Fields, ) -> HQResult> { - let if_instructions = func.compile_inner_step(Rc::clone(branch_if))?; - let else_instructions = func.compile_inner_step(Rc::clone(branch_else))?; + let if_instructions = func.compile_inner_step(branch_if)?; + let else_instructions = func.compile_inner_step(branch_else)?; let block_type = func .registries() .types() diff --git a/src/instructions/control/loop.rs b/src/instructions/control/loop.rs index 71cbb13f..d4ccca5a 100644 --- a/src/instructions/control/loop.rs +++ b/src/instructions/control/loop.rs @@ -22,11 +22,13 @@ pub fn wasm( flip_if, }: &Fields, ) -> HQResult> { - let inner_instructions = func.compile_inner_step(Rc::clone(body))?; - let first_condition_instructions = func.compile_inner_step(Rc::clone( - &first_condition.clone().unwrap_or(Rc::clone(condition)), - ))?; - let condition_instructions = func.compile_inner_step(Rc::clone(condition))?; + let inner_instructions = func.compile_inner_step(body)?; + let first_condition_instructions = func.compile_inner_step( + &first_condition + .clone() + .unwrap_or_else(|| Rc::clone(condition)), + )?; + let condition_instructions = func.compile_inner_step(condition)?; Ok(wasm![Block(BlockType::Empty),] .into_iter() .chain(first_condition_instructions) diff --git a/src/instructions/data/setvariableto.rs b/src/instructions/data/setvariableto.rs index b7955cee..b8a9f8b1 100644 --- a/src/instructions/data/setvariableto.rs +++ b/src/instructions/data/setvariableto.rs @@ -11,7 +11,7 @@ pub fn wasm( ) -> HQResult> { let t1 = inputs[0]; if variable.0.local() { - let local_index: u32 = func.local_variable(RcVar::clone(variable))?; + let local_index: u32 = func.local_variable(variable)?; if variable.0.possible_types().is_base_type() { Ok(wasm![LocalSet(local_index)]) } else { @@ -21,10 +21,7 @@ pub fn wasm( ]) } } else { - let global_index: u32 = func - .registries() - .variables() - .register(RcVar::clone(variable))?; + let global_index: u32 = func.registries().variables().register(variable)?; if variable.0.possible_types().is_base_type() { Ok(wasm![GlobalSet(global_index)]) } else { diff --git a/src/instructions/data/teevariable.rs b/src/instructions/data/teevariable.rs index c4025e0f..9111087e 100644 --- a/src/instructions/data/teevariable.rs +++ b/src/instructions/data/teevariable.rs @@ -13,7 +13,7 @@ pub fn wasm( ) -> HQResult> { let t1 = inputs[0]; if variable.0.local() { - let local_index: u32 = func.local_variable(RcVar::clone(variable))?; + let local_index: u32 = func.local_variable(variable)?; if variable.0.possible_types().is_base_type() { Ok(wasm![LocalTee(local_index)]) } else { @@ -23,10 +23,7 @@ pub fn wasm( ]) } } else { - let global_index: u32 = func - .registries() - .variables() - .register(RcVar::clone(variable))?; + let global_index: u32 = func.registries().variables().register(variable)?; if variable.0.possible_types().is_base_type() { Ok(wasm![GlobalSet(global_index), GlobalGet(global_index)]) } else { diff --git a/src/instructions/data/variable.rs b/src/instructions/data/variable.rs index 65b68215..72b24489 100644 --- a/src/instructions/data/variable.rs +++ b/src/instructions/data/variable.rs @@ -10,13 +10,10 @@ pub fn wasm( Fields(variable): &Fields, ) -> HQResult> { if variable.0.local() { - let local_index: u32 = func.local_variable(RcVar::clone(variable))?; + let local_index: u32 = func.local_variable(variable)?; Ok(wasm![LocalGet(local_index)]) } else { - let global_index: u32 = func - .registries() - .variables() - .register(RcVar::clone(variable))?; + let global_index: u32 = func.registries().variables().register(variable)?; Ok(wasm![GlobalGet(global_index)]) } } diff --git a/src/instructions/hq/cast.rs b/src/instructions/hq/cast.rs index e0d48bc1..5e03e85e 100644 --- a/src/instructions/hq/cast.rs +++ b/src/instructions/hq/cast.rs @@ -9,7 +9,7 @@ fn best_cast_candidate(from: IrType, to: IrType) -> HQResult { let Some(from_base) = from.base_type() else { hq_bug!("from type has no base type") }; - Ok(if to_base_types.contains(&&from_base) { + Ok(if to_base_types.contains(&from_base) { from_base } else { let mut candidates = vec![]; @@ -19,7 +19,7 @@ fn best_cast_candidate(from: IrType, to: IrType) -> HQResult { IrType::String => &[IrType::Float, IrType::QuasiInt] as &[IrType], _ => unreachable!(), } { - if to_base_types.contains(&preference) { + if to_base_types.contains(preference) { candidates.push(preference); } } @@ -96,11 +96,11 @@ pub fn output_type(inputs: Rc<[IrType]>, &Fields(to): &Fields) -> HQResult>>()? .into_iter() - .reduce(|acc, el| acc.or(el)) - .ok_or(make_hq_bug!("input was empty"))?, + .reduce(IrType::or) + .ok_or_else(|| make_hq_bug!("input was empty"))?, )) } diff --git a/src/instructions/hq/float.rs b/src/instructions/hq/float.rs index 0d61b4d5..81f853a0 100644 --- a/src/instructions/hq/float.rs +++ b/src/instructions/hq/float.rs @@ -1,3 +1,8 @@ +#![allow( + clippy::trivially_copy_pass_by_ref, + reason = "Fields should be passed by reference for type signature consistency" +)] + use super::super::prelude::*; #[derive(Clone, Copy, Debug)] diff --git a/src/instructions/hq/integer.rs b/src/instructions/hq/integer.rs index 250284b7..ac7841ee 100644 --- a/src/instructions/hq/integer.rs +++ b/src/instructions/hq/integer.rs @@ -1,3 +1,8 @@ +#![allow( + clippy::trivially_copy_pass_by_ref, + reason = "Fields should be passed by reference for type signature consistency" +)] + use super::super::prelude::*; #[derive(Clone, Copy, Debug)] diff --git a/src/instructions/hq/text.rs b/src/instructions/hq/text.rs index 22ef836e..6975eb31 100644 --- a/src/instructions/hq/text.rs +++ b/src/instructions/hq/text.rs @@ -39,12 +39,10 @@ pub fn output_type(_inputs: Rc<[IrType]>, Fields(val): &Fields) -> HQResult { IrType::StringBoolean } - num if num.parse::().is_ok() => { - if num.parse::().unwrap().is_nan() { - IrType::StringNan - } else { - IrType::StringNumber - } + num if let Ok(float) = num.parse::() + && !float.is_nan() => + { + IrType::StringNumber } _ => IrType::StringNan, })) diff --git a/src/instructions/hq/yield.rs b/src/instructions/hq/yield.rs index 3f7f9ae4..bb216f7b 100644 --- a/src/instructions/hq/yield.rs +++ b/src/instructions/hq/yield.rs @@ -102,10 +102,10 @@ pub fn wasm( ] } }, - YieldMode::Inline(step) => func.compile_inner_step(Rc::clone(step))?, + YieldMode::Inline(step) => func.compile_inner_step(step)?, YieldMode::Schedule(weak_step) => { - let step = - Weak::upgrade(weak_step).ok_or(make_hq_bug!("couldn't upgrade Weak"))?; + let step = Weak::upgrade(weak_step) + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; step.make_used_non_inline()?; match func.flags().scheduler { Scheduler::CallIndirect => { @@ -145,7 +145,7 @@ pub fn wasm( } } } - _ => hq_todo!(), + YieldMode::Tail(_) => hq_todo!(), }) } diff --git a/src/instructions/input_switcher.rs b/src/instructions/input_switcher.rs index 8e10d2ae..d7767eeb 100644 --- a/src/instructions/input_switcher.rs +++ b/src/instructions/input_switcher.rs @@ -13,6 +13,112 @@ use wasm_encoder::ConstExpr; use wasm_encoder::{BlockType, Instruction as WInstruction, RefType}; use wasm_gen::wasm; +fn cast_instructions( + pos: usize, + base: IrType, + if_block_type: BlockType, + local_idx: u32, + func: &StepFunc, + possible_types_num: usize, +) -> HQResult> { + Ok(if pos == 0 { + match base { + IrType::QuasiInt => wasm![ + I64Const(BOXED_INT_PATTERN), + I64And, + I64Const(BOXED_INT_PATTERN), + I64Eq, + If(if_block_type), + LocalGet(local_idx), + I32WrapI64, + ], + IrType::String => { + let table_index = func.registries().tables().register( + "strings".into(), + TableOptions { + element_type: RefType::EXTERNREF, + min: 0, + // TODO: use js string imports for preknown strings + max: None, + init: None, + }, + )?; + wasm![ + I64Const(BOXED_STRING_PATTERN), + I64And, + I64Const(BOXED_STRING_PATTERN), + I64Eq, + If(if_block_type), + LocalGet(local_idx), + I32WrapI64, + TableGet(table_index), + ] + } + // float guaranteed to be last so no need to check + _ => unreachable!(), + } + } else if pos == possible_types_num - 1 { + match base { + IrType::Float => wasm![Else, LocalGet(local_idx), F64ReinterpretI64], // float guaranteed to be last so no need to check + IrType::QuasiInt => wasm![Else, LocalGet(local_idx), I32WrapI64], + IrType::String => { + let table_index = func.registries().tables().register( + "strings".into(), + TableOptions { + element_type: RefType::EXTERNREF, + min: 0, + // TODO: use js string imports for preknown strings + max: None, + init: None, + }, + )?; + wasm![Else, LocalGet(local_idx), I32WrapI64, TableGet(table_index)] + } + _ => unreachable!(), + } + } else { + match base { + // float guaranteed to be last so no need to check + IrType::Float => wasm![Else, LocalGet(local_idx), F64ReinterpretI64], + IrType::QuasiInt => wasm![ + Else, + LocalGet(local_idx), + I64Const(BOXED_INT_PATTERN), + I64And, + I64Const(BOXED_INT_PATTERN), + I64Eq, + If(if_block_type), + LocalGet(local_idx), + I32WrapI64, + ], + IrType::String => { + let table_index = func.registries().tables().register( + "strings".into(), + TableOptions { + element_type: RefType::EXTERNREF, + min: 0, + max: None, + init: None, + }, + )?; + wasm![ + Else, + LocalGet(local_idx), + I64Const(BOXED_STRING_PATTERN), + I64And, + I64Const(BOXED_STRING_PATTERN), + I64Eq, + If(if_block_type), + LocalGet(local_idx), + I32WrapI64, + TableGet(table_index), + ] + } + _ => unreachable!(), + } + }) +} + /// generates branches (or not, if an input is not boxed) for a list of remaining input types. /// This sort of recursion makes me feel distinctly uneasy; I'm just waiting for a stack /// overflow. @@ -25,19 +131,20 @@ fn generate_branches( output_type: Option, ) -> HQResult> { if remaining_inputs.is_empty() { - hq_assert!(processed_inputs.iter().all(|ty| ty.is_base_type())); - let processed_inputs = processed_inputs.into(); - let mut wasm = opcode.wasm(func, Rc::clone(&processed_inputs))?; + hq_assert!(processed_inputs.iter().copied().all(IrType::is_base_type)); + let rc_processed_inputs = processed_inputs.into(); + let mut wasm = opcode.wasm(func, Rc::clone(&rc_processed_inputs))?; // if the overall output is boxed, but this particular branch produces an unboxed result // (which i think all branches probably should?), box it. // TODO: split this into another function somewhere? it seems like this should // be useful somewhere else as well - if let Some(this_output) = opcode.output_type(processed_inputs)? { + if let Some(this_output) = opcode.output_type(rc_processed_inputs)? { if this_output.is_base_type() && !output_type - .ok_or(make_hq_bug!("expected no output type but got one"))? + .ok_or_else(|| make_hq_bug!("expected no output type but got one"))? .is_base_type() { + #[expect(clippy::unwrap_used, reason = "asserted that type is base type")] let this_base_type = this_output.base_type().unwrap(); wasm.append(&mut wasm![@boxed(this_base_type)]); } @@ -47,23 +154,22 @@ fn generate_branches( let (curr_input, local_idx) = &remaining_inputs[0]; let local_idx = *local_idx; // variable shadowing feels evil but hey it works let mut wasm = wasm![LocalGet(local_idx)]; - Ok(if curr_input.len() == 1 { - let mut processed_inputs = processed_inputs.to_vec(); - processed_inputs.push(curr_input[0]); + if curr_input.len() == 1 { + let mut vec_processed_inputs = processed_inputs.to_vec(); + vec_processed_inputs.push(curr_input[0]); wasm.append(&mut generate_branches( func, - &processed_inputs, + &vec_processed_inputs, &remaining_inputs[1..], opcode, output_type, )?); - wasm } else { let if_block_type = BlockType::FunctionType( func.registries().types().register_default(( processed_inputs .iter() - .cloned() // &T.clone() is *T + .copied() .map(WasmProject::ir_type_to_wasm) .collect::>>()?, if let Some(out_ty) = output_type { @@ -76,135 +182,53 @@ fn generate_branches( let possible_types_num = curr_input.len(); let allowed_input_types = opcode.acceptable_inputs()[processed_inputs.len()]; for (i, ty) in curr_input.iter().enumerate() { - let base = ty.base_type().ok_or(make_hq_bug!("non-base type found"))?; - wasm.append(&mut if i == 0 { - match base { - IrType::QuasiInt => wasm![ - I64Const(BOXED_INT_PATTERN), - I64And, - I64Const(BOXED_INT_PATTERN), - I64Eq, - If(if_block_type), - LocalGet(local_idx), - I32WrapI64, - ], - IrType::String => { - let table_index = func.registries().tables().register( - "strings".into(), - TableOptions { - element_type: RefType::EXTERNREF, - min: 0, - // TODO: use js string imports for preknown strings - max: None, - init: None, - }, - )?; - wasm![ - I64Const(BOXED_STRING_PATTERN), - I64And, - I64Const(BOXED_STRING_PATTERN), - I64Eq, - If(if_block_type), - LocalGet(local_idx), - I32WrapI64, - TableGet(table_index), - ] - } - // float guaranteed to be last so no need to check - _ => unreachable!(), - } - } else if i == possible_types_num - 1 { - match base { - IrType::Float => wasm![Else, LocalGet(local_idx), F64ReinterpretI64], // float guaranteed to be last so no need to check - IrType::QuasiInt => wasm![Else, LocalGet(local_idx), I32WrapI64], - IrType::String => { - let table_index = func.registries().tables().register( - "strings".into(), - TableOptions { - element_type: RefType::EXTERNREF, - min: 0, - // TODO: use js string imports for preknown strings - max: None, - init: None, - }, - )?; - wasm![Else, LocalGet(local_idx), I32WrapI64, TableGet(table_index)] - } - _ => unreachable!(), - } - } else { - match base { - // float guaranteed to be last so no need to check - IrType::Float => wasm![Else, LocalGet(local_idx), F64ReinterpretI64], - IrType::QuasiInt => wasm![ - Else, - LocalGet(local_idx), - I64Const(BOXED_INT_PATTERN), - I64And, - I64Const(BOXED_INT_PATTERN), - I64Eq, - If(if_block_type), - LocalGet(local_idx), - I32WrapI64, - ], - IrType::String => { - let table_index = func.registries().tables().register( - "strings".into(), - TableOptions { - element_type: RefType::EXTERNREF, - min: 0, - max: None, - init: None, - }, - )?; - wasm![ - Else, - LocalGet(local_idx), - I64Const(BOXED_STRING_PATTERN), - I64And, - I64Const(BOXED_STRING_PATTERN), - I64Eq, - If(if_block_type), - LocalGet(local_idx), - I32WrapI64, - TableGet(table_index), - ] - } - _ => unreachable!(), - } - }); - if !allowed_input_types.base_types().any(|ty| *ty == base) { + let base = ty + .base_type() + .ok_or_else(|| make_hq_bug!("non-base type found"))?; + wasm.append(&mut cast_instructions( + i, + base, + if_block_type, + local_idx, + func, + possible_types_num, + )?); + if !allowed_input_types.base_types().any(|ty| ty == base) { wasm.append( &mut IrOpcode::hq_cast(HqCastFields(allowed_input_types)) .wasm(func, Rc::new([*ty]))?, ); } - let mut processed_inputs = processed_inputs.to_vec(); - processed_inputs.push( + let mut vec_processed_inputs = processed_inputs.to_vec(); + vec_processed_inputs.push( IrOpcode::hq_cast(HqCastFields(allowed_input_types)) .output_type(Rc::new([*ty]))? - .ok_or(make_hq_bug!("hq_cast output type was None"))?, + .ok_or_else(|| make_hq_bug!("hq_cast output type was None"))?, ); wasm.append(&mut generate_branches( func, - &processed_inputs, + &vec_processed_inputs, &remaining_inputs[1..], opcode, output_type, - )?) + )?); } wasm.extend(core::iter::repeat_n( InternalInstruction::Immediate(WInstruction::End), possible_types_num - 1, // the last else doesn't need an additional `end` instruction )); - wasm - }) + } + Ok(wasm) } +#[expect( + clippy::needless_pass_by_value, + reason = "passing an Rc by reference doesn't make much sense a lot of the time" +)] pub fn wrap_instruction( func: &StepFunc, inputs: Rc<[IrType]>, - opcode: IrOpcode, + opcode: &IrOpcode, ) -> HQResult> { let output = opcode.output_type(Rc::clone(&inputs))?; @@ -239,7 +263,7 @@ pub fn wrap_instruction( let mut wasm = locals .iter() .rev() - .cloned() + .copied() .map(WInstruction::LocalSet) .map(InternalInstruction::Immediate) .collect::>(); @@ -249,10 +273,10 @@ pub fn wrap_instruction( &[], base_types .into_iter() - .zip_eq(locals.iter().cloned()) + .zip_eq(locals.iter().copied()) .collect::>() .as_slice(), - &opcode, + opcode, output, )?); if opcode.requests_screen_refresh() { diff --git a/src/instructions/looks/say.rs b/src/instructions/looks/say.rs index c7f1f679..6fd23fe1 100644 --- a/src/instructions/looks/say.rs +++ b/src/instructions/looks/say.rs @@ -12,7 +12,7 @@ pub fn wasm( &Fields { debug, target_idx }: &Fields, ) -> HQResult> { let prefix = String::from(if debug { "say_debug" } else { "say" }); - let target_idx: i32 = target_idx + let itarget_idx: i32 = target_idx .try_into() .map_err(|_| make_hq_bug!("target index out of bounds"))?; Ok(if IrType::QuasiInt.contains(inputs[0]) { @@ -20,19 +20,19 @@ pub fn wasm( ("looks", format!("{prefix}_int").into_boxed_str()), (vec![ValType::I32, ValType::I32], vec![]), )?; - wasm![I32Const(target_idx), Call(func_index)] + wasm![I32Const(itarget_idx), Call(func_index)] } else if IrType::Float.contains(inputs[0]) { let func_index = func.registries().external_functions().register( ("looks", format!("{prefix}_float").into_boxed_str()), (vec![ValType::F64, ValType::I32], vec![]), )?; - wasm![I32Const(target_idx), Call(func_index)] + wasm![I32Const(itarget_idx), Call(func_index)] } else if IrType::String.contains(inputs[0]) { let func_index = func.registries().external_functions().register( ("looks", format!("{prefix}_string").into_boxed_str()), (vec![ValType::EXTERNREF, ValType::I32], vec![]), )?; - wasm![I32Const(target_idx), Call(func_index)] + wasm![I32Const(itarget_idx), Call(func_index)] } else { hq_bug!("bad input") }) diff --git a/src/instructions/looks/think.rs b/src/instructions/looks/think.rs index d445f894..08b4e104 100644 --- a/src/instructions/looks/think.rs +++ b/src/instructions/looks/think.rs @@ -12,7 +12,7 @@ pub fn wasm( &Fields { debug, target_idx }: &Fields, ) -> HQResult> { let prefix = String::from(if debug { "think_debug" } else { "think" }); - let target_idx: i32 = target_idx + let itarget_idx: i32 = target_idx .try_into() .map_err(|_| make_hq_bug!("target index out of bounds"))?; Ok(if IrType::QuasiInt.contains(inputs[0]) { @@ -20,19 +20,19 @@ pub fn wasm( ("looks", format!("{prefix}_int").into_boxed_str()), (vec![ValType::I32, ValType::I32], vec![]), )?; - wasm![I32Const(target_idx), Call(func_index)] + wasm![I32Const(itarget_idx), Call(func_index)] } else if IrType::Float.contains(inputs[0]) { let func_index = func.registries().external_functions().register( ("looks", format!("{prefix}_float").into_boxed_str()), (vec![ValType::F64, ValType::I32], vec![]), )?; - wasm![I32Const(target_idx), Call(func_index)] + wasm![I32Const(itarget_idx), Call(func_index)] } else if IrType::String.contains(inputs[0]) { let func_index = func.registries().external_functions().register( ("looks", format!("{prefix}_string").into_boxed_str()), (vec![ValType::EXTERNREF, ValType::I32], vec![]), )?; - wasm![I32Const(target_idx), Call(func_index)] + wasm![I32Const(itarget_idx), Call(func_index)] } else { hq_bug!("bad input") }) diff --git a/src/instructions/operator/add.rs b/src/instructions/operator/add.rs index 683502a1..3d447d71 100644 --- a/src/instructions/operator/add.rs +++ b/src/instructions/operator/add.rs @@ -51,6 +51,7 @@ pub fn acceptable_inputs() -> Rc<[IrType]> { } pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); let t1 = inputs[0]; let t2 = inputs[1]; let maybe_positive = t1.maybe_positive() || t2.maybe_positive(); diff --git a/src/instructions/operator/divide.rs b/src/instructions/operator/divide.rs index 08da2a30..3ffa157a 100644 --- a/src/instructions/operator/divide.rs +++ b/src/instructions/operator/divide.rs @@ -20,6 +20,7 @@ pub fn acceptable_inputs() -> Rc<[IrType]> { } pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); let t1 = inputs[0]; let t2 = inputs[1]; let maybe_positive = (t1.maybe_positive() && t2.maybe_positive()) diff --git a/src/instructions/operator/equals.rs b/src/instructions/operator/equals.rs index c01bb554..d92d3bdd 100644 --- a/src/instructions/operator/equals.rs +++ b/src/instructions/operator/equals.rs @@ -2,6 +2,7 @@ use wasm_encoder::BlockType; use super::super::prelude::*; +#[expect(clippy::too_many_lines, reason = "long monomorphisation routine")] pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult> { hq_assert_eq!(inputs.len(), 2); let t1 = inputs[0]; diff --git a/src/instructions/operator/gt.rs b/src/instructions/operator/gt.rs index 756de18e..771a2bfb 100644 --- a/src/instructions/operator/gt.rs +++ b/src/instructions/operator/gt.rs @@ -81,13 +81,7 @@ pub fn wasm(func: &StepFunc, inputs: Rc<[IrType]>) -> HQResult number is always true - End, + I32Const(1), // NaN > number is always true Else, LocalGet(local1), LocalGet(local2), diff --git a/src/instructions/operator/multiply.rs b/src/instructions/operator/multiply.rs index a45b7d78..6c239168 100644 --- a/src/instructions/operator/multiply.rs +++ b/src/instructions/operator/multiply.rs @@ -51,6 +51,7 @@ pub fn acceptable_inputs() -> Rc<[IrType]> { } pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { + hq_assert_eq!(inputs.len(), 2); let t1 = inputs[0]; let t2 = inputs[1]; let maybe_positive = (t1.maybe_positive() && t2.maybe_positive()) diff --git a/src/instructions/operator/subtract.rs b/src/instructions/operator/subtract.rs index 2a796bf8..72cfc40a 100644 --- a/src/instructions/operator/subtract.rs +++ b/src/instructions/operator/subtract.rs @@ -51,13 +51,14 @@ pub fn acceptable_inputs() -> Rc<[IrType]> { } pub fn output_type(inputs: Rc<[IrType]>) -> HQResult> { + hq_assert!(inputs.len() == 2); let t1 = inputs[0]; let t2 = inputs[1]; let maybe_positive = t1.maybe_positive() || t2.maybe_negative(); let maybe_negative = t1.maybe_negative() || t2.maybe_positive(); let maybe_zero = ((t1.maybe_zero() || t1.maybe_nan()) && (t2.maybe_zero() || t2.maybe_nan())) - || (t1.maybe_positive() && t2.maybe_negative()) - || (t1.maybe_negative() && t2.maybe_positive()); + || (t1.maybe_positive() && t2.maybe_positive()) + || (t1.maybe_negative() && t2.maybe_negative()); let maybe_nan = IrType::FloatPosInf.intersects(t1.and(t2)) || IrType::FloatNegInf.intersects(t1.and(t2)); Ok(Some(if IrType::QuasiInt.contains(t1.or(t2)) { diff --git a/src/instructions/procedures/argument.rs b/src/instructions/procedures/argument.rs index 3f576bd3..59235022 100644 --- a/src/instructions/procedures/argument.rs +++ b/src/instructions/procedures/argument.rs @@ -11,7 +11,7 @@ pub fn wasm( ) -> HQResult> { hq_assert!( WasmProject::ir_type_to_wasm(*ty)? - == *func.params().get(*index).ok_or(make_hq_bug!( + == *func.params().get(*index).ok_or_else(|| make_hq_bug!( "proc argument index was out of bounds for func params" ))?, "proc argument type didn't match that of the corresponding function param" diff --git a/src/instructions/tests.rs b/src/instructions/tests.rs index f3afdbc8..d84e5996 100644 --- a/src/instructions/tests.rs +++ b/src/instructions/tests.rs @@ -1,5 +1,7 @@ -/// Generates unit tests for instructions files. Takes a module name, followed by a semicolon, -/// collowed by the full name of the opcode (in category_opcode form), followed by an optional +/// Generates unit tests for instructions files. +/// +/// Takes a module name, followed by a semicolon, +/// collowed by the full name of the opcode (in the form of `_`), followed by an optional /// comma-separated list of arbitrary identifiers corresponding to the number of inputs the block /// takes, optionally followed by a semicolon and an expression for a sensible default for any fields, /// optionally followed by a semicolon and a `WasmFlags` configuration (defaults to `Default::default()`). @@ -7,7 +9,7 @@ /// different module names. /// /// Example: -/// For a block foo_bar, which takes 2 inputs, with Fields(bool), +/// For a block `foo_bar`, which takes 2 inputs, with Fields(bool), /// ```ignore /// instructions_test!(test; foo_bar; t1, t2 @ super::Fields(true)); /// instructions_test!(test; foo_bar; t1, t2 @ super::Fields(false)); @@ -33,7 +35,6 @@ macro_rules! instructions_test { }; use $crate::wasm::{flags::all_wasm_features, StepFunc, Registries, WasmProject, WasmFlags}; - #[allow(unused)] macro_rules! ident_as_irtype { ( $_:ident ) => { IrType }; } @@ -82,12 +83,9 @@ macro_rules! instructions_test { #[test] fn wasm_output_type_matches_expected_output_type() -> HQResult<()> { for ($($type_arg,)*) in types_iter(true) { - let output_type = match output_type(Rc::new([$($type_arg,)*]), $(&$fields)?) { - Ok(a) => a, - Err(_) => { - println!("skipping failed output_type"); - continue; - } + let Ok(output_type) = output_type(Rc::new([$($type_arg,)*]), $(&$fields)?) else { + println!("skipping failed output_type"); + continue; }; let registries = Rc::new(Registries::default()); let types: &[IrType] = &[$($type_arg,)*]; @@ -96,13 +94,10 @@ macro_rules! instructions_test { Some(output) => Some(WasmProject::ir_type_to_wasm(output)?), None => None, }; - let step_func = StepFunc::new_with_types(params.into(), result, Rc::clone(®istries), Default::default(), flags())?; - let wasm = match wasm(&step_func, Rc::new([$($type_arg,)*]), $(&$fields)?) { - Ok(a) => a, - Err(_) => { - println!("skipping failed wasm"); - continue; - } + let step_func = StepFunc::new_with_types(params.into(), result, Rc::clone(®istries), Default::default(), flags()); + let Ok(wasm) = wasm(&step_func, Rc::new([$($type_arg,)*]), $(&$fields)?) else { + println!("skipping failed wasm"); + continue; }; for (i, _) in types.iter().enumerate() { step_func.add_instructions([$crate::wasm::InternalInstruction::Immediate(wasm_encoder::Instruction::LocalGet((i + 1).try_into().unwrap()))])? @@ -130,7 +125,7 @@ macro_rules! instructions_test { let imported_func_count: u32 = registries.external_functions().registry().borrow().len().try_into().unwrap(); registries.external_functions().clone().finish(&mut imports, registries.types())?; - step_func.finish(&mut functions, &mut codes, Default::default(), imported_func_count)?; + step_func.finish(&mut functions, &mut codes, &Default::default(), imported_func_count)?; registries.types().clone().finish(&mut types); registries.tables().clone().finish(&imports, &mut tables, &mut exports); registries.globals().clone().finish(&imports, &mut globals, &mut exports); @@ -153,12 +148,9 @@ macro_rules! instructions_test { #[test] fn wasm_output_type_matches_wrapped_expected_output_type() -> HQResult<()> { for ($($type_arg,)*) in types_iter(false) { - let output_type = match output_type(Rc::new([$($type_arg,)*]), $(&$fields)?) { - Ok(a) => a, - Err(_) => { - println!("skipping failed output_type"); - continue; - } + let Ok(output_type) = output_type(Rc::new([$($type_arg,)*]), $(&$fields)?) else { + println!("skipping failed output_type"); + continue; }; println!("{output_type:?}"); let registries = Rc::new(Registries::default()); @@ -169,8 +161,8 @@ macro_rules! instructions_test { None => None, }; println!("{result:?}"); - let step_func = StepFunc::new_with_types(params.into(), result, Rc::clone(®istries), Default::default(), flags())?; - let wasm = match $crate::instructions::wrap_instruction(&step_func, Rc::new([$($type_arg,)*]), $crate::instructions::IrOpcode::$opcode$(($fields))?) { + let step_func = StepFunc::new_with_types(params.into(), result, Rc::clone(®istries), Default::default(), flags()); + let wasm = match $crate::instructions::wrap_instruction(&step_func, Rc::new([$($type_arg,)*]), &$crate::instructions::IrOpcode::$opcode$(($fields))?) { Ok(a) => a, Err(e) => { println!("skipping failed wasm (message: {})", e.msg); @@ -205,7 +197,7 @@ macro_rules! instructions_test { }); let imported_func_count: u32 = registries.external_functions().registry().borrow().len().try_into().unwrap(); registries.external_functions().clone().finish(&mut imports, registries.types())?; - step_func.finish(&mut functions, &mut codes, Default::default(), imported_func_count)?; + step_func.finish(&mut functions, &mut codes, &Default::default(), imported_func_count)?; registries.types().clone().finish(&mut types); registries.tables().clone().finish(&imports, &mut tables, &mut exports); registries.globals().clone().finish(&imports, &mut globals, &mut exports); diff --git a/src/ir.rs b/src/ir.rs index b988f4c3..abf24b60 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -10,12 +10,11 @@ mod types; mod variable; use context::StepContext; -pub(crate) use event::Event; -#[allow(unused_imports)] -pub(crate) use proc::{PartialStep, Proc, ProcContext, ProcMap}; -pub(crate) use project::IrProject; -pub(crate) use step::Step; +pub use event::Event; +pub use proc::{PartialStep, Proc, ProcContext}; +pub use project::IrProject; +pub use step::Step; use target::Target; use thread::Thread; -pub(crate) use types::Type; -pub(crate) use variable::{RcVar, Variable}; +pub use types::Type; +pub use variable::{RcVar, Variable}; diff --git a/src/ir/blocks.rs b/src/ir/blocks.rs index 4f64da9b..aabaaff0 100644 --- a/src/ir/blocks.rs +++ b/src/ir/blocks.rs @@ -1,6 +1,14 @@ use super::context::StepContext; use super::{IrProject, RcVar, Step, Type as IrType}; -use crate::instructions::{fields::*, IrOpcode, YieldMode}; +use crate::instructions::{ + fields::{ + ControlIfElseFields, ControlLoopFields, DataSetvariabletoFields, DataTeevariableFields, + DataVariableFields, HqCastFields, HqFloatFields, HqIntegerFields, HqTextFields, + HqYieldFields, LooksSayFields, LooksThinkFields, ProceduresArgumentFields, + ProceduresCallWarpFields, + }, + IrOpcode, YieldMode, +}; use crate::ir::Variable; use crate::prelude::*; use crate::sb3; @@ -55,25 +63,25 @@ pub struct NextBlockInfo { pub struct NextBlocks(Vec, bool); impl NextBlocks { - pub fn new(terminating: bool) -> Self { - NextBlocks(vec![], terminating) + pub const fn new(terminating: bool) -> Self { + Self(vec![], terminating) } - pub fn terminating(&self) -> bool { + pub const fn terminating(&self) -> bool { self.1 } pub fn extend_with_inner(&self, new: NextBlockInfo) -> Self { let mut cloned = self.0.clone(); cloned.push(new); - NextBlocks(cloned, self.terminating()) + Self(cloned, self.terminating()) } pub fn pop_inner(self) -> (Option, Self) { let terminating = self.terminating(); let mut vec = self.0; let popped = vec.pop(); - (popped, NextBlocks(vec, terminating)) + (popped, Self(vec, terminating)) } } @@ -81,18 +89,13 @@ pub fn from_block( block: &Block, blocks: &BlockMap, context: &StepContext, - project: Weak, + project: &Weak, final_next_blocks: NextBlocks, ) -> HQResult> { insert_casts(match block { - Block::Normal { block_info, .. } => from_normal_block( - block_info, - blocks, - context, - Weak::clone(&project), - final_next_blocks, - )? - .to_vec(), + Block::Normal { block_info, .. } => { + from_normal_block(block_info, blocks, context, project, final_next_blocks)?.to_vec() + } Block::Special(block_array) => vec![from_special_block(block_array, context)?], }) } @@ -104,63 +107,71 @@ pub fn input_names(block_info: &BlockInfo, context: &StepContext) -> HQResult"))?; + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; let procs = target.procedures()?; - Ok(match opcode { - BlockOpcode::looks_say | BlockOpcode::looks_think => vec!["MESSAGE"], - BlockOpcode::operator_add - | BlockOpcode::operator_divide - | BlockOpcode::operator_subtract - | BlockOpcode::operator_multiply => vec!["NUM1", "NUM2"], - BlockOpcode::operator_lt - | BlockOpcode::operator_gt - | BlockOpcode::operator_equals - | BlockOpcode::operator_and - | BlockOpcode::operator_or => vec!["OPERAND1", "OPERAND2"], - BlockOpcode::operator_join | BlockOpcode::operator_contains => vec!["STRING1", "STRING2"], - BlockOpcode::operator_letter_of => vec!["LETTER", "STRING"], - BlockOpcode::sensing_dayssince2000 - | BlockOpcode::data_variable - | BlockOpcode::argument_reporter_boolean - | BlockOpcode::argument_reporter_string_number => vec![], - BlockOpcode::data_setvariableto | BlockOpcode::data_changevariableby => vec!["VALUE"], - BlockOpcode::control_if - | BlockOpcode::control_if_else - | BlockOpcode::control_repeat_until => vec!["CONDITION"], - BlockOpcode::operator_not => vec!["OPERAND"], - BlockOpcode::control_repeat => vec!["TIMES"], - BlockOpcode::operator_length => vec!["STRING"], - BlockOpcode::procedures_call => { - let serde_json::Value::String(proccode) = block_info - .mutation - .mutations - .get("proccode") - .ok_or(make_hq_bad_proj!("missing proccode on procedures_call"))? - else { - hq_bad_proj!("non-string proccode on procedures_call"); - }; - let Some(proc) = procs.get(proccode.as_str()) else { - hq_bad_proj!("procedures_call proccode doesn't exist") - }; - proc.context().arg_ids().iter().map(|b| &**b).collect() + Ok( + #[expect( + clippy::wildcard_enum_match_arm, + reason = "too many opcodes to match individually" + )] + match opcode { + BlockOpcode::looks_say | BlockOpcode::looks_think => vec!["MESSAGE"], + BlockOpcode::operator_add + | BlockOpcode::operator_divide + | BlockOpcode::operator_subtract + | BlockOpcode::operator_multiply => vec!["NUM1", "NUM2"], + BlockOpcode::operator_lt + | BlockOpcode::operator_gt + | BlockOpcode::operator_equals + | BlockOpcode::operator_and + | BlockOpcode::operator_or => vec!["OPERAND1", "OPERAND2"], + BlockOpcode::operator_join | BlockOpcode::operator_contains => { + vec!["STRING1", "STRING2"] + } + BlockOpcode::operator_letter_of => vec!["LETTER", "STRING"], + BlockOpcode::sensing_dayssince2000 + | BlockOpcode::data_variable + | BlockOpcode::argument_reporter_boolean + | BlockOpcode::argument_reporter_string_number => vec![], + BlockOpcode::data_setvariableto | BlockOpcode::data_changevariableby => vec!["VALUE"], + BlockOpcode::control_if + | BlockOpcode::control_if_else + | BlockOpcode::control_repeat_until => vec!["CONDITION"], + BlockOpcode::operator_not => vec!["OPERAND"], + BlockOpcode::control_repeat => vec!["TIMES"], + BlockOpcode::operator_length => vec!["STRING"], + BlockOpcode::procedures_call => { + let serde_json::Value::String(proccode) = block_info + .mutation + .mutations + .get("proccode") + .ok_or_else(|| make_hq_bad_proj!("missing proccode on procedures_call"))? + else { + hq_bad_proj!("non-string proccode on procedures_call"); + }; + let Some(proc) = procs.get(proccode.as_str()) else { + hq_bad_proj!("procedures_call proccode doesn't exist") + }; + proc.context().arg_ids().iter().map(|b| &**b).collect() + } + other => hq_todo!("unimplemented input_names for {:?}", other), } - other => hq_todo!("unimplemented input_names for {:?}", other), - } - .into_iter() - .map(String::from) - .collect()) + .into_iter() + .map(String::from) + .collect(), + ) } pub fn inputs( block_info: &BlockInfo, blocks: &BlockMap, context: &StepContext, - project: Weak, + project: &Weak, ) -> HQResult> { Ok(input_names(block_info, context)? .into_iter() .map(|name| -> HQResult> { - match match block_info.inputs.get((*name).into()) { + let input = match block_info.inputs.get((*name).into()) { Some(input) => input, None => { if name.starts_with("CONDITION") { @@ -172,16 +183,21 @@ pub fn inputs( hq_bad_proj!("missing input {}", name) } } - } { + }; + #[expect( + clippy::wildcard_enum_match_arm, + reason = "all variants covered in previous match guards" + )] + match input { Input::NoShadow(_, Some(block)) | Input::Shadow(_, Some(block), _) => match block { BlockArrayOrId::Array(arr) => Ok(vec![from_special_block(arr, context)?]), BlockArrayOrId::Id(id) => from_block( - blocks - .get(id) - .ok_or(make_hq_bad_proj!("block for input {} doesn't exist", name))?, + blocks.get(id).ok_or_else(|| { + make_hq_bad_proj!("block for input {} doesn't exist", name) + })?, blocks, context, - Weak::clone(&project), + project, NextBlocks::new(false), ), }, @@ -195,17 +211,17 @@ pub fn inputs( .collect()) } -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ProcArgType { Boolean, StringNumber, } impl ProcArgType { - fn default_block(&self) -> Vec { + fn default_block(self) -> Vec { vec![match self { - ProcArgType::Boolean => IrOpcode::hq_integer(HqIntegerFields(0)), - ProcArgType::StringNumber => IrOpcode::hq_text(HqTextFields("".into())), + Self::Boolean => IrOpcode::hq_integer(HqIntegerFields(0)), + Self::StringNumber => IrOpcode::hq_text(HqTextFields("".into())), }] } } @@ -221,26 +237,23 @@ fn procedure_argument( let sb3::VarVal::String(arg_name) = block_info .fields .get("VALUE") - .ok_or(make_hq_bad_proj!("missing VALUE field for proc argument"))? + .ok_or_else(|| make_hq_bad_proj!("missing VALUE field for proc argument"))? .get_0() - .clone() - .ok_or(make_hq_bad_proj!("missing value of VALUE field"))? + .ok_or_else(|| make_hq_bad_proj!("missing value of VALUE field"))? else { hq_bad_proj!("non-string proc argument name") }; let Some(index) = proc_context .arg_names() .iter() - .position(|name| *name == arg_name) + .position(|name| name == arg_name) else { return Ok(arg_type.default_block()); }; let expected_type = proc_context .arg_types() .get(index) - .ok_or(make_hq_bad_proj!( - "argument index not in range of argumenttypes" - ))?; + .ok_or_else(|| make_hq_bad_proj!("argument index not in range of argumenttypes"))?; hq_assert!( (arg_type == ProcArgType::Boolean && *expected_type == IrType::Boolean) || (arg_type == ProcArgType::StringNumber @@ -252,7 +265,8 @@ fn procedure_argument( )]) } -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments, reason = "too many arguments!")] +// TODO: put these arguments into a struct? fn generate_loop( warp: bool, should_break: &mut bool, @@ -270,69 +284,100 @@ fn generate_loop( None => return Ok(vec![IrOpcode::hq_drop]), // TODO: consider loops without input (i.e. forever) } .get_1() - .ok_or(make_hq_bug!(""))? + .ok_or_else(|| make_hq_bug!(""))? .clone() - .ok_or(make_hq_bug!(""))? + .ok_or_else(|| make_hq_bug!(""))? else { hq_bad_proj!("malformed SUBSTACK input") }; let Some(substack_block) = blocks.get(&substack_id) else { hq_bad_proj!("SUBSTACK block doesn't seem to exist") }; - if !warp { + if warp { + // TODO: can this be expressed in the same way as non-warping loops, + // just with yield_first: false? + let substack_blocks = from_block( + substack_block, + blocks, + context, + &context.project()?, + NextBlocks::new(false), + )?; + let substack_step = + Step::new_rc(None, context.clone(), substack_blocks, &context.project()?)?; + let condition_step = Step::new_rc( + None, + context.clone(), + condition_instructions, + &context.project()?, + )?; + let first_condition_step = if let Some(instrs) = first_condition_instructions { + Some(Step::new_rc( + None, + context.clone(), + instrs, + &context.project()?, + )?) + } else { + None + }; + Ok(setup_instructions + .into_iter() + .chain(vec![IrOpcode::control_loop(ControlLoopFields { + first_condition: first_condition_step, + condition: condition_step, + body: substack_step, + flip_if, + })]) + .collect()) + } else { *should_break = true; let (next_block, outer_next_blocks) = if let Some(ref next_block) = block_info.next { - ( - Some(NextBlock::ID(next_block.clone())), - final_next_blocks.clone(), - ) + (Some(NextBlock::ID(next_block.clone())), final_next_blocks) } else if let (Some(next_block_info), popped_next_blocks) = final_next_blocks.clone().pop_inner() { (Some(next_block_info.block), popped_next_blocks) } else { - (None, final_next_blocks.clone()) + (None, final_next_blocks) }; - let next_step = if next_block.is_some() { - match next_block.clone().unwrap() { - NextBlock::ID(id) => { - let Some(next_block_block) = blocks.get(&id.clone()) else { - hq_bad_proj!("next block doesn't exist") - }; - Step::from_block( - next_block_block, - id, - blocks, - context.clone(), - context.project()?, - outer_next_blocks, - )? - } - NextBlock::Step(ref step) => step - .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))?, + let next_step = match next_block { + Some(NextBlock::ID(id)) => { + let Some(next_block_block) = blocks.get(&id) else { + hq_bad_proj!("next block doesn't exist") + }; + Step::from_block( + next_block_block, + id, + blocks, + context, + &context.project()?, + outer_next_blocks, + )? } - } else { - Step::new_terminating(context.clone(), context.project()?)? + Some(NextBlock::Step(ref step)) => step + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?, + None => Step::new_terminating(context.clone(), &context.project()?)?, }; let condition_step = Step::new_rc( None, context.clone(), condition_instructions.clone(), - context.project()?, + &context.project()?, )?; let substack_blocks = from_block( substack_block, blocks, context, - context.project()?, + &context.project()?, NextBlocks::new(false).extend_with_inner(NextBlockInfo { yield_first: true, block: NextBlock::Step(Rc::downgrade(&condition_step)), }), )?; let substack_step = - Step::new_rc(None, context.clone(), substack_blocks, context.project()?)?; + Step::new_rc(None, context.clone(), substack_blocks, &context.project()?)?; condition_step .opcodes_mut()? .push(IrOpcode::control_if_else(ControlIfElseFields { @@ -341,65 +386,28 @@ fn generate_loop( })); Ok(setup_instructions .into_iter() - .chain(if let Some(instrs) = first_condition_instructions { - instrs - } else { - condition_instructions - }) + .chain(first_condition_instructions.map_or(condition_instructions, |instrs| instrs)) .chain(vec![IrOpcode::control_if_else(ControlIfElseFields { branch_if: if flip_if { - next_step.clone() + Rc::clone(&next_step) } else { - substack_step.clone() + Rc::clone(&substack_step) }, branch_else: if flip_if { substack_step } else { next_step }, })]) .collect()) - } else { - // TODO: can this be expressed in the same way as non-warping loops, - // just with yield_first: false? - let substack_blocks = from_block( - substack_block, - blocks, - context, - context.project()?, - NextBlocks::new(false), - )?; - let substack_step = - Step::new_rc(None, context.clone(), substack_blocks, context.project()?)?; - let condition_step = Step::new_rc( - None, - context.clone(), - condition_instructions.clone(), - context.project()?, - )?; - let first_condition_step = if let Some(instrs) = first_condition_instructions { - Some(Step::new_rc( - None, - context.clone(), - instrs, - context.project()?, - )?) - } else { - None - }; - Ok(setup_instructions - .into_iter() - .chain(vec![IrOpcode::control_loop(ControlLoopFields { - first_condition: first_condition_step, - condition: condition_step, - body: substack_step, - flip_if, - })]) - .collect()) } } +#[expect( + clippy::too_many_lines, + reason = "a big monolithic function is somewhat unavoidable here" +)] fn from_normal_block( block_info: &BlockInfo, blocks: &BlockMap, context: &StepContext, - project: Weak, + project: &Weak, final_next_blocks: NextBlocks, ) -> HQResult> { let mut curr_block = Some(block_info); @@ -408,196 +416,218 @@ fn from_normal_block( let mut should_break = false; while let Some(block_info) = curr_block { opcodes.append( - &mut inputs(block_info, blocks, context, Weak::clone(&project))? + &mut inputs(block_info, blocks, context, project)? .into_iter() - .chain(match &block_info.opcode { - BlockOpcode::operator_add => vec![IrOpcode::operator_add], - BlockOpcode::operator_subtract => vec![IrOpcode::operator_subtract], - BlockOpcode::operator_multiply => vec![IrOpcode::operator_multiply], - BlockOpcode::operator_divide => vec![IrOpcode::operator_divide], - BlockOpcode::looks_say => vec![IrOpcode::looks_say(LooksSayFields { - debug: context.debug, - target_idx: context - .target - .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))? - .index(), - })], - BlockOpcode::looks_think => vec![IrOpcode::looks_think(LooksThinkFields { - debug: context.debug, - target_idx: context - .target - .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))? - .index(), - })], - BlockOpcode::operator_join => vec![IrOpcode::operator_join], - BlockOpcode::operator_length => vec![IrOpcode::operator_length], - BlockOpcode::operator_contains => vec![IrOpcode::operator_contains], - BlockOpcode::operator_letter_of => vec![IrOpcode::operator_letter_of], - BlockOpcode::sensing_dayssince2000 => vec![IrOpcode::sensing_dayssince2000], - BlockOpcode::operator_lt => vec![IrOpcode::operator_lt], - BlockOpcode::operator_gt => vec![IrOpcode::operator_gt], - BlockOpcode::operator_equals => vec![IrOpcode::operator_equals], - BlockOpcode::operator_not => vec![IrOpcode::operator_not], - BlockOpcode::data_setvariableto => { - let sb3::Field::ValueId(_val, maybe_id) = - block_info.fields.get("VARIABLE").ok_or(make_hq_bad_proj!( - "invalid project.json - missing field VARIABLE" - ))? - else { - hq_bad_proj!( - "invalid project.json - missing variable id for VARIABLE field" - ); - }; - let id = maybe_id.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null variable id for VARIABLE field" - ))?; - let target = context - .target - .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))?; - let variable = if let Some(var) = target.variables().get(&id) { - var - } else if let Some(var) = context - .project()? - .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))? - .global_variables() - .get(&id) - { - &Rc::clone(var) - } else { - hq_bad_proj!("variable not found") - }; - vec![IrOpcode::data_setvariableto(DataSetvariabletoFields( - RcVar(Rc::clone(variable)), - ))] - } - BlockOpcode::data_changevariableby => { - let sb3::Field::ValueId(_val, maybe_id) = - block_info.fields.get("VARIABLE").ok_or(make_hq_bad_proj!( - "invalid project.json - missing field VARIABLE" - ))? - else { - hq_bad_proj!( - "invalid project.json - missing variable id for VARIABLE field" - ); - }; - let id = maybe_id.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null variable id for VARIABLE field" - ))?; - let target = context - .target - .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))?; - let variable = if let Some(var) = target.variables().get(&id) { - var - } else if let Some(var) = context - .project()? - .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))? - .global_variables() - .get(&id) - { - &Rc::clone(var) - } else { - hq_bad_proj!("variable not found") - }; - vec![ - IrOpcode::data_variable(DataVariableFields(RcVar(Rc::clone(variable)))), - IrOpcode::operator_add, - IrOpcode::data_setvariableto(DataSetvariabletoFields(RcVar( - Rc::clone(variable), - ))), - ] - } - BlockOpcode::data_variable => { - let sb3::Field::ValueId(_val, maybe_id) = - block_info.fields.get("VARIABLE").ok_or(make_hq_bad_proj!( - "invalid project.json - missing field VARIABLE" - ))? - else { - hq_bad_proj!( - "invalid project.json - missing variable id for VARIABLE field" - ); - }; - let id = maybe_id.clone().ok_or(make_hq_bad_proj!( - "invalid project.json - null variable id for VARIABLE field" - ))?; - let target = context - .target - .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))?; - let variable = if let Some(var) = target.variables().get(&id) { - var - } else if let Some(var) = context - .project()? - .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))? - .global_variables() - .get(&id) - { - &Rc::clone(var) - } else { - hq_bad_proj!("variable not found") - }; - vec![IrOpcode::data_variable(DataVariableFields(RcVar( - Rc::clone(variable), - )))] - } - BlockOpcode::control_if => 'block: { - let BlockArrayOrId::Id(substack_id) = - match block_info.inputs.get("SUBSTACK") { - Some(input) => input, - None => break 'block vec![IrOpcode::hq_drop], - } - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - else { - hq_bad_proj!("malformed SUBSTACK input") - }; - let Some(substack_block) = blocks.get(&substack_id) else { - hq_bad_proj!("SUBSTACK block doesn't seem to exist") - }; - let (next_block, if_next_blocks, else_next_blocks) = - if let Some(ref next_block) = block_info.next { - ( - Some(NextBlock::ID(next_block.clone())), - final_next_blocks.extend_with_inner(NextBlockInfo { - yield_first: false, - block: NextBlock::ID(next_block.clone()), - }), - final_next_blocks.clone(), + .chain( + #[expect( + clippy::wildcard_enum_match_arm, + reason = "too many opcodes to match individually" + )] + match &block_info.opcode { + BlockOpcode::operator_add => vec![IrOpcode::operator_add], + BlockOpcode::operator_subtract => vec![IrOpcode::operator_subtract], + BlockOpcode::operator_multiply => vec![IrOpcode::operator_multiply], + BlockOpcode::operator_divide => vec![IrOpcode::operator_divide], + BlockOpcode::looks_say => vec![IrOpcode::looks_say(LooksSayFields { + debug: context.debug, + target_idx: context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .index(), + })], + BlockOpcode::looks_think => vec![IrOpcode::looks_think(LooksThinkFields { + debug: context.debug, + target_idx: context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .index(), + })], + BlockOpcode::operator_join => vec![IrOpcode::operator_join], + BlockOpcode::operator_length => vec![IrOpcode::operator_length], + BlockOpcode::operator_contains => vec![IrOpcode::operator_contains], + BlockOpcode::operator_letter_of => vec![IrOpcode::operator_letter_of], + BlockOpcode::sensing_dayssince2000 => vec![IrOpcode::sensing_dayssince2000], + BlockOpcode::operator_lt => vec![IrOpcode::operator_lt], + BlockOpcode::operator_gt => vec![IrOpcode::operator_gt], + BlockOpcode::operator_equals => vec![IrOpcode::operator_equals], + BlockOpcode::operator_not => vec![IrOpcode::operator_not], + BlockOpcode::data_setvariableto => { + let sb3::Field::ValueId(_val, maybe_id) = + block_info.fields.get("VARIABLE").ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - missing field VARIABLE" + ) + })? + else { + hq_bad_proj!( + "invalid project.json - missing variable id for VARIABLE field" + ); + }; + let id = maybe_id.clone().ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - null variable id for VARIABLE field" ) - } else if let (Some(next_block_info), popped_next_blocks) = - final_next_blocks.clone().pop_inner() + })?; + let target = context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; + let variable = if let Some(var) = target.variables().get(&id) { + var + } else if let Some(var) = context + .project()? + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .global_variables() + .get(&id) { - ( - Some(next_block_info.block), - final_next_blocks.clone(), - popped_next_blocks, + &Rc::clone(var) + } else { + hq_bad_proj!("variable not found") + }; + vec![IrOpcode::data_setvariableto(DataSetvariabletoFields( + RcVar(Rc::clone(variable)), + ))] + } + BlockOpcode::data_changevariableby => { + let sb3::Field::ValueId(_val, maybe_id) = + block_info.fields.get("VARIABLE").ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - missing field VARIABLE" + ) + })? + else { + hq_bad_proj!( + "invalid project.json - missing variable id for VARIABLE field" + ); + }; + let id = maybe_id.clone().ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - null variable id for VARIABLE field" ) + })?; + let target = context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; + let variable = if let Some(var) = target.variables().get(&id) { + var + } else if let Some(var) = context + .project()? + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .global_variables() + .get(&id) + { + &Rc::clone(var) } else { - ( - None, - final_next_blocks.clone(), // preserve termination behaviour - final_next_blocks.clone(), + hq_bad_proj!("variable not found") + }; + vec![ + IrOpcode::data_variable(DataVariableFields(RcVar(Rc::clone( + variable, + )))), + IrOpcode::operator_add, + IrOpcode::data_setvariableto(DataSetvariabletoFields(RcVar( + Rc::clone(variable), + ))), + ] + } + BlockOpcode::data_variable => { + let sb3::Field::ValueId(_val, maybe_id) = + block_info.fields.get("VARIABLE").ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - missing field VARIABLE" + ) + })? + else { + hq_bad_proj!( + "invalid project.json - missing variable id for VARIABLE field" + ); + }; + let id = maybe_id.clone().ok_or_else(|| { + make_hq_bad_proj!( + "invalid project.json - null variable id for VARIABLE field" ) + })?; + let target = context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; + let variable = if let Some(var) = target.variables().get(&id) { + var + } else if let Some(var) = context + .project()? + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? + .global_variables() + .get(&id) + { + &Rc::clone(var) + } else { + hq_bad_proj!("variable not found") }; - let if_step = Step::from_block( - substack_block, - substack_id, - blocks, - context.clone(), - context.project()?, - if_next_blocks, - )?; - let else_step = if next_block.is_some() { - match next_block.clone().unwrap() { - NextBlock::ID(id) => { + vec![IrOpcode::data_variable(DataVariableFields(RcVar( + Rc::clone(variable), + )))] + } + BlockOpcode::control_if => 'block: { + let BlockArrayOrId::Id(substack_id) = + match block_info.inputs.get("SUBSTACK") { + Some(input) => input, + None => break 'block vec![IrOpcode::hq_drop], + } + .get_1() + .ok_or_else(|| make_hq_bug!(""))? + .clone() + .ok_or_else(|| make_hq_bug!(""))? + else { + hq_bad_proj!("malformed SUBSTACK input") + }; + let Some(substack_block) = blocks.get(&substack_id) else { + hq_bad_proj!("SUBSTACK block doesn't seem to exist") + }; + let (next_block, if_next_blocks, else_next_blocks) = + #[expect( + clippy::option_if_let_else, + reason = "map_or_else alternative is too complex" + )] + if let Some(ref next_block) = block_info.next { + ( + Some(NextBlock::ID(next_block.clone())), + final_next_blocks.extend_with_inner(NextBlockInfo { + yield_first: false, + block: NextBlock::ID(next_block.clone()), + }), + final_next_blocks.clone(), + ) + } else if let (Some(next_block_info), popped_next_blocks) = + final_next_blocks.clone().pop_inner() + { + ( + Some(next_block_info.block), + final_next_blocks.clone(), + popped_next_blocks, + ) + } else { + ( + None, + final_next_blocks.clone(), // preserve termination behaviour + final_next_blocks.clone(), + ) + }; + let if_step = Step::from_block( + substack_block, + substack_id, + blocks, + context, + &context.project()?, + if_next_blocks, + )?; + let else_step = match next_block { + Some(NextBlock::ID(id)) => { let Some(next_block_block) = blocks.get(&id.clone()) else { hq_bad_proj!("next block doesn't exist") }; @@ -605,174 +635,178 @@ fn from_normal_block( next_block_block, id.clone(), blocks, - context.clone(), - context.project()?, + context, + &context.project()?, else_next_blocks, )? } - NextBlock::Step(step) => step + Some(NextBlock::Step(step)) => step .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))?, - } - } else { - Step::new_terminating(context.clone(), context.project()?)? - }; - should_break = true; - vec![IrOpcode::control_if_else(ControlIfElseFields { - branch_if: if_step, - branch_else: else_step, - })] - } - BlockOpcode::control_if_else => 'block: { - let BlockArrayOrId::Id(substack1_id) = - match block_info.inputs.get("SUBSTACK") { - Some(input) => input, - None => break 'block vec![IrOpcode::hq_drop], - } - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - else { - hq_bad_proj!("malformed SUBSTACK input") - }; - let Some(substack1_block) = blocks.get(&substack1_id) else { - hq_bad_proj!("SUBSTACK block doesn't seem to exist") - }; - let BlockArrayOrId::Id(substack2_id) = - match block_info.inputs.get("SUBSTACK2") { - Some(input) => input, - None => break 'block vec![IrOpcode::hq_drop], - } - .get_1() - .ok_or(make_hq_bug!(""))? - .clone() - .ok_or(make_hq_bug!(""))? - else { - hq_bad_proj!("malformed SUBSTACK2 input") - }; - let Some(substack2_block) = blocks.get(&substack2_id) else { - hq_bad_proj!("SUBSTACK2 block doesn't seem to exist") - }; - let next_blocks = if let Some(ref next_block) = block_info.next { - final_next_blocks.extend_with_inner(NextBlockInfo { - yield_first: false, - block: NextBlock::ID(next_block.clone()), - }) - } else { - final_next_blocks.clone() // preserve termination behaviour - }; - let if_step = Step::from_block( - substack1_block, - substack1_id, - blocks, - context.clone(), - context.project()?, - next_blocks.clone(), - )?; - let else_step = Step::from_block( - substack2_block, - substack2_id, - blocks, - context.clone(), - context.project()?, - next_blocks, - )?; - should_break = true; - vec![IrOpcode::control_if_else(ControlIfElseFields { - branch_if: if_step, - branch_else: else_step, - })] - } - BlockOpcode::control_repeat => { - let variable = RcVar(Rc::new(Variable::new( - IrType::Int, - sb3::VarVal::Float(0.0), - context.warp, - ))); - let condition_instructions = vec![ - IrOpcode::data_variable(DataVariableFields(variable.clone())), - IrOpcode::hq_integer(HqIntegerFields(1)), - IrOpcode::operator_subtract, - IrOpcode::data_teevariable(DataTeevariableFields(variable.clone())), - IrOpcode::hq_integer(HqIntegerFields(0)), - IrOpcode::operator_gt, - ]; - let first_condition_instructions = Some(vec![ - IrOpcode::data_variable(DataVariableFields(variable.clone())), - IrOpcode::hq_integer(HqIntegerFields(0)), - IrOpcode::operator_gt, - ]); - let setup_instructions = vec![ - IrOpcode::hq_cast(HqCastFields(IrType::Int)), - IrOpcode::data_setvariableto(DataSetvariabletoFields(variable)), - ]; - generate_loop( - context.warp, - &mut should_break, - block_info, - blocks, - context, - final_next_blocks.clone(), - first_condition_instructions, - condition_instructions, - false, - setup_instructions, - )? - } - BlockOpcode::control_repeat_until => { - let condition_instructions = - inputs(block_info, blocks, context, context.project()?)?; - let first_condition_instructions = None; - let setup_instructions = vec![IrOpcode::hq_drop]; - generate_loop( - context.warp, - &mut should_break, - block_info, - blocks, - context, - final_next_blocks.clone(), - first_condition_instructions, - condition_instructions, - true, - setup_instructions, - )? - } - BlockOpcode::procedures_call => { - let target = context - .target - .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))?; - let procs = target.procedures()?; - let serde_json::Value::String(proccode) = block_info - .mutation - .mutations - .get("proccode") - .ok_or(make_hq_bad_proj!("missing proccode on procedures_call"))? - else { - hq_bad_proj!("non-string proccode on procedures_call") - }; - let proc = procs.get(proccode.as_str()).ok_or(make_hq_bad_proj!( - "non-existant proccode on procedures_call" - ))?; - let warp = context.warp || proc.always_warped(); - if warp { - proc.compile_warped(blocks)?; - vec![IrOpcode::procedures_call_warp(ProceduresCallWarpFields { - proc: Rc::clone(proc), + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?, + None => { + Step::new_terminating(context.clone(), &context.project()?)? + } + }; + should_break = true; + vec![IrOpcode::control_if_else(ControlIfElseFields { + branch_if: if_step, + branch_else: else_step, })] - } else { - hq_todo!("non-warped procedures") } - } - BlockOpcode::argument_reporter_boolean => { - procedure_argument(ProcArgType::Boolean, block_info, context)? - } - BlockOpcode::argument_reporter_string_number => { - procedure_argument(ProcArgType::StringNumber, block_info, context)? - } - other => hq_todo!("unimplemented block: {:?}", other), - }) + BlockOpcode::control_if_else => 'block: { + let BlockArrayOrId::Id(substack1_id) = + match block_info.inputs.get("SUBSTACK") { + Some(input) => input, + None => break 'block vec![IrOpcode::hq_drop], + } + .get_1() + .ok_or_else(|| make_hq_bug!(""))? + .clone() + .ok_or_else(|| make_hq_bug!(""))? + else { + hq_bad_proj!("malformed SUBSTACK input") + }; + let Some(substack1_block) = blocks.get(&substack1_id) else { + hq_bad_proj!("SUBSTACK block doesn't seem to exist") + }; + let BlockArrayOrId::Id(substack2_id) = + match block_info.inputs.get("SUBSTACK2") { + Some(input) => input, + None => break 'block vec![IrOpcode::hq_drop], + } + .get_1() + .ok_or_else(|| make_hq_bug!(""))? + .clone() + .ok_or_else(|| make_hq_bug!(""))? + else { + hq_bad_proj!("malformed SUBSTACK2 input") + }; + let Some(substack2_block) = blocks.get(&substack2_id) else { + hq_bad_proj!("SUBSTACK2 block doesn't seem to exist") + }; + let next_blocks = block_info.next.as_ref().map_or_else( + || final_next_blocks.clone(), // preserve termination behaviour + |next_block| { + final_next_blocks.extend_with_inner(NextBlockInfo { + yield_first: false, + block: NextBlock::ID(next_block.clone()), + }) + }, + ); + let if_step = Step::from_block( + substack1_block, + substack1_id, + blocks, + context, + &context.project()?, + next_blocks.clone(), + )?; + let else_step = Step::from_block( + substack2_block, + substack2_id, + blocks, + context, + &context.project()?, + next_blocks, + )?; + should_break = true; + vec![IrOpcode::control_if_else(ControlIfElseFields { + branch_if: if_step, + branch_else: else_step, + })] + } + BlockOpcode::control_repeat => { + let variable = RcVar(Rc::new(Variable::new( + IrType::Int, + sb3::VarVal::Float(0.0), + context.warp, + ))); + let condition_instructions = vec![ + IrOpcode::data_variable(DataVariableFields(variable.clone())), + IrOpcode::hq_integer(HqIntegerFields(1)), + IrOpcode::operator_subtract, + IrOpcode::data_teevariable(DataTeevariableFields(variable.clone())), + IrOpcode::hq_integer(HqIntegerFields(0)), + IrOpcode::operator_gt, + ]; + let first_condition_instructions = Some(vec![ + IrOpcode::data_variable(DataVariableFields(variable.clone())), + IrOpcode::hq_integer(HqIntegerFields(0)), + IrOpcode::operator_gt, + ]); + let setup_instructions = vec![ + IrOpcode::hq_cast(HqCastFields(IrType::Int)), + IrOpcode::data_setvariableto(DataSetvariabletoFields(variable)), + ]; + generate_loop( + context.warp, + &mut should_break, + block_info, + blocks, + context, + final_next_blocks.clone(), + first_condition_instructions, + condition_instructions, + false, + setup_instructions, + )? + } + BlockOpcode::control_repeat_until => { + let condition_instructions = + inputs(block_info, blocks, context, &context.project()?)?; + let first_condition_instructions = None; + let setup_instructions = vec![IrOpcode::hq_drop]; + generate_loop( + context.warp, + &mut should_break, + block_info, + blocks, + context, + final_next_blocks.clone(), + first_condition_instructions, + condition_instructions, + true, + setup_instructions, + )? + } + BlockOpcode::procedures_call => { + let target = context + .target + .upgrade() + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; + let procs = target.procedures()?; + let serde_json::Value::String(proccode) = block_info + .mutation + .mutations + .get("proccode") + .ok_or_else(|| { + make_hq_bad_proj!("missing proccode on procedures_call") + })? + else { + hq_bad_proj!("non-string proccode on procedures_call") + }; + let proc = procs.get(proccode.as_str()).ok_or_else(|| { + make_hq_bad_proj!("non-existant proccode on procedures_call") + })?; + let warp = context.warp || proc.always_warped(); + if warp { + proc.compile_warped(blocks)?; + vec![IrOpcode::procedures_call_warp(ProceduresCallWarpFields { + proc: Rc::clone(proc), + })] + } else { + hq_todo!("non-warped procedures") + } + } + BlockOpcode::argument_reporter_boolean => { + procedure_argument(ProcArgType::Boolean, block_info, context)? + } + BlockOpcode::argument_reporter_string_number => { + procedure_argument(ProcArgType::StringNumber, block_info, context)? + } + other => hq_todo!("unimplemented block: {:?}", other), + }, + ) .collect(), ); if should_break { @@ -781,15 +815,19 @@ fn from_normal_block( curr_block = if let Some(ref next_id) = block_info.next { let next_block = blocks .get(next_id) - .ok_or(make_hq_bad_proj!("missing next block"))?; - if opcodes.last().is_some_and(|o| o.requests_screen_refresh()) && !context.warp { + .ok_or_else(|| make_hq_bad_proj!("missing next block"))?; + if opcodes + .last() + .is_some_and(super::super::instructions::IrOpcode::requests_screen_refresh) + && !context.warp + { opcodes.push(IrOpcode::hq_yield(HqYieldFields { mode: YieldMode::Schedule(Rc::downgrade(&Step::from_block( next_block, next_id.clone(), blocks, - context.clone(), - Weak::clone(&project), + context, + project, final_next_blocks.clone(), )?)), })); @@ -804,9 +842,11 @@ fn from_normal_block( NextBlock::ID(id) => { let next_block = blocks .get(&id) - .ok_or(make_hq_bad_proj!("missing next block"))?; + .ok_or_else(|| make_hq_bad_proj!("missing next block"))?; if (popped_next.yield_first - || opcodes.last().is_some_and(|o| o.requests_screen_refresh())) + || opcodes.last().is_some_and( + super::super::instructions::IrOpcode::requests_screen_refresh, + )) && !context.warp { opcodes.push(IrOpcode::hq_yield(HqYieldFields { @@ -814,8 +854,8 @@ fn from_normal_block( next_block, id.clone(), blocks, - context.clone(), - Weak::clone(&project), + context, + project, new_next_blocks_stack, )?)), })); @@ -827,22 +867,23 @@ fn from_normal_block( } NextBlock::Step(ref step) => { if (popped_next.yield_first - || opcodes.last().is_some_and(|o| o.requests_screen_refresh())) + || opcodes.last().is_some_and( + super::super::instructions::IrOpcode::requests_screen_refresh, + )) && !context.warp { opcodes.push(IrOpcode::hq_yield(HqYieldFields { mode: YieldMode::Schedule(Weak::clone(step)), })); - None } else { opcodes.push(IrOpcode::hq_yield(HqYieldFields { mode: YieldMode::Inline( step.upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))?, + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?, ), })); - None } + None } } } else if final_next_blocks.terminating() { @@ -860,34 +901,98 @@ fn from_normal_block( fn from_special_block(block_array: &BlockArray, context: &StepContext) -> HQResult { Ok(match block_array { BlockArray::NumberOrAngle(ty, value) => match ty { - 4 | 5 | 8 => IrOpcode::hq_float(HqFloatFields(*value)), - 6 | 7 => IrOpcode::hq_integer(HqIntegerFields(*value as i32)), + // number, positive number or angle + 4 | 5 | 8 => { + // proactively convert to an integer if possible; + // if a float is needed, it will be cast at const-fold time (TODO), + // and if integers are disabled (TODO) a float will be emitted anyway + if value % 1.0 == 0.0 { + #[expect( + clippy::cast_possible_truncation, + reason = "integer-ness already confirmed; `as` is saturating." + )] + IrOpcode::hq_integer(HqIntegerFields(*value as i32)) + } else { + IrOpcode::hq_float(HqFloatFields(*value)) + } + } + // positive integer, integer + 6 | 7 => { + hq_assert!( + value % 1.0 == 0.0, + "inputs of integer or positive integer types should be integers" + ); + #[expect( + clippy::cast_possible_truncation, + reason = "integer-ness already confirmed; `as` is saturating." + )] + IrOpcode::hq_integer(HqIntegerFields(*value as i32)) + } _ => hq_bad_proj!("bad project json (block array of type ({}, f64))", ty), }, + // a string input should really be a colour or a string, but often numbers + // are serialised as strings in the project.json BlockArray::ColorOrString(ty, value) => match ty { + // number, positive number or integer 4 | 5 | 8 => { - IrOpcode::hq_float(HqFloatFields(value.parse().map_err(|_| make_hq_bug!(""))?)) + let float = value + .parse() + .map_err(|_| make_hq_bug!("expected a float-parseable value"))?; + // proactively convert to an integer if possible; + // if a float is needed, it will be cast at const-fold time (TODO), + // and if integers are disabled (TODO) a float will be emitted anyway + if float % 1.0 == 0.0 { + #[expect( + clippy::cast_possible_truncation, + reason = "integer-ness already confirmed; `as` is saturating." + )] + IrOpcode::hq_integer(HqIntegerFields(float as i32)) + } else { + IrOpcode::hq_float(HqFloatFields(float)) + } } + // integer, positive integer 6 | 7 => IrOpcode::hq_integer(HqIntegerFields( - value.parse().map_err(|_| make_hq_bug!(""))?, + value + .parse() + .map_err(|_| make_hq_bug!("expected and int-parseable value"))?, )), - 9 => hq_todo!(""), - 10 => IrOpcode::hq_text(HqTextFields(value.clone())), + // colour + 9 => hq_todo!("colour inputs"), + // string + 10 => 'textBlock: { + // proactively convert to a number + if let Ok(float) = value.parse::() { + if *float.to_string() == **value { + break 'textBlock if float % 1.0 == 0.0 { + #[expect( + clippy::cast_possible_truncation, + reason = "integer-ness already confirmed; `as` is saturating." + )] + IrOpcode::hq_integer(HqIntegerFields(float as i32)) + } else { + IrOpcode::hq_float(HqFloatFields(float)) + }; + } + } + IrOpcode::hq_text(HqTextFields(value.clone())) + } _ => hq_bad_proj!("bad project json (block array of type ({}, string))", ty), }, BlockArray::Broadcast(ty, _name, id) | BlockArray::VariableOrList(ty, _name, id, _, _) => { match ty { + 11 => hq_todo!("broadcast input"), 12 => { let target = context .target .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))?; + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?; let variable = if let Some(var) = target.variables().get(id) { var } else if let Some(var) = context .project()? .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))? + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? .global_variables() .get(id) { @@ -897,7 +1002,11 @@ fn from_special_block(block_array: &BlockArray, context: &StepContext) -> HQResu }; IrOpcode::data_variable(DataVariableFields(RcVar(Rc::clone(variable)))) } - _ => hq_todo!(""), + 13 => hq_todo!("list input"), + _ => hq_bad_proj!( + "bad project json (block array of type ({}, string, string))", + ty + ), } } }) diff --git a/src/ir/context.rs b/src/ir/context.rs index e782daa2..acfbbf1c 100644 --- a/src/ir/context.rs +++ b/src/ir/context.rs @@ -18,7 +18,7 @@ impl StepContext { Ok(self .target .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))? + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? .project()) } } diff --git a/src/ir/proc.rs b/src/ir/proc.rs index a06c09be..31e6ff5a 100644 --- a/src/ir/proc.rs +++ b/src/ir/proc.rs @@ -7,7 +7,7 @@ use core::cell::Ref; use lazy_regex::{lazy_regex, Lazy}; use regex::Regex; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum PartialStep { None, StartedCompilation, @@ -15,8 +15,8 @@ pub enum PartialStep { } impl PartialStep { - pub fn is_finished(&self) -> bool { - matches!(self, PartialStep::Finished(_)) + pub const fn is_finished(&self) -> bool { + matches!(self, Self::Finished(_)) } } @@ -68,15 +68,19 @@ impl Proc { Ok(self.warped_first_step.try_borrow()?) } - pub fn first_step_id(&self) -> &Option> { - &self.first_step_id + #[expect( + clippy::borrowed_box, + reason = "reference is inside borrow so difficult to unbox" + )] + pub const fn first_step_id(&self) -> Option<&Box> { + self.first_step_id.as_ref() } - pub fn always_warped(&self) -> bool { + pub const fn always_warped(&self) -> bool { self.always_warped } - pub fn context(&self) -> &ProcContext { + pub const fn context(&self) -> &ProcContext { &self.context } @@ -87,10 +91,10 @@ impl Proc { static ARG_REGEX: Lazy = lazy_regex!(r#"[^\\]%[nbs]"#); -fn arg_types_from_proccode(proccode: Box) -> Result, HQError> { +fn arg_types_from_proccode(proccode: &str) -> Result, HQError> { // https://github.com/scratchfoundation/scratch-blocks/blob/abbfe9/blocks_vertical/procedures.js#L207-L215 (*ARG_REGEX) - .find_iter(&proccode) + .find_iter(proccode) .map(|s| s.as_str().to_string().trim().to_string()) .filter(|s| s.as_str().starts_with('%')) .map(|s| s[..2].to_string()) @@ -113,14 +117,20 @@ impl Proc { Ok( match mutations .get(id) - .ok_or(make_hq_bad_proj!("missing {id} mutation"))? + .ok_or_else(|| make_hq_bad_proj!("missing {id} mutation"))? { serde_json::Value::Array(values) => values .iter() - .map(|val| match val { - serde_json::Value::String(s) => Ok(s.clone().into_boxed_str()), - _ => hq_bad_proj!("non-string {id} member in"), - }) + .map( + #[expect( + clippy::wildcard_enum_match_arm, + reason = "too many variants to match" + )] + |val| match val { + serde_json::Value::String(s) => Ok(s.clone().into_boxed_str()), + _ => hq_bad_proj!("non-string {id} member in"), + }, + ) .collect::>>()?, serde_json::Value::String(string_arr) => { if string_arr == "[]" { @@ -128,15 +138,18 @@ impl Proc { } else { string_arr .strip_prefix("[\"") - .ok_or(make_hq_bug!("malformed {id} array"))? + .ok_or_else(|| make_hq_bug!("malformed {id} array"))? .strip_suffix("\"]") - .ok_or(make_hq_bug!("malformed {id} array"))? + .ok_or_else(|| make_hq_bug!("malformed {id} array"))? .split("\",\"") .map(Box::from) .collect::>() } } - _ => hq_bad_proj!("non-array {id}"), + serde_json::Value::Null + | serde_json::Value::Bool(_) + | serde_json::Value::Number(_) + | serde_json::Value::Object(_) => hq_bad_proj!("non-array {id}"), }, ) } @@ -149,27 +162,35 @@ impl Proc { hq_assert!(prototype .block_info() .is_some_and(|info| info.opcode == BlockOpcode::procedures_prototype)); + #[expect( + clippy::unwrap_used, + reason = "previously asserted that block_info is Some" + )] let mutations = &prototype.block_info().unwrap().mutation.mutations; - let serde_json::Value::String(proccode) = mutations.get("proccode").ok_or( - make_hq_bad_proj!("missing proccode on procedures_prototype"), - )? + let serde_json::Value::String(proccode) = mutations + .get("proccode") + .ok_or_else(|| make_hq_bad_proj!("missing proccode on procedures_prototype"))? else { hq_bad_proj!("proccode wasn't a string"); }; - let arg_types = arg_types_from_proccode(proccode.as_str().into())?; + let arg_types = arg_types_from_proccode(proccode.as_str())?; let Some(def_block) = blocks.get( + #[expect( + clippy::unwrap_used, + reason = "previously asserted that block_info is Some" + )] &prototype .block_info() .unwrap() .parent .clone() - .ok_or(make_hq_bad_proj!("prototype block without parent"))?, + .ok_or_else(|| make_hq_bad_proj!("prototype block without parent"))?, ) else { hq_bad_proj!("no definition block found for {proccode}") }; let first_step_id = def_block .block_info() - .ok_or(make_hq_bad_proj!("special block where normal def expected"))? + .ok_or_else(|| make_hq_bad_proj!("special block where normal def expected"))? .next .clone(); let Some(warp_val) = mutations.get("warp") else { @@ -182,17 +203,22 @@ impl Proc { "false" => false, _ => hq_bad_proj!("unexpected string for warp mutation for {proccode}"), }, - _ => hq_bad_proj!("bad type for warp mutation for {proccode}"), + serde_json::Value::Null + | serde_json::Value::Number(_) + | serde_json::Value::Array(_) + | serde_json::Value::Object(_) => { + hq_bad_proj!("bad type for warp mutation for {proccode}") + } }; - let arg_ids = Proc::string_vec_mutation(mutations, "argumentids")?; - let arg_names = Proc::string_vec_mutation(mutations, "argumentnames")?; + let arg_ids = Self::string_vec_mutation(mutations, "argumentids")?; + let arg_names = Self::string_vec_mutation(mutations, "argumentnames")?; let context = ProcContext { arg_types, arg_ids, arg_names, target, }; - Ok(Rc::new(Proc { + Ok(Rc::new(Self { proccode: proccode.as_str().into(), always_warped: warp, non_warped_first_step: RefCell::new(PartialStep::None), @@ -225,15 +251,15 @@ impl Proc { step_context.project()?, )), Some(ref id) => { - let block = blocks.get(id).ok_or(make_hq_bad_proj!( - "procedure's first step block doesn't exist" - ))?; + let block = blocks.get(id).ok_or_else(|| { + make_hq_bad_proj!("procedure's first step block doesn't exist") + })?; Step::from_block( block, id.clone(), blocks, - step_context.clone(), - step_context.project()?, + &step_context, + &step_context.project()?, NextBlocks::new(false), )? } @@ -245,7 +271,7 @@ impl Proc { pub type ProcMap = BTreeMap, Rc>; -pub fn procs_from_target(sb3_target: &Sb3Target, ir_target: Rc) -> HQResult<()> { +pub fn procs_from_target(sb3_target: &Sb3Target, ir_target: &Rc) -> HQResult<()> { let mut proc_map = ir_target.procedures_mut()?; for block in sb3_target.blocks.values() { let Block::Normal { block_info, .. } = block else { @@ -254,7 +280,7 @@ pub fn procs_from_target(sb3_target: &Sb3Target, ir_target: Rc) -> HQR if block_info.opcode != BlockOpcode::procedures_prototype { continue; } - let proc = Proc::from_prototype(block, &sb3_target.blocks, Rc::downgrade(&ir_target))?; + let proc = Proc::from_prototype(block, &sb3_target.blocks, Rc::downgrade(ir_target))?; let proccode = proc.proccode(); proc_map.insert(proccode.into(), proc); } diff --git a/src/ir/project.rs b/src/ir/project.rs index e7a20a95..e778018d 100644 --- a/src/ir/project.rs +++ b/src/ir/project.rs @@ -15,28 +15,28 @@ pub struct IrProject { } impl IrProject { - pub fn threads(&self) -> &RefCell> { + pub const fn threads(&self) -> &RefCell> { &self.threads } - pub fn steps(&self) -> &RefCell { + pub const fn steps(&self) -> &RefCell { &self.steps } - pub fn targets(&self) -> &RefCell, Rc>> { + pub const fn targets(&self) -> &RefCell, Rc>> { &self.targets } - pub fn global_variables(&self) -> &BTreeMap, Rc> { + pub const fn global_variables(&self) -> &BTreeMap, Rc> { &self.global_variables } pub fn new(global_variables: BTreeMap, Rc>) -> Self { - IrProject { + Self { threads: RefCell::new(Box::new([])), - steps: RefCell::new(Default::default()), + steps: RefCell::new(IndexSet::default()), global_variables, - targets: RefCell::new(Default::default()), + targets: RefCell::new(IndexMap::default()), } } @@ -57,12 +57,12 @@ impl TryFrom for Rc { sb3.targets .iter() .find(|target| target.is_stage) - .ok_or(make_hq_bad_proj!("missing stage target"))?, + .ok_or_else(|| make_hq_bad_proj!("missing stage target"))?, ); - let project = Rc::new(IrProject::new(global_variables)); + let project = Self::new(IrProject::new(global_variables)); - let (threads, targets): (Vec<_>, Vec<_>) = sb3 + let (threads_vec, targets): (Vec<_>, Vec<_>) = sb3 .targets .iter() .enumerate() @@ -72,13 +72,13 @@ impl TryFrom for Rc { let ir_target = Rc::new(Target::new( target.is_stage, variables, - Rc::downgrade(&project), + Self::downgrade(&project), procedures, index .try_into() .map_err(|_| make_hq_bug!("target index out of bounds"))?, )); - procs_from_target(target, Rc::clone(&ir_target))?; + procs_from_target(target, &ir_target)?; let blocks = &target.blocks; let threads = blocks .iter() @@ -87,7 +87,7 @@ impl TryFrom for Rc { block, blocks, Rc::downgrade(&ir_target), - Rc::downgrade(&project), + &Self::downgrade(&project), target.comments.clone().iter().any(|(_id, comment)| { matches!(comment.block_id.clone(), Some(d) if &d == id) && *comment.text.clone() == *"hq-dbg" @@ -103,7 +103,7 @@ impl TryFrom for Rc { .iter() .cloned() .unzip(); - let threads = threads.into_iter().flatten().collect::>(); + let threads = threads_vec.into_iter().flatten().collect::>(); *project .threads .try_borrow_mut() diff --git a/src/ir/step.rs b/src/ir/step.rs index 42f3626b..1fda47a5 100644 --- a/src/ir/step.rs +++ b/src/ir/step.rs @@ -25,19 +25,19 @@ impl PartialEq for Step { impl Eq for Step {} impl Step { - pub fn context(&self) -> &StepContext { + pub const fn context(&self) -> &StepContext { &self.context } - pub fn opcodes(&self) -> &RefCell> { + pub const fn opcodes(&self) -> &RefCell> { &self.opcodes } - pub fn used_non_inline(&self) -> &RefCell { + pub const fn used_non_inline(&self) -> &RefCell { &self.used_non_inline } - pub fn inline(&self) -> &RefCell { + pub const fn inline(&self) -> &RefCell { &self.inline } @@ -51,7 +51,7 @@ impl Step { opcodes: Vec, project: Weak, ) -> Self { - Step { + Self { id: id.unwrap_or_else(|| Uuid::new_v4().to_string().into()), context, opcodes: RefCell::new(opcodes), @@ -65,12 +65,12 @@ impl Step { id: Option>, context: StepContext, opcodes: Vec, - project: Weak, + project: &Weak, ) -> HQResult> { - let step = Rc::new(Step::new(id, context, opcodes, Weak::clone(&project))); + let step = Rc::new(Self::new(id, context, opcodes, Weak::clone(project))); project .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))? + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? .steps() .try_borrow_mut() .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? @@ -79,7 +79,7 @@ impl Step { } pub fn new_empty() -> Self { - Step { + Self { id: "".into(), context: StepContext { target: Weak::new(), @@ -94,9 +94,9 @@ impl Step { } } - pub fn new_terminating(context: StepContext, project: Weak) -> HQResult> { + pub fn new_terminating(context: StepContext, project: &Weak) -> HQResult> { const ID: &str = "__terminating_step_hopefully_this_id_wont_cause_any_clashes"; - let step = Rc::new(Step { + let step = Rc::new(Self { id: ID.into(), context, opcodes: RefCell::new(vec![IrOpcode::hq_yield(HqYieldFields { @@ -104,11 +104,11 @@ impl Step { })]), used_non_inline: RefCell::new(false), inline: RefCell::new(false), - project: Weak::clone(&project), + project: Weak::clone(project), }); project .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))? + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? .steps() .try_borrow_mut() .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? @@ -126,13 +126,13 @@ impl Step { block: &Block, block_id: Box, blocks: &BlockMap, - context: StepContext, - project: Weak, + context: &StepContext, + project: &Weak, final_next_blocks: NextBlocks, - ) -> HQResult> { + ) -> HQResult> { if let Some(existing_step) = project .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))? + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? .steps() .try_borrow() .map_err(|_| make_hq_bug!("couldn't immutably borrow cell"))? @@ -142,21 +142,15 @@ impl Step { crate::log("step from_block already exists!"); return Ok(Rc::clone(existing_step)); } - let step = Rc::new(Step::new( + let step = Rc::new(Self::new( Some(block_id), context.clone(), - blocks::from_block( - block, - blocks, - &context, - Weak::clone(&project), - final_next_blocks, - )?, - Weak::clone(&project), + blocks::from_block(block, blocks, context, project, final_next_blocks)?, + Weak::clone(project), )); project .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))? + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))? .steps() .try_borrow_mut() .map_err(|_| make_hq_bug!("couldn't mutably borrow cell"))? @@ -168,7 +162,7 @@ impl Step { pub fn make_used_non_inline(&self) -> HQResult<()> { if *self.used_non_inline.try_borrow()? { return Ok(()); - }; + } *self .used_non_inline .try_borrow_mut() @@ -179,7 +173,7 @@ impl Step { pub fn make_inlined(&self) -> HQResult<()> { if *self.inline.try_borrow()? { return Ok(()); - }; + } *self .inline .try_borrow_mut() diff --git a/src/ir/target.rs b/src/ir/target.rs index a4e243c0..09bcb5a0 100644 --- a/src/ir/target.rs +++ b/src/ir/target.rs @@ -12,11 +12,11 @@ pub struct Target { } impl Target { - pub fn is_stage(&self) -> bool { + pub const fn is_stage(&self) -> bool { self.is_stage } - pub fn variables(&self) -> &BTreeMap, Rc> { + pub const fn variables(&self) -> &BTreeMap, Rc> { &self.variables } @@ -32,18 +32,18 @@ impl Target { Ok(self.procedures.try_borrow_mut()?) } - pub fn index(&self) -> u32 { + pub const fn index(&self) -> u32 { self.index } - pub fn new( + pub const fn new( is_stage: bool, variables: BTreeMap, Rc>, project: Weak, procedures: RefCell, Rc>>, index: u32, ) -> Self { - Target { + Self { is_stage, variables, project, diff --git a/src/ir/thread.rs b/src/ir/thread.rs index 741a7cd4..19716146 100644 --- a/src/ir/thread.rs +++ b/src/ir/thread.rs @@ -11,11 +11,11 @@ pub struct Thread { } impl Thread { - pub fn event(&self) -> Event { + pub const fn event(&self) -> Event { self.event } - pub fn first_step(&self) -> &Rc { + pub const fn first_step(&self) -> &Rc { &self.first_step } @@ -29,12 +29,13 @@ impl Thread { block: &Block, blocks: &BlockMap, target: Weak, - project: Weak, + project: &Weak, debug: bool, ) -> HQResult> { let Some(block_info) = block.block_info() else { return Ok(None); }; + #[expect(clippy::wildcard_enum_match_arm, reason = "too many variants to match")] let event = match block_info.opcode { BlockOpcode::event_whenflagclicked => Event::FlagCLicked, BlockOpcode::event_whenbackdropswitchesto @@ -48,27 +49,26 @@ impl Thread { } _ => return Ok(None), }; - let next_id = match &block_info.next { - Some(next) => next, - None => return Ok(None), + let Some(next_id) = &block_info.next else { + return Ok(None); }; let next = blocks .get(next_id) - .ok_or(make_hq_bug!("block not found in BlockMap"))?; + .ok_or_else(|| make_hq_bug!("block not found in BlockMap"))?; let first_step = Step::from_block( next, next_id.clone(), blocks, - StepContext { + &StepContext { target: Weak::clone(&target), proc_context: None, warp: false, // steps from top blocks are never warped debug, }, - Weak::clone(&project), + project, NextBlocks::new(true), )?; - Ok(Some(Thread { + Ok(Some(Self { event, first_step, target, diff --git a/src/ir/types.rs b/src/ir/types.rs index b4841012..599790f3 100644 --- a/src/ir/types.rs +++ b/src/ir/types.rs @@ -65,54 +65,59 @@ pub enum Type { Color, } impl Type { - pub const BASE_TYPES: [Type; 3] = [Type::String, Type::QuasiInt, Type::Float]; + pub const BASE_TYPES: [Self; 3] = [Self::String, Self::QuasiInt, Self::Float]; - pub fn is_base_type(&self) -> bool { - (!self.is_none()) && Type::BASE_TYPES.iter().any(|ty| ty.contains(*self)) + pub fn is_base_type(self) -> bool { + (!self.is_none()) && Self::BASE_TYPES.iter().any(|ty| ty.contains(self)) } - pub fn base_type(&self) -> Option { + pub fn base_type(self) -> Option { if !self.is_base_type() { return None; } - Type::BASE_TYPES + Self::BASE_TYPES .iter() - .cloned() - .find(|&ty| ty.contains(*self)) + .copied() + .find(|&ty| ty.contains(self)) } - pub fn base_types(&self) -> Box + '_> { + pub fn base_types(self) -> Box> { if self.is_none() { return Box::new(core::iter::empty()); } - Box::new(Type::BASE_TYPES.iter().filter(|ty| ty.intersects(*self))) + Box::new( + Self::BASE_TYPES + .iter() + .filter(move |ty| ty.intersects(self)) + .copied(), + ) } - pub fn maybe_positive(&self) -> bool { - self.contains(Type::IntPos) - || self.intersects(Type::FloatPos) - || self.contains(Type::BooleanTrue) + pub const fn maybe_positive(self) -> bool { + self.contains(Self::IntPos) + || self.intersects(Self::FloatPos) + || self.contains(Self::BooleanTrue) } - pub fn maybe_negative(&self) -> bool { - self.contains(Type::IntNeg) || self.intersects(Type::FloatNeg) + pub const fn maybe_negative(self) -> bool { + self.contains(Self::IntNeg) || self.intersects(Self::FloatNeg) } - pub fn maybe_zero(&self) -> bool { - self.contains(Type::IntZero) - || self.contains(Type::BooleanFalse) - || self.intersects(Type::FloatZero) + pub const fn maybe_zero(self) -> bool { + self.contains(Self::IntZero) + || self.contains(Self::BooleanFalse) + || self.intersects(Self::FloatZero) } - pub fn maybe_nan(&self) -> bool { - self.intersects(Type::FloatNan) || self.contains(Type::StringNan) + pub const fn maybe_nan(self) -> bool { + self.intersects(Self::FloatNan) || self.contains(Self::StringNan) } - pub fn none_if_false(condition: bool, if_true: Type) -> Type { + pub const fn none_if_false(condition: bool, if_true: Self) -> Self { if condition { if_true } else { - Type::none() + Self::none() } } } diff --git a/src/ir/variable.rs b/src/ir/variable.rs index 1f13714e..a6f1b28b 100644 --- a/src/ir/variable.rs +++ b/src/ir/variable.rs @@ -14,8 +14,8 @@ pub struct Variable { } impl Variable { - pub fn new(ty: Type, initial_value: VarVal, local: bool) -> Self { - Variable { + pub const fn new(ty: Type, initial_value: VarVal, local: bool) -> Self { + Self { possible_types: RefCell::new(ty), initial_value, local, @@ -31,11 +31,11 @@ impl Variable { self.possible_types.borrow() } - pub fn initial_value(&self) -> &VarVal { + pub const fn initial_value(&self) -> &VarVal { &self.initial_value } - pub fn local(&self) -> bool { + pub const fn local(&self) -> bool { self.local } } @@ -66,6 +66,7 @@ pub fn variables_from_target(target: &Sb3Target) -> BTreeMap, Rc HQResult { let sb3_proj = sb3::Sb3Project::try_from(proj)?; let ir_proj = sb3_proj.try_into()?; - optimisation::ir_optimise(Rc::clone(&ir_proj))?; - wasm::WasmProject::from_ir(ir_proj, flags)?.finish() + optimisation::ir_optimise(&ir_proj)?; + wasm::WasmProject::from_ir(&ir_proj, flags)?.finish() } diff --git a/src/optimisation.rs b/src/optimisation.rs index 1eac44ea..fe26e438 100644 --- a/src/optimisation.rs +++ b/src/optimisation.rs @@ -3,7 +3,7 @@ use crate::prelude::*; mod var_types; -pub fn ir_optimise(ir: Rc) -> HQResult<()> { - var_types::optimise_var_types(Rc::clone(&ir))?; +pub fn ir_optimise(ir: &Rc) -> HQResult<()> { + var_types::optimise_var_types(ir)?; Ok(()) } diff --git a/src/optimisation/var_types.rs b/src/optimisation/var_types.rs index bff3b5e4..8d8277d1 100644 --- a/src/optimisation/var_types.rs +++ b/src/optimisation/var_types.rs @@ -2,7 +2,7 @@ use crate::instructions::{DataSetvariabletoFields, IrOpcode}; use crate::ir::{IrProject, Type as IrType}; use crate::prelude::*; -pub fn optimise_var_types(project: Rc) -> HQResult<()> { +pub fn optimise_var_types(project: &Rc) -> HQResult<()> { crate::log("optimise vars"); for step in project.steps().borrow().iter() { let mut type_stack: Vec = vec![]; // a vector of types, and where they came from diff --git a/src/registry.rs b/src/registry.rs index 5e84e397..39cdcfe8 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -11,8 +11,8 @@ impl Default for MapRegistry where K: Hash + Eq + Clone, { - fn default() -> MapRegistry { - MapRegistry(RefCell::new(Default::default())) + fn default() -> Self { + Self(RefCell::new(IndexMap::default())) } } @@ -37,8 +37,8 @@ impl Default for SetRegistry where K: Hash + Eq + Clone, { - fn default() -> SetRegistry { - SetRegistry(RefCell::new(Default::default())) + fn default() -> Self { + Self(RefCell::new(IndexMap::default())) } } @@ -69,7 +69,7 @@ pub trait Registry: Sized { fn register(&self, key: Self::Key, value: Self::Value) -> HQResult where N: TryFrom, - >::Error: alloc::fmt::Debug, + >::Error: core::fmt::Debug, { self.registry() .try_borrow_mut() @@ -80,7 +80,7 @@ pub trait Registry: Sized { self.registry() .try_borrow()? .get_index_of(&key) - .ok_or(make_hq_bug!("couldn't find entry in Registry"))?, + .ok_or_else(|| make_hq_bug!("couldn't find entry in Registry"))?, ) .map_err(|_| make_hq_bug!("registry item index out of bounds")) } @@ -88,7 +88,7 @@ pub trait Registry: Sized { fn register_override(&self, key: Self::Key, value: Self::Value) -> HQResult where N: TryFrom, - >::Error: alloc::fmt::Debug, + >::Error: core::fmt::Debug, { self.registry() .try_borrow_mut() @@ -99,7 +99,7 @@ pub trait Registry: Sized { self.registry() .try_borrow()? .get_index_of(&key) - .ok_or(make_hq_bug!("couldn't find entry in Registry"))?, + .ok_or_else(|| make_hq_bug!("couldn't find entry in Registry"))?, ) .map_err(|_| make_hq_bug!("registry item index out of bounds")) } @@ -109,9 +109,9 @@ pub trait RegistryDefault: Registry { fn register_default(&self, key: Self::Key) -> HQResult where N: TryFrom, - >::Error: alloc::fmt::Debug, + >::Error: core::fmt::Debug, { - self.register(key, Default::default()) + self.register(key, Self::Value::default()) } } diff --git a/src/sb3.rs b/src/sb3.rs index e8c2a7dd..acfbb2d2 100644 --- a/src/sb3.rs +++ b/src/sb3.rs @@ -32,8 +32,8 @@ pub struct Comment { /// A possible block opcode, encompassing the default block pallette, the pen extension, /// and a few hidden but non-obsolete blocks. A block being listed here does not imply that -/// it is supported by HyperQuark. -#[allow(non_camel_case_types)] +/// it is supported by `HyperQuark`. +#[expect(non_camel_case_types, reason = "opcodes are snake_case")] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub enum BlockOpcode { control_repeat, @@ -262,10 +262,9 @@ pub enum Field { impl Field { // this isn't auto implemented by EnumFieldGetter because Field::Value is actually a tuple // in a tuple, so that serde correctly parses a single-item array - pub fn get_0(&self) -> &Option { + pub const fn get_0(&self) -> Option<&VarVal> { match self { - Field::Value((val,)) => val, - Field::ValueId(val, _) => val, + Self::ValueId(val, _) | Self::Value((val,)) => val.as_ref(), } } } @@ -285,9 +284,9 @@ pub struct Mutation { impl Default for Mutation { fn default() -> Self { - Mutation { + Self { tag_name: "mutation".into(), - children: Default::default(), + children: vec![], mutations: BTreeMap::new(), } } @@ -309,8 +308,8 @@ pub struct BlockInfo { } /// the data format of a costume -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] -#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[expect(non_camel_case_types, reason = "lowercase in project.json")] pub enum CostumeDataFormat { png, svg, @@ -495,7 +494,7 @@ impl TryFrom<&str> for Sb3Project { err.line(), err.column() ), - _ => hq_bad_proj!("Failed to deserialize json"), + Category::Io => hq_bug!("Failed to deserialize json due to IO error"), }, } } diff --git a/src/wasm.rs b/src/wasm.rs index 59b24c4d..db8d0614 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -9,15 +9,13 @@ mod tables; mod type_registry; mod variable; -pub(crate) use external::{ExternalEnvironment, ExternalFunctionRegistry}; +pub use external::{ExternalEnvironment, ExternalFunctionRegistry}; pub use flags::WasmFlags; -pub(crate) use func::{Instruction as InternalInstruction, StepFunc}; -pub(crate) use globals::{ - Exportable as GlobalExportable, GlobalRegistry, Mutable as GlobalMutable, -}; -pub(crate) use project::{FinishedWasm, WasmProject}; -pub(crate) use registries::Registries; -pub(crate) use strings::StringRegistry; -pub(crate) use tables::{TableOptions, TableRegistry}; -pub(crate) use type_registry::TypeRegistry; -pub(crate) use variable::VariableRegistry; +pub use func::{Instruction as InternalInstruction, StepFunc}; +pub use globals::{Exportable as GlobalExportable, GlobalRegistry, Mutable as GlobalMutable}; +pub use project::{FinishedWasm, WasmProject}; +pub use registries::Registries; +pub use strings::StringRegistry; +pub use tables::{TableOptions, TableRegistry}; +pub use type_registry::TypeRegistry; +pub use variable::VariableRegistry; diff --git a/src/wasm/flags.rs b/src/wasm/flags.rs index 80b225d5..c3e73111 100644 --- a/src/wasm/flags.rs +++ b/src/wasm/flags.rs @@ -10,21 +10,21 @@ pub enum WasmStringType { //Manual, } -#[derive(Copy, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] #[wasm_bindgen] pub enum WasmOpt { On, Off, } -#[derive(Copy, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] #[wasm_bindgen] pub enum Scheduler { TypedFuncRef, CallIndirect, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] #[wasm_bindgen] pub enum WasmFeature { ReferenceTypes, @@ -34,14 +34,14 @@ pub enum WasmFeature { #[wasm_bindgen] pub fn all_wasm_features() -> Vec { - use WasmFeature::*; + use WasmFeature::{JSStringBuiltins, ReferenceTypes, TypedFunctionReferences}; vec![ReferenceTypes, TypedFunctionReferences, JSStringBuiltins] } // no &self because wasm_bidgen doesn't like it #[wasm_bindgen] pub fn wasm_feature_detect_name(feat: WasmFeature) -> String { - use WasmFeature::*; + use WasmFeature::{JSStringBuiltins, ReferenceTypes, TypedFunctionReferences}; match feat { ReferenceTypes => "referenceTypes", TypedFunctionReferences => "typedFunctionReferences", @@ -52,6 +52,10 @@ pub fn wasm_feature_detect_name(feat: WasmFeature) -> String { #[derive(Clone, Serialize, Deserialize)] #[wasm_bindgen(getter_with_clone)] +#[expect( + clippy::unsafe_derive_deserialize, + reason = "wasm-bindgen introduces unsafe methods" +)] pub struct FlagInfo { /// a human-readable name for the flag pub name: String, @@ -64,11 +68,11 @@ pub struct FlagInfo { #[wasm_bindgen] impl FlagInfo { fn new() -> Self { - FlagInfo { - name: "".into(), - description: "".into(), - ty: "".into(), - wasm_features: Default::default(), + Self { + name: String::new(), + description: String::new(), + ty: String::new(), + wasm_features: BTreeMap::default(), } } @@ -93,11 +97,10 @@ impl FlagInfo { } #[wasm_bindgen] - pub fn wasm_features(&self, flag: String) -> Option> { - self.wasm_features.get(&flag).cloned() + pub fn wasm_features(&self, flag: &str) -> Option> { + self.wasm_features.get(flag).cloned() } - #[allow(clippy::wrong_self_convention)] #[wasm_bindgen] pub fn to_js(&self) -> HQResult { serde_wasm_bindgen::to_value(&self) @@ -122,6 +125,10 @@ macro_rules! ty_str { /// compilation flags #[derive(Copy, Clone, Serialize, Deserialize)] #[wasm_bindgen] +#[expect( + clippy::unsafe_derive_deserialize, + reason = "wasm-bindgen introduces unsafe methods" +)] pub struct WasmFlags { pub string_type: WasmStringType, pub wasm_opt: WasmOpt, @@ -130,13 +137,23 @@ pub struct WasmFlags { #[wasm_bindgen] impl WasmFlags { + // these attributes should be at the item level, but they don't seem to work there. + #![expect( + clippy::wrong_self_convention, + clippy::trivially_copy_pass_by_ref, + reason = "to_js taking `self` causes weird errors when wasm-fied" + )] + #![expect( + clippy::needless_pass_by_value, + reason = "wasm-bindgen does not support &[T]" + )] + #[wasm_bindgen] - pub fn from_js(js: JsValue) -> HQResult { + pub fn from_js(js: JsValue) -> HQResult { serde_wasm_bindgen::from_value(js) .map_err(|_| make_hq_bug!("couldn't convert JsValue to WasmFlags")) } - #[allow(clippy::wrong_self_convention)] #[wasm_bindgen] pub fn to_js(&self) -> HQResult { serde_wasm_bindgen::to_value(&self) @@ -144,9 +161,9 @@ impl WasmFlags { } #[wasm_bindgen(constructor)] - pub fn new(wasm_features: Vec) -> WasmFlags { - crate::log(format!("{:?}", wasm_features).as_str()); - WasmFlags { + pub fn new(wasm_features: Vec) -> Self { + crate::log(format!("{wasm_features:?}").as_str()); + Self { wasm_opt: WasmOpt::On, string_type: if wasm_features.contains(&WasmFeature::JSStringBuiltins) { WasmStringType::JsStringBuiltins diff --git a/src/wasm/func.rs b/src/wasm/func.rs index 3cbd4953..beed5b9e 100644 --- a/src/wasm/func.rs +++ b/src/wasm/func.rs @@ -18,45 +18,45 @@ pub enum Instruction { impl Instruction { pub fn eval( &self, - steps: Rc, StepFunc>>>, + steps: &Rc, StepFunc>>>, imported_func_count: u32, ) -> HQResult> { Ok(match self { - Instruction::Immediate(instr) => instr.clone(), - Instruction::LazyStepRef(step) => { + Self::Immediate(instr) => instr.clone(), + Self::LazyStepRef(step) => { let step_index: u32 = steps .try_borrow()? .get_index_of( &step .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))?, + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?, ) - .ok_or(make_hq_bug!("couldn't find step in step map"))? + .ok_or_else(|| make_hq_bug!("couldn't find step in step map"))? .try_into() .map_err(|_| make_hq_bug!("step index out of bounds"))?; WInstruction::RefFunc(imported_func_count + step_index) } - Instruction::LazyStepIndex(step) => { + Self::LazyStepIndex(step) => { let step_index: i32 = steps .try_borrow()? .get_index_of( &step .upgrade() - .ok_or(make_hq_bug!("couldn't upgrade Weak"))?, + .ok_or_else(|| make_hq_bug!("couldn't upgrade Weak"))?, ) - .ok_or(make_hq_bug!("couldn't find step in step map"))? + .ok_or_else(|| make_hq_bug!("couldn't find step in step map"))? .try_into() .map_err(|_| make_hq_bug!("step index out of bounds"))?; WInstruction::I32Const(step_index) } - Instruction::LazyWarpedProcCall(proc) => { + Self::LazyWarpedProcCall(proc) => { let PartialStep::Finished(ref step) = *proc.warped_first_step()? else { hq_bug!("tried to use uncompiled warped procedure step") }; let step_index: u32 = steps .try_borrow()? .get_index_of(step) - .ok_or(make_hq_bug!("couldn't find step in step map"))? + .ok_or_else(|| make_hq_bug!("couldn't find step in step map"))? .try_into() .map_err(|_| make_hq_bug!("step index out of bounds"))?; WInstruction::Call(imported_func_count + step_index) @@ -83,15 +83,15 @@ impl StepFunc { Rc::clone(&self.registries) } - pub fn flags(&self) -> WasmFlags { + pub const fn flags(&self) -> WasmFlags { self.flags } - pub fn instructions(&self) -> &RefCell> { + pub const fn instructions(&self) -> &RefCell> { &self.instructions } - pub fn steps(&self) -> Rc, StepFunc>>> { + pub fn steps(&self) -> Rc, Self>>> { Rc::clone(&self.steps) } @@ -102,10 +102,10 @@ impl StepFunc { /// creates a new step function, with one paramter pub fn new( registries: Rc, - steps: Rc, StepFunc>>>, + steps: Rc, Self>>>, flags: WasmFlags, ) -> Self { - StepFunc { + Self { locals: RefCell::new(vec![]), instructions: RefCell::new(vec![]), params: Box::new([ValType::I32]), @@ -113,7 +113,7 @@ impl StepFunc { registries, flags, steps, - local_variables: Default::default(), + local_variables: RefCell::new(HashMap::default()), } } @@ -123,10 +123,10 @@ impl StepFunc { params: Box<[ValType]>, output: Option, registries: Rc, - steps: Rc, StepFunc>>>, + steps: Rc, Self>>>, flags: WasmFlags, - ) -> HQResult { - Ok(StepFunc { + ) -> Self { + Self { locals: RefCell::new(vec![]), instructions: RefCell::new(vec![]), params, @@ -134,11 +134,11 @@ impl StepFunc { registries, flags, steps, - local_variables: Default::default(), - }) + local_variables: RefCell::new(HashMap::default()), + } } - pub fn local_variable(&self, var: RcVar) -> HQResult { + pub fn local_variable(&self, var: &RcVar) -> HQResult { Ok( match self.local_variables.try_borrow_mut()?.entry(var.clone()) { hash_map::Entry::Occupied(entry) => *entry.get(), @@ -178,21 +178,17 @@ impl StepFunc { self, funcs: &mut FunctionSection, code: &mut CodeSection, - steps: Rc, StepFunc>>>, + steps: &Rc, Self>>>, imported_func_count: u32, ) -> HQResult<()> { let mut func = Function::new_with_locals_types(self.locals.take()); for instruction in self.instructions.take() { - func.instruction(&instruction.eval(Rc::clone(&steps), imported_func_count)?); + func.instruction(&instruction.eval(steps, imported_func_count)?); } func.instruction(&wasm_encoder::Instruction::End); let type_index = self.registries().types().register_default(( self.params.into(), - if let Some(output) = self.output { - vec![output] - } else { - vec![] - }, + self.output.map_or_else(Vec::new, |output| vec![output]), ))?; funcs.function(type_index); code.function(&func); @@ -201,10 +197,10 @@ impl StepFunc { pub fn compile_step( step: Rc, - steps: Rc, StepFunc>>>, + steps: &Rc, Self>>>, registries: Rc, flags: WasmFlags, - ) -> HQResult { + ) -> HQResult { if let Some(step_func) = steps.try_borrow()?.get(&step) { return Ok(step_func.clone()); } @@ -212,13 +208,13 @@ impl StepFunc { let arg_types = proc_context .arg_types() .iter() - .cloned() + .copied() .map(WasmProject::ir_type_to_wasm) .collect::>>()?; - let input_types = arg_types.iter().chain(&[ValType::I32]).cloned().collect(); - StepFunc::new_with_types(input_types, None, registries, Rc::clone(&steps), flags)? + let input_types = arg_types.iter().chain(&[ValType::I32]).copied().collect(); + Self::new_with_types(input_types, None, registries, Rc::clone(steps), flags) } else { - StepFunc::new(registries, Rc::clone(&steps), flags) + Self::new(registries, Rc::clone(steps), flags) }; let mut instrs = vec![]; let mut type_stack = vec![]; @@ -229,7 +225,7 @@ impl StepFunc { instrs.append(&mut wrap_instruction( &step_func, Rc::clone(&inputs), - opcode.clone(), + opcode, )?); if let Some(output) = opcode.output_type(inputs)? { type_stack.push(output); @@ -243,7 +239,7 @@ impl StepFunc { Ok(step_func) } - pub fn compile_inner_step(&self, step: Rc) -> HQResult> { + pub fn compile_inner_step(&self, step: &Rc) -> HQResult> { step.make_inlined()?; let mut instrs = vec![]; let mut type_stack = vec![]; @@ -251,11 +247,7 @@ impl StepFunc { let inputs = type_stack .splice((type_stack.len() - opcode.acceptable_inputs().len()).., []) .collect(); - instrs.append(&mut wrap_instruction( - self, - Rc::clone(&inputs), - opcode.clone(), - )?); + instrs.append(&mut wrap_instruction(self, Rc::clone(&inputs), opcode)?); if let Some(output) = opcode.output_type(inputs)? { type_stack.push(output); } diff --git a/src/wasm/globals.rs b/src/wasm/globals.rs index 3b44b561..078b4620 100644 --- a/src/wasm/globals.rs +++ b/src/wasm/globals.rs @@ -34,13 +34,13 @@ impl GlobalRegistry { globals: &mut GlobalSection, exports: &mut ExportSection, ) { - for (key, (ty, initial, mutable, export)) in self.registry().take() { + for (key, (ty, suggested_initial, mutable, export)) in self.registry().take() { if *export { exports.export(&key, ExportKind::Global, globals.len()); } - let initial = match &*key { + let actual_initial = match &*key { "noop_func" => ConstExpr::ref_func(imports.len()), - _ => initial, + _ => suggested_initial, }; globals.global( GlobalType { @@ -48,7 +48,7 @@ impl GlobalRegistry { mutable: *mutable, shared: false, }, - &initial, + &actual_initial, ); } } diff --git a/src/wasm/project.rs b/src/wasm/project.rs index 33cd64f5..7f8835ca 100644 --- a/src/wasm/project.rs +++ b/src/wasm/project.rs @@ -12,35 +12,26 @@ use wasm_encoder::{ }; use wasm_gen::wasm; -#[allow(dead_code)] -pub mod byte_offset { - pub const REDRAW_REQUESTED: i32 = 0; - pub const THREAD_NUM: i32 = 4; - pub const THREADS: i32 = 8; -} - /// A respresentation of a WASM representation of a project. Cannot be created directly; /// use `TryFrom`. pub struct WasmProject { - #[allow(dead_code)] flags: WasmFlags, steps: Rc, StepFunc>>>, - /// maps an event to a list of *step_func* indices (NOT function indices) which are + /// maps an event to a list of *`step_func`* indices (NOT function indices) which are /// triggered by that event. events: BTreeMap>, registries: Rc, target_names: Vec>, - #[allow(dead_code)] environment: ExternalEnvironment, } impl WasmProject { - #[allow(dead_code)] + #[expect(dead_code, reason = "pub item may be used in the future")] pub fn new(flags: WasmFlags, environment: ExternalEnvironment) -> Self { - WasmProject { + Self { flags, - steps: Default::default(), - events: Default::default(), + steps: Rc::new(RefCell::new(IndexMap::default())), + events: BTreeMap::default(), environment, registries: Rc::new(Registries::default()), target_names: vec![], @@ -51,13 +42,13 @@ impl WasmProject { Rc::clone(&self.registries) } - #[allow(dead_code)] - pub fn environment(&self) -> ExternalEnvironment { + #[expect(dead_code, reason = "pub item may be used in future")] + pub const fn environment(&self) -> ExternalEnvironment { self.environment } - pub fn steps(&self) -> Rc, StepFunc>>> { - Rc::clone(&self.steps) + pub const fn steps(&self) -> &Rc, StepFunc>>> { + &self.steps } /// maps a broad IR type to a WASM type @@ -143,9 +134,13 @@ impl WasmProject { init: None, }, )?; + #[expect( + clippy::cast_possible_truncation, + reason = "step count should never get near to u32::MAX" + )] let func_indices: Vec = (0..step_count) - .map(|i| self.imported_func_count().unwrap() + i as u32) - .collect(); + .map(|i| Ok(self.imported_func_count()? + i as u32)) + .collect::>()?; elements.active( Some(steps_table_index), &ConstExpr::i32_const(0), @@ -198,7 +193,7 @@ impl WasmProject { target_names: self .target_names .into_iter() - .map(|bstr| bstr.into()) + .map(core::convert::Into::into) .collect(), }) } @@ -240,7 +235,7 @@ impl WasmProject { fn threads_table_index(&self) -> HQResult where N: TryFrom, - >::Error: alloc::fmt::Debug, + >::Error: core::fmt::Debug, { let step_func_ty = self .registries() @@ -266,7 +261,7 @@ impl WasmProject { fn steps_table_index(&self) -> HQResult where N: TryFrom, - >::Error: alloc::fmt::Debug, + >::Error: core::fmt::Debug, { match self.flags.scheduler { Scheduler::CallIndirect => self.registries().tables().register( @@ -376,7 +371,7 @@ impl WasmProject { codes: &mut CodeSection, exports: &mut ExportSection, ) -> HQResult<()> { - for (event, indices) in self.events.iter() { + for (event, indices) in &self.events { self.finish_event( match event { Event::FlagCLicked => "flag_clicked", @@ -444,8 +439,7 @@ impl WasmProject { BrIf(0), End, ], - _ => wasm![ - // Default: typed function references (call_ref) + Scheduler::TypedFuncRef => wasm![ TableSize(self.threads_table_index()?), LocalTee(1), I32Eqz, @@ -483,24 +477,20 @@ impl WasmProject { Ok(()) } - pub fn from_ir(ir_project: Rc, flags: WasmFlags) -> HQResult { - let steps: Rc, StepFunc>>> = Default::default(); + pub fn from_ir(ir_project: &Rc, flags: WasmFlags) -> HQResult { + let steps: Rc, StepFunc>>> = + Rc::new(RefCell::new(IndexMap::default())); let registries = Rc::new(Registries::default()); - let mut events: BTreeMap> = Default::default(); + let mut events: BTreeMap> = BTreeMap::default(); StepFunc::compile_step( Rc::new(Step::new_empty()), - Rc::clone(&steps), + &steps, Rc::clone(®istries), flags, )?; // compile every step for step in ir_project.steps().try_borrow()?.iter() { - StepFunc::compile_step( - Rc::clone(step), - Rc::clone(&steps), - Rc::clone(®istries), - flags, - )?; + StepFunc::compile_step(Rc::clone(step), &steps, Rc::clone(®istries), flags)?; } // mark first steps as used in a non-inline context for thread in ir_project.threads().try_borrow()?.iter() { @@ -519,14 +509,14 @@ impl WasmProject { steps .try_borrow()? .get_index_of(thread.first_step()) - .ok_or(make_hq_bug!( - "Thread's first_step wasn't found in Thread::steps()" - ))?, + .ok_or_else(|| { + make_hq_bug!("Thread's first_step wasn't found in Thread::steps()") + })?, ) .map_err(|_| make_hq_bug!("step func index out of bounds"))?, ); } - Ok(WasmProject { + Ok(Self { flags, steps, events, @@ -558,10 +548,10 @@ mod tests { #[test] fn empty_project_is_valid_wasm() { let registries = Rc::new(Registries::default()); - let steps = Default::default(); + let steps = Rc::new(RefCell::new(IndexMap::default())); StepFunc::compile_step( Rc::new(Step::new_empty()), - Rc::clone(&steps), + &steps, Rc::clone(®istries), WasmFlags::new(all_wasm_features()), ) diff --git a/src/wasm/registries.rs b/src/wasm/registries.rs index 18f3edbf..19947d0e 100644 --- a/src/wasm/registries.rs +++ b/src/wasm/registries.rs @@ -15,33 +15,33 @@ pub struct Registries { impl Default for Registries { fn default() -> Self { - let globals = Default::default(); + let globals = Rc::new(GlobalRegistry::default()); let variables = VariableRegistry::new(&globals); - Registries { + Self { globals, variables, - strings: Default::default(), - external_functions: Default::default(), - tables: Default::default(), - types: Default::default(), + strings: StringRegistry::default(), + external_functions: ExternalFunctionRegistry::default(), + tables: TableRegistry::default(), + types: TypeRegistry::default(), } } } impl Registries { - pub fn strings(&self) -> &StringRegistry { + pub const fn strings(&self) -> &StringRegistry { &self.strings } - pub fn external_functions(&self) -> &ExternalFunctionRegistry { + pub const fn external_functions(&self) -> &ExternalFunctionRegistry { &self.external_functions } - pub fn types(&self) -> &TypeRegistry { + pub const fn types(&self) -> &TypeRegistry { &self.types } - pub fn tables(&self) -> &TableRegistry { + pub const fn tables(&self) -> &TableRegistry { &self.tables } @@ -49,7 +49,7 @@ impl Registries { &self.globals } - pub fn variables(&self) -> &VariableRegistry { + pub const fn variables(&self) -> &VariableRegistry { &self.variables } } diff --git a/src/wasm/strings.rs b/src/wasm/strings.rs index 685a2b1a..5b764f76 100644 --- a/src/wasm/strings.rs +++ b/src/wasm/strings.rs @@ -9,7 +9,7 @@ impl StringRegistry { .take() .keys() .cloned() - .map(|s| s.into_string()) + .map(str::into_string) .collect() } } diff --git a/src/wasm/tables.rs b/src/wasm/tables.rs index 2580886e..779d11c8 100644 --- a/src/wasm/tables.rs +++ b/src/wasm/tables.rs @@ -33,11 +33,11 @@ impl TableRegistry { { // TODO: allow choosing whether to export a table or not? exports.export(&key, ExportKind::Table, tables.len()); - let init = match &*key { + let maybe_init = match &*key { "threads" => Some(ConstExpr::ref_func(imports.len())), _ => init, }; - if let Some(init) = init { + if let Some(init) = maybe_init { tables.table_with_init( TableType { element_type, diff --git a/src/wasm/variable.rs b/src/wasm/variable.rs index 77f48a74..783bddad 100644 --- a/src/wasm/variable.rs +++ b/src/wasm/variable.rs @@ -7,15 +7,15 @@ use super::{GlobalExportable, GlobalMutable, GlobalRegistry, WasmProject}; pub struct VariableRegistry(Rc); impl VariableRegistry { - fn globals(&self) -> &Rc { + const fn globals(&self) -> &Rc { &self.0 } pub fn new(globals: &Rc) -> Self { - VariableRegistry(Rc::clone(globals)) + Self(Rc::clone(globals)) } - pub fn register(&self, var: RcVar) -> HQResult { + pub fn register(&self, var: &RcVar) -> HQResult { self.globals().register( format!("__rcvar_{:p}", Rc::as_ptr(&var.0)).into(), (