input output property: fix semantic with two ways bindings

This commit is contained in:
Olivier Goffart 2022-11-10 15:21:13 +01:00 committed by Olivier Goffart
parent 15d9a2665b
commit 6ab6ae63d4
10 changed files with 477 additions and 23 deletions

View file

@ -1130,7 +1130,26 @@ impl Expression {
nr.mark_as_set();
let lookup = nr.element().borrow().lookup_property(nr.name());
if lookup.is_valid_for_assignment() {
true
if !nr
.element()
.borrow()
.property_analysis
.borrow()
.get(nr.name())
.map_or(false, |d| d.is_linked_to_read_only)
{
true
} else if is_legacy_component {
diag.push_warning("Modifying a property that is linked to a read-only property is deprecated".into(), node);
true
} else {
diag.push_error(
"Cannot modify a property that is linked to a read-only property"
.into(),
node,
);
false
}
} else if is_legacy_component
&& lookup.property_visibility == PropertyVisibility::Output
{

View file

@ -346,7 +346,7 @@ pub struct BuiltinPropertyInfo {
impl BuiltinPropertyInfo {
pub fn new(ty: Type) -> Self {
Self { ty, default_value: None, property_visibility: PropertyVisibility::Private }
Self { ty, default_value: None, property_visibility: PropertyVisibility::InOut }
}
pub fn is_native_output(&self) -> bool {

View file

@ -86,6 +86,8 @@ pub fn load_builtins(register: &mut TypeRegister) {
register,
));
info.property_visibility = PropertyVisibility::Private;
for token in p.children_with_tokens() {
if token.kind() != SyntaxKind::Identifier {
continue;

View file

@ -614,6 +614,9 @@ pub struct PropertyAnalysis {
/// true if this property is read from another component
pub is_read_externally: bool,
/// True if the property is linked to another property that is read only. That property becomes read-only
pub is_linked_to_read_only: bool,
}
impl PropertyAnalysis {
@ -1233,7 +1236,7 @@ impl Element {
&& lookup_result.property_visibility == PropertyVisibility::Output
{
diag.push_warning(
format!("Assigning to out property '{unresolved_name}' is deprecated"),
format!("Assigning to output property '{unresolved_name}' is deprecated"),
&name_token,
);
} else {

View file

@ -30,7 +30,6 @@ fn resolve_expression(
scope: &ComponentScope,
type_register: &TypeRegister,
type_loader: &crate::typeloader::TypeLoader,
two_ways: &mut Vec<(String, NamedReference)>,
diag: &mut BuildDiagnostics,
) {
if let Expression::Uncompiled(node) = expr {
@ -59,14 +58,7 @@ fn resolve_expression(
Expression::from_binding_expression_node(node.clone(), &mut lookup_ctx)
}
SyntaxKind::TwoWayBinding => {
if lookup_ctx.property_type == Type::Invalid {
// An attempt to resolve this already failed when trying to resolve the property type
assert!(diag.has_error());
return;
}
if let Some(nr) = resolve_two_way_binding(node.clone().into(), &mut lookup_ctx) {
two_ways.push((property_name.unwrap().into(), nr));
}
assert!(diag.has_error(), "Two way binding should have been resolved already (property: {property_name:?})");
Expression::Invalid
}
_ => {
@ -83,6 +75,8 @@ pub fn resolve_expressions(
type_loader: &crate::typeloader::TypeLoader,
diag: &mut BuildDiagnostics,
) {
resolve_two_way_bindings(doc, &doc.local_registry, diag);
for component in doc.inner_components.iter() {
let scope = ComponentScope(vec![]);
@ -90,7 +84,6 @@ pub fn resolve_expressions(
let mut new_scope = scope.clone();
let mut is_repeated = elem.borrow().repeated.is_some();
new_scope.0.push(elem.clone());
let mut two_ways = vec![];
visit_element_expressions(elem, |expr, property_name, property_type| {
if is_repeated {
// The first expression is always the model and it needs to be resolved with the parent scope
@ -102,7 +95,6 @@ pub fn resolve_expressions(
&scope,
&doc.local_registry,
type_loader,
&mut two_ways,
diag,
);
is_repeated = false;
@ -114,14 +106,10 @@ pub fn resolve_expressions(
&new_scope,
&doc.local_registry,
type_loader,
&mut two_ways,
diag,
)
}
});
for (prop, nr) in two_ways {
elem.borrow().bindings.get(&prop).unwrap().borrow_mut().two_way_bindings.push(nr);
}
new_scope
})
}
@ -1139,6 +1127,123 @@ fn maybe_lookup_object(
base
}
/// Go through all the two way binding and resolve them first
fn resolve_two_way_bindings(
doc: &Document,
type_register: &TypeRegister,
diag: &mut BuildDiagnostics,
) {
for component in doc.inner_components.iter() {
let scope = ComponentScope(vec![]);
recurse_elem(&component.root_element, &scope, &mut |elem, scope| {
let mut new_scope = scope.clone();
new_scope.0.push(elem.clone());
for (prop_name, binding) in &elem.borrow().bindings {
let mut binding = binding.borrow_mut();
if let Expression::Uncompiled(node) = binding.expression.clone() {
if let Some(n) = syntax_nodes::TwoWayBinding::new(node.clone()) {
let lhs_lookup = elem.borrow().lookup_property(prop_name);
if lhs_lookup.property_type == Type::Invalid {
// An attempt to resolve this already failed when trying to resolve the property type
assert!(diag.has_error());
continue;
}
let mut lookup_ctx = LookupCtx {
property_name: Some(prop_name.as_str()),
property_type: lhs_lookup.property_type.clone(),
component_scope: &new_scope.0,
diag,
arguments: vec![],
type_register,
type_loader: None,
current_token: Some(node.clone().into()),
};
binding.expression = Expression::Invalid;
if let Some(nr) = resolve_two_way_binding(n, &mut lookup_ctx) {
binding.two_way_bindings.push(nr.clone());
// Check the compatibility.
let rhs_lookup = nr.element().borrow().lookup_property(nr.name());
if !rhs_lookup.is_valid_for_assignment() {
match (
lhs_lookup.property_visibility,
rhs_lookup.property_visibility,
) {
(PropertyVisibility::Input, PropertyVisibility::Input)
if !lhs_lookup.is_local_to_component =>
{
assert!(rhs_lookup.is_local_to_component);
marked_linked_read_only(elem, prop_name);
}
(
PropertyVisibility::Output | PropertyVisibility::Private,
PropertyVisibility::Output | PropertyVisibility::Input,
) => {
assert!(lhs_lookup.is_local_to_component);
marked_linked_read_only(elem, prop_name);
}
(PropertyVisibility::Input, PropertyVisibility::Output)
if !lhs_lookup.is_local_to_component =>
{
assert!(!rhs_lookup.is_local_to_component);
marked_linked_read_only(elem, prop_name);
}
_ => {
if lookup_ctx.is_legacy_component() {
diag.push_warning(
format!(
"Link to a {} property is deprecated",
rhs_lookup.property_visibility
),
&node,
);
} else {
diag.push_error(
format!(
"Cannot link to a {} property",
rhs_lookup.property_visibility
),
&node,
)
}
}
}
} else if !lhs_lookup.is_valid_for_assignment() {
if rhs_lookup.is_local_to_component
&& rhs_lookup.property_visibility == PropertyVisibility::InOut
{
if lookup_ctx.is_legacy_component() {
debug_assert!(!diag.is_empty()); // warning should already be reported
} else {
diag.push_error("Cannot link input property".into(), &node);
}
} else {
// This is allowed, but then the rhs must also become read only.
marked_linked_read_only(&nr.element(), nr.name());
}
}
}
}
}
}
new_scope
})
}
fn marked_linked_read_only(elem: &ElementRc, prop_name: &str) {
elem.borrow()
.property_analysis
.borrow_mut()
.entry(prop_name.to_string())
.or_default()
.is_linked_to_read_only = true;
}
}
pub fn resolve_two_way_binding(
node: syntax_nodes::TwoWayBinding,
ctx: &mut LookupCtx,

View file

@ -45,7 +45,7 @@ OldCompo := Rectangle {
// ^warning{Self assignment on an output property is deprecated}
}
has-hover: true;
// ^warning{Assigning to out property 'has-hover' is deprecated}
// ^warning{Assigning to output property 'has-hover' is deprecated}
}
}

View file

@ -0,0 +1,325 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
component Button {
in property<bool> enabled : true;
out property <bool> pressed;
in-out property <bool> checked;
callback clicked();
}
component C1 {
out property <bool> out;
in property <bool> in;
in-out property <bool> inout;
property <bool> priv;
// pressed is "output" in Button
out <=> b.pressed; // ok (but then there should be no other assignment?)
in <=> b.pressed; // Error
// ^error{Cannot link to a output property}
inout <=> b.pressed;
// ^error{Cannot link to a output property}
priv <=> b.pressed; // makes assignment forbidden
b:= Button {
clicked => {
in = !in;
// ^error{Assignment on a input property}
out = !out;
// ^error{Cannot modify a property that is linked to a read-only property}
inout = !inout;
priv = !priv;
// ^error{Cannot modify a property that is linked to a read-only property}
self.enabled = !self.enabled;
self.checked = !self.checked;
self.pressed = !self.pressed;
// ^error{Assignment on a output property}
}
}
}
component C2 {
out property <bool> out;
in property <bool> in;
in-out property <bool> inout;
property <bool> priv;
// enabled is "input" in Button
out <=> b.enabled;
in <=> b.enabled;
inout <=> b.enabled;
priv <=> b.enabled;
b:= Button {
clicked => {
in = !in;
// ^error{Assignment on a input property}
out = !out;
inout = !inout;
priv = !priv;
self.enabled = !self.enabled;
// ^error{Cannot modify a property that is linked to a read-only property}
self.checked = !self.checked;
self.pressed = !self.pressed;
// ^error{Assignment on a output property}
}
}
}
component C3 {
out property <bool> out;
in property <bool> in;
in-out property <bool> inout;
property <bool> priv;
// checked is "input/output" in Button
out <=> b.checked;
in <=> b.checked;
inout <=> b.checked;
priv <=> b.checked;
b:= Button {
clicked => {
in = !in;
// ^error{Assignment on a input property}
out = !out;
inout = !inout;
priv = !priv;
self.enabled = !self.enabled;
self.checked = !self.checked;
// ^error{Cannot modify a property that is linked to a read-only property}
self.pressed = !self.pressed;
// ^error{Assignment on a output property}
}
}
}
component C5 {
out property <bool> out;
in property <bool> in;
in-out property <bool> inout;
property <bool> priv;
Button { enabled <=> out; }
Button {
enabled <=> in;
clicked => { self.enabled = !self.enabled; }
// ^error{Cannot modify a property that is linked to a read-only property}
}
Button { enabled <=> inout; }
Button { enabled <=> priv; }
}
component C6 {
out property <bool> out;
in property <bool> in;
in-out property <bool> inout;
property <bool> priv;
Button {
checked <=> out;
clicked => { out = !out; self.checked = !self.checked; }
}
Button {
checked <=> in;
// ^error{Cannot link to a input property}
clicked => { self.checked = !self.checked; }
}
Button {
checked <=> inout;
clicked => { inout = !inout; self.checked = !self.checked; }
}
Button {
checked <=> priv;
clicked => { priv = !priv; self.checked = !self.checked; }
}
}
component C7 {
b1 := Button {
clicked => {
self.enabled = !self.enabled;
self.checked = !self.checked;
self.pressed = !self.pressed;
// ^error{Assignment on a output property}
}
}
Button {
enabled <=> b1.pressed;
clicked => { self.enabled = !self.enabled; }
// ^error{Cannot modify a property that is linked to a read-only property}
}
b2 := Button {
clicked => {
self.enabled = !self.enabled;
self.checked = !self.checked;
self.pressed = !self.pressed;
// ^error{Assignment on a output property}
}
}
Button { checked <=> b2.pressed; }
// ^error{Cannot link to a output property}
b3 := Button {
clicked => {
self.enabled = !self.enabled;
self.checked = !self.checked;
self.pressed = !self.pressed;
// ^error{Assignment on a output property}
}
}
Button {
checked <=> b3.checked;
enabled <=> b3.checked;
}
Button {
checked <=> b3.enabled;
enabled <=> b3.enabled;
}
}
component C8 {
out property <bool> out1;
out property <bool> out2;
out property <bool> out3;
out property <bool> out4;
out property <bool> out <=> out1;
in property <bool> in <=> out2;
in-out property <bool> inout <=> out3;
property <bool> priv <=> out4;
Button {
clicked => {
out1 = !out1;
out2 = !out2;
// ^error{Cannot modify a property that is linked to a read-only property}
out3 = !out3;
out4 = !out4;
}
}
}
component C9 {
in property <bool> in1;
in property <bool> in2;
in property <bool> in3;
in property <bool> in4;
out property <bool> out <=> in1;
in property <bool> in <=> in2;
// ^error{Cannot link to a input property}
in-out property <bool> inout <=> in3;
// ^error{Cannot link to a input property}
property <bool> priv <=> in4;
Button {
clicked => {
out = !out;
// ^error{Cannot modify a property that is linked to a read-only property}
in = !in;
// ^error{Assignment on a input property}
inout = !inout;
priv = !priv;
// ^error{Cannot modify a property that is linked to a read-only property}
}
}
}
component C10 {
in-out property <bool> inout1;
in-out property <bool> inout2;
in-out property <bool> inout3;
in-out property <bool> inout4;
out property <bool> out <=> inout1;
in property <bool> in <=> inout2;
// ^error{Cannot link input property}
in-out property <bool> inout <=> inout3;
property <bool> priv <=> inout4;
Button {
clicked => {
inout1 = !inout1;
inout2 = !inout2;
inout3 = !inout3;
inout4 = !inout4;
out = !out;
in = !in;
// ^error{Assignment on a input property}
inout = !inout;
priv = !priv;
}
}
}
component C11 {
property <bool> priv1;
property <bool> priv2;
property <bool> priv3;
property <bool> priv4;
out property <bool> out <=> priv1;
in property <bool> in <=> priv2;
in-out property <bool> inout <=> priv3;
property <bool> priv <=> priv4;
Button {
clicked => {
priv1 = !priv1;
priv2 = !priv2;
// ^error{Cannot modify a property that is linked to a read-only property}
priv3 = !priv3;
priv4 = !priv4;
out = !out;
in = !in;
// ^error{Assignment on a input property}
inout = !inout;
priv = !priv;
}
}
}
Legacy1 := Rectangle {
b1:= Button {}
in property in1 <=> b1.pressed;
// ^warning{Link to a output property is deprecated}
out property out1 <=> b1.pressed;
in-out property inout1 <=> b1.pressed;
// ^warning{Link to a output property is deprecated}
property <bool> p1;
Button {
pressed <=> p1;
// ^warning{Assigning to output property 'pressed' is deprecated}
clicked => {
p1 = !p1;
out1 = !out1;
// ^warning{Modifying a property that is linked to a read-only property is deprecated}
}
}
Button {
enabled <=> self.pressed;
clicked => {
self.enabled = !self.enabled;
// ^warning{Modifying a property that is linked to a read-only property is deprecated}
}
}
}

View file

@ -11,7 +11,7 @@ export LineEditInner := Rectangle {
property text <=> input.text;
property placeholder-color <=> placeholder.color;
property enabled <=> input.enabled;
property has-focus <=> input.has-focus;
property <bool> has-focus: input.has-focus;
property input-type <=> input.input-type;
property horizontal-alignment <=> input.horizontal-alignment;
property read-only <=> input.read-only;
@ -47,7 +47,7 @@ export LineEditInner := Rectangle {
export TextEdit := ScrollView {
property <length> font-size <=> input.font-size;
property <string> text <=> input.text;
has-focus <=> input.has-focus;
has-focus: input.has-focus;
enabled <=> input.enabled;
property <TextWrap> wrap <=> input.wrap;
property horizontal-alignment <=> input.horizontal-alignment;

View file

@ -83,7 +83,7 @@ export global StyleMetrics := {
export Button := Rectangle {
callback clicked;
property<string> text <=> text.text;
property<bool> has-focus <=> fs.has-focus;
property<bool> has-focus: fs.has-focus;
property<bool> pressed: self.enabled && touch.pressed;
property<bool> enabled <=> touch.enabled;
property<bool> checkable;

View file

@ -218,7 +218,7 @@ export Slider := Rectangle {
property<float> maximum: 100;
property<float> minimum: 0;
property<float> value;
property<bool> has-focus <=> fs.has-focus;
property<bool> has-focus: fs.has-focus;
property<bool> enabled <=> touch.enabled;
callback changed(float);