Math postfix member functions on numbers

Closes #5328
This commit is contained in:
Olivier Goffart 2024-08-09 17:51:18 +02:00
parent 4dd7d96a28
commit 9b71cf1a36
12 changed files with 226 additions and 25 deletions

View file

@ -962,6 +962,12 @@ impl LookupObject for Expression {
Type::Brush | Type::Color => ColorExpression(self).for_each_entry(ctx, f),
Type::Image => ImageExpression(self).for_each_entry(ctx, f),
Type::Array(_) => ArrayExpression(self).for_each_entry(ctx, f),
Type::Float32 | Type::Int32 | Type::Percent => {
NumberExpression(self).for_each_entry(ctx, f)
}
ty if ty.as_unit_product().is_some() => {
NumValueExpression(self).for_each_entry(ctx, f)
}
_ => None,
},
}
@ -981,6 +987,10 @@ impl LookupObject for Expression {
Type::Brush | Type::Color => ColorExpression(self).lookup(ctx, name),
Type::Image => ImageExpression(self).lookup(ctx, name),
Type::Array(_) => ArrayExpression(self).lookup(ctx, name),
Type::Float32 | Type::Int32 | Type::Percent => {
NumberExpression(self).lookup(ctx, name)
}
ty if ty.as_unit_product().is_some() => NumValueExpression(self).lookup(ctx, name),
_ => None,
},
}
@ -1106,3 +1116,77 @@ impl<'a> LookupObject for ArrayExpression<'a> {
None.or_else(|| f("length", member_function(BuiltinFunction::ArrayLength)))
}
}
/// An expression of type int or float
struct NumberExpression<'a>(&'a Expression);
impl<'a> LookupObject for NumberExpression<'a> {
fn for_each_entry<R>(
&self,
ctx: &LookupCtx,
f: &mut impl FnMut(&str, LookupResult) -> Option<R>,
) -> Option<R> {
let member_function = |f: BuiltinFunction| {
LookupResult::from(Expression::MemberFunction {
base: Box::new(self.0.clone()),
base_node: ctx.current_token.clone(), // Note that this is not the base_node, but the function's node
member: Box::new(Expression::BuiltinFunctionReference(
f,
ctx.current_token.as_ref().map(|t| t.to_source_location()),
)),
})
};
None.or_else(|| f("round", member_function(BuiltinFunction::Round)))
.or_else(|| f("ceil", member_function(BuiltinFunction::Ceil)))
.or_else(|| f("floor", member_function(BuiltinFunction::Floor)))
.or_else(|| f("sqrt", member_function(BuiltinFunction::Sqrt)))
.or_else(|| f("asin", member_function(BuiltinFunction::ASin)))
.or_else(|| f("acos", member_function(BuiltinFunction::ACos)))
.or_else(|| f("atan", member_function(BuiltinFunction::ATan)))
.or_else(|| f("log", member_function(BuiltinFunction::Log)))
.or_else(|| f("pow", member_function(BuiltinFunction::Pow)))
.or_else(|| NumValueExpression(self.0).for_each_entry(ctx, f))
}
}
/// An expression of any numerical value with an unit
struct NumValueExpression<'a>(&'a Expression);
impl<'a> LookupObject for NumValueExpression<'a> {
fn for_each_entry<R>(
&self,
ctx: &LookupCtx,
f: &mut impl FnMut(&str, LookupResult) -> Option<R>,
) -> Option<R> {
let member_macro = |f: BuiltinMacroFunction| {
LookupResult::from(Expression::MemberFunction {
base: Box::new(self.0.clone()),
base_node: ctx.current_token.clone(), // Note that this is not the base_node, but the function's node
member: Box::new(Expression::BuiltinMacroReference(f, ctx.current_token.clone())),
})
};
None.or_else(|| f("mod", member_macro(BuiltinMacroFunction::Mod)))
.or_else(|| f("clamp", member_macro(BuiltinMacroFunction::Clamp)))
.or_else(|| f("abs", member_macro(BuiltinMacroFunction::Abs)))
.or_else(|| f("max", member_macro(BuiltinMacroFunction::Max)))
.or_else(|| f("min", member_macro(BuiltinMacroFunction::Min)))
.or_else(|| {
if self.0.ty() != Type::Angle {
return None;
}
let member_function = |f: BuiltinFunction| {
LookupResult::from(Expression::MemberFunction {
base: Box::new(self.0.clone()),
base_node: ctx.current_token.clone(), // Note that this is not the base_node, but the function's node
member: Box::new(Expression::BuiltinFunctionReference(
f,
ctx.current_token.as_ref().map(|t| t.to_source_location()),
)),
})
};
None.or_else(|| f("sin", member_function(BuiltinFunction::Sin)))
.or_else(|| f("cos", member_function(BuiltinFunction::Cos)))
.or_else(|| f("tan", member_function(BuiltinFunction::Tan)))
})
}
}

