From c285049d8e3012359a9e52c7b4e2bd501ac607f1 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Sat, 18 Oct 2025 11:13:58 +0100 Subject: [PATCH 01/28] compiler: Introduce ElementType::Interface Add a new ElementType variant that will allow us to parse interface declarations using the existing element parsing code. The match arms implemented are my best guess based on the context of the enclosing function and how a global is handled. --- internal/compiler/generator.rs | 2 +- internal/compiler/langtype.rs | 8 +++++++- internal/compiler/namedreference.rs | 4 +++- internal/compiler/object_tree.rs | 8 ++++++++ internal/compiler/passes/materialize_fake_properties.rs | 4 ++-- internal/compiler/passes/resolve_native_classes.rs | 4 +++- internal/compiler/passes/resolving.rs | 8 ++++---- internal/compiler/tests/consistent_styles.rs | 1 + internal/interpreter/global_component.rs | 5 ++++- 9 files changed, 33 insertions(+), 11 deletions(-) diff --git a/internal/compiler/generator.rs b/internal/compiler/generator.rs index 0a8e4418027..e1f3ede4365 100644 --- a/internal/compiler/generator.rs +++ b/internal/compiler/generator.rs @@ -463,7 +463,7 @@ pub fn for_each_const_properties( ElementType::Builtin(_) => { unreachable!("builtin element should have been resolved") } - ElementType::Global | ElementType::Error => break, + ElementType::Global | ElementType::Interface | ElementType::Error => break, } } for c in all_prop { diff --git a/internal/compiler/langtype.rs b/internal/compiler/langtype.rs index bce71785525..cb312513755 100644 --- a/internal/compiler/langtype.rs +++ b/internal/compiler/langtype.rs @@ -405,6 +405,8 @@ pub enum ElementType { Error, /// This should be the base type of the root element of a global component Global, + /// This should be the base type of the root element of an interface + Interface, } impl PartialEq for ElementType { @@ -413,7 +415,9 @@ impl PartialEq for ElementType { (Self::Component(a), Self::Component(b)) => Rc::ptr_eq(a, b), (Self::Builtin(a), Self::Builtin(b)) => Rc::ptr_eq(a, b), (Self::Native(a), Self::Native(b)) => Rc::ptr_eq(a, b), - (Self::Error, Self::Error) | (Self::Global, Self::Global) => true, + (Self::Error, Self::Error) + | (Self::Global, Self::Global) + | (Self::Interface, Self::Interface) => true, _ => false, } } @@ -615,6 +619,7 @@ impl ElementType { ElementType::Native(_) => None, // Too late, caller should call this function before the native class lowering ElementType::Error => None, ElementType::Global => None, + ElementType::Interface => None, } } } @@ -627,6 +632,7 @@ impl Display for ElementType { Self::Native(b) => b.class_name.fmt(f), Self::Error => write!(f, ""), Self::Global => Ok(()), + Self::Interface => Ok(()), } } } diff --git a/internal/compiler/namedreference.rs b/internal/compiler/namedreference.rs index 99e76214c5e..0d91dfc274a 100644 --- a/internal/compiler/namedreference.rs +++ b/internal/compiler/namedreference.rs @@ -117,7 +117,9 @@ impl NamedReference { ElementType::Native(n) => { return n.properties.get(self.name()).is_none_or(|pi| !pi.is_native_output()); } - crate::langtype::ElementType::Error | crate::langtype::ElementType::Global => { + crate::langtype::ElementType::Error + | crate::langtype::ElementType::Global + | crate::langtype::ElementType::Interface => { return true; } } diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index 58dcfb54c78..d20d62c8ea5 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -473,6 +473,14 @@ impl Component { } } + /// This is an interface introduced with the "interface" keyword + pub fn is_interface(&self) -> bool { + match &self.root_element.borrow().base_type { + ElementType::Interface => true, + _ => false, + } + } + /// Returns the names of aliases to global singletons, exactly as /// specified in the .slint markup (not normalized). pub fn global_aliases(&self) -> Vec { diff --git a/internal/compiler/passes/materialize_fake_properties.rs b/internal/compiler/passes/materialize_fake_properties.rs index 39b9e5640fb..3ec2e3d1a53 100644 --- a/internal/compiler/passes/materialize_fake_properties.rs +++ b/internal/compiler/passes/materialize_fake_properties.rs @@ -96,7 +96,7 @@ fn should_materialize( ElementType::Component(c) => has_declared_property(&c.root_element.borrow(), prop), ElementType::Builtin(b) => b.native_class.lookup_property(prop).is_some(), ElementType::Native(n) => n.lookup_property(prop).is_some(), - ElementType::Global | ElementType::Error => false, + ElementType::Global | ElementType::Interface | ElementType::Error => false, }; if !has_declared_property { @@ -129,7 +129,7 @@ pub fn has_declared_property(elem: &Element, prop: &str) -> bool { ElementType::Component(c) => has_declared_property(&c.root_element.borrow(), prop), ElementType::Builtin(b) => b.native_class.lookup_property(prop).is_some(), ElementType::Native(n) => n.lookup_property(prop).is_some(), - ElementType::Global | ElementType::Error => false, + ElementType::Global | ElementType::Interface | ElementType::Error => false, } } diff --git a/internal/compiler/passes/resolve_native_classes.rs b/internal/compiler/passes/resolve_native_classes.rs index 62adfab7912..be0106ef822 100644 --- a/internal/compiler/passes/resolve_native_classes.rs +++ b/internal/compiler/passes/resolve_native_classes.rs @@ -26,7 +26,9 @@ pub fn resolve_native_classes(component: &Component) { // already native return; } - ElementType::Global | ElementType::Error => panic!("This should not happen"), + ElementType::Global | ElementType::Interface | ElementType::Error => { + panic!("This should not happen") + } }; let analysis = elem.property_analysis.borrow(); diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs index 73e7e7e4221..4cbf4fbc834 100644 --- a/internal/compiler/passes/resolving.rs +++ b/internal/compiler/passes/resolving.rs @@ -1853,10 +1853,10 @@ fn continue_lookup_within_element( } else { let mut err = |extra: &str| { let what = match &elem.borrow().base_type { - ElementType::Global => { - let global = elem.borrow().enclosing_component.upgrade().unwrap(); - assert!(global.is_global()); - format!("'{}'", global.id) + ElementType::Global | ElementType::Interface => { + let enclosing_type = elem.borrow().enclosing_component.upgrade().unwrap(); + assert!(enclosing_type.is_global() || enclosing_type.is_interface()); + format!("'{}'", enclosing_type.id) } ElementType::Component(c) => format!("Element '{}'", c.id), ElementType::Builtin(b) => format!("Element '{}'", b.name), diff --git a/internal/compiler/tests/consistent_styles.rs b/internal/compiler/tests/consistent_styles.rs index 770c1a88813..1067f07d2ed 100644 --- a/internal/compiler/tests/consistent_styles.rs +++ b/internal/compiler/tests/consistent_styles.rs @@ -131,6 +131,7 @@ fn load_component(component: &Rc) -> C i_slint_compiler::langtype::ElementType::Native(_) => unreachable!(), i_slint_compiler::langtype::ElementType::Error => unreachable!(), i_slint_compiler::langtype::ElementType::Global => break, + i_slint_compiler::langtype::ElementType::Interface => break, }; elem = e; } diff --git a/internal/interpreter/global_component.rs b/internal/interpreter/global_component.rs index eb81ad3d4cd..3a90a011c20 100644 --- a/internal/interpreter/global_component.rs +++ b/internal/interpreter/global_component.rs @@ -384,6 +384,9 @@ fn generate(component: &Rc) -> CompiledGlobal { public_properties: Default::default(), _original: component.clone(), }, - ElementType::Error | ElementType::Native(_) | ElementType::Component(_) => unreachable!(), + ElementType::Error + | ElementType::Interface + | ElementType::Native(_) + | ElementType::Component(_) => unreachable!(), } } From 821ed797e3db5e9978770829c03cd247b16598b8 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Sat, 18 Oct 2025 10:48:54 +0100 Subject: [PATCH 02/28] compiler: `interface` is a valid top-level item keyword A document may contain zero or more interfaces. Interfaces do not support inheritance. --- internal/compiler/parser/document.rs | 13 ++++++++----- .../tests/syntax/basic/interface_inheritance.slint | 6 ++++++ .../compiler/tests/syntax/basic/interfaces.slint | 9 +++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 internal/compiler/tests/syntax/basic/interface_inheritance.slint create mode 100644 internal/compiler/tests/syntax/basic/interfaces.slint diff --git a/internal/compiler/parser/document.rs b/internal/compiler/parser/document.rs index f4f0c8a8b64..9dd433aad07 100644 --- a/internal/compiler/parser/document.rs +++ b/internal/compiler/parser/document.rs @@ -99,28 +99,31 @@ pub fn parse_document(p: &mut impl Parser) -> bool { /// global Struct { property xx; } /// component C { property xx; } /// component C inherits D { } +/// interface I { property xx; } /// ``` pub fn parse_component(p: &mut impl Parser) -> bool { let simple_component = p.nth(1).kind() == SyntaxKind::ColonEqual; let is_global = !simple_component && p.peek().as_str() == "global"; + let is_interface = !simple_component && p.peek().as_str() == "interface"; let is_new_component = !simple_component && p.peek().as_str() == "component"; - if !is_global && !simple_component && !is_new_component { + if !is_global && !simple_component && !is_new_component && !is_interface { p.error( "Parse error: expected a top-level item such as a component, a struct, or a global", ); return false; } let mut p = p.start_node(SyntaxKind::Component); - if is_global || is_new_component { + if is_global || is_new_component || is_interface { p.consume(); } if !p.start_node(SyntaxKind::DeclaredIdentifier).expect(SyntaxKind::Identifier) { drop(p.start_node(SyntaxKind::Element)); return false; } - if is_global { + if is_global || is_interface { if p.peek().kind() == SyntaxKind::ColonEqual { - p.warning("':=' to declare a global is deprecated. Remove the ':='"); + let description = if is_global { "a global" } else { "an interface" }; + p.warning(format!("':=' to declare {description} is deprecated. Remove the ':='")); p.consume(); } } else if !is_new_component { @@ -144,7 +147,7 @@ pub fn parse_component(p: &mut impl Parser) -> bool { return false; } - if is_global && p.peek().kind() == SyntaxKind::LBrace { + if (is_global || is_interface) && p.peek().kind() == SyntaxKind::LBrace { let mut p = p.start_node(SyntaxKind::Element); p.consume(); parse_element_content(&mut *p); diff --git a/internal/compiler/tests/syntax/basic/interface_inheritance.slint b/internal/compiler/tests/syntax/basic/interface_inheritance.slint new file mode 100644 index 00000000000..c8506dca4ad --- /dev/null +++ b/internal/compiler/tests/syntax/basic/interface_inheritance.slint @@ -0,0 +1,6 @@ +// Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +export interface DerivedInterface inherits HelloInterface { +// ^error{Syntax error: expected '{'} +} diff --git a/internal/compiler/tests/syntax/basic/interfaces.slint b/internal/compiler/tests/syntax/basic/interfaces.slint new file mode 100644 index 00000000000..9eb97eeb4cb --- /dev/null +++ b/internal/compiler/tests/syntax/basic/interfaces.slint @@ -0,0 +1,9 @@ +// Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +export interface HelloInterface { +} + +export interface DeprecatedInterface := { +// ^warning{':=' to declare an interface is deprecated. Remove the ':='} +} From dc44f4d6f171d8f3136b88349e22ea61cf55a7bb Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 21 Nov 2025 11:08:00 +0000 Subject: [PATCH 03/28] compiler: An interface excludes the same content as a global Verify that an interface cannot have: - sub elements; - repeated elements; - property animations; - states; - transitions; - an init callback declaration; - an init callback implementation. --- internal/compiler/object_tree.rs | 21 +++++++++++------- .../compiler/passes/resolve_native_classes.rs | 6 ++++- .../basic/interface_invalid_content.slint | 22 +++++++++++++++++++ 3 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 internal/compiler/tests/syntax/basic/interface_invalid_content.slint diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index d20d62c8ea5..a46cc032eec 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -438,10 +438,10 @@ impl Component { root_element: Element::from_node( node.Element(), "root".into(), - if node.child_text(SyntaxKind::Identifier).is_some_and(|t| t == "global") { - ElementType::Global - } else { - ElementType::Error + match node.child_text(SyntaxKind::Identifier) { + Some(t) if t == "global" => ElementType::Global, + Some(t) if t == "interface" => ElementType::Interface, + _ => ElementType::Error, }, &mut child_insertion_point, is_legacy_syntax, @@ -1027,10 +1027,15 @@ impl Element { ElementType::Error } } - } else if parent_type == ElementType::Global { - // This must be a global component it can only have properties and callback + } else if parent_type == ElementType::Global || parent_type == ElementType::Interface { + // This must be a global component or interface. It can only have properties and callbacks let mut error_on = |node: &dyn Spanned, what: &str| { - diag.push_error(format!("A global component cannot have {what}"), node); + let element_type = match parent_type { + ElementType::Global => "A global component", + ElementType::Interface => "An interface", + _ => "An unexpected type", + }; + diag.push_error(format!("{element_type} cannot have {what}"), node); }; node.SubElement().for_each(|n| error_on(&n, "sub elements")); node.RepeatedElement().for_each(|n| error_on(&n, "sub elements")); @@ -1051,7 +1056,7 @@ impl Element { } }); - ElementType::Global + parent_type } else if parent_type != ElementType::Error { // This should normally never happen because the parser does not allow for this assert!(diag.has_errors()); diff --git a/internal/compiler/passes/resolve_native_classes.rs b/internal/compiler/passes/resolve_native_classes.rs index be0106ef822..285c130f619 100644 --- a/internal/compiler/passes/resolve_native_classes.rs +++ b/internal/compiler/passes/resolve_native_classes.rs @@ -26,7 +26,11 @@ pub fn resolve_native_classes(component: &Component) { // already native return; } - ElementType::Global | ElementType::Interface | ElementType::Error => { + ElementType::Interface => { + // TODO: I don't think we should be here - but it is too early to tell. Don't panic though. + return; + } + ElementType::Global | ElementType::Error => { panic!("This should not happen") } }; diff --git a/internal/compiler/tests/syntax/basic/interface_invalid_content.slint b/internal/compiler/tests/syntax/basic/interface_invalid_content.slint new file mode 100644 index 00000000000..1fbae294a8c --- /dev/null +++ b/internal/compiler/tests/syntax/basic/interface_invalid_content.slint @@ -0,0 +1,22 @@ +// Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +export interface InvalidInterface { + Rectangle {} +// ^error{An interface cannot have sub elements} + for x in 2: Text {} +// ^error{An interface cannot have sub elements} + + out property i; + animate i { duration: 100ms; } +// ^error{An interface cannot have animations} + states [ ] +// ^error{An interface cannot have states} + transitions [ ] +// ^error{An interface cannot have transitions} +// ^^error{'transitions' block are no longer supported. Use 'in {...}' and 'out {...}' directly in the state definition} + callback init; +// ^error{An interface cannot have an 'init' callback} + init => { debug("nope"); } +// ^error{An interface cannot have an 'init' callback} +} From f6a3d67eca79e906c5563da0d3c1f8baa8ff6a28 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Wed, 22 Oct 2025 14:09:33 +0100 Subject: [PATCH 04/28] compiler: An interface can have public properties Warn if private properties are declared. --- internal/compiler/object_tree.rs | 9 ++++++++- .../tests/syntax/basic/interface_properties.slint | 15 +++++++++++++++ .../compiler/tests/syntax/basic/interfaces.slint | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 internal/compiler/tests/syntax/basic/interface_properties.slint diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index a46cc032eec..245efcfde37 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -1076,7 +1076,7 @@ impl Element { .to_string(); let mut r = Element { id, - base_type, + base_type: base_type.clone(), debug: vec![ElementDebugInfo { qualified_id, element_hash: 0, @@ -1160,6 +1160,13 @@ impl Element { } }); + if base_type == ElementType::Interface && visibility == PropertyVisibility::Private { + diag.push_warning( + "'private' properties are inaccessible in an interface".into(), + &prop_decl, + ); + } + r.property_declarations.insert( prop_name.clone().into(), PropertyDeclaration { diff --git a/internal/compiler/tests/syntax/basic/interface_properties.slint b/internal/compiler/tests/syntax/basic/interface_properties.slint new file mode 100644 index 00000000000..e0762e0ee4b --- /dev/null +++ b/internal/compiler/tests/syntax/basic/interface_properties.slint @@ -0,0 +1,15 @@ +// Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +export interface ValidInterfacePropertyDeclarations { + property implicit-private; +// ^warning{'private' properties are inaccessible in an interface} + + private property private; +// ^warning{'private' properties are inaccessible in an interface} + + in property in-property; + out property out-property; + in-out property in-out-property; + in_out property in-out-underscore-property; +} diff --git a/internal/compiler/tests/syntax/basic/interfaces.slint b/internal/compiler/tests/syntax/basic/interfaces.slint index 9eb97eeb4cb..4a206a5be43 100644 --- a/internal/compiler/tests/syntax/basic/interfaces.slint +++ b/internal/compiler/tests/syntax/basic/interfaces.slint @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 export interface HelloInterface { + out property message; } export interface DeprecatedInterface := { From 1a56ac87668756a44060001ae5327adf951988c3 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 21 Nov 2025 17:57:06 +0000 Subject: [PATCH 05/28] compiler: `implements` is a valid keyword It is treated as an alias for `inherits`, for now. --- internal/compiler/parser/document.rs | 5 ++++- .../compiler/tests/syntax/basic/interfaces.slint | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/internal/compiler/parser/document.rs b/internal/compiler/parser/document.rs index 9dd433aad07..5c30f513ce0 100644 --- a/internal/compiler/parser/document.rs +++ b/internal/compiler/parser/document.rs @@ -100,6 +100,7 @@ pub fn parse_document(p: &mut impl Parser) -> bool { /// component C { property xx; } /// component C inherits D { } /// interface I { property xx; } +/// component E implements I { } /// ``` pub fn parse_component(p: &mut impl Parser) -> bool { let simple_component = p.nth(1).kind() == SyntaxKind::ColonEqual; @@ -134,6 +135,8 @@ pub fn parse_component(p: &mut impl Parser) -> bool { drop(p.start_node(SyntaxKind::Element)); return false; } + } else if p.peek().as_str() == "implements" { + p.consume(); } else if p.peek().as_str() == "inherits" { p.consume(); } else if p.peek().kind() == SyntaxKind::LBrace { @@ -142,7 +145,7 @@ pub fn parse_component(p: &mut impl Parser) -> bool { parse_element_content(&mut *p); return p.expect(SyntaxKind::RBrace); } else { - p.error("Expected '{' or keyword 'inherits'"); + p.error("Expected '{', keyword 'implements' or keyword 'inherits'"); drop(p.start_node(SyntaxKind::Element)); return false; } diff --git a/internal/compiler/tests/syntax/basic/interfaces.slint b/internal/compiler/tests/syntax/basic/interfaces.slint index 4a206a5be43..1c0564b4384 100644 --- a/internal/compiler/tests/syntax/basic/interfaces.slint +++ b/internal/compiler/tests/syntax/basic/interfaces.slint @@ -8,3 +8,17 @@ export interface HelloInterface { export interface DeprecatedInterface := { // ^warning{':=' to declare an interface is deprecated. Remove the ':='} } + +export interface LineEditInterface { + in-out property text; +} + +export component LineEditBase implements LineEditInterface { + text <=> text-input.text; + color <=> text-input.color; +// ^error{Unknown property color in LineEditInterface} + text-input := TextInput { } +} + +export component Foo implements Bar {} +// ^error{Unknown element 'Bar'} From 8336cb09dd11c9101ba2223c55026299958e7380 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Mon, 24 Nov 2025 16:58:45 +0000 Subject: [PATCH 06/28] compiler: Cannot "implement" non-interface components The compiler produces an error when the `implements` keyword is used before a global, component, or builtin type. The parser doesn't know about this kind of relationship - the "implements" and "inherits" keywords are just consumed. For now we just go back and check what the expected relationship type was whilst constructing an Element. --- internal/compiler/object_tree.rs | 49 +++++++++++++++++-- .../tests/syntax/basic/interfaces.slint | 10 ++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index 245efcfde37..26630601b6e 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -993,6 +993,28 @@ pub struct RepeatedElementInfo { pub type ElementRc = Rc>; pub type ElementWeak = Weak>; +#[derive(Debug, PartialEq)] +/// The kind of relationship a component has the component or interface it derives from, if any. +enum ParentRelationship { + /// The component inherits from the parent component. + Inherits, + /// The component implements the parent interface. + Implements, +} + +/// Determine the expected relationship to the parent component/interface, if any. +fn expected_relationship_to_parent(node: &syntax_nodes::Element) -> Option { + let parent = node.parent().filter(|p| p.kind() == SyntaxKind::Component)?; + let implements_inherits_identifier = + parent.children_with_tokens().filter(|n| n.kind() == SyntaxKind::Identifier).nth(1)?; + let token = implements_inherits_identifier.as_token()?; + return match token.text() { + "inherits" => Some(ParentRelationship::Inherits), + "implements" => Some(ParentRelationship::Implements), + _ => None, + }; +} + impl Element { pub fn make_rc(self) -> ElementRc { let r = ElementRc::new(RefCell::new(self)); @@ -1013,16 +1035,35 @@ impl Element { let base_type = if let Some(base_node) = node.QualifiedName() { let base = QualifiedTypeName::from_node(base_node.clone()); let base_string = base.to_smolstr(); - match parent_type.lookup_type_for_child_element(&base_string, tr) { - Ok(ElementType::Component(c)) if c.is_global() => { + match ( + parent_type.lookup_type_for_child_element(&base_string, tr), + expected_relationship_to_parent(&node), + ) { + (Ok(ElementType::Component(c)), _) if c.is_global() => { diag.push_error( "Cannot create an instance of a global component".into(), &base_node, ); ElementType::Error } - Ok(ty) => ty, - Err(err) => { + (Ok(ElementType::Component(c)), Some(ParentRelationship::Implements)) + if !c.is_interface() => + { + diag.push_error( + format!("Cannot implement {}. It is not an interface", base_string), + &base_node, + ); + ElementType::Error + } + (Ok(ElementType::Builtin(_bt)), Some(ParentRelationship::Implements)) => { + diag.push_error( + format!("Cannot implement {}. It is not an interface", base_string), + &base_node, + ); + ElementType::Error + } + (Ok(ty), _) => ty, + (Err(err), _) => { diag.push_error(err, &base_node); ElementType::Error } diff --git a/internal/compiler/tests/syntax/basic/interfaces.slint b/internal/compiler/tests/syntax/basic/interfaces.slint index 1c0564b4384..03a867a374a 100644 --- a/internal/compiler/tests/syntax/basic/interfaces.slint +++ b/internal/compiler/tests/syntax/basic/interfaces.slint @@ -22,3 +22,13 @@ export component LineEditBase implements LineEditInterface { export component Foo implements Bar {} // ^error{Unknown element 'Bar'} + +export component MyRectangle implements Rectangle {} +// ^error{Cannot implement Rectangle. It is not an interface} + +export component LineEdit implements LineEditBase {} +// ^error{Cannot implement LineEditBase. It is not an interface} + +global MyGlobal {} +export component MyGlobalG implements MyGlobal {} +// ^error{Cannot create an instance of a global component} From fd2c9e954d4089470776957c9a48afa20f64af72 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Wed, 26 Nov 2025 11:29:53 +0000 Subject: [PATCH 07/28] compiler: interface properties are exposed Attempt to verify that the interpreter can see and use properties declared in an interface that a component implements. This is an initial proof-of-concept, we will need to come back and add checks and tests for mixing 'inherits' with 'implements'. --- internal/compiler/object_tree.rs | 30 ++++++++++++----- .../tests/syntax/basic/interfaces.slint | 4 +-- tests/cases/interfaces/implements.slint | 32 +++++++++++++++++++ 3 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 tests/cases/interfaces/implements.slint diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index 26630601b6e..abf282fdda4 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -1032,6 +1032,7 @@ impl Element { diag: &mut BuildDiagnostics, tr: &TypeRegister, ) -> ElementRc { + let mut interfaces: Vec> = Vec::new(); let base_type = if let Some(base_node) = node.QualifiedName() { let base = QualifiedTypeName::from_node(base_node.clone()); let base_string = base.to_smolstr(); @@ -1046,14 +1047,19 @@ impl Element { ); ElementType::Error } - (Ok(ElementType::Component(c)), Some(ParentRelationship::Implements)) - if !c.is_interface() => - { - diag.push_error( - format!("Cannot implement {}. It is not an interface", base_string), - &base_node, - ); - ElementType::Error + (Ok(ElementType::Component(c)), Some(ParentRelationship::Implements)) => { + if !c.is_interface() { + diag.push_error( + format!("Cannot implement {}. It is not an interface", base_string), + &base_node, + ); + ElementType::Error + } else { + c.used.set(true); + interfaces.push(c); + // We are implementing an interface - not inheriting from it + tr.empty_type() + } } (Ok(ElementType::Builtin(_bt)), Some(ParentRelationship::Implements)) => { diag.push_error( @@ -1130,6 +1136,14 @@ impl Element { ..Default::default() }; + for interface in interfaces.iter() { + for (prop_name, prop_decl) in + interface.root_element.borrow().property_declarations.iter() + { + r.property_declarations.insert(prop_name.clone(), prop_decl.clone()); + } + } + for prop_decl in node.PropertyDeclaration() { let prop_type = prop_decl .Type() diff --git a/internal/compiler/tests/syntax/basic/interfaces.slint b/internal/compiler/tests/syntax/basic/interfaces.slint index 03a867a374a..7210c2e2312 100644 --- a/internal/compiler/tests/syntax/basic/interfaces.slint +++ b/internal/compiler/tests/syntax/basic/interfaces.slint @@ -9,14 +9,14 @@ export interface DeprecatedInterface := { // ^warning{':=' to declare an interface is deprecated. Remove the ':='} } -export interface LineEditInterface { +interface LineEditInterface { in-out property text; } export component LineEditBase implements LineEditInterface { text <=> text-input.text; color <=> text-input.color; -// ^error{Unknown property color in LineEditInterface} +// ^error{Unknown property color} text-input := TextInput { } } diff --git a/tests/cases/interfaces/implements.slint b/tests/cases/interfaces/implements.slint new file mode 100644 index 00000000000..203573c70e6 --- /dev/null +++ b/tests/cases/interfaces/implements.slint @@ -0,0 +1,32 @@ +// Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +interface TextInterface { + in-out property text; +} + +export component TestCase implements TextInterface { + text <=> text-input.text; + + text-input := TextInput { + text: "Hello TextInterface"; + } +} + +/* +```rust +let instance = TestCase::new().unwrap(); +assert_eq!(instance.get_text(), "Hello TextInterface"); +``` + +```cpp +auto handle = TestCase::create(); +const TestCase &instance = *handle; +assert_eq(instance.get_text(), "Hello TextInterface"); +``` + +```js +var instance = new slint.TestCase({}); +assert(instance.text == "Hello TextInterface"); +``` +*/ From 4149a45ce6587a2375c149e7c300fed7c6681873 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Wed, 26 Nov 2025 16:55:11 +0000 Subject: [PATCH 08/28] Move existing interface syntax texts to syntax/interfaces Group interface related tests in a subdirectory of their own. --- .../syntax/{basic => interfaces}/interface_inheritance.slint | 0 .../syntax/{basic => interfaces}/interface_invalid_content.slint | 0 .../tests/syntax/{basic => interfaces}/interface_properties.slint | 0 .../compiler/tests/syntax/{basic => interfaces}/interfaces.slint | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename internal/compiler/tests/syntax/{basic => interfaces}/interface_inheritance.slint (100%) rename internal/compiler/tests/syntax/{basic => interfaces}/interface_invalid_content.slint (100%) rename internal/compiler/tests/syntax/{basic => interfaces}/interface_properties.slint (100%) rename internal/compiler/tests/syntax/{basic => interfaces}/interfaces.slint (100%) diff --git a/internal/compiler/tests/syntax/basic/interface_inheritance.slint b/internal/compiler/tests/syntax/interfaces/interface_inheritance.slint similarity index 100% rename from internal/compiler/tests/syntax/basic/interface_inheritance.slint rename to internal/compiler/tests/syntax/interfaces/interface_inheritance.slint diff --git a/internal/compiler/tests/syntax/basic/interface_invalid_content.slint b/internal/compiler/tests/syntax/interfaces/interface_invalid_content.slint similarity index 100% rename from internal/compiler/tests/syntax/basic/interface_invalid_content.slint rename to internal/compiler/tests/syntax/interfaces/interface_invalid_content.slint diff --git a/internal/compiler/tests/syntax/basic/interface_properties.slint b/internal/compiler/tests/syntax/interfaces/interface_properties.slint similarity index 100% rename from internal/compiler/tests/syntax/basic/interface_properties.slint rename to internal/compiler/tests/syntax/interfaces/interface_properties.slint diff --git a/internal/compiler/tests/syntax/basic/interfaces.slint b/internal/compiler/tests/syntax/interfaces/interfaces.slint similarity index 100% rename from internal/compiler/tests/syntax/basic/interfaces.slint rename to internal/compiler/tests/syntax/interfaces/interfaces.slint From 11fd152acc5ab34584ca0d31387142903c5ba7b5 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Wed, 26 Nov 2025 16:51:47 +0000 Subject: [PATCH 09/28] compiler: Add uses specified as valid new component syntax Allow a new component to specify that it exposes one or more interfaces via a given child element. This commit introduces the syntax, we will add the implementation in a future commit. --- internal/compiler/parser.rs | 7 +- internal/compiler/parser/document.rs | 83 +++++++++++++++++++ .../syntax/interfaces/interface_uses1.slint | 12 +++ .../syntax/interfaces/interface_uses2.slint | 12 +++ .../syntax/interfaces/interface_uses3.slint | 12 +++ .../tests/syntax/interfaces/interfaces.slint | 2 + 6 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 internal/compiler/tests/syntax/interfaces/interface_uses1.slint create mode 100644 internal/compiler/tests/syntax/interfaces/interface_uses2.slint create mode 100644 internal/compiler/tests/syntax/interfaces/interface_uses3.slint diff --git a/internal/compiler/parser.rs b/internal/compiler/parser.rs index 264ce4802f4..c3f87e42822 100644 --- a/internal/compiler/parser.rs +++ b/internal/compiler/parser.rs @@ -332,7 +332,7 @@ declare_syntax! { { Document -> [ *Component, *ExportsList, *ImportSpecifier, *StructDeclaration, *EnumDeclaration ], /// `DeclaredIdentifier := Element { ... }` - Component -> [ DeclaredIdentifier, Element ], + Component -> [ DeclaredIdentifier, ?UsesSpecifier, Element ], /// `id := Element { ... }` SubElement -> [ Element ], Element -> [ ?QualifiedName, *PropertyDeclaration, *Binding, *CallbackConnection, @@ -450,6 +450,11 @@ declare_syntax! { EnumValue -> [], /// `@rust-attr(...)` AtRustAttr -> [], + /// `uses { Foo from Bar, Baz from Qux }` + UsesSpecifier -> [ UsesIdenfifierList ], + UsesIdenfifierList -> [ *UsesIdentifier ], + /// `Interface.Foo from bar` + UsesIdentifier -> [QualifiedName, DeclaredIdentifier], } } diff --git a/internal/compiler/parser/document.rs b/internal/compiler/parser/document.rs index 5c30f513ce0..a03aa25bc29 100644 --- a/internal/compiler/parser/document.rs +++ b/internal/compiler/parser/document.rs @@ -101,6 +101,12 @@ pub fn parse_document(p: &mut impl Parser) -> bool { /// component C inherits D { } /// interface I { property xx; } /// component E implements I { } +/// component F uses { I from A } { } +/// component F uses { I from A } implements J { } +/// component F uses { I from A } inherits B { } +/// component F uses { I from A, J from B } { } +/// component F uses { I from A, J from B } implements J { } +/// component F uses { I from A, J from B } inherits C { } /// ``` pub fn parse_component(p: &mut impl Parser) -> bool { let simple_component = p.nth(1).kind() == SyntaxKind::ColonEqual; @@ -121,6 +127,18 @@ pub fn parse_component(p: &mut impl Parser) -> bool { drop(p.start_node(SyntaxKind::Element)); return false; } + if p.peek().as_str() == "uses" { + if !is_new_component { + p.error("Only components can have 'uses' clauses"); + drop(p.start_node(SyntaxKind::Element)); + return false; + } + + if !parse_uses_specifier(&mut *p) { + drop(p.start_node(SyntaxKind::Element)); + return false; + } + } if is_global || is_interface { if p.peek().kind() == SyntaxKind::ColonEqual { let description = if is_global { "a global" } else { "an interface" }; @@ -365,3 +383,68 @@ fn parse_import_identifier(p: &mut impl Parser) -> bool { } true } + +#[cfg_attr(test, parser_test)] +/// ```test,UsesSpecifier +/// uses { Interface from child } +/// uses { Interface1 from child1, Interface2 from child2 } +/// uses { Interface3 from child3, Qualified.Interface from child4 } +/// ``` +fn parse_uses_specifier(p: &mut impl Parser) -> bool { + debug_assert_eq!(p.peek().as_str(), "uses"); + let mut p = p.start_node(SyntaxKind::UsesSpecifier); + p.expect(SyntaxKind::Identifier); // "uses" + parse_uses_identifier_list(&mut *p) +} + +#[cfg_attr(test, parser_test)] +/// ```test,UsesIdenfifierList +/// { Interface1 from child1} +/// { Interface2 from child2, } +/// { Iterface3 from child3, Interface4 from child4 } +/// { Iterface5 from child5, Interface6 from child6, } +/// { Qualified.Interface1 from child7 } +/// { Qualified.Interface2 from child8, } +/// { Qualified.Interface3 from child9, Interface7 from child10 } +/// { Interface8 from child11, Qualified.Interface4 from child12 } +/// ``` +fn parse_uses_identifier_list(p: &mut impl Parser) -> bool { + let mut p = p.start_node(SyntaxKind::UsesIdenfifierList); + if !p.expect(SyntaxKind::LBrace) { + return false; + } + loop { + if p.test(SyntaxKind::RBrace) { + return true; + } + if !parse_uses_identifier(&mut *p) { + return false; + } + if !p.test(SyntaxKind::Comma) && p.nth(0).kind() != SyntaxKind::RBrace { + p.error("Expected comma or brace"); + return false; + } + } +} + +#[cfg_attr(test, parser_test)] +/// ```test,UsesIdentifier +/// Interface from child +/// Fully.Qualified.Interface from child-component +/// ``` +fn parse_uses_identifier(p: &mut impl Parser) -> bool { + let mut p = p.start_node(SyntaxKind::UsesIdentifier); + + if !parse_qualified_name(&mut *p) { + return false; + } + + if !(p.nth(0).kind() == SyntaxKind::Identifier && p.peek().as_str() == "from") { + p.error("Expected 'from' keyword in uses specifier"); + return false; + } + p.consume(); + + let mut p = p.start_node(SyntaxKind::DeclaredIdentifier); + p.expect(SyntaxKind::Identifier) +} diff --git a/internal/compiler/tests/syntax/interfaces/interface_uses1.slint b/internal/compiler/tests/syntax/interfaces/interface_uses1.slint new file mode 100644 index 00000000000..2413d52434c --- /dev/null +++ b/internal/compiler/tests/syntax/interfaces/interface_uses1.slint @@ -0,0 +1,12 @@ +// Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + + +interface InterfaceI { + out property test; +} + +component Base implements InterfaceI {} + +export component InvalidUses uses { InterfaceI } {} +// ^error{Expected 'from' keyword in uses specifier} diff --git a/internal/compiler/tests/syntax/interfaces/interface_uses2.slint b/internal/compiler/tests/syntax/interfaces/interface_uses2.slint new file mode 100644 index 00000000000..79faf8a4a67 --- /dev/null +++ b/internal/compiler/tests/syntax/interfaces/interface_uses2.slint @@ -0,0 +1,12 @@ +// Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + + +interface InterfaceI { + out property test; +} + +component Base implements InterfaceI {} + +export component AnotherInvalidUses uses { from Base } {} +// ^error{Expected 'from' keyword in uses specifier} diff --git a/internal/compiler/tests/syntax/interfaces/interface_uses3.slint b/internal/compiler/tests/syntax/interfaces/interface_uses3.slint new file mode 100644 index 00000000000..0ce5176f68a --- /dev/null +++ b/internal/compiler/tests/syntax/interfaces/interface_uses3.slint @@ -0,0 +1,12 @@ +// Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + + +interface InterfaceI { + out property test; +} + +component Base implements InterfaceI {} + +export component InvalidUses uses { InterfaceI from, } {} +// ^error{Syntax error: expected Identifier} diff --git a/internal/compiler/tests/syntax/interfaces/interfaces.slint b/internal/compiler/tests/syntax/interfaces/interfaces.slint index 7210c2e2312..dab6b3d33f8 100644 --- a/internal/compiler/tests/syntax/interfaces/interfaces.slint +++ b/internal/compiler/tests/syntax/interfaces/interfaces.slint @@ -20,6 +20,8 @@ export component LineEditBase implements LineEditInterface { text-input := TextInput { } } +export component MyLineEdit uses { LineEditInterface from base } {} + export component Foo implements Bar {} // ^error{Unknown element 'Bar'} From c862c7484484dc53e0d1770fa05a48f7ccd283fc Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 28 Nov 2025 10:35:02 +0000 Subject: [PATCH 10/28] compiler: uses statements expose interface properties from child For each uses statement, iterate through the interface properties and create two-way bindings to the property on the specified base component. The error cases will be populated in follow-up commit. For now, demonstrate that the initial concept works. --- internal/compiler/object_tree.rs | 105 ++++++++++++++++++ .../tests/syntax/interfaces/interfaces.slint | 4 +- tests/cases/interfaces/uses.slint | 45 ++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 tests/cases/interfaces/uses.slint diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index abf282fdda4..83ca7721909 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -347,6 +347,59 @@ pub struct UsedSubTypes { pub library_global_imports: Vec<(SmolStr, LibraryInfo)>, } +/// A parsed [syntax_nodes::UsesIdentifier]. +#[derive(Clone, Debug)] +struct UsesStatement { + interface_name: QualifiedTypeName, + interface_name_node: syntax_nodes::QualifiedName, + child_id: SmolStr, + child_id_node: syntax_nodes::DeclaredIdentifier, +} + +impl UsesStatement { + /// Lookup the interface component for this uses statement. Emits an error if the iterface could not be found, or + /// was not actually an interface. + fn lookup_interface( + &self, + tr: &TypeRegister, + _diag: &mut BuildDiagnostics, + ) -> Result, ()> { + let interface_name = self.interface_name.to_smolstr(); + match tr.lookup_element(&interface_name) { + Ok(element_type) => match element_type { + ElementType::Component(component) => { + if !component.is_interface() { + todo!(); + } + + Ok(component) + } + _ => todo!(), + }, + Err(_) => todo!(), + } + } +} + +impl TryFrom<&syntax_nodes::UsesIdentifier> for UsesStatement { + type Error = (); + fn try_from( + node: &syntax_nodes::UsesIdentifier, + ) -> Result>::Error> { + let interface_name_node = node.child_node(SyntaxKind::QualifiedName).ok_or(())?; + let interface_name = QualifiedTypeName::from_node(interface_name_node.clone().into()); + let child_id_node = node.child_node(SyntaxKind::DeclaredIdentifier).ok_or(())?; + let child_id = parser::identifier_text(&child_id_node).ok_or(())?; + + Ok(UsesStatement { + interface_name, + interface_name_node: interface_name_node.into(), + child_id, + child_id_node: child_id_node.into(), + }) + } +} + #[derive(Debug, Default, Clone)] pub struct InitCode { // Code from init callbacks collected from elements @@ -461,6 +514,7 @@ impl Component { *qualified_id = format_smolstr!("{}::{}", c.id, qualified_id); } }); + apply_uses_statement(&c.root_element, node.UsesSpecifier(), tr, diag); c } @@ -2063,6 +2117,57 @@ fn apply_default_type_properties(element: &mut Element) { } } +fn apply_uses_statement( + e: &ElementRc, + uses_specifier: Option, + tr: &TypeRegister, + diag: &mut BuildDiagnostics, +) { + let Some(uses_specifier) = uses_specifier else { + return; + }; + + for uses_identifier_node in uses_specifier.UsesIdenfifierList().UsesIdentifier() { + let Ok(uses_statement): Result = (&uses_identifier_node).try_into() + else { + // We should already have reported a syntax error + continue; + }; + + let Ok(interface) = uses_statement.lookup_interface(tr, diag) else { + todo!(); + }; + + let Some(child) = find_element_by_id(e, &uses_statement.child_id) else { + todo!(); + }; + + for (prop_name, prop_decl) in &interface.root_element.borrow().property_declarations { + if e.borrow_mut() + .property_declarations + .insert(prop_name.clone(), prop_decl.clone()) + .is_some() + { + todo!(); + } + + if e.borrow_mut() + .bindings + .insert( + prop_name.clone(), + BindingExpression::new_two_way( + NamedReference::new(&child, prop_name.clone()).into(), + ) + .into(), + ) + .is_some() + { + todo!(); + } + } + } +} + /// Create a Type for this node pub fn type_from_node( node: syntax_nodes::Type, diff --git a/internal/compiler/tests/syntax/interfaces/interfaces.slint b/internal/compiler/tests/syntax/interfaces/interfaces.slint index dab6b3d33f8..97847547d04 100644 --- a/internal/compiler/tests/syntax/interfaces/interfaces.slint +++ b/internal/compiler/tests/syntax/interfaces/interfaces.slint @@ -20,7 +20,9 @@ export component LineEditBase implements LineEditInterface { text-input := TextInput { } } -export component MyLineEdit uses { LineEditInterface from base } {} +export component MyLineEdit uses { LineEditInterface from base } { + base := LineEditBase {} +} export component Foo implements Bar {} // ^error{Unknown element 'Bar'} diff --git a/tests/cases/interfaces/uses.slint b/tests/cases/interfaces/uses.slint new file mode 100644 index 00000000000..a07758babd6 --- /dev/null +++ b/tests/cases/interfaces/uses.slint @@ -0,0 +1,45 @@ +// Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +interface TestInterface { + in-out property test: false; +} + +component TestBase implements TestInterface { + test: true; +} + +component TestA uses { TestInterface from base } { + base := TestBase {} +} + + +component TestB uses { TestInterface from base } { + base := TestBase {} +} + +export component TestCase { + + test-a := TestA {} + test-b := TestB {} + + out property test: test-a.test && test-b.test; +} + +/* +```rust +let instance = TestCase::new().unwrap(); +assert!(instance.get_test()); +``` + +```cpp +auto handle = TestCase::create(); +const TestCase &instance = *handle; +assert(instance.get_test()); +``` + +```js +var instance = new slint.TestCase({}); +assert(instance.test); +``` +*/ From 9c3cef26d323cd77902560cd12ee0014bc1fb867 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 28 Nov 2025 11:07:06 +0000 Subject: [PATCH 11/28] compiler: emit errors for invalid interfaces in uses statements Detect and emit errors where: - the type specified in a uses statement is not an interface; - the type specified in a uses statement is unknown; - the type specified in a uses statement cannot be used in this context. --- internal/compiler/object_tree.rs | 23 +++++++--- .../tests/syntax/interfaces/uses_errors.slint | 43 +++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 internal/compiler/tests/syntax/interfaces/uses_errors.slint diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index 83ca7721909..6b1e6b66821 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -362,21 +362,34 @@ impl UsesStatement { fn lookup_interface( &self, tr: &TypeRegister, - _diag: &mut BuildDiagnostics, + diag: &mut BuildDiagnostics, ) -> Result, ()> { let interface_name = self.interface_name.to_smolstr(); match tr.lookup_element(&interface_name) { Ok(element_type) => match element_type { ElementType::Component(component) => { if !component.is_interface() { - todo!(); + diag.push_error( + format!("'{}' is not an interface", self.interface_name), + &self.interface_name_node, + ); + return Err(()); } Ok(component) } - _ => todo!(), + _ => { + diag.push_error( + format!("'{}' is not an interface", self.interface_name), + &self.interface_name_node, + ); + Err(()) + } }, - Err(_) => todo!(), + Err(error) => { + diag.push_error(error, &self.interface_name_node); + Err(()) + } } } } @@ -2135,7 +2148,7 @@ fn apply_uses_statement( }; let Ok(interface) = uses_statement.lookup_interface(tr, diag) else { - todo!(); + continue; }; let Some(child) = find_element_by_id(e, &uses_statement.child_id) else { diff --git a/internal/compiler/tests/syntax/interfaces/uses_errors.slint b/internal/compiler/tests/syntax/interfaces/uses_errors.slint new file mode 100644 index 00000000000..14b826ab6ea --- /dev/null +++ b/internal/compiler/tests/syntax/interfaces/uses_errors.slint @@ -0,0 +1,43 @@ +// Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +export component UsesBuiltin + uses { Rectangle from base } { +// ^error{'Rectangle' is not an interface} + base := Rectangle {} +} + +component ComponentA {} + +export component UsesComponent + uses { ComponentA from base } { +// ^error{'ComponentA' is not an interface} + base := ComponentA {} +} + +export component UsesMissing + uses { MissingInterface from base } { +// ^error{Unknown element 'MissingInterface'} + base := ComponentA {} +} + +export component UsesRow + uses { Row from base } { +// ^error{Row can only be within a GridLayout element} + base := ComponentA {} +} + +export component UsesRowInGridLayout + uses { Row from base } inherits GridLayout { +// ^error{Row can only be within a GridLayout element} + base := ComponentA {} +} + +export component MultipleErrors + uses { MissingInterface from base, Rectangle from base, ComponentA from base, Row from base } { +// ^error{Unknown element 'MissingInterface'} +// ^^error{'Rectangle' is not an interface} +// ^^^error{'ComponentA' is not an interface} +// ^^^^error{Row can only be within a GridLayout element} + base := ComponentA {} +} From 25d5c30a7413080a352351f40e35c70c6f0ed9cb Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 28 Nov 2025 11:13:42 +0000 Subject: [PATCH 12/28] compiler: emit an error when the child specified in a uses statement does not exist Add a test case to verify the expected error message. --- internal/compiler/object_tree.rs | 6 +++++- .../compiler/tests/syntax/interfaces/uses_errors.slint | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index 6b1e6b66821..e10483b25fe 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -2152,7 +2152,11 @@ fn apply_uses_statement( }; let Some(child) = find_element_by_id(e, &uses_statement.child_id) else { - todo!(); + diag.push_error( + format!("'{}' does not exist", uses_statement.child_id), + &uses_statement.child_id_node, + ); + continue; }; for (prop_name, prop_decl) in &interface.root_element.borrow().property_declarations { diff --git a/internal/compiler/tests/syntax/interfaces/uses_errors.slint b/internal/compiler/tests/syntax/interfaces/uses_errors.slint index 14b826ab6ea..99ced4b00bd 100644 --- a/internal/compiler/tests/syntax/interfaces/uses_errors.slint +++ b/internal/compiler/tests/syntax/interfaces/uses_errors.slint @@ -41,3 +41,11 @@ export component MultipleErrors // ^^^^error{Row can only be within a GridLayout element} base := ComponentA {} } + +export interface ValidInterface { + in-out property value; +} + +export component MissingChild + uses { ValidInterface from base} {} +// ^error{'base' does not exist} From 54d3e1888c3ec09dadc5fbde1e237ddd3fdc01b2 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 28 Nov 2025 12:22:21 +0000 Subject: [PATCH 13/28] compiler: verify that external interfaces can be implemented Add a test to verify that interfaces from imported modules can be used. --- tests/cases/imports/external_interfaces.slint | 37 +++++++++++++++++++ .../helper_components/export_interfaces.slint | 6 +++ 2 files changed, 43 insertions(+) create mode 100644 tests/cases/imports/external_interfaces.slint create mode 100644 tests/helper_components/export_interfaces.slint diff --git a/tests/cases/imports/external_interfaces.slint b/tests/cases/imports/external_interfaces.slint new file mode 100644 index 00000000000..f365981ab97 --- /dev/null +++ b/tests/cases/imports/external_interfaces.slint @@ -0,0 +1,37 @@ +// Copyright © SixtyFPS GmbH , Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +// Python is ignored because include paths aren't forwarded in the generated stubs. +//ignore: pyi + +//include_path: ../../helper_components +import { ExportedInterface } from "export_interfaces.slint"; + +component Base + implements ExportedInterface { + value: 2.71; +} + +export component TestCase uses { ExportedInterface from base } { + base := Base {} + + out property test: self.value == 2.71; +} + +/* +```rust +let instance = TestCase::new().unwrap(); +assert!(instance.get_test()); +``` + +```cpp +auto handle = TestCase::create(); +const TestCase &instance = *handle; +assert(instance.get_test()); +``` + +```js +var instance = new slint.TestCase({}); +assert(instance.test); +``` +*/ diff --git a/tests/helper_components/export_interfaces.slint b/tests/helper_components/export_interfaces.slint new file mode 100644 index 00000000000..f9ca44ac06f --- /dev/null +++ b/tests/helper_components/export_interfaces.slint @@ -0,0 +1,6 @@ +// Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +export interface ExportedInterface { + in-out property value: 3.14; +} From 435739fb5135b7e7ef4d711199f398e2f896ca7d Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 28 Nov 2025 12:39:07 +0000 Subject: [PATCH 14/28] compiler: verify that renamed components and interfaces can be used Add a test case to verify that: - we can import an interface as another name and re-use it; - we can import a component that implements a renamed interface and use it. --- .../imports/external_interfaces_as.slint | 36 +++++++++++++++++++ .../helper_components/export_interfaces.slint | 4 +++ 2 files changed, 40 insertions(+) create mode 100644 tests/cases/imports/external_interfaces_as.slint diff --git a/tests/cases/imports/external_interfaces_as.slint b/tests/cases/imports/external_interfaces_as.slint new file mode 100644 index 00000000000..a4c79bea67f --- /dev/null +++ b/tests/cases/imports/external_interfaces_as.slint @@ -0,0 +1,36 @@ +// Copyright © SixtyFPS GmbH , Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +// Python is ignored because include paths aren't forwarded in the generated stubs. +//ignore: pyi + +//include_path: ../../helper_components +import { + ExportedBase as Base, + ExportedInterface as Interface, +} from "export_interfaces.slint"; + +export component TestCase + uses { Interface from base } { + base := Base {} + + out property test: self.value == 2.71; +} + +/* +```rust +let instance = TestCase::new().unwrap(); +assert!(instance.get_test()); +``` + +```cpp +auto handle = TestCase::create(); +const TestCase &instance = *handle; +assert(instance.get_test()); +``` + +```js +var instance = new slint.TestCase({}); +assert(instance.test); +``` +*/ diff --git a/tests/helper_components/export_interfaces.slint b/tests/helper_components/export_interfaces.slint index f9ca44ac06f..ac35752be8c 100644 --- a/tests/helper_components/export_interfaces.slint +++ b/tests/helper_components/export_interfaces.slint @@ -4,3 +4,7 @@ export interface ExportedInterface { in-out property value: 3.14; } + +export component ExportedBase implements ExportedInterface { + value: 2.71; +} From 16d15ab9db4b782779efae44d9130d2595efdf84 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 28 Nov 2025 15:53:21 +0000 Subject: [PATCH 15/28] compiler: verify that the child component implements the interface For each property in an interface that a child element is expected to implement, verify that the child has a property with the same name, type and visibility. This is easier than attempting to store and match interface names which may change if the user renames an interface when importing. --- internal/compiler/object_tree.rs | 35 +++++++++++++++++++ .../tests/syntax/interfaces/uses_errors.slint | 6 ++++ 2 files changed, 41 insertions(+) diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index e10483b25fe..7cb48fbc4d4 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -2159,6 +2159,10 @@ fn apply_uses_statement( continue; }; + if !element_implements_interface(&child, &interface, &uses_statement, diag) { + continue; + } + for (prop_name, prop_decl) in &interface.root_element.borrow().property_declarations { if e.borrow_mut() .property_declarations @@ -2185,6 +2189,37 @@ fn apply_uses_statement( } } +/// Check that the given element implements the given interface. Emits a diagnostic if the interface is not implemented. +fn element_implements_interface( + element: &ElementRc, + interface: &Rc, + uses_statement: &UsesStatement, + diag: &mut BuildDiagnostics, +) -> bool { + let property_matches_interface = + |property: &PropertyLookupResult, interface_declaration: &PropertyDeclaration| -> bool { + property.property_type == interface_declaration.property_type + && property.property_visibility == interface_declaration.visibility + }; + + for (property_name, property_declaration) in + interface.root_element.borrow().property_declarations.iter() + { + let lookup_result = element.borrow().lookup_property(property_name); + if !property_matches_interface(&lookup_result, property_declaration) { + diag.push_error( + format!( + "{} does not implement {}", + uses_statement.child_id, uses_statement.interface_name + ), + &uses_statement.child_id_node, + ); + return false; + } + } + true +} + /// Create a Type for this node pub fn type_from_node( node: syntax_nodes::Type, diff --git a/internal/compiler/tests/syntax/interfaces/uses_errors.slint b/internal/compiler/tests/syntax/interfaces/uses_errors.slint index 99ced4b00bd..223b84d9ab6 100644 --- a/internal/compiler/tests/syntax/interfaces/uses_errors.slint +++ b/internal/compiler/tests/syntax/interfaces/uses_errors.slint @@ -49,3 +49,9 @@ export interface ValidInterface { export component MissingChild uses { ValidInterface from base} {} // ^error{'base' does not exist} + +export component ChildDoesNotImplementInterface + uses { ValidInterface from base } { +// ^error{base does not implement ValidInterface} + base := ComponentA {} +} From e889d05a227d6c80c59be39bb5cd5dfa9360debe Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 28 Nov 2025 16:57:32 +0000 Subject: [PATCH 16/28] compiler: do not allow interface properties to be overridden Emit a compile error if the user attempts to declare a property that would override a property from an interface. --- internal/compiler/object_tree.rs | 25 +++++++++++++++---- .../tests/syntax/interfaces/uses_errors.slint | 9 +++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index 7cb48fbc4d4..4f8c093ac88 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -2164,12 +2164,27 @@ fn apply_uses_statement( } for (prop_name, prop_decl) in &interface.root_element.borrow().property_declarations { - if e.borrow_mut() - .property_declarations - .insert(prop_name.clone(), prop_decl.clone()) - .is_some() + if let Some(existing_property) = + e.borrow_mut().property_declarations.insert(prop_name.clone(), prop_decl.clone()) { - todo!(); + let source = existing_property + .node + .as_ref() + .and_then(|node| node.child_node(SyntaxKind::DeclaredIdentifier)) + .and_then(|node| node.child_token(SyntaxKind::Identifier)) + .map_or_else( + || parser::NodeOrToken::Node(uses_statement.child_id_node.clone().into()), + |token| parser::NodeOrToken::Token(token), + ); + + diag.push_error( + format!( + "Cannot override property '{}' from '{}'", + prop_name, uses_statement.interface_name + ), + &source, + ); + continue; } if e.borrow_mut() diff --git a/internal/compiler/tests/syntax/interfaces/uses_errors.slint b/internal/compiler/tests/syntax/interfaces/uses_errors.slint index 223b84d9ab6..589c4de587f 100644 --- a/internal/compiler/tests/syntax/interfaces/uses_errors.slint +++ b/internal/compiler/tests/syntax/interfaces/uses_errors.slint @@ -55,3 +55,12 @@ export component ChildDoesNotImplementInterface // ^error{base does not implement ValidInterface} base := ComponentA {} } + +component ValidBase implements ValidInterface {} + +export component DuplicatePropertyName + uses { ValidInterface from base } { + base := ValidBase {} + in-out property value; +// ^error{Cannot override property 'value' from 'ValidInterface'} +} From 735346b7b03bbc0a4c17cad6760a1adc3463ef02 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 28 Nov 2025 17:13:09 +0000 Subject: [PATCH 17/28] compiler: do not permit interfaces to be used if they would override an inherited property Look up the interface property on the base type before allowing a component to implement the interface. --- internal/compiler/object_tree.rs | 12 ++++++++++++ .../tests/syntax/interfaces/uses_errors.slint | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index 4f8c093ac88..d66ea2e34f5 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -2164,6 +2164,18 @@ fn apply_uses_statement( } for (prop_name, prop_decl) in &interface.root_element.borrow().property_declarations { + let lookup_result = e.borrow().base_type.lookup_property(prop_name); + if lookup_result.is_valid() { + diag.push_error( + format!( + "Cannot use interface '{}' because property '{}' conflicts with existing property in '{}'", + uses_statement.interface_name, prop_name, e.borrow().base_type + ), + &uses_statement.interface_name_node, + ); + continue; + } + if let Some(existing_property) = e.borrow_mut().property_declarations.insert(prop_name.clone(), prop_decl.clone()) { diff --git a/internal/compiler/tests/syntax/interfaces/uses_errors.slint b/internal/compiler/tests/syntax/interfaces/uses_errors.slint index 589c4de587f..a65219633ea 100644 --- a/internal/compiler/tests/syntax/interfaces/uses_errors.slint +++ b/internal/compiler/tests/syntax/interfaces/uses_errors.slint @@ -64,3 +64,13 @@ export component DuplicatePropertyName in-out property value; // ^error{Cannot override property 'value' from 'ValidInterface'} } + +component BaseWithProperty { + in-out property value: 10; + @children +} + +export component DuplicatPropertyOnBase uses { ValidInterface from base } inherits BaseWithProperty { +// ^error{Cannot use interface 'ValidInterface' because property 'value' conflicts with existing property in 'BaseWithProperty'} + base := ValidBase {} +} From d0aa690bfebf2d943be174e093da57eb1c16dd12 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 28 Nov 2025 17:44:49 +0000 Subject: [PATCH 18/28] compiler: emit an error if a uses statement attempts to override an existing binding I couldn't figure out a good way to test this - everything I came up with got caught by the prior check that the property does not already exist. --- internal/compiler/object_tree.rs | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index d66ea2e34f5..9d4d8f3a770 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -2199,18 +2199,24 @@ fn apply_uses_statement( continue; } - if e.borrow_mut() - .bindings - .insert( - prop_name.clone(), - BindingExpression::new_two_way( - NamedReference::new(&child, prop_name.clone()).into(), - ) - .into(), + if let Some(existing_binding) = e.borrow_mut().bindings.insert( + prop_name.clone(), + BindingExpression::new_two_way( + NamedReference::new(&child, prop_name.clone()).into(), ) - .is_some() - { - todo!(); + .into(), + ) { + let message = format!( + "Cannot override binding for property '{}' from interface '{}'", + prop_name, uses_statement.interface_name + ); + if let Some(location) = &existing_binding.borrow().span { + diag.push_error(message, location); + } else { + diag.push_error(message, &uses_statement.interface_name_node); + } + + continue; } } } From f6bedb8b45db832544195899fe585ed155ad79ad Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Sat, 29 Nov 2025 11:54:19 +0000 Subject: [PATCH 19/28] compiler: 'interface', 'implements' and 'uses' are experimental features Guard these keywords behind the experimental features flag. Slint must be compiled with the `SLINT_ENABLE_EXPERIMENTAL_FEATURES` environment variable set to enable these keywords. --- internal/compiler/object_tree.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index 9d4d8f3a770..d4e0e850928 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -506,7 +506,14 @@ impl Component { "root".into(), match node.child_text(SyntaxKind::Identifier) { Some(t) if t == "global" => ElementType::Global, - Some(t) if t == "interface" => ElementType::Interface, + Some(t) if t == "interface" => { + if !diag.enable_experimental { + diag.push_error("'interface' is an experimental feature".into(), &node); + ElementType::Error + } else { + ElementType::Interface + } + } _ => ElementType::Error, }, &mut child_insertion_point, @@ -1121,6 +1128,12 @@ impl Element { &base_node, ); ElementType::Error + } else if !diag.enable_experimental { + diag.push_error( + format!("'implements' is an experimental feature"), + &base_node, + ); + ElementType::Error } else { c.used.set(true); interfaces.push(c); @@ -2140,6 +2153,11 @@ fn apply_uses_statement( return; }; + if !diag.enable_experimental { + diag.push_error("'uses' is an experimental feature".into(), &uses_specifier); + return; + } + for uses_identifier_node in uses_specifier.UsesIdenfifierList().UsesIdentifier() { let Ok(uses_statement): Result = (&uses_identifier_node).try_into() else { From f27c6f4a0249b55090c9a78fd0fa0f6d440f1689 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Wed, 3 Dec 2025 12:05:30 +0000 Subject: [PATCH 20/28] compiler: parse_uses_identifier returns a valid SyntaxKind::UsesIdentifier Previously we were not creating a DeclaredIdentifier if a syntax error was encountered. This would cause a panic when `UsesIdentifier::DeclaredIdentifier()` was called for a node with a syntax error. As a result, we can convert `TryFrom<&syntax_nodes::UsesIdentifier> for UsesStatement` to `From<&syntax_nodes::UsesIdentifier> for UsesStatement`. --- internal/compiler/object_tree.rs | 33 ++++++++++------------------ internal/compiler/parser/document.rs | 2 ++ 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index d4e0e850928..11e171ed872 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -394,22 +394,16 @@ impl UsesStatement { } } -impl TryFrom<&syntax_nodes::UsesIdentifier> for UsesStatement { - type Error = (); - fn try_from( - node: &syntax_nodes::UsesIdentifier, - ) -> Result>::Error> { - let interface_name_node = node.child_node(SyntaxKind::QualifiedName).ok_or(())?; - let interface_name = QualifiedTypeName::from_node(interface_name_node.clone().into()); - let child_id_node = node.child_node(SyntaxKind::DeclaredIdentifier).ok_or(())?; - let child_id = parser::identifier_text(&child_id_node).ok_or(())?; - - Ok(UsesStatement { - interface_name, - interface_name_node: interface_name_node.into(), - child_id, - child_id_node: child_id_node.into(), - }) +impl From<&syntax_nodes::UsesIdentifier> for UsesStatement { + fn from(node: &syntax_nodes::UsesIdentifier) -> UsesStatement { + UsesStatement { + interface_name: QualifiedTypeName::from_node( + node.child_node(SyntaxKind::QualifiedName).unwrap().clone().into(), + ), + interface_name_node: node.QualifiedName(), + child_id: parser::identifier_text(&node.DeclaredIdentifier()).unwrap_or_default(), + child_id_node: node.DeclaredIdentifier(), + } } } @@ -2159,12 +2153,7 @@ fn apply_uses_statement( } for uses_identifier_node in uses_specifier.UsesIdenfifierList().UsesIdentifier() { - let Ok(uses_statement): Result = (&uses_identifier_node).try_into() - else { - // We should already have reported a syntax error - continue; - }; - + let uses_statement: UsesStatement = (&uses_identifier_node).into(); let Ok(interface) = uses_statement.lookup_interface(tr, diag) else { continue; }; diff --git a/internal/compiler/parser/document.rs b/internal/compiler/parser/document.rs index a03aa25bc29..4792d901411 100644 --- a/internal/compiler/parser/document.rs +++ b/internal/compiler/parser/document.rs @@ -436,11 +436,13 @@ fn parse_uses_identifier(p: &mut impl Parser) -> bool { let mut p = p.start_node(SyntaxKind::UsesIdentifier); if !parse_qualified_name(&mut *p) { + drop(p.start_node(SyntaxKind::DeclaredIdentifier)); return false; } if !(p.nth(0).kind() == SyntaxKind::Identifier && p.peek().as_str() == "from") { p.error("Expected 'from' keyword in uses specifier"); + drop(p.start_node(SyntaxKind::DeclaredIdentifier)); return false; } p.consume(); From 7788b9d0cb2730e2dd57e8d87a1a33925cb79bfe Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Wed, 3 Dec 2025 12:21:30 +0000 Subject: [PATCH 21/28] compiler: simplify `UsesSpecifier -> UsesSpecifierList -> UsesIdentifier` to `UsesSpecifier -> UsesIdentifier` We don't need to intermediate list object because we only expect a UsesSpecifier to contain UsesIdentifiers. --- internal/compiler/object_tree.rs | 2 +- internal/compiler/parser.rs | 3 +-- internal/compiler/parser/document.rs | 23 ++++++----------------- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index 11e171ed872..fcecef0ca52 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -2152,7 +2152,7 @@ fn apply_uses_statement( return; } - for uses_identifier_node in uses_specifier.UsesIdenfifierList().UsesIdentifier() { + for uses_identifier_node in uses_specifier.UsesIdentifier() { let uses_statement: UsesStatement = (&uses_identifier_node).into(); let Ok(interface) = uses_statement.lookup_interface(tr, diag) else { continue; diff --git a/internal/compiler/parser.rs b/internal/compiler/parser.rs index c3f87e42822..a38f82ee7f9 100644 --- a/internal/compiler/parser.rs +++ b/internal/compiler/parser.rs @@ -451,8 +451,7 @@ declare_syntax! { /// `@rust-attr(...)` AtRustAttr -> [], /// `uses { Foo from Bar, Baz from Qux }` - UsesSpecifier -> [ UsesIdenfifierList ], - UsesIdenfifierList -> [ *UsesIdentifier ], + UsesSpecifier -> [ *UsesIdentifier ], /// `Interface.Foo from bar` UsesIdentifier -> [QualifiedName, DeclaredIdentifier], } diff --git a/internal/compiler/parser/document.rs b/internal/compiler/parser/document.rs index 4792d901411..10d0ef2b969 100644 --- a/internal/compiler/parser/document.rs +++ b/internal/compiler/parser/document.rs @@ -387,29 +387,18 @@ fn parse_import_identifier(p: &mut impl Parser) -> bool { #[cfg_attr(test, parser_test)] /// ```test,UsesSpecifier /// uses { Interface from child } +/// uses { Interface from child, } /// uses { Interface1 from child1, Interface2 from child2 } -/// uses { Interface3 from child3, Qualified.Interface from child4 } +/// uses { Interface1 from child2, Qualified.Interface from child2 } +/// uses { Qualified.Interface from child } +/// uses { Qualified.Interface from child, } +/// uses { Qualified.Interface from child1, Interface from child2 } +/// uses { Interface from child1, Qualified.Interface from child2 } /// ``` fn parse_uses_specifier(p: &mut impl Parser) -> bool { debug_assert_eq!(p.peek().as_str(), "uses"); let mut p = p.start_node(SyntaxKind::UsesSpecifier); p.expect(SyntaxKind::Identifier); // "uses" - parse_uses_identifier_list(&mut *p) -} - -#[cfg_attr(test, parser_test)] -/// ```test,UsesIdenfifierList -/// { Interface1 from child1} -/// { Interface2 from child2, } -/// { Iterface3 from child3, Interface4 from child4 } -/// { Iterface5 from child5, Interface6 from child6, } -/// { Qualified.Interface1 from child7 } -/// { Qualified.Interface2 from child8, } -/// { Qualified.Interface3 from child9, Interface7 from child10 } -/// { Interface8 from child11, Qualified.Interface4 from child12 } -/// ``` -fn parse_uses_identifier_list(p: &mut impl Parser) -> bool { - let mut p = p.start_node(SyntaxKind::UsesIdenfifierList); if !p.expect(SyntaxKind::LBrace) { return false; } From 848c0a9a9478b2c024d0dd8519a44db4d66a666b Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Thu, 4 Dec 2025 16:10:29 +0000 Subject: [PATCH 22/28] lsp: Ensure that uses statements are not broken Previously the space between the DeclaredIdentifier and the `from` keyword was being removed. E.g.: ```slint component C uses { Foo from bar } { } ``` would become: ```slint component C uses { Foofrom bar } { } ``` This would cause a syntax error. This no longer happens. --- tools/lsp/fmt/fmt.rs | 50 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/tools/lsp/fmt/fmt.rs b/tools/lsp/fmt/fmt.rs index cd39bac8944..277eafb5143 100644 --- a/tools/lsp/fmt/fmt.rs +++ b/tools/lsp/fmt/fmt.rs @@ -165,6 +165,9 @@ fn format_node( SyntaxKind::ImportSpecifier => { return format_import_specifier(node, writer, state); } + SyntaxKind::UsesSpecifier => { + return format_uses_specifier(node, writer, state); + } _ => (), } @@ -299,7 +302,7 @@ fn format_component( && whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, " ")?; let r = whitespace_to_one_of( &mut sub, - &[SyntaxKind::Identifier, SyntaxKind::Element], + &[SyntaxKind::Identifier, SyntaxKind::UsesSpecifier, SyntaxKind::Element], writer, state, " ", @@ -1488,6 +1491,51 @@ fn format_import_identifier( Ok(()) } +/// Formats a uses specifier. +/// +/// Ensures that the QualifiedName and `from` Identifier are separated by a space. +fn format_uses_specifier( + node: &SyntaxNode, + writer: &mut impl TokenWriter, + state: &mut FormatState, +) -> Result<(), std::io::Error> { + let sub = node.children_with_tokens(); + for n in sub { + match n.kind() { + SyntaxKind::Whitespace => { + fold(n, writer, state)?; + } + SyntaxKind::LBrace => { + fold(n, writer, state)?; + } + SyntaxKind::UsesIdentifier => { + if let Some(uses_node) = n.as_node() { + state.skip_all_whitespace = true; + for child in uses_node.children_with_tokens() { + match child.kind() { + SyntaxKind::Identifier => { + state.whitespace_to_add = Some(" ".into()); + fold(child, writer, state)?; + } + _ => { + fold(child, writer, state)?; + } + } + } + } + } + SyntaxKind::RBrace => { + fold(n, writer, state)?; + } + _ => { + state.skip_all_whitespace = true; + fold(n, writer, state)?; + } + } + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; From 017b1a9aeeed3bfd0f3be01b904f65214d2e032f Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 5 Dec 2025 12:16:24 +0000 Subject: [PATCH 23/28] compiler: ':=' to declare an interface is an error Previously this was a warning to match the global behaviour. Given that interfaces are a new feature, make it an error to declare them using the deprecated ':='. --- internal/compiler/parser/document.rs | 10 +++++++--- .../compiler/tests/syntax/interfaces/interfaces.slint | 4 ---- .../syntax/interfaces/interfaces_colon_equal.slint | 6 ++++++ 3 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 internal/compiler/tests/syntax/interfaces/interfaces_colon_equal.slint diff --git a/internal/compiler/parser/document.rs b/internal/compiler/parser/document.rs index 10d0ef2b969..1e37c792cec 100644 --- a/internal/compiler/parser/document.rs +++ b/internal/compiler/parser/document.rs @@ -139,10 +139,14 @@ pub fn parse_component(p: &mut impl Parser) -> bool { return false; } } - if is_global || is_interface { + if is_global { if p.peek().kind() == SyntaxKind::ColonEqual { - let description = if is_global { "a global" } else { "an interface" }; - p.warning(format!("':=' to declare {description} is deprecated. Remove the ':='")); + p.warning(format!("':=' to declare a global is deprecated. Remove the ':='")); + p.consume(); + } + } else if is_interface { + if p.peek().kind() == SyntaxKind::ColonEqual { + p.error(format!("':=' to declare an interface is not supported. Remove the ':='")); p.consume(); } } else if !is_new_component { diff --git a/internal/compiler/tests/syntax/interfaces/interfaces.slint b/internal/compiler/tests/syntax/interfaces/interfaces.slint index 97847547d04..337dc91ad4a 100644 --- a/internal/compiler/tests/syntax/interfaces/interfaces.slint +++ b/internal/compiler/tests/syntax/interfaces/interfaces.slint @@ -5,10 +5,6 @@ export interface HelloInterface { out property message; } -export interface DeprecatedInterface := { -// ^warning{':=' to declare an interface is deprecated. Remove the ':='} -} - interface LineEditInterface { in-out property text; } diff --git a/internal/compiler/tests/syntax/interfaces/interfaces_colon_equal.slint b/internal/compiler/tests/syntax/interfaces/interfaces_colon_equal.slint new file mode 100644 index 00000000000..10a16672210 --- /dev/null +++ b/internal/compiler/tests/syntax/interfaces/interfaces_colon_equal.slint @@ -0,0 +1,6 @@ +// Copyright © 2025 Klarälvdalens Datakonsult AB, a KDAB Group company , author Nathan Collins +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +interface UnsupportedInterface := { +// ^error{':=' to declare an interface is not supported. Remove the ':='} +} From 2c740dcd6c28104ea6185ed26f97129958062840 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 5 Dec 2025 12:22:12 +0000 Subject: [PATCH 24/28] compiler: private properties in an interface are an error It does not make sense to declare an interface with properties that are not accessible to the user of the interface. --- internal/compiler/object_tree.rs | 2 +- .../tests/syntax/interfaces/interface_properties.slint | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index fcecef0ca52..62d03d85f99 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -1290,7 +1290,7 @@ impl Element { }); if base_type == ElementType::Interface && visibility == PropertyVisibility::Private { - diag.push_warning( + diag.push_error( "'private' properties are inaccessible in an interface".into(), &prop_decl, ); diff --git a/internal/compiler/tests/syntax/interfaces/interface_properties.slint b/internal/compiler/tests/syntax/interfaces/interface_properties.slint index e0762e0ee4b..08821d2ab32 100644 --- a/internal/compiler/tests/syntax/interfaces/interface_properties.slint +++ b/internal/compiler/tests/syntax/interfaces/interface_properties.slint @@ -3,10 +3,10 @@ export interface ValidInterfacePropertyDeclarations { property implicit-private; -// ^warning{'private' properties are inaccessible in an interface} +// ^error{'private' properties are inaccessible in an interface} private property private; -// ^warning{'private' properties are inaccessible in an interface} +// ^error{'private' properties are inaccessible in an interface} in property in-property; out property out-property; From 56d9063058416cf51b3e9cf6306756a2f9198941 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 5 Dec 2025 13:47:46 +0000 Subject: [PATCH 25/28] Update insterface syntax tests The syntax for declaring an expected diagnostic has changed to include the span (#9703). Update the interface tests that were added using the old syntax. --- .../interfaces/interface_inheritance.slint | 2 +- .../interface_invalid_content.slint | 17 ++++++------ .../interfaces/interface_properties.slint | 4 +-- .../syntax/interfaces/interface_uses2.slint | 2 +- .../tests/syntax/interfaces/interfaces.slint | 10 +++---- .../interfaces/interfaces_colon_equal.slint | 2 +- .../tests/syntax/interfaces/uses_errors.slint | 26 +++++++++---------- 7 files changed, 32 insertions(+), 31 deletions(-) diff --git a/internal/compiler/tests/syntax/interfaces/interface_inheritance.slint b/internal/compiler/tests/syntax/interfaces/interface_inheritance.slint index c8506dca4ad..d9d30a9bfb8 100644 --- a/internal/compiler/tests/syntax/interfaces/interface_inheritance.slint +++ b/internal/compiler/tests/syntax/interfaces/interface_inheritance.slint @@ -2,5 +2,5 @@ // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 export interface DerivedInterface inherits HelloInterface { -// ^error{Syntax error: expected '{'} +// > i; animate i { duration: 100ms; } -// ^error{An interface cannot have animations} +// > <^error{'transitions' block are no longer supported. Use 'in {...}' and 'out {...}' directly in the state definition} callback init; -// ^error{An interface cannot have an 'init' callback} +// > { debug("nope"); } -// ^error{An interface cannot have an 'init' callback} +// >error{An interface cannot have an 'init' callback} } +//<<< implicit-private; -// ^error{'private' properties are inaccessible in an interface} +// > private; -// ^error{'private' properties are inaccessible in an interface} +// > in-property; out property out-property; diff --git a/internal/compiler/tests/syntax/interfaces/interface_uses2.slint b/internal/compiler/tests/syntax/interfaces/interface_uses2.slint index 79faf8a4a67..d682f7f80ae 100644 --- a/internal/compiler/tests/syntax/interfaces/interface_uses2.slint +++ b/internal/compiler/tests/syntax/interfaces/interface_uses2.slint @@ -9,4 +9,4 @@ interface InterfaceI { component Base implements InterfaceI {} export component AnotherInvalidUses uses { from Base } {} -// ^error{Expected 'from' keyword in uses specifier} +// > text-input.text; color <=> text-input.color; -// ^error{Unknown property color} +// > <^error{'Rectangle' is not an interface} +// > <^^error{'ComponentA' is not an interface} +// > <^^^error{Row can only be within a GridLayout element} base := ComponentA {} } @@ -48,11 +48,11 @@ export interface ValidInterface { export component MissingChild uses { ValidInterface from base} {} -// ^error{'base' does not exist} +// > value; -// ^error{Cannot override property 'value' from 'ValidInterface'} +// > Date: Fri, 5 Dec 2025 15:35:21 +0000 Subject: [PATCH 26/28] compiler: check experimental features before emitting more specific interface errors The `implements` keyword should not be supported if experimental features are not enabled. --- internal/compiler/object_tree.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index 62d03d85f99..660f364cdae 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -1116,15 +1116,15 @@ impl Element { ElementType::Error } (Ok(ElementType::Component(c)), Some(ParentRelationship::Implements)) => { - if !c.is_interface() { + if !diag.enable_experimental { diag.push_error( - format!("Cannot implement {}. It is not an interface", base_string), + format!("'implements' is an experimental feature"), &base_node, ); ElementType::Error - } else if !diag.enable_experimental { + } else if !c.is_interface() { diag.push_error( - format!("'implements' is an experimental feature"), + format!("Cannot implement {}. It is not an interface", base_string), &base_node, ); ElementType::Error @@ -1136,10 +1136,17 @@ impl Element { } } (Ok(ElementType::Builtin(_bt)), Some(ParentRelationship::Implements)) => { - diag.push_error( - format!("Cannot implement {}. It is not an interface", base_string), - &base_node, - ); + if !diag.enable_experimental { + diag.push_error( + format!("'implements' is an experimental feature"), + &base_node, + ); + } else { + diag.push_error( + format!("Cannot implement {}. It is not an interface", base_string), + &base_node, + ); + } ElementType::Error } (Ok(ty), _) => ty, From 5f1c28630a376cbfc5b12335e1020519825d9fdf Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 5 Dec 2025 15:51:46 +0000 Subject: [PATCH 27/28] compiler: Simplify UsesStatement Only store the `syntax_nodes::UsesIdentifier` and replace the old members with functions returning the appropriate child node of the `UsesIdentifier`. --- internal/compiler/object_tree.rs | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index 660f364cdae..04aa9899bed 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -351,12 +351,21 @@ pub struct UsedSubTypes { #[derive(Clone, Debug)] struct UsesStatement { interface_name: QualifiedTypeName, - interface_name_node: syntax_nodes::QualifiedName, child_id: SmolStr, - child_id_node: syntax_nodes::DeclaredIdentifier, + node: syntax_nodes::UsesIdentifier, } impl UsesStatement { + /// Get the node representing the interface name. + fn interface_name_node(&self) -> syntax_nodes::QualifiedName { + self.node.QualifiedName() + } + + /// Get the node representing the child identifier. + fn child_id_node(&self) -> syntax_nodes::DeclaredIdentifier { + self.node.DeclaredIdentifier() + } + /// Lookup the interface component for this uses statement. Emits an error if the iterface could not be found, or /// was not actually an interface. fn lookup_interface( @@ -371,7 +380,7 @@ impl UsesStatement { if !component.is_interface() { diag.push_error( format!("'{}' is not an interface", self.interface_name), - &self.interface_name_node, + &self.interface_name_node(), ); return Err(()); } @@ -381,13 +390,13 @@ impl UsesStatement { _ => { diag.push_error( format!("'{}' is not an interface", self.interface_name), - &self.interface_name_node, + &self.interface_name_node(), ); Err(()) } }, Err(error) => { - diag.push_error(error, &self.interface_name_node); + diag.push_error(error, &self.interface_name_node()); Err(()) } } @@ -400,9 +409,8 @@ impl From<&syntax_nodes::UsesIdentifier> for UsesStatement { interface_name: QualifiedTypeName::from_node( node.child_node(SyntaxKind::QualifiedName).unwrap().clone().into(), ), - interface_name_node: node.QualifiedName(), child_id: parser::identifier_text(&node.DeclaredIdentifier()).unwrap_or_default(), - child_id_node: node.DeclaredIdentifier(), + node: node.clone(), } } } @@ -2168,7 +2176,7 @@ fn apply_uses_statement( let Some(child) = find_element_by_id(e, &uses_statement.child_id) else { diag.push_error( format!("'{}' does not exist", uses_statement.child_id), - &uses_statement.child_id_node, + &uses_statement.child_id_node(), ); continue; }; @@ -2185,7 +2193,7 @@ fn apply_uses_statement( "Cannot use interface '{}' because property '{}' conflicts with existing property in '{}'", uses_statement.interface_name, prop_name, e.borrow().base_type ), - &uses_statement.interface_name_node, + &uses_statement.interface_name_node(), ); continue; } @@ -2199,7 +2207,7 @@ fn apply_uses_statement( .and_then(|node| node.child_node(SyntaxKind::DeclaredIdentifier)) .and_then(|node| node.child_token(SyntaxKind::Identifier)) .map_or_else( - || parser::NodeOrToken::Node(uses_statement.child_id_node.clone().into()), + || parser::NodeOrToken::Node(uses_statement.child_id_node().into()), |token| parser::NodeOrToken::Token(token), ); @@ -2227,7 +2235,7 @@ fn apply_uses_statement( if let Some(location) = &existing_binding.borrow().span { diag.push_error(message, location); } else { - diag.push_error(message, &uses_statement.interface_name_node); + diag.push_error(message, &uses_statement.interface_name_node()); } continue; @@ -2259,7 +2267,7 @@ fn element_implements_interface( "{} does not implement {}", uses_statement.child_id, uses_statement.interface_name ), - &uses_statement.child_id_node, + &uses_statement.child_id_node(), ); return false; } From 01df9832e3f577f8ed7c3db812b67ba70b8b75a8 Mon Sep 17 00:00:00 2001 From: Nathan Collins Date: Fri, 5 Dec 2025 16:00:47 +0000 Subject: [PATCH 28/28] compiler: clean up ElementType::Interface TODO comment We cannot instantiate an interface element, so we should not be able to reach one by recursing into the children of a component. --- internal/compiler/passes/resolve_native_classes.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/compiler/passes/resolve_native_classes.rs b/internal/compiler/passes/resolve_native_classes.rs index 285c130f619..13551e77e4f 100644 --- a/internal/compiler/passes/resolve_native_classes.rs +++ b/internal/compiler/passes/resolve_native_classes.rs @@ -26,11 +26,7 @@ pub fn resolve_native_classes(component: &Component) { // already native return; } - ElementType::Interface => { - // TODO: I don't think we should be here - but it is too early to tell. Don't panic though. - return; - } - ElementType::Global | ElementType::Error => { + ElementType::Interface | ElementType::Global | ElementType::Error => { panic!("This should not happen") } };