// Tests for ASG IR // // Copyright (C) 2014-2023 Ryan Specialty, LLC. // // This file is part of TAME. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! These are tested as if they are another API directly atop of the ASG, //! since that is how they are used. use super::{super::Ident, expr::test::collect_subexprs, *}; use crate::{ asg::{Expr, ExprOp, IdentKind, Source}, parse::{ParseError, Parsed, Parser}, span::dummy::*, }; use std::assert_matches::assert_matches; type Sut = AirAggregate; #[test] fn ident_decl() { let id = SPair("foo".into(), S1); let kind = IdentKind::Tpl; let src = Source { src: Some("test/decl".into()), ..Default::default() }; let toks = vec![Air::IdentDecl(id, kind.clone(), src.clone())].into_iter(); let mut sut = Sut::parse(toks); assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); let asg = sut.finalize().unwrap().into_context(); let ident_node = asg.lookup(id).expect("identifier was not added to graph"); let ident = asg.get(ident_node).unwrap(); assert_eq!( Ok(ident), Ident::declare(id) .resolve(S1, kind.clone(), src.clone()) .as_ref(), ); // Re-instantiate the parser and test an error by attempting to // redeclare the same identifier. let bad_toks = vec![Air::IdentDecl(SPair(id.symbol(), S2), kind, src)].into_iter(); let mut sut = Sut::parse_with_context(bad_toks, asg); assert_matches!( sut.next(), Some(Err(ParseError::StateError(AsgError::IdentTransition(_)))), ); } #[test] fn ident_extern_decl() { let id = SPair("foo".into(), S1); let kind = IdentKind::Tpl; let src = Source { src: Some("test/decl-extern".into()), ..Default::default() }; let toks = vec![Air::IdentExternDecl(id, kind.clone(), src.clone())].into_iter(); let mut sut = Sut::parse(toks); assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); let asg = sut.finalize().unwrap().into_context(); let ident_node = asg.lookup(id).expect("identifier was not added to graph"); let ident = asg.get(ident_node).unwrap(); assert_eq!( Ok(ident), Ident::declare(id).extern_(S1, kind, src.clone()).as_ref(), ); // Re-instantiate the parser and test an error by attempting to // redeclare with a different kind. let different_kind = IdentKind::Meta; let bad_toks = vec![Air::IdentExternDecl( SPair(id.symbol(), S2), different_kind, src, )] .into_iter(); let mut sut = Sut::parse_with_context(bad_toks, asg); assert_matches!( sut.next(), Some(Err(ParseError::StateError(AsgError::IdentTransition(_)))), ); } #[test] fn ident_dep() { let id = SPair("foo".into(), S1); let dep = SPair("dep".into(), S2); let toks = vec![Air::IdentDep(id, dep)].into_iter(); let mut sut = Sut::parse(toks); assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); let asg = sut.finalize().unwrap().into_context(); let ident_node = asg.lookup(id).expect("identifier was not added to graph"); let dep_node = asg.lookup(dep).expect("dep was not added to graph"); assert!(asg.has_dep(ident_node, dep_node)); } #[test] fn ident_fragment() { let id = SPair("frag".into(), S1); let kind = IdentKind::Tpl; let src = Source { src: Some("test/frag".into()), ..Default::default() }; let frag = "fragment text".into(); let toks = vec![ // Identifier must be declared before it can be given a // fragment. Air::IdentDecl(id, kind.clone(), src.clone()), Air::IdentFragment(id, frag), ] .into_iter(); let mut sut = Sut::parse(toks); assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // IdentDecl assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // IdentFragment let asg = sut.finalize().unwrap().into_context(); let ident_node = asg.lookup(id).expect("identifier was not added to graph"); let ident = asg.get(ident_node).unwrap(); assert_eq!( Ok(ident), Ident::declare(id) .resolve(S1, kind.clone(), src.clone()) .and_then(|resolved| resolved.set_fragment(frag)) .as_ref(), ); // Re-instantiate the parser and test an error by attempting to // re-set the fragment. let bad_toks = vec![Air::IdentFragment(id, frag)].into_iter(); let mut sut = Sut::parse_with_context(bad_toks, asg); assert_matches!( sut.next(), Some(Err(ParseError::StateError(AsgError::IdentTransition(_)))), ); } // Adding a root before the identifier exists should add a // `Ident::Missing`. #[test] fn ident_root_missing() { let id = SPair("toroot".into(), S1); let toks = vec![Air::IdentRoot(id)].into_iter(); let mut sut = Sut::parse(toks); assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); let asg = sut.finalize().unwrap().into_context(); let ident_node = asg .lookup(id) .expect("identifier was not added to the graph"); let ident = asg.get(ident_node).unwrap(); // The identifier did not previously exist, // and so a missing node is created as a placeholder. assert_eq!(&Ident::Missing(id), ident); // And that missing identifier should be rooted. assert!(asg.is_rooted(ident_node)); } #[test] fn ident_root_existing() { let id = SPair("toroot".into(), S1); let kind = IdentKind::Tpl; let src = Source { src: Some("test/root-existing".into()), ..Default::default() }; // Ensure that it won't auto-root based on the kind, // otherwise we won't be testing the right thing. assert!(!kind.is_auto_root()); let toks = vec![ Air::IdentDecl(id, kind.clone(), src.clone()), Air::IdentRoot(SPair(id.symbol(), S2)), ] .into_iter(); let mut sut = Sut::parse(toks); assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // IdentDecl assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // IdentRoot let asg = sut.finalize().unwrap().into_context(); let ident_node = asg .lookup(id) .expect("identifier was not added to the graph"); let ident = asg.get(ident_node).unwrap(); // The previously-declared identifier... assert_eq!( Ok(ident), Ident::declare(id) .resolve(S1, kind.clone(), src.clone()) .as_ref() ); // ...should have been subsequently rooted. assert!(asg.is_rooted(ident_node)); } #[test] fn pkg_is_rooted() { let toks = vec![Air::PkgOpen(S1), Air::PkgClose(S2)]; let mut sut = Sut::parse(toks.into_iter()); assert!(sut.all(|x| x.is_ok())); let asg = sut.finalize().unwrap().into_context(); let oi_root = asg.root(S3); let pkg = oi_root .edges_filtered::(&asg) .next() .expect("missing rooted package") .resolve(&asg); assert_eq!(pkg.span(), S1.merge(S2).unwrap()); } #[test] fn close_pkg_without_open() { let toks = vec![ Air::PkgClose(S1), // RECOVERY: Try again. Air::PkgOpen(S2), Air::PkgClose(S3), ]; assert_eq!( vec![ Err(ParseError::StateError(AsgError::InvalidPkgCloseContext(S1))), // RECOVERY Ok(Parsed::Incomplete), // PkgOpen Ok(Parsed::Incomplete), // PkgClose ], Sut::parse(toks.into_iter()).collect::>(), ); } #[test] fn nested_open_pkg() { let toks = vec![ Air::PkgOpen(S1), Air::PkgOpen(S2), // RECOVERY Air::PkgClose(S3), ]; assert_eq!( vec![ Ok(Parsed::Incomplete), // PkgOpen Err(ParseError::StateError(AsgError::NestedPkgOpen(S2, S1))), // RECOVERY Ok(Parsed::Incomplete), // PkgClose ], Sut::parse(toks.into_iter()).collect::>(), ); } /// Parse using [`Sut`] when the test does not care about the outer package. pub fn parse_as_pkg_body>( toks: I, ) -> Parser + Debug> where ::IntoIter: Debug, { use std::iter; Sut::parse( iter::once(Air::PkgOpen(S1)) .chain(toks.into_iter()) .chain(iter::once(Air::PkgClose(S1))), ) } // A template is defined by the package containing it, // like an expression. #[test] fn tpl_defining_pkg() { let id_tpl = SPair("_tpl_".into(), S3); let toks = vec![ Air::PkgOpen(S1), // This also tests tpl as a transition away from the package header. Air::TplOpen(S2), Air::BindIdent(id_tpl), Air::TplClose(S4), Air::PkgClose(S5), ]; let mut sut = Sut::parse(toks.into_iter()); assert!(sut.all(|x| x.is_ok())); let asg = sut.finalize().unwrap().into_context(); let tpl = asg.expect_ident_obj::(id_tpl); assert_eq!(S2.merge(S4).unwrap(), tpl.span()); let oi_id_tpl = asg.lookup(id_tpl).unwrap(); assert_eq!( S1.merge(S5), oi_id_tpl.src_pkg(&asg).map(|pkg| pkg.resolve(&asg).span()), ); } #[test] fn tpl_after_expr() { let id_expr = SPair("expr".into(), S3); let id_tpl = SPair("_tpl_".into(), S6); #[rustfmt::skip] let toks = vec![ Air::PkgOpen(S1), // This expression is incidental to this test; // it need only parse. Air::ExprOpen(ExprOp::Sum, S2), Air::BindIdent(id_expr), Air::ExprClose(S4), // Open after an expression. Air::TplOpen(S5), Air::BindIdent(id_tpl), Air::TplClose(S7), Air::PkgClose(S8), ]; let mut sut = Sut::parse(toks.into_iter()); assert!(sut.all(|x| x.is_ok())); let asg = sut.finalize().unwrap().into_context(); let tpl = asg.expect_ident_obj::(id_tpl); assert_eq!(S5.merge(S7).unwrap(), tpl.span()); } // Templates within expressions are permitted by NIR at the time of writing // (and so cannot be observed in system tests using TAME's source // language), // but it _is_ permitted by AIR, // to simplify lowering, desugaring, and template expansion. // // This test is a rather important one, // since it ensures that expression context is properly restored // regardless of whether a template is encountered. // This context includes the entire active expression stack. #[test] fn tpl_within_expr() { let id_expr = SPair("expr".into(), S3); let id_tpl = SPair("_tpl_".into(), S7); #[rustfmt::skip] let toks = vec![ Air::PkgOpen(S1), Air::ExprOpen(ExprOp::Sum, S2), Air::BindIdent(id_expr), // Child expression before the template to ensure that the // context is properly restored after template parsing. Air::ExprOpen(ExprOp::Sum, S4), Air::ExprClose(S5), // Template _within_ an expression. // This will not be present in the final expression, // as if it were hoisted out. Air::TplOpen(S6), Air::BindIdent(id_tpl), Air::TplClose(S8), // Child expression _after_ the template for the same reason. Air::ExprOpen(ExprOp::Sum, S9), Air::ExprClose(S10), Air::ExprClose(S11), Air::PkgClose(S12), ]; let mut sut = Sut::parse(toks.into_iter()); assert!(sut.all(|x| x.is_ok())); let asg = sut.finalize().unwrap().into_context(); // The inner template. let tpl = asg.expect_ident_obj::(id_tpl); assert_eq!(S6.merge(S8).unwrap(), tpl.span()); // The expression that was produced on the graph ought to be equivalent // to the expression without the template being present at all // (noting that the spans are of course not adjusted). let oi_expr = asg.expect_ident_oi::(id_expr); let expr = oi_expr.resolve(&asg); assert_eq!(S2.merge(S11).unwrap(), expr.span()); assert_eq!( #[rustfmt::skip] vec![ S4.merge(S5).unwrap(), S9.merge(S10).unwrap(), ], collect_subexprs(&asg, oi_expr) .iter() .map(|(_, expr)| expr.span()) .rev() .collect::>(), ); } pub fn asg_from_toks>(toks: I) -> Asg where I::IntoIter: Debug, { let mut sut = parse_as_pkg_body(toks); assert!(sut.all(|x| x.is_ok())); sut.finalize().unwrap().into_context() }