From a2dd09a2f33aa2148dd5b753238d4b9d00ce844f Mon Sep 17 00:00:00 2001 From: John-John Tedro Date: Sat, 27 Apr 2024 08:10:43 +0200 Subject: [PATCH] Support constants in patterns (fixes #696) --- crates/rune/src/hir/lowering.rs | 436 +++++++++++++++++++----------- crates/rune/src/tests/patterns.rs | 68 +++++ 2 files changed, 342 insertions(+), 162 deletions(-) diff --git a/crates/rune/src/hir/lowering.rs b/crates/rune/src/hir/lowering.rs index cef532db0..1fd22cd9a 100644 --- a/crates/rune/src/hir/lowering.rs +++ b/crates/rune/src/hir/lowering.rs @@ -14,6 +14,7 @@ use crate::hir; use crate::indexing; use crate::parse::Resolve; use crate::query::{self, Build, BuildEntry, GenericsParameters, Named, Query}; +use crate::runtime::ConstValue; use crate::runtime::{Type, TypeCheck}; use crate::SourceId; @@ -710,6 +711,97 @@ pub(crate) fn expr<'hir>( }) } +/// Construct a pattern from a constant value. +#[instrument(span = span)] +pub(crate) fn pat_const_value<'hir>( + cx: &mut Ctxt<'hir, '_, '_>, + const_value: &ConstValue, + span: &dyn Spanned, +) -> compile::Result> { + alloc_with!(cx, span); + + let kind = 'kind: { + let lit = match *const_value { + ConstValue::Bool(b) => hir::Lit::Bool(b), + ConstValue::Byte(b) => hir::Lit::Byte(b), + ConstValue::Char(ch) => hir::Lit::Char(ch), + ConstValue::String(ref string) => hir::Lit::Str(alloc_str!(string.as_ref())), + ConstValue::Bytes(ref bytes) => hir::Lit::ByteStr(alloc_bytes!(bytes.as_ref())), + ConstValue::Integer(integer) => hir::Lit::Integer(integer), + ConstValue::Vec(ref items) => { + let items = iter!(items.iter(), items.len(), |value| pat_const_value( + cx, value, span + )?); + + break 'kind hir::PatKind::Sequence(alloc!(hir::PatSequence { + kind: hir::PatSequenceKind::Anonymous { + type_check: TypeCheck::Vec, + count: items.len(), + is_open: false, + }, + items, + })); + } + ConstValue::EmptyTuple => { + break 'kind hir::PatKind::Sequence(alloc!(hir::PatSequence { + kind: hir::PatSequenceKind::Anonymous { + type_check: TypeCheck::EmptyTuple, + count: 0, + is_open: false, + }, + items: &[], + })); + } + ConstValue::Tuple(ref items) => { + let items = iter!(items.iter(), items.len(), |value| pat_const_value( + cx, value, span + )?); + + break 'kind hir::PatKind::Sequence(alloc!(hir::PatSequence { + kind: hir::PatSequenceKind::Anonymous { + type_check: TypeCheck::Vec, + count: items.len(), + is_open: false, + }, + items, + })); + } + ConstValue::Object(ref fields) => { + let bindings = iter!(fields.iter(), fields.len(), |(key, value)| { + let pat = alloc!(pat_const_value(cx, value, span)?); + + hir::Binding::Binding(span.span(), alloc_str!(key.as_ref()), pat) + }); + + break 'kind hir::PatKind::Object(alloc!(hir::PatObject { + kind: hir::PatSequenceKind::Anonymous { + type_check: TypeCheck::Object, + count: bindings.len(), + is_open: false, + }, + bindings, + })); + } + _ => { + return Err(compile::Error::msg( + span, + "Unsupported constant value in pattern", + )); + } + }; + + hir::PatKind::Lit(alloc!(hir::Expr { + span: span.span(), + kind: hir::ExprKind::Lit(lit), + })) + }; + + Ok(hir::Pat { + span: span.span(), + kind, + }) +} + #[instrument(span = ast)] pub(crate) fn expr_if<'hir>( cx: &mut Ctxt<'hir, '_, '_>, @@ -1086,198 +1178,218 @@ fn pat<'hir>(cx: &mut Ctxt<'hir, '_, '_>, ast: &ast::Pat) -> compile::Result hir::PatKind::Ignore, - ast::Pat::Path(ast) => { - let named = cx.q.convert_path(&ast.path)?; - let parameters = generics_parameters(cx, &named)?; + let kind = { + match ast { + ast::Pat::Ignore(..) => hir::PatKind::Ignore, + ast::Pat::Path(ast) => { + let named = cx.q.convert_path(&ast.path)?; + let parameters = generics_parameters(cx, &named)?; + + let path = 'path: { + if let Some(meta) = cx.try_lookup_meta(&ast, named.item, ¶meters)? { + match meta.kind { + meta::Kind::Const => { + let Some(const_value) = cx.q.get_const_value(meta.hash) else { + return Err(compile::Error::msg( + ast, + try_format!("Missing constant for hash {}", meta.hash), + )); + }; + + let const_value = const_value.try_clone().with_span(ast)?; + return pat_const_value(cx, &const_value, ast); + } + _ => { + if let Some((0, kind)) = tuple_match_for(cx, &meta) { + break 'path hir::PatPathKind::Kind(alloc!(kind)); + } + } + } + }; - let kind = 'ok: { - if let Some(meta) = cx.try_lookup_meta(&ast, named.item, ¶meters)? { - if let Some((0, kind)) = tuple_match_for(cx, &meta) { - break 'ok hir::PatPathKind::Kind(alloc!(kind)); + if let Some(ident) = ast.path.try_as_ident() { + let name = alloc_str!(ident.resolve(resolve_context!(cx.q))?); + cx.scopes.define(hir::Name::Str(name), ast)?; + break 'path hir::PatPathKind::Ident(name); } - } - if let Some(ident) = ast.path.try_as_ident() { - let name = alloc_str!(ident.resolve(resolve_context!(cx.q))?); - cx.scopes.define(hir::Name::Str(name), ast)?; - break 'ok hir::PatPathKind::Ident(name); - } + return Err(compile::Error::new(ast, ErrorKind::UnsupportedBinding)); + }; - return Err(compile::Error::new(ast, ErrorKind::UnsupportedBinding)); - }; + hir::PatKind::Path(alloc!(path)) + } + ast::Pat::Lit(ast) => hir::PatKind::Lit(alloc!(expr(cx, &ast.expr)?)), + ast::Pat::Vec(ast) => { + let (is_open, count) = pat_items_count(ast.items.as_slice())?; + let items = iter!( + ast.items.iter().filter_map(filter), + ast.items.len(), + |ast| pat(cx, ast)? + ); + + hir::PatKind::Sequence(alloc!(hir::PatSequence { + kind: hir::PatSequenceKind::Anonymous { + type_check: TypeCheck::Vec, + count, + is_open + }, + items, + })) + } + ast::Pat::Tuple(ast) => { + let (is_open, count) = pat_items_count(ast.items.as_slice())?; + let items = iter!( + ast.items.iter().filter_map(filter), + ast.items.len(), + |ast| pat(cx, ast)? + ); + + let kind = if let Some(path) = &ast.path { + let named = cx.q.convert_path(path)?; + let parameters = generics_parameters(cx, &named)?; + let meta = cx.lookup_meta(path, named.item, parameters)?; - hir::PatKind::Path(alloc!(kind)) - } - ast::Pat::Lit(ast) => hir::PatKind::Lit(alloc!(expr(cx, &ast.expr)?)), - ast::Pat::Vec(ast) => { - let (is_open, count) = pat_items_count(ast.items.as_slice())?; - let items = iter!( - ast.items.iter().filter_map(filter), - ast.items.len(), - |ast| pat(cx, ast)? - ); - - hir::PatKind::Sequence(alloc!(hir::PatSequence { - kind: hir::PatSequenceKind::Anonymous { - type_check: TypeCheck::Vec, - count, - is_open - }, - items, - })) - } - ast::Pat::Tuple(ast) => { - let (is_open, count) = pat_items_count(ast.items.as_slice())?; - let items = iter!( - ast.items.iter().filter_map(filter), - ast.items.len(), - |ast| pat(cx, ast)? - ); - - let kind = if let Some(path) = &ast.path { - let named = cx.q.convert_path(path)?; - let parameters = generics_parameters(cx, &named)?; - let meta = cx.lookup_meta(path, named.item, parameters)?; + // Treat the current meta as a tuple and get the number of arguments it + // should receive and the type check that applies to it. + let Some((args, kind)) = tuple_match_for(cx, &meta) else { + return Err(compile::Error::expected_meta( + path, + meta.info(cx.q.pool)?, + "type that can be used in a tuple pattern", + )); + }; - // Treat the current meta as a tuple and get the number of arguments it - // should receive and the type check that applies to it. - let Some((args, kind)) = tuple_match_for(cx, &meta) else { - return Err(compile::Error::expected_meta( - path, - meta.info(cx.q.pool)?, - "type that can be used in a tuple pattern", - )); + if !(args == count || count < args && is_open) { + return Err(compile::Error::new( + path, + ErrorKind::UnsupportedArgumentCount { + expected: args, + actual: count, + }, + )); + } + + kind + } else { + hir::PatSequenceKind::Anonymous { + type_check: TypeCheck::Tuple, + count, + is_open, + } }; - if !(args == count || count < args && is_open) { - return Err(compile::Error::new( - path, - ErrorKind::UnsupportedArgumentCount { - expected: args, - actual: count, - }, - )); - } + hir::PatKind::Sequence(alloc!(hir::PatSequence { kind, items })) + } + ast::Pat::Object(ast) => { + let (is_open, count) = pat_items_count(ast.items.as_slice())?; - kind - } else { - hir::PatSequenceKind::Anonymous { - type_check: TypeCheck::Tuple, - count, - is_open, - } - }; + let mut keys_dup = HashMap::new(); - hir::PatKind::Sequence(alloc!(hir::PatSequence { kind, items })) - } - ast::Pat::Object(ast) => { - let (is_open, count) = pat_items_count(ast.items.as_slice())?; - - let mut keys_dup = HashMap::new(); - - let bindings = iter!(ast.items.iter().take(count), |(pat, _)| { - let (key, binding) = match pat { - ast::Pat::Binding(binding) => { - let (span, key) = object_key(cx, &binding.key)?; - ( - key, - hir::Binding::Binding( - span.span(), + let bindings = iter!(ast.items.iter().take(count), |(pat, _)| { + let (key, binding) = match pat { + ast::Pat::Binding(binding) => { + let (span, key) = object_key(cx, &binding.key)?; + ( key, - alloc!(self::pat(cx, &binding.pat)?), - ), - ) - } - ast::Pat::Path(path) => { - let Some(ident) = path.path.try_as_ident() else { + hir::Binding::Binding( + span.span(), + key, + alloc!(self::pat(cx, &binding.pat)?), + ), + ) + } + ast::Pat::Path(path) => { + let Some(ident) = path.path.try_as_ident() else { + return Err(compile::Error::new( + path, + ErrorKind::UnsupportedPatternExpr, + )); + }; + + let key = alloc_str!(ident.resolve(resolve_context!(cx.q))?); + cx.scopes.define(hir::Name::Str(key), ident)?; + (key, hir::Binding::Ident(path.span(), key)) + } + _ => { return Err(compile::Error::new( - path, + pat, ErrorKind::UnsupportedPatternExpr, )); - }; + } + }; - let key = alloc_str!(ident.resolve(resolve_context!(cx.q))?); - cx.scopes.define(hir::Name::Str(key), ident)?; - (key, hir::Binding::Ident(path.span(), key)) - } - _ => { - return Err(compile::Error::new(pat, ErrorKind::UnsupportedPatternExpr)); + if let Some(_existing) = keys_dup.try_insert(key, pat)? { + return Err(compile::Error::new( + pat, + ErrorKind::DuplicateObjectKey { + #[cfg(feature = "emit")] + existing: _existing.span(), + #[cfg(feature = "emit")] + object: pat.span(), + }, + )); } - }; - if let Some(_existing) = keys_dup.try_insert(key, pat)? { - return Err(compile::Error::new( - pat, - ErrorKind::DuplicateObjectKey { - #[cfg(feature = "emit")] - existing: _existing.span(), - #[cfg(feature = "emit")] - object: pat.span(), - }, - )); - } + binding + }); - binding - }); + let kind = match &ast.ident { + ast::ObjectIdent::Named(path) => { + let named = cx.q.convert_path(path)?; + let parameters = generics_parameters(cx, &named)?; + let meta = cx.lookup_meta(path, named.item, parameters)?; - let kind = match &ast.ident { - ast::ObjectIdent::Named(path) => { - let named = cx.q.convert_path(path)?; - let parameters = generics_parameters(cx, &named)?; - let meta = cx.lookup_meta(path, named.item, parameters)?; + let Some((mut fields, kind)) = + struct_match_for(cx, &meta, is_open && count == 0)? + else { + return Err(compile::Error::expected_meta( + path, + meta.info(cx.q.pool)?, + "type that can be used in a struct pattern", + )); + }; - let Some((mut fields, kind)) = - struct_match_for(cx, &meta, is_open && count == 0)? - else { - return Err(compile::Error::expected_meta( - path, - meta.info(cx.q.pool)?, - "type that can be used in a struct pattern", - )); - }; + for binding in bindings.iter() { + if !fields.remove(binding.key()) { + return Err(compile::Error::new( + ast, + ErrorKind::LitObjectNotField { + field: binding.key().try_into()?, + item: cx.q.pool.item(meta.item_meta.item).try_to_owned()?, + }, + )); + } + } + + if !is_open && !fields.is_empty() { + let mut fields = fields.into_iter().try_collect::>()?; + + fields.sort(); - for binding in bindings.iter() { - if !fields.remove(binding.key()) { return Err(compile::Error::new( ast, - ErrorKind::LitObjectNotField { - field: binding.key().try_into()?, + ErrorKind::PatternMissingFields { item: cx.q.pool.item(meta.item_meta.item).try_to_owned()?, + #[cfg(feature = "emit")] + fields, }, )); } - } - - if !is_open && !fields.is_empty() { - let mut fields = fields.into_iter().try_collect::>()?; - - fields.sort(); - return Err(compile::Error::new( - ast, - ErrorKind::PatternMissingFields { - item: cx.q.pool.item(meta.item_meta.item).try_to_owned()?, - #[cfg(feature = "emit")] - fields, - }, - )); + kind } + ast::ObjectIdent::Anonymous(..) => hir::PatSequenceKind::Anonymous { + type_check: TypeCheck::Object, + count, + is_open, + }, + }; - kind - } - ast::ObjectIdent::Anonymous(..) => hir::PatSequenceKind::Anonymous { - type_check: TypeCheck::Object, - count, - is_open, - }, - }; - - hir::PatKind::Object(alloc!(hir::PatObject { kind, bindings })) - } - _ => { - return Err(compile::Error::new(ast, ErrorKind::UnsupportedPatternExpr)); + hir::PatKind::Object(alloc!(hir::PatObject { kind, bindings })) + } + _ => { + return Err(compile::Error::new(ast, ErrorKind::UnsupportedPatternExpr)); + } } }; diff --git a/crates/rune/src/tests/patterns.rs b/crates/rune/src/tests/patterns.rs index a93f6f0ab..3ed73c0ca 100644 --- a/crates/rune/src/tests/patterns.rs +++ b/crates/rune/src/tests/patterns.rs @@ -1,5 +1,6 @@ prelude!(); +use ErrorKind::*; use VmErrorKind::*; #[test] @@ -259,3 +260,70 @@ fn test_bad_pattern() { } ); } + +#[test] +fn test_const_in_pattern() { + macro_rules! test_case_s { + ($pat1:expr, $pat2:expr) => { + let string = format! { + r#" + const PAT1 = {pat1}; + const PAT2 = {pat2}; + const PAT3 = {{ + if true {{ + {pat2} + }} else {{ + {pat1} + }} + }}; + + pub fn main() {{ + let value = {pat1}; + let a = match value {{ PAT => 1, _ => 5 }}; + let b = match value {{ PAT2 => 2, _ => 6 }}; + let c = match value {{ PAT3 => 3, _ => 7 }}; + let d = match value {{ PAT4 => 4, _ => 8 }}; + (a, b, c, d) + }} + "#, + pat1 = $pat1, + pat2 = $pat2, + }; + + let tuple: (i64, i64, i64, i64) = rune_s!(string.as_str()); + assert_eq!(tuple, (1, 6, 7, 4)); + }; + } + + macro_rules! test_case { + ($pat1:expr, $pat2:expr) => { + test_case_s!(stringify!($pat1), stringify!($pat2)) + }; + } + + test_case!(true, false); + test_case!('a', 'b'); + test_case!(b'a', b'b'); + test_case!(10, 20); + test_case!("abc", "def"); + test_case!(b"abc", b"def"); + test_case!((1, 2), (3, 4)); + test_case!([1, 2], [3, 4]); + test_case!([1, (3, 4)], [3, (3, 4)]); + test_case_s!("#{foo: 12}", "#{bar: 12}"); + test_case_s!("#{}", "#{bar: 12}"); + + assert_errors! { + r#" + const PAT = 3.1415; + + pub fn main() { + let value = 3.1415; + match value { PAT => true, _ => false } + } + "#, + span!(112, 115), Custom { error } => { + assert_eq!(error.to_string(), "Unsupported constant value in pattern"); + } + } +}