diff --git a/src/men-sharp-asm/src/emulator.rs b/src/men-sharp-asm/src/emulator.rs index c6db58b..eb8772b 100644 --- a/src/men-sharp-asm/src/emulator.rs +++ b/src/men-sharp-asm/src/emulator.rs @@ -303,6 +303,10 @@ impl Emulator { _ => Value::Null, }, HeapInit::Boolean(v) => Value::Boolean(*v), + HeapInit::SByte(v) => Value::Int32(i32::from(*v)), + HeapInit::Byte(v) => Value::Int32(i32::from(*v)), + HeapInit::Int16(v) => Value::Int32(i32::from(*v)), + HeapInit::UInt16(v) => Value::Int32(i32::from(*v)), HeapInit::Int32(v) => Value::Int32(*v), HeapInit::Int64(v) => Value::Int64(*v), HeapInit::UInt32(v) => Value::UInt32(*v), @@ -563,6 +567,12 @@ impl Emulator { | "SystemInt32.__op_Remainder__SystemInt32_SystemInt32__SystemInt32" => { binary_i32!(|a: i32, b: i32| Value::Int32(a.wrapping_rem(b))) } + "SystemInt32.__op_LeftShift__SystemInt32_SystemInt32__SystemInt32" => { + binary_i32!(|a: i32, b: i32| Value::Int32(a.wrapping_shl(b as u32))) + } + "SystemInt32.__op_RightShift__SystemInt32_SystemInt32__SystemInt32" => { + binary_i32!(|a: i32, b: i32| Value::Int32(a.wrapping_shr(b as u32))) + } "SystemInt32.__op_UnaryMinus__SystemInt32__SystemInt32" => { let args = self.pop_arguments(2)?; let a = self.heap[args[0]].as_i32()?; @@ -1250,10 +1260,12 @@ impl Emulator { Ok(()) } "SystemConvert.__ToInt64__SystemInt32__SystemInt64" + | "SystemConvert.__ToInt64__SystemUInt32__SystemInt64" | "SystemConvert.__ToInt64__SystemObject__SystemInt64" => { let args = self.pop_arguments(2)?; let value = match &self.heap[args[0]] { Value::Int64(value) => *value, + Value::UInt32(value) => i64::from(*value), other => i64::from(other.as_i32()?), }; self.heap[args[1]] = Value::Int64(value); diff --git a/src/men-sharp-asm/src/lib.rs b/src/men-sharp-asm/src/lib.rs index 3e60005..41fffba 100644 --- a/src/men-sharp-asm/src/lib.rs +++ b/src/men-sharp-asm/src/lib.rs @@ -23,6 +23,39 @@ pub use world::World; mod tests { use super::*; + #[test] + fn floating_metadata_uses_dotnet_special_value_spellings() { + // Unity Mono parses Infinity/-Infinity, not Rust's inf/-inf. Keep + // finite formatting (including signed zero) and NaN unchanged. + let mut asm = Asm::new(); + for (name, value, expected) in [ + ("positive", f64::INFINITY, "Infinity"), + ("negative", f64::NEG_INFINITY, "-Infinity"), + ("nan", f64::NAN, "NaN"), + ("finite", 1.25, "1.25"), + ("zero", 0.0, "0.0"), + ("negative_zero", -0.0, "-0.0"), + ] { + asm.slot( + &format!("single_{name}"), + "SystemSingle", + HeapInit::Single(value as f32), + ); + asm.slot( + &format!("double_{name}"), + "SystemDouble", + HeapInit::Double(value), + ); + let meta = asm.program.to_meta_json().unwrap(); + for (prefix, kind) in [("single", "Single"), ("double", "Double")] { + let entry = format!( + "\"name\": \"{prefix}_{name}\", \"kind\": \"{kind}\", \"value\": \"{expected}\"" + ); + assert!(meta.contains(&entry), "{meta}"); + } + } + } + /// Small builder so tests read like assembly listings. struct Asm { program: Program, diff --git a/src/men-sharp-asm/src/program.rs b/src/men-sharp-asm/src/program.rs index 7795980..5639930 100644 --- a/src/men-sharp-asm/src/program.rs +++ b/src/men-sharp-asm/src/program.rs @@ -50,6 +50,10 @@ pub type UdonType = String; pub enum HeapInit { Null, Boolean(bool), + SByte(i8), + Byte(u8), + Int16(i16), + UInt16(u16), Int32(i32), Int64(i64), UInt32(u32), @@ -632,6 +636,18 @@ impl Program { HeapInit::Boolean(v) => { let _ = write!(out, "Boolean\", \"value\": \"{v}\""); } + HeapInit::SByte(v) => { + let _ = write!(out, "SByte\", \"value\": \"{v}\""); + } + HeapInit::Byte(v) => { + let _ = write!(out, "Byte\", \"value\": \"{v}\""); + } + HeapInit::Int16(v) => { + let _ = write!(out, "Int16\", \"value\": \"{v}\""); + } + HeapInit::UInt16(v) => { + let _ = write!(out, "UInt16\", \"value\": \"{v}\""); + } HeapInit::Int32(v) => { let _ = write!(out, "Int32\", \"value\": \"{v}\""); } @@ -645,10 +661,32 @@ impl Program { let _ = write!(out, "UInt64\", \"value\": \"{v}\""); } HeapInit::Single(v) => { - let _ = write!(out, "Single\", \"value\": \"{v:?}\""); + // Metadata is read by Unity Mono using invariant-culture Parse. + // Rust Debug spells infinity as inf, which that parser rejects. + if v.is_infinite() { + let text = if v.is_sign_negative() { + "-Infinity" + } else { + "Infinity" + }; + let _ = write!(out, "Single\", \"value\": \"{text}\""); + } else { + let _ = write!(out, "Single\", \"value\": \"{v:?}\""); + } } HeapInit::Double(v) => { - let _ = write!(out, "Double\", \"value\": \"{v:?}\""); + // Metadata is read by Unity Mono using invariant-culture Parse. + // Rust Debug spells infinity as inf, which that parser rejects. + if v.is_infinite() { + let text = if v.is_sign_negative() { + "-Infinity" + } else { + "Infinity" + }; + let _ = write!(out, "Double\", \"value\": \"{text}\""); + } else { + let _ = write!(out, "Double\", \"value\": \"{v:?}\""); + } } HeapInit::Decimal(v) => { out.push_str("Decimal\", \"value\": "); diff --git a/src/men-sharp-codegen/src/generator.rs b/src/men-sharp-codegen/src/generator.rs index 816b1aa..c29fbeb 100644 --- a/src/men-sharp-codegen/src/generator.rs +++ b/src/men-sharp-codegen/src/generator.rs @@ -3156,6 +3156,26 @@ impl<'a, 'ast> Generator<'a, 'ast> { ); return Some(slot); } + // Metadata stores all small integer constants as Int/UInt, + // but their boxed heap values must retain the declared type. + let integer = match constant { + ExternalConstant::Int(v) => Some(*v), + ExternalConstant::UInt(v) => i64::try_from(*v).ok(), + _ => None, + }; + if let Some(value) = integer { + let name = self.heap_type(ty); + let init = match name.as_str() { + "SystemSByte" => Some(HeapInit::SByte(value as i8)), + "SystemByte" => Some(HeapInit::Byte(value as u8)), + "SystemInt16" => Some(HeapInit::Int16(value as i16)), + "SystemUInt16" => Some(HeapInit::UInt16(value as u16)), + _ => None, + }; + if let Some(init) = init { + return Some(self.constant(&name, &value.to_string(), init)); + } + } let slot = match constant { ExternalConstant::Int(value) => match self.heap_type(ty).as_str() { "SystemInt64" => self.constant( diff --git a/src/men-sharp-codegen/src/generator/exceptions.rs b/src/men-sharp-codegen/src/generator/exceptions.rs index f814ad6..71f5c0a 100644 --- a/src/men-sharp-codegen/src/generator/exceptions.rs +++ b/src/men-sharp-codegen/src/generator/exceptions.rs @@ -887,19 +887,27 @@ impl<'a, 'ast> Generator<'a, 'ast> { divisor_type: &Type, span: Range, ) { - if self.heap_type(divisor_type) != "SystemInt32" { - return; - } - if let HeapInit::Int32(value) = self.program.data[divisor.0].init - && value != 0 + let name = self.heap_type(divisor_type); + let init = match name.as_str() { + "SystemInt32" => HeapInit::Int32(0), + "SystemInt64" => HeapInit::Int64(0), + "SystemUInt32" => HeapInit::UInt32(0), + "SystemUInt64" => HeapInit::UInt64(0), + _ => return, + }; + if matches!(self.program.data[divisor.0].init, + HeapInit::Int32(v) if v != 0) + || matches!(self.program.data[divisor.0].init, HeapInit::Int64(v) if v != 0) + || matches!(self.program.data[divisor.0].init, HeapInit::UInt32(v) if v != 0) + || matches!(self.program.data[divisor.0].init, HeapInit::UInt64(v) if v != 0) { return; } - let zero = self.int_constant(0); + let zero = self.constant(&name, "0", init); let is_zero = self.temp("SystemBoolean"); self.call_extern( ctx, - "SystemInt32.__op_Equality__SystemInt32_SystemInt32__SystemBoolean", + &format!("{name}.__op_Equality__{name}_{name}__SystemBoolean"), &[divisor, zero, is_zero], span.clone(), ); diff --git a/src/men-sharp-codegen/src/generator/expressions.rs b/src/men-sharp-codegen/src/generator/expressions.rs index dc5ff93..fcbc12e 100644 --- a/src/men-sharp-codegen/src/generator/expressions.rs +++ b/src/men-sharp-codegen/src/generator/expressions.rs @@ -1106,6 +1106,17 @@ impl<'a, 'ast> Generator<'a, 'ast> { { self.wrap_int32_to_small(ctx, source, &to_name, span) } + (Some(from_name), Some(to_name)) + if from_name == "SystemChar" + && matches!( + to_name.as_str(), + "SystemSingle" | "SystemDouble" | "SystemDecimal" + ) => + { + let int = self.corlib_type("Int32"); + let value = self.convert(ctx, source, from, &int, span.clone()); + self.convert(ctx, value, &int, to, span) + } (Some(from_name), Some(to_name)) if from_name != to_name => { let method = match to_name.as_str() { "SystemInt32" => Some("ToInt32"), @@ -1726,9 +1737,6 @@ impl<'a, 'ast> Generator<'a, 'ast> { if let Some(result) = self.user_operator_call(ctx, node, &[left, right], span.clone()) { return result; } - if matches!(operator, Divide | Modulo) { - self.check_divisor(ctx, right.0, right.1, span.clone()); - } let system_is = |ty: &Type, name: &str| { self.extern_type_name(ty) @@ -1772,11 +1780,38 @@ impl<'a, 'ast> Generator<'a, 'ast> { // binary numeric promotion (§12.4.7): `long == int`, `float * int` // — both operands are brought to the wider type first, as an extern // takes two slots of exactly its own type - let (left, right) = - self.promote_numeric_operands(ctx, operator, left, right, result_type, span.clone()); + let selected = node + .and_then(|id| self.bodies.numeric_promotions.get(&id)) + .cloned(); + let (left, right) = if let Some((left_type, right_type)) = selected { + ( + ( + self.convert(ctx, left.0, left.1, &left_type, span.clone()), + left_type, + ), + ( + self.convert(ctx, right.0, right.1, &right_type, span.clone()), + right_type, + ), + ) + } else { + self.promote_numeric_operands(ctx, operator, left, right, result_type, span.clone()) + }; let left = (left.0, &left.1); let right = (right.0, &right.1); + if matches!(operator, Divide | Modulo) { + self.check_divisor(ctx, right.0, right.1, span.clone()); + } + if operator == Modulo + && matches!( + self.extern_type_name(left.1).as_deref(), + Some("SystemUInt32" | "SystemInt64" | "SystemUInt64") + ) + { + return self.integer_remainder(ctx, left, right.0, span); + } + let name = match operator { Add => "op_Addition", Subtract => "op_Subtraction", @@ -2000,6 +2035,68 @@ impl<'a, 'ast> Generator<'a, 'ast> { } } + /// Udon lacks remainder externs for uint, long and ulong. The quotient + /// is truncated, so a - (a / b) * b gives the same remainder without + /// overflowing the product. Handle long.MinValue / -1 before dividing. + fn integer_remainder( + &mut self, + ctx: &mut Ctx<'ast>, + left: (DataId, &Type), + right: DataId, + span: Range, + ) -> Option { + let name = self.extern_type_name(left.1)?; + let out = self.temp(&name); + if name == "SystemInt64" { + let ordinary = self.fresh_label("remainder_divide"); + let minus_one = self.constant("SystemInt64", "-1", HeapInit::Int64(-1)); + let is_minus_one = self.temp("SystemBoolean"); + self.call_extern( + ctx, + "SystemInt64.__op_Equality__SystemInt64_SystemInt64__SystemBoolean", + &[right, minus_one, is_minus_one], + span.clone(), + ); + self.program.code.push(Op::Push(is_minus_one)); + self.program + .code + .push(Op::JumpIfFalse(Target::Label(ordinary))); + let minimum = self.constant( + "SystemInt64", + &i64::MIN.to_string(), + HeapInit::Int64(i64::MIN), + ); + let is_minimum = self.temp("SystemBoolean"); + self.call_extern( + ctx, + "SystemInt64.__op_Equality__SystemInt64_SystemInt64__SystemBoolean", + &[left.0, minimum, is_minimum], + span.clone(), + ); + self.program.code.push(Op::Push(is_minimum)); + self.program + .code + .push(Op::JumpIfFalse(Target::Label(ordinary))); + self.throw_new(ctx, &["System", "OverflowException"], None, span.clone()); + self.program.code.push(Op::Label(ordinary)); + } + let quotient = self.temp(&name); + let product = self.temp(&name); + for (op, args) in [ + ("op_Division", [left.0, right, quotient]), + ("op_Multiplication", [quotient, right, product]), + ("op_Subtraction", [left.0, product, out]), + ] { + self.call_extern( + ctx, + &format!("{name}.__{op}__{name}_{name}__{name}"), + &args, + span.clone(), + ); + } + Some(out) + } + /// The rank of a numeric type in binary promotion; `None` for anything /// that is not a primitive number. fn numeric_rank(&self, ty: &Type) -> Option { @@ -2018,10 +2115,9 @@ impl<'a, 'ast> Generator<'a, 'ast> { }) } - /// Both operands of a numeric operator converted to the promoted type: - /// the result type for arithmetic, the wider operand for comparisons. - /// A shift keeps its `int` count. Operands that are not both numeric - /// come back as they were. + /// Promotion for synthesized numeric operations such as ++. Explicit + /// expressions use the operand types recorded by the checker above. + /// A shift keeps its `int` count; non-numeric operands stay as they were. fn promote_numeric_operands( &mut self, ctx: &mut Ctx<'ast>, @@ -2032,40 +2128,27 @@ impl<'a, 'ast> Generator<'a, 'ast> { span: Range, ) -> ((DataId, Type), (DataId, Type)) { use BinaryOperator::*; - let as_is = ((left.0, left.1.clone()), (right.0, right.1.clone())); - let (Some(left_rank), Some(right_rank)) = - (self.numeric_rank(left.1), self.numeric_rank(right.1)) - else { - return as_is; + let system = self.type_system(); + let (Some(l), Some(r)) = (system.numeric_kind(left.1), system.numeric_kind(right.1)) else { + return ((left.0, left.1.clone()), (right.0, right.1.clone())); }; - if matches!(operator, LeftShift | RightShift) { - return as_is; - } - // one type on both sides: its own operator applies (Udon has - // `char == char`, and no promotion is needed) - if self.extern_type_name(left.1) == self.extern_type_name(right.1) { - return as_is; - } - let comparison = matches!( - operator, - Equal | NotEqual | LessThan | GreaterThan | LessThanEqual | GreaterThanEqual - ); - let target = if !comparison && self.numeric_rank(result_type).is_some() { - result_type.clone() - } else if left_rank >= right_rank { - left.1.clone() + let shift = matches!(operator, LeftShift | RightShift | UnsignedRightShift); + let kind = if shift { + Some(l.unary_promoted()) } else { - right.1.clone() + l.binary_promoted(r) }; - // a small type (byte, short, char) computes as an int - let target = if self.numeric_rank(&target) == Some(0) { + let target = kind + .map(|k| self.corlib_type(k.corlib_name())) + .unwrap_or_else(|| result_type.clone()); + let right_target = if shift { self.corlib_type("Int32") } else { - target + target.clone() }; let promoted_left = self.convert(ctx, left.0, left.1, &target, span.clone()); - let promoted_right = self.convert(ctx, right.0, right.1, &target, span); - ((promoted_left, target.clone()), (promoted_right, target)) + let promoted_right = self.convert(ctx, right.0, right.1, &right_target, span); + ((promoted_left, target), (promoted_right, right_target)) } /// A value as a `string`, via the type's own `ToString` extern. @@ -2254,6 +2337,11 @@ impl<'a, 'ast> Generator<'a, 'ast> { { return result; } + let operand = if self.numeric_rank(operand_type).is_some() { + self.convert(ctx, operand, operand_type, result_type, span.clone()) + } else { + operand + }; match operator { UnaryOperator::Not => { let out = self.temp("SystemBoolean"); @@ -2320,6 +2408,11 @@ impl<'a, 'ast> Generator<'a, 'ast> { "SystemUInt32" => { self.constant("SystemUInt32", "4294967295", HeapInit::UInt32(u32::MAX)) } + "SystemUInt64" => self.constant( + "SystemUInt64", + &u64::MAX.to_string(), + HeapInit::UInt64(u64::MAX), + ), _ => { self.error( ctx, @@ -2594,7 +2687,8 @@ impl<'a, 'ast> Generator<'a, 'ast> { /// `x` (byte, sbyte, short, ushort, char) C# computes in `int` — or in /// `y`'s wider type — and casts back, `x = (byte)(x + y)`; Udon has no /// operators on the small types, so this is also the only way to - /// compute it. Anything else computes in `x`'s own type. + /// compute it. The checker supplies the operand types for explicit + /// compound assignments, including constant-expression conversions. #[allow(clippy::too_many_arguments)] fn compound_result( &mut self, @@ -2606,14 +2700,29 @@ impl<'a, 'ast> Generator<'a, 'ast> { node: Option, ) -> Option { let target = current.1.clone(); - let computed = if self.numeric_rank(&target) == Some(0) { - match self.numeric_rank(value.1) { - Some(rank) if rank > 1 => value.1.clone(), - _ => self.corlib_type("Int32"), - } - } else { - target.clone() - }; + let system = self.type_system(); + let computed = + if let Some((left, _)) = node.and_then(|id| self.bodies.numeric_promotions.get(&id)) { + left.clone() + } else { + match (system.numeric_kind(&target), system.numeric_kind(value.1)) { + (Some(l), Some(r)) => { + let kind = if matches!( + operator, + BinaryOperator::LeftShift + | BinaryOperator::RightShift + | BinaryOperator::UnsignedRightShift + ) { + Some(l.unary_promoted()) + } else { + l.binary_promoted(r) + }; + kind.map(|k| self.corlib_type(k.corlib_name())) + .unwrap_or_else(|| target.clone()) + } + _ => target.clone(), + } + }; let result = self.emit_binary_operator( ctx, operator, diff --git a/src/men-sharp-compiler/tests/codegen.rs b/src/men-sharp-compiler/tests/codegen.rs index 2f873ba..bf4e75e 100644 --- a/src/men-sharp-compiler/tests/codegen.rs +++ b/src/men-sharp-compiler/tests/codegen.rs @@ -6,7 +6,12 @@ use men_sharp_asm::{Emulator, Value}; use men_sharp_compiler::{Compiler, CompilerSettings, SourceCode}; fn dotnet_shared_dir() -> Option { - for root in ["/usr/share/dotnet", "/usr/lib/dotnet"] { + let configured = std::env::var("DOTNET_ROOT").ok(); + for root in configured + .as_deref() + .into_iter() + .chain(["/usr/share/dotnet", "/usr/lib/dotnet"]) + { let base = std::path::Path::new(root).join("shared/Microsoft.NETCore.App"); if let Ok(entries) = std::fs::read_dir(base) { let mut versions: Vec<_> = entries.flatten().collect(); @@ -1782,7 +1787,7 @@ fn small_integer_compound_assignment_computes_in_int_and_casts_back() { char c = 'A'; c++; after = c.ToString(); // "B" - c += 1; + c += (char)1; code = c; // 67 char w = (char)65601; // 65601 & 0xFFFF = 65: an unchecked cast wraps wrapped = w.ToString(); // "A" @@ -14103,3 +14108,252 @@ fn an_initializer_udon_can_run_is_not_handed_to_the_proxy() { "{uasm}" ); } + +#[test] +fn integer_operands_are_promoted_before_calling_externs() { + // Small operands must be converted before calling Int32 externs; merely + // choosing the promoted result type leaves incompatible values on the heap. + for ty in ["sbyte", "byte", "short", "ushort", "char"] { + let source = format!( + r#" + namespace Game {{ public class Program {{ + public static object plus, minus, complement, remainder, shifted; + public static void Main() {{ + {ty} a = ({ty})13, b = ({ty})3; + plus = +a; minus = -a; complement = ~a; + remainder = a % b; shifted = a << b; + a %= b; a >>= b; + }} + }} }} + "# + ); + let Some((program, _)) = build(vec![SourceCode::new("test.cs", source.as_str())]) else { + return; + }; + let dump = program.dump(); + assert!(dump.contains("SystemConvert.__ToInt32__"), "{ty}: {dump}"); + assert!( + dump.contains("SystemInt32.__op_Remainder__SystemInt32_SystemInt32__SystemInt32"), + "{ty}: {dump}" + ); + assert!( + dump.contains("SystemInt32.__op_UnaryMinus__SystemInt32__SystemInt32"), + "{ty}: {dump}" + ); + } +} + +#[test] +fn signed_uint_comparisons_use_long_in_both_orders() { + // A negative signed operand cannot be converted to uint. Both operand + // orders must select long, as must unary minus on a uint operand. + let Some(emulator) = run( + r#" + namespace Game { public class Program { + public static int result; + public static long negated; + public static void Main() { + int a = -13; uint b = 3u; + if (a < b && b > a && a != b && b != a) result = 1; + negated = -b; + } + } } + "#, + "Main", + ) else { + return; + }; + assert_eq!(int_of(&emulator, "result"), 1); + assert!(matches!( + emulator.value_of("negated"), + Some(Value::Int64(-3)) + )); +} + +#[test] +fn wide_remainder_handles_boundaries() { + // Lowered remainder must preserve the dividend's sign and catchable exceptions. + let Some(emulator) = run( + r#" + namespace Game { public class Program { + public static long negative, min; + public static ulong wide, complement; + public static int caught; + public static void Main() { + long a = -13L, b = 3L; + negative = a % b; + long lowest = long.MinValue, minusOne = -1L; + // Unity Mono throws here; the lowered remainder must remain catchable. + try { min = lowest % minusOne; } catch (System.OverflowException) { min = 7; } + ulong top = ulong.MaxValue, divisor = 10UL; + wide = top % divisor; + complement = ~top; + long zero = 0L; + try { negative %= zero; } catch (System.DivideByZeroException) { caught = 1; } + } + } } + "#, + "Main", + ) else { + return; + }; + for (name, value) in [("negative", -1), ("min", 7)] { + assert!( + matches!(emulator.value_of(name), Some(Value::Int64(v)) if *v == value), + "{name}" + ); + } + assert!(matches!(emulator.value_of("wide"), Some(Value::UInt64(5)))); + assert!(matches!( + emulator.value_of("complement"), + Some(Value::UInt64(0)) + )); + + assert_eq!(int_of(&emulator, "caught"), 1); +} + +#[test] +fn small_integer_metadata_constants_keep_their_heap_types() { + // The emulator represents small integers as Int32, so checking its values + // would miss an initializer with the right value but the wrong boxed type. + // Inspect the heap initializers that the Unity importer uses instead. + let Some((program, _)) = build(vec![SourceCode::new( + "test.cs", + r#" + namespace Game { public class Program { + public static object[] limits; + public static void Main() { + limits = new object[] { sbyte.MinValue, byte.MaxValue, short.MinValue, ushort.MaxValue }; + } + } } + "#, + )]) else { + return; + }; + for (name, value) in [ + ("SystemSByte", -128), + ("SystemByte", 255), + ("SystemInt16", -32768), + ("SystemUInt16", 65535), + ] { + let slot = program.data.iter().find(|s| { + s.udon_type == name + && match s.init { + men_sharp_asm::HeapInit::SByte(v) => i64::from(v) == value, + men_sharp_asm::HeapInit::Byte(v) => i64::from(v) == value, + men_sharp_asm::HeapInit::Int16(v) => i64::from(v) == value, + men_sharp_asm::HeapInit::UInt16(v) => i64::from(v) == value, + _ => false, + } + }); + assert!(slot.is_some(), "missing typed constant {name}"); + } +} + +#[test] +fn invalid_numeric_operators_are_rejected() { + // A usable Udon conversion does not make an operator legal in C#. + // Reject invalid operand pairs during checking, including compound + // assignments whose right operand cannot implicitly convert to the left. + let Some(dir) = dotnet_shared_dir() else { + return; + }; + let compiler = Compiler::new(CompilerSettings::default()).unwrap(); + let bytes = vec![std::fs::read(dir.join("System.Private.CoreLib.dll")).unwrap()]; + let references = compiler.load_references(&bytes).unwrap(); + for body in [ + "long a = 1; ulong b = 1; var c = a + b;", + "long a = 1; ulong b = 1; var c = a == b;", + "int a = 1; uint b = 1; var c = a << b;", + "float a = 1; var c = a << 1;", + "decimal a = 1; double b = 1; var c = a + b;", + "int a = 1; var c = a && a;", + "ulong a = 1; var c = -a;", + "byte a = 1; long b = 1; a += b;", + "char a = 'a'; a += 1;", + "byte a = 1; a += 300;", + ] { + let source = format!("class Probe {{ void Run() {{ {body} }} }}"); + let files = compiler.parse(vec![SourceCode::new("test.cs", source)]); + let declarations = compiler.collect_declarations(&files); + let signatures = compiler.resolve_signatures(&declarations, &references); + let bodies = compiler.check_bodies(&declarations, &signatures, &references); + assert!(!bodies.errors.is_empty(), "accepted {body}"); + } +} + +#[test] +fn unsigned_compound_assignment_accepts_constant_operands() { + // A fitting int constant can select an unsigned operator. Starting at + // ulong.MaxValue exposes an incorrect signed conversion before arithmetic. + let Some(emulator) = run( + r#" + namespace Game { public class Program { + public static ulong wrapped; + public static void Main() { + ulong a = ulong.MaxValue; + a += 2; a -= 2; a *= 2; + wrapped = a; + } + } } + "#, + "Main", + ) else { + return; + }; + assert!(matches!(emulator.value_of("wrapped"), Some(Value::UInt64(v)) if *v == u64::MAX - 1)); + let Some(_) = build(vec![SourceCode::new( + "test.cs", + r#" + namespace Game { public class Program { + public static void Main() { sbyte a = 1; a += (-1); } + } } + "#, + )]) else { + return; + }; +} + +#[test] +fn constant_expressions_keep_the_selected_unsigned_operator() { + // Constant conversions also apply to const fields, locals and expressions. + // Code generation must retain the operator selected by the checker rather + // than infer it again from the constant's original Int32 heap type. + let Some(emulator) = run( + r#" + namespace Game { public class Program { + const int FieldDelta = 2; + public static ulong wrapped; + public static int comparison; + public static void Main() { + const int two = 2; + ulong a = ulong.MaxValue; + a += FieldDelta; + wrapped = a + (1 + 1); + if (a == (two - 1)) comparison = 1; + } + } } + "#, + "Main", + ) else { + return; + }; + assert!(matches!( + emulator.value_of("wrapped"), + Some(Value::UInt64(3)) + )); + assert_eq!(int_of(&emulator, "comparison"), 1); + let Some(_) = build(vec![SourceCode::new( + "test.cs", + r#" + namespace Game { public class Program { + public static void Main() { + const int delta = 2; + byte b = 1; b += delta; b += (1 << 32); + } + } } + "#, + )]) else { + return; + }; +} diff --git a/src/men-sharp-semantics/src/semantics/check.rs b/src/men-sharp-semantics/src/semantics/check.rs index 99953dd..64e88df 100644 --- a/src/men-sharp-semantics/src/semantics/check.rs +++ b/src/men-sharp-semantics/src/semantics/check.rs @@ -74,6 +74,8 @@ use crate::types::{FunctionSignature, Type}; pub struct BodyCheck { /// A type for every checked expression node, keyed by node identity. pub expression_types: HashMap, + /// Operand types selected for built-in numeric operators. + pub numeric_promotions: HashMap, /// Expression-level type references (casts, `new T`, locals, ...), resolved. pub resolved_types: HashMap, /// What each name/call/member node *bound to* — the code generator's map @@ -136,6 +138,7 @@ pub struct LocalFunctionSignature { impl BodyCheck { pub fn merge(&mut self, other: BodyCheck) { self.expression_types.extend(other.expression_types); + self.numeric_promotions.extend(other.numeric_promotions); self.resolved_types.extend(other.resolved_types); self.targets.extend(other.targets); self.attribute_types.extend(other.attribute_types); @@ -345,6 +348,7 @@ pub fn check_file( local_order: 0, local_function_order: HashMap::default(), expression_types: HashMap::default(), + numeric_promotions: HashMap::default(), attribute_types: HashMap::default(), targets: HashMap::default(), pattern_inputs: HashMap::default(), @@ -391,6 +395,7 @@ pub fn check_file( BodyCheck { expression_types: checker.expression_types, + numeric_promotions: checker.numeric_promotions, resolved_types: checker.resolver.out.type_of, targets: checker.targets, attribute_types: checker.attribute_types, @@ -538,6 +543,7 @@ struct Scope<'ast> { /// what a local function declared above it may not reach (CS0841). struct LocalVariable { ty: Type, + integer_constant: Option, order: usize, } @@ -591,6 +597,7 @@ struct Checker<'a, 'ast> { /// themselves are visible throughout their block, above and below. local_function_order: HashMap, expression_types: HashMap, + numeric_promotions: HashMap, targets: HashMap, /// See [`BodyCheck::attribute_types`]. attribute_types: HashMap, diff --git a/src/men-sharp-semantics/src/semantics/check/checker.rs b/src/men-sharp-semantics/src/semantics/check/checker.rs index 74e44fb..6b3ad8c 100644 --- a/src/men-sharp-semantics/src/semantics/check/checker.rs +++ b/src/men-sharp-semantics/src/semantics/check/checker.rs @@ -145,7 +145,14 @@ impl<'a, 'ast> Checker<'a, 'ast> { self.local_order += 1; let order = self.local_order; if let Some(scope) = self.locals.last_mut() { - scope.locals.insert(name, LocalVariable { ty, order }); + scope.locals.insert( + name, + LocalVariable { + ty, + order, + integer_constant: None, + }, + ); } } @@ -155,6 +162,7 @@ impl<'a, 'ast> Checker<'a, 'ast> { self.local_order += 1; LocalVariable { ty, + integer_constant: None, order: self.local_order, } } @@ -291,6 +299,163 @@ impl<'a, 'ast> Checker<'a, 'ast> { } } + /// Integral constant expressions used by operator overload resolution. + /// Keep values only for const locals, never for ordinary variables whose + /// initializer happens to be a literal. + pub(super) fn integer_constant(&self, expression: &Expression<'ast, 'ast>) -> Option { + self.integer_constant_inner(expression, 64) + } + + fn integer_constant_inner( + &self, + expression: &Expression<'ast, 'ast>, + depth: usize, + ) -> Option { + use crate::types::conversions::NumericKind::*; + if depth == 0 { + return None; + } + let Some(ty) = self.expression_types.get(&EntityID::from(expression)) else { + return super::exhaustive::integer_literal_value(expression).map(i128::from); + }; + let kind = self.system().numeric_kind(ty)?; + if !kind.is_integral() { + return None; + } + let value = match expression { + Expression::Primary(primary) + if self + .targets + .get( + &primary + .chain + .last() + .map(EntityID::from) + .unwrap_or_else(|| EntityID::from(&primary.left)), + ) + .is_some_and(|t| matches!(t, super::ResolvedTarget::Member(_))) => + { + let node = primary + .chain + .last() + .map(EntityID::from) + .unwrap_or_else(|| EntityID::from(&primary.left)); + let super::ResolvedTarget::Member(member) = self.targets.get(&node)? else { + return None; + }; + match &member.origin { + crate::types::lookup::MemberOrigin::External { member, .. } => { + match member.constant.as_ref()? { + crate::ExternalConstant::Int(v) => i128::from(*v), + crate::ExternalConstant::UInt(v) => i128::from(*v), + _ => return None, + } + } + crate::types::lookup::MemberOrigin::Source(id) => { + let symbol = self.resolver.declarations.table.symbol(*id); + let value = symbol.declarations.iter().find_map(|site| { + if let crate::symbol::SyntaxRef::Field { field, declarator } = + site.syntax + && field + .modifiers + .iter() + .any(|m| m.value == men_sharp_parser::ast::Modifier::Const) + && let Some(men_sharp_parser::ast::InitializerValue::Expression( + value, + )) = &declarator.initializer + { + return Some(value); + } + None + })?; + self.integer_constant_inner(value, depth - 1)? + } + _ => return None, + } + } + Expression::Primary(primary) if primary.chain.is_empty() => match &primary.left { + PrimaryLeft::Parenthesized { expression, .. } => { + self.integer_constant_inner(expression, depth - 1)? + } + PrimaryLeft::Identifier { name, .. } => { + self.locals + .iter() + .rev() + .find_map(|scope| scope.locals.get(name.value))? + .integer_constant? + } + _ => i128::from(super::exhaustive::integer_literal_value(expression)?), + }, + Expression::Cast(cast) => { + self.integer_constant_inner(cast.value.as_ref().ok()?, depth - 1)? + } + Expression::Unary(unary) => { + let value = self.integer_constant_inner(unary.operand.as_ref().ok()?, depth - 1)?; + match unary.operator.value { + UnaryOperator::Plus => value, + UnaryOperator::Minus => value.checked_neg()?, + UnaryOperator::BitwiseNot => !value, + _ => return None, + } + } + Expression::Binary(binary) => { + let left = self.integer_constant_inner(&binary.left, depth - 1)?; + let right = self.integer_constant_inner(binary.right.as_ref().ok()?, depth - 1)?; + match binary.operator.value { + BinaryOperator::Add => left.checked_add(right)?, + BinaryOperator::Subtract => left.checked_sub(right)?, + BinaryOperator::Multiply => left.checked_mul(right)?, + BinaryOperator::Divide => left.checked_div(right)?, + BinaryOperator::Modulo => left.checked_rem(right)?, + BinaryOperator::BitwiseAnd => left & right, + BinaryOperator::BitwiseOr => left | right, + BinaryOperator::BitwiseXor => left ^ right, + BinaryOperator::LeftShift + | BinaryOperator::RightShift + | BinaryOperator::UnsignedRightShift => { + let bits = if matches!(kind, Int64 | UInt64) { + 64 + } else { + 32 + }; + let count = (right as u32) & (bits - 1); + let shifted = match binary.operator.value { + BinaryOperator::LeftShift => left << count, + BinaryOperator::UnsignedRightShift if bits == 32 => { + i128::from((left as u32) >> count) + } + BinaryOperator::UnsignedRightShift => { + i128::from((left as u64) >> count) + } + _ => left >> count, + }; + match kind { + Int32 => i128::from(shifted as i32), + UInt32 => i128::from(shifted as u32), + Int64 => i128::from(shifted as i64), + UInt64 => i128::from(shifted as u64), + _ => return None, + } + } + _ => return None, + } + } + _ => return None, + }; + let fits = match kind { + SByte => i8::try_from(value).is_ok(), + Byte => u8::try_from(value).is_ok(), + Int16 => i16::try_from(value).is_ok(), + UInt16 | Char => u16::try_from(value).is_ok(), + Int32 => i32::try_from(value).is_ok(), + UInt32 => u32::try_from(value).is_ok(), + Int64 => i64::try_from(value).is_ok(), + UInt64 => u64::try_from(value).is_ok(), + _ => false, + }; + fits.then_some(value) + } + /// The constant-expression allowance, without evaluating: an integer /// constant expression may sit in any integral slot (the C# LSP checks the /// actual range). diff --git a/src/men-sharp-semantics/src/semantics/check/exhaustive.rs b/src/men-sharp-semantics/src/semantics/check/exhaustive.rs index b639567..0aa0014 100644 --- a/src/men-sharp-semantics/src/semantics/check/exhaustive.rs +++ b/src/men-sharp-semantics/src/semantics/check/exhaustive.rs @@ -90,12 +90,23 @@ fn bool_literal(expression: &Expression) -> Option { /// allowed to be. Anything else is `None`: the member is then a case of /// its own, which is right unless it aliases another. pub(super) fn integer_literal_value(expression: &Expression) -> Option { + if let Expression::Unary(unary) = expression { + let value = integer_literal_value(unary.operand.as_ref().ok()?)?; + return match unary.operator.value { + men_sharp_parser::ast::UnaryOperator::Plus => Some(value), + men_sharp_parser::ast::UnaryOperator::Minus => value.checked_neg(), + _ => None, + }; + } let Expression::Primary(primary) = expression else { return None; }; if !primary.chain.is_empty() { return None; } + if let PrimaryLeft::Parenthesized { expression, .. } = &primary.left { + return integer_literal_value(expression); + } let PrimaryLeft::Literal(LiteralExpression::Integer(text)) = &primary.left else { return None; }; diff --git a/src/men-sharp-semantics/src/semantics/check/expressions.rs b/src/men-sharp-semantics/src/semantics/check/expressions.rs index 08cf76f..85127a0 100644 --- a/src/men-sharp-semantics/src/semantics/check/expressions.rs +++ b/src/men-sharp-semantics/src/semantics/check/expressions.rs @@ -101,20 +101,72 @@ impl<'a, 'ast> Checker<'a, 'ast> { .ok() .and_then(super::exhaustive::integer_literal_value) == Some(0); + let operator_value_type = { + use crate::types::conversions::NumericKind::*; + let pair = ( + self.system().numeric_kind(&value_type), + self.system().numeric_kind(&target), + ); + if !matches!( + operator, + BinaryOperator::LeftShift + | BinaryOperator::RightShift + | BinaryOperator::UnsignedRightShift + ) && matches!( + pair, + (Some(Int32), Some(UInt32 | UInt64)) | (Some(Int64), Some(UInt64)) + ) && self.integer_constant(value).is_some_and(|v| v >= 0) + { + target.clone() + } else { + value_type.clone() + } + }; let result = self.binary_type( operator, target.clone(), - value_type, + operator_value_type, literal, zero_literal, assignment.span.clone(), Some(EntityID::from(*assignment)), ); - // compound assignment narrows back implicitly (int += byte) + // A predefined compound operator may narrow its result, + // but the RHS must still convert implicitly to the LHS + // (except for shifts). `byte += byte` is valid; `byte += long` is not. if !matches!(result, Type::Error) { - let compatible = - self.system().is_implicitly_convertible(&result, &target) - || self.system().numeric_kind(&target).is_some(); + let system = self.system(); + let numeric = system.numeric_kind(&target).is_some() + && system.numeric_kind(&value_type).is_some(); + let shift = matches!( + operator, + BinaryOperator::LeftShift + | BinaryOperator::RightShift + | BinaryOperator::UnsignedRightShift + ); + let constant_fits = if system.numeric_kind(&value_type) + == Some(crate::types::conversions::NumericKind::Int32) + { + self.integer_constant(value).is_some_and(|v| { + use crate::types::conversions::NumericKind::*; + match system.numeric_kind(&target) { + Some(SByte) => i8::try_from(v).is_ok(), + Some(Byte) => u8::try_from(v).is_ok(), + Some(Int16) => i16::try_from(v).is_ok(), + Some(UInt16) => u16::try_from(v).is_ok(), + Some(UInt32) => u32::try_from(v).is_ok(), + Some(UInt64) => v >= 0, + _ => false, + } + }) + } else { + false + }; + let compatible = system.is_implicitly_convertible(&result, &target) + || (numeric + && (shift + || system.is_implicitly_convertible(&value_type, &target) + || constant_fits)); if !compatible { let kind = SemanticErrorKind::TypeMismatch { expected: self.describe(&target), @@ -207,6 +259,49 @@ impl<'a, 'ast> Checker<'a, 'ast> { other => other, }; } + // Non-negative int constants convert to uint/ulong before + // overload resolution; long constants may convert to ulong. + let promote_constant = |ty: Type, other: &Type, expression: &Expression| { + use crate::types::conversions::NumericKind::*; + let system = self.system(); + let pair = (system.numeric_kind(&ty), system.numeric_kind(other)); + let fits = self.integer_constant(expression).is_some_and(|v| v >= 0); + if fits + && matches!( + pair, + (Some(Int32), Some(UInt32 | UInt64)) | (Some(Int64), Some(UInt64)) + ) + { + other.clone() + } else { + ty + } + }; + let original_left = left.clone(); + let left = if !matches!( + binary.operator.value, + BinaryOperator::LeftShift + | BinaryOperator::RightShift + | BinaryOperator::UnsignedRightShift + ) { + promote_constant(left, &right, &binary.left) + } else { + left + }; + let right = if let Ok(expression) = &binary.right { + if !matches!( + binary.operator.value, + BinaryOperator::LeftShift + | BinaryOperator::RightShift + | BinaryOperator::UnsignedRightShift + ) { + promote_constant(right, &original_left, expression) + } else { + right + } + } else { + right + }; self.binary_type( binary.operator.value, left, diff --git a/src/men-sharp-semantics/src/semantics/check/operators.rs b/src/men-sharp-semantics/src/semantics/check/operators.rs index ff89d52..8af6457 100644 --- a/src/men-sharp-semantics/src/semantics/check/operators.rs +++ b/src/men-sharp-semantics/src/semantics/check/operators.rs @@ -49,14 +49,19 @@ impl<'a, 'ast> Checker<'a, 'ast> { } UnaryOperator::Plus | UnaryOperator::Minus => { if let Some(kind) = system.numeric_kind(&operand) { - // small operands promote to int - let promoted = match kind { - NumericKind::SByte - | NumericKind::Byte - | NumericKind::Int16 - | NumericKind::UInt16 - | NumericKind::Char => NumericKind::Int32, - other => other, + let promoted = match (operator, kind) { + (UnaryOperator::Minus, NumericKind::UInt32) => NumericKind::Int64, + (UnaryOperator::Minus, NumericKind::UInt64) => { + self.error( + SemanticErrorKind::InvalidOperator { + left: self.describe(&operand), + right: None, + }, + span, + ); + return Type::Error; + } + _ => kind.unary_promoted(), }; self.corlib(promoted.corlib_name()) } else if let Some(result) = @@ -79,7 +84,11 @@ impl<'a, 'ast> Checker<'a, 'ast> { .unwrap_or(false) || system.is_enum_type(&operand) { - operand + if let Some(kind) = system.numeric_kind(&operand) { + self.corlib(kind.unary_promoted().corlib_name()) + } else { + operand + } } else if let Some(result) = self.user_defined_unary(operator, &operand, &span, node) { @@ -219,6 +228,12 @@ impl<'a, 'ast> Checker<'a, 'ast> { }; } Equal | NotEqual => { + if let (Some(l), Some(r)) = + (system.numeric_kind(&left), system.numeric_kind(&right)) + { + return self.numeric_binary(operator, l, r, span, node); + } + let comparable = matches!(left, Type::Null) || matches!(right, Type::Null) || (system.numeric_kind(&left).is_some() @@ -268,7 +283,7 @@ impl<'a, 'ast> Checker<'a, 'ast> { if let (Some(left_kind), Some(right_kind)) = (system.numeric_kind(&left), system.numeric_kind(&right)) { - return self.numeric_binary(operator, left_kind, right_kind, span); + return self.numeric_binary(operator, left_kind, right_kind, span, node); } // enums @@ -327,55 +342,59 @@ impl<'a, 'ast> Checker<'a, 'ast> { left: NumericKind, right: NumericKind, span: Range, + node: Option, ) -> Type { use BinaryOperator::*; use NumericKind::*; - let promoted = if left == Decimal || right == Decimal { - Decimal - } else if left == Double || right == Double { - Double - } else if left == Single || right == Single { - Single - } else if left == UInt64 || right == UInt64 { - UInt64 - } else if left == Int64 || right == Int64 { - Int64 - } else if left == UInt32 || right == UInt32 { - // uint with a signed operand goes to long - let signed = |kind: NumericKind| matches!(kind, SByte | Int16 | Int32); - if signed(left) || signed(right) { - Int64 - } else { - UInt32 - } + let shift = matches!(operator, LeftShift | RightShift | UnsignedRightShift); + let promoted = if shift { + (left.is_integral() && right.unary_promoted() == Int32).then_some(left.unary_promoted()) } else { - Int32 + left.binary_promoted(right) }; - - match operator { - LessThan | GreaterThan | LessThanEqual | GreaterThanEqual | Equal | NotEqual => { - self.corlib("Boolean") - } - LeftShift | RightShift | UnsignedRightShift => { - // the left operand alone decides, promoted to at least int - let shifted = match left { - SByte | Byte | Int16 | UInt16 | Char => Int32, - other => other, - }; - self.corlib(shifted.corlib_name()) - } - BitwiseAnd | BitwiseOr | BitwiseXor - if matches!(promoted, Single | Double | Decimal) => - { - let kind = SemanticErrorKind::InvalidOperator { - left: promoted.corlib_name().to_string(), - right: Some(promoted.corlib_name().to_string()), - }; - self.error(kind, span); - Type::Error - } - _ => self.corlib(promoted.corlib_name()), + let valid_operator = matches!( + operator, + Add | Subtract + | Multiply + | Divide + | Modulo + | LessThan + | GreaterThan + | LessThanEqual + | GreaterThanEqual + | Equal + | NotEqual + ) || (matches!(operator, BitwiseAnd | BitwiseOr | BitwiseXor) + && left.is_integral() + && right.is_integral()) + || shift; + let Some(promoted) = promoted.filter(|_| valid_operator) else { + self.error( + SemanticErrorKind::InvalidOperator { + left: left.corlib_name().to_string(), + right: Some(right.corlib_name().to_string()), + }, + span, + ); + return Type::Error; + }; + if let Some(node) = node { + let left = self.corlib(promoted.corlib_name()); + let right = if shift { + self.corlib("Int32") + } else { + left.clone() + }; + self.numeric_promotions.insert(node, (left, right)); + } + if matches!( + operator, + LessThan | GreaterThan | LessThanEqual | GreaterThanEqual | Equal | NotEqual + ) { + self.corlib("Boolean") + } else { + self.corlib(promoted.corlib_name()) } } diff --git a/src/men-sharp-semantics/src/semantics/check/statements.rs b/src/men-sharp-semantics/src/semantics/check/statements.rs index 18f738c..02a9211 100644 --- a/src/men-sharp-semantics/src/semantics/check/statements.rs +++ b/src/men-sharp-semantics/src/semantics/check/statements.rs @@ -353,7 +353,22 @@ impl<'a, 'ast> Checker<'a, 'ast> { (declared, _) => declared.clone(), }; + let constant = if declaration.const_keyword.is_some() { + match &declarator.initializer { + Some(InitializerValue::Expression(value)) => self.integer_constant(value), + _ => None, + } + } else { + None + }; self.declare_local(declarator.name.value, ty); + if let Some(local) = self + .locals + .last_mut() + .and_then(|s| s.locals.get_mut(declarator.name.value)) + { + local.integer_constant = constant; + } } } diff --git a/src/men-sharp-semantics/src/types/conversions.rs b/src/men-sharp-semantics/src/types/conversions.rs index 9201359..fe6ee11 100644 --- a/src/men-sharp-semantics/src/types/conversions.rs +++ b/src/men-sharp-semantics/src/types/conversions.rs @@ -95,6 +95,49 @@ impl NumericKind { } } + /// Unary numeric promotion (§12.4.7.1), also used for shift operands. + pub fn unary_promoted(self) -> Self { + match self { + Self::SByte | Self::Byte | Self::Int16 | Self::UInt16 | Self::Char => Self::Int32, + other => other, + } + } + + /// Binary numeric promotion (§12.4.7.2). `None` means that the built-in + /// operator has no common operand type (for example, long with ulong). + pub fn binary_promoted(self, other: Self) -> Option { + use NumericKind::*; + let pair = [self, other]; + Some(if pair.contains(&Decimal) { + if pair.contains(&Single) || pair.contains(&Double) { + return None; + } + Decimal + } else if pair.contains(&Double) { + Double + } else if pair.contains(&Single) { + Single + } else if pair.contains(&UInt64) { + if pair + .iter() + .any(|k| matches!(k, SByte | Int16 | Int32 | Int64)) + { + return None; + } + UInt64 + } else if pair.contains(&Int64) { + Int64 + } else if pair.contains(&UInt32) { + if pair.iter().any(|k| matches!(k, SByte | Int16 | Int32)) { + Int64 + } else { + UInt32 + } + } else { + Int32 + }) + } + pub fn is_integral(&self) -> bool { !matches!( self, diff --git a/tests/unity-project/Assets/MenSharp/MenSharpRuntimeSmoke.cs b/tests/unity-project/Assets/MenSharp/MenSharpRuntimeSmoke.cs index a567084..3e72a52 100644 --- a/tests/unity-project/Assets/MenSharp/MenSharpRuntimeSmoke.cs +++ b/tests/unity-project/Assets/MenSharp/MenSharpRuntimeSmoke.cs @@ -452,6 +452,53 @@ public void DefensiveCopies() public int smallUShort; public string smallChar; + // Check the actual boxed result types: a small heap slot cannot be + // passed straight to an Int32 extern even when its value fits. + public object[] promotedIntegers; + public bool signedUnsignedComparison; + public double charFloating; + public void IntegerPromotion() + { + sbyte sb = 13; byte b = 13; short sh = 13; ushort us = 13; char c = (char)13; + promotedIntegers = new object[] { + +sb, +b, +sh, +us, +c, + -sb, -b, -sh, -us, -c, + ~sb, ~b, ~sh, ~us, ~c, + sb % sb, b % b, sh % sh, us % us, c % c, + sb << b, b << sh, sh << us, us << c, c << 1 + }; + int negative = -13; uint positive = 3; + signedUnsignedComparison = negative < positive && positive > negative; + charFloating = c + 0.5; + } + + public object[] wideIntegerResults; + public int remainderZeroCaught; + public int remainderOverflowCaught; + public void WideIntegers() + { + uint u = uint.MaxValue, three = 3; + long l = long.MinValue, minusOne = -1; + ulong ul = ulong.MaxValue, ten = 10; + wideIntegerResults = new object[] { u % three, l % 3L, ul % ten, ~ul }; + try { l %= minusOne; } catch (OverflowException) { remainderOverflowCaught = 1; } + u %= three; ul %= ten; + wideIntegerResults[0] = u; wideIntegerResults[2] = ul; + ulong zero = 0; + try { ul %= zero; } catch (DivideByZeroException) { remainderZeroCaught = 1; } + } + + public object[] smallLimits; + public object[] smallLimitsAfter; + public void SmallIntegerConstants() + { + sbyte sb = sbyte.MaxValue; byte b = byte.MaxValue; + short sh = short.MaxValue; ushort us = ushort.MaxValue; + smallLimits = new object[] { sb, b, sh, us }; + sb++; b++; sh++; us++; + smallLimitsAfter = new object[] { sb, b, sh, us }; + } + public void SmallIntegers() { byte b = 1; diff --git a/tests/unity-project/Assets/Tests/Editor/MenSharpIntegrationTests.cs b/tests/unity-project/Assets/Tests/Editor/MenSharpIntegrationTests.cs index 7a36018..3046d13 100644 --- a/tests/unity-project/Assets/Tests/Editor/MenSharpIntegrationTests.cs +++ b/tests/unity-project/Assets/Tests/Editor/MenSharpIntegrationTests.cs @@ -68,6 +68,37 @@ public class MenSharpIntegrationTests "VerifyWake", }; + [TestCase("Single", "NaN")] + [TestCase("Single", "1.25")] + [TestCase("Single", "Infinity")] + [TestCase("Single", "-Infinity")] + [TestCase("Double", "NaN")] + [TestCase("Double", "1.25")] + [TestCase("Double", "Infinity")] + [TestCase("Double", "-Infinity")] + public void FloatingMetadataPreservesSpecialValuesAndBoxedTypes(string kind, string text) + { + // Verify the metadata spellings emitted by the compiler on Unity Mono. + // Assert boxed types as well as values: Single slots must receive floats. + var decode = typeof(MenSharpProgramAsset).GetMethod("Decode", + BindingFlags.Static | BindingFlags.NonPublic); + Assert.IsNotNull(decode); + object actual = decode.Invoke(null, new object[] { + new MenSharpHeapEntry { kind = kind, value = text } + }); + + if (kind == "Single") + { + Assert.IsInstanceOf(actual); + Assert.AreEqual(float.Parse(text, System.Globalization.CultureInfo.InvariantCulture), actual); + } + else + { + Assert.IsInstanceOf(actual); + Assert.AreEqual(double.Parse(text, System.Globalization.CultureInfo.InvariantCulture), actual); + } + } + [Test] public void CompilerCreatesEveryManualFixtureAsAnSdkProgramAsset() { @@ -298,6 +329,37 @@ public IEnumerator GeneratedProgramsExecuteInTheSdkUdonVm() Assert.AreEqual(3, smoke.GetProgramVariable("copyIn")); // small integral types: compound ops compute in int, cast back, wrap + smoke.RunProgram("IntegerPromotion"); + var promoted = (object[])smoke.GetProgramVariable("promotedIntegers"); + Assert.AreEqual(25, promoted.Length); + for (int i = 0; i < promoted.Length; i++) + { + Assert.IsInstanceOf(promoted[i], "promotion index " + i); + int expected = i < 5 ? 13 : i < 10 ? -13 : i < 15 ? -14 : i < 20 ? 0 : i == 24 ? 26 : 106496; + Assert.AreEqual(expected, promoted[i], "promotion index " + i); + } + Assert.AreEqual(true, smoke.GetProgramVariable("signedUnsignedComparison")); + Assert.AreEqual(13.5, smoke.GetProgramVariable("charFloating")); + + smoke.RunProgram("WideIntegers"); + CollectionAssert.AreEqual(new object[] { 0u, -2L, 5UL, 0UL }, + (object[])smoke.GetProgramVariable("wideIntegerResults")); + Assert.AreEqual(1, smoke.GetProgramVariable("remainderZeroCaught")); + Assert.AreEqual(1, smoke.GetProgramVariable("remainderOverflowCaught")); + + smoke.RunProgram("SmallIntegerConstants"); + var limits = (object[])smoke.GetProgramVariable("smallLimits"); + var limitsAfter = (object[])smoke.GetProgramVariable("smallLimitsAfter"); + object[] expectedLimits = { sbyte.MaxValue, byte.MaxValue, short.MaxValue, ushort.MaxValue }; + object[] expectedAfter = { sbyte.MinValue, (byte)0, short.MinValue, (ushort)0 }; + for (int i = 0; i < limits.Length; i++) + { + Assert.AreEqual(expectedLimits[i].GetType(), limits[i].GetType()); + Assert.AreEqual(expectedLimits[i], limits[i]); + Assert.AreEqual(expectedAfter[i].GetType(), limitsAfter[i].GetType()); + Assert.AreEqual(expectedAfter[i], limitsAfter[i]); + } + smoke.RunProgram("SmallIntegers"); Assert.AreEqual(3, smoke.GetProgramVariable("smallByte")); Assert.AreEqual(1, smoke.GetProgramVariable("smallByteWrapped")); diff --git a/unity/io.tesca.mensharp/Editor/MenSharpProgramAsset.cs b/unity/io.tesca.mensharp/Editor/MenSharpProgramAsset.cs index c33ad43..65c2c4a 100644 --- a/unity/io.tesca.mensharp/Editor/MenSharpProgramAsset.cs +++ b/unity/io.tesca.mensharp/Editor/MenSharpProgramAsset.cs @@ -529,6 +529,10 @@ private static object Decode(MenSharpHeapEntry entry) { switch (entry.kind) { + case "SByte": return sbyte.Parse(entry.value); + case "Byte": return byte.Parse(entry.value); + case "Int16": return short.Parse(entry.value); + case "UInt16": return ushort.Parse(entry.value); case "Int32": return int.Parse(entry.value); case "UInt32": return uint.Parse(entry.value); case "UInt64": return ulong.Parse(entry.value);