From eaa558a00efb02bdf0473234ab4bb2bd4ead6df9 Mon Sep 17 00:00:00 2001 From: Scott Carda <55811729+ScottCarda-MS@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:04:25 -0700 Subject: [PATCH] Q# Structs: Declaration and Constructor Syntax (#1573) Declaration and Constructor syntax for Q# structs. Structs can be declared as namespace items with the following syntax: ```qsharp struct Pair { First : Int, Second : Int } ``` Structs types that have been declared can be constructed using the following expression syntax: ```qsharp new Pair { First = 3, Second = 4 } ``` Additionally, structs can be constructed from other structs using the following copy constructor syntax: ```qsharp new Pair { ...existing_pair } ``` and specific fields can be altered while copying: ```qsharp new Pair { ...existing_pair, Second = 7 } ``` --- compiler/qsc_ast/src/assigner.rs | 19 +- compiler/qsc_ast/src/ast.rs | 120 ++++++++ compiler/qsc_ast/src/mut_visit.rs | 44 ++- compiler/qsc_ast/src/validate.rs | 19 +- compiler/qsc_ast/src/visit.rs | 41 ++- compiler/qsc_codegen/src/qsharp.rs | 48 +++ compiler/qsc_codegen/src/qsharp/tests.rs | 128 ++++++++ compiler/qsc_eval/src/lib.rs | 46 ++- compiler/qsc_eval/src/tests.rs | 33 ++ compiler/qsc_fir/src/fir.rs | 47 +++ compiler/qsc_fir/src/mut_visit.rs | 11 +- compiler/qsc_fir/src/visit.rs | 11 +- compiler/qsc_formatter/src/formatter.rs | 6 +- compiler/qsc_frontend/src/lower.rs | 30 ++ compiler/qsc_frontend/src/lower/tests.rs | 188 ++++++++++++ compiler/qsc_frontend/src/resolve.rs | 180 ++++++----- compiler/qsc_frontend/src/resolve/tests.rs | 166 ++++++++++ compiler/qsc_frontend/src/typeck.rs | 9 + compiler/qsc_frontend/src/typeck/check.rs | 28 ++ compiler/qsc_frontend/src/typeck/convert.rs | 59 ++-- compiler/qsc_frontend/src/typeck/infer.rs | 97 +++++- compiler/qsc_frontend/src/typeck/rules.rs | 67 +++- compiler/qsc_frontend/src/typeck/tests.rs | 323 ++++++++++++++++++++ compiler/qsc_hir/src/hir.rs | 47 +++ compiler/qsc_hir/src/mut_visit.rs | 17 +- compiler/qsc_hir/src/ty.rs | 22 +- compiler/qsc_hir/src/visit.rs | 16 +- compiler/qsc_lowerer/src/lib.rs | 26 ++ compiler/qsc_parse/src/expr.rs | 41 ++- compiler/qsc_parse/src/expr/tests.rs | 54 ++++ compiler/qsc_parse/src/item.rs | 35 ++- compiler/qsc_parse/src/item/tests.rs | 48 +++ compiler/qsc_parse/src/keyword.rs | 8 + compiler/qsc_parse/src/prim.rs | 4 + compiler/qsc_partial_eval/src/lib.rs | 4 + compiler/qsc_passes/src/logic_sep.rs | 1 + compiler/qsc_rca/src/core.rs | 57 +++- compiler/qsc_rca/tests/structs.rs | 124 ++++++++ samples/language/CopyAndUpdateOperator.qs | 10 +- 39 files changed, 2099 insertions(+), 135 deletions(-) create mode 100644 compiler/qsc_rca/tests/structs.rs diff --git a/compiler/qsc_ast/src/assigner.rs b/compiler/qsc_ast/src/assigner.rs index 4fd3de45d0..c4a40aacb4 100644 --- a/compiler/qsc_ast/src/assigner.rs +++ b/compiler/qsc_ast/src/assigner.rs @@ -3,8 +3,8 @@ use crate::{ ast::{ - Attr, Block, CallableDecl, Expr, FunctorExpr, Ident, Item, Namespace, NodeId, Package, Pat, - Path, QubitInit, SpecDecl, Stmt, Ty, TyDef, Visibility, + Attr, Block, CallableDecl, Expr, FieldAssign, FunctorExpr, Ident, Item, Namespace, NodeId, + Package, Pat, Path, QubitInit, SpecDecl, Stmt, Ty, TyDef, Visibility, }, mut_visit::{self, MutVisitor}, }; @@ -76,6 +76,16 @@ impl MutVisitor for Assigner { mut_visit::walk_callable_decl(self, decl); } + fn visit_struct_decl(&mut self, decl: &mut crate::ast::StructDecl) { + self.assign(&mut decl.id); + mut_visit::walk_struct_decl(self, decl); + } + + fn visit_field_def(&mut self, def: &mut crate::ast::FieldDef) { + self.assign(&mut def.id); + mut_visit::walk_field_def(self, def); + } + fn visit_spec_decl(&mut self, decl: &mut SpecDecl) { self.assign(&mut decl.id); mut_visit::walk_spec_decl(self, decl); @@ -106,6 +116,11 @@ impl MutVisitor for Assigner { mut_visit::walk_expr(self, expr); } + fn visit_field_assign(&mut self, assign: &mut FieldAssign) { + self.assign(&mut assign.id); + mut_visit::walk_field_assign(self, assign); + } + fn visit_pat(&mut self, pat: &mut Pat) { self.assign(&mut pat.id); mut_visit::walk_pat(self, pat); diff --git a/compiler/qsc_ast/src/ast.rs b/compiler/qsc_ast/src/ast.rs index 7fa37c6307..c379a29b5b 100644 --- a/compiler/qsc_ast/src/ast.rs +++ b/compiler/qsc_ast/src/ast.rs @@ -262,6 +262,8 @@ pub enum ItemKind { Open(Idents, Option>), /// A `newtype` declaration. Ty(Box, Box), + /// A `struct` declaration. + Struct(Box), } impl Display for ItemKind { @@ -274,6 +276,7 @@ impl Display for ItemKind { None => write!(f, "Open ({name})")?, }, ItemKind::Ty(name, t) => write!(f, "New Type ({name}): {t}")?, + ItemKind::Struct(s) => write!(f, "{s}")?, } Ok(()) } @@ -390,6 +393,70 @@ impl Display for TyDefKind { } } +/// A struct definition. +#[derive(Clone, Debug, PartialEq, Default)] +pub struct StructDecl { + /// The node ID. + pub id: NodeId, + /// The span. + pub span: Span, + /// The name of the struct. + pub name: Box, + /// The type definition kind. + pub fields: Box<[Box]>, +} + +impl Display for StructDecl { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut indent = set_indentation(indented(f), 0); + write!(indent, "Struct {} {} ({}):", self.id, self.span, self.name)?; + if self.fields.is_empty() { + write!(indent, " ")?; + } else { + indent = set_indentation(indent, 1); + for field in &*self.fields { + write!(indent, "\n{field}")?; + } + } + Ok(()) + } +} + +impl WithSpan for StructDecl { + fn with_span(self, span: Span) -> Self { + Self { span, ..self } + } +} + +/// A struct field definition. +#[derive(Clone, Debug, PartialEq, Default)] +pub struct FieldDef { + /// The node ID. + pub id: NodeId, + /// The span. + pub span: Span, + /// The name of the field. + pub name: Box, + /// The type of the field. + pub ty: Box, +} + +impl Display for FieldDef { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "FieldDef {} {} ({}): {}", + self.id, self.span, self.name, self.ty + ) + } +} + +impl WithSpan for FieldDef { + fn with_span(self, span: Span) -> Self { + Self { span, ..self } + } +} + /// A callable declaration header. #[derive(Clone, Debug, PartialEq)] pub struct CallableDecl { @@ -807,6 +874,8 @@ pub enum ExprKind { Repeat(Box, Box, Option>), /// A return: `return a`. Return(Box), + /// A struct constructor. + Struct(Box, Option>, Box<[Box]>), /// A ternary operator. TernOp(TernOp, Box, Box, Box), /// A tuple: `(a, b, c)`. @@ -847,6 +916,7 @@ impl Display for ExprKind { ExprKind::Range(start, step, end) => display_range(indent, start, step, end)?, ExprKind::Repeat(repeat, until, fixup) => display_repeat(indent, repeat, until, fixup)?, ExprKind::Return(e) => write!(indent, "Return: {e}")?, + ExprKind::Struct(name, copy, fields) => display_struct(indent, name, copy, fields)?, ExprKind::TernOp(op, expr1, expr2, expr3) => { display_tern_op(indent, *op, expr1, expr2, expr3)?; } @@ -1058,6 +1128,27 @@ fn display_repeat( Ok(()) } +fn display_struct( + mut indent: Indented, + name: &Path, + copy: &Option>, + fields: &[Box], +) -> fmt::Result { + write!(indent, "Struct ({name}):")?; + if copy.is_none() && fields.is_empty() { + write!(indent, " ")?; + return Ok(()); + } + indent = set_indentation(indent, 1); + if let Some(copy) = copy { + write!(indent, "\nCopy: {copy}")?; + } + for field in fields { + write!(indent, "\n{field}")?; + } + Ok(()) +} + fn display_tern_op( mut indent: Indented, op: TernOp, @@ -1101,6 +1192,35 @@ fn display_while(mut indent: Indented, cond: &Expr, block: &Block) -> Ok(()) } +/// A field assignment in a struct constructor expression. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct FieldAssign { + /// The node ID. + pub id: NodeId, + /// The span. + pub span: Span, + /// The field to assign. + pub field: Box, + /// The value to assign to the field. + pub value: Box, +} + +impl WithSpan for FieldAssign { + fn with_span(self, span: Span) -> Self { + Self { span, ..self } + } +} + +impl Display for FieldAssign { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "FieldsAssign {} {}: ({}) {}", + self.id, self.span, self.field, self.value + ) + } +} + /// An interpolated string component. #[derive(Clone, Debug, PartialEq)] pub enum StringComponent { diff --git a/compiler/qsc_ast/src/mut_visit.rs b/compiler/qsc_ast/src/mut_visit.rs index 78ede9bfe5..e3ff2a7cb6 100644 --- a/compiler/qsc_ast/src/mut_visit.rs +++ b/compiler/qsc_ast/src/mut_visit.rs @@ -2,10 +2,10 @@ // Licensed under the MIT License. use crate::ast::{ - Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FunctorExpr, FunctorExprKind, Ident, - Item, ItemKind, Namespace, Package, Pat, PatKind, Path, QubitInit, QubitInitKind, SpecBody, - SpecDecl, Stmt, StmtKind, StringComponent, TopLevelNode, Ty, TyDef, TyDefKind, TyKind, - Visibility, + Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FieldAssign, FieldDef, FunctorExpr, + FunctorExprKind, Ident, Item, ItemKind, Namespace, Package, Pat, PatKind, Path, QubitInit, + QubitInitKind, SpecBody, SpecDecl, Stmt, StmtKind, StringComponent, StructDecl, TopLevelNode, + Ty, TyDef, TyDefKind, TyKind, Visibility, }; use qsc_data_structures::span::Span; @@ -36,6 +36,14 @@ pub trait MutVisitor: Sized { walk_callable_decl(self, decl); } + fn visit_struct_decl(&mut self, decl: &mut StructDecl) { + walk_struct_decl(self, decl); + } + + fn visit_field_def(&mut self, def: &mut FieldDef) { + walk_field_def(self, def); + } + fn visit_spec_decl(&mut self, decl: &mut SpecDecl) { walk_spec_decl(self, decl); } @@ -60,6 +68,10 @@ pub trait MutVisitor: Sized { walk_expr(self, expr); } + fn visit_field_assign(&mut self, assign: &mut FieldAssign) { + walk_field_assign(self, assign); + } + fn visit_pat(&mut self, pat: &mut Pat) { walk_pat(self, pat); } @@ -116,6 +128,7 @@ pub fn walk_item(vis: &mut impl MutVisitor, item: &mut Item) { vis.visit_ident(ident); vis.visit_ty_def(def); } + ItemKind::Struct(decl) => vis.visit_struct_decl(decl), } } @@ -155,6 +168,18 @@ pub fn walk_callable_decl(vis: &mut impl MutVisitor, decl: &mut CallableDecl) { } } +pub fn walk_struct_decl(vis: &mut impl MutVisitor, decl: &mut StructDecl) { + vis.visit_span(&mut decl.span); + vis.visit_ident(&mut decl.name); + decl.fields.iter_mut().for_each(|f| vis.visit_field_def(f)); +} + +pub fn walk_field_def(vis: &mut impl MutVisitor, def: &mut FieldDef) { + vis.visit_span(&mut def.span); + vis.visit_ident(&mut def.name); + vis.visit_ty(&mut def.ty); +} + pub fn walk_spec_decl(vis: &mut impl MutVisitor, decl: &mut SpecDecl) { vis.visit_span(&mut decl.span); @@ -296,6 +321,11 @@ pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) { vis.visit_expr(until); fixup.iter_mut().for_each(|f| vis.visit_block(f)); } + ExprKind::Struct(name, copy, fields) => { + vis.visit_path(name); + copy.iter_mut().for_each(|c| vis.visit_expr(c)); + fields.iter_mut().for_each(|f| vis.visit_field_assign(f)); + } ExprKind::TernOp(_, e1, e2, e3) => { vis.visit_expr(e1); vis.visit_expr(e2); @@ -310,6 +340,12 @@ pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) { } } +pub fn walk_field_assign(vis: &mut impl MutVisitor, assign: &mut FieldAssign) { + vis.visit_span(&mut assign.span); + vis.visit_ident(&mut assign.field); + vis.visit_expr(&mut assign.value); +} + pub fn walk_pat(vis: &mut impl MutVisitor, pat: &mut Pat) { vis.visit_span(&mut pat.span); diff --git a/compiler/qsc_ast/src/validate.rs b/compiler/qsc_ast/src/validate.rs index 935c25bd73..acdcf90bde 100644 --- a/compiler/qsc_ast/src/validate.rs +++ b/compiler/qsc_ast/src/validate.rs @@ -3,8 +3,8 @@ use crate::{ ast::{ - Attr, Block, CallableDecl, Expr, FunctorExpr, Ident, Item, Namespace, NodeId, Package, Pat, - Path, QubitInit, SpecDecl, Stmt, Ty, TyDef, Visibility, + Attr, Block, CallableDecl, Expr, FieldAssign, FunctorExpr, Ident, Item, Namespace, NodeId, + Package, Pat, Path, QubitInit, SpecDecl, Stmt, Ty, TyDef, Visibility, }, visit::{self, Visitor}, }; @@ -63,6 +63,16 @@ impl Visitor<'_> for Validator { visit::walk_callable_decl(self, decl); } + fn visit_struct_decl(&mut self, decl: &'_ crate::ast::StructDecl) { + self.check(decl.id, decl); + visit::walk_struct_decl(self, decl); + } + + fn visit_field_def(&mut self, def: &'_ crate::ast::FieldDef) { + self.check(def.id, def); + visit::walk_field_def(self, def); + } + fn visit_spec_decl(&mut self, decl: &SpecDecl) { self.check(decl.id, decl); visit::walk_spec_decl(self, decl); @@ -93,6 +103,11 @@ impl Visitor<'_> for Validator { visit::walk_expr(self, expr); } + fn visit_field_assign(&mut self, assign: &FieldAssign) { + self.check(assign.id, assign); + visit::walk_field_assign(self, assign); + } + fn visit_pat(&mut self, pat: &Pat) { self.check(pat.id, pat); visit::walk_pat(self, pat); diff --git a/compiler/qsc_ast/src/visit.rs b/compiler/qsc_ast/src/visit.rs index d16f2f3463..79a67ce789 100644 --- a/compiler/qsc_ast/src/visit.rs +++ b/compiler/qsc_ast/src/visit.rs @@ -2,10 +2,10 @@ // Licensed under the MIT License. use crate::ast::{ - Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FunctorExpr, FunctorExprKind, Ident, - Idents, Item, ItemKind, Namespace, Package, Pat, PatKind, Path, QubitInit, QubitInitKind, - SpecBody, SpecDecl, Stmt, StmtKind, StringComponent, TopLevelNode, Ty, TyDef, TyDefKind, - TyKind, Visibility, + Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FieldAssign, FieldDef, FunctorExpr, + FunctorExprKind, Ident, Idents, Item, ItemKind, Namespace, Package, Pat, PatKind, Path, + QubitInit, QubitInitKind, SpecBody, SpecDecl, Stmt, StmtKind, StringComponent, StructDecl, + TopLevelNode, Ty, TyDef, TyDefKind, TyKind, Visibility, }; pub trait Visitor<'a>: Sized { @@ -35,6 +35,14 @@ pub trait Visitor<'a>: Sized { walk_callable_decl(self, decl); } + fn visit_struct_decl(&mut self, decl: &'a StructDecl) { + walk_struct_decl(self, decl); + } + + fn visit_field_def(&mut self, def: &'a FieldDef) { + walk_field_def(self, def); + } + fn visit_spec_decl(&mut self, decl: &'a SpecDecl) { walk_spec_decl(self, decl); } @@ -59,6 +67,10 @@ pub trait Visitor<'a>: Sized { walk_expr(self, expr); } + fn visit_field_assign(&mut self, assign: &'a FieldAssign) { + walk_field_assign(self, assign); + } + fn visit_pat(&mut self, pat: &'a Pat) { walk_pat(self, pat); } @@ -105,6 +117,7 @@ pub fn walk_item<'a>(vis: &mut impl Visitor<'a>, item: &'a Item) { vis.visit_ident(ident); vis.visit_ty_def(def); } + ItemKind::Struct(decl) => vis.visit_struct_decl(decl), } } @@ -137,6 +150,16 @@ pub fn walk_callable_decl<'a>(vis: &mut impl Visitor<'a>, decl: &'a CallableDecl } } +pub fn walk_struct_decl<'a>(vis: &mut impl Visitor<'a>, decl: &'a StructDecl) { + vis.visit_ident(&decl.name); + decl.fields.iter().for_each(|f| vis.visit_field_def(f)); +} + +pub fn walk_field_def<'a>(vis: &mut impl Visitor<'a>, def: &'a FieldDef) { + vis.visit_ident(&def.name); + vis.visit_ty(&def.ty); +} + pub fn walk_spec_decl<'a>(vis: &mut impl Visitor<'a>, decl: &'a SpecDecl) { match &decl.body { SpecBody::Gen(_) => {} @@ -267,6 +290,11 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) { vis.visit_expr(until); fixup.iter().for_each(|f| vis.visit_block(f)); } + ExprKind::Struct(name, copy, fields) => { + vis.visit_path(name); + copy.iter().for_each(|c| vis.visit_expr(c)); + fields.iter().for_each(|f| vis.visit_field_assign(f)); + } ExprKind::TernOp(_, e1, e2, e3) => { vis.visit_expr(e1); vis.visit_expr(e2); @@ -281,6 +309,11 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) { } } +pub fn walk_field_assign<'a>(vis: &mut impl Visitor<'a>, assign: &'a FieldAssign) { + vis.visit_ident(&assign.field); + vis.visit_expr(&assign.value); +} + pub fn walk_pat<'a>(vis: &mut impl Visitor<'a>, pat: &'a Pat) { match &*pat.kind { PatKind::Bind(name, ty) => { diff --git a/compiler/qsc_codegen/src/qsharp.rs b/compiler/qsc_codegen/src/qsharp.rs index d4ea80b295..5e2ef4716a 100644 --- a/compiler/qsc_codegen/src/qsharp.rs +++ b/compiler/qsc_codegen/src/qsharp.rs @@ -138,6 +138,7 @@ impl Visitor<'_> for QSharpGen { self.visit_ty_def(def); self.writeln(";"); } + ItemKind::Struct(decl) => self.visit_struct_decl(decl), } } @@ -220,6 +221,26 @@ impl Visitor<'_> for QSharpGen { } } + fn visit_struct_decl(&mut self, decl: &'_ ast::StructDecl) { + self.write("struct "); + self.visit_ident(&decl.name); + self.writeln(" {"); + if let Some((last, most)) = decl.fields.split_last() { + for i in most { + self.visit_field_def(i); + self.write(", "); + } + self.visit_field_def(last); + } + self.writeln("}"); + } + + fn visit_field_def(&mut self, def: &'_ ast::FieldDef) { + self.visit_ident(&def.name); + self.write(" : "); + self.visit_ty(&def.ty); + } + fn visit_spec_decl(&mut self, decl: &'_ SpecDecl) { match decl.spec { ast::Spec::Body => self.write("body "), @@ -490,6 +511,27 @@ impl Visitor<'_> for QSharpGen { self.write("return "); self.visit_expr(expr); } + ExprKind::Struct(name, copy, assigns) => { + self.write("new "); + self.visit_path(name); + self.writeln(" {"); + if let Some(copy) = copy { + self.write("..."); + self.visit_expr(copy); + if !assigns.is_empty() { + self.writeln(","); + } + } + if let Some((last, most)) = assigns.split_last() { + for assign in most { + self.visit_field_assign(assign); + self.writeln(","); + } + self.visit_field_assign(last); + self.writeln(""); + } + self.writeln("}"); + } ExprKind::UnOp(op, expr) => { let op_str = unop_as_str(op); if op == &UnOp::Unwrap { @@ -639,6 +681,12 @@ impl Visitor<'_> for QSharpGen { } } + fn visit_field_assign(&mut self, assign: &'_ ast::FieldAssign) { + self.visit_ident(&assign.field); + self.write(" = "); + self.visit_expr(&assign.value); + } + fn visit_pat(&mut self, pat: &'_ Pat) { match &*pat.kind { PatKind::Bind(name, ty) => { diff --git a/compiler/qsc_codegen/src/qsharp/tests.rs b/compiler/qsc_codegen/src/qsharp/tests.rs index b103c6bffb..9d521b5346 100644 --- a/compiler/qsc_codegen/src/qsharp/tests.rs +++ b/compiler/qsc_codegen/src/qsharp/tests.rs @@ -90,6 +90,134 @@ fn newtype() { ); } +#[test] +fn struct_decl() { + check( + indoc! {r#" + namespace Sample { + struct A {} + struct B { Only : Int } + struct C { First : Int, Second : Double, Third : Bool } + struct D { First : Int, Second: B } + }"#}, + None, + &expect![[r#" + namespace Sample { + struct A {} + struct B { + Only : Int + } + struct C { + First : Int, + Second : Double, + Third : Bool + } + struct D { + First : Int, + Second : B + } + }"#]], + ); +} + +#[test] +fn struct_cons() { + check( + indoc! {r#" + namespace Sample { + struct A {} + struct B { Only : Int } + struct C { First : Int, Second : Double, Third : Bool } + struct D { First : Int, Second: B } + function Foo() : Unit { + let a = new A {}; + let b = new B { Only = 1 }; + let c = new C { Third = true, First = 1, Second = 2.0 }; + let d = new D { First = 1, Second = new B { Only = 2 } }; + } + }"#}, + None, + &expect![[r#" + namespace Sample { + struct A {} + struct B { + Only : Int + } + struct C { + First : Int, + Second : Double, + Third : Bool + } + struct D { + First : Int, + Second : B + } + function Foo() : Unit { + let a = new A {}; + let b = new B { + Only = 1 + }; + let c = new C { + Third = true, + First = 1, + Second = 2. + }; + let d = new D { + First = 1, + Second = new B { + Only = 2 + } + + }; + } + }"#]], + ); +} + +#[test] +fn struct_copy_cons() { + check( + indoc! {r#" + namespace Sample { + struct A { First : Int, Second : Double, Third : Bool } + function Foo() : Unit { + let a = new A { First = 1, Second = 2.0, Third = true }; + let b = new A { ...a }; + let c = new A { ...a, Second = 3.0 }; + let d = new A { ...a, Second = 3.0, Third = false }; + } + }"#}, + None, + &expect![[r#" + namespace Sample { + struct A { + First : Int, + Second : Double, + Third : Bool + } + function Foo() : Unit { + let a = new A { + First = 1, + Second = 2., + Third = true + }; + let b = new A { + ...a + }; + let c = new A { + ...a, + Second = 3. + }; + let d = new A { + ...a, + Second = 3., + Third = false + }; + } + }"#]], + ); +} + #[test] fn statements() { check( diff --git a/compiler/qsc_eval/src/lib.rs b/compiler/qsc_eval/src/lib.rs index b37d468549..2a9b374dcc 100644 --- a/compiler/qsc_eval/src/lib.rs +++ b/compiler/qsc_eval/src/lib.rs @@ -36,9 +36,9 @@ use num_bigint::BigInt; use output::Receiver; use qsc_data_structures::{functors::FunctorApp, index_map::IndexMap, span::Span}; use qsc_fir::fir::{ - self, BinOp, CallableImpl, ExecGraphNode, Expr, ExprId, ExprKind, Field, Global, Lit, - LocalItemId, LocalVarId, PackageId, PackageStoreLookup, PatId, PatKind, PrimField, Res, StmtId, - StoreItemId, StringComponent, UnOp, + self, BinOp, CallableImpl, ExecGraphNode, Expr, ExprId, ExprKind, Field, FieldAssign, Global, + Lit, LocalItemId, LocalVarId, PackageId, PackageStoreLookup, PatId, PatKind, PrimField, Res, + StmtId, StoreItemId, StringComponent, UnOp, }; use qsc_fir::ty::Ty; use qsc_lowerer::map_fir_package_to_hir; @@ -788,6 +788,7 @@ impl State { self.eval_range(start.is_some(), step.is_some(), end.is_some()); } ExprKind::Return(..) => panic!("return expr should be handled by control flow"), + ExprKind::Struct(_, copy, fields) => self.eval_struct(*copy, fields), ExprKind::String(components) => self.collect_string(components), ExprKind::UpdateIndex(_, mid, _) => { let mid_span = globals.get_expr((self.package, *mid).into()).span; @@ -1082,6 +1083,45 @@ impl State { self.set_val_register(Value::Range(val::Range { start, step, end }.into())); } + fn eval_struct(&mut self, copy: Option, fields: &[FieldAssign]) { + // Extract a flat list of field indexes. + let field_indexes = fields + .iter() + .map(|f| match &f.field { + Field::Path(path) => match path.indices.as_slice() { + &[i] => i, + _ => panic!("field path for struct should have a single index"), + }, + _ => panic!("invalid field for struct"), + }) + .collect::>(); + + let len = fields.len(); + + let (field_vals, mut strct) = if copy.is_some() { + // Get the field values and the copy struct value. + let field_vals = self.pop_vals(len + 1); + let (copy, field_vals) = field_vals.split_first().expect("copy value is expected"); + + // Make a clone of the copy struct value. + (field_vals.to_vec(), copy.clone().unwrap_tuple().to_vec()) + } else { + // Make an empty struct of the appropriate size. + (self.pop_vals(len), vec![Value::Int(0); len]) + }; + + // Insert the field values into the new struct. + assert!( + field_vals.len() == field_indexes.len(), + "number of given field values should match the number of given struct fields" + ); + for (i, val) in field_indexes.iter().zip(field_vals.into_iter()) { + strct[*i] = val; + } + + self.set_val_register(Value::Tuple(strct.into())); + } + fn eval_update_index(&mut self, span: Span) -> Result<(), Error> { let values = self.take_val_register().unwrap_array(); let update = self.pop_val(); diff --git a/compiler/qsc_eval/src/tests.rs b/compiler/qsc_eval/src/tests.rs index 6f0197922c..2b38a6c31e 100644 --- a/compiler/qsc_eval/src/tests.rs +++ b/compiler/qsc_eval/src/tests.rs @@ -2123,6 +2123,39 @@ fn update_array_index_expr() { ); } +#[test] +fn struct_cons() { + check_expr( + indoc! {" + namespace A { + struct Pair { First : Int, Second : Int } + } + "}, + indoc! {"{ + open A; + new Pair { First = 1, Second = 2} + }"}, + &expect!["(1, 2)"], + ); +} + +#[test] +fn struct_copy_cons() { + check_expr( + indoc! {" + namespace A { + struct Pair { First : Int, Second : Int } + } + "}, + indoc! {"{ + open A; + let p = new Pair { First = 1, Second = 2}; + new Pair { ...p, First = 3 } + }"}, + &expect!["(3, 2)"], + ); +} + #[test] fn update_udt_known_field_name() { check_expr( diff --git a/compiler/qsc_fir/src/fir.rs b/compiler/qsc_fir/src/fir.rs index cf18ea7c86..535f2f7e94 100644 --- a/compiler/qsc_fir/src/fir.rs +++ b/compiler/qsc_fir/src/fir.rs @@ -1062,6 +1062,8 @@ pub enum ExprKind { Range(Option, Option, Option), /// A return: `return a`. Return(ExprId), + /// A struct constructor. + Struct(Res, Option, Vec), /// A string. String(Vec), /// Update array index: `a w/ b <- c`. @@ -1104,6 +1106,7 @@ impl Display for ExprKind { ExprKind::Lit(lit) => write!(indent, "Lit: {lit}")?, ExprKind::Range(start, step, end) => display_range(indent, *start, *step, *end)?, ExprKind::Return(e) => write!(indent, "Return: {e}")?, + ExprKind::Struct(name, copy, fields) => display_struct(indent, name, *copy, fields)?, ExprKind::String(components) => display_string(indent, components)?, ExprKind::UpdateIndex(expr1, expr2, expr3) => { display_update_index(indent, *expr1, *expr2, *expr3)?; @@ -1278,6 +1281,27 @@ fn display_range( Ok(()) } +fn display_struct( + mut indent: Indented, + name: &Res, + copy: Option, + fields: &Vec, +) -> fmt::Result { + write!(indent, "Struct ({name}):")?; + if copy.is_none() && fields.is_empty() { + write!(indent, " ")?; + return Ok(()); + } + indent = set_indentation(indent, 1); + if let Some(copy) = copy { + write!(indent, "\nCopy: {copy}")?; + } + for field in fields { + write!(indent, "\n{field}")?; + } + Ok(()) +} + fn display_string(mut indent: Indented, components: &[StringComponent]) -> fmt::Result { write!(indent, "String:")?; indent = set_indentation(indent, 1); @@ -1363,6 +1387,29 @@ fn display_while(mut indent: Indented, cond: ExprId, block: BlockId) Ok(()) } +/// A field assignment in a struct constructor expression. +#[derive(Clone, Debug, PartialEq)] +pub struct FieldAssign { + /// The node ID. + pub id: NodeId, + /// The span. + pub span: Span, + /// The field to assign. + pub field: Field, + /// The value to assign to the field. + pub value: ExprId, +} + +impl Display for FieldAssign { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "FieldsAssign {} {}: ({}) {}", + self.id, self.span, self.field, self.value + ) + } +} + /// A string component. #[derive(Clone, Debug, PartialEq)] pub enum StringComponent { diff --git a/compiler/qsc_fir/src/mut_visit.rs b/compiler/qsc_fir/src/mut_visit.rs index 1b144a9f6d..cb1d9013eb 100644 --- a/compiler/qsc_fir/src/mut_visit.rs +++ b/compiler/qsc_fir/src/mut_visit.rs @@ -2,8 +2,9 @@ // Licensed under the MIT License. use crate::fir::{ - Block, BlockId, CallableDecl, CallableImpl, Expr, ExprId, ExprKind, Ident, Item, ItemKind, - Package, Pat, PatId, PatKind, SpecDecl, SpecImpl, Stmt, StmtId, StmtKind, StringComponent, + Block, BlockId, CallableDecl, CallableImpl, Expr, ExprId, ExprKind, FieldAssign, Ident, Item, + ItemKind, Package, Pat, PatId, PatKind, SpecDecl, SpecImpl, Stmt, StmtId, StmtKind, + StringComponent, }; pub trait MutVisitor<'a>: Sized { @@ -169,6 +170,12 @@ pub fn walk_expr<'a>(vis: &mut impl MutVisitor<'a>, expr: ExprId) { step.iter().for_each(|s| vis.visit_expr(*s)); end.iter().for_each(|e| vis.visit_expr(*e)); } + ExprKind::Struct(_, copy, fields) => { + copy.iter().for_each(|c| vis.visit_expr(*c)); + fields + .iter() + .for_each(|FieldAssign { value, .. }| vis.visit_expr(*value)); + } ExprKind::String(components) => { for component in components { match component { diff --git a/compiler/qsc_fir/src/visit.rs b/compiler/qsc_fir/src/visit.rs index 81cb167c47..0999565730 100644 --- a/compiler/qsc_fir/src/visit.rs +++ b/compiler/qsc_fir/src/visit.rs @@ -2,8 +2,9 @@ // Licensed under the MIT License. use crate::fir::{ - Block, BlockId, CallableDecl, CallableImpl, Expr, ExprId, ExprKind, Ident, Item, ItemKind, - Package, Pat, PatId, PatKind, SpecDecl, SpecImpl, Stmt, StmtId, StmtKind, StringComponent, + Block, BlockId, CallableDecl, CallableImpl, Expr, ExprId, ExprKind, FieldAssign, Ident, Item, + ItemKind, Package, Pat, PatId, PatKind, SpecDecl, SpecImpl, Stmt, StmtId, StmtKind, + StringComponent, }; pub trait Visitor<'a>: Sized { @@ -169,6 +170,12 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: ExprId) { step.iter().for_each(|s| vis.visit_expr(*s)); end.iter().for_each(|e| vis.visit_expr(*e)); } + ExprKind::Struct(_, copy, fields) => { + copy.iter().for_each(|c| vis.visit_expr(*c)); + fields + .iter() + .for_each(|FieldAssign { value, .. }| vis.visit_expr(*value)); + } ExprKind::String(components) => { for component in components { match component { diff --git a/compiler/qsc_formatter/src/formatter.rs b/compiler/qsc_formatter/src/formatter.rs index 9bb3816135..b54d2989a1 100644 --- a/compiler/qsc_formatter/src/formatter.rs +++ b/compiler/qsc_formatter/src/formatter.rs @@ -697,6 +697,7 @@ fn is_newline_keyword_or_ampersat(cooked: &TokenKind) -> bool { | Operation | Function | Newtype + | Struct | Namespace | Open | Body @@ -714,7 +715,10 @@ fn is_newline_keyword_or_ampersat(cooked: &TokenKind) -> bool { fn is_starter_keyword(keyword: &Keyword) -> bool { use Keyword::*; - matches!(keyword, For | While | Repeat | If | Within | Return | Fail) + matches!( + keyword, + For | While | Repeat | If | Within | New | Return | Fail + ) } fn is_newline_after_brace(cooked: &TokenKind, delim_state: Delimiter) -> bool { diff --git a/compiler/qsc_frontend/src/lower.rs b/compiler/qsc_frontend/src/lower.rs index c64c6d2180..9f76ef74d3 100644 --- a/compiler/qsc_frontend/src/lower.rs +++ b/compiler/qsc_frontend/src/lower.rs @@ -190,6 +190,19 @@ impl With<'_> { (id, hir::ItemKind::Ty(self.lower_ident(name), udt.clone())) } + ast::ItemKind::Struct(decl) => { + let id = resolve_id(decl.name.id); + let strct = self + .tys + .udts + .get(&id) + .expect("type item should have lowered struct"); + + ( + id, + hir::ItemKind::Ty(self.lower_ident(&decl.name), strct.clone()), + ) + } }; self.lowerer.items.push(hir::Item { @@ -535,6 +548,14 @@ impl With<'_> { fixup.as_ref().map(|f| self.lower_block(f)), ), ast::ExprKind::Return(expr) => hir::ExprKind::Return(Box::new(self.lower_expr(expr))), + ast::ExprKind::Struct(name, copy, fields) => hir::ExprKind::Struct( + self.lower_path(name), + copy.as_ref().map(|c| Box::new(self.lower_expr(c))), + fields + .iter() + .map(|f| Box::new(self.lower_field_assign(&ty, f))) + .collect(), + ), ast::ExprKind::Interpolate(components) => hir::ExprKind::String( components .iter() @@ -579,6 +600,15 @@ impl With<'_> { } } + fn lower_field_assign(&mut self, ty: &Ty, field_assign: &ast::FieldAssign) -> hir::FieldAssign { + hir::FieldAssign { + id: self.lower_id(field_assign.id), + span: field_assign.span, + field: self.lower_field(ty, &field_assign.field.name), + value: Box::new(self.lower_expr(&field_assign.value)), + } + } + fn lower_partial_app( &mut self, callee: &ast::Expr, diff --git a/compiler/qsc_frontend/src/lower/tests.rs b/compiler/qsc_frontend/src/lower/tests.rs index 8f0a9b6545..249673ba7e 100644 --- a/compiler/qsc_frontend/src/lower/tests.rs +++ b/compiler/qsc_frontend/src/lower/tests.rs @@ -616,6 +616,194 @@ fn lift_newtype_from_newtype() { ); } +#[test] +fn lower_struct_decl() { + check_hir( + indoc! {" + namespace A { + struct Foo { + x: Int, + y: Double, + } + } + "}, + &expect![[r#" + Package: + Item 0 [0-73] (Public): + Namespace (Ident 1 [10-11] "A"): Item 1 + Item 1 [18-71] (Public): + Parent: 0 + Type (Ident 0 [25-28] "Foo"): UDT [18-71]: + TyDef [18-71]: Tuple: + TyDef [39-45]: Field: + name: x [39-40] + type: Int + TyDef [55-64]: Field: + name: y [55-56] + type: Double"#]], + ); +} + +#[test] +fn lower_struct_constructor() { + check_hir( + indoc! {" + namespace A { + struct Foo { + x: Int, + y: Double, + } + operation Bar() : Unit { + let z = new Foo { x = 1, y = 2.3 }; + } + } + "}, + &expect![[r#" + Package: + Item 0 [0-152] (Public): + Namespace (Ident 14 [10-11] "A"): Item 1, Item 2 + Item 1 [18-71] (Public): + Parent: 0 + Type (Ident 0 [25-28] "Foo"): UDT [18-71]: + TyDef [18-71]: Tuple: + TyDef [39-45]: Field: + name: x [39-40] + type: Int + TyDef [55-64]: Field: + name: y [55-56] + type: Double + Item 2 [76-150] (Public): + Parent: 0 + Callable 1 [76-150] (operation): + name: Ident 2 [86-89] "Bar" + input: Pat 3 [89-91] [Type Unit]: Unit + output: Unit + functors: empty set + body: SpecDecl 4 [76-150]: Impl: + Block 5 [99-150] [Type Unit]: + Stmt 6 [109-144]: Local (Immutable): + Pat 7 [113-114] [Type UDT<"Foo": Item 1>]: Bind: Ident 8 [113-114] "z" + Expr 9 [117-143] [Type UDT<"Foo": Item 1>]: Struct (Item 1): + FieldsAssign 10 [127-132]: (Path([0])) Expr 11 [131-132] [Type Int]: Lit: Int(1) + FieldsAssign 12 [134-141]: (Path([1])) Expr 13 [138-141] [Type Double]: Lit: Double(2.3) + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn lower_struct_copy_constructor() { + check_hir( + indoc! {" + namespace A { + struct Foo { + x: Int, + y: Double, + } + operation Bar() : Foo { + let z = new Foo { x = 1, y = 2.3 }; + new Foo { ...z, x = 4 }; + } + } + "}, + &expect![[r#" + Package: + Item 0 [0-184] (Public): + Namespace (Ident 19 [10-11] "A"): Item 1, Item 2 + Item 1 [18-71] (Public): + Parent: 0 + Type (Ident 0 [25-28] "Foo"): UDT [18-71]: + TyDef [18-71]: Tuple: + TyDef [39-45]: Field: + name: x [39-40] + type: Int + TyDef [55-64]: Field: + name: y [55-56] + type: Double + Item 2 [76-182] (Public): + Parent: 0 + Callable 1 [76-182] (operation): + name: Ident 2 [86-89] "Bar" + input: Pat 3 [89-91] [Type Unit]: Unit + output: UDT<"Foo": Item 1> + functors: empty set + body: SpecDecl 4 [76-182]: Impl: + Block 5 [98-182] [Type Unit]: + Stmt 6 [108-143]: Local (Immutable): + Pat 7 [112-113] [Type UDT<"Foo": Item 1>]: Bind: Ident 8 [112-113] "z" + Expr 9 [116-142] [Type UDT<"Foo": Item 1>]: Struct (Item 1): + FieldsAssign 10 [126-131]: (Path([0])) Expr 11 [130-131] [Type Int]: Lit: Int(1) + FieldsAssign 12 [133-140]: (Path([1])) Expr 13 [137-140] [Type Double]: Lit: Double(2.3) + Stmt 14 [152-176]: Semi: Expr 15 [152-175] [Type UDT<"Foo": Item 1>]: Struct (Item 1): + Copy: Expr 16 [165-166] [Type UDT<"Foo": Item 1>]: Var: Local 8 + FieldsAssign 17 [168-173]: (Path([0])) Expr 18 [172-173] [Type Int]: Lit: Int(4) + adj: + ctl: + ctl-adj: "#]], + ); +} + +#[test] +fn lower_struct_copy_constructor_with_alternative_fields() { + check_hir( + indoc! {r#" + namespace A { + struct Foo { + x: Int, + y: Double, + z: String + } + operation Bar() : Foo { + let z = new Foo { x = 1, y = 2.3, z = "four" }; + new Foo { ...z, z = "five", y = 6.7 }; + } + } + "#}, + &expect![[r#" + Package: + Item 0 [0-228] (Public): + Namespace (Ident 23 [10-11] "A"): Item 1, Item 2 + Item 1 [18-89] (Public): + Parent: 0 + Type (Ident 0 [25-28] "Foo"): UDT [18-89]: + TyDef [18-89]: Tuple: + TyDef [39-45]: Field: + name: x [39-40] + type: Int + TyDef [55-64]: Field: + name: y [55-56] + type: Double + TyDef [74-83]: Field: + name: z [74-75] + type: String + Item 2 [94-226] (Public): + Parent: 0 + Callable 1 [94-226] (operation): + name: Ident 2 [104-107] "Bar" + input: Pat 3 [107-109] [Type Unit]: Unit + output: UDT<"Foo": Item 1> + functors: empty set + body: SpecDecl 4 [94-226]: Impl: + Block 5 [116-226] [Type Unit]: + Stmt 6 [126-173]: Local (Immutable): + Pat 7 [130-131] [Type UDT<"Foo": Item 1>]: Bind: Ident 8 [130-131] "z" + Expr 9 [134-172] [Type UDT<"Foo": Item 1>]: Struct (Item 1): + FieldsAssign 10 [144-149]: (Path([0])) Expr 11 [148-149] [Type Int]: Lit: Int(1) + FieldsAssign 12 [151-158]: (Path([1])) Expr 13 [155-158] [Type Double]: Lit: Double(2.3) + FieldsAssign 14 [160-170]: (Path([2])) Expr 15 [164-170] [Type String]: String: + Lit: "four" + Stmt 16 [182-220]: Semi: Expr 17 [182-219] [Type UDT<"Foo": Item 1>]: Struct (Item 1): + Copy: Expr 18 [195-196] [Type UDT<"Foo": Item 1>]: Var: Local 8 + FieldsAssign 19 [198-208]: (Path([2])) Expr 20 [202-208] [Type String]: String: + Lit: "five" + FieldsAssign 21 [210-217]: (Path([1])) Expr 22 [214-217] [Type Double]: Lit: Double(6.7) + adj: + ctl: + ctl-adj: "#]], + ); +} + #[test] fn lambda_function_empty_closure() { check_hir( diff --git a/compiler/qsc_frontend/src/resolve.rs b/compiler/qsc_frontend/src/resolve.rs index 07f110ef8b..5349f385e2 100644 --- a/compiler/qsc_frontend/src/resolve.rs +++ b/compiler/qsc_frontend/src/resolve.rs @@ -569,6 +569,19 @@ impl Resolver { scope.tys.insert(Rc::clone(&name.name), id); scope.terms.insert(Rc::clone(&name.name), id); } + ast::ItemKind::Struct(decl) => { + let id = intrapackage(assigner.next_item()); + self.names.insert( + decl.name.id, + Res::Item( + id, + ItemStatus::from_attrs(&ast_attrs_as_hir_attrs(&item.attrs)), + ), + ); + let scope = self.current_scope_mut(); + scope.tys.insert(Rc::clone(&decl.name.name), id); + scope.terms.insert(Rc::clone(&decl.name.name), id); + } ast::ItemKind::Err => {} } } @@ -799,6 +812,11 @@ impl AstVisitor<'_> for With<'_> { } self.visit_expr(replace); } + ast::ExprKind::Struct(path, copy, fields) => { + self.resolver.resolve_path(NameKind::Ty, path); + copy.iter().for_each(|c| self.visit_expr(c)); + fields.iter().for_each(|f| self.visit_field_assign(f)); + } _ => ast_visit::walk_expr(self, expr), } } @@ -990,81 +1008,101 @@ fn bind_global_item( ) -> Result<(), Vec> { match &*item.kind { ast::ItemKind::Callable(decl) => { - let item_id = next_id(); - let status = ItemStatus::from_attrs(&ast_attrs_as_hir_attrs(item.attrs.as_ref())); - let res = Res::Item(item_id, status); - names.insert(decl.name.id, res); - let mut errors = Vec::new(); - match scope - .terms - .get_mut_or_default(namespace) - .entry(Rc::clone(&decl.name.name)) - { - Entry::Occupied(_) => { - let namespace_name = scope - .namespaces - .find_namespace_by_id(&namespace) - .0 - .join("."); - errors.push(Error::Duplicate( - decl.name.name.to_string(), - namespace_name.to_string(), - decl.name.span, - )); - } - Entry::Vacant(entry) => { - entry.insert(res); - } - } + bind_callable(decl, namespace, next_id, item, names, scope) + } + ast::ItemKind::Ty(name, _) => bind_ty(name, namespace, next_id, item, names, scope), + ast::ItemKind::Struct(decl) => bind_ty(&decl.name, namespace, next_id, item, names, scope), + ast::ItemKind::Err | ast::ItemKind::Open(..) => Ok(()), + } +} - if decl_is_intrinsic(decl) && !scope.intrinsics.insert(Rc::clone(&decl.name.name)) { - errors.push(Error::DuplicateIntrinsic( - decl.name.name.to_string(), - decl.name.span, - )); - } +fn bind_callable( + decl: &CallableDecl, + namespace: NamespaceId, + next_id: impl FnOnce() -> ItemId, + item: &ast::Item, + names: &mut IndexMap, + scope: &mut GlobalScope, +) -> Result<(), Vec> { + let item_id = next_id(); + let status = ItemStatus::from_attrs(&ast_attrs_as_hir_attrs(item.attrs.as_ref())); + let res = Res::Item(item_id, status); + names.insert(decl.name.id, res); + let mut errors = Vec::new(); + + match scope + .terms + .get_mut_or_default(namespace) + .entry(Rc::clone(&decl.name.name)) + { + Entry::Occupied(_) => { + let namespace_name = scope + .namespaces + .find_namespace_by_id(&namespace) + .0 + .join("."); + errors.push(Error::Duplicate( + decl.name.name.to_string(), + namespace_name.to_string(), + decl.name.span, + )); + } + Entry::Vacant(entry) => { + entry.insert(res); + } + } + if decl_is_intrinsic(decl) && !scope.intrinsics.insert(Rc::clone(&decl.name.name)) { + errors.push(Error::DuplicateIntrinsic( + decl.name.name.to_string(), + decl.name.span, + )); + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} - if errors.is_empty() { - Ok(()) - } else { - Err(errors) - } +fn bind_ty( + name: &Ident, + namespace: NamespaceId, + next_id: impl FnOnce() -> ItemId, + item: &ast::Item, + names: &mut IndexMap, + scope: &mut GlobalScope, +) -> Result<(), Vec> { + let item_id = next_id(); + let status = ItemStatus::from_attrs(&ast_attrs_as_hir_attrs(item.attrs.as_ref())); + let res = Res::Item(item_id, status); + names.insert(name.id, res); + match ( + scope + .terms + .get_mut_or_default(namespace) + .entry(Rc::clone(&name.name)), + scope + .tys + .get_mut_or_default(namespace) + .entry(Rc::clone(&name.name)), + ) { + (Entry::Occupied(_), _) | (_, Entry::Occupied(_)) => { + let namespace_name = scope + .namespaces + .find_namespace_by_id(&namespace) + .0 + .join("."); + Err(vec![Error::Duplicate( + name.name.to_string(), + namespace_name, + name.span, + )]) } - ast::ItemKind::Ty(name, _) => { - let item_id = next_id(); - let status = ItemStatus::from_attrs(&ast_attrs_as_hir_attrs(item.attrs.as_ref())); - let res = Res::Item(item_id, status); - names.insert(name.id, res); - match ( - scope - .terms - .get_mut_or_default(namespace) - .entry(Rc::clone(&name.name)), - scope - .tys - .get_mut_or_default(namespace) - .entry(Rc::clone(&name.name)), - ) { - (Entry::Occupied(_), _) | (_, Entry::Occupied(_)) => { - let namespace_name = scope - .namespaces - .find_namespace_by_id(&namespace) - .0 - .join("."); - Err(vec![Error::Duplicate( - name.name.to_string(), - namespace_name, - name.span, - )]) - } - (Entry::Vacant(term_entry), Entry::Vacant(ty_entry)) => { - term_entry.insert(res); - ty_entry.insert(res); - Ok(()) - } - } + (Entry::Vacant(term_entry), Entry::Vacant(ty_entry)) => { + term_entry.insert(res); + ty_entry.insert(res); + Ok(()) } - ast::ItemKind::Err | ast::ItemKind::Open(..) => Ok(()), } } diff --git a/compiler/qsc_frontend/src/resolve/tests.rs b/compiler/qsc_frontend/src/resolve/tests.rs index 186de5e7b9..de299d9426 100644 --- a/compiler/qsc_frontend/src/resolve/tests.rs +++ b/compiler/qsc_frontend/src/resolve/tests.rs @@ -901,6 +901,24 @@ fn ty_decl() { ); } +#[test] +fn struct_decl() { + check( + indoc! {" + namespace Foo { + struct A {} + function B(a : A) : Unit {} + } + "}, + &expect![[r#" + namespace namespace7 { + struct item1 {} + function item2(local11 : item1) : Unit {} + } + "#]], + ); +} + #[test] fn ty_decl_duplicate_error() { check( @@ -921,6 +939,26 @@ fn ty_decl_duplicate_error() { ); } +#[test] +fn struct_decl_duplicate_error() { + check( + indoc! {" + namespace Foo { + struct A {} + struct A { first : Bool } + } + "}, + &expect![[r#" + namespace namespace7 { + struct item1 {} + struct item2 { first : Bool } + } + + // Duplicate("A", "Foo", Span { lo: 43, hi: 44 }) + "#]], + ); +} + #[test] fn ty_decl_duplicate_error_on_built_in_ty() { check( @@ -939,6 +977,24 @@ fn ty_decl_duplicate_error_on_built_in_ty() { ); } +#[test] +fn struct_decl_duplicate_error_on_built_in_ty() { + check( + indoc! {" + namespace Microsoft.Quantum.Core { + struct Pauli {} + } + "}, + &expect![[r#" + namespace namespace4 { + struct item1 {} + } + + // Duplicate("Pauli", "Microsoft.Quantum.Core", Span { lo: 46, hi: 51 }) + "#]], + ); +} + #[test] fn ty_decl_in_ty_decl() { check( @@ -957,6 +1013,24 @@ fn ty_decl_in_ty_decl() { ); } +#[test] +fn struct_decl_in_struct_decl() { + check( + indoc! {" + namespace Foo { + struct A {} + struct B { a : A } + } + "}, + &expect![[r#" + namespace namespace7 { + struct item1 {} + struct item2 { a : item1 } + } + "#]], + ); +} + #[test] fn ty_decl_recursive() { check( @@ -973,6 +1047,22 @@ fn ty_decl_recursive() { ); } +#[test] +fn struct_decl_recursive() { + check( + indoc! {" + namespace Foo { + struct A { a : A } + } + "}, + &expect![[r#" + namespace namespace7 { + struct item1 { a : item1 } + } + "#]], + ); +} + #[test] fn ty_decl_cons() { check( @@ -997,6 +1087,82 @@ fn ty_decl_cons() { ); } +#[test] +fn struct_decl_call_cons() { + check( + indoc! {" + namespace Foo { + struct A {} + + function B() : A { + A() + } + } + "}, + &expect![[r#" + namespace namespace7 { + struct item1 {} + + function item2() : item1 { + item1() + } + } + "#]], + ); +} + +#[test] +fn struct_decl_cons() { + check( + indoc! {" + namespace Foo { + struct A {} + + function B() : A { + new A {} + } + } + "}, + &expect![[r#" + namespace namespace7 { + struct item1 {} + + function item2() : item1 { + new item1 {} + } + } + "#]], + ); +} + +#[test] +fn struct_decl_cons_with_fields() { + check( + indoc! {" + namespace Foo { + struct A {} + struct B {} + struct C { a : A, b : B } + + function D() : C { + new C { a = new A {}, b = new B {} } + } + } + "}, + &expect![[r#" + namespace namespace7 { + struct item1 {} + struct item2 {} + struct item3 { a : item1, b : item2 } + + function item4() : item3 { + new item3 { a = new item1 {}, b = new item2 {} } + } + } + "#]], + ); +} + #[test] fn unknown_term() { check( diff --git a/compiler/qsc_frontend/src/typeck.rs b/compiler/qsc_frontend/src/typeck.rs index f44217f24d..46a412c4cb 100644 --- a/compiler/qsc_frontend/src/typeck.rs +++ b/compiler/qsc_frontend/src/typeck.rs @@ -69,6 +69,15 @@ enum ErrorKind { #[error("type {0} does not have a field `{1}`")] #[diagnostic(code("Qsc.TypeCk.MissingClassHasField"))] MissingClassHasField(String, String, #[label] Span), + #[error("type {0} is not a struct")] + #[diagnostic(code("Qsc.TypeCk.MissingClassStruct"))] + MissingClassStruct(String, #[label] Span), + #[error("duplicate field `{1}` listed in constructor for type {0}")] + #[diagnostic(code("Qsc.TypeCk.DuplicateField"))] + DuplicateField(String, String, #[label] Span), + #[error("incorrect number of field assignments for type {0}")] + #[diagnostic(code("Qsc.TypeCk.MissingClassCorrectFieldCount"))] + MissingClassCorrectFieldCount(String, #[label] Span), #[error("type {0} cannot be indexed by type {1}")] #[diagnostic(help( "only array types can be indexed, and only Int and Range can be used as the index" diff --git a/compiler/qsc_frontend/src/typeck/check.rs b/compiler/qsc_frontend/src/typeck/check.rs index 644ec07c20..3d85d53f1f 100644 --- a/compiler/qsc_frontend/src/typeck/check.rs +++ b/compiler/qsc_frontend/src/typeck/check.rs @@ -243,6 +243,34 @@ impl Visitor<'_> for ItemCollector<'_> { ); self.checker.globals.insert(item, cons); } + ast::ItemKind::Struct(decl) => { + let span = item.span; + let Some(&Res::Item(item, _)) = self.names.get(decl.name.id) else { + panic!("type should have item ID"); + }; + + let def = convert::ast_struct_decl_as_ty_def(decl); + + let (cons, cons_errors) = + convert::ast_ty_def_cons(self.names, &decl.name.name, item, &def); + let (udt_def, def_errors) = convert::ast_ty_def(self.names, &def); + self.checker.errors.extend( + cons_errors + .into_iter() + .chain(def_errors) + .map(|MissingTyError(span)| Error(ErrorKind::MissingItemTy(span))), + ); + + self.checker.table.udts.insert( + item, + Udt { + name: decl.name.name.clone(), + span, + definition: udt_def, + }, + ); + self.checker.globals.insert(item, cons); + } _ => {} } diff --git a/compiler/qsc_frontend/src/typeck/convert.rs b/compiler/qsc_frontend/src/typeck/convert.rs index 955647d06d..9f2c5caf4a 100644 --- a/compiler/qsc_frontend/src/typeck/convert.rs +++ b/compiler/qsc_frontend/src/typeck/convert.rs @@ -6,7 +6,7 @@ use std::rc::Rc; use crate::resolve::{self, Names}; use qsc_ast::ast::{ self, CallableBody, CallableDecl, CallableKind, FunctorExpr, FunctorExprKind, Ident, Pat, - PatKind, SetOp, Spec, TyDef, TyDefKind, TyKind, + PatKind, Path, SetOp, Spec, StructDecl, TyDef, TyDefKind, TyKind, }; use qsc_data_structures::span::Span; use qsc_hir::{ @@ -42,25 +42,7 @@ pub(crate) fn ty_from_ast(names: &Names, ty: &ast::Ty) -> (Ty, Vec (Ty::Err, vec![MissingTyError(ty.span)]), TyKind::Paren(inner) => ty_from_ast(names, inner), - TyKind::Path(path) => { - let ty = match names.get(path.id) { - Some(&resolve::Res::Item(item, _)) => { - Ty::Udt(path.name.name.clone(), hir::Res::Item(item)) - } - Some(&resolve::Res::PrimTy(prim)) => Ty::Prim(prim), - Some(resolve::Res::UnitTy) => Ty::Tuple(Vec::new()), - // a path should never resolve to a parameter, - // as there is a syntactic difference between - // paths and parameters. - // So realistically, by construction, `Param` here is unreachable. - Some(resolve::Res::Local(_) | resolve::Res::Param(_)) => unreachable!( - "A path should never resolve \ - to a local or a parameter, as there is syntactic differentiation." - ), - None => Ty::Err, - }; - (ty, Vec::new()) - } + TyKind::Path(path) => (ty_from_path(names, path), Vec::new()), TyKind::Param(name) => match names.get(name.id) { Some(resolve::Res::Param(id)) => (Ty::Param(name.name.clone(), *id), Vec::new()), Some(_) => unreachable!( @@ -83,6 +65,43 @@ pub(crate) fn ty_from_ast(names: &Names, ty: &ast::Ty) -> (Ty, Vec Ty { + match names.get(path.id) { + Some(&resolve::Res::Item(item, _)) => Ty::Udt(path.name.name.clone(), hir::Res::Item(item)), + Some(&resolve::Res::PrimTy(prim)) => Ty::Prim(prim), + Some(resolve::Res::UnitTy) => Ty::Tuple(Vec::new()), + // a path should never resolve to a parameter, + // as there is a syntactic difference between + // paths and parameters. + // So realistically, by construction, `Param` here is unreachable. + Some(resolve::Res::Local(_) | resolve::Res::Param(_)) => unreachable!( + "A path should never resolve \ + to a local or a parameter, as there is syntactic differentiation." + ), + None => Ty::Err, + } +} + +/// Convert a struct declaration into a UDT type definition. +pub(super) fn ast_struct_decl_as_ty_def(decl: &StructDecl) -> TyDef { + TyDef { + id: decl.id, + span: decl.span, + kind: Box::new(TyDefKind::Tuple( + decl.fields + .iter() + .map(|f| { + Box::new(TyDef { + id: f.id, + span: f.span, + kind: Box::new(TyDefKind::Field(Some(f.name.clone()), f.ty.clone())), + }) + }) + .collect(), + )), + } +} + pub(super) fn ast_ty_def_cons( names: &Names, ty_name: &Rc, diff --git a/compiler/qsc_frontend/src/typeck/infer.rs b/compiler/qsc_frontend/src/typeck/infer.rs index 817701e893..7a8c08d48f 100644 --- a/compiler/qsc_frontend/src/typeck/infer.rs +++ b/compiler/qsc_frontend/src/typeck/infer.rs @@ -10,7 +10,7 @@ use qsc_hir::{ Prim, Scheme, Ty, Udt, }, }; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use std::{ collections::{hash_map::Entry, VecDeque}, fmt::Debug, @@ -47,6 +47,12 @@ pub(super) enum Class { name: String, item: Ty, }, + Struct(Ty), + HasStructShape { + record: Ty, + is_copy: bool, + fields: Vec<(String, Span)>, + }, HasIndex { container: Ty, index: Ty, @@ -73,13 +79,14 @@ impl Class { | Self::Eq(ty) | Self::Integral(ty) | Self::Num(ty) - | Self::Show(ty) => { + | Self::Show(ty) + | Self::Struct(ty) => { vec![ty] } Self::Call { callee, .. } => vec![callee], Self::Ctl { op, .. } => vec![op], Self::Exp { base, .. } => vec![base], - Self::HasField { record, .. } => vec![record], + Self::HasField { record, .. } | Self::HasStructShape { record, .. } => vec![record], Self::HasIndex { container, index, .. } => vec![container, index], @@ -115,6 +122,16 @@ impl Class { name, item: f(item), }, + Self::Struct(ty) => Self::Struct(f(ty)), + Self::HasStructShape { + record, + is_copy, + fields, + } => Self::HasStructShape { + record: f(record), + is_copy, + fields, + }, Self::HasIndex { container, index, @@ -157,6 +174,12 @@ impl Class { Class::HasField { record, name, item } => { check_has_field(udts, &record, name, item, span) } + Class::Struct(ty) => check_struct(udts, &ty, span), + Class::HasStructShape { + record, + is_copy, + fields, + } => check_has_struct_shape(udts, &record, is_copy, &fields, span), Class::HasIndex { container, index, @@ -931,6 +954,74 @@ fn check_has_field( } } +fn ty_to_udt<'a>(udts: &'a FxHashMap, record: &Ty) -> Option<&'a Udt> { + match record { + Ty::Udt(_, Res::Item(id)) => udts.get(id), + _ => None, + } +} + +fn check_struct( + udts: &FxHashMap, + record: &Ty, + span: Span, +) -> (Vec, Vec) { + match ty_to_udt(udts, record) { + Some(udt) if udt.is_struct() => (Vec::new(), Vec::new()), + _ => ( + Vec::new(), + vec![Error(ErrorKind::MissingClassStruct(record.display(), span))], + ), + } +} + +fn check_has_struct_shape( + udts: &FxHashMap, + record: &Ty, + is_copy: bool, + fields: &[(String, Span)], + span: Span, +) -> (Vec, Vec) { + let mut errors = Vec::new(); + + // Check for duplicate fields. + let mut seen = FxHashSet::default(); + for (field_name, field_span) in fields { + if !seen.insert(field_name) { + errors.push(Error(ErrorKind::DuplicateField( + record.display(), + field_name.clone(), + *field_span, + ))); + } + } + + match ty_to_udt(udts, record) { + Some(udt) => { + // We could compare the actual field names, but the HasField constraint already + // ensures all the listed fields are valid, and we just checked against duplicates, + // so we can just check the count. + + let definition_field_count = match &udt.definition.kind { + qsc_hir::ty::UdtDefKind::Field(_) => 0, + qsc_hir::ty::UdtDefKind::Tuple(fields) => fields.len(), + }; + + if (is_copy && fields.len() > definition_field_count) + || (!is_copy && fields.len() != definition_field_count) + { + errors.push(Error(ErrorKind::MissingClassCorrectFieldCount( + record.display(), + span, + ))); + } + + (Vec::new(), errors) + } + None => (Vec::new(), errors), + } +} + fn check_has_index( container: Ty, index: Ty, diff --git a/compiler/qsc_frontend/src/typeck/rules.rs b/compiler/qsc_frontend/src/typeck/rules.rs index 480779016f..7084f28a7a 100644 --- a/compiler/qsc_frontend/src/typeck/rules.rs +++ b/compiler/qsc_frontend/src/typeck/rules.rs @@ -8,7 +8,7 @@ use super::{ }; use crate::resolve::{self, Names, Res}; use qsc_ast::ast::{ - self, BinOp, Block, Expr, ExprKind, Functor, Lit, NodeId, Pat, PatKind, QubitInit, + self, BinOp, Block, Expr, ExprKind, Functor, Ident, Lit, NodeId, Pat, PatKind, QubitInit, QubitInitKind, Spec, Stmt, StmtKind, StringComponent, TernOp, TyKind, UnOp, }; use qsc_data_structures::span::Span; @@ -444,6 +444,50 @@ impl<'a> Context<'a> { } self.diverge() } + ExprKind::Struct(name, copy, fields) => { + let container = convert::ty_from_path(self.names, name); + + self.inferrer + .class(name.span, Class::Struct(container.clone())); + + // If the container is not a struct type, assign type Err and don't continue to process the fields. + match &container { + Ty::Udt(_, hir::Res::Item(item_id)) => match self.table.udts.get(item_id) { + Some(udt) if udt.is_struct() => {} + _ => return converge(Ty::Err), + }, + _ => return converge(Ty::Err), + } + + self.inferrer.class( + expr.span, + Class::HasStructShape { + record: container.clone(), + is_copy: copy.is_some(), + fields: fields + .iter() + .map(|field| (field.field.name.to_string(), field.span)) + .collect(), + }, + ); + + // Ensure that the copy expression has the same type as the given struct. + if let Some(copy) = copy { + let copy_ty = self.infer_expr(copy); + self.inferrer.eq(copy.span, container.clone(), copy_ty.ty); + } + + for field in fields.iter() { + self.infer_field_assign( + field.span, + container.clone(), + &field.field, + &field.value, + ); + } + + converge(container) + } ExprKind::TernOp(TernOp::Cond, cond, if_true, if_false) => { let cond_span = cond.span; let cond = self.infer_expr(cond); @@ -671,6 +715,27 @@ impl<'a> Context<'a> { } } + fn infer_field_assign( + &mut self, + span: Span, + container_ty: Ty, + field_name: &Ident, + value: &Expr, + ) -> Partial { + let value = self.infer_expr(value); + let field = field_name.name.to_string(); + self.inferrer.class( + span, + Class::HasField { + record: container_ty.clone(), + name: field, + item: value.ty.clone(), + }, + ); + + self.diverge_if(value.diverges, converge(container_ty)) + } + fn infer_pat(&mut self, pat: &Pat) -> Ty { let ty = match &*pat.kind { PatKind::Bind(name, None) => { diff --git a/compiler/qsc_frontend/src/typeck/tests.rs b/compiler/qsc_frontend/src/typeck/tests.rs index f1dc0c6871..4041d910ed 100644 --- a/compiler/qsc_frontend/src/typeck/tests.rs +++ b/compiler/qsc_frontend/src/typeck/tests.rs @@ -2190,6 +2190,329 @@ fn newtype_cons_wrong_input() { ); } +#[test] +fn struct_cons() { + check( + indoc! {" + namespace A { + struct Pair { First : Int, Second : Int } + function Foo() : Pair { new Pair { First = 5, Second = 6 } } + } + "}, + "", + &expect![[r#" + #19 76-78 "()" : Unit + #23 86-124 "{ new Pair { First = 5, Second = 6 } }" : UDT<"Pair": Item 1> + #25 88-122 "new Pair { First = 5, Second = 6 }" : UDT<"Pair": Item 1> + #30 107-108 "5" : Int + #33 119-120 "6" : Int + "#]], + ); +} + +#[test] +fn struct_cons_wrong_input() { + check( + indoc! {" + namespace A { + struct Pair { First : Int, Second : Int } + function Foo() : Pair { new Pair { First = 5.0, Second = 6 } } + } + "}, + "", + &expect![[r#" + #19 76-78 "()" : Unit + #23 86-126 "{ new Pair { First = 5.0, Second = 6 } }" : UDT<"Pair": Item 1> + #25 88-124 "new Pair { First = 5.0, Second = 6 }" : UDT<"Pair": Item 1> + #30 107-110 "5.0" : Double + #33 121-122 "6" : Int + Error(Type(Error(TyMismatch("Double", "Int", Span { lo: 99, hi: 110 })))) + "#]], + ); +} + +#[test] +fn struct_cons_wrong_field() { + check( + indoc! {" + namespace A { + struct Pair { First : Int, Second : Int } + function Foo() : Pair { new Pair { First = 5, NotSecond = 6 } } + } + "}, + "", + &expect![[r#" + #19 76-78 "()" : Unit + #23 86-127 "{ new Pair { First = 5, NotSecond = 6 } }" : UDT<"Pair": Item 1> + #25 88-125 "new Pair { First = 5, NotSecond = 6 }" : UDT<"Pair": Item 1> + #30 107-108 "5" : Int + #33 122-123 "6" : Int + Error(Type(Error(MissingClassHasField("Pair", "NotSecond", Span { lo: 110, hi: 123 })))) + "#]], + ); +} + +#[test] +fn struct_cons_dup_field() { + check( + indoc! {" + namespace A { + struct Pair { First : Int, Second : Int } + function Foo() : Pair { new Pair { First = 5, First = 6 } } + } + "}, + "", + &expect![[r#" + #19 76-78 "()" : Unit + #23 86-123 "{ new Pair { First = 5, First = 6 } }" : UDT<"Pair": Item 1> + #25 88-121 "new Pair { First = 5, First = 6 }" : UDT<"Pair": Item 1> + #30 107-108 "5" : Int + #33 118-119 "6" : Int + Error(Type(Error(DuplicateField("Pair", "First", Span { lo: 110, hi: 119 })))) + "#]], + ); +} + +#[test] +fn struct_cons_too_few_fields() { + check( + indoc! {" + namespace A { + struct Pair { First : Int, Second : Int } + function Foo() : Pair { new Pair { First = 5 } } + } + "}, + "", + &expect![[r#" + #19 76-78 "()" : Unit + #23 86-112 "{ new Pair { First = 5 } }" : UDT<"Pair": Item 1> + #25 88-110 "new Pair { First = 5 }" : UDT<"Pair": Item 1> + #30 107-108 "5" : Int + Error(Type(Error(MissingClassCorrectFieldCount("Pair", Span { lo: 88, hi: 110 })))) + "#]], + ); +} + +#[test] +fn struct_cons_too_many_fields() { + check( + indoc! {" + namespace A { + struct Pair { First : Int, Second : Int } + function Foo() : Pair { new Pair { First = 5, Second = 6, Third = 7 } } + } + "}, + "", + &expect![[r#" + #19 76-78 "()" : Unit + #23 86-135 "{ new Pair { First = 5, Second = 6, Third = 7 } }" : UDT<"Pair": Item 1> + #25 88-133 "new Pair { First = 5, Second = 6, Third = 7 }" : UDT<"Pair": Item 1> + #30 107-108 "5" : Int + #33 119-120 "6" : Int + #36 130-131 "7" : Int + Error(Type(Error(MissingClassCorrectFieldCount("Pair", Span { lo: 88, hi: 133 })))) + Error(Type(Error(MissingClassHasField("Pair", "Third", Span { lo: 122, hi: 131 })))) + "#]], + ); +} + +#[test] +fn struct_copy_cons() { + check( + indoc! {" + namespace A { + struct Pair { First : Int, Second : Int } + function Foo() : Pair { + let pair = new Pair { First = 5, Second = 6 }; + new Pair { ...pair } + } + } + "}, + "", + &expect![[r#" + #19 76-78 "()" : Unit + #23 86-177 "{\n let pair = new Pair { First = 5, Second = 6 };\n new Pair { ...pair }\n }" : UDT<"Pair": Item 1> + #25 100-104 "pair" : UDT<"Pair": Item 1> + #27 107-141 "new Pair { First = 5, Second = 6 }" : UDT<"Pair": Item 1> + #32 126-127 "5" : Int + #35 138-139 "6" : Int + #37 151-171 "new Pair { ...pair }" : UDT<"Pair": Item 1> + #40 165-169 "pair" : UDT<"Pair": Item 1> + "#]], + ); +} + +#[test] +fn struct_copy_cons_with_fields() { + check( + indoc! {" + namespace A { + struct Pair { First : Int, Second : Int } + function Foo() : Pair { + let pair = new Pair { First = 5, Second = 6 }; + new Pair { ...pair, First = 7 } + } + } + "}, + "", + &expect![[r#" + #19 76-78 "()" : Unit + #23 86-188 "{\n let pair = new Pair { First = 5, Second = 6 };\n new Pair { ...pair, First = 7 }\n }" : UDT<"Pair": Item 1> + #25 100-104 "pair" : UDT<"Pair": Item 1> + #27 107-141 "new Pair { First = 5, Second = 6 }" : UDT<"Pair": Item 1> + #32 126-127 "5" : Int + #35 138-139 "6" : Int + #37 151-182 "new Pair { ...pair, First = 7 }" : UDT<"Pair": Item 1> + #40 165-169 "pair" : UDT<"Pair": Item 1> + #45 179-180 "7" : Int + "#]], + ); +} + +#[test] +fn struct_copy_cons_too_many_fields() { + check( + indoc! {" + namespace A { + struct Pair { First : Int, Second : Int } + function Foo() : Pair { + let pair = new Pair { First = 5, Second = 6 }; + new Pair { ...pair, First = 7, Second = 8, Third = 9 } + } + } + "}, + "", + &expect![[r#" + #19 76-78 "()" : Unit + #23 86-211 "{\n let pair = new Pair { First = 5, Second = 6 };\n new Pair { ...pair, First = 7, Second = 8, Third = 9 }\n }" : UDT<"Pair": Item 1> + #25 100-104 "pair" : UDT<"Pair": Item 1> + #27 107-141 "new Pair { First = 5, Second = 6 }" : UDT<"Pair": Item 1> + #32 126-127 "5" : Int + #35 138-139 "6" : Int + #37 151-205 "new Pair { ...pair, First = 7, Second = 8, Third = 9 }" : UDT<"Pair": Item 1> + #40 165-169 "pair" : UDT<"Pair": Item 1> + #45 179-180 "7" : Int + #48 191-192 "8" : Int + #51 202-203 "9" : Int + Error(Type(Error(MissingClassCorrectFieldCount("Pair", Span { lo: 151, hi: 205 })))) + Error(Type(Error(MissingClassHasField("Pair", "Third", Span { lo: 194, hi: 203 })))) + "#]], + ); +} + +#[test] +fn struct_cons_udt_not_struct() { + check( + indoc! {" + namespace A { + newtype Triple = (Int, Int, Int); + function Foo() : Triple { new Triple { First = 5, Second = 6 } } + } + "}, + "", + &expect![[r#" + #19 68-70 "()" : Unit + #23 80-120 "{ new Triple { First = 5, Second = 6 } }" : ? + #25 82-118 "new Triple { First = 5, Second = 6 }" : ? + #30 103-104 "5" : ? + #33 115-116 "6" : ? + Error(Type(Error(MissingClassStruct("Triple", Span { lo: 86, hi: 92 })))) + "#]], + ); +} + +#[test] +fn struct_cons_struct_like_udt() { + check( + indoc! {" + namespace A { + newtype Pair = (First : Int, Second : Int); + function Foo() : Pair { new Pair { First = 5, Second = 6 } } + } + "}, + "", + &expect![[r##" + #19 78-80 "()" : Unit + #23 88-126 "{ new Pair { First = 5, Second = 6 } }" : UDT<"Pair": Item 1> + #25 90-124 "new Pair { First = 5, Second = 6 }" : UDT<"Pair": Item 1> + #30 109-110 "5" : Int + #33 121-122 "6" : Int + "##]], + ); +} + +#[test] +fn struct_cons_ty_not_struct() { + check( + indoc! {" + namespace A { + function Foo() : Int { new Int { First = 5, Second = 6 } } + } + "}, + "", + &expect![[r#" + #6 30-32 "()" : Unit + #10 39-76 "{ new Int { First = 5, Second = 6 } }" : ? + #12 41-74 "new Int { First = 5, Second = 6 }" : ? + #17 59-60 "5" : ? + #20 71-72 "6" : ? + Error(Type(Error(MissingClassStruct("Int", Span { lo: 45, hi: 48 })))) + "#]], + ); +} + +#[test] +fn struct_cons_ident_not_struct() { + check( + indoc! {" + namespace A { + function Foo() : Int { + let q = 3; + new q { First = 5, Second = 6 } + } + } + "}, + "", + &expect![[r#" + #6 30-32 "()" : Unit + #10 39-105 "{\n let q = 3;\n new q { First = 5, Second = 6 }\n }" : ? + #12 53-54 "q" : Int + #14 57-58 "3" : Int + #16 68-99 "new q { First = 5, Second = 6 }" : ? + #21 84-85 "5" : ? + #24 96-97 "6" : ? + Error(Resolve(NotFound("q", Span { lo: 72, hi: 73 }))) + "#]], + ); +} + +#[test] +fn struct_cons_call_not_struct() { + check( + indoc! {" + namespace A { + struct Pair { First : Int, Second : Int } + function Bar() : Pair { new Pair { First = 1, Second = 2 } } + function Foo() : Pair { new Bar { First = 5, Second = 6 } } + } + "}, + "", + &expect![[r#" + #19 76-78 "()" : Unit + #23 86-124 "{ new Pair { First = 1, Second = 2 } }" : UDT<"Pair": Item 1> + #25 88-122 "new Pair { First = 1, Second = 2 }" : UDT<"Pair": Item 1> + #30 107-108 "1" : Int + #33 119-120 "2" : Int + #37 141-143 "()" : Unit + #41 151-188 "{ new Bar { First = 5, Second = 6 } }" : ? + #43 153-186 "new Bar { First = 5, Second = 6 }" : ? + #48 171-172 "5" : ? + #51 183-184 "6" : ? + Error(Resolve(NotFound("Bar", Span { lo: 157, hi: 160 }))) + "#]], + ); +} + #[test] fn newtype_does_not_match_base_ty() { check( diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs index bcff00e40d..13a4160af2 100644 --- a/compiler/qsc_hir/src/hir.rs +++ b/compiler/qsc_hir/src/hir.rs @@ -650,6 +650,8 @@ pub enum ExprKind { Repeat(Block, Box, Option), /// A return: `return a`. Return(Box), + /// A struct constructor. + Struct(Res, Option>, Box<[Box]>), /// A string. String(Vec), /// Update array index: `a w/ b <- c`. @@ -699,6 +701,7 @@ impl Display for ExprKind { ExprKind::Range(start, step, end) => display_range(indent, start, step, end)?, ExprKind::Repeat(repeat, until, fixup) => display_repeat(indent, repeat, until, fixup)?, ExprKind::Return(e) => write!(indent, "Return: {e}")?, + ExprKind::Struct(name, copy, fields) => display_struct(indent, name, copy, fields)?, ExprKind::String(components) => display_string(indent, components)?, ExprKind::UpdateIndex(expr1, expr2, expr3) => { display_update_index(indent, expr1, expr2, expr3)?; @@ -916,6 +919,27 @@ fn display_repeat( Ok(()) } +fn display_struct( + mut indent: Indented, + name: &Res, + copy: &Option>, + fields: &[Box], +) -> fmt::Result { + write!(indent, "Struct ({name}):")?; + if copy.is_none() && fields.is_empty() { + write!(indent, " ")?; + return Ok(()); + } + indent = set_indentation(indent, 1); + if let Some(copy) = copy { + write!(indent, "\nCopy: {copy}")?; + } + for field in fields { + write!(indent, "\n{field}")?; + } + Ok(()) +} + fn display_string(mut indent: Indented, components: &[StringComponent]) -> fmt::Result { write!(indent, "String:")?; indent = set_indentation(indent, 1); @@ -1001,6 +1025,29 @@ fn display_while(mut indent: Indented, cond: &Expr, block: &Block) -> Ok(()) } +/// A field assignment in a struct constructor expression. +#[derive(Clone, Debug, PartialEq)] +pub struct FieldAssign { + /// The node ID. + pub id: NodeId, + /// The span. + pub span: Span, + /// The field to assign. + pub field: Field, + /// The value to assign to the field. + pub value: Box, +} + +impl Display for FieldAssign { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "FieldsAssign {} {}: ({}) {}", + self.id, self.span, self.field, self.value + ) + } +} + /// A string component. #[derive(Clone, Debug, PartialEq)] pub enum StringComponent { diff --git a/compiler/qsc_hir/src/mut_visit.rs b/compiler/qsc_hir/src/mut_visit.rs index 9a07f2bf4a..c016b73d6a 100644 --- a/compiler/qsc_hir/src/mut_visit.rs +++ b/compiler/qsc_hir/src/mut_visit.rs @@ -2,8 +2,8 @@ // Licensed under the MIT License. use crate::hir::{ - Block, CallableDecl, Expr, ExprKind, Ident, Item, ItemKind, Package, Pat, PatKind, QubitInit, - QubitInitKind, SpecBody, SpecDecl, Stmt, StmtKind, StringComponent, + Block, CallableDecl, Expr, ExprKind, FieldAssign, Ident, Item, ItemKind, Package, Pat, PatKind, + QubitInit, QubitInitKind, SpecBody, SpecDecl, Stmt, StmtKind, StringComponent, }; use qsc_data_structures::span::Span; @@ -36,6 +36,10 @@ pub trait MutVisitor: Sized { walk_expr(self, expr); } + fn visit_field_assign(&mut self, assign: &mut FieldAssign) { + walk_field_assign(self, assign); + } + fn visit_pat(&mut self, pat: &mut Pat) { walk_pat(self, pat); } @@ -184,6 +188,10 @@ pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) { vis.visit_expr(until); fixup.iter_mut().for_each(|f| vis.visit_block(f)); } + ExprKind::Struct(_, copy, fields) => { + copy.iter_mut().for_each(|c| vis.visit_expr(c)); + fields.iter_mut().for_each(|f| vis.visit_field_assign(f)); + } ExprKind::String(components) => { for component in components { match component { @@ -210,6 +218,11 @@ pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) { } } +pub fn walk_field_assign(vis: &mut impl MutVisitor, assign: &mut FieldAssign) { + vis.visit_span(&mut assign.span); + vis.visit_expr(&mut assign.value); +} + pub fn walk_pat(vis: &mut impl MutVisitor, pat: &mut Pat) { vis.visit_span(&mut pat.span); diff --git a/compiler/qsc_hir/src/ty.rs b/compiler/qsc_hir/src/ty.rs index d5001695ce..30a6a983f3 100644 --- a/compiler/qsc_hir/src/ty.rs +++ b/compiler/qsc_hir/src/ty.rs @@ -522,7 +522,7 @@ impl Display for FunctorSetValue { } } -/// A user-defined type. +/// The item for a user-defined type. #[derive(Clone, Debug, PartialEq)] pub struct Udt { /// The span. @@ -637,6 +637,26 @@ impl Udt { pub fn field_ty_by_name(&self, name: &str) -> Option<&Ty> { self.find_field_by_name(name).map(|field| &field.ty) } + + /// Returns true if the udt satisfies the conditions for a struct. + /// Conditions for a struct are that the udt is a tuple with all its top-level fields named. + /// Otherwise, returns false. + #[must_use] + pub fn is_struct(&self) -> bool { + match &self.definition.kind { + UdtDefKind::Field(_) => false, + UdtDefKind::Tuple(fields) => fields.iter().all(|field| match &field.kind { + UdtDefKind::Field(field) => { + if let (Some(name), Some(_)) = (&field.name, &field.name_span) { + !name.is_empty() + } else { + false + } + } + UdtDefKind::Tuple(_) => false, + }), + } + } } impl Display for Udt { diff --git a/compiler/qsc_hir/src/visit.rs b/compiler/qsc_hir/src/visit.rs index 01f3679e66..1a47312677 100644 --- a/compiler/qsc_hir/src/visit.rs +++ b/compiler/qsc_hir/src/visit.rs @@ -2,8 +2,8 @@ // Licensed under the MIT License. use crate::hir::{ - Block, CallableDecl, Expr, ExprKind, Ident, Idents, Item, ItemKind, Package, Pat, PatKind, - QubitInit, QubitInitKind, SpecBody, SpecDecl, Stmt, StmtKind, StringComponent, + Block, CallableDecl, Expr, ExprKind, FieldAssign, Ident, Idents, Item, ItemKind, Package, Pat, + PatKind, QubitInit, QubitInitKind, SpecBody, SpecDecl, Stmt, StmtKind, StringComponent, }; pub trait Visitor<'a>: Sized { @@ -35,6 +35,10 @@ pub trait Visitor<'a>: Sized { walk_expr(self, expr); } + fn visit_field_assign(&mut self, assign: &'a FieldAssign) { + walk_field_assign(self, assign); + } + fn visit_pat(&mut self, pat: &'a Pat) { walk_pat(self, pat); } @@ -165,6 +169,10 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) { vis.visit_expr(until); fixup.iter().for_each(|f| vis.visit_block(f)); } + ExprKind::Struct(_, copy, fields) => { + copy.iter().for_each(|c| vis.visit_expr(c)); + fields.iter().for_each(|f| vis.visit_field_assign(f)); + } ExprKind::String(components) => { for component in components { match component { @@ -191,6 +199,10 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) { } } +pub fn walk_field_assign<'a>(vis: &mut impl Visitor<'a>, assign: &'a FieldAssign) { + vis.visit_expr(&assign.value); +} + pub fn walk_pat<'a>(vis: &mut impl Visitor<'a>, pat: &'a Pat) { match &pat.kind { PatKind::Bind(name) => vis.visit_ident(name), diff --git a/compiler/qsc_lowerer/src/lib.rs b/compiler/qsc_lowerer/src/lib.rs index 3ac3af198e..4b9639cd57 100644 --- a/compiler/qsc_lowerer/src/lib.rs +++ b/compiler/qsc_lowerer/src/lib.rs @@ -602,6 +602,23 @@ impl Lowerer { self.exec_graph.push(self.ret_node); fir::ExprKind::Return(expr) } + hir::ExprKind::Struct(name, copy, fields) => { + let res = self.lower_res(name); + let copy = copy.as_ref().map(|c| { + let id = self.lower_expr(c); + self.exec_graph.push(ExecGraphNode::Store); + id + }); + let fields = fields + .iter() + .map(|f| { + let f = self.lower_field_assign(f); + self.exec_graph.push(ExecGraphNode::Store); + f + }) + .collect(); + fir::ExprKind::Struct(res, copy, fields) + } hir::ExprKind::Tuple(items) => fir::ExprKind::Tuple( items .iter() @@ -697,6 +714,15 @@ impl Lowerer { id } + fn lower_field_assign(&mut self, field_assign: &hir::FieldAssign) -> fir::FieldAssign { + fir::FieldAssign { + id: self.lower_id(field_assign.id), + span: field_assign.span, + field: lower_field(&field_assign.field), + value: self.lower_expr(&field_assign.value), + } + } + fn lower_string_component(&mut self, component: &hir::StringComponent) -> fir::StringComponent { match component { hir::StringComponent::Expr(expr) => { diff --git a/compiler/qsc_parse/src/expr.rs b/compiler/qsc_parse/src/expr.rs index 6624e0e9d4..9ad1eb13f7 100644 --- a/compiler/qsc_parse/src/expr.rs +++ b/compiler/qsc_parse/src/expr.rs @@ -13,15 +13,15 @@ use crate::{ ClosedBinOp, Delim, InterpolatedEnding, InterpolatedStart, Radix, StringToken, Token, TokenKind, }, - prim::{ident, opt, pat, path, seq, shorten, token}, + prim::{ident, opt, pat, path, recovering_token, seq, shorten, token}, scan::ParserContext, stmt, Error, ErrorKind, Result, }; use num_bigint::BigInt; use num_traits::Num; use qsc_ast::ast::{ - self, BinOp, CallableKind, Expr, ExprKind, Functor, Lit, NodeId, Pat, PatKind, Pauli, - StringComponent, TernOp, UnOp, + self, BinOp, CallableKind, Expr, ExprKind, FieldAssign, Functor, Lit, NodeId, Pat, PatKind, + Pauli, StringComponent, TernOp, UnOp, }; use qsc_data_structures::span::Span; use std::{result, str::FromStr}; @@ -198,6 +198,8 @@ fn expr_base(s: &mut ParserContext) -> Result> { token(s, TokenKind::Keyword(Keyword::Apply))?; let inner = stmt::parse_block(s)?; Ok(Box::new(ExprKind::Conjugate(outer, inner))) + } else if token(s, TokenKind::Keyword(Keyword::New)).is_ok() { + parse_struct(s) } else if let Some(a) = opt(s, expr_array)? { Ok(a) } else if let Some(b) = opt(s, stmt::parse_block)? { @@ -221,6 +223,39 @@ fn expr_base(s: &mut ParserContext) -> Result> { })) } +fn parse_struct(s: &mut ParserContext) -> Result> { + let name = path(s)?; + token(s, TokenKind::Open(Delim::Brace))?; + let copy: Option> = opt(s, |s| { + token(s, TokenKind::DotDotDot)?; + expr(s) + })?; + let mut fields = vec![]; + if copy.is_none() || copy.is_some() && token(s, TokenKind::Comma).is_ok() { + (fields, _) = seq(s, parse_field_assign)?; + } + recovering_token(s, TokenKind::Close(Delim::Brace)); + + Ok(Box::new(ExprKind::Struct( + name, + copy, + fields.into_boxed_slice(), + ))) +} + +fn parse_field_assign(s: &mut ParserContext) -> Result> { + let lo = s.peek().span.lo; + let field = ident(s)?; + token(s, TokenKind::Eq)?; + let value = expr(s)?; + Ok(Box::new(FieldAssign { + id: NodeId::default(), + span: s.span(lo), + field, + value, + })) +} + fn expr_if(s: &mut ParserContext) -> Result> { let cond = expr(s)?; let body = stmt::parse_block(s)?; diff --git a/compiler/qsc_parse/src/expr/tests.rs b/compiler/qsc_parse/src/expr/tests.rs index d336303963..aa69751fd8 100644 --- a/compiler/qsc_parse/src/expr/tests.rs +++ b/compiler/qsc_parse/src/expr/tests.rs @@ -1677,6 +1677,60 @@ fn call_op_unit() { ); } +#[test] +fn struct_cons_empty() { + check( + expr, + "new Foo { }", + &expect![[ + r#"Expr _id_ [0-11]: Struct (Path _id_ [4-7] (Ident _id_ [4-7] "Foo")): "# + ]], + ); +} + +#[test] +fn struct_cons() { + check( + expr, + "new Foo { field1 = 3, field2 = 6, field3 = { 2 + 15 } }", + &expect![[r#" + Expr _id_ [0-55]: Struct (Path _id_ [4-7] (Ident _id_ [4-7] "Foo")): + FieldsAssign _id_ [10-20]: (Ident _id_ [10-16] "field1") Expr _id_ [19-20]: Lit: Int(3) + FieldsAssign _id_ [22-32]: (Ident _id_ [22-28] "field2") Expr _id_ [31-32]: Lit: Int(6) + FieldsAssign _id_ [34-53]: (Ident _id_ [34-40] "field3") Expr _id_ [43-53]: Expr Block: Block _id_ [43-53]: + Stmt _id_ [45-51]: Expr: Expr _id_ [45-51]: BinOp (Add): + Expr _id_ [45-46]: Lit: Int(2) + Expr _id_ [49-51]: Lit: Int(15)"#]], + ); +} + +#[test] +fn struct_copy_cons() { + check( + expr, + "new Foo { ...foo, field1 = 3, field3 = { 2 + 15 } }", + &expect![[r#" + Expr _id_ [0-51]: Struct (Path _id_ [4-7] (Ident _id_ [4-7] "Foo")): + Copy: Expr _id_ [13-16]: Path: Path _id_ [13-16] (Ident _id_ [13-16] "foo") + FieldsAssign _id_ [18-28]: (Ident _id_ [18-24] "field1") Expr _id_ [27-28]: Lit: Int(3) + FieldsAssign _id_ [30-49]: (Ident _id_ [30-36] "field3") Expr _id_ [39-49]: Expr Block: Block _id_ [39-49]: + Stmt _id_ [41-47]: Expr: Expr _id_ [41-47]: BinOp (Add): + Expr _id_ [41-42]: Lit: Int(2) + Expr _id_ [45-47]: Lit: Int(15)"#]], + ); +} + +#[test] +fn struct_copy_cons_empty() { + check( + expr, + "new Foo { ...foo }", + &expect![[r#" + Expr _id_ [0-18]: Struct (Path _id_ [4-7] (Ident _id_ [4-7] "Foo")): + Copy: Expr _id_ [13-16]: Path: Path _id_ [13-16] (Ident _id_ [13-16] "foo")"#]], + ); +} + #[test] fn call_op_one() { check( diff --git a/compiler/qsc_parse/src/item.rs b/compiler/qsc_parse/src/item.rs index 358ddc594a..1cab7d6ec9 100644 --- a/compiler/qsc_parse/src/item.rs +++ b/compiler/qsc_parse/src/item.rs @@ -24,8 +24,8 @@ use crate::{ ErrorKind, }; use qsc_ast::ast::{ - Attr, Block, CallableBody, CallableDecl, CallableKind, Ident, Idents, Item, ItemKind, - Namespace, NodeId, Pat, PatKind, Path, Spec, SpecBody, SpecDecl, SpecGen, StmtKind, + Attr, Block, CallableBody, CallableDecl, CallableKind, FieldDef, Ident, Idents, Item, ItemKind, + Namespace, NodeId, Pat, PatKind, Path, Spec, SpecBody, SpecDecl, SpecGen, StmtKind, StructDecl, TopLevelNode, Ty, TyDef, TyDefKind, TyKind, Visibility, VisibilityKind, }; use qsc_data_structures::language_features::LanguageFeatures; @@ -40,6 +40,8 @@ pub(super) fn parse(s: &mut ParserContext) -> Result> { open } else if let Some(ty) = opt(s, parse_newtype)? { ty + } else if let Some(strct) = opt(s, parse_struct)? { + strct } else if let Some(callable) = opt(s, parse_callable_decl)? { Box::new(ItemKind::Callable(callable)) } else if visibility.is_some() { @@ -76,6 +78,7 @@ fn parse_many(s: &mut ParserContext) -> Result>> { TokenKind::Keyword(Keyword::Internal), TokenKind::Keyword(Keyword::Open), TokenKind::Keyword(Keyword::Newtype), + TokenKind::Keyword(Keyword::Struct), TokenKind::Keyword(Keyword::Operation), TokenKind::Keyword(Keyword::Function), ]; @@ -323,6 +326,34 @@ fn parse_newtype(s: &mut ParserContext) -> Result> { Ok(Box::new(ItemKind::Ty(name, def))) } +fn parse_struct(s: &mut ParserContext) -> Result> { + let lo = s.peek().span.lo; + token(s, TokenKind::Keyword(Keyword::Struct))?; + let name = ident(s)?; + token(s, TokenKind::Open(Delim::Brace))?; + let (fields, _) = seq(s, |s| { + let lo = s.peek().span.lo; + let name = ident(s)?; + token(s, TokenKind::Colon)?; + let field_ty = ty(s)?; + Ok(Box::new(FieldDef { + id: NodeId::default(), + span: s.span(lo), + name, + ty: Box::new(field_ty), + })) + })?; + recovering_token(s, TokenKind::Close(Delim::Brace)); + let decl = StructDecl { + id: NodeId::default(), + span: s.span(lo), + name, + fields: fields.into_boxed_slice(), + }; + + Ok(Box::new(ItemKind::Struct(Box::new(decl)))) +} + fn try_tydef_as_ty(tydef: &TyDef) -> Option { match tydef.kind.as_ref() { TyDefKind::Field(Some(_), _) | TyDefKind::Err => None, diff --git a/compiler/qsc_parse/src/item/tests.rs b/compiler/qsc_parse/src/item/tests.rs index 40b84a6dd7..140518478e 100644 --- a/compiler/qsc_parse/src/item/tests.rs +++ b/compiler/qsc_parse/src/item/tests.rs @@ -136,6 +136,54 @@ fn open_alias() { ); } +#[test] +fn struct_decl_empty() { + check( + parse, + "struct Foo { }", + &expect![[r#" + Item _id_ [0-14]: + Struct _id_ [0-14] (Ident _id_ [7-10] "Foo"): "#]], + ); +} + +#[test] +fn struct_decl() { + check( + parse, + "struct Foo { field : Int }", + &expect![[r#" + Item _id_ [0-26]: + Struct _id_ [0-26] (Ident _id_ [7-10] "Foo"): + FieldDef _id_ [13-24] (Ident _id_ [13-18] "field"): Type _id_ [21-24]: Path: Path _id_ [21-24] (Ident _id_ [21-24] "Int")"#]], + ); +} + +#[test] +fn struct_decl_no_fields() { + check( + parse, + "struct Foo { }", + &expect![[r#" + Item _id_ [0-14]: + Struct _id_ [0-14] (Ident _id_ [7-10] "Foo"): "#]], + ); +} + +#[test] +fn struct_decl_multiple_fields() { + check( + parse, + "struct Foo { x : Int, y : Double, z : String }", + &expect![[r#" + Item _id_ [0-46]: + Struct _id_ [0-46] (Ident _id_ [7-10] "Foo"): + FieldDef _id_ [13-20] (Ident _id_ [13-14] "x"): Type _id_ [17-20]: Path: Path _id_ [17-20] (Ident _id_ [17-20] "Int") + FieldDef _id_ [22-32] (Ident _id_ [22-23] "y"): Type _id_ [26-32]: Path: Path _id_ [26-32] (Ident _id_ [26-32] "Double") + FieldDef _id_ [34-44] (Ident _id_ [34-35] "z"): Type _id_ [38-44]: Path: Path _id_ [38-44] (Ident _id_ [38-44] "String")"#]], + ); +} + #[test] fn ty_decl() { check( diff --git a/compiler/qsc_parse/src/keyword.rs b/compiler/qsc_parse/src/keyword.rs index 2a808e7806..708d868595 100644 --- a/compiler/qsc_parse/src/keyword.rs +++ b/compiler/qsc_parse/src/keyword.rs @@ -39,6 +39,7 @@ pub enum Keyword { Mutable, Namespace, Newtype, + New, Not, One, Open, @@ -52,6 +53,7 @@ pub enum Keyword { Return, Slf, Set, + Struct, True, Underscore, Until, @@ -94,6 +96,7 @@ impl Keyword { Self::Mutable => "mutable", Self::Namespace => "namespace", Self::Newtype => "newtype", + Self::New => "new", Self::Not => "not", Self::One => "One", Self::Open => "open", @@ -107,6 +110,7 @@ impl Keyword { Self::Return => "return", Self::Slf => "self", Self::Set => "set", + Self::Struct => "struct", Self::True => "true", Self::Underscore => "_", Self::Until => "until", @@ -179,6 +183,10 @@ impl FromStr for Keyword { "until" => Ok(Self::Until), "repeat" => Ok(Self::Repeat), "fixup" => Ok(Self::Fixup), + // The next two are new keywords and their + // usage has yet to be measured. + "new" => Ok(Self::New), + "struct" => Ok(Self::Struct), // The next three were not found or measured // in the standard library for priority order. "PauliY" => Ok(Self::PauliY), diff --git a/compiler/qsc_parse/src/prim.rs b/compiler/qsc_parse/src/prim.rs index bdf7ac37a6..c7118bd8f9 100644 --- a/compiler/qsc_parse/src/prim.rs +++ b/compiler/qsc_parse/src/prim.rs @@ -145,6 +145,10 @@ pub(super) fn pat(s: &mut ParserContext) -> Result> { })) } +/// Optionally parse with the given parser. +/// Returns Ok(Some(value)) if the parser succeeded, +/// Ok(None) if the parser failed on the first token, +/// Err(error) if the parser failed after consuming some tokens. pub(super) fn opt(s: &mut ParserContext, mut p: impl Parser) -> Result> { let offset = s.peek().span.lo; match p(s) { diff --git a/compiler/qsc_partial_eval/src/lib.rs b/compiler/qsc_partial_eval/src/lib.rs index 94ea26aa32..a41d144964 100644 --- a/compiler/qsc_partial_eval/src/lib.rs +++ b/compiler/qsc_partial_eval/src/lib.rs @@ -969,6 +969,10 @@ impl<'a> PartialEvaluator<'a> { expr_package_span, )), ExprKind::Return(expr_id) => self.eval_expr_return(*expr_id), + ExprKind::Struct(..) => Err(Error::Unexpected( + "instruction generation for struct constructor expressions is invalid".to_string(), + expr_package_span, + )), ExprKind::String(_) => Err(Error::Unexpected( "dynamic strings are invalid".to_string(), expr_package_span, diff --git a/compiler/qsc_passes/src/logic_sep.rs b/compiler/qsc_passes/src/logic_sep.rs index 66c26292fc..8852f229b3 100644 --- a/compiler/qsc_passes/src/logic_sep.rs +++ b/compiler/qsc_passes/src/logic_sep.rs @@ -186,6 +186,7 @@ impl SepCheck { | ExprKind::Index(..) | ExprKind::Lit(..) | ExprKind::Range(..) + | ExprKind::Struct(..) | ExprKind::String(..) | ExprKind::UpdateIndex(..) | ExprKind::Tuple(..) diff --git a/compiler/qsc_rca/src/core.rs b/compiler/qsc_rca/src/core.rs index 8a71a46de6..007a899714 100644 --- a/compiler/qsc_rca/src/core.rs +++ b/compiler/qsc_rca/src/core.rs @@ -16,9 +16,9 @@ use qsc_fir::{ extensions::InputParam, fir::{ BinOp, Block, BlockId, CallableDecl, CallableImpl, CallableKind, Expr, ExprId, ExprKind, - Global, Ident, Item, ItemKind, LocalVarId, Mutability, Package, PackageId, PackageLookup, - PackageStore, PackageStoreLookup, Pat, PatId, PatKind, Res, SpecDecl, SpecImpl, Stmt, - StmtId, StmtKind, StoreExprId, StoreItemId, StorePatId, StringComponent, + FieldAssign, Global, Ident, Item, ItemKind, LocalVarId, Mutability, Package, PackageId, + PackageLookup, PackageStore, PackageStoreLookup, Pat, PatId, PatKind, Res, SpecDecl, + SpecImpl, Stmt, StmtId, StmtKind, StoreExprId, StoreItemId, StorePatId, StringComponent, }, ty::{Arrow, FunctorSetValue, Prim, Ty}, visit::Visitor, @@ -862,6 +862,52 @@ impl<'a> Analyzer<'a> { compute_kind } + fn analyze_expr_struct( + &mut self, + copy: Option, + fields: &[FieldAssign], + expr_type: &Ty, + ) -> ComputeKind { + let default_value_kind = ValueKind::Element(RuntimeKind::Static); + let mut compute_kind = ComputeKind::Classical; + let mut has_dynamic_sub_exprs = false; + if let Some(copy_expr_id) = copy { + // Visit the copy expression to determine its compute kind. + self.visit_expr(copy_expr_id); + let application_instance = self.get_current_application_instance(); + let expr_compute_kind = *application_instance.get_expr_compute_kind(copy_expr_id); + compute_kind = + compute_kind.aggregate_runtime_features(expr_compute_kind, default_value_kind); + has_dynamic_sub_exprs |= expr_compute_kind.is_dynamic(); + } + + // Visit the fields to determine their compute kind. + for expr_id in fields.iter().map(|field| field.value) { + self.visit_expr(expr_id); + let application_instance = self.get_current_application_instance(); + let expr_compute_kind = *application_instance.get_expr_compute_kind(expr_id); + compute_kind = + compute_kind.aggregate_runtime_features(expr_compute_kind, default_value_kind); + has_dynamic_sub_exprs |= expr_compute_kind.is_dynamic(); + } + + // If any of the sub-expressions are dynamic, then the struct expression is dynamic as well. + if has_dynamic_sub_exprs { + compute_kind.aggregate_value_kind(ValueKind::Element(RuntimeKind::Dynamic)); + } + + // If the constructor is dynamic, aggregate the corresponding runtime features depending on its type. + if let ComputeKind::Quantum(quantum_properties) = &mut compute_kind { + quantum_properties.runtime_features |= + derive_runtime_features_for_value_kind_associated_to_type( + quantum_properties.value_kind, + expr_type, + ); + } + + compute_kind + } + fn analyze_expr_string(&mut self, components: &Vec) -> ComputeKind { // Visit the string components to determine their compute kind, aggregate its runtime features and track whether // any of them is dynamic to construct the compute kind of the string expression itself. @@ -1776,6 +1822,7 @@ impl<'a> Visitor<'a> for Analyzer<'a> { .push((expr_id, *value_expr_id)); compute_kind } + ExprKind::Struct(_, copy, fields) => self.analyze_expr_struct(*copy, fields, &expr.ty), ExprKind::String(components) => self.analyze_expr_string(components), ExprKind::Tuple(exprs) => self.analyze_expr_tuple(exprs), ExprKind::UnOp(_, operand_expr_id) => self.analyze_expr_un_op(*operand_expr_id), @@ -1798,7 +1845,7 @@ impl<'a> Visitor<'a> for Analyzer<'a> { // the final compute kind for the expression. if let ComputeKind::Quantum(quantum_properties) = &mut compute_kind { // Since the value kind does not handle all type structures (e.g. it does not handle the structure of a - // tuple type), there could be a mistmatch between the expected value kind variant for the expression's type + // tuple type), there could be a mismatch between the expected value kind variant for the expression's type // and the value kind that we got. // We fix this mismatch here. let mut value_kind = ValueKind::new_static_from_type(&expr.ty); @@ -1808,7 +1855,7 @@ impl<'a> Visitor<'a> for Analyzer<'a> { quantum_properties.value_kind = value_kind; } - // Finally, insert the expresion's compute kind in the application instance. + // Finally, insert the expression's compute kind in the application instance. let application_instance = self.get_current_application_instance_mut(); application_instance.insert_expr_compute_kind(expr_id, compute_kind); } diff --git a/compiler/qsc_rca/tests/structs.rs b/compiler/qsc_rca/tests/structs.rs new file mode 100644 index 0000000000..c21fdd32ee --- /dev/null +++ b/compiler/qsc_rca/tests/structs.rs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +pub mod test_utils; + +use expect_test::expect; +use test_utils::{check_last_statement_compute_properties, CompilationContext}; + +#[test] +fn check_rca_for_struct_constructor_with_classical_values() { + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + open Microsoft.Quantum.Math; + new Complex { Real = 0.0, Imag = 0.0 }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_last_statement_compute_properties( + package_store_compute_properties, + &expect![ + r#" + ApplicationsGeneratorSet: + inherent: Classical + dynamic_param_applications: "# + ], + ); +} + +#[test] +fn check_rca_for_struct_constructor_with_a_dynamic_value() { + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + open Microsoft.Quantum.Math; + use q = Qubit(); + let r = M(q) == Zero ? 0.0 | 1.0; + new Complex { Real = r, Imag = 0.0 }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_last_statement_compute_properties( + package_store_compute_properties, + &expect![ + r#" + ApplicationsGeneratorSet: + inherent: Quantum: QuantumProperties: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | UseOfDynamicDouble | UseOfDynamicUdt) + value_kind: Element(Dynamic) + dynamic_param_applications: "# + ], + ); +} + +#[test] +fn check_rca_for_struct_copy_constructor_with_classical_value() { + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + open Microsoft.Quantum.Math; + let c = new Complex { Real = 0.0, Imag = 0.0 }; + new Complex { ...c, Real = 1.0 }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_last_statement_compute_properties( + package_store_compute_properties, + &expect![ + r#" + ApplicationsGeneratorSet: + inherent: Classical + dynamic_param_applications: "# + ], + ); +} + +#[test] +fn check_rca_for_struct_copy_constructor_with_dynamic_value() { + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + open Microsoft.Quantum.Math; + let c = new Complex { Real = 0.0, Imag = 0.0 }; + use q = Qubit(); + let i = M(q) == Zero ? 0.0 | 1.0; + new Complex { ...c, Imag = i }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_last_statement_compute_properties( + package_store_compute_properties, + &expect![ + r#" + ApplicationsGeneratorSet: + inherent: Quantum: QuantumProperties: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | UseOfDynamicDouble | UseOfDynamicUdt) + value_kind: Element(Dynamic) + dynamic_param_applications: "# + ], + ); +} + +#[test] +fn check_rca_for_struct_dynamic_constructor_overwritten_with_classic_value() { + let mut compilation_context = CompilationContext::default(); + compilation_context.update( + r#" + open Microsoft.Quantum.Math; + use q = Qubit(); + let i = M(q) == Zero ? 0.0 | 1.0; + let c = new Complex { Real = 0.0, Imag = i }; + new Complex { ...c, Imag = 0.0 }"#, + ); + let package_store_compute_properties = compilation_context.get_compute_properties(); + check_last_statement_compute_properties( + package_store_compute_properties, + &expect![ + r#" + ApplicationsGeneratorSet: + inherent: Quantum: QuantumProperties: + runtime_features: RuntimeFeatureFlags(UseOfDynamicBool | UseOfDynamicDouble | UseOfDynamicUdt) + value_kind: Element(Dynamic) + dynamic_param_applications: "# + ], + ); +} diff --git a/samples/language/CopyAndUpdateOperator.qs b/samples/language/CopyAndUpdateOperator.qs index 94e053aaac..f12dc9d3b3 100644 --- a/samples/language/CopyAndUpdateOperator.qs +++ b/samples/language/CopyAndUpdateOperator.qs @@ -12,7 +12,7 @@ namespace MyQuantumApp { @EntryPoint() operation Main() : Unit { let array = [10, 11, 12, 13]; - let struct = Pair(20, 21); + let pair = Pair(20, 21); // `w/` followed by the `<-` copies and updates a single element. @@ -28,9 +28,9 @@ namespace MyQuantumApp { w/ 3 <- 200; Message($"Updated array: {new_array}"); - // `new_struct` is a Pair with value `Pair(20, 100)`. - // `struct` is unchanged. - let new_struct = struct w/ second <- 100; - Message($"Updated struct: (first:{new_struct::first}, second:{new_struct::second})"); + // `new_pair` is a Pair with value `Pair(20, 100)`. + // `pair` is unchanged. + let new_pair = pair w/ second <- 100; + Message($"Updated struct: (first:{new_pair::first}, second:{new_pair::second})"); } }