diff --git a/src/protocol/eval/executor.rs b/src/protocol/eval/executor.rs index 49f04a23fc0f9ac0c7a3908d7c29b02c7c17b4d9..18de494a20c56c87ca3744076a9517f205d1676f 100644 --- a/src/protocol/eval/executor.rs +++ b/src/protocol/eval/executor.rs @@ -498,7 +498,15 @@ impl Prompt { // Typechecking reduced this to two cases: either we // have casting noop (same types), or we're casting // between integer/bool/char types. + let subject = cur_frame.expr_values.pop_back().unwrap(); + match apply_casting(&mut self.store, output_type, &subject) { + Ok(value) => cur_frame.expr_values.push_back(value), + Err(msg) => { + return Err(EvalError::new_error_at_expr(self, modules, heap, expr.this.upcast(), msg)); + } + } + self.store.drop_value(subject.get_heap_pos()); } Expression::Call(expr) => { // Push a new frame. Note that all expressions have diff --git a/src/protocol/eval/value.rs b/src/protocol/eval/value.rs index 0f2f4f91c321b73beaa45ab6b75ed8245181322f..1d36c208fabd2d8b60bce1a9fc46db00bbc128b5 100644 --- a/src/protocol/eval/value.rs +++ b/src/protocol/eval/value.rs @@ -8,6 +8,7 @@ use crate::protocol::ast::{ ConcreteType, ConcreteTypePart, }; +use crate::protocol::parser::token_parsing::*; pub type StackPos = u32; pub type HeapPos = u32; @@ -497,27 +498,28 @@ pub(crate) fn apply_unary_operator(store: &mut Store, op: UnaryOperator, value: } } -pub(crate) fn apply_casting(store: &mut Store, output_type: &ConcreteType, subject: &Value) -> Value { +pub(crate) fn apply_casting(store: &mut Store, output_type: &ConcreteType, subject: &Value) -> Result { // To simplify the casting logic: if the output type is not a simple // integer/boolean/character, then the type checker made sure that the two // types must be equal, hence we can do a simple clone. use ConcreteTypePart as CTP; let part = &output_type.parts[0]; - if output_type.parts.len() > 1 || ( - part != CTP::Bool && - part != CTP::Character && - part != CTP::UInt8 && part != CTP::UInt16 && part != CTP::UInt32 && part != CTP::UInt64 && - part != CTP::SInt8 && part != CTP::SInt16 && part != CTP::SInt32 && part != CTP::SInt64 - ) { - // Complex thingamajig - return store.clone_value(subject.clone()); + match part { + CTP::Bool | CTP::Character | + CTP::UInt8 | CTP::UInt16 | CTP::UInt32 | CTP::UInt64 | + CTP::SInt8 | CTP::SInt16 | CTP::SInt32 | CTP::SInt64 => { + // Do the checking of these below + debug_assert_eq!(output_type.parts.len(), 1); + }, + _ => { + return Ok(store.clone_value(subject.clone())); + }, } + // Note: character is not included, needs per-type checking macro_rules! unchecked_cast { ($input: expr, $output_part: expr) => { - match $output_part { - CTP::Bool => Value::Bool($input as bool), - CTP::Character => Value::Char($input as char), + return Ok(match $output_part { CTP::UInt8 => Value::UInt8($input as u8), CTP::UInt16 => Value::UInt16($input as u16), CTP::UInt32 => Value::UInt32($input as u32), @@ -527,24 +529,180 @@ pub(crate) fn apply_casting(store: &mut Store, output_type: &ConcreteType, subje CTP::SInt32 => Value::SInt32($input as i32), CTP::SInt64 => Value::SInt64($input as i64), _ => unreachable!() - } + }) } }; + macro_rules! from_unsigned_cast { + ($input:expr, $input_type:ty, $output_part:expr) => { + { + let target_type_name = match $output_part { + CTP::Bool => return Ok(Value::Bool($input != 0)), + CTP::Character => if $input <= u8::MAX as $input_type { + return Ok(Value::Char(($input as u8) as char)) + } else { + KW_TYPE_CHAR_STR + }, + CTP::UInt8 => if $input <= u8::MAX as $input_type { + return Ok(Value::UInt8($input as u8)) + } else { + KW_TYPE_UINT8_STR + }, + CTP::UInt16 => if $input <= u16::MAX as $input_type { + return Ok(Value::UInt16($input as u16)) + } else { + KW_TYPE_UINT16_STR + }, + CTP::UInt32 => if $input <= u32::MAX as $input_type { + return Ok(Value::UInt32($input as u32)) + } else { + KW_TYPE_UINT32_STR + }, + CTP::UInt64 => return Ok(Value::UInt64($input as u64)), // any unsigned int to u64 is fine + CTP::SInt8 => if $input <= i8::MAX as $input_type { + return Ok(Value::SInt8($input as i8)) + } else { + KW_TYPE_SINT8_STR + }, + CTP::SInt16 => if $input <= i16::MAX as $input_type { + return Ok(Value::SInt16($input as i16)) + } else { + KW_TYPE_SINT16_STR + }, + CTP::SInt32 => if $input <= i32::MAX as $input_type { + return Ok(Value::SInt32($input as i32)) + } else { + KW_TYPE_SINT32_STR + }, + CTP::SInt64 => if $input <= i64::MAX as $input_type { + return Ok(Value::SInt64($input as i64)) + } else { + KW_TYPE_SINT64_STR + }, + _ => unreachable!(), + }; + + return Err(format!("value is '{}' which doesn't fit in a type '{}'", $input, target_type_name)); + } + } + } + + macro_rules! from_signed_cast { + // Programmer note: for signed checking we cannot do + // output_type::MAX as input_type, + // + // because if the output type's width is larger than the input type, + // then the cast results in a negative number. So we mask with the + // maximum possible value the input type can become. As in: + // (output_type::MAX as input_type) & input_type::MAX + // + // This way: + // 1. output width is larger than input width: fine in all cases, we + // simply compare against the max input value, which is always true. + // 2. output width is equal to input width: by masking we "remove the + // signed bit from the unsigned number" and again compare against the + // maximum input value. + // 3. output width is smaller than the input width: masking does nothing + // because the signed bit is never set, and we simply compare against + // the maximum possible output value. + // + // A similar kind of mechanism for the minimum value, but here we do + // a binary OR. We do a: + // (output_type::MIN as input_type) & input_type::MIN + // + // This way: + // 1. output width is larger than input width: initial cast truncates to + // 0, then we OR with the actual minimum value, so we attain the + // minimum value of the input type. + // 2. output width is equal to input width: we OR the minimum value with + // itself. + // 3. output width is smaller than input width: the cast produces the + // min value of the output type, the subsequent OR does nothing, as it + // essentially just sets the signed bit (which must already be set, + // since we're dealing with a signed minimum value) + // + // After all of this expanding, we simply hope the compiler does a best + // effort constant expression evaluation, and presto! + ($input:expr, $input_type:ty, $output_type:expr) => { + { + let target_type_name = match $output_type { + CTP::Bool => return Ok(Value::Bool($input != 0)), + CTP::Character => if $input >= 0 && $input <= (u8::max as $input_type & <$input_type>::MAX) { + return Ok(Value::Char(($input as u8) as char)) + } else { + KW_TYPE_CHAR_STR + }, + CTP::UInt8 => if $input >= 0 && $input <= ((u8::MAX as $input_type) & <$input_type>::MAX) { + return Ok(Value::UInt8($input as u8)); + } else { + KW_TYPE_UINT8_STR + }, + CTP::UInt16 => if $input >= 0 && $input <= ((u16::MAX as $input_type) & <$input_type>::MAX) { + return Ok(Value::UInt16($input as u16)); + } else { + KW_TYPE_UINT16_STR + }, + CTP::UInt32 => if $input >= 0 && $input <= ((u32::MAX as $input_type) & <$input_type>::MAX) { + return Ok(Value::UInt32($input as u32)); + } else { + KW_TYPE_UINT32_STR + }, + CTP::UInt64 => if $input >= 0 && $input <= ((u64::MAX as $input_type) & <$input_type>::MAX) { + return Ok(Value::UInt64($input as u64)); + } else { + KW_TYPE_UINT64_STR + }, + CTP::SInt8 => if $input >= ((i8::MIN as $input_type) | <$input_type>::MIN) && $input <= ((i8::MAX as $input_type) & <$input_type>::MAX) { + return Ok(Value::SInt8($input as i8)); + } else { + KW_TYPE_SINT8_STR + }, + CTP::SInt16 => if $input >= ((i16::MIN as $input_type | <$input_type>::MIN)) && $input <= ((i16::MAX as $input_type) & <$input_type>::MAX) { + return Ok(Value::SInt16($input as i16)); + } else { + KW_TYPE_SINT16_STR + }, + CTP::SInt32 => if $input >= ((i32::MIN as $input_type | <$input_type>::MIN)) && $input <= ((i32::MAX as $input_type) & <$input_type>::MAX) { + return Ok(Value::SInt32($input as i32)); + } else { + KW_TYPE_SINT32_STR + }, + CTP::SInt64 => return Ok(Value::SInt64($input as i64)), + _ => unreachable!(), + }; + + return Err(format!("value is '{}' which doesn't fit in a type '{}'", $input, target_type_name)); + } + } + } + // If here, then the types might still be equal, but at least we're dealing // with a simple integer/boolean/character input and output type. let subject = store.maybe_read_ref(subject); match subject { - Value::Bool(val) => unchecked_cast!(*val, part), - Value::Char(val) => unchecked_cast!(*val, part), - Value::UInt8(val) => {}, - Value::UInt16(val) => {}, - Value::UInt32(val) => {}, - Value::UInt64(val) => {}, - Value::SInt8(val) => {}, - Value::SInt16(val) => {}, - Value::SInt32(val) => {}, - Value::SInt64(val) => {}, + Value::Bool(val) => { + match part { + CTP::Bool => return Ok(Value::Bool(*val)), + CTP::Character => return Ok(Value::Char(1 as char)), + _ => unchecked_cast!(*val, part), + } + }, + Value::Char(val) => { + match part { + CTP::Bool => return Ok(Value::Bool(*val != 0 as char)), + CTP::Character => return Ok(Value::Char(*val)), + _ => unchecked_cast!(*val, part), + } + }, + Value::UInt8(val) => from_unsigned_cast!(*val, u8, part), + Value::UInt16(val) => from_unsigned_cast!(*val, u16, part), + Value::UInt32(val) => from_unsigned_cast!(*val, u32, part), + Value::UInt64(val) => from_unsigned_cast!(*val, u64, part), + Value::SInt8(val) => from_signed_cast!(*val, i8, part), + Value::SInt16(val) => from_signed_cast!(*val, i16, part), + Value::SInt32(val) => from_signed_cast!(*val, i32, part), + Value::SInt64(val) => from_signed_cast!(*val, i64, part), + _ => unreachable!("mismatch between 'cast' type checking and 'cast' evaluation"), } } diff --git a/src/protocol/parser/pass_definitions.rs b/src/protocol/parser/pass_definitions.rs index e6d6204d97a66a4f9a0493382f8c15fd4887cf04..1a1a92c07859c18f086da9214930b80c467bdd38 100644 --- a/src/protocol/parser/pass_definitions.rs +++ b/src/protocol/parser/pass_definitions.rs @@ -1635,6 +1635,10 @@ fn consume_parser_type( } } + for _ in 0..first_angle_depth { + consume_token(source, iter, TokenKind::CloseAngle); + } + return Ok(ParserType{ elements }); }; diff --git a/src/protocol/tests/eval_casting.rs b/src/protocol/tests/eval_casting.rs new file mode 100644 index 0000000000000000000000000000000000000000..b69bc79bc17ab356f20bb9d1b424485e39b6b2cf --- /dev/null +++ b/src/protocol/tests/eval_casting.rs @@ -0,0 +1,81 @@ +use super::*; + +#[test] +fn test_valid_unsigned_casting() { + Tester::new_single_source_expect_ok("cast u8", " + func foo() -> bool { + u64 large_width = 255; + u8 small_width = 255; + + // Explicit casting + auto large_exp_to_08 = cast (large_width); + auto large_exp_to_16 = cast(large_width); + auto large_exp_to_32 = cast(large_width); + auto large_exp_to_64 = cast(large_width); + + auto small_exp_to_08 = cast (small_width); + auto small_exp_to_16 = cast(small_width); + auto small_exp_to_32 = cast(small_width); + auto small_exp_to_64 = cast(small_width); + + // Implicit casting + u8 large_imp_to_08 = cast(large_width); + u16 large_imp_to_16 = cast(large_width); + u32 large_imp_to_32 = cast(large_width); + u64 large_imp_to_64 = cast(large_width); + + u8 small_imp_to_08 = cast(small_width); + u16 small_imp_to_16 = cast(small_width); + u32 small_imp_to_32 = cast(small_width); + u64 small_imp_to_64 = cast(small_width); + + return + large_exp_to_08 == 255 && large_exp_to_16 == 255 && large_exp_to_32 == 255 && large_exp_to_64 == 255 && + small_exp_to_08 == 255 && small_exp_to_16 == 255 && small_exp_to_32 == 255 && small_exp_to_64 == 255 && + large_imp_to_08 == 255 && large_imp_to_16 == 255 && large_imp_to_32 == 255 && large_imp_to_64 == 255 && + small_imp_to_08 == 255 && small_imp_to_16 == 255 && small_imp_to_32 == 255 && small_imp_to_64 == 255; + } + ").for_function("foo", |f| { f + .call_ok(Some(Value::Bool(true))); + }); +} + +#[test] +fn test_invalid_casting() { + fn generate_source(input_type: &str, input_value: &str, output_type: &str) -> String { + return format!(" + func foo() -> u32 {{ + {} value = {}; + {} result = cast(value); + return 0; + }} + ", input_type, input_value, output_type); + } + + fn perform_test(input_type: &str, input_value: &str, output_type: &str) { + Tester::new_single_source_expect_ok( + format!("invalid cast {} to {}", input_type, output_type), + generate_source(input_type, input_value, output_type) + ).for_function("foo", |f| { + f.call_err(&format!("'{}' which doesn't fit in a type '{}'", input_value, output_type)); + }); + } + + // Not exhaustive, good enough + let tests = [ + ("u16", "256", "u8"), + ("u32", "256", "u8"), + ("u64", "256", "u8"), + ("u32", "65536", "u16"), + ("u64", "65536", "u16"), + ("s8", "-1", "u8"), + ("s32", "-1", "u16"), + ("s32", "65536", "u16"), + ("s16", "-129", "s8"), + ("s16", "128", "s8") + ]; + + for (input_type, input_value, output_type) in &tests { + perform_test(input_type, input_value, output_type); + } +} \ No newline at end of file diff --git a/src/protocol/tests/mod.rs b/src/protocol/tests/mod.rs index e10f3657e438837822df5f51d791f793ebdf1a96..6883830de0057e8b6ee9947e5bf0abcf1fe8f04c 100644 --- a/src/protocol/tests/mod.rs +++ b/src/protocol/tests/mod.rs @@ -16,6 +16,7 @@ mod parser_monomorphs; mod parser_imports; mod eval_operators; mod eval_calls; +mod eval_casting; mod eval_silly; pub(crate) use utils::{Tester}; // the testing harness diff --git a/src/protocol/tests/utils.rs b/src/protocol/tests/utils.rs index 6bed679520f0ac6a2d6f63cedaa64e30e99164b2..3cd2bd816cb7b230fff2b7daffde989300b5f3ea 100644 --- a/src/protocol/tests/utils.rs +++ b/src/protocol/tests/utils.rs @@ -592,6 +592,7 @@ impl<'a> FunctionTester<'a> { ); }, Err(err) => { + println!("DEBUG: Formatted error:\n{}", err); assert!( false, "[{}] Expected call to succeed, but got {:?} for {}",