diff --git a/api/cpp/include/slint_models.h b/api/cpp/include/slint_models.h index 8b1da8546a5..b3aba060722 100644 --- a/api/cpp/include/slint_models.h +++ b/api/cpp/include/slint_models.h @@ -47,6 +47,36 @@ long int model_length(const std::shared_ptr &model) } } +template +bool model_any(const std::shared_ptr &model, P predicate) +{ + long int count = model_length(model); + + for (long int i = 0; i < count; ++i) { + auto data = access_array_index(model, i); + if (predicate(data)) { + return true; + } + } + + return false; +} + +template +bool model_all(const std::shared_ptr &model, P predicate) +{ + long int count = model_length(model); + + for (long int i = 0; i < count; ++i) { + auto data = access_array_index(model, i); + if (!predicate(data)) { + return false; + } + } + + return true; +} + } // namespace private_api /// \rst diff --git a/api/node/rust/interpreter/value.rs b/api/node/rust/interpreter/value.rs index 4b2bb0db15e..339d857515c 100644 --- a/api/node/rust/interpreter/value.rs +++ b/api/node/rust/interpreter/value.rs @@ -290,7 +290,8 @@ pub fn to_value(env: &Env, unknown: JsUnknown, typ: &Type) -> Result { | Type::Easing | Type::PathData | Type::LayoutCache - | Type::ElementReference => Err(napi::Error::from_reason("reason")), + | Type::ElementReference + | Type::Predicate => Err(napi::Error::from_reason("reason")), } } diff --git a/internal/compiler/builtin_macros.rs b/internal/compiler/builtin_macros.rs index 3655d4db2ab..8fe8f910960 100644 --- a/internal/compiler/builtin_macros.rs +++ b/internal/compiler/builtin_macros.rs @@ -315,7 +315,8 @@ fn to_debug_string( | Type::ElementReference | Type::LayoutCache | Type::Model - | Type::PathData => { + | Type::PathData + | Type::Predicate => { diag.push_error("Cannot debug this expression".into(), node); Expression::Invalid } diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index bd47c698427..1d37bae4ab4 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -76,6 +76,8 @@ pub enum BuiltinFunction { ColorWithAlpha, ImageSize, ArrayLength, + ArrayAny, + ArrayAll, Rgb, Hsv, ColorScheme, @@ -239,6 +241,8 @@ declare_builtin_function_types!( rust_attributes: None, })), ArrayLength: (Type::Model) -> Type::Int32, + ArrayAny: (Type::Model, Type::Predicate) -> Type::Bool, + ArrayAll: (Type::Model, Type::Predicate) -> Type::Bool, Rgb: (Type::Int32, Type::Int32, Type::Int32, Type::Float32) -> Type::Color, Hsv: (Type::Float32, Type::Float32, Type::Float32, Type::Float32) -> Type::Color, ColorScheme: () -> Type::Enumeration( @@ -359,6 +363,8 @@ impl BuiltinFunction { BuiltinFunction::StartTimer => false, BuiltinFunction::StopTimer => false, BuiltinFunction::RestartTimer => false, + BuiltinFunction::ArrayAny => true, + BuiltinFunction::ArrayAll => true, } } @@ -435,6 +441,8 @@ impl BuiltinFunction { BuiltinFunction::StartTimer => false, BuiltinFunction::StopTimer => false, BuiltinFunction::RestartTimer => false, + BuiltinFunction::ArrayAny => true, + BuiltinFunction::ArrayAll => true, } } } @@ -750,6 +758,11 @@ pub enum Expression { }, EmptyComponentFactory, + + Predicate { + arg_name: SmolStr, + expression: Box, + }, } impl Expression { @@ -871,6 +884,7 @@ impl Expression { Expression::MinMax { ty, .. } => ty.clone(), Expression::EmptyComponentFactory => Type::ComponentFactory, Expression::DebugHook { expression, .. } => expression.ty(), + Expression::Predicate { .. } => Type::Predicate, } } @@ -966,6 +980,7 @@ impl Expression { } Expression::EmptyComponentFactory => {} Expression::DebugHook { expression, .. } => visitor(expression), + Expression::Predicate { expression, .. } => visitor(expression), } } @@ -1063,6 +1078,7 @@ impl Expression { } Expression::EmptyComponentFactory => {} Expression::DebugHook { expression, .. } => visitor(expression), + Expression::Predicate { expression, .. } => visitor(expression), } } @@ -1145,6 +1161,7 @@ impl Expression { Expression::MinMax { lhs, rhs, .. } => lhs.is_constant() && rhs.is_constant(), Expression::EmptyComponentFactory => true, Expression::DebugHook { .. } => false, + Expression::Predicate { expression, .. } => expression.is_constant(), } } @@ -1378,6 +1395,7 @@ impl Expression { Expression::EnumerationValue(enumeration.clone().default_value()) } Type::ComponentFactory => Expression::EmptyComponentFactory, + Type::Predicate => Expression::Invalid, } } @@ -1788,5 +1806,6 @@ pub fn pretty_print(f: &mut dyn std::fmt::Write, expression: &Expression) -> std pretty_print(f, expression)?; write!(f, "\"{id}\")") } + Expression::Predicate { .. } => todo!(), } } diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index 62b9954f862..89251932dd4 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -3520,6 +3520,12 @@ fn compile_expression(expr: &llr::Expression, ctx: &EvaluationContext) -> String None => format!("slint::private_api::translate_from_bundle(slint_translation_bundle_{string_index}, {args})"), } }, + Expression::Predicate { arg_name, expression } => { + let arg = ident(arg_name); + let expr = compile_expression(expression, ctx); + + format!("[&](auto {arg}) -> bool {{ return {expr}; }}") + }, } } @@ -4012,6 +4018,12 @@ fn compile_builtin_function_call( panic!("internal error: invalid args to RetartTimer {arguments:?}") } } + BuiltinFunction::ArrayAny => { + format!("slint::private_api::model_any({}, {})", a.next().unwrap(), a.next().unwrap()) + }, + BuiltinFunction::ArrayAll => { + format!("slint::private_api::model_all({}, {})", a.next().unwrap(), a.next().unwrap()) + }, } } diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index d11b3348256..caaa8afb60a 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -2677,6 +2677,13 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream } }, + Expression::Predicate { arg_name, expression } => { + let arg_name = ident(arg_name); + let expression = compile_expression(expression, ctx); + quote! { + |#arg_name| {#expression} + } + }, } } @@ -3296,6 +3303,28 @@ fn compile_builtin_function_call( panic!("internal error: invalid args to RetartTimer {arguments:?}") } } + BuiltinFunction::ArrayAny => { + if let [_, Expression::Predicate { .. }] = arguments { + let arr_expression = a.next().unwrap(); + let predicate_expression = a.next().unwrap(); + quote!(match #arr_expression { x => { + x.iter().any(#predicate_expression) + }}) + } else { + panic!("internal error: invalid args to ArrayAny {arguments:?}") + } + } + BuiltinFunction::ArrayAll => { + if let [_, Expression::Predicate { .. }] = arguments { + let arr_expression = a.next().unwrap(); + let predicate_expression = a.next().unwrap(); + quote!(match #arr_expression { x => { + x.iter().all(#predicate_expression) + }}) + } else { + panic!("internal error: invalid args to ArrayAll {arguments:?}") + } + } } } diff --git a/internal/compiler/langtype.rs b/internal/compiler/langtype.rs index d543531e0d1..994e12d8c08 100644 --- a/internal/compiler/langtype.rs +++ b/internal/compiler/langtype.rs @@ -64,6 +64,7 @@ pub enum Type { /// This is a `SharedArray` LayoutCache, + Predicate, } impl core::cmp::PartialEq for Type { @@ -104,6 +105,7 @@ impl core::cmp::PartialEq for Type { Type::UnitProduct(a) => matches!(other, Type::UnitProduct(b) if a == b), Type::ElementReference => matches!(other, Type::ElementReference), Type::LayoutCache => matches!(other, Type::LayoutCache), + Type::Predicate => matches!(other, Type::Predicate), } } } @@ -178,6 +180,7 @@ impl Display for Type { } Type::ElementReference => write!(f, "element ref"), Type::LayoutCache => write!(f, "layout cache"), + Type::Predicate => write!(f, "predicate"), } } } @@ -314,6 +317,7 @@ impl Type { Type::UnitProduct(_) => None, Type::ElementReference => None, Type::LayoutCache => None, + Type::Predicate => None, } } diff --git a/internal/compiler/llr/expression.rs b/internal/compiler/llr/expression.rs index fd983649355..e73ae052733 100644 --- a/internal/compiler/llr/expression.rs +++ b/internal/compiler/llr/expression.rs @@ -205,6 +205,11 @@ pub enum Expression { /// The `n` value to use for the plural form if it is a plural form plural: Option>, }, + + Predicate { + arg_name: SmolStr, + expression: Box, + }, } impl Expression { @@ -217,7 +222,8 @@ impl Expression { | Type::InferredProperty | Type::InferredCallback | Type::ElementReference - | Type::LayoutCache => return None, + | Type::LayoutCache + | Type::Predicate => return None, Type::Float32 | Type::Duration | Type::Int32 @@ -320,6 +326,7 @@ impl Expression { Self::MinMax { ty, .. } => ty.clone(), Self::EmptyComponentFactory => Type::ComponentFactory, Self::TranslationReference { .. } => Type::String, + Self::Predicate { .. } => Type::Predicate, } } } @@ -409,6 +416,9 @@ macro_rules! visit_impl { $visitor(plural); } } + Expression::Predicate { expression, .. } => { + $visitor(expression); + } } }; } diff --git a/internal/compiler/llr/lower_expression.rs b/internal/compiler/llr/lower_expression.rs index 13408ba4379..58ef6351557 100644 --- a/internal/compiler/llr/lower_expression.rs +++ b/internal/compiler/llr/lower_expression.rs @@ -254,6 +254,10 @@ pub fn lower_expression( }, tree_Expression::EmptyComponentFactory => llr_Expression::EmptyComponentFactory, tree_Expression::DebugHook { expression, .. } => lower_expression(expression, ctx), + tree_Expression::Predicate { arg_name, expression } => llr_Expression::Predicate { + arg_name: arg_name.clone(), + expression: Box::new(lower_expression(expression, ctx)), + }, } } diff --git a/internal/compiler/llr/optim_passes/inline_expressions.rs b/internal/compiler/llr/optim_passes/inline_expressions.rs index b73e477d0b9..ebe547ae188 100644 --- a/internal/compiler/llr/optim_passes/inline_expressions.rs +++ b/internal/compiler/llr/optim_passes/inline_expressions.rs @@ -70,6 +70,7 @@ fn expression_cost(exp: &Expression, ctx: &EvaluationContext) -> isize { Expression::MinMax { .. } => 10, Expression::EmptyComponentFactory => 10, Expression::TranslationReference { .. } => PROPERTY_ACCESS_COST + 2 * ALLOC_COST, + Expression::Predicate { expression, .. } => expression_cost(expression, ctx), }; exp.visit(|e| cost = cost.saturating_add(expression_cost(e, ctx))); @@ -153,6 +154,8 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize { BuiltinFunction::StartTimer => 10, BuiltinFunction::StopTimer => 10, BuiltinFunction::RestartTimer => 10, + BuiltinFunction::ArrayAny => isize::MAX, + BuiltinFunction::ArrayAll => isize::MAX, } } diff --git a/internal/compiler/llr/pretty_print.rs b/internal/compiler/llr/pretty_print.rs index 9b9fab139b3..5c220677593 100644 --- a/internal/compiler/llr/pretty_print.rs +++ b/internal/compiler/llr/pretty_print.rs @@ -368,6 +368,7 @@ impl<'a, T> Display for DisplayExpression<'a, T> { ), } } + Expression::Predicate { .. } => todo!(), } } } diff --git a/internal/compiler/lookup.rs b/internal/compiler/lookup.rs index 2449dd4e92b..a4d4f9471b0 100644 --- a/internal/compiler/lookup.rs +++ b/internal/compiler/lookup.rs @@ -51,6 +51,17 @@ pub struct LookupCtx<'a> { /// A stack of local variable scopes pub local_variables: Vec>, + + /// A stack of predicate argument types + /// This is a hack to infer the type of the predicate argument + pub predicate_argument_types: Vec, + + /// A stack of predicate arguments + /// Theoretically a predicate could include another predicate, so this is a stack + pub predicate_arguments: Vec, + + /// A flag that indicates if predicates are currently allowed (currently only inside a function argument) + pub predicates_allowed: bool, } impl<'a> LookupCtx<'a> { @@ -66,6 +77,9 @@ impl<'a> LookupCtx<'a> { type_loader: None, current_token: None, local_variables: Default::default(), + predicate_arguments: Default::default(), + predicate_argument_types: Default::default(), + predicates_allowed: false, } } @@ -266,6 +280,28 @@ impl LookupObject for ArgumentsLookup { } } +struct PredicateArgumentsLookup; +impl LookupObject for PredicateArgumentsLookup { + fn for_each_entry( + &self, + ctx: &LookupCtx, + f: &mut impl FnMut(&SmolStr, LookupResult) -> Option, + ) -> Option { + // we reverse the types here so that the most recently added predicate argument is the first one for shadowing purposes + // this is done in case someone does `arr.any(x => x.any(x => x > 0))`... why anyone would do this is beyond me though + for (name, ty) in + ctx.predicate_arguments.iter().zip(ctx.predicate_argument_types.iter().rev()) + { + if let Some(r) = + f(name, Expression::ReadLocalVariable { name: name.clone(), ty: ty.clone() }.into()) + { + return Some(r); + } + } + None + } +} + struct SpecialIdLookup; impl LookupObject for SpecialIdLookup { fn for_each_entry( @@ -856,20 +892,23 @@ impl LookupObject for BuiltinNamespaceLookup { pub fn global_lookup() -> impl LookupObject { ( - LocalVariableLookup, + PredicateArgumentsLookup, ( - ArgumentsLookup, + LocalVariableLookup, ( - SpecialIdLookup, + ArgumentsLookup, ( - IdLookup, + SpecialIdLookup, ( - InScopeLookup, + IdLookup, ( - LookupType, + InScopeLookup, ( - BuiltinNamespaceLookup, - (ReturnTypeSpecificLookup, BuiltinFunctionLookup), + LookupType, + ( + BuiltinNamespaceLookup, + (ReturnTypeSpecificLookup, BuiltinFunctionLookup), + ), ), ), ), @@ -1055,15 +1094,25 @@ impl LookupObject for ArrayExpression<'_> { f: &mut impl FnMut(&SmolStr, LookupResult) -> Option, ) -> Option { let member_function = |f: BuiltinFunction| { + LookupResult::Callable(LookupResultCallable::MemberFunction { + base: self.0.clone(), + base_node: ctx.current_token.clone(), // Note that this is not the base_node, but the function's node + member: LookupResultCallable::Callable(Callable::Builtin(f)).into(), + }) + }; + let function_call = |f: BuiltinFunction| { LookupResult::from(Expression::FunctionCall { function: Callable::Builtin(f), source_location: ctx.current_token.as_ref().map(|t| t.to_source_location()), arguments: vec![self.0.clone()], }) }; + None.or_else(|| { - f(&SmolStr::new_static("length"), member_function(BuiltinFunction::ArrayLength)) + f(&SmolStr::new_static("length"), function_call(BuiltinFunction::ArrayLength)) }) + .or_else(|| f(&SmolStr::new_static("any"), member_function(BuiltinFunction::ArrayAny))) + .or_else(|| f(&SmolStr::new_static("all"), member_function(BuiltinFunction::ArrayAll))) } } diff --git a/internal/compiler/parser.rs b/internal/compiler/parser.rs index 95da6211ce3..f4487339816 100644 --- a/internal/compiler/parser.rs +++ b/internal/compiler/parser.rs @@ -374,8 +374,8 @@ declare_syntax! { Expression-> [ ?Expression, ?FunctionCallExpression, ?IndexExpression, ?SelfAssignment, ?ConditionalExpression, ?QualifiedName, ?BinaryExpression, ?Array, ?ObjectLiteral, ?UnaryOpExpression, ?CodeBlock, ?StringTemplate, ?AtImageUrl, ?AtGradient, ?AtTr, - ?MemberAccess ], - /// Concatenate the Expressions to make a string (usually expanded from a template string) + ?MemberAccess, ?Predicate ], + /// Concatenate the Expressions to make a string (usually expended from a template string) StringTemplate -> [*Expression], /// `@image-url("foo.png")` AtImageUrl -> [], @@ -449,6 +449,8 @@ declare_syntax! { EnumValue -> [], /// `@rust-attr(...)` AtRustAttr -> [], + /// `|x| x > 0` + Predicate -> [DeclaredIdentifier, Expression], } } diff --git a/internal/compiler/parser/expressions.rs b/internal/compiler/parser/expressions.rs index 833cd8cb23a..42be62d4cd7 100644 --- a/internal/compiler/parser/expressions.rs +++ b/internal/compiler/parser/expressions.rs @@ -30,6 +30,7 @@ use super::prelude::*; /// array[index] /// {object:42} /// "foo".bar.something().something.xx({a: 1.foo}.a) +/// x => x > 0 /// ``` pub fn parse_expression(p: &mut impl Parser) -> bool { p.peek(); // consume the whitespace so they aren't part of the Expression node @@ -58,7 +59,11 @@ fn parse_expression_helper(p: &mut impl Parser, precedence: OperatorPrecedence) let mut possible_range = false; match p.nth(0).kind() { SyntaxKind::Identifier => { - parse_qualified_name(&mut *p); + if p.nth(1).kind() == SyntaxKind::FatArrow { + parse_predicate(&mut *p); + } else { + parse_qualified_name(&mut *p); + } } SyntaxKind::StringLiteral => { if p.nth(0).as_str().ends_with('{') { @@ -227,6 +232,25 @@ fn parse_expression_helper(p: &mut impl Parser, precedence: OperatorPrecedence) true } +#[cfg_attr(test, parser_test)] +/// ```test +/// x => x > 0 +/// y => y == 42 +/// z => true +/// ``` +fn parse_predicate(p: &mut impl Parser) { + let mut p = p.start_node(SyntaxKind::Predicate); + + { + let mut p = p.start_node(SyntaxKind::DeclaredIdentifier); + p.expect(SyntaxKind::Identifier); + } + + p.expect(SyntaxKind::FatArrow); + + parse_expression(&mut *p); +} + #[cfg_attr(test, parser_test)] /// ```test /// @image-url("/foo/bar.png") diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs index 9ffba3ac12e..84c14aef693 100644 --- a/internal/compiler/passes/resolving.rs +++ b/internal/compiler/passes/resolving.rs @@ -46,6 +46,9 @@ fn resolve_expression( type_loader: Some(type_loader), current_token: None, local_variables: vec![], + predicate_arguments: vec![], + predicate_argument_types: vec![], + predicates_allowed: false, }; let new_expr = match node.kind() { @@ -368,6 +371,7 @@ impl Expression { SyntaxKind::StringTemplate => { Some(Self::from_string_template_node(node.into(), ctx)) } + SyntaxKind::Predicate => Some(Self::from_predicate_node(node.into(), ctx)), _ => None, }, NodeOrToken::Token(token) => match token.kind() { @@ -878,21 +882,62 @@ impl Expression { } return Self::Invalid; }; + + let (function, array_base_ty) = match function { + Some(LookupResult::Callable(function)) => { + let base = match &function { + LookupResultCallable::MemberFunction { base, member, .. } => match **member { + LookupResultCallable::Callable(Callable::Builtin( + BuiltinFunction::ArrayAny | BuiltinFunction::ArrayAll, + )) => match base.ty() { + Type::Array(ty) => { + // only set predicates to be allowed if we are in one of the hardcoded array member functions + ctx.predicates_allowed = true; + Some((*ty).clone()) + } + _ => unreachable!(), // you won't have access to these member functions if the base is not an array + }, + _ => None, + }, + _ => None, + }; + + (function, base) + } + Some(_) => { + // Check sub expressions anyway + sub_expr.for_each(|n| { + Self::from_expression_node(n.clone(), ctx); + }); + ctx.diag.push_error("The expression is not a function".into(), &node); + ctx.predicates_allowed = false; + return Self::Invalid; + } + None => { + // Check sub expressions anyway + sub_expr.for_each(|n| { + Self::from_expression_node(n.clone(), ctx); + }); + assert!(ctx.diag.has_errors()); + return Self::Invalid; + } + }; + + // dirty hack to supply the type of the predicate argument for array member functions, + // we check if we are dealing with an array builtin, then we push the type to the context, + // to be popped at the end of this function after all the predicate's expression is resolved + let mut should_pop_predicate_args = false; + match (ctx.predicates_allowed, array_base_ty) { + (true, Some(ty)) => { + should_pop_predicate_args = true; + ctx.predicate_argument_types.push(ty); + } + _ => (), + } + let sub_expr = sub_expr.map(|n| { (Self::from_expression_node(n.clone(), ctx), Some(NodeOrToken::from((*n).clone()))) }); - let Some(function) = function else { - // Check sub expressions anyway - sub_expr.count(); - assert!(ctx.diag.has_errors()); - return Self::Invalid; - }; - let LookupResult::Callable(function) = function else { - // Check sub expressions anyway - sub_expr.count(); - ctx.diag.push_error("The expression is not a function".into(), &node); - return Self::Invalid; - }; let mut adjust_arg_count = 0; let function = match function { @@ -907,7 +952,7 @@ impl Expression { ); } LookupResultCallable::MemberFunction { member, base, base_node } => { - arguments.push((base, base_node)); + arguments.push((base.clone(), base_node)); adjust_arg_count = 1; match *member { LookupResultCallable::Callable(c) => c, @@ -959,6 +1004,13 @@ impl Expression { } }; + // if we pushed the predicate argument type, we pop it now + if should_pop_predicate_args { + ctx.predicate_argument_types.pop(); + } + + ctx.predicates_allowed = false; + Expression::FunctionCall { function, arguments, source_location: Some(source_location) } } @@ -1253,6 +1305,33 @@ impl Expression { Expression::Array { element_ty, values } } + fn from_predicate_node(node: syntax_nodes::Predicate, ctx: &mut LookupCtx) -> Expression { + if !ctx.predicates_allowed { + ctx.diag.push_error( + "Predicate expressions are not permitted outside of array builtin function arguments".to_string(), + &node, + ); + return Expression::Invalid; + } + + let arg_name = node.DeclaredIdentifier().to_smolstr(); + + ctx.predicate_arguments.push(arg_name.clone()); + let expression = Expression::from_expression_node(node.Expression(), ctx); + let ty = expression.ty(); + if ty != Type::Bool { + ctx.diag.push_error( + format!("Predicate expression must be of type bool, but is {}", ty), + &node.Expression(), + ); + return Expression::Invalid; + } + + ctx.predicate_arguments.pop(); + + Expression::Predicate { arg_name, expression: Box::new(expression) } + } + fn from_string_template_node( node: syntax_nodes::StringTemplate, ctx: &mut LookupCtx, @@ -1691,6 +1770,9 @@ fn resolve_two_way_bindings( type_loader: None, current_token: Some(node.clone().into()), local_variables: vec![], + predicate_arguments: vec![], + predicate_argument_types: vec![], + predicates_allowed: false, }; binding.expression = Expression::Invalid; diff --git a/internal/compiler/tests/syntax/expressions/predicates.slint b/internal/compiler/tests/syntax/expressions/predicates.slint new file mode 100644 index 00000000000..37629fda72d --- /dev/null +++ b/internal/compiler/tests/syntax/expressions/predicates.slint @@ -0,0 +1,37 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +export component Test inherits Window { + property <[int]> ints: [1, 2, 3, 4, 5]; + + if x => x > 0 : Rectangle {} +// ^error{Predicate expressions are not permitted outside of array builtin function arguments} + + init => { + ints.any(1); +// ^error{Cannot convert float to predicate} + ints.all(1); +// ^error{Cannot convert float to predicate} + + ints.any(x => 1); +// ^error{Predicate expression must be of type bool, but is float} + ints.all(x => 1); +// ^error{Predicate expression must be of type bool, but is float} + + ints.any(x => x); +// ^error{Predicate expression must be of type bool, but is int} + ints.all(x => x); +// ^error{Predicate expression must be of type bool, but is int} + + debug(x => x > 0); +// ^error{Predicate expressions are not permitted outside of array builtin function arguments} + + y => y > 0; +// ^error{Predicate expressions are not permitted outside of array builtin function arguments} + } + + TouchArea { + clicked => x => x > 0; +// ^error{Predicate expressions are not permitted outside of array builtin function arguments} + } +} diff --git a/internal/interpreter/api.rs b/internal/interpreter/api.rs index da5d228c1be..99c14cd6bab 100644 --- a/internal/interpreter/api.rs +++ b/internal/interpreter/api.rs @@ -11,6 +11,7 @@ use i_slint_core::model::{Model, ModelExt, ModelRc}; use i_slint_core::window::WindowInner; use i_slint_core::{PathData, SharedVector}; use smol_str::SmolStr; +use std::cell::RefCell; use std::collections::HashMap; use std::future::Future; use std::path::{Path, PathBuf}; @@ -28,6 +29,7 @@ pub use i_slint_core::graphics::{ use i_slint_core::items::*; use crate::dynamic_item_tree::{ErasedItemTreeBox, WindowOptions}; +use crate::eval::EvalLocalContext; /// This enum represents the different public variants of the [`Value`] enum, without /// the contained values. @@ -128,6 +130,8 @@ pub enum Value { #[doc(hidden)] /// Correspond to the `component-factory` type in .slint ComponentFactory(ComponentFactory) = 12, + /// A predicate + Predicate(Rc Value>>) = 13, } impl Value { @@ -173,6 +177,7 @@ impl PartialEq for Value { Value::ComponentFactory(lhs) => { matches!(other, Value::ComponentFactory(rhs) if lhs == rhs) } + Value::Predicate(p) => matches!(other, Value::Predicate(q) if Rc::ptr_eq(p, q)), } } } @@ -197,6 +202,7 @@ impl std::fmt::Debug for Value { Value::EnumerationValue(n, v) => write!(f, "Value::EnumerationValue({n:?}, {v:?})"), Value::LayoutCache(v) => write!(f, "Value::LayoutCache({v:?})"), Value::ComponentFactory(factory) => write!(f, "Value::ComponentFactory({factory:?})"), + Value::Predicate(_) => write!(f, "Value::Predicate(...)"), } } } @@ -239,6 +245,7 @@ declare_value_conversion!(PathData => [PathData]); declare_value_conversion!(EasingCurve => [i_slint_core::animations::EasingCurve]); declare_value_conversion!(LayoutCache => [SharedVector] ); declare_value_conversion!(ComponentFactory => [ComponentFactory] ); +declare_value_conversion!(Predicate => [Rc Value>>]); /// Implement From / TryFrom for Value that convert a `struct` to/from `Value::Struct` macro_rules! declare_value_struct_conversion { diff --git a/internal/interpreter/dynamic_item_tree.rs b/internal/interpreter/dynamic_item_tree.rs index bf4b8e65856..7835435d84b 100644 --- a/internal/interpreter/dynamic_item_tree.rs +++ b/internal/interpreter/dynamic_item_tree.rs @@ -1260,7 +1260,8 @@ pub(crate) fn generate_item_tree<'id>( | Type::Model | Type::PathData | Type::UnitProduct(_) - | Type::ElementReference => panic!("bad type {ty:?}"), + | Type::ElementReference + | Type::Predicate => panic!("bad type {ty:?}"), }) } diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index e2472118b71..348482cfd02 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -22,6 +22,7 @@ use i_slint_core as corelib; use i_slint_core::input::FocusReason; use i_slint_core::items::ItemRc; use smol_str::SmolStr; +use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; @@ -408,6 +409,16 @@ pub fn eval_expression(expression: &Expression, local_context: &mut EvalLocalCon } Expression::EmptyComponentFactory => Value::ComponentFactory(Default::default()), Expression::DebugHook { expression, .. } => eval_expression(expression, local_context), + Expression::Predicate { arg_name, expression } => { + let arg_name = arg_name.clone(); + let expression = expression.clone(); + let predicate = Rc::new(RefCell::new(move |local_context: &mut EvalLocalContext<'_, '_>, value: &Value| { + local_context.local_variables.insert(arg_name.clone(), value.clone()); + eval_expression(&expression, local_context) + })); + + Value::Predicate(predicate) + }, } } @@ -1374,6 +1385,37 @@ fn call_builtin_function( panic!("internal error: argument to RestartTimer must be an element") } } + BuiltinFunction::ArrayAny => { + let model: ModelRc = + eval_expression(&arguments[0], local_context).try_into().unwrap(); + let predicate: Rc Value>> = + eval_expression(&arguments[1], local_context).try_into().unwrap(); + let mut predicate = predicate.borrow_mut(); + + for x in model.iter() { + if predicate(local_context, &x).try_into().unwrap() { + return Value::Bool(true); + } + } + + Value::Bool(false) + } + BuiltinFunction::ArrayAll => { + let model: ModelRc = + eval_expression(&arguments[0], local_context).try_into().unwrap(); + let predicate: Rc Value>> = + eval_expression(&arguments[1], local_context).try_into().unwrap(); + let mut predicate = predicate.borrow_mut(); + + for x in model.iter() { + let result: bool = predicate(local_context, &x).try_into().unwrap(); + if !result { + return Value::Bool(false); + } + } + + Value::Bool(true) + } } } @@ -1664,7 +1706,8 @@ fn check_value_type(value: &Value, ty: &Type) -> bool { | Type::InferredCallback | Type::Callback { .. } | Type::Function { .. } - | Type::ElementReference => panic!("not valid property type"), + | Type::ElementReference + | Type::Predicate => panic!("not valid property type"), Type::Float32 => matches!(value, Value::Number(_)), Type::Int32 => matches!(value, Value::Number(_)), Type::String => matches!(value, Value::String(_)), @@ -1992,7 +2035,8 @@ pub fn default_value_for_type(ty: &Type) -> Value { Type::InferredProperty | Type::InferredCallback | Type::ElementReference - | Type::Function { .. } => { + | Type::Function { .. } + | Type::Predicate => { panic!("There can't be such property") } } diff --git a/tests/cases/models/array_predicates.slint b/tests/cases/models/array_predicates.slint new file mode 100644 index 00000000000..f08c2b92b3c --- /dev/null +++ b/tests/cases/models/array_predicates.slint @@ -0,0 +1,89 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +struct Person { + name: string, + age: int, +} + +export component TestCase { + property <[int]> ints: [1, 2, 3, 4, 5]; + property <[string]> strings: ["hello", "world", "foo", "bar"]; + property <[Person]> people: [ + { name: "Alice", age: 30 }, + { name: "Bob", age: 25 }, + { name: "Charlie", age: 35 }, + { name: "Diana", age: 28 } + ]; + property <[[Person]]> groups: [ + [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }], + [{ name: "Charlie", age: 35 }, { name: "Diana", age: 28 }], + [{ name: "Evan", age: 22 }, { name: "Fiona", age: 27 }] + ]; + + out property test: { + // test that predicate args shadow outer variables regardless of type + let x = "hello world"; + let s = 42; + let p = 42px; + + let ints = self.ints.all(x + + => x > 0 + ) && !self.ints.all(x => x > 1 + ) && !self.ints.any(x => x == 0 + ) && self.ints.any(x => x == 5 + ) && self.ints.any(x => x < 5 + ); + + let strings = self.strings.all(s => s.character-count > 0 + ) && !self.strings.all(s => s.is-empty + ) && self.strings.any(s => s == "hello" + ) && !self.strings.any(s => s == "test" + ) && !self.strings.any(s => s.is-empty + ); + + let people = self.people.all(p => p.age > 20 + ) && !self.people.all(p => p.age < 30 + ) && self.people.any(p => p.name == "Alice" + ) && !self.people.any(p => p.name == "Evan" + ) && self.people.any(p => p.age < 30 + ); + + let groups = self.groups.all(p => p.all(p + => p.age > 20)) && !self.groups.all(p => p.all(p + => p.age < 30)) && self.groups.all(p => p.any(p + => p.age >= 25)) && !self.groups.any(p => p.all(p + => p.name == "Alice")) && self.groups.any(p => p.all(p + => p.name.character-count > 4)) && self.groups.any(p => p.any(p + => p.name.character-count == 7)); + + let shadowing = x == "hello world" && s == 42 && p == 42px; + + ints && strings && people && groups && shadowing + } + + // with the groups here we are also testing arg shadowing when nested predicate args have the same name +} + +/* +```cpp +auto handle = TestCase::create(); +const TestCase &instance = *handle; + +assert(instance.get_test()); +``` + + +```rust +let instance = TestCase::new().unwrap(); + +assert!(instance.get_test()); +``` + +```js +var instance = new slint.TestCase(); + +assert(instance.test); +``` +*/ diff --git a/tools/lsp/fmt/fmt.rs b/tools/lsp/fmt/fmt.rs index 2e9e4d4ed33..cb7bdc7bdf5 100644 --- a/tools/lsp/fmt/fmt.rs +++ b/tools/lsp/fmt/fmt.rs @@ -162,7 +162,9 @@ fn format_node( SyntaxKind::MemberAccess => { return format_member_access(node, writer, state); } - + SyntaxKind::Predicate => { + return format_predicate(node, writer, state); + } _ => (), } @@ -1320,6 +1322,26 @@ fn format_member_access( Ok(()) } +fn format_predicate( + node: &SyntaxNode, + writer: &mut impl TokenWriter, + state: &mut FormatState, +) -> Result<(), std::io::Error> { + for s in node.children_with_tokens() { + state.skip_all_whitespace = true; + match s.kind() { + SyntaxKind::FatArrow => { + state.insert_whitespace(" "); + fold(s, writer, state)?; + state.insert_whitespace(" "); + } + _ => fold(s, writer, state)?, + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -2189,6 +2211,34 @@ export component MainWindow2 inherits Rectangle { let bar: int = 42; } } +"#, + ); + } + + #[test] + fn predicate() { + assert_formatting( + "component X { property <[int]> arr: [1, 2, 3, 4, 5]; function foo() { arr.any(x\n => x == 1 ); } }", + r#"component X { + property <[int]> arr: [1, 2, 3, 4, 5]; + function foo() { + arr.any(x => x == 1); + } +} +"#, + ); + } + + #[test] + fn nested_predicate() { + assert_formatting( + "component X { property <[[int]]> arr: [[1, 2, 3, 4, 5]]; function foo() { arr.any(x => x.all(y => y == 7 ) ); } }", + r#"component X { + property <[[int]]> arr: [[1, 2, 3, 4, 5]]; + function foo() { + arr.any(x => x.all(y => y == 7)); + } +} "#, ); }