Skip to content

Commit

Permalink
ChangeLog: Add callback forwarding
Browse files Browse the repository at this point in the history
Closes #6373
  • Loading branch information
crai0 committed Mar 5, 2025
1 parent efb4d23 commit a17c925
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,25 @@ export component Example inherits Rectangle {
}
```

## Forwarding

A callback can be forwarded to another callback as long as the number and types of their arguments match:

```slint
export component Example inherits Rectangle {
callback hello;
foo := TouchArea {
// forward clicked calls to hello using `=>`
clicked => hello;
}
callback bye(foo: int);
bar := TouchArea {
// Compiler error!
// clicked => bye;
}
}
```

26 changes: 26 additions & 0 deletions internal/compiler/object_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,32 @@ impl Element {
}
}

for fwd_node in node.CallbackForwarding() {
let unresolved_name = unwrap_or_continue!(parser::identifier_text(&fwd_node); diag);
let PropertyLookupResult { resolved_name, property_type, .. } =
r.lookup_property(&unresolved_name);
if let Type::Callback(_) = &property_type {
} else if property_type == Type::InferredCallback {
} else {
if r.base_type != ElementType::Error {
diag.push_error(
format!("'{}' is not a callback in {}", unresolved_name, r.base_type),
&fwd_node.child_token(SyntaxKind::Identifier).unwrap(),
);
}
continue;
}
match r.bindings.entry(resolved_name.into()) {
Entry::Vacant(e) => {
e.insert(BindingExpression::new_uncompiled(fwd_node.clone().into()).into());
}
Entry::Occupied(_) => diag.push_error(
"Duplicated callback".into(),
&fwd_node.child_token(SyntaxKind::Identifier).unwrap(),
),
}
}

for anim in node.PropertyAnimation() {
if let Some(star) = anim.child_token(SyntaxKind::Star) {
diag.push_error(
Expand Down
6 changes: 4 additions & 2 deletions internal/compiler/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,8 @@ declare_syntax! {
/// `id := Element { ... }`
SubElement -> [ Element ],
Element -> [ ?QualifiedName, *PropertyDeclaration, *Binding, *CallbackConnection,
*CallbackDeclaration, *ConditionalElement, *Function, *SubElement,
*RepeatedElement, *PropertyAnimation, *PropertyChangedCallback,
*CallbackForwarding, *CallbackDeclaration, *ConditionalElement, *Function,
*SubElement, *RepeatedElement, *PropertyAnimation, *PropertyChangedCallback,
*TwoWayBinding, *States, *Transitions, ?ChildrenPlaceholder ],
RepeatedElement -> [ ?DeclaredIdentifier, ?RepeatedIndex, Expression , SubElement],
RepeatedIndex -> [],
Expand All @@ -350,6 +350,8 @@ declare_syntax! {
/// `-> type` (but without the ->)
ReturnType -> [Type],
CallbackConnection -> [ *DeclaredIdentifier, CodeBlock ],
/// `xxx => yyy`
CallbackForwarding -> [ Expression ],
/// Declaration of a property.
PropertyDeclaration-> [ ?Type , DeclaredIdentifier, ?BindingExpression, ?TwoWayBinding ],
/// QualifiedName are the properties name
Expand Down
20 changes: 19 additions & 1 deletion internal/compiler/parser/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub fn parse_element(p: &mut impl Parser) -> bool {
/// for xx in model: Sub {}
/// if condition : Sub {}
/// clicked => {}
/// clicked => root.clicked;
/// callback foobar;
/// property<int> width;
/// animate someProp { }
Expand All @@ -63,9 +64,13 @@ pub fn parse_element_content(p: &mut impl Parser) {
SyntaxKind::ColonEqual | SyntaxKind::LBrace => {
had_parse_error |= !parse_sub_element(&mut *p)
}
SyntaxKind::FatArrow | SyntaxKind::LParent if p.peek().as_str() != "if" => {
SyntaxKind::LParent if p.peek().as_str() != "if" => {
parse_callback_connection(&mut *p)
}
SyntaxKind::FatArrow if p.peek().as_str() != "if" => match p.nth(2).kind() {
SyntaxKind::LBrace => parse_callback_connection(&mut *p),
_ => parse_callback_forwarding(&mut *p),
},
SyntaxKind::DoubleArrow => parse_two_way_binding(&mut *p),
SyntaxKind::Identifier if p.peek().as_str() == "for" => {
parse_repeated_element(&mut *p);
Expand Down Expand Up @@ -297,6 +302,19 @@ fn parse_callback_connection(p: &mut impl Parser) {
parse_code_block(&mut *p);
}

#[cfg_attr(test, parser_test)]
/// ```test,CallbackForwarding
/// clicked => clicked;
/// clicked => root.clicked;
/// ```
fn parse_callback_forwarding(p: &mut impl Parser) {
let mut p = p.start_node(SyntaxKind::CallbackForwarding);
p.consume(); // the indentifier
p.expect(SyntaxKind::FatArrow);
parse_expression(&mut *p);
p.expect(SyntaxKind::Semicolon);
}

