Skip to content

Commit

Permalink
Check types and do casting in classical declarations (#205)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jlapeyre committed Mar 28, 2024
1 parent 35131ef commit 88de376
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 37 deletions.
2 changes: 1 addition & 1 deletion crates/oq3_semantics/src/asg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
34 changes: 28 additions & 6 deletions crates/oq3_semantics/src/syntax_to_semantics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
65 changes: 35 additions & 30 deletions crates/oq3_semantics/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(..)))
}
144 changes: 144 additions & 0 deletions crates/oq3_semantics/tests/from_string_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
));
}

0 comments on commit 88de376

Please sign in to comment.