View file

@ -6,6 +6,7 @@ use std::rc::Rc;
use crate::diagnostics::BuildDiagnostics;
use crate::expression_tree::{BuiltinFunction, Expression};
use crate::object_tree::{visit_all_expressions, Component};
use crate::parser::SyntaxKind;
/// Check the validity of expressions
///
@ -18,9 +19,13 @@ pub fn check_expressions(doc: &crate::object_tree::Document, diag: &mut BuildDia
fn check_expression(component: &Rc<Component>, e: &Expression, diag: &mut BuildDiagnostics) {
match e {
Expression::MemberFunction { .. } => {
// Must already have been be reported.
debug_assert!(diag.has_errors());
Expression::MemberFunction { base_node, .. } => {
if base_node.as_ref().is_some_and(|n| n.kind() == SyntaxKind::QualifiedName) {
// Must already have been be reported in Expression::from_expression_node
debug_assert!(diag.has_errors());
} else {
diag.push_error("Member function must be called".into(), base_node);
}
}
Expression::BuiltinMacroReference(_, node) => {
diag.push_error("Builtin function must be called".into(), node);

View file

@ -908,6 +908,15 @@ impl Expression {
}
Expression::MemberFunction { base, base_node, member } => {
arguments.push((*base, base_node));
if let Expression::BuiltinMacroReference(mac, n) = *member {
arguments.extend(sub_expr);
return crate::builtin_macros::lower_macro(
mac,
n,
arguments.into_iter(),
ctx.diag,
);
}
adjust_arg_count = 1;
member
}

View file

@ -21,4 +21,18 @@ export component SuperSimple {
// ^error{Cannot convert float to length}
property <float> e: clamp(42.0, 23.0, 84.0, 32.0);
// ^error{`clamp` needs three values}
property <float> f: ok1.clamp();
// ^error{`clamp` needs three values}
property <float> g: ok1.clamp(1,2,3);
// ^error{`clamp` needs three values}
property <float> h: ok1.clamp;
// ^error{Member function must be called}
property <float> i: 42.0.clamp;
// ^error{Member function must be called}
}

View file

@ -25,6 +25,11 @@ export component Foo {
property <duration> m7: mod(5, 4ms);
// ^error{Cannot convert float to duration}
property <duration> m8: (5).mod(4ms);
// ^error{Cannot convert float to duration}
property <duration> m9: 5ms.mod(4);
// ^error{Cannot convert float to duration}
property <float> a1: abs();
// ^error{Needs 1 argument}
@ -41,4 +46,18 @@ export component Foo {
property <string> a5: abs(45px);
// ^error{Cannot convert length to string}
property <string> a6: abs;
// ^error{Builtin function must be called}
property <string> a7: (-21).abs;
// ^error{Member function must be called}
property <float> sq1: 1.0.sqrt(1);
// ^error{The callback or function expects 0 arguments, but 1 are provided}
property <float> sq2: 1.0.sqrt;
// ^error{Member function must be called}
// ^^error{Cannot convert function\(float\) -> float to float}
}

View file

@ -60,4 +60,7 @@ export X := Rectangle {
// ^error{Cannot convert float to \[void\]}
// ^^error{Cannot convert void to int}
property <int> to-float: "foobar".to-float;
// ^error{Member function must be called}
// ^^error{Cannot convert function\(string\) -> float to int}
}

View file

@ -6,12 +6,20 @@ export component TestCase {
out property <float> t1: clamp(value, 10.0, 53.0);
out property <float> t2: clamp(value, 43.0, 53.0);
out property <float> t3: clamp(value, 10.0, 41.0);
out property <float> s1: value.clamp(10.0, 53.0);
out property <float> s2: value.clamp(43.0, 53.0);
out property <float> s3: value.clamp(10.0, 41.0);
r := Rectangle {
property <int> max: 42;
property <int> xx: Math.clamp(5, 2, 3) + max;
}
out property <bool> test: root.t1 == 42.0 && root.t2 == 43.0 && root.t3 == 41.0 && r.xx == 42 + 3;
out property <duration> dur: 45ms.clamp(0, 50ms);
out property<bool> test_dur: dur == 5ms.clamp(45ms, 50ms);
out property <bool> test: root.t1 == 42.0 && root.t2 == 43.0 && root.t3 == 41.0 && r.xx == 42 + 3 && root.s1 == 42.0 && root.s2 == 43.0 && root.s3 == 41.0 && test_dur;
}
/*
```cpp

View file

@ -1,11 +1,13 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
TestCase := Rectangle {
property<float> t1: cos(0);
property<float> t2: cos(180deg);
property<float> t3: cos(60deg);
property<float> t4: cos(90deg);
component TestCase inherits Window {
out property<float> t1: cos(0);
out property<float> t2: cos(180deg);
out property<float> t3: cos(60deg);
out property<float> t4: cos(90deg);
out property<bool> test: (0deg.cos() - 1.0).abs() < 0.00001 && 90deg.cos().abs() < 0.000001;
}
/*
```cpp
@ -15,6 +17,7 @@ assert(std::abs(instance.get_t1() - 1.0) < 0.0001);
assert(std::abs(instance.get_t2() + 1.0) < 0.0001);
assert(std::abs(instance.get_t3() - 0.5) < 0.0001);
assert(std::abs(instance.get_t4()) < 0.0001);
assert(instance.get_test());
```
```rust
@ -23,6 +26,7 @@ assert!((instance.get_t1() - 1.0).abs() < 0.0001);
assert!((instance.get_t2() + 1.0).abs() < 0.0001);
assert!((instance.get_t3() - 0.5).abs() < 0.0001);
assert!((instance.get_t4()).abs() < 0.0001);
assert!(instance.get_test());
```
```js
@ -31,5 +35,6 @@ assert(Math.abs(instance.t1 - 1) < Number.EPSILON);
assert(Math.abs(instance.t2 - -1) < Number.EPSILON);
assert(Math.abs(instance.t3 - 0.5) < Number.EPSILON);
assert(Math.abs(instance.t4) < Number.EPSILON);
assert(instance.test);
```
*/

View file

@ -1,18 +1,25 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
export component TestCase {
export component TestCase inherits Window {
in property <float> thousand: 1000;
out property <bool> test_sqrt: sqrt(100) == 10 && Math.sqrt(1) == 1 && sqrt(6.25) == 2.5 && abs(sqrt(thousand) - sqrt(1000)) < 0.00001;
out property <bool> test_sqrt2: 100 .sqrt() == 10 && 1.0.sqrt() == 1 && 6.25.sqrt() == 2.5 && (thousand.sqrt() - (1000).sqrt()).abs() < 0.00001;
out property <bool> test_abs: abs(100.5) == 100.5 && Math.abs(-200.5) == 200.5 && abs(0) == 0 && Math.abs(-thousand) == 1000;
out property <bool> test_abs2: 100.5.abs() == 100.5 && (-200.5).abs() == 200.5 && 0 .abs() == 0 && (-thousand).abs() == 1000;
out property <bool> test_log: log(4,2) == 2 && Math.log(9,3) == 2 && log(64,4) == 3;
out property <bool> test_log2: 4 .log(2) == 2 && (9).log(3) == 2 && 64.0.log(4) == 3;
out property <bool> test_pow: pow(4,2) == 16 && Math.pow(9,3) == 729 && pow(4,3) == 64 && abs(log(pow(thousand, 5), thousand) - 5) < 0.00001;
out property <bool> test_pow2: 4..pow(2) == 16 && 9.0.pow(3) == 729 && (4).pow(3) == 64 && (thousand.pow(5).log(thousand) - 5).abs() < 0.00001;
out property <int> test_div_zero: 42 / 0;
out property <bool> test: test_sqrt && test_abs && test_log && test_pow && (test_div_zero) > -1;
out property <bool> test2: test_sqrt2 && test_abs2 && test_log2 && test_pow2;
out property <bool> test1: test_sqrt && test_abs && test_log && test_pow;
out property <bool> test: test1 && test2 && (test_div_zero) > -1;
}
/*
```cpp

View file

@ -1,16 +1,16 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
TestCase := Rectangle {
property <int> a;
property <float> t1: max(41, 12, min(100, 12), max(-10000, 0+98.5), -4) + min(a, 0.5);
property <bool> t2: round(10/4) == 3 && floor(10/4) == 2 && ceil(10/4) == 3;
component TestCase inherits Window {
in property <int> a;
out property <float> t1: max(41, 12, min(100, 12), max(-10000, 0+98.5), -4) + min(a, 0.5);
out property <bool> t2: round(10/4) == 3 && floor(10/4) == 2 && ceil(10/4) == 3;
r := Rectangle {
property <int> max: 42;
property <int> xx: Math.max(1, 2, 3) + max;
}
property <bool> test: t2 && r.xx == 42 + 3;
out property <bool> test: t2 && r.xx == 42 + 3 && 88px.max(5px, 45px) == 88px && 88ms.min(5ms, 45ms) == 5ms;
}
/*
```cpp
@ -18,6 +18,7 @@ auto handle = TestCase::create();
const TestCase &instance = *handle;
assert_eq(instance.get_t1(), 98.5);
assert_eq(instance.get_t2(), true);
assert(instance.get_test());
```
@ -25,11 +26,13 @@ assert_eq(instance.get_t2(), true);
let instance = TestCase::new().unwrap();
assert_eq!(instance.get_t1(), 98.5);
assert_eq!(instance.get_t2(), true);
assert!(instance.get_test());
```
```js
var instance = new slint.TestCase({});
assert.equal(instance.t1, 98.5);
assert(instance.t2);
assert(instance.test);
```
*/

View file

@ -1,16 +1,18 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
TestCase := Rectangle {
property<int> t1: round(42.2);
property<int> t2: round(23.5);
property<int> t3: round(24.6);
property<int> t4: round(25);
component TestCase inherits Window {
out property<int> t1: round(42.2);
out property<int> t2: round(23.5);
out property<int> t3: round(24.6);
out property<int> t4: round(25);
property<int> n1: round(-42.2);
property<int> n2: round(-23.5);
property<int> n3: round(-24.6);
property<int> n4: round(-25);
out property<int> n1: round(-42.2);
out property<int> n2: round(-23.5);
out property<int> n3: round(-24.6);
out property<int> n4: round(-25);
out property <bool> test: 188.9.round() == 189 && (-4.58).round() == -(5.1).round();
}
/*
```cpp
@ -24,6 +26,7 @@ assert_eq(instance.get_n1(), -42);
assert_eq(instance.get_n2(), -24);
assert_eq(instance.get_n3(), -25);
assert_eq(instance.get_n4(), -25);
assert(instance.get_test());
```
```rust
@ -36,6 +39,7 @@ assert_eq!(instance.get_n1(), -42);
assert_eq!(instance.get_n2(), -24);
assert_eq!(instance.get_n3(), -25);
assert_eq!(instance.get_n4(), -25);
assert!(instance.get_test());
```
```js
@ -48,5 +52,6 @@ assert.equal(instance.n1, -42);
assert.equal(instance.n2, -24);
assert.equal(instance.n3, -25);
assert.equal(instance.n4, -25);
assert(instance.test);
```
*/

View file

@ -150,6 +150,9 @@ fn format_node(
SyntaxKind::PropertyChangedCallback => {
return format_property_changed_callback(node, writer, state);
}
SyntaxKind::MemberAccess => {
return format_member_access(node, writer, state);
}
_ => (),
}
@ -1233,10 +1236,34 @@ fn format_property_changed_callback(
&& whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, " ")?
&& whitespace_to(&mut sub, SyntaxKind::FatArrow, writer, state, " ")?
&& whitespace_to(&mut sub, SyntaxKind::CodeBlock, writer, state, " ")?;
for n in sub {
fold(n, writer, state)?;
}
state.new_line();
Ok(())
}
fn format_member_access(
node: &SyntaxNode,
writer: &mut impl TokenWriter,
state: &mut FormatState,
) -> Result<(), std::io::Error> {
let n = syntax_nodes::MemberAccess::from(node.clone());
// Special case fo things like `42 .mod(x)` where a space is needed otherwise it lexes differently
let need_space = n.Expression().child_token(SyntaxKind::NumberLiteral).is_some_and(|nl| {
!nl.text().contains('.') && nl.text().chars().last().is_some_and(|c| c.is_numeric())
});
let space_before_dot = if need_space { " " } else { "" };
let mut sub = n.children_with_tokens();
let _ok = whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, "")?
&& whitespace_to(&mut sub, SyntaxKind::Dot, writer, state, space_before_dot)?
&& whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?;
for n in sub {
fold(n, writer, state)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@ -2027,6 +2054,18 @@ export component MainWindow2 inherits Rectangle {
y += 1;
}
}
"#,
);
}
#[test]
fn access_member() {
assert_formatting(
"component X { expr: 42 .log(x) + 41 . log(y) + foo . bar + 21.0.log(0) + 54. .log(8) ; x: 42px.max(42px . min (0.px)); }",
r#"component X {
expr: 42 .log(x) + 41 .log(y) + foo.bar + 21.0.log(0) + 54..log(8);
x: 42px.max(42px.min(0.px));
}
"#,
);
}