Skip to content

Add callback forwarding #7813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ But there are also differences:
- The syntax for defining a callback is different
- Callbacks can be declared without assigning a block of code to them
- Callbacks have a special syntax for declaring aliases using the two-way binding operator `<=>`
- Callbacks have a special syntax for forwarding them using the `=>` operator
- Callback visibility is always similar to `public` functions

In general, the biggest reason to use callbacks is to be able to handle them from the backend code. Use
Expand Down Expand Up @@ -217,3 +218,66 @@ export component Example inherits Rectangle {
}
```

## Forwarding

A callback can be forwarded to another callback or function as long as they have compatible signatures:

```slint
export component HasCallbacks {
callback no-args;
callback int-arg(n: int);
callback string-arg(s: string);
callback int-ret() -> int;
callback string-ret() -> string;
callback args-2(int, float);
callback args-3(int, float, string);
pure callback cb-pure;
callback cb-impure;
callback cb;
callback abs(int) -> int;
}

export component CallbackForwarding {
callback no-args;
callback int-arg(n: int);
callback string-arg(s: string);
callback int-ret() -> int;
callback string-ret() -> string;
callback args-2(int, float);
callback args-3(int, float, string);
pure callback cb-pure;
callback cb-impure;
callback cb;
function fn() {}

HasCallbacks {
// forward no-args calls to root.no-args using `=>`
no-args => root.no-args;

// arguments and return types are converted automatically if possible
int-arg => root.string-arg;
// compiler error! string argument cannot be converted to int
// string-arg => root.int-arg;
string-ret => root.int-ret;
// compiler error! string return type cannot be converted to int
// int-ret => root.string-ret;

// additional arguments will not be forwarded
args-3 => root.args-2;
// compiler error! root.args-3 requires 3 arguments but args-2 only has 2
// args-2 => root.args-3;

// purity constraints are respected
cb-pure => root.cb-pure;
cb-impure => root.cb-pure;
// compiler error! cannot forward pure callback to impure callback
// cb-pure => root.cb-impure;

// callbacks can be forwarded to functions
cb => root.fn;
// including built-in functions
abs => Math.abs;
}
}
```

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
149 changes: 148 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,138 @@ 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 {
ctx.diag.push_error(
"The expression in a callback forwarding must be a callback/function reference"
.into(),
&node.Expression(),
);
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
Loading
Loading