From b4a9c41d70da31e12e0db81c7684dcb1a36e5e56 2021-05-24 18:28:25 From: MH Date: 2021-05-24 18:28:25 Subject: [PATCH] Initial casting implementation Explicit casts can be performed with the syntax 'cast(input)' and implicit casts can be performed with the syntax 'cast(input)' where the output type is determined by inference. To prevent casting shenanigans we only allow casting of primitive types and of types to themselves (essentially creating a copy). --- 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 {}",