diff --git a/api/sixtyfps-cpp/include/sixtyfps_string.h b/api/sixtyfps-cpp/include/sixtyfps_string.h index c38100b58..978829bfd 100644 --- a/api/sixtyfps-cpp/include/sixtyfps_string.h +++ b/api/sixtyfps-cpp/include/sixtyfps_string.h @@ -82,6 +82,9 @@ struct SharedString return cbindgen_private::sixtyfps_shared_string_bytes(this); } + const char *begin() const { return data(); } + const char *end() const { return std::string_view(*this).end(); } + /// Creates a new SharedString from the given number \a n. The string representation of the /// number uses a minimal formatting scheme: If \a n has no fractional part, the number will be /// formatted as an integer. diff --git a/docs/langref.md b/docs/langref.md index 395cc7f23..7838f72ef 100644 --- a/docs/langref.md +++ b/docs/langref.md @@ -244,6 +244,8 @@ Example := Window { * Object types convert with another object type if they have the same property names and their types can be converted. The source object can have either missing properties, or extra properties. But not both. * Array generaly do not convert between eachother. But array literal can be converted if the type does convert. + * String can be converted to float by using the `to_float` function. That function returns 0 if the string is not + a valid number. you can check with `is_float` if the string contains a valid number ```60 Example := Window { @@ -255,6 +257,10 @@ Example := Window { property<{a: string, b: int}> prop2: { a: "x", b: 12, c: 42 }; // ERROR: b is missing and c is extra, this does not compile, because it could be a typo. // property<{a: string, b: int}> prop2: { a: "x", c: 42 }; + + property xxx: "42.1"; + property xxx1: xxx.to_float(); // 42.1 + property xxx2: xxx.is_float(); // true } ``` diff --git a/sixtyfps_compiler/expression_tree.rs b/sixtyfps_compiler/expression_tree.rs index 96530fce8..2d0e16773 100644 --- a/sixtyfps_compiler/expression_tree.rs +++ b/sixtyfps_compiler/expression_tree.rs @@ -10,7 +10,7 @@ LICENSE END */ use crate::diagnostics::{BuildDiagnostics, Spanned, SpannedWithSourceFile}; use crate::langtype::{BuiltinElement, EnumerationValue, Type}; use crate::object_tree::*; -use crate::parser::SyntaxNodeWithSourceFile; +use crate::parser::{NodeOrTokenWithSourceFile, SyntaxNodeWithSourceFile}; use core::cell::RefCell; use std::collections::HashMap; use std::hash::Hash; @@ -50,6 +50,10 @@ pub enum BuiltinFunction { GetWindowScaleFactor, Debug, SetFocusItem, + /// the "42".to_float() + StringToFloat, + /// the "42".is_float() + StringIsFloat, } impl BuiltinFunction { @@ -65,6 +69,12 @@ impl BuiltinFunction { return_type: Box::new(Type::Void), args: vec![Type::ElementReference], }, + BuiltinFunction::StringToFloat => { + Type::Function { return_type: Box::new(Type::Float32), args: vec![Type::String] } + } + BuiltinFunction::StringIsFloat => { + Type::Function { return_type: Box::new(Type::Bool), args: vec![Type::String] } + } } } } @@ -200,7 +210,7 @@ pub enum Expression { /// a regular FunctionCall expression where the base becomes the first argument. MemberFunction { base: Box, - base_node: SyntaxNodeWithSourceFile, + base_node: NodeOrTokenWithSourceFile, member: Box, }, @@ -370,7 +380,10 @@ impl Expression { }, Expression::Cast { to, .. } => to.clone(), Expression::CodeBlock(sub) => sub.last().map_or(Type::Void, |e| e.ty()), - Expression::FunctionCall { function, .. } => function.ty(), + Expression::FunctionCall { function, .. } => match function.ty() { + Type::Function { return_type, .. } => *return_type, + _ => Type::Invalid, + }, Expression::SelfAssignment { .. } => Type::Void, Expression::ResourceReference { .. } => Type::Resource, Expression::Condition { condition: _, true_expr, false_expr } => { diff --git a/sixtyfps_compiler/generator/cpp.rs b/sixtyfps_compiler/generator/cpp.rs index 93432e11d..3a815c6a8 100644 --- a/sixtyfps_compiler/generator/cpp.rs +++ b/sixtyfps_compiler/generator/cpp.rs @@ -509,6 +509,7 @@ pub fn generate(doc: &Document, diag: &mut BuildDiagnostics) -> Option".into()); file.includes.push("".into()); + file.includes.push("".into()); // TODO: ideally only include this if needed (by to_float) file.includes.push("".into()); for ty in &doc.inner_structs { @@ -1262,6 +1263,25 @@ fn compile_expression(e: &crate::expression_tree::Expression, component: &Rc { format!("{}.set_focus_item", window_ref_expression(component)) } + + /* std::from_chars is unfortunately not yet implemented in gcc + BuiltinFunction::SringIsFloat => { + "[](const auto &a){ double v; auto r = std::from_chars(std::begin(a), std::end(a), v); return r.ptr == std::end(a); }" + .into() + } + BuiltinFunction::StringToFloat => { + "[](const auto &a){ double v; auto r = std::from_chars(std::begin(a), std::end(a), v); return r.ptr == std::end(a) ? v : 0; }" + .into() + }*/ + BuiltinFunction::StringIsFloat => { + "[](const auto &a){ auto e1 = std::end(a); auto e2 = const_cast(e1); std::strtod(std::begin(a), &e2); return e1 == e2; }" + .into() + } + BuiltinFunction::StringToFloat => { + "[](const auto &a){ auto e1 = std::end(a); auto e2 = const_cast(e1); auto r = std::strtod(std::begin(a), &e2); return e1 == e2 ? r : 0; }" + .into() + } + }, Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"), Expression::MemberFunction { .. } => panic!("member function expressions must not appear in the code generator anymore"), diff --git a/sixtyfps_compiler/generator/rust.rs b/sixtyfps_compiler/generator/rust.rs index 0b43c770d..967484dd6 100644 --- a/sixtyfps_compiler/generator/rust.rs +++ b/sixtyfps_compiler/generator/rust.rs @@ -1049,7 +1049,15 @@ fn compile_expression(e: &Expression, component: &Rc) -> TokenStream quote!(#window_ref.scale_factor) } BuiltinFunction::Debug => quote!((|x| println!("{:?}", x))), - BuiltinFunction::SetFocusItem => panic!("internal error: SetFocusItem is handled directly in CallFunction") + BuiltinFunction::SetFocusItem => { + panic!("internal error: SetFocusItem is handled directly in CallFunction") + } + BuiltinFunction::StringToFloat => { + quote!((|x: SharedString| -> f64 { ::core::str::FromStr::from_str(x.as_str()).unwrap_or_default() } )) + } + BuiltinFunction::StringIsFloat => { + quote!((|x: SharedString| { ::from_str(x.as_str()).is_ok() } )) + } }, Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"), Expression::MemberFunction{ .. } => panic!("member function expressions must not appear in the code generator anymore"), diff --git a/sixtyfps_compiler/langtype.rs b/sixtyfps_compiler/langtype.rs index 3cb35bda2..d1e6146de 100644 --- a/sixtyfps_compiler/langtype.rs +++ b/sixtyfps_compiler/langtype.rs @@ -238,7 +238,9 @@ impl Type { pub fn lookup_member_function(&self, name: &str) -> Expression { match self { - Type::Builtin(builtin) => builtin.member_functions.get(name).unwrap().clone(), + Type::Builtin(builtin) => { + builtin.member_functions.get(name).cloned().unwrap_or(Expression::Invalid) + } _ => Expression::Invalid, } } diff --git a/sixtyfps_compiler/parser.rs b/sixtyfps_compiler/parser.rs index a642875cc..c8e6bc43d 100644 --- a/sixtyfps_compiler/parser.rs +++ b/sixtyfps_compiler/parser.rs @@ -639,6 +639,12 @@ impl Spanned for SyntaxToken { } } +impl Spanned for rowan::NodeOrToken { + fn span(&self) -> crate::diagnostics::Span { + crate::diagnostics::Span::new(self.text_range().start().into()) + } +} + #[derive(Debug, Clone)] pub struct SyntaxNodeWithSourceFile { pub node: SyntaxNode, @@ -699,11 +705,24 @@ impl SyntaxNodeWithSourceFile { } } +#[derive(Debug, Clone)] pub struct NodeOrTokenWithSourceFile { node_or_token: rowan::NodeOrToken, source_file: Option, } +impl From for NodeOrTokenWithSourceFile { + fn from(n: SyntaxNodeWithSourceFile) -> Self { + Self { node_or_token: n.node.into(), source_file: n.source_file } + } +} + +impl From for NodeOrTokenWithSourceFile { + fn from(n: SyntaxTokenWithSourceFile) -> Self { + Self { node_or_token: n.token.into(), source_file: n.source_file } + } +} + impl NodeOrTokenWithSourceFile { pub fn kind(&self) -> SyntaxKind { self.node_or_token.kind() @@ -767,6 +786,18 @@ impl SpannedWithSourceFile for SyntaxTokenWithSourceFile { } } +impl Spanned for NodeOrTokenWithSourceFile { + fn span(&self) -> crate::diagnostics::Span { + self.node_or_token.span() + } +} + +impl SpannedWithSourceFile for NodeOrTokenWithSourceFile { + fn source_file(&self) -> Option<&SourceFile> { + self.source_file.as_ref() + } +} + /// return the normalized identifier string of the first SyntaxKind::Identifier in this node pub fn identifier_text(node: &SyntaxNodeWithSourceFile) -> Option { node.child_text(SyntaxKind::Identifier).map(|x| normalize_identifier(&x)) diff --git a/sixtyfps_compiler/passes/resolving.rs b/sixtyfps_compiler/passes/resolving.rs index f0662431d..5c7714561 100644 --- a/sixtyfps_compiler/passes/resolving.rs +++ b/sixtyfps_compiler/passes/resolving.rs @@ -518,7 +518,7 @@ impl Expression { ctx: &mut LookupCtx, ) -> Expression { let mut sub_expr = - node.Expression().map(|n| (Self::from_expression_node(n.clone(), ctx), n.0)); + node.Expression().map(|n| (Self::from_expression_node(n.clone(), ctx), n.0.into())); let mut arguments = Vec::new(); @@ -814,7 +814,7 @@ fn continue_lookup_within_element( let member = elem.borrow().base_type.lookup_member_function(&prop_name); return Expression::MemberFunction { base: Box::new(Expression::ElementReference(Rc::downgrade(elem))), - base_node: node, + base_node: node.into(), member: Box::new(member), }; } else { @@ -876,6 +876,24 @@ fn maybe_lookup_object( }); } } + Type::String => { + return Expression::MemberFunction { + base: Box::new(base), + base_node: next.clone().into(), // Note that this is not the base_node, but the function's node + member: Box::new(match next_str.as_str() { + "is_float" => { + Expression::BuiltinFunctionReference(BuiltinFunction::StringIsFloat) + } + "to_float" => { + Expression::BuiltinFunctionReference(BuiltinFunction::StringToFloat) + } + _ => { + ctx.diag.push_error("Cannot access fields of string".into(), &next); + return Expression::Invalid; + } + }), + }; + } _ => { ctx.diag.push_error("Cannot access fields of property".into(), &next); return Expression::Invalid; diff --git a/sixtyfps_runtime/interpreter/eval.rs b/sixtyfps_runtime/interpreter/eval.rs index ae1471863..f848beae5 100644 --- a/sixtyfps_runtime/interpreter/eval.rs +++ b/sixtyfps_runtime/interpreter/eval.rs @@ -384,6 +384,26 @@ pub fn eval_expression(e: &Expression, local_context: &mut EvalLocalContext) -> panic!("internal error: argument to SetFocusItem must be an element") } } + Expression::BuiltinFunctionReference(BuiltinFunction::StringIsFloat) => { + if arguments.len() != 1 { + panic!("internal error: incorrect argument count to StringIsFloat") + } + if let Value::String(s) = eval_expression(&arguments[0], local_context) { + Value::Bool(::from_str(s.as_str()).is_ok()) + } else { + panic!("Argument not a string"); + } + } + Expression::BuiltinFunctionReference(BuiltinFunction::StringToFloat) => { + if arguments.len() != 1 { + panic!("internal error: incorrect argument count to StringToFloat") + } + if let Value::String(s) = eval_expression(&arguments[0], local_context) { + Value::Number(core::str::FromStr::from_str(s.as_str()).unwrap_or(0.)) + } else { + panic!("Argument not a string"); + } + } _ => panic!("call of something not a signal"), } Expression::SelfAssignment { lhs, rhs, op } => { diff --git a/tests/cases/types/string_to_float.60 b/tests/cases/types/string_to_float.60 new file mode 100644 index 000000000..a28c8109b --- /dev/null +++ b/tests/cases/types/string_to_float.60 @@ -0,0 +1,49 @@ +/* LICENSE BEGIN + This file is part of the SixtyFPS Project -- https://sixtyfps.io + Copyright (c) 2020 Olivier Goffart + Copyright (c) 2020 Simon Hausmann + + SPDX-License-Identifier: GPL-3.0-only + This file is also available under commercial licensing terms. + Please contact info@sixtyfps.io for more information. +LICENSE END */ +TestCase := Rectangle { + property hello: "hello"; + property number: "42.56"; + property invalid: "132a"; + property negative: "-1200000.1"; + + property number_as_float: number.to_float(); + property negative_as_float: negative.to_float(); + property test_is_float: !hello.is_float() && number.is_float() && + !invalid.is_float() && negative.is_float(); +} + + +/* + +```cpp +auto fuzzy_compare = [](float a, float b) { return std::abs(a - b) < 0.00000001; }; +TestCase instance; +assert(instance.get_test_is_float()); +assert(fuzzy_compare(instance.get_number_as_float(), 42.56)); +assert(fuzzy_compare(instance.get_negative_as_float(), -1200000.1)); +``` + +```rust +let instance = TestCase::new(); +let instance = instance.as_ref(); +assert!(instance.get_test_is_float()); +assert_eq!(instance.get_number_as_float(), 42.56); +assert_eq!(instance.get_negative_as_float(), -1200000.1); +``` + +```js +function n(a) { return Math.round(a*10000) } +var instance = new sixtyfps.TestCase({}); +assert(instance.test_is_float); +assert.equal(n(instance.number_as_float), n(42.56)); +assert.equal(n(instance.negative_as_float/1000), n(-1200000.1/1000)); +``` + +*/ \ No newline at end of file