#[cfg_attr(test, parser_test)]
/// ```test,TwoWayBinding
/// foo <=> bar;
Expand Down
141 changes: 140 additions & 1 deletion internal/compiler/passes/resolving.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use crate::diagnostics::{BuildDiagnostics, Spanned};
use crate::expression_tree::*;
use crate::langtype::{ElementType, Struct, Type};
use crate::langtype::{ElementType, Function, Struct, Type};
use crate::lookup::{LookupCtx, LookupObject, LookupResult, LookupResultCallable};
use crate::object_tree::*;
use crate::parser::{identifier_text, syntax_nodes, NodeOrToken, SyntaxKind, SyntaxNode};
Expand Down Expand Up @@ -50,6 +50,21 @@ fn resolve_expression(
SyntaxKind::CallbackConnection => {
Expression::from_callback_connection(node.clone().into(), &mut lookup_ctx)
}
SyntaxKind::CallbackForwarding => {
if let Type::Callback(callback) = lookup_ctx.property_type.clone() {
Expression::from_callback_forwarding(
callback,
node.clone().into(),
&mut lookup_ctx,
)
} else {
assert!(
diag.has_errors(),
"Property for callback forwarding should have been type checked"
);
Expression::Invalid
}
}
SyntaxKind::Function => Expression::from_function(node.clone().into(), &mut lookup_ctx),
SyntaxKind::Expression => {
//FIXME again: this happen for non-binding expression (i.e: model)
Expand Down Expand Up @@ -255,6 +270,130 @@ impl Expression {
)
}

fn from_callback_forwarding(
lhs: Rc<Function>,
node: syntax_nodes::CallbackForwarding,
ctx: &mut LookupCtx,
) -> Expression {
let (function, source_location) = if let Some(qn) = node.Expression().QualifiedName() {
let sl = qn.last_token().unwrap().to_source_location();
(lookup_qualified_name_node(qn, ctx, LookupPhase::default()), sl)
} else {
return Self::Invalid;
};
let Some(function) = function else {
return Self::Invalid;
};
let LookupResult::Callable(function) = function else {
ctx.diag.push_error("Callbacks can only be forwarded to callbacks and functions".into(), &node);
return Self::Invalid;
};

let mut arguments = Vec::new();
let mut adjust_arg_count = 0;

let lhs_args: Vec<_> = lhs
.args
.iter()
.enumerate()
.map(|(index, ty)| {
(
Expression::FunctionParameterReference { index, ty: ty.clone() },
Some(NodeOrToken::Node(node.clone().into())),
)
})
.collect();

let function = match function {
LookupResultCallable::Callable(c) => c,
LookupResultCallable::Macro(mac) => {
arguments.extend(lhs_args);
return crate::builtin_macros::lower_macro(
mac,
&source_location,
arguments.into_iter(),
ctx.diag,
);
}
LookupResultCallable::MemberFunction { base, base_node, member } => {
arguments.push((base, base_node));
adjust_arg_count = 1;
match *member {
LookupResultCallable::Callable(c) => c,
LookupResultCallable::Macro(mac) => {
arguments.extend(lhs_args);
return crate::builtin_macros::lower_macro(
mac,
&source_location,
arguments.into_iter(),
ctx.diag,
);
}
LookupResultCallable::MemberFunction { .. } => {
unreachable!()
}
}
}
};

arguments.extend(lhs_args);

let arguments = match function.ty() {
Type::Callback(rhs) | Type::Function(rhs) => {
if lhs.args.len() < (rhs.args.len() - adjust_arg_count) {
ctx.diag.push_error(
format!(
"Cannot forward callback with {} arguments to callback or function with {} arguments",
lhs.args.len(), rhs.args.len() - adjust_arg_count
),
&node,
);
arguments.into_iter().map(|x| x.0).collect()
} else {
arguments
.into_iter()
.zip(rhs.args.iter())
.enumerate()
.map(|(index, ((e, _), rhs_ty))| {
if e.ty().can_convert(rhs_ty) {
e.maybe_convert_to(rhs_ty.clone(), &node, ctx.diag)
} else {
ctx.diag.push_error(
format!(
"Cannot forward argument {} of callback because {} cannot be converted to {}",
index - adjust_arg_count, e.ty(), rhs_ty
),
&node,
);
Expression::Invalid
}
})
.collect()
}
}
Type::Invalid => {
debug_assert!(ctx.diag.has_errors(), "The error must already have been reported.");
arguments.into_iter().map(|x| x.0).collect()
}
_ => {
ctx.diag.push_error(
"Callbacks can only be forwarded to callbacks and functions".into(),
&node,
);
arguments.into_iter().map(|x| x.0).collect()
}
};

Expression::CodeBlock(vec![Expression::ReturnStatement(Some(Box::new(
Expression::FunctionCall {
function,
arguments,
source_location: Some(node.to_source_location()),
}
.maybe_convert_to(lhs.return_type.clone(), &node, ctx.diag),
)))])
}

fn from_function(node: syntax_nodes::Function, ctx: &mut LookupCtx) -> Expression {
ctx.arguments = node
.ArgumentDeclaration()
Expand Down
49 changes: 49 additions & 0 deletions internal/compiler/tests/syntax/basic/signal.slint
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,28 @@ export SubElements := Rectangle {
callback invalid_arg(InvalidType);
// ^error{Unknown type 'InvalidType'}

callback return_string() -> string;
callback return_int() -> int;
callback return_void();

callback string_arg(bool, string);
callback int_arg(bool, int);

callback abs(int) -> int;

TouchArea {
clicked => { foobar() }
}

TouchArea {
clicked => foobar;
}

TouchArea {
clicked => callback_with_arg;
// ^error{Cannot forward callback with 0 arguments to callback or function with 2 arguments}
}

TouchArea {
clicked: 45;
// ^error{'clicked' is a callback. Use `=>` to connect}
Expand All @@ -32,6 +50,18 @@ export SubElements := Rectangle {
// ^error{Duplicated callback}
}

TouchArea {
clicked => foobar;
clicked => foobar;
// ^error{Duplicated callback}
}

TouchArea {
clicked => { foobar() }
clicked => foobar;
// ^error{Duplicated callback}
}

does_not_exist => {
// ^error{'does-not-exist' is not a callback in Rectangle}
root.does_not_exist();
Expand All @@ -55,4 +85,23 @@ export SubElements := Rectangle {

callback width;
// ^error{Cannot declare callback 'width' when a property with the same name exists}

TouchArea {
clicked => pressed;
// ^error{Callbacks can only be forwarded to callbacks and functions}
}

return_int => return_string;
// ^error{Cannot convert string to int}

return_string => return_int;

return_void => return_int;

string_arg => int_arg;
// ^error{Cannot forward argument 1 of callback because string cannot be converted to int}

int_arg => string_arg;

abs => Math.abs;
}
16 changes: 16 additions & 0 deletions tools/lsp/common/token_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,22 @@ pub fn token_info(document_cache: &common::DocumentCache, token: SyntaxToken) ->
return Some(TokenInfo::LocalCallback(p));
}
return find_property_declaration_in_base(document_cache, element, &prop_name);
} else if let Some(n) = syntax_nodes::CallbackForwarding::new(node.clone()) {
if token.kind() != SyntaxKind::Identifier {
return None;
}
let prop_name = i_slint_compiler::parser::normalize_identifier(token.text());
if prop_name != i_slint_compiler::parser::identifier_text(&n)? {
return None;
}
let element = syntax_nodes::Element::new(n.parent()?)?;
if let Some(p) = element.CallbackDeclaration().find_map(|p| {
(i_slint_compiler::parser::identifier_text(&p.DeclaredIdentifier())? == prop_name)
.then_some(p)
}) {
return Some(TokenInfo::LocalCallback(p));
}
return find_property_declaration_in_base(document_cache, element, &prop_name);
} else if node.kind() == SyntaxKind::DeclaredIdentifier {
if token.kind() != SyntaxKind::Identifier {
return None;
Expand Down
Loading

0 comments on commit a17c925

Please sign in to comment.