mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-02 18:03:07 +00:00

See https://rust-lang.github.io/rust-clippy/master/index.html#useless_format ``` __CARGO_FIX_YOLO=1 cargo clippy --fix --all-targets --workspace --exclude gstreamer-player --exclude i-slint-backend-linuxkms --exclude uefi-demo --exclude ffmpeg -- -A clippy::all -W clippy::useless_format cargo fmt --all ``` `__CARGO_FIX_YOLO=1` is a hack, but it does help a lot with the tedious fixes where the result is fairly clear.
1790 lines
74 KiB
Rust
1790 lines
74 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
//! Passes that resolve the property binding expression.
|
|
//!
|
|
//! Before this pass, all the expression are of type Expression::Uncompiled,
|
|
//! and there should no longer be Uncompiled expression after this pass.
|
|
//!
|
|
//! Most of the code for the resolving actually lies in the expression_tree module
|
|
|
|
use crate::diagnostics::{BuildDiagnostics, Spanned};
|
|
use crate::expression_tree::*;
|
|
use crate::langtype::{ElementType, Struct, Type};
|
|
use crate::lookup::{LookupCtx, LookupObject, LookupResult, LookupResultCallable};
|
|
use crate::object_tree::*;
|
|
use crate::parser::{identifier_text, syntax_nodes, NodeOrToken, SyntaxKind, SyntaxNode};
|
|
use crate::typeregister::TypeRegister;
|
|
use core::num::IntErrorKind;
|
|
use smol_str::{SmolStr, ToSmolStr};
|
|
use std::collections::HashMap;
|
|
use std::rc::Rc;
|
|
|
|
/// This represents a scope for the Component, where Component is the repeated component, but
|
|
/// does not represent a component in the .slint file
|
|
#[derive(Clone)]
|
|
struct ComponentScope(Vec<ElementRc>);
|
|
|
|
fn resolve_expression(
|
|
expr: &mut Expression,
|
|
property_name: Option<&str>,
|
|
property_type: Type,
|
|
scope: &[ElementRc],
|
|
type_register: &TypeRegister,
|
|
type_loader: &crate::typeloader::TypeLoader,
|
|
diag: &mut BuildDiagnostics,
|
|
) {
|
|
if let Expression::Uncompiled(node) = expr {
|
|
let mut lookup_ctx = LookupCtx {
|
|
property_name,
|
|
property_type,
|
|
component_scope: scope,
|
|
diag,
|
|
arguments: vec![],
|
|
type_register,
|
|
type_loader: Some(type_loader),
|
|
current_token: None,
|
|
};
|
|
|
|
let new_expr = match node.kind() {
|
|
SyntaxKind::CallbackConnection => {
|
|
Expression::from_callback_connection(node.clone().into(), &mut lookup_ctx)
|
|
}
|
|
SyntaxKind::Function => Expression::from_function(node.clone().into(), &mut lookup_ctx),
|
|
SyntaxKind::Expression => {
|
|
//FIXME again: this happen for non-binding expression (i.e: model)
|
|
Expression::from_expression_node(node.clone().into(), &mut lookup_ctx)
|
|
.maybe_convert_to(lookup_ctx.property_type.clone(), node, diag)
|
|
}
|
|
SyntaxKind::BindingExpression => {
|
|
Expression::from_binding_expression_node(node.clone(), &mut lookup_ctx)
|
|
}
|
|
SyntaxKind::PropertyChangedCallback => Expression::from_codeblock_node(
|
|
syntax_nodes::PropertyChangedCallback::from(node.clone()).CodeBlock(),
|
|
&mut lookup_ctx,
|
|
),
|
|
SyntaxKind::TwoWayBinding => {
|
|
assert!(diag.has_errors(), "Two way binding should have been resolved already (property: {property_name:?})");
|
|
Expression::Invalid
|
|
}
|
|
_ => {
|
|
debug_assert!(diag.has_errors());
|
|
Expression::Invalid
|
|
}
|
|
};
|
|
*expr = new_expr;
|
|
}
|
|
}
|
|
|
|
/// Call the visitor for each children of the element recursively, starting with the element itself
|
|
///
|
|
/// The item that is being visited will be pushed to the scope and popped once visitation is over.
|
|
fn recurse_elem_with_scope(
|
|
elem: &ElementRc,
|
|
mut scope: ComponentScope,
|
|
vis: &mut impl FnMut(&ElementRc, &ComponentScope),
|
|
) -> ComponentScope {
|
|
scope.0.push(elem.clone());
|
|
vis(elem, &scope);
|
|
for sub in &elem.borrow().children {
|
|
scope = recurse_elem_with_scope(sub, scope, vis);
|
|
}
|
|
scope.0.pop();
|
|
scope
|
|
}
|
|
|
|
pub fn resolve_expressions(
|
|
doc: &Document,
|
|
type_loader: &crate::typeloader::TypeLoader,
|
|
diag: &mut BuildDiagnostics,
|
|
) {
|
|
resolve_two_way_bindings(doc, &doc.local_registry, diag);
|
|
|
|
for component in doc.inner_components.iter() {
|
|
recurse_elem_with_scope(
|
|
&component.root_element,
|
|
ComponentScope(vec![]),
|
|
&mut |elem, scope| {
|
|
let mut is_repeated = elem.borrow().repeated.is_some();
|
|
visit_element_expressions(elem, |expr, property_name, property_type| {
|
|
let scope = if is_repeated {
|
|
// The first expression is always the model and it needs to be resolved with the parent scope
|
|
debug_assert!(matches!(
|
|
elem.borrow().repeated.as_ref().unwrap().model,
|
|
Expression::Invalid
|
|
)); // should be Invalid because it is taken by the visit_element_expressions function
|
|
|
|
is_repeated = false;
|
|
|
|
debug_assert!(scope.0.len() > 1);
|
|
&scope.0[..scope.0.len() - 1]
|
|
} else {
|
|
&scope.0
|
|
};
|
|
|
|
resolve_expression(
|
|
expr,
|
|
property_name,
|
|
property_type(),
|
|
scope,
|
|
&doc.local_registry,
|
|
type_loader,
|
|
diag,
|
|
);
|
|
});
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// To be used in [`Expression::from_qualified_name_node`] to specify if the lookup is performed
|
|
/// for two ways binding (which happens before the models and other expressions are resolved),
|
|
/// or after that.
|
|
#[derive(Default)]
|
|
enum LookupPhase {
|
|
#[default]
|
|
UnspecifiedPhase,
|
|
ResolvingTwoWayBindings,
|
|
}
|
|
|
|
impl Expression {
|
|
pub fn from_binding_expression_node(node: SyntaxNode, ctx: &mut LookupCtx) -> Self {
|
|
debug_assert_eq!(node.kind(), SyntaxKind::BindingExpression);
|
|
let e = node
|
|
.children()
|
|
.find_map(|n| match n.kind() {
|
|
SyntaxKind::Expression => Some(Self::from_expression_node(n.into(), ctx)),
|
|
SyntaxKind::CodeBlock => Some(Self::from_codeblock_node(n.into(), ctx)),
|
|
_ => None,
|
|
})
|
|
.unwrap_or(Self::Invalid);
|
|
if ctx.property_type == Type::LogicalLength && e.ty() == Type::Percent {
|
|
// See if a conversion from percentage to length is allowed
|
|
const RELATIVE_TO_PARENT_PROPERTIES: &[&str] =
|
|
&["width", "height", "preferred-width", "preferred-height"];
|
|
let property_name = ctx.property_name.unwrap_or_default();
|
|
if RELATIVE_TO_PARENT_PROPERTIES.contains(&property_name) {
|
|
return e;
|
|
} else {
|
|
ctx.diag.push_error(
|
|
format!(
|
|
"Automatic conversion from percentage to length is only possible for the following properties: {}",
|
|
RELATIVE_TO_PARENT_PROPERTIES.join(", ")
|
|
),
|
|
&node
|
|
);
|
|
return Expression::Invalid;
|
|
}
|
|
};
|
|
if !matches!(ctx.property_type, Type::Callback { .. } | Type::Function { .. }) {
|
|
e.maybe_convert_to(ctx.property_type.clone(), &node, ctx.diag)
|
|
} else {
|
|
// Binding to a callback or function shouldn't happen
|
|
assert!(ctx.diag.has_errors());
|
|
e
|
|
}
|
|
}
|
|
|
|
fn from_codeblock_node(node: syntax_nodes::CodeBlock, ctx: &mut LookupCtx) -> Expression {
|
|
debug_assert_eq!(node.kind(), SyntaxKind::CodeBlock);
|
|
|
|
let mut statements_or_exprs = node
|
|
.children()
|
|
.filter_map(|n| match n.kind() {
|
|
SyntaxKind::Expression => Some(Self::from_expression_node(n.into(), ctx)),
|
|
SyntaxKind::ReturnStatement => Some(Self::from_return_statement(n.into(), ctx)),
|
|
_ => None,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let exit_points_and_return_types = statements_or_exprs
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(index, statement_or_expr)| {
|
|
if index == statements_or_exprs.len()
|
|
|| matches!(statement_or_expr, Expression::ReturnStatement(..))
|
|
{
|
|
Some((index, statement_or_expr.ty()))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let common_return_type = Self::common_target_type_for_type_list(
|
|
exit_points_and_return_types.iter().map(|(_, ty)| ty.clone()),
|
|
);
|
|
|
|
exit_points_and_return_types.into_iter().for_each(|(index, _)| {
|
|
let mut expr = std::mem::replace(&mut statements_or_exprs[index], Expression::Invalid);
|
|
expr = expr.maybe_convert_to(common_return_type.clone(), &node, ctx.diag);
|
|
statements_or_exprs[index] = expr;
|
|
});
|
|
|
|
Expression::CodeBlock(statements_or_exprs)
|
|
}
|
|
|
|
fn from_return_statement(
|
|
node: syntax_nodes::ReturnStatement,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let return_type = ctx.return_type().clone();
|
|
let e = node.Expression();
|
|
if e.is_none() && !matches!(return_type, Type::Void | Type::Invalid) {
|
|
ctx.diag.push_error(format!("Must return a value of type '{return_type}'"), &node);
|
|
}
|
|
Expression::ReturnStatement(e.map(|n| {
|
|
Box::new(Self::from_expression_node(n, ctx).maybe_convert_to(
|
|
return_type,
|
|
&node,
|
|
ctx.diag,
|
|
))
|
|
}))
|
|
}
|
|
|
|
fn from_callback_connection(
|
|
node: syntax_nodes::CallbackConnection,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
ctx.arguments =
|
|
node.DeclaredIdentifier().map(|x| identifier_text(&x).unwrap_or_default()).collect();
|
|
Self::from_codeblock_node(node.CodeBlock(), ctx).maybe_convert_to(
|
|
ctx.return_type().clone(),
|
|
&node,
|
|
ctx.diag,
|
|
)
|
|
}
|
|
|
|
fn from_function(node: syntax_nodes::Function, ctx: &mut LookupCtx) -> Expression {
|
|
ctx.arguments = node
|
|
.ArgumentDeclaration()
|
|
.map(|x| identifier_text(&x.DeclaredIdentifier()).unwrap_or_default())
|
|
.collect();
|
|
Self::from_codeblock_node(node.CodeBlock(), ctx).maybe_convert_to(
|
|
ctx.return_type().clone(),
|
|
&node,
|
|
ctx.diag,
|
|
)
|
|
}
|
|
|
|
fn from_expression_node(node: syntax_nodes::Expression, ctx: &mut LookupCtx) -> Self {
|
|
node.children_with_tokens()
|
|
.find_map(|child| match child {
|
|
NodeOrToken::Node(node) => match node.kind() {
|
|
SyntaxKind::Expression => Some(Self::from_expression_node(node.into(), ctx)),
|
|
SyntaxKind::AtImageUrl => Some(Self::from_at_image_url_node(node.into(), ctx)),
|
|
SyntaxKind::AtGradient => Some(Self::from_at_gradient(node.into(), ctx)),
|
|
SyntaxKind::AtTr => Some(Self::from_at_tr(node.into(), ctx)),
|
|
SyntaxKind::QualifiedName => Some(Self::from_qualified_name_node(
|
|
node.clone().into(),
|
|
ctx,
|
|
LookupPhase::default(),
|
|
)),
|
|
SyntaxKind::FunctionCallExpression => {
|
|
Some(Self::from_function_call_node(node.into(), ctx))
|
|
}
|
|
SyntaxKind::MemberAccess => {
|
|
Some(Self::from_member_access_node(node.into(), ctx))
|
|
}
|
|
SyntaxKind::IndexExpression => {
|
|
Some(Self::from_index_expression_node(node.into(), ctx))
|
|
}
|
|
SyntaxKind::SelfAssignment => {
|
|
Some(Self::from_self_assignment_node(node.into(), ctx))
|
|
}
|
|
SyntaxKind::BinaryExpression => {
|
|
Some(Self::from_binary_expression_node(node.into(), ctx))
|
|
}
|
|
SyntaxKind::UnaryOpExpression => {
|
|
Some(Self::from_unaryop_expression_node(node.into(), ctx))
|
|
}
|
|
SyntaxKind::ConditionalExpression => {
|
|
Some(Self::from_conditional_expression_node(node.into(), ctx))
|
|
}
|
|
SyntaxKind::ObjectLiteral => {
|
|
Some(Self::from_object_literal_node(node.into(), ctx))
|
|
}
|
|
SyntaxKind::Array => Some(Self::from_array_node(node.into(), ctx)),
|
|
SyntaxKind::CodeBlock => Some(Self::from_codeblock_node(node.into(), ctx)),
|
|
SyntaxKind::StringTemplate => {
|
|
Some(Self::from_string_template_node(node.into(), ctx))
|
|
}
|
|
_ => None,
|
|
},
|
|
NodeOrToken::Token(token) => match token.kind() {
|
|
SyntaxKind::StringLiteral => Some(
|
|
crate::literals::unescape_string(token.text())
|
|
.map(Self::StringLiteral)
|
|
.unwrap_or_else(|| {
|
|
ctx.diag.push_error("Cannot parse string literal".into(), &token);
|
|
Self::Invalid
|
|
}),
|
|
),
|
|
SyntaxKind::NumberLiteral => Some(
|
|
crate::literals::parse_number_literal(token.text().into()).unwrap_or_else(
|
|
|e| {
|
|
ctx.diag.push_error(e.to_string(), &node);
|
|
Self::Invalid
|
|
},
|
|
),
|
|
),
|
|
SyntaxKind::ColorLiteral => Some(
|
|
crate::literals::parse_color_literal(token.text())
|
|
.map(|i| Expression::Cast {
|
|
from: Box::new(Expression::NumberLiteral(i as _, Unit::None)),
|
|
to: Type::Color,
|
|
})
|
|
.unwrap_or_else(|| {
|
|
ctx.diag.push_error("Invalid color literal".into(), &node);
|
|
Self::Invalid
|
|
}),
|
|
),
|
|
|
|
_ => None,
|
|
},
|
|
})
|
|
.unwrap_or(Self::Invalid)
|
|
}
|
|
|
|
fn from_at_image_url_node(node: syntax_nodes::AtImageUrl, ctx: &mut LookupCtx) -> Self {
|
|
let s = match node
|
|
.child_text(SyntaxKind::StringLiteral)
|
|
.and_then(|x| crate::literals::unescape_string(&x))
|
|
{
|
|
Some(s) => s,
|
|
None => {
|
|
ctx.diag.push_error("Cannot parse string literal".into(), &node);
|
|
return Self::Invalid;
|
|
}
|
|
};
|
|
|
|
if s.is_empty() {
|
|
return Expression::ImageReference {
|
|
resource_ref: ImageReference::None,
|
|
source_location: Some(node.to_source_location()),
|
|
nine_slice: None,
|
|
};
|
|
}
|
|
|
|
let absolute_source_path = {
|
|
let path = std::path::Path::new(&s);
|
|
if crate::pathutils::is_absolute(path) {
|
|
s
|
|
} else {
|
|
ctx.type_loader
|
|
.and_then(|loader| {
|
|
loader.resolve_import_path(Some(&(*node).clone().into()), &s)
|
|
})
|
|
.map(|i| i.0.to_string_lossy().into())
|
|
.unwrap_or_else(|| {
|
|
crate::pathutils::join(
|
|
&crate::pathutils::dirname(node.source_file.path()),
|
|
path,
|
|
)
|
|
.map(|p| p.to_string_lossy().into())
|
|
.unwrap_or(s.clone())
|
|
})
|
|
}
|
|
};
|
|
|
|
let nine_slice = node
|
|
.children_with_tokens()
|
|
.filter_map(|n| n.into_token())
|
|
.filter(|t| t.kind() == SyntaxKind::NumberLiteral)
|
|
.map(|arg| {
|
|
arg.text().parse().unwrap_or_else(|err: std::num::ParseIntError| {
|
|
match err.kind() {
|
|
IntErrorKind::PosOverflow | IntErrorKind::NegOverflow => {
|
|
ctx.diag.push_error("Number too big".into(), &arg)
|
|
}
|
|
IntErrorKind::InvalidDigit => ctx.diag.push_error(
|
|
"Border widths of a nine-slice can't have units".into(),
|
|
&arg,
|
|
),
|
|
_ => ctx.diag.push_error("Cannot parse number literal".into(), &arg),
|
|
};
|
|
0u16
|
|
})
|
|
})
|
|
.collect::<Vec<u16>>();
|
|
|
|
let nine_slice = match nine_slice.as_slice() {
|
|
[x] => Some([*x, *x, *x, *x]),
|
|
[x, y] => Some([*x, *y, *x, *y]),
|
|
[x, y, z, w] => Some([*x, *y, *z, *w]),
|
|
[] => None,
|
|
_ => {
|
|
assert!(ctx.diag.has_errors());
|
|
None
|
|
}
|
|
};
|
|
|
|
Expression::ImageReference {
|
|
resource_ref: ImageReference::AbsolutePath(absolute_source_path),
|
|
source_location: Some(node.to_source_location()),
|
|
nine_slice,
|
|
}
|
|
}
|
|
|
|
fn from_at_gradient(node: syntax_nodes::AtGradient, ctx: &mut LookupCtx) -> Self {
|
|
enum GradKind {
|
|
Linear { angle: Box<Expression> },
|
|
Radial,
|
|
}
|
|
|
|
let mut subs = node
|
|
.children_with_tokens()
|
|
.filter(|n| matches!(n.kind(), SyntaxKind::Comma | SyntaxKind::Expression));
|
|
|
|
let grad_token = node.child_token(SyntaxKind::Identifier).unwrap();
|
|
let grad_text = grad_token.text();
|
|
|
|
let grad_kind = if grad_text.starts_with("linear") {
|
|
let angle_expr = match subs.next() {
|
|
Some(e) if e.kind() == SyntaxKind::Expression => {
|
|
syntax_nodes::Expression::from(e.into_node().unwrap())
|
|
}
|
|
_ => {
|
|
ctx.diag.push_error("Expected angle expression".into(), &node);
|
|
return Expression::Invalid;
|
|
}
|
|
};
|
|
if subs.next().is_some_and(|s| s.kind() != SyntaxKind::Comma) {
|
|
ctx.diag.push_error(
|
|
"Angle expression must be an angle followed by a comma".into(),
|
|
&node,
|
|
);
|
|
return Expression::Invalid;
|
|
}
|
|
let angle = Box::new(
|
|
Expression::from_expression_node(angle_expr.clone(), ctx).maybe_convert_to(
|
|
Type::Angle,
|
|
&angle_expr,
|
|
ctx.diag,
|
|
),
|
|
);
|
|
GradKind::Linear { angle }
|
|
} else if grad_text.starts_with("radial") {
|
|
if !matches!(subs.next(), Some(NodeOrToken::Node(n)) if n.text().to_string().trim() == "circle")
|
|
{
|
|
ctx.diag.push_error("Expected 'circle': currently, only @radial-gradient(circle, ...) are supported".into(), &node);
|
|
return Expression::Invalid;
|
|
}
|
|
let comma = subs.next();
|
|
if matches!(&comma, Some(NodeOrToken::Node(n)) if n.text().to_string().trim() == "at") {
|
|
ctx.diag.push_error("'at' in @radial-gradient is not yet supported".into(), &comma);
|
|
return Expression::Invalid;
|
|
}
|
|
if comma.as_ref().is_some_and(|s| s.kind() != SyntaxKind::Comma) {
|
|
ctx.diag.push_error(
|
|
"'circle' must be followed by a comma".into(),
|
|
comma.as_ref().map_or(&node, |x| x as &dyn Spanned),
|
|
);
|
|
return Expression::Invalid;
|
|
}
|
|
GradKind::Radial
|
|
} else {
|
|
// Parser should have ensured we have one of the linear or radial gradient
|
|
panic!("Not a gradient {grad_text:?}");
|
|
};
|
|
|
|
let mut stops = vec![];
|
|
enum Stop {
|
|
Empty,
|
|
Color(Expression),
|
|
Finished,
|
|
}
|
|
let mut current_stop = Stop::Empty;
|
|
for n in subs {
|
|
if n.kind() == SyntaxKind::Comma {
|
|
match std::mem::replace(&mut current_stop, Stop::Empty) {
|
|
Stop::Empty => {
|
|
ctx.diag.push_error("Expected expression".into(), &n);
|
|
break;
|
|
}
|
|
Stop::Finished => {}
|
|
Stop::Color(col) => stops.push((
|
|
col,
|
|
if stops.is_empty() {
|
|
Expression::NumberLiteral(0., Unit::None)
|
|
} else {
|
|
Expression::Invalid
|
|
},
|
|
)),
|
|
}
|
|
} else {
|
|
// To facilitate color literal conversion, adjust the expected return type.
|
|
let e = {
|
|
let old_property_type = std::mem::replace(&mut ctx.property_type, Type::Color);
|
|
let e =
|
|
Expression::from_expression_node(n.as_node().unwrap().clone().into(), ctx);
|
|
ctx.property_type = old_property_type;
|
|
e
|
|
};
|
|
match std::mem::replace(&mut current_stop, Stop::Finished) {
|
|
Stop::Empty => {
|
|
current_stop = Stop::Color(e.maybe_convert_to(Type::Color, &n, ctx.diag))
|
|
}
|
|
Stop::Finished => {
|
|
ctx.diag.push_error("Expected comma".into(), &n);
|
|
break;
|
|
}
|
|
Stop::Color(col) => {
|
|
stops.push((col, e.maybe_convert_to(Type::Float32, &n, ctx.diag)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
match current_stop {
|
|
Stop::Color(col) => stops.push((col, Expression::NumberLiteral(1., Unit::None))),
|
|
Stop::Empty => {
|
|
if let Some((_, e @ Expression::Invalid)) = stops.last_mut() {
|
|
*e = Expression::NumberLiteral(1., Unit::None)
|
|
}
|
|
}
|
|
Stop::Finished => (),
|
|
};
|
|
|
|
// Fix the stop so each has a position.
|
|
let mut start = 0;
|
|
while start < stops.len() {
|
|
start += match stops[start..].iter().position(|s| matches!(s.1, Expression::Invalid)) {
|
|
Some(p) => p,
|
|
None => break,
|
|
};
|
|
let (before, rest) = stops.split_at_mut(start);
|
|
let pos =
|
|
rest.iter().position(|s| !matches!(s.1, Expression::Invalid)).unwrap_or(rest.len());
|
|
if pos > 0 && pos < rest.len() {
|
|
let (middle, after) = rest.split_at_mut(pos);
|
|
let begin = before
|
|
.last()
|
|
.map(|s| &s.1)
|
|
.unwrap_or(&Expression::NumberLiteral(1., Unit::None));
|
|
let end = &after.first().expect("The last should never be invalid").1;
|
|
for (i, (_, e)) in middle.iter_mut().enumerate() {
|
|
debug_assert!(matches!(e, Expression::Invalid));
|
|
// e = begin + (i+1) * (end - begin) / (pos+1)
|
|
*e = Expression::BinaryExpression {
|
|
lhs: Box::new(begin.clone()),
|
|
rhs: Box::new(Expression::BinaryExpression {
|
|
lhs: Box::new(Expression::BinaryExpression {
|
|
lhs: Box::new(Expression::NumberLiteral(i as f64 + 1., Unit::None)),
|
|
rhs: Box::new(Expression::BinaryExpression {
|
|
lhs: Box::new(end.clone()),
|
|
rhs: Box::new(begin.clone()),
|
|
op: '-',
|
|
}),
|
|
op: '*',
|
|
}),
|
|
rhs: Box::new(Expression::NumberLiteral(pos as f64 + 1., Unit::None)),
|
|
op: '/',
|
|
}),
|
|
op: '+',
|
|
};
|
|
}
|
|
}
|
|
start += pos + 1;
|
|
}
|
|
|
|
match grad_kind {
|
|
GradKind::Linear { angle } => Expression::LinearGradient { angle, stops },
|
|
GradKind::Radial => Expression::RadialGradient { stops },
|
|
}
|
|
}
|
|
|
|
fn from_at_tr(node: syntax_nodes::AtTr, ctx: &mut LookupCtx) -> Expression {
|
|
let Some(string) = node
|
|
.child_text(SyntaxKind::StringLiteral)
|
|
.and_then(|s| crate::literals::unescape_string(&s))
|
|
else {
|
|
ctx.diag.push_error("Cannot parse string literal".into(), &node);
|
|
return Expression::Invalid;
|
|
};
|
|
let context = node.TrContext().map(|n| {
|
|
n.child_text(SyntaxKind::StringLiteral)
|
|
.and_then(|s| crate::literals::unescape_string(&s))
|
|
.unwrap_or_else(|| {
|
|
ctx.diag.push_error("Cannot parse string literal".into(), &n);
|
|
Default::default()
|
|
})
|
|
});
|
|
let plural = node.TrPlural().map(|pl| {
|
|
let s = pl
|
|
.child_text(SyntaxKind::StringLiteral)
|
|
.and_then(|s| crate::literals::unescape_string(&s))
|
|
.unwrap_or_else(|| {
|
|
ctx.diag.push_error("Cannot parse string literal".into(), &pl);
|
|
Default::default()
|
|
});
|
|
let n = pl.Expression();
|
|
let expr = Expression::from_expression_node(n.clone(), ctx).maybe_convert_to(
|
|
Type::Int32,
|
|
&n,
|
|
ctx.diag,
|
|
);
|
|
(s, expr)
|
|
});
|
|
|
|
let domain = ctx
|
|
.type_loader
|
|
.and_then(|tl| tl.compiler_config.translation_domain.clone())
|
|
.unwrap_or_default();
|
|
|
|
let subs = node.Expression().map(|n| {
|
|
Expression::from_expression_node(n.clone(), ctx).maybe_convert_to(
|
|
Type::String,
|
|
&n,
|
|
ctx.diag,
|
|
)
|
|
});
|
|
let values = subs.collect::<Vec<_>>();
|
|
|
|
// check format string
|
|
{
|
|
let mut arg_idx = 0;
|
|
let mut pos_max = 0;
|
|
let mut pos = 0;
|
|
let mut has_n = false;
|
|
while let Some(mut p) = string[pos..].find(['{', '}']) {
|
|
if string.len() - pos < p + 1 {
|
|
ctx.diag.push_error(
|
|
"Unescaped trailing '{' in format string. Escape '{' with '{{'".into(),
|
|
&node,
|
|
);
|
|
break;
|
|
}
|
|
p += pos;
|
|
|
|
// Skip escaped }
|
|
if string.get(p..=p) == Some("}") {
|
|
if string.get(p + 1..=p + 1) == Some("}") {
|
|
pos = p + 2;
|
|
continue;
|
|
} else {
|
|
ctx.diag.push_error(
|
|
"Unescaped '}' in format string. Escape '}' with '}}'".into(),
|
|
&node,
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Skip escaped {
|
|
if string.get(p + 1..=p + 1) == Some("{") {
|
|
pos = p + 2;
|
|
continue;
|
|
}
|
|
|
|
// Find the argument
|
|
let end = if let Some(end) = string[p..].find('}') {
|
|
end + p
|
|
} else {
|
|
ctx.diag.push_error(
|
|
"Unterminated placeholder in format string. '{' must be escaped with '{{'"
|
|
.into(),
|
|
&node,
|
|
);
|
|
break;
|
|
};
|
|
let argument = &string[p + 1..end];
|
|
if argument.is_empty() {
|
|
arg_idx += 1;
|
|
} else if let Ok(n) = argument.parse::<u16>() {
|
|
pos_max = pos_max.max(n as usize + 1);
|
|
} else if argument == "n" {
|
|
has_n = true;
|
|
if plural.is_none() {
|
|
ctx.diag.push_error(
|
|
"`{n}` placeholder can only be found in plural form".into(),
|
|
&node,
|
|
);
|
|
}
|
|
} else {
|
|
ctx.diag
|
|
.push_error("Invalid '{...}' placeholder in format string. The placeholder must be a number, or braces must be escaped with '{{' and '}}'".into(), &node);
|
|
break;
|
|
};
|
|
pos = end + 1;
|
|
}
|
|
if arg_idx > 0 && pos_max > 0 {
|
|
ctx.diag.push_error(
|
|
"Cannot mix positional and non-positional placeholder in format string".into(),
|
|
&node,
|
|
);
|
|
} else if arg_idx > values.len() || pos_max > values.len() {
|
|
let num = arg_idx.max(pos_max);
|
|
let note = if !has_n && plural.is_some() {
|
|
". Note: use `{n}` for the argument after '%'"
|
|
} else {
|
|
""
|
|
};
|
|
ctx.diag.push_error(
|
|
format!("Format string contains {num} placeholders, but only {} extra arguments were given{note}", values.len()),
|
|
&node,
|
|
);
|
|
}
|
|
}
|
|
|
|
let plural =
|
|
plural.unwrap_or((SmolStr::default(), Expression::NumberLiteral(1., Unit::None)));
|
|
|
|
let get_component_name = || {
|
|
ctx.component_scope
|
|
.first()
|
|
.and_then(|e| e.borrow().enclosing_component.upgrade())
|
|
.map(|c| c.id.clone())
|
|
};
|
|
|
|
Expression::FunctionCall {
|
|
function: BuiltinFunction::Translate.into(),
|
|
arguments: vec![
|
|
Expression::StringLiteral(string),
|
|
Expression::StringLiteral(context.or_else(get_component_name).unwrap_or_default()),
|
|
Expression::StringLiteral(domain.into()),
|
|
Expression::Array { element_ty: Type::String, values },
|
|
plural.1,
|
|
Expression::StringLiteral(plural.0),
|
|
],
|
|
source_location: Some(node.to_source_location()),
|
|
}
|
|
}
|
|
|
|
/// Perform the lookup
|
|
fn from_qualified_name_node(
|
|
node: syntax_nodes::QualifiedName,
|
|
ctx: &mut LookupCtx,
|
|
phase: LookupPhase,
|
|
) -> Self {
|
|
Self::from_lookup_result(lookup_qualified_name_node(node.clone(), ctx, phase), ctx, &node)
|
|
}
|
|
|
|
fn from_lookup_result(
|
|
r: Option<LookupResult>,
|
|
ctx: &mut LookupCtx,
|
|
node: &dyn Spanned,
|
|
) -> Self {
|
|
let Some(r) = r else {
|
|
assert!(ctx.diag.has_errors());
|
|
return Self::Invalid;
|
|
};
|
|
match r {
|
|
LookupResult::Expression { expression, .. } => expression,
|
|
LookupResult::Callable(c) => {
|
|
let what = match c {
|
|
LookupResultCallable::Callable(Callable::Callback(..)) => "Callback",
|
|
LookupResultCallable::Callable(Callable::Builtin(..)) => "Builtin function",
|
|
LookupResultCallable::Macro(..) => "Builtin function",
|
|
LookupResultCallable::MemberFunction { .. } => "Member function",
|
|
_ => "Function",
|
|
};
|
|
ctx.diag
|
|
.push_error(format!("{what} must be called. Did you forgot the '()'?",), node);
|
|
Self::Invalid
|
|
}
|
|
LookupResult::Enumeration(..) => {
|
|
ctx.diag.push_error("Cannot take reference to an enum".to_string(), node);
|
|
Self::Invalid
|
|
}
|
|
LookupResult::Namespace(..) => {
|
|
ctx.diag.push_error("Cannot take reference to a namespace".to_string(), node);
|
|
Self::Invalid
|
|
}
|
|
}
|
|
}
|
|
|
|
fn from_function_call_node(
|
|
node: syntax_nodes::FunctionCallExpression,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let mut arguments = Vec::new();
|
|
|
|
let mut sub_expr = node.Expression();
|
|
|
|
let func_expr = sub_expr.next().unwrap();
|
|
|
|
let (function, source_location) = if let Some(qn) = func_expr.QualifiedName() {
|
|
let sl = qn.last_token().unwrap().to_source_location();
|
|
(lookup_qualified_name_node(qn, ctx, LookupPhase::default()), sl)
|
|
} else if let Some(ma) = func_expr.MemberAccess() {
|
|
let base = Self::from_expression_node(ma.Expression(), ctx);
|
|
let field = ma.child_token(SyntaxKind::Identifier);
|
|
let sl = field.to_source_location();
|
|
(maybe_lookup_object(base.into(), field.clone().into_iter(), ctx), sl)
|
|
} else {
|
|
if Self::from_expression_node(func_expr, ctx).ty() == Type::Invalid {
|
|
assert!(ctx.diag.has_errors());
|
|
} else {
|
|
ctx.diag.push_error("The expression is not a function".into(), &node);
|
|
}
|
|
return Self::Invalid;
|
|
};
|
|
let sub_expr = sub_expr.map(|n| {
|
|
(Self::from_expression_node(n.clone(), ctx), Some(NodeOrToken::from((*n).clone())))
|
|
});
|
|
let Some(function) = function else {
|
|
// Check sub expressions anyway
|
|
sub_expr.count();
|
|
assert!(ctx.diag.has_errors());
|
|
return Self::Invalid;
|
|
};
|
|
let LookupResult::Callable(function) = function else {
|
|
// Check sub expressions anyway
|
|
sub_expr.count();
|
|
ctx.diag.push_error("The expression is not a function".into(), &node);
|
|
return Self::Invalid;
|
|
};
|
|
|
|
let mut adjust_arg_count = 0;
|
|
let function = match function {
|
|
LookupResultCallable::Callable(c) => c,
|
|
LookupResultCallable::Macro(mac) => {
|
|
arguments.extend(sub_expr);
|
|
return crate::builtin_macros::lower_macro(
|
|
mac,
|
|
&source_location,
|
|
arguments.into_iter(),
|
|
ctx.diag,
|
|
);
|
|
}
|
|
LookupResultCallable::MemberFunction { member, base, base_node } => {
|
|
arguments.push((base, base_node));
|
|
adjust_arg_count = 1;
|
|
match *member {
|
|
LookupResultCallable::Callable(c) => c,
|
|
LookupResultCallable::Macro(mac) => {
|
|
arguments.extend(sub_expr);
|
|
return crate::builtin_macros::lower_macro(
|
|
mac,
|
|
&source_location,
|
|
arguments.into_iter(),
|
|
ctx.diag,
|
|
);
|
|
}
|
|
LookupResultCallable::MemberFunction { .. } => {
|
|
unreachable!()
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
arguments.extend(sub_expr);
|
|
|
|
let arguments = match function.ty() {
|
|
Type::Function(function) | Type::Callback(function) => {
|
|
if arguments.len() != function.args.len() {
|
|
ctx.diag.push_error(
|
|
format!(
|
|
"The callback or function expects {} arguments, but {} are provided",
|
|
function.args.len() - adjust_arg_count,
|
|
arguments.len() - adjust_arg_count,
|
|
),
|
|
&node,
|
|
);
|
|
arguments.into_iter().map(|x| x.0).collect()
|
|
} else {
|
|
arguments
|
|
.into_iter()
|
|
.zip(function.args.iter())
|
|
.map(|((e, node), ty)| e.maybe_convert_to(ty.clone(), &node, ctx.diag))
|
|
.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("The expression is not a function".into(), &node);
|
|
arguments.into_iter().map(|x| x.0).collect()
|
|
}
|
|
};
|
|
|
|
Expression::FunctionCall { function, arguments, source_location: Some(source_location) }
|
|
}
|
|
|
|
fn from_member_access_node(
|
|
node: syntax_nodes::MemberAccess,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let base = Self::from_expression_node(node.Expression(), ctx);
|
|
let field = node.child_token(SyntaxKind::Identifier);
|
|
Self::from_lookup_result(
|
|
maybe_lookup_object(base.into(), field.clone().into_iter(), ctx),
|
|
ctx,
|
|
&field,
|
|
)
|
|
}
|
|
|
|
fn from_self_assignment_node(
|
|
node: syntax_nodes::SelfAssignment,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let (lhs_n, rhs_n) = node.Expression();
|
|
let mut lhs = Self::from_expression_node(lhs_n.clone(), ctx);
|
|
let op = node
|
|
.children_with_tokens()
|
|
.find_map(|n| match n.kind() {
|
|
SyntaxKind::PlusEqual => Some('+'),
|
|
SyntaxKind::MinusEqual => Some('-'),
|
|
SyntaxKind::StarEqual => Some('*'),
|
|
SyntaxKind::DivEqual => Some('/'),
|
|
SyntaxKind::Equal => Some('='),
|
|
_ => None,
|
|
})
|
|
.unwrap_or('_');
|
|
if lhs.ty() != Type::Invalid {
|
|
lhs.try_set_rw(ctx, if op == '=' { "Assignment" } else { "Self assignment" }, &node);
|
|
}
|
|
let ty = lhs.ty();
|
|
let expected_ty = match op {
|
|
'=' => ty,
|
|
'+' if ty == Type::String || ty.as_unit_product().is_some() => ty,
|
|
'-' if ty.as_unit_product().is_some() => ty,
|
|
'/' | '*' if ty.as_unit_product().is_some() => Type::Float32,
|
|
_ => {
|
|
if ty != Type::Invalid {
|
|
ctx.diag.push_error(
|
|
format!("the {op}= operation cannot be done on a {ty}"),
|
|
&lhs_n,
|
|
);
|
|
}
|
|
Type::Invalid
|
|
}
|
|
};
|
|
let rhs = Self::from_expression_node(rhs_n.clone(), ctx);
|
|
Expression::SelfAssignment {
|
|
lhs: Box::new(lhs),
|
|
rhs: Box::new(rhs.maybe_convert_to(expected_ty, &rhs_n, ctx.diag)),
|
|
op,
|
|
node: Some(NodeOrToken::Node(node.into())),
|
|
}
|
|
}
|
|
|
|
fn from_binary_expression_node(
|
|
node: syntax_nodes::BinaryExpression,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let op = node
|
|
.children_with_tokens()
|
|
.find_map(|n| match n.kind() {
|
|
SyntaxKind::Plus => Some('+'),
|
|
SyntaxKind::Minus => Some('-'),
|
|
SyntaxKind::Star => Some('*'),
|
|
SyntaxKind::Div => Some('/'),
|
|
SyntaxKind::LessEqual => Some('≤'),
|
|
SyntaxKind::GreaterEqual => Some('≥'),
|
|
SyntaxKind::LAngle => Some('<'),
|
|
SyntaxKind::RAngle => Some('>'),
|
|
SyntaxKind::EqualEqual => Some('='),
|
|
SyntaxKind::NotEqual => Some('!'),
|
|
SyntaxKind::AndAnd => Some('&'),
|
|
SyntaxKind::OrOr => Some('|'),
|
|
_ => None,
|
|
})
|
|
.unwrap_or('_');
|
|
|
|
let (lhs_n, rhs_n) = node.Expression();
|
|
let lhs = Self::from_expression_node(lhs_n.clone(), ctx);
|
|
let rhs = Self::from_expression_node(rhs_n.clone(), ctx);
|
|
|
|
let expected_ty = match operator_class(op) {
|
|
OperatorClass::ComparisonOp => {
|
|
Self::common_target_type_for_type_list([lhs.ty(), rhs.ty()].iter().cloned())
|
|
}
|
|
OperatorClass::LogicalOp => Type::Bool,
|
|
OperatorClass::ArithmeticOp => {
|
|
let (lhs_ty, rhs_ty) = (lhs.ty(), rhs.ty());
|
|
if op == '+' && (lhs_ty == Type::String || rhs_ty == Type::String) {
|
|
Type::String
|
|
} else if op == '+' || op == '-' {
|
|
if lhs_ty.default_unit().is_some() {
|
|
lhs_ty
|
|
} else if rhs_ty.default_unit().is_some() {
|
|
rhs_ty
|
|
} else if matches!(lhs_ty, Type::UnitProduct(_)) {
|
|
lhs_ty
|
|
} else if matches!(rhs_ty, Type::UnitProduct(_)) {
|
|
rhs_ty
|
|
} else {
|
|
Type::Float32
|
|
}
|
|
} else if op == '*' || op == '/' {
|
|
let has_unit = |ty: &Type| {
|
|
matches!(ty, Type::UnitProduct(_)) || ty.default_unit().is_some()
|
|
};
|
|
match (has_unit(&lhs_ty), has_unit(&rhs_ty)) {
|
|
(true, true) => {
|
|
return Expression::BinaryExpression {
|
|
lhs: Box::new(lhs),
|
|
rhs: Box::new(rhs),
|
|
op,
|
|
}
|
|
}
|
|
(true, false) => {
|
|
return Expression::BinaryExpression {
|
|
lhs: Box::new(lhs),
|
|
rhs: Box::new(rhs.maybe_convert_to(
|
|
Type::Float32,
|
|
&rhs_n,
|
|
ctx.diag,
|
|
)),
|
|
op,
|
|
}
|
|
}
|
|
(false, true) => {
|
|
return Expression::BinaryExpression {
|
|
lhs: Box::new(lhs.maybe_convert_to(
|
|
Type::Float32,
|
|
&lhs_n,
|
|
ctx.diag,
|
|
)),
|
|
rhs: Box::new(rhs),
|
|
op,
|
|
}
|
|
}
|
|
(false, false) => Type::Float32,
|
|
}
|
|
} else {
|
|
unreachable!()
|
|
}
|
|
}
|
|
};
|
|
Expression::BinaryExpression {
|
|
lhs: Box::new(lhs.maybe_convert_to(expected_ty.clone(), &lhs_n, ctx.diag)),
|
|
rhs: Box::new(rhs.maybe_convert_to(expected_ty, &rhs_n, ctx.diag)),
|
|
op,
|
|
}
|
|
}
|
|
|
|
fn from_unaryop_expression_node(
|
|
node: syntax_nodes::UnaryOpExpression,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let exp_n = node.Expression();
|
|
let exp = Self::from_expression_node(exp_n, ctx);
|
|
|
|
let op = node
|
|
.children_with_tokens()
|
|
.find_map(|n| match n.kind() {
|
|
SyntaxKind::Plus => Some('+'),
|
|
SyntaxKind::Minus => Some('-'),
|
|
SyntaxKind::Bang => Some('!'),
|
|
_ => None,
|
|
})
|
|
.unwrap_or('_');
|
|
|
|
let exp = match op {
|
|
'!' => exp.maybe_convert_to(Type::Bool, &node, ctx.diag),
|
|
'+' | '-' => {
|
|
let ty = exp.ty();
|
|
if ty.default_unit().is_none()
|
|
&& !matches!(
|
|
ty,
|
|
Type::Int32
|
|
| Type::Float32
|
|
| Type::Percent
|
|
| Type::UnitProduct(..)
|
|
| Type::Invalid
|
|
)
|
|
{
|
|
ctx.diag.push_error(format!("Unary '{op}' not supported on {ty}"), &node);
|
|
}
|
|
exp
|
|
}
|
|
_ => {
|
|
assert!(ctx.diag.has_errors());
|
|
exp
|
|
}
|
|
};
|
|
|
|
Expression::UnaryOp { sub: Box::new(exp), op }
|
|
}
|
|
|
|
fn from_conditional_expression_node(
|
|
node: syntax_nodes::ConditionalExpression,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let (condition_n, true_expr_n, false_expr_n) = node.Expression();
|
|
// FIXME: we should we add bool to the context
|
|
let condition = Self::from_expression_node(condition_n.clone(), ctx).maybe_convert_to(
|
|
Type::Bool,
|
|
&condition_n,
|
|
ctx.diag,
|
|
);
|
|
let true_expr = Self::from_expression_node(true_expr_n.clone(), ctx);
|
|
let false_expr = Self::from_expression_node(false_expr_n.clone(), ctx);
|
|
let result_ty = Self::common_target_type_for_type_list(
|
|
[true_expr.ty(), false_expr.ty()].iter().cloned(),
|
|
);
|
|
let true_expr = true_expr.maybe_convert_to(result_ty.clone(), &true_expr_n, ctx.diag);
|
|
let false_expr = false_expr.maybe_convert_to(result_ty, &false_expr_n, ctx.diag);
|
|
Expression::Condition {
|
|
condition: Box::new(condition),
|
|
true_expr: Box::new(true_expr),
|
|
false_expr: Box::new(false_expr),
|
|
}
|
|
}
|
|
|
|
fn from_index_expression_node(
|
|
node: syntax_nodes::IndexExpression,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let (array_expr_n, index_expr_n) = node.Expression();
|
|
let array_expr = Self::from_expression_node(array_expr_n, ctx);
|
|
let index_expr = Self::from_expression_node(index_expr_n.clone(), ctx).maybe_convert_to(
|
|
Type::Int32,
|
|
&index_expr_n,
|
|
ctx.diag,
|
|
);
|
|
|
|
let ty = array_expr.ty();
|
|
if !matches!(ty, Type::Array(_) | Type::Invalid | Type::Function(_) | Type::Callback(_)) {
|
|
ctx.diag.push_error(format!("{ty} is not an indexable type"), &node);
|
|
}
|
|
Expression::ArrayIndex { array: Box::new(array_expr), index: Box::new(index_expr) }
|
|
}
|
|
|
|
fn from_object_literal_node(
|
|
node: syntax_nodes::ObjectLiteral,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let values: HashMap<SmolStr, Expression> = node
|
|
.ObjectMember()
|
|
.map(|n| {
|
|
(
|
|
identifier_text(&n).unwrap_or_default(),
|
|
Expression::from_expression_node(n.Expression(), ctx),
|
|
)
|
|
})
|
|
.collect();
|
|
let ty = Rc::new(Struct {
|
|
fields: values.iter().map(|(k, v)| (k.clone(), v.ty())).collect(),
|
|
name: None,
|
|
node: None,
|
|
rust_attributes: None,
|
|
});
|
|
Expression::Struct { ty, values }
|
|
}
|
|
|
|
fn from_array_node(node: syntax_nodes::Array, ctx: &mut LookupCtx) -> Expression {
|
|
let mut values: Vec<Expression> =
|
|
node.Expression().map(|e| Expression::from_expression_node(e, ctx)).collect();
|
|
|
|
let element_ty = if values.is_empty() {
|
|
Type::Void
|
|
} else {
|
|
Self::common_target_type_for_type_list(values.iter().map(|expr| expr.ty()))
|
|
};
|
|
|
|
for e in values.iter_mut() {
|
|
*e = core::mem::replace(e, Expression::Invalid).maybe_convert_to(
|
|
element_ty.clone(),
|
|
&node,
|
|
ctx.diag,
|
|
);
|
|
}
|
|
|
|
Expression::Array { element_ty, values }
|
|
}
|
|
|
|
fn from_string_template_node(
|
|
node: syntax_nodes::StringTemplate,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let mut exprs = node.Expression().map(|e| {
|
|
Expression::from_expression_node(e.clone(), ctx).maybe_convert_to(
|
|
Type::String,
|
|
&e,
|
|
ctx.diag,
|
|
)
|
|
});
|
|
let mut result = exprs.next().unwrap_or_default();
|
|
for x in exprs {
|
|
result = Expression::BinaryExpression {
|
|
lhs: Box::new(std::mem::take(&mut result)),
|
|
rhs: Box::new(x),
|
|
op: '+',
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
/// This function is used to find a type that's suitable for casting each instance of a bunch of expressions
|
|
/// to a type that captures most aspects. For example for an array of object literals the result is a merge of
|
|
/// all seen fields.
|
|
pub fn common_target_type_for_type_list(types: impl Iterator<Item = Type>) -> Type {
|
|
types.fold(Type::Invalid, |target_type, expr_ty| {
|
|
if target_type == expr_ty {
|
|
target_type
|
|
} else if target_type == Type::Invalid {
|
|
expr_ty
|
|
} else {
|
|
match (target_type, expr_ty) {
|
|
(Type::Struct(ref result), Type::Struct(ref elem)) => {
|
|
let mut fields = result.fields.clone();
|
|
for (elem_name, elem_ty) in elem.fields.iter() {
|
|
match fields.entry(elem_name.clone()) {
|
|
std::collections::btree_map::Entry::Vacant(free_entry) => {
|
|
free_entry.insert(elem_ty.clone());
|
|
}
|
|
std::collections::btree_map::Entry::Occupied(
|
|
mut existing_field,
|
|
) => {
|
|
*existing_field.get_mut() =
|
|
Self::common_target_type_for_type_list(
|
|
[existing_field.get().clone(), elem_ty.clone()]
|
|
.into_iter(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Type::Struct(Rc::new(Struct {
|
|
name: result.name.as_ref().or(elem.name.as_ref()).cloned(),
|
|
fields,
|
|
node: result.node.as_ref().or(elem.node.as_ref()).cloned(),
|
|
rust_attributes: result
|
|
.rust_attributes
|
|
.as_ref()
|
|
.or(elem.rust_attributes.as_ref())
|
|
.cloned(),
|
|
}))
|
|
}
|
|
(Type::Array(lhs), Type::Array(rhs)) => Type::Array(if *lhs == Type::Void {
|
|
rhs
|
|
} else if *rhs == Type::Void {
|
|
lhs
|
|
} else {
|
|
Self::common_target_type_for_type_list(
|
|
[(*lhs).clone(), (*rhs).clone()].into_iter(),
|
|
)
|
|
.into()
|
|
}),
|
|
(Type::Color, Type::Brush) | (Type::Brush, Type::Color) => Type::Brush,
|
|
(target_type, expr_ty) => {
|
|
if expr_ty.can_convert(&target_type) {
|
|
target_type
|
|
} else if target_type.can_convert(&expr_ty)
|
|
|| (expr_ty.default_unit().is_some()
|
|
&& matches!(target_type, Type::Float32 | Type::Int32))
|
|
{
|
|
// in the or case: The `0` literal.
|
|
expr_ty
|
|
} else {
|
|
// otherwise, use the target type and let further conversion report an error
|
|
target_type
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Perform the lookup
|
|
fn lookup_qualified_name_node(
|
|
node: syntax_nodes::QualifiedName,
|
|
ctx: &mut LookupCtx,
|
|
phase: LookupPhase,
|
|
) -> Option<LookupResult> {
|
|
let mut it = node
|
|
.children_with_tokens()
|
|
.filter(|n| n.kind() == SyntaxKind::Identifier)
|
|
.filter_map(|n| n.into_token());
|
|
|
|
let first = if let Some(first) = it.next() {
|
|
first
|
|
} else {
|
|
// There must be at least one member (parser should ensure that)
|
|
debug_assert!(ctx.diag.has_errors());
|
|
return None;
|
|
};
|
|
|
|
ctx.current_token = Some(first.clone().into());
|
|
let first_str = crate::parser::normalize_identifier(first.text());
|
|
let global_lookup = crate::lookup::global_lookup();
|
|
let result = match global_lookup.lookup(ctx, &first_str) {
|
|
None => {
|
|
if let Some(minus_pos) = first.text().find('-') {
|
|
// Attempt to recover if the user wanted to write "-" for minus
|
|
let first_str = &first.text()[0..minus_pos];
|
|
if global_lookup
|
|
.lookup(ctx, &crate::parser::normalize_identifier(first_str))
|
|
.is_some()
|
|
{
|
|
ctx.diag.push_error(format!("Unknown unqualified identifier '{}'. Use space before the '-' if you meant a subtraction", first.text()), &node);
|
|
return None;
|
|
}
|
|
}
|
|
for (prefix, e) in
|
|
[("self", ctx.component_scope.last()), ("root", ctx.component_scope.first())]
|
|
{
|
|
if let Some(e) = e {
|
|
if e.lookup(ctx, &first_str).is_some() {
|
|
ctx.diag.push_error(format!("Unknown unqualified identifier '{0}'. Did you mean '{prefix}.{0}'?", first.text()), &node);
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
|
|
if it.next().is_some() {
|
|
ctx.diag.push_error(format!("Cannot access id '{}'", first.text()), &node);
|
|
} else {
|
|
ctx.diag.push_error(
|
|
format!("Unknown unqualified identifier '{}'", first.text()),
|
|
&node,
|
|
);
|
|
}
|
|
return None;
|
|
}
|
|
Some(x) => x,
|
|
};
|
|
|
|
if let Some(depr) = result.deprecated() {
|
|
ctx.diag.push_property_deprecation_warning(&first_str, depr, &first);
|
|
}
|
|
|
|
match result {
|
|
LookupResult::Expression { expression: Expression::ElementReference(e), .. } => {
|
|
continue_lookup_within_element(&e.upgrade().unwrap(), &mut it, node, ctx)
|
|
}
|
|
LookupResult::Expression {
|
|
expression: Expression::RepeaterModelReference { .. }, ..
|
|
} if matches!(phase, LookupPhase::ResolvingTwoWayBindings) => {
|
|
ctx.diag.push_error(
|
|
"Two-way bindings to model data is not supported yet".to_string(),
|
|
&node,
|
|
);
|
|
None
|
|
}
|
|
result => maybe_lookup_object(result, it, ctx),
|
|
}
|
|
}
|
|
|
|
fn continue_lookup_within_element(
|
|
elem: &ElementRc,
|
|
it: &mut impl Iterator<Item = crate::parser::SyntaxToken>,
|
|
node: syntax_nodes::QualifiedName,
|
|
ctx: &mut LookupCtx,
|
|
) -> Option<LookupResult> {
|
|
let second = if let Some(second) = it.next() {
|
|
second
|
|
} else if matches!(ctx.property_type, Type::ElementReference) {
|
|
return Some(Expression::ElementReference(Rc::downgrade(elem)).into());
|
|
} else {
|
|
// Try to recover in case we wanted to access a property
|
|
let mut rest = String::new();
|
|
if let Some(LookupResult::Expression {
|
|
expression: Expression::PropertyReference(nr),
|
|
..
|
|
}) = crate::lookup::InScopeLookup.lookup(ctx, &elem.borrow().id)
|
|
{
|
|
let e = nr.element();
|
|
let e_borrowed = e.borrow();
|
|
let mut id = e_borrowed.id.as_str();
|
|
if id.is_empty() {
|
|
if ctx.component_scope.last().is_some_and(|x| Rc::ptr_eq(&e, x)) {
|
|
id = "self";
|
|
} else if ctx.component_scope.first().is_some_and(|x| Rc::ptr_eq(&e, x)) {
|
|
id = "root";
|
|
} else if ctx.component_scope.iter().nth_back(1).is_some_and(|x| Rc::ptr_eq(&e, x))
|
|
{
|
|
id = "parent";
|
|
}
|
|
};
|
|
if !id.is_empty() {
|
|
rest =
|
|
format!(". Use '{id}.{}' to access the property with the same name", nr.name());
|
|
}
|
|
} else if let Some(LookupResult::Expression {
|
|
expression: Expression::EnumerationValue(value),
|
|
..
|
|
}) = crate::lookup::ReturnTypeSpecificLookup.lookup(ctx, &elem.borrow().id)
|
|
{
|
|
rest = format!(
|
|
". Use '{}.{value}' to access the enumeration value",
|
|
value.enumeration.name
|
|
);
|
|
}
|
|
ctx.diag.push_error(format!("Cannot take reference of an element{rest}"), &node);
|
|
return None;
|
|
};
|
|
let prop_name = crate::parser::normalize_identifier(second.text());
|
|
|
|
let lookup_result = elem.borrow().lookup_property(&prop_name);
|
|
let local_to_component = lookup_result.is_local_to_component && ctx.is_local_element(elem);
|
|
|
|
if lookup_result.property_type.is_property_type() {
|
|
if !local_to_component && lookup_result.property_visibility == PropertyVisibility::Private {
|
|
ctx.diag.push_error(format!("The property '{}' is private. Annotate it with 'in', 'out' or 'in-out' to make it accessible from other components", second.text()), &second);
|
|
return None;
|
|
} else if lookup_result.property_visibility == PropertyVisibility::Fake {
|
|
ctx.diag.push_error(
|
|
"This special property can only be used to make a binding and cannot be accessed"
|
|
.to_string(),
|
|
&second,
|
|
);
|
|
return None;
|
|
} else if lookup_result.resolved_name != prop_name.as_str() {
|
|
ctx.diag.push_property_deprecation_warning(
|
|
&prop_name,
|
|
&lookup_result.resolved_name,
|
|
&second,
|
|
);
|
|
} else if let Some(deprecated) =
|
|
crate::lookup::check_deprecated_stylemetrics(elem, ctx, &prop_name)
|
|
{
|
|
ctx.diag.push_property_deprecation_warning(&prop_name, &deprecated, &second);
|
|
}
|
|
let prop = Expression::PropertyReference(NamedReference::new(
|
|
elem,
|
|
lookup_result.resolved_name.to_smolstr(),
|
|
));
|
|
maybe_lookup_object(prop.into(), it, ctx)
|
|
} else if matches!(lookup_result.property_type, Type::Callback { .. }) {
|
|
if let Some(x) = it.next() {
|
|
ctx.diag.push_error("Cannot access fields of callback".into(), &x)
|
|
}
|
|
Some(LookupResult::Callable(LookupResultCallable::Callable(Callable::Callback(
|
|
NamedReference::new(elem, lookup_result.resolved_name.to_smolstr()),
|
|
))))
|
|
} else if matches!(lookup_result.property_type, Type::Function { .. }) {
|
|
if lookup_result.property_visibility == PropertyVisibility::Private && !local_to_component {
|
|
let message = format!("The function '{}' is private. Annotate it with 'public' to make it accessible from other components", second.text());
|
|
if !lookup_result.is_local_to_component {
|
|
ctx.diag.push_error(message, &second);
|
|
} else {
|
|
ctx.diag.push_warning(message+". Note: this used to be allowed in previous version, but this should be considered an error", &second);
|
|
}
|
|
} else if lookup_result.property_visibility == PropertyVisibility::Protected
|
|
&& !local_to_component
|
|
&& !(lookup_result.is_in_direct_base
|
|
&& ctx.component_scope.first().is_some_and(|x| Rc::ptr_eq(x, elem)))
|
|
{
|
|
ctx.diag.push_error(format!("The function '{}' is protected", second.text()), &second);
|
|
}
|
|
if let Some(x) = it.next() {
|
|
ctx.diag.push_error("Cannot access fields of a function".into(), &x)
|
|
}
|
|
if let Some(f) =
|
|
elem.borrow().base_type.lookup_member_function(&lookup_result.resolved_name)
|
|
{
|
|
// builtin member function
|
|
LookupResult::Callable(LookupResultCallable::MemberFunction {
|
|
base: Expression::ElementReference(Rc::downgrade(elem)),
|
|
base_node: Some(NodeOrToken::Node(node.into())),
|
|
member: Box::new(LookupResultCallable::Callable(f.into())),
|
|
})
|
|
.into()
|
|
} else {
|
|
LookupResult::from(Callable::Function(NamedReference::new(
|
|
elem,
|
|
lookup_result.resolved_name.to_smolstr(),
|
|
)))
|
|
.into()
|
|
}
|
|
} 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::Component(c) => format!("Element '{}'", c.id),
|
|
ElementType::Builtin(b) => format!("Element '{}'", b.name),
|
|
ElementType::Native(_) => unreachable!("the native pass comes later"),
|
|
ElementType::Error => {
|
|
assert!(ctx.diag.has_errors());
|
|
return;
|
|
}
|
|
};
|
|
ctx.diag.push_error(
|
|
format!("{} does not have a property '{}'{}", what, second.text(), extra),
|
|
&second,
|
|
);
|
|
};
|
|
if let Some(minus_pos) = second.text().find('-') {
|
|
// Attempt to recover if the user wanted to write "-"
|
|
if elem
|
|
.borrow()
|
|
.lookup_property(&crate::parser::normalize_identifier(&second.text()[0..minus_pos]))
|
|
.property_type
|
|
!= Type::Invalid
|
|
{
|
|
err(". Use space before the '-' if you meant a subtraction");
|
|
return None;
|
|
}
|
|
}
|
|
err("");
|
|
None
|
|
}
|
|
}
|
|
|
|
fn maybe_lookup_object(
|
|
mut base: LookupResult,
|
|
it: impl Iterator<Item = crate::parser::SyntaxToken>,
|
|
ctx: &mut LookupCtx,
|
|
) -> Option<LookupResult> {
|
|
for next in it {
|
|
let next_str = crate::parser::normalize_identifier(next.text());
|
|
ctx.current_token = Some(next.clone().into());
|
|
match base.lookup(ctx, &next_str) {
|
|
Some(r) => {
|
|
base = r;
|
|
}
|
|
None => {
|
|
if let Some(minus_pos) = next.text().find('-') {
|
|
if base.lookup(ctx, &SmolStr::new(&next.text()[0..minus_pos])).is_some() {
|
|
ctx.diag.push_error(format!("Cannot access the field '{}'. Use space before the '-' if you meant a subtraction", next.text()), &next);
|
|
return None;
|
|
}
|
|
}
|
|
|
|
match base {
|
|
LookupResult::Callable(LookupResultCallable::Callable(Callable::Callback(
|
|
..,
|
|
))) => ctx.diag.push_error("Cannot access fields of callback".into(), &next),
|
|
LookupResult::Callable(..) => {
|
|
ctx.diag.push_error("Cannot access fields of a function".into(), &next)
|
|
}
|
|
LookupResult::Enumeration(enumeration) => ctx.diag.push_error(
|
|
format!(
|
|
"'{}' is not a member of the enum {}",
|
|
next.text(),
|
|
enumeration.name
|
|
),
|
|
&next,
|
|
),
|
|
|
|
LookupResult::Namespace(ns) => {
|
|
ctx.diag.push_error(
|
|
format!("'{}' is not a member of the namespace {}", next.text(), ns),
|
|
&next,
|
|
);
|
|
}
|
|
LookupResult::Expression { expression, .. } => {
|
|
let ty_descr = match expression.ty() {
|
|
Type::Struct { .. } => String::new(),
|
|
ty => format!(" of {ty}"),
|
|
};
|
|
ctx.diag.push_error(
|
|
format!("Cannot access the field '{}'{}", next.text(), ty_descr),
|
|
&next,
|
|
);
|
|
}
|
|
}
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
Some(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() {
|
|
recurse_elem_with_scope(
|
|
&component.root_element,
|
|
ComponentScope(vec![]),
|
|
&mut |elem, scope| {
|
|
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.is_valid() {
|
|
// An attempt to resolve this already failed when trying to resolve the property type
|
|
assert!(diag.has_errors());
|
|
continue;
|
|
}
|
|
let mut lookup_ctx = LookupCtx {
|
|
property_name: Some(prop_name.as_str()),
|
|
property_type: lhs_lookup.property_type.clone(),
|
|
component_scope: &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());
|
|
|
|
nr.element()
|
|
.borrow()
|
|
.property_analysis
|
|
.borrow_mut()
|
|
.entry(nr.name().clone())
|
|
.or_default()
|
|
.is_linked = true;
|
|
|
|
if matches!(
|
|
lhs_lookup.property_visibility,
|
|
PropertyVisibility::Private | PropertyVisibility::Output
|
|
) && !lhs_lookup.is_local_to_component
|
|
{
|
|
// invalid property assignment should have been reported earlier
|
|
assert!(diag.has_errors() || elem.borrow().is_legacy_syntax);
|
|
continue;
|
|
}
|
|
|
|
// Check the compatibility.
|
|
let mut rhs_lookup =
|
|
nr.element().borrow().lookup_property(nr.name());
|
|
if rhs_lookup.property_type == Type::Invalid {
|
|
// An attempt to resolve this already failed when trying to resolve the property type
|
|
assert!(diag.has_errors());
|
|
continue;
|
|
}
|
|
rhs_lookup.is_local_to_component &=
|
|
lookup_ctx.is_local_element(&nr.element());
|
|
|
|
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 if rhs_lookup.property_visibility
|
|
== PropertyVisibility::InOut
|
|
{
|
|
diag.push_warning("Linking input properties to input output properties is deprecated".into(), &node);
|
|
marked_linked_read_only(&nr.element(), nr.name());
|
|
} else {
|
|
// This is allowed, but then the rhs must also become read only.
|
|
marked_linked_read_only(&nr.element(), nr.name());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
fn marked_linked_read_only(elem: &ElementRc, prop_name: &str) {
|
|
elem.borrow()
|
|
.property_analysis
|
|
.borrow_mut()
|
|
.entry(prop_name.into())
|
|
.or_default()
|
|
.is_linked_to_read_only = true;
|
|
}
|
|
}
|
|
|
|
pub fn resolve_two_way_binding(
|
|
node: syntax_nodes::TwoWayBinding,
|
|
ctx: &mut LookupCtx,
|
|
) -> Option<NamedReference> {
|
|
let Some(n) = node.Expression().QualifiedName() else {
|
|
ctx.diag.push_error(
|
|
"The expression in a two way binding must be a property reference".into(),
|
|
&node.Expression(),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
let Some(r) = lookup_qualified_name_node(n, ctx, LookupPhase::ResolvingTwoWayBindings) else {
|
|
assert!(ctx.diag.has_errors());
|
|
return None;
|
|
};
|
|
|
|
// If type is invalid, error has already been reported, when inferring, the error will be reported by the inferring code
|
|
let report_error = !matches!(
|
|
ctx.property_type,
|
|
Type::InferredProperty | Type::InferredCallback | Type::Invalid
|
|
);
|
|
match r {
|
|
LookupResult::Expression { expression: Expression::PropertyReference(n), .. } => {
|
|
if report_error && n.ty() != ctx.property_type {
|
|
ctx.diag.push_error(
|
|
"The property does not have the same type as the bound property".into(),
|
|
&node,
|
|
);
|
|
}
|
|
Some(n)
|
|
}
|
|
LookupResult::Callable(LookupResultCallable::Callable(Callable::Callback(n))) => {
|
|
if report_error && n.ty() != ctx.property_type {
|
|
ctx.diag.push_error("Cannot bind to a callback".into(), &node);
|
|
None
|
|
} else {
|
|
Some(n)
|
|
}
|
|
}
|
|
LookupResult::Callable(..) => {
|
|
if report_error {
|
|
ctx.diag.push_error("Cannot bind to a function".into(), &node);
|
|
}
|
|
None
|
|
}
|
|
_ => {
|
|
ctx.diag.push_error(
|
|
"The expression in a two way binding must be a property reference".into(),
|
|
&node,
|
|
);
|
|
None
|
|
}
|
|
}
|
|
}
|