From 88de3768e495581a294e4c92cb1ae57add3432f9 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Wed, 27 Mar 2024 22:09:50 -0400 Subject: [PATCH] Check types and do casting in classical declarations (#205) Implement checking compatibility of types of lhs and rhs in classical assignment statement. If an implicit cast is needed, add an explicit cast. Prior to this commit there was essentially nothing in place to do these things (just a very small amount, and broken at that). This is certainly not the end of the story. There will be edge cases. But this is a good start which should capture many common scenarios. Since we are developing quickly and writing tests for this ASG is very tedious and verbose, we have not been testing very carefully. However, the kind of thing done in this commit is inherently fragile, so we need to begin with more testing. Closes #203 --- crates/oq3_semantics/src/asg.rs | 2 +- .../oq3_semantics/src/syntax_to_semantics.rs | 34 ++++- crates/oq3_semantics/src/types.rs | 65 ++++---- .../oq3_semantics/tests/from_string_tests.rs | 144 ++++++++++++++++++ 4 files changed, 208 insertions(+), 37 deletions(-) diff --git a/crates/oq3_semantics/src/asg.rs b/crates/oq3_semantics/src/asg.rs index 48a518c..fcc715d 100644 --- a/crates/oq3_semantics/src/asg.rs +++ b/crates/oq3_semantics/src/asg.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // The definition of the abstract semantic graph (ASG) as well as the API for using it. -// Construction of this typed ASG from syntactic AST is in string_to_semantic.rs. +// Construction of this typed ASG from syntactic AST is in syntax_to_semantics.rs // SymbolIdResult can represent a valid symbol symbol, via a symbol id, or a semantic error. // We need to insert SymbolIdResult everywhere a symbol is needed in the semantic tree. diff --git a/crates/oq3_semantics/src/syntax_to_semantics.rs b/crates/oq3_semantics/src/syntax_to_semantics.rs index 2ee89a0..e3797b1 100644 --- a/crates/oq3_semantics/src/syntax_to_semantics.rs +++ b/crates/oq3_semantics/src/syntax_to_semantics.rs @@ -994,17 +994,39 @@ fn from_classical_declaration_statement( panic!("Array types are not supported yet in the ASG"); } let scalar_type = type_decl.scalar_type().unwrap(); - let typ = from_scalar_type(&scalar_type, type_decl.const_token().is_some(), context); + let lhs_type = from_scalar_type(&scalar_type, type_decl.const_token().is_some(), context); let name_str = type_decl.name().unwrap().string(); let initializer = from_expr(type_decl.expr(), context); // FIXME: This error and several others can and should be moved to a subsequent pass. // However, we would lose the information in `text_range` unless we do something to preserve it. - let symbol_id = context.new_binding(name_str.as_ref(), &typ, type_decl); - if let Some(ref initializer) = initializer { - let init_typ = initializer.get_type(); - if !(&typ == init_typ) && !types::can_cast_loose(&typ, init_typ) { - context.insert_error(IncompatibleTypesError, type_decl); + let symbol_id = context.new_binding(name_str.as_ref(), &lhs_type, type_decl); + // If there is an initializer, check that types are compatible and if there is + // an implicit cast, make it explicit. + if let Some(initializer) = initializer { + let init_type = initializer.get_type(); + if types::equal_up_to_constness(&lhs_type, init_type) { + return asg::DeclareClassical::new(symbol_id, Some(initializer)).to_stmt(); } + let promoted_type = types::promote_types_not_equal(&lhs_type, init_type); + // dbg!((&promoted_type, &lhs_type)); + // dbg!(promoted_type == lhs_type); + let new_initializer = if types::equal_up_to_constness(&promoted_type, &lhs_type) { + // Very often `promoted_type` is correct. But it may be off by constness. + // We cast to exactly the type of the lhs, by cloning it. + // The important thing is to filter out cases where casting is either + // not necessary, or not allowed. + asg::Cast::new(initializer.clone(), lhs_type.clone()).to_texpr() + } else { + // Either the type can't be promoted, + // or promote_types says to promote lhs to rhs, which is allowed + // for some binary expressions, but not for assignment because the + // type of the lhs is fixed. + if promoted_type == Type::Void || &promoted_type == init_type { + context.insert_error(IncompatibleTypesError, type_decl); + } + initializer + }; + return asg::DeclareClassical::new(symbol_id, Some(new_initializer)).to_stmt(); } asg::DeclareClassical::new(symbol_id, initializer).to_stmt() } diff --git a/crates/oq3_semantics/src/types.rs b/crates/oq3_semantics/src/types.rs index 7006bfc..9fd5405 100644 --- a/crates/oq3_semantics/src/types.rs +++ b/crates/oq3_semantics/src/types.rs @@ -81,6 +81,28 @@ pub enum Type { Undefined, } +// wow. Is there a less boiler-plated way? +// Return `true` if `ty1 == ty2` except that the `is_const` +// property is allowed to differ. +pub(crate) fn equal_up_to_constness(ty1: &Type, ty2: &Type) -> bool { + use Type::*; + if ty1 == ty2 { + return true; + } + match (ty1, ty2) { + (Bit(_), Bit(_)) => true, + (Duration(_), Duration(_)) => true, + (Bool(_), Bool(_)) => true, + (Stretch(_), Stretch(_)) => true, + (Int(w1, _), Int(w2, _)) => w1 == w2, + (UInt(w1, _), UInt(w2, _)) => w1 == w2, + (Float(w1, _), Float(w2, _)) => w1 == w2, + (Angle(w1, _), Angle(w2, _)) => w1 == w2, + (BitArray(dims1, _), BitArray(dims2, _)) => dims1 == dims2, + _ => false, + } +} + // OQ3 supports arrays with number of dims up to seven. // Probably exists a much better way to represent dims... [usize, N] // Could use Box for higher dimensional arrays, or... @@ -257,9 +279,21 @@ fn promote_width(ty1: &Type, ty2: &Type) -> Width { } } +pub fn promote_types_not_equal(ty1: &Type, ty2: &Type) -> Type { + let typ = promote_types_symmetric(ty1, ty2); + if typ != Type::Void { + return typ; + } + let typ = promote_types_asymmetric(ty1, ty2); + if typ == Type::Void { + return promote_types_asymmetric(ty2, ty1); + } + typ +} + // promotion suitable for some binary operations, eg +, -, * pub fn promote_types(ty1: &Type, ty2: &Type) -> Type { - if ty1 == ty2 { + if equal_up_to_constness(ty1, ty2) { return ty1.clone(); } let typ = promote_types_symmetric(ty1, ty2); @@ -292,32 +326,3 @@ fn promote_types_asymmetric(ty1: &Type, ty2: &Type) -> Type { _ => Void, } } - -// FIXME: always returns false -// Return `true` if `ty1 == ty2` except that the `is_const` -// property is allowed to differ. -fn equal_up_to_constness(_ty1: &Type, _ty2: &Type) -> bool { - false - // match (ty1, ty2) { - // // (Bit(w1, _), Bit(w2, _)) => w1 == w2, - // _ => false, - // } -} - -// Return `true` if `from_type` can be cast to `to_type`. -// Warning: It is assumed that `equal_up_to_constness(from_type, to_type)` -// returns `false`. If not, `can_cast_strict` may give incorrect results. -// Unused at the moment -// fn can_cast_strict(from_type: &Type, to_type: &Type) -> bool { -// use Type::*; -// !matches!((from_type, to_type), (Bit(..), Bit(..))) -// } - -// FIXME: This in unfinished and permissive. -/// Return `true` if `from_type` is equal to `to_type` up to constness, -/// or if `from_type` can be cast to `to_type`. What "can be cast" means -/// is perhaps a bit vague at the moment. -pub(crate) fn can_cast_loose(from_type: &Type, to_type: &Type) -> bool { - use Type::*; - equal_up_to_constness(from_type, to_type) || !matches!((from_type, to_type), (Bit(..), Bit(..))) -} diff --git a/crates/oq3_semantics/tests/from_string_tests.rs b/crates/oq3_semantics/tests/from_string_tests.rs index 2abd34b..3382435 100644 --- a/crates/oq3_semantics/tests/from_string_tests.rs +++ b/crates/oq3_semantics/tests/from_string_tests.rs @@ -853,3 +853,147 @@ bit mid = measure $1; assert_eq!(errors.len(), 0); assert_eq!(program.len(), 1); } + +// Issue #203 +#[test] +fn test_from_string_declaration_type_check_1() { + let code = r##" +qubit q; +float a = q; +"##; + let (program, errors, _symbol_table) = parse_string(code); + assert_eq!(errors.len(), 1); + assert_eq!(program.len(), 2); + assert!(matches!( + &errors[0].kind(), + SemanticErrorKind::IncompatibleTypesError + )); +} + +// Issue #203 +#[test] +fn test_from_string_declaration_type_check_2() { + let code = r##" +float c = 2.1; +int d = c; +"##; + let (program, errors, _symbol_table) = parse_string(code); + assert_eq!(errors.len(), 1); + assert_eq!(program.len(), 2); + assert!(matches!( + &errors[0].kind(), + SemanticErrorKind::IncompatibleTypesError + )); +} + +// Issue #203 +#[test] +fn test_from_string_declaration_type_check_3() { + let code = r##" +float c = 2.1; +int d = c; +"##; + let (program, errors, _symbol_table) = parse_string(code); + assert_eq!(errors.len(), 1); + assert_eq!(program.len(), 2); + assert!(matches!( + &errors[0].kind(), + SemanticErrorKind::IncompatibleTypesError + )); +} + +// at least it's not python +// Check that the classical declaration statment `stmt` casts its RHS +// to type `expected_type`. +fn _check_cast_type(stmt: &asg::Stmt, expected_type: &Type) { + match stmt { + asg::Stmt::DeclareClassical(decl) => match decl.initializer() { + Some(texpr) => { + match texpr.expression() { + asg::Expr::Cast(cast) => { + assert!(cast.get_type() == expected_type) + } + _ => unreachable!(), + }; + } + _ => unreachable!(), + }, + _ => unreachable!(), + } +} + +// Issue #203 +#[test] +fn test_from_string_declaration_type_check_4() { + let code = r##" +int a = 2; +float b = a; +"##; + let (program, errors, _symbol_table) = parse_string(code); + assert_eq!(errors.len(), 0); + assert_eq!(program.len(), 2); + _check_cast_type(&program[1], &Type::Float(None, IsConst::False)); +} + +#[test] +fn test_from_string_declaration_type_check_5() { + let code = r##" +int a = 2; +float[64] b = a; +"##; + let (program, errors, _symbol_table) = parse_string(code); + assert_eq!(errors.len(), 0); + assert_eq!(program.len(), 2); + _check_cast_type(&program[1], &Type::Float(Some(64), IsConst::False)); +} + +#[test] +fn test_from_string_declaration_type_check_6() { + let code = r##" +float[32] a; +float[64] b = a; +"##; + let (program, errors, _symbol_table) = parse_string(code); + assert_eq!(errors.len(), 0); + assert_eq!(program.len(), 2); + _check_cast_type(&program[1], &Type::Float(Some(64), IsConst::False)); +} + +#[test] +fn test_from_string_declaration_type_check_7() { + let code = r##" +float[32] a; +const float[64] b = a; +"##; + let (program, errors, _symbol_table) = parse_string(code); + assert_eq!(errors.len(), 0); + assert_eq!(program.len(), 2); + _check_cast_type(&program[1], &Type::Float(Some(64), IsConst::True)); +} + +#[test] +fn test_from_string_declaration_type_check_8() { + let code = r##" +float a; +float[64] b = a; +"##; + let (_program, errors, _symbol_table) = parse_string(code); + assert_eq!(errors.len(), 1); + assert!(matches!( + &errors[0].kind(), + SemanticErrorKind::IncompatibleTypesError + )); +} + +#[test] +fn test_from_string_declaration_type_check_9() { + let code = r##" +duration a = 2.0; +"##; + let (_program, errors, _symbol_table) = parse_string(code); + assert_eq!(errors.len(), 1); + assert!(matches!( + &errors[0].kind(), + SemanticErrorKind::IncompatibleTypesError + )); +}