mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-02 06:41:14 +00:00

When referencing an image a repeated element and were targeting a configuration that requires resource embedding, then that image would not embedded. This was due to the fact that we didn't recurse into sub-components in the resource collection phase and the generators made a per-component embedding decision. The field responsible for that was also not propagated to sub-components. This patch addresses these two bugs by cleaning up the entire mechanism: The compiler first generates the new ResourceReference::AbsolutePath for all img!"foo.png" expressions. If the compiler is configured to embed resources, then the embed_resources pass will traverse all sub-components and expressions in them to change them to ResourceReference::EmbeddedData with a unique integer id. Simultaenously all the resources to be embedded get also collected in the root component, so that the build script as well as the generator can take care of dependency handling and actual resource embedding.
1125 lines
45 KiB
Rust
1125 lines
45 KiB
Rust
/* LICENSE BEGIN
|
|
This file is part of the SixtyFPS Project -- https://sixtyfps.io
|
|
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
|
|
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
|
|
|
|
SPDX-License-Identifier: GPL-3.0-only
|
|
This file is also available under commercial licensing terms.
|
|
Please contact info@sixtyfps.io for more information.
|
|
LICENSE END */
|
|
//! 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 actualy lies in the expression_tree module
|
|
|
|
use crate::diagnostics::BuildDiagnostics;
|
|
use crate::expression_tree::*;
|
|
use crate::langtype::Type;
|
|
use crate::object_tree::*;
|
|
use crate::parser::{
|
|
identifier_text, syntax_nodes, NodeOrTokenWithSourceFile, SyntaxKind, SyntaxNodeWithSourceFile,
|
|
};
|
|
use crate::typeregister::TypeRegister;
|
|
use std::{collections::HashMap, rc::Rc};
|
|
|
|
/// This represeresent a scope for the Component, where Component is the repeated component, but
|
|
/// does not represent a component in the .60 file
|
|
#[derive(Clone)]
|
|
struct ComponentScope(Vec<ElementRc>);
|
|
|
|
fn resolve_expression(
|
|
expr: &mut Expression,
|
|
property_name: Option<&str>,
|
|
property_type: Type,
|
|
scope: &ComponentScope,
|
|
type_register: &TypeRegister,
|
|
diag: &mut BuildDiagnostics,
|
|
) {
|
|
if let Expression::Uncompiled(node) = expr {
|
|
let mut lookup_ctx = LookupCtx {
|
|
property_name,
|
|
property_type,
|
|
component_scope: &scope.0,
|
|
diag,
|
|
arguments: vec![],
|
|
type_register,
|
|
};
|
|
|
|
let new_expr = match node.kind() {
|
|
SyntaxKind::SignalConnection => {
|
|
//FIXME: proper signal suport (node is a codeblock)
|
|
Expression::from_signal_connection(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(),
|
|
lookup_ctx.symmetric_parent_property(),
|
|
node,
|
|
diag,
|
|
)
|
|
}
|
|
SyntaxKind::BindingExpression => {
|
|
Expression::from_binding_expression_node(node.clone(), &mut lookup_ctx)
|
|
}
|
|
SyntaxKind::TwoWayBinding => {
|
|
Expression::from_two_way_binding(node.clone().into(), &mut lookup_ctx)
|
|
}
|
|
_ => {
|
|
debug_assert!(diag.has_error());
|
|
Expression::Invalid
|
|
}
|
|
};
|
|
*expr = new_expr;
|
|
}
|
|
}
|
|
|
|
pub fn resolve_expressions(doc: &Document, diag: &mut BuildDiagnostics) {
|
|
for component in doc.inner_components.iter() {
|
|
let scope = ComponentScope(vec![component.root_element.clone()]);
|
|
|
|
recurse_elem(&component.root_element, &scope, &mut |elem, scope| {
|
|
let mut new_scope = scope.clone();
|
|
let mut is_repeated = elem.borrow().repeated.is_some();
|
|
if is_repeated {
|
|
new_scope.0.push(elem.clone())
|
|
}
|
|
new_scope.0.push(elem.clone());
|
|
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
|
|
debug_assert!(elem.borrow().repeated.as_ref().is_none()); // should be none because it is taken by the visit_element_expressions function
|
|
resolve_expression(
|
|
expr,
|
|
property_name,
|
|
property_type(),
|
|
scope,
|
|
&doc.local_registry,
|
|
diag,
|
|
);
|
|
is_repeated = false;
|
|
} else {
|
|
resolve_expression(
|
|
expr,
|
|
property_name,
|
|
property_type(),
|
|
&new_scope,
|
|
&doc.local_registry,
|
|
diag,
|
|
)
|
|
}
|
|
});
|
|
new_scope.0.pop();
|
|
new_scope
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Contains information which allow to lookup identifier in expressions
|
|
pub struct LookupCtx<'a> {
|
|
/// the name of the property for which this expression refers.
|
|
property_name: Option<&'a str>,
|
|
|
|
/// the type of the property for which this expression refers.
|
|
/// (some property come in the scope)
|
|
property_type: Type,
|
|
|
|
/// Here is the stack in which id applies
|
|
component_scope: &'a [ElementRc],
|
|
|
|
/// Somewhere to report diagnostics
|
|
diag: &'a mut BuildDiagnostics,
|
|
|
|
/// The name of the arguments of the signal or function
|
|
arguments: Vec<String>,
|
|
|
|
/// The type register in which to look for Globals
|
|
type_register: &'a TypeRegister,
|
|
}
|
|
|
|
impl<'a> LookupCtx<'a> {
|
|
pub fn symmetric_parent_property(&self) -> Option<(Type, NamedReference)> {
|
|
self.component_scope.last().and_then(find_parent_element).and_then(|parent| {
|
|
self.property_name.and_then(|name| {
|
|
let ty = parent.borrow().lookup_property(name);
|
|
if ty.is_property_type() {
|
|
Some((
|
|
ty,
|
|
NamedReference { element: Rc::downgrade(&parent), name: name.to_string() },
|
|
))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
/// Return a context that is just suitable to build simple const expression
|
|
pub fn empty_context(type_register: &'a TypeRegister, diag: &'a mut BuildDiagnostics) -> Self {
|
|
Self {
|
|
property_name: Default::default(),
|
|
property_type: Default::default(),
|
|
component_scope: Default::default(),
|
|
diag,
|
|
arguments: Default::default(),
|
|
type_register,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn find_element_by_id(roots: &[ElementRc], name: &str) -> Option<ElementRc> {
|
|
for e in roots.iter().rev() {
|
|
if e.borrow().id == name {
|
|
return Some(e.clone());
|
|
}
|
|
for x in &e.borrow().children {
|
|
if x.borrow().repeated.is_some() {
|
|
continue;
|
|
}
|
|
if let Some(x) = find_element_by_id(&[x.clone()], name) {
|
|
return Some(x);
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Find the parent element to a given element.
|
|
/// (since there is no parent mapping we need to fo an exhaustive search)
|
|
fn find_parent_element(e: &ElementRc) -> Option<ElementRc> {
|
|
fn recurse(base: &ElementRc, e: &ElementRc) -> Option<ElementRc> {
|
|
for child in &base.borrow().children {
|
|
if Rc::ptr_eq(child, e) {
|
|
return Some(base.clone());
|
|
}
|
|
if let Some(x) = recurse(child, e) {
|
|
return Some(x);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
let root = e.borrow().enclosing_component.upgrade().unwrap().root_element.clone();
|
|
if Rc::ptr_eq(&root, e) {
|
|
return None;
|
|
}
|
|
recurse(&root, e)
|
|
}
|
|
|
|
impl Expression {
|
|
pub fn from_binding_expression_node(
|
|
node: SyntaxNodeWithSourceFile,
|
|
ctx: &mut LookupCtx,
|
|
) -> Self {
|
|
debug_assert_eq!(node.kind(), SyntaxKind::BindingExpression);
|
|
let e = node
|
|
.child_node(SyntaxKind::Expression)
|
|
.map(|n| Self::from_expression_node(n.into(), ctx))
|
|
.or_else(|| {
|
|
node.child_node(SyntaxKind::CodeBlock)
|
|
.map(|c| Self::from_codeblock_node(c.into(), ctx))
|
|
})
|
|
.unwrap_or(Self::Invalid);
|
|
e.maybe_convert_to(
|
|
ctx.property_type.clone(),
|
|
ctx.symmetric_parent_property(),
|
|
&node,
|
|
&mut ctx.diag,
|
|
)
|
|
}
|
|
|
|
fn from_codeblock_node(node: syntax_nodes::CodeBlock, ctx: &mut LookupCtx) -> Expression {
|
|
debug_assert_eq!(node.kind(), SyntaxKind::CodeBlock);
|
|
Expression::CodeBlock(
|
|
node.children()
|
|
.filter(|n| n.kind() == SyntaxKind::Expression)
|
|
.map(|n| Self::from_expression_node(n.into(), ctx))
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
fn from_signal_connection(
|
|
node: syntax_nodes::SignalConnection,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
ctx.arguments =
|
|
node.DeclaredIdentifier().map(|x| identifier_text(&x).unwrap_or_default()).collect();
|
|
Self::from_codeblock_node(node.CodeBlock(), ctx)
|
|
}
|
|
|
|
fn from_two_way_binding(node: syntax_nodes::TwoWayBinding, ctx: &mut LookupCtx) -> Expression {
|
|
let e = Self::from_expression_node(node.Expression(), ctx);
|
|
let ty = e.ty();
|
|
match e {
|
|
Expression::PropertyReference(n) => {
|
|
if ty != ctx.property_type {
|
|
ctx.diag.push_error(
|
|
"The property does not have the same type as the bound property".into(),
|
|
&node,
|
|
);
|
|
}
|
|
Expression::TwoWayBinding(n, None)
|
|
}
|
|
_ => {
|
|
ctx.diag.push_error(
|
|
"The expression in a two way binding must be a property reference".into(),
|
|
&node,
|
|
);
|
|
e
|
|
}
|
|
}
|
|
}
|
|
|
|
fn from_expression_node(node: syntax_nodes::Expression, ctx: &mut LookupCtx) -> Self {
|
|
node.Expression()
|
|
.map(|n| Self::from_expression_node(n, ctx))
|
|
.or_else(|| {
|
|
node.BangExpression().map(|n| Self::from_bang_expression_node(n.into(), ctx))
|
|
})
|
|
.or_else(|| node.QualifiedName().map(|s| Self::from_qualified_name_node(s.into(), ctx)))
|
|
.or_else(|| {
|
|
node.child_text(SyntaxKind::StringLiteral).map(|s| {
|
|
unescape_string(&s).map(Self::StringLiteral).unwrap_or_else(|| {
|
|
ctx.diag.push_error("Cannot parse string literal".into(), &node);
|
|
Self::Invalid
|
|
})
|
|
})
|
|
})
|
|
.or_else(|| {
|
|
node.child_text(SyntaxKind::NumberLiteral)
|
|
.map(parse_number_literal)
|
|
.transpose()
|
|
.unwrap_or_else(|e| {
|
|
ctx.diag.push_error(e, &node);
|
|
Some(Self::Invalid)
|
|
})
|
|
})
|
|
.or_else(|| {
|
|
node.child_text(SyntaxKind::ColorLiteral).map(|s| {
|
|
parse_color_literal(&s)
|
|
.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
|
|
})
|
|
})
|
|
})
|
|
.or_else(|| {
|
|
node.FunctionCallExpression().map(|n| Self::from_function_call_node(n, ctx))
|
|
})
|
|
.or_else(|| node.SelfAssignment().map(|n| Self::from_self_assignement_node(n, ctx)))
|
|
.or_else(|| node.BinaryExpression().map(|n| Self::from_binary_expression_node(n, ctx)))
|
|
.or_else(|| {
|
|
node.UnaryOpExpression().map(|n| Self::from_unaryop_expression_node(n, ctx))
|
|
})
|
|
.or_else(|| {
|
|
node.ConditionalExpression().map(|n| Self::from_conditional_expression_node(n, ctx))
|
|
})
|
|
.or_else(|| node.ObjectLiteral().map(|n| Self::from_object_literal_node(n, ctx)))
|
|
.or_else(|| node.Array().map(|n| Self::from_array_node(n, ctx)))
|
|
.or_else(|| node.CodeBlock().map(|n| Self::from_codeblock_node(n, ctx)))
|
|
.unwrap_or(Self::Invalid)
|
|
}
|
|
|
|
fn from_bang_expression_node(node: SyntaxNodeWithSourceFile, ctx: &mut LookupCtx) -> Self {
|
|
match identifier_text(&node).as_ref().map(|x| x.as_str()) {
|
|
None => {
|
|
debug_assert!(false, "the parser should not allow that");
|
|
ctx.diag.push_error("Missing bang keyword".into(), &node);
|
|
return Self::Invalid;
|
|
}
|
|
Some("img") => {
|
|
// FIXME: we probably need a better syntax and make this at another level.
|
|
let s = match node
|
|
.child_node(SyntaxKind::Expression)
|
|
.map_or(Self::Invalid, |n| Self::from_expression_node(n.into(), ctx))
|
|
{
|
|
Expression::StringLiteral(p) => p,
|
|
_ => {
|
|
ctx.diag.push_error("img! Must be followed by a valid path".into(), &node);
|
|
return Self::Invalid;
|
|
}
|
|
};
|
|
|
|
let absolute_source_path = {
|
|
let path = std::path::Path::new(&s);
|
|
|
|
if path.is_absolute() || s.starts_with("http://") || s.starts_with("https://") {
|
|
s
|
|
} else {
|
|
let path = node
|
|
.source_file
|
|
.unwrap_or_default()
|
|
.parent()
|
|
.map(|b| b.join(path))
|
|
.unwrap_or_else(|| path.to_owned());
|
|
if path.is_absolute() {
|
|
path.to_string_lossy().to_string()
|
|
} else {
|
|
std::env::current_dir()
|
|
.map(|b| b.join(&path))
|
|
.unwrap_or(path)
|
|
.to_string_lossy()
|
|
.to_string()
|
|
}
|
|
}
|
|
};
|
|
|
|
Expression::ResourceReference(ResourceReference::AbsolutePath(absolute_source_path))
|
|
}
|
|
Some(x) => {
|
|
ctx.diag.push_error(format!("Unknown bang keyword `{}`", x), &node);
|
|
return Self::Invalid;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Perform the lookup
|
|
fn from_qualified_name_node(node: SyntaxNodeWithSourceFile, ctx: &mut LookupCtx) -> Self {
|
|
debug_assert_eq!(node.kind(), SyntaxKind::QualifiedName);
|
|
|
|
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_error());
|
|
return Self::Invalid;
|
|
};
|
|
|
|
let first_str = crate::parser::normalize_identifier(first.text().as_str());
|
|
|
|
if let Some(index) = ctx.arguments.iter().position(|x| x == &first_str) {
|
|
let ty = match &ctx.property_type {
|
|
Type::Signal { args } | Type::Function { args, .. } => args[index].clone(),
|
|
_ => panic!("There should only be argument within functions or signal"),
|
|
};
|
|
let e = Expression::FunctionParameterReference { index, ty };
|
|
return maybe_lookup_object(e, it, ctx);
|
|
}
|
|
|
|
let elem_opt = match first_str.as_str() {
|
|
"self" => ctx.component_scope.last().cloned(),
|
|
"parent" => ctx.component_scope.last().and_then(find_parent_element),
|
|
"true" => return Self::BoolLiteral(true),
|
|
"false" => return Self::BoolLiteral(false),
|
|
_ => find_element_by_id(ctx.component_scope, &first_str).or_else(|| {
|
|
if let Type::Component(c) = ctx.type_register.lookup(&first_str) {
|
|
if c.is_global() {
|
|
return Some(c.root_element.clone());
|
|
}
|
|
}
|
|
None
|
|
}),
|
|
};
|
|
|
|
if let Some(elem) = elem_opt {
|
|
return continue_lookup_within_element(&elem, &mut it, node, ctx);
|
|
}
|
|
|
|
for elem in ctx.component_scope.iter().rev() {
|
|
if let Some(repeated) = &elem.borrow().repeated {
|
|
if first_str == repeated.index_id {
|
|
return Expression::RepeaterIndexReference { element: Rc::downgrade(elem) };
|
|
} else if first_str == repeated.model_data_id {
|
|
let base = Expression::RepeaterModelReference { element: Rc::downgrade(elem) };
|
|
return maybe_lookup_object(base, it, ctx);
|
|
}
|
|
}
|
|
|
|
let property = elem.borrow().lookup_property(&first_str);
|
|
if property.is_property_type() {
|
|
let prop = Self::PropertyReference(NamedReference {
|
|
element: Rc::downgrade(&elem),
|
|
name: first_str.to_string(),
|
|
});
|
|
return maybe_lookup_object(prop, it, ctx);
|
|
} else if matches!(property, Type::Signal{..}) {
|
|
if let Some(x) = it.next() {
|
|
ctx.diag.push_error("Cannot access fields of signal".into(), &x)
|
|
}
|
|
return Self::SignalReference(NamedReference {
|
|
element: Rc::downgrade(&elem),
|
|
name: first_str.to_string(),
|
|
});
|
|
} else if property.is_object_type() {
|
|
todo!("Continue lookling up");
|
|
}
|
|
}
|
|
|
|
if it.next().is_some() {
|
|
ctx.diag.push_error(format!("Cannot access id '{}'", first_str), &node);
|
|
return Expression::Invalid;
|
|
}
|
|
|
|
match &ctx.property_type {
|
|
Type::Color => {
|
|
if let Some(c) = css_color_parser2::NAMED_COLORS.get(first_str.as_str()) {
|
|
let value = ((c.a as u32 * 255) << 24)
|
|
| ((c.r as u32) << 16)
|
|
| ((c.g as u32) << 8)
|
|
| (c.b as u32);
|
|
return Expression::Cast {
|
|
from: Box::new(Expression::NumberLiteral(value as f64, Unit::None)),
|
|
to: Type::Color,
|
|
};
|
|
}
|
|
}
|
|
Type::Easing => {
|
|
// These value are coming from CSSn with - replaced by _
|
|
let value = match first_str.as_str() {
|
|
"linear" => Some(EasingCurve::Linear),
|
|
"ease" => Some(EasingCurve::CubicBezier(0.25, 0.1, 0.25, 1.0)),
|
|
"ease_in" => Some(EasingCurve::CubicBezier(0.42, 0.0, 1.0, 1.0)),
|
|
"ease_in_out" => Some(EasingCurve::CubicBezier(0.42, 0.0, 0.58, 1.0)),
|
|
"ease_out" => Some(EasingCurve::CubicBezier(0.0, 0.0, 0.58, 1.0)),
|
|
"cubic_bezier" => {
|
|
return Expression::BuiltinMacroReference(
|
|
BuiltinMacroFunction::CubicBezier,
|
|
first.into(),
|
|
)
|
|
}
|
|
_ => None,
|
|
};
|
|
if let Some(curve) = value {
|
|
return Expression::EasingCurve(curve);
|
|
}
|
|
}
|
|
Type::Enumeration(enumeration) => {
|
|
if let Some(value) = enumeration.clone().try_value_from_string(&first_str) {
|
|
return Expression::EnumerationValue(value);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
// Builtin functions FIXME: handle that in a registery or something
|
|
match first_str.as_str() {
|
|
"debug" => return Expression::BuiltinFunctionReference(BuiltinFunction::Debug),
|
|
"mod" => return Expression::BuiltinFunctionReference(BuiltinFunction::Mod),
|
|
"round" => return Expression::BuiltinFunctionReference(BuiltinFunction::Round),
|
|
"ceil" => return Expression::BuiltinFunctionReference(BuiltinFunction::Ceil),
|
|
"floor" => return Expression::BuiltinFunctionReference(BuiltinFunction::Floor),
|
|
"max" => {
|
|
return Expression::BuiltinMacroReference(BuiltinMacroFunction::Max, first.into())
|
|
}
|
|
"min" => {
|
|
return Expression::BuiltinMacroReference(BuiltinMacroFunction::Min, first.into())
|
|
}
|
|
_ => {}
|
|
};
|
|
|
|
// Attempt to recover if the user wanted to write "-"
|
|
if let Some(minus_pos) = first.text().find('-') {
|
|
let report_minus_error = |ctx: &mut LookupCtx| {
|
|
ctx.diag.push_error(format!("Unknown unqualified identifier '{}'. Use space before the '-' if you meant a substraction.", first.text()), &node);
|
|
};
|
|
let first_str = &first.text()[0..minus_pos];
|
|
for elem in ctx.component_scope.iter().rev() {
|
|
if let Some(repeated) = &elem.borrow().repeated {
|
|
if first_str == repeated.index_id || first_str == repeated.model_data_id {
|
|
report_minus_error(ctx);
|
|
return Expression::Invalid;
|
|
}
|
|
}
|
|
|
|
let property = elem.borrow().lookup_property(&first_str);
|
|
if property.is_property_type() {
|
|
report_minus_error(ctx);
|
|
return Expression::Invalid;
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.diag.push_error(format!("Unknown unqualified identifier '{}'", first.text()), &node);
|
|
|
|
Self::Invalid
|
|
}
|
|
|
|
fn from_function_call_node(
|
|
node: syntax_nodes::FunctionCallExpression,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let mut sub_expr = node.Expression().map(|n| {
|
|
(Self::from_expression_node(n.clone(), ctx), NodeOrTokenWithSourceFile::from(n.0))
|
|
});
|
|
|
|
let mut arguments = Vec::new();
|
|
|
|
let (function, f_node) =
|
|
sub_expr.next().unwrap_or_else(|| (Expression::Invalid, node.0.clone().into()));
|
|
|
|
let function = match function {
|
|
Expression::BuiltinMacroReference(mac, n) => match mac {
|
|
BuiltinMacroFunction::Min => {
|
|
return min_max_macro(n, '<', sub_expr.collect(), &mut ctx.diag);
|
|
}
|
|
BuiltinMacroFunction::Max => {
|
|
return min_max_macro(n, '>', sub_expr.collect(), &mut ctx.diag);
|
|
}
|
|
BuiltinMacroFunction::CubicBezier => {
|
|
let mut has_error = None;
|
|
// FIXME: this is not pretty to be handling there.
|
|
// Maybe "cubic_bezier" should be a function that is lowered later
|
|
let mut a = || match sub_expr.next() {
|
|
None => {
|
|
has_error.get_or_insert((f_node.clone(), "Not enough arguments"));
|
|
0.
|
|
}
|
|
Some((Expression::NumberLiteral(val, Unit::None), _)) => val as f32,
|
|
Some((_, n)) => {
|
|
has_error.get_or_insert((
|
|
n,
|
|
"Arguments to cubic bezier curve must be number literal",
|
|
));
|
|
0.
|
|
}
|
|
};
|
|
let expr =
|
|
Expression::EasingCurve(EasingCurve::CubicBezier(a(), a(), a(), a()));
|
|
if let Some((_, n)) = sub_expr.next() {
|
|
has_error.get_or_insert((n, "Too many argument for bezier curve"));
|
|
}
|
|
if let Some((n, msg)) = has_error {
|
|
ctx.diag.push_error(msg.into(), &n);
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
},
|
|
Expression::MemberFunction { base, base_node, member } => {
|
|
arguments.push((*base, base_node));
|
|
member
|
|
}
|
|
_ => Box::new(function),
|
|
};
|
|
arguments.extend(sub_expr);
|
|
|
|
let arguments = match function.ty() {
|
|
Type::Function { args, .. } | Type::Signal { args } => {
|
|
if arguments.len() != args.len() {
|
|
ctx.diag.push_error(
|
|
format!(
|
|
"The signal or function expects {} arguments, but {} are provided",
|
|
args.len(),
|
|
arguments.len()
|
|
),
|
|
&node,
|
|
);
|
|
arguments.into_iter().map(|x| x.0).collect()
|
|
} else {
|
|
arguments
|
|
.into_iter()
|
|
.zip(args.iter())
|
|
.map(|((e, node), ty)| {
|
|
e.maybe_convert_to(ty.clone(), None, &node, &mut ctx.diag)
|
|
})
|
|
.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 }
|
|
}
|
|
|
|
fn from_self_assignement_node(
|
|
node: syntax_nodes::SelfAssignment,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let (lhs_n, rhs_n) = node.Expression();
|
|
let lhs = Self::from_expression_node(lhs_n.into(), ctx);
|
|
let op = None
|
|
.or(node.child_token(SyntaxKind::PlusEqual).and(Some('+')))
|
|
.or(node.child_token(SyntaxKind::MinusEqual).and(Some('-')))
|
|
.or(node.child_token(SyntaxKind::StarEqual).and(Some('*')))
|
|
.or(node.child_token(SyntaxKind::DivEqual).and(Some('/')))
|
|
.or(node.child_token(SyntaxKind::Equal).and(Some('=')))
|
|
.unwrap_or('_');
|
|
if !lhs.is_rw() && lhs.ty() != Type::Invalid {
|
|
ctx.diag.push_error(
|
|
format!(
|
|
"{} need to be done on a property",
|
|
if op == '=' { "Assignement" } else { "Self assignement" }
|
|
),
|
|
&node,
|
|
);
|
|
}
|
|
let rhs = Self::from_expression_node(rhs_n.clone().into(), ctx).maybe_convert_to(
|
|
lhs.ty(),
|
|
None,
|
|
&rhs_n,
|
|
&mut ctx.diag,
|
|
);
|
|
Expression::SelfAssignment { lhs: Box::new(lhs), rhs: Box::new(rhs), op }
|
|
}
|
|
|
|
fn from_binary_expression_node(
|
|
node: syntax_nodes::BinaryExpression,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let op = None
|
|
.or(node.child_token(SyntaxKind::Plus).and(Some('+')))
|
|
.or(node.child_token(SyntaxKind::Minus).and(Some('-')))
|
|
.or(node.child_token(SyntaxKind::Star).and(Some('*')))
|
|
.or(node.child_token(SyntaxKind::Div).and(Some('/')))
|
|
.or(node.child_token(SyntaxKind::LessEqual).and(Some('≤')))
|
|
.or(node.child_token(SyntaxKind::GreaterEqual).and(Some('≥')))
|
|
.or(node.child_token(SyntaxKind::LAngle).and(Some('<')))
|
|
.or(node.child_token(SyntaxKind::RAngle).and(Some('>')))
|
|
.or(node.child_token(SyntaxKind::EqualEqual).and(Some('=')))
|
|
.or(node.child_token(SyntaxKind::NotEqual).and(Some('!')))
|
|
.or(node.child_token(SyntaxKind::AndAnd).and(Some('&')))
|
|
.or(node.child_token(SyntaxKind::OrOr).and(Some('|')))
|
|
.unwrap_or('_');
|
|
|
|
let (lhs_n, rhs_n) = node.Expression();
|
|
let lhs = Self::from_expression_node(lhs_n.clone().into(), ctx);
|
|
let rhs = Self::from_expression_node(rhs_n.clone().into(), ctx);
|
|
|
|
let expected_ty = match operator_class(op) {
|
|
OperatorClass::ComparisonOp => {
|
|
let (lhs_ty, rhs_ty) = (lhs.ty(), rhs.ty());
|
|
if rhs_ty.can_convert(&lhs_ty) {
|
|
lhs_ty
|
|
} else {
|
|
rhs_ty
|
|
}
|
|
}
|
|
OperatorClass::LogicalOp => Type::Bool,
|
|
OperatorClass::ArithmeticOp => {
|
|
macro_rules! unit_operations {
|
|
($($unit:ident)*) => {
|
|
match (op, lhs.ty(), rhs.ty()) {
|
|
('+', Type::String, _) => Type::String,
|
|
('+', _, Type::String) => Type::String,
|
|
|
|
$(
|
|
('+', Type::$unit, _) => Type::$unit,
|
|
('-', Type::$unit, _) => Type::$unit,
|
|
('*', Type::$unit, _) => {
|
|
return Expression::BinaryExpression {
|
|
lhs: Box::new(lhs),
|
|
rhs: Box::new(rhs.maybe_convert_to(
|
|
Type::Float32,
|
|
None,
|
|
&lhs_n,
|
|
&mut ctx.diag,
|
|
)),
|
|
op,
|
|
}
|
|
}
|
|
('*', _, Type::$unit) => {
|
|
return Expression::BinaryExpression {
|
|
lhs: Box::new(lhs.maybe_convert_to(
|
|
Type::Float32,
|
|
None,
|
|
&lhs_n,
|
|
&mut ctx.diag,
|
|
)),
|
|
rhs: Box::new(rhs),
|
|
op,
|
|
}
|
|
}
|
|
('/', Type::$unit, Type::$unit) => {
|
|
return Expression::BinaryExpression {
|
|
lhs: Box::new(lhs),
|
|
rhs: Box::new(rhs),
|
|
op,
|
|
}
|
|
}
|
|
('/', Type::$unit, _) => {
|
|
return Expression::BinaryExpression {
|
|
lhs: Box::new(lhs),
|
|
rhs: Box::new(rhs.maybe_convert_to(
|
|
Type::Float32,
|
|
None,
|
|
&lhs_n,
|
|
&mut ctx.diag,
|
|
)),
|
|
op,
|
|
}
|
|
}
|
|
)*
|
|
_ => Type::Float32,
|
|
}
|
|
};
|
|
}
|
|
unit_operations!(Duration Length LogicalLength)
|
|
}
|
|
};
|
|
Expression::BinaryExpression {
|
|
lhs: Box::new(lhs.maybe_convert_to(expected_ty.clone(), None, &lhs_n, &mut ctx.diag)),
|
|
rhs: Box::new(rhs.maybe_convert_to(expected_ty, None, &rhs_n, &mut 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.clone().into(), ctx);
|
|
|
|
Expression::UnaryOp {
|
|
sub: Box::new(exp),
|
|
op: None
|
|
.or(node.child_token(SyntaxKind::Plus).and(Some('+')))
|
|
.or(node.child_token(SyntaxKind::Minus).and(Some('-')))
|
|
.or(node.child_token(SyntaxKind::Bang).and(Some('!')))
|
|
.unwrap_or('_'),
|
|
}
|
|
}
|
|
|
|
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().into(), ctx)
|
|
.maybe_convert_to(Type::Bool, None, &condition_n, &mut ctx.diag);
|
|
let mut true_expr = Self::from_expression_node(true_expr_n.clone().into(), ctx);
|
|
let mut false_expr = Self::from_expression_node(false_expr_n.clone().into(), ctx);
|
|
let (true_ty, false_ty) = (true_expr.ty(), false_expr.ty());
|
|
if true_ty != false_ty {
|
|
if false_ty.can_convert(&true_ty) {
|
|
false_expr =
|
|
false_expr.maybe_convert_to(true_ty, None, &false_expr_n, &mut ctx.diag);
|
|
} else {
|
|
true_expr = true_expr.maybe_convert_to(false_ty, None, &true_expr_n, &mut ctx.diag);
|
|
}
|
|
}
|
|
Expression::Condition {
|
|
condition: Box::new(condition),
|
|
true_expr: Box::new(true_expr),
|
|
false_expr: Box::new(false_expr),
|
|
}
|
|
}
|
|
|
|
fn from_object_literal_node(
|
|
node: syntax_nodes::ObjectLiteral,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let values: HashMap<String, Expression> = node
|
|
.ObjectMember()
|
|
.map(|n| {
|
|
(
|
|
identifier_text(&n).unwrap_or_default(),
|
|
Expression::from_expression_node(n.Expression(), ctx),
|
|
)
|
|
})
|
|
.collect();
|
|
let ty = Type::Object {
|
|
fields: values.iter().map(|(k, v)| (k.clone(), v.ty())).collect(),
|
|
name: None,
|
|
};
|
|
Expression::Object { 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();
|
|
|
|
// FIXME: what's the type of an empty array ?
|
|
// Also, be smarter about finding a common type
|
|
let element_ty = values.first().map_or(Type::Invalid, |e| e.ty());
|
|
|
|
for e in values.iter_mut() {
|
|
*e = core::mem::replace(e, Expression::Invalid).maybe_convert_to(
|
|
element_ty.clone(),
|
|
None,
|
|
&node,
|
|
ctx.diag,
|
|
);
|
|
}
|
|
|
|
Expression::Array { element_ty, values }
|
|
}
|
|
}
|
|
|
|
fn min_max_macro(
|
|
node: NodeOrTokenWithSourceFile,
|
|
op: char,
|
|
args: Vec<(Expression, NodeOrTokenWithSourceFile)>,
|
|
diag: &mut BuildDiagnostics,
|
|
) -> Expression {
|
|
if args.is_empty() {
|
|
diag.push_error("Needs at least one argument".into(), &node);
|
|
return Expression::Invalid;
|
|
}
|
|
let mut args = args.into_iter();
|
|
let (mut base, arg_node) = args.next().unwrap();
|
|
let ty = match base.ty() {
|
|
Type::Float32 => Type::Float32,
|
|
// In case there are other floats, we don't want to conver tthe result to int
|
|
Type::Int32 => Type::Float32,
|
|
Type::Length => Type::Length,
|
|
Type::LogicalLength => Type::LogicalLength,
|
|
Type::Duration => Type::Duration,
|
|
Type::Percent => Type::Float32,
|
|
_ => {
|
|
diag.push_error("Invalid argument type".into(), &arg_node);
|
|
return Expression::Invalid;
|
|
}
|
|
};
|
|
while let Some((next, arg_node)) = args.next() {
|
|
let rhs = next.maybe_convert_to(ty.clone(), None, &arg_node, diag);
|
|
static COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(1);
|
|
let id = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
let n1 = format!("minmax_lhs{}", id);
|
|
let n2 = format!("minmax_rhs{}", id);
|
|
let a1 = Box::new(Expression::ReadLocalVariable { name: n1.clone(), ty: ty.clone() });
|
|
let a2 = Box::new(Expression::ReadLocalVariable { name: n2.clone(), ty: ty.clone() });
|
|
base = Expression::CodeBlock(vec![
|
|
Expression::StoreLocalVariable { name: n1, value: Box::new(base) },
|
|
Expression::StoreLocalVariable { name: n2, value: Box::new(rhs) },
|
|
Expression::Condition {
|
|
condition: Box::new(Expression::BinaryExpression {
|
|
lhs: a1.clone(),
|
|
rhs: a2.clone(),
|
|
op,
|
|
}),
|
|
true_expr: a1,
|
|
false_expr: a2,
|
|
},
|
|
]);
|
|
}
|
|
base
|
|
}
|
|
|
|
fn continue_lookup_within_element(
|
|
elem: &ElementRc,
|
|
it: &mut impl Iterator<Item = crate::parser::SyntaxTokenWithSourceFile>,
|
|
node: SyntaxNodeWithSourceFile,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
let second = if let Some(second) = it.next() {
|
|
second
|
|
} else if matches!(ctx.property_type, Type::ElementReference) {
|
|
return Expression::ElementReference(Rc::downgrade(elem));
|
|
} else {
|
|
ctx.diag.push_error("Cannot take reference of an element".into(), &node);
|
|
return Expression::Invalid;
|
|
};
|
|
let prop_name = crate::parser::normalize_identifier(second.text().as_str());
|
|
|
|
let p = elem.borrow().lookup_property(&prop_name);
|
|
if p.is_property_type() {
|
|
let prop = Expression::PropertyReference(NamedReference {
|
|
element: Rc::downgrade(elem),
|
|
name: prop_name,
|
|
});
|
|
return maybe_lookup_object(prop, it, ctx);
|
|
} else if matches!(p, Type::Signal{..}) {
|
|
if let Some(x) = it.next() {
|
|
ctx.diag.push_error("Cannot access fields of signal".into(), &x)
|
|
}
|
|
return Expression::SignalReference(NamedReference {
|
|
element: Rc::downgrade(elem),
|
|
name: prop_name.to_string(),
|
|
});
|
|
} else if matches!(p, Type::Function{..}) {
|
|
let member = elem.borrow().base_type.lookup_member_function(&prop_name);
|
|
return Expression::MemberFunction {
|
|
base: Box::new(Expression::ElementReference(Rc::downgrade(elem))),
|
|
base_node: node.into(),
|
|
member: Box::new(member),
|
|
};
|
|
} else {
|
|
if let Some(minus_pos) = second.text().find('-') {
|
|
// Attempt to recover if the user wanted to write "-"
|
|
if elem.borrow().lookup_property(&second.text()[0..minus_pos]) != Type::Invalid {
|
|
ctx.diag.push_error(format!("Cannot access property '{}'. Use space before the '-' if you meant a substraction.", second.text()), &second);
|
|
return Expression::Invalid;
|
|
}
|
|
}
|
|
ctx.diag.push_error(format!("Cannot access property '{}'", second.text()), &second);
|
|
return Expression::Invalid;
|
|
}
|
|
}
|
|
|
|
fn maybe_lookup_object(
|
|
mut base: Expression,
|
|
mut it: impl Iterator<Item = crate::parser::SyntaxTokenWithSourceFile>,
|
|
ctx: &mut LookupCtx,
|
|
) -> Expression {
|
|
fn error_or_try_minus(
|
|
ctx: &mut LookupCtx,
|
|
ident: crate::parser::SyntaxTokenWithSourceFile,
|
|
lookup: impl Fn(&str) -> bool,
|
|
) -> Expression {
|
|
if let Some(minus_pos) = ident.text().find('-') {
|
|
if lookup(&ident.text()[0..minus_pos]) {
|
|
ctx.diag.push_error(format!("Cannot access the field '{}'. Use space before the '-' if you meant a substraction.", ident.text()), &ident);
|
|
return Expression::Invalid;
|
|
}
|
|
}
|
|
ctx.diag.push_error(format!("Cannot access the field '{}'", ident.text()), &ident);
|
|
Expression::Invalid
|
|
}
|
|
|
|
while let Some(next) = it.next() {
|
|
let next_str = crate::parser::normalize_identifier(next.text().as_str());
|
|
match base.ty() {
|
|
Type::Object { fields, .. } => {
|
|
if fields.get(next_str.as_str()).is_some() {
|
|
base = Expression::ObjectAccess {
|
|
base: Box::new(std::mem::replace(&mut base, Expression::Invalid)),
|
|
name: next_str,
|
|
}
|
|
} else {
|
|
return error_or_try_minus(ctx, next, |x| fields.get(x).is_some());
|
|
}
|
|
}
|
|
Type::Component(c) => {
|
|
let prop_ty = c.root_element.borrow().lookup_property(next_str.as_str());
|
|
if prop_ty != Type::Invalid {
|
|
base = Expression::ObjectAccess {
|
|
base: Box::new(std::mem::replace(&mut base, Expression::Invalid)),
|
|
name: next.to_string(),
|
|
}
|
|
} else {
|
|
return error_or_try_minus(ctx, next, |x| {
|
|
c.root_element.borrow().lookup_property(x) != Type::Invalid
|
|
});
|
|
}
|
|
}
|
|
Type::String => {
|
|
return Expression::MemberFunction {
|
|
base: Box::new(base),
|
|
base_node: next.clone().into(), // Note that this is not the base_node, but the function's node
|
|
member: Box::new(match next_str.as_str() {
|
|
"is_float" => {
|
|
Expression::BuiltinFunctionReference(BuiltinFunction::StringIsFloat)
|
|
}
|
|
"to_float" => {
|
|
Expression::BuiltinFunctionReference(BuiltinFunction::StringToFloat)
|
|
}
|
|
_ => {
|
|
ctx.diag.push_error("Cannot access fields of string".into(), &next);
|
|
return Expression::Invalid;
|
|
}
|
|
}),
|
|
};
|
|
}
|
|
_ => {
|
|
ctx.diag.push_error("Cannot access fields of property".into(), &next);
|
|
return Expression::Invalid;
|
|
}
|
|
}
|
|
}
|
|
base
|
|
}
|
|
|
|
fn parse_color_literal(s: &str) -> Option<u32> {
|
|
if !s.starts_with("#") {
|
|
return None;
|
|
}
|
|
if !s.is_ascii() {
|
|
return None;
|
|
}
|
|
let s = &s[1..];
|
|
let (r, g, b, a) = match s.len() {
|
|
3 => (
|
|
u8::from_str_radix(&s[0..=0], 16).ok()? * 0x11,
|
|
u8::from_str_radix(&s[1..=1], 16).ok()? * 0x11,
|
|
u8::from_str_radix(&s[2..=2], 16).ok()? * 0x11,
|
|
255u8,
|
|
),
|
|
4 => (
|
|
u8::from_str_radix(&s[0..=0], 16).ok()? * 0x11,
|
|
u8::from_str_radix(&s[1..=1], 16).ok()? * 0x11,
|
|
u8::from_str_radix(&s[2..=2], 16).ok()? * 0x11,
|
|
u8::from_str_radix(&s[3..=3], 16).ok()? * 0x11,
|
|
),
|
|
6 => (
|
|
u8::from_str_radix(&s[0..2], 16).ok()?,
|
|
u8::from_str_radix(&s[2..4], 16).ok()?,
|
|
u8::from_str_radix(&s[4..6], 16).ok()?,
|
|
255u8,
|
|
),
|
|
8 => (
|
|
u8::from_str_radix(&s[0..2], 16).ok()?,
|
|
u8::from_str_radix(&s[2..4], 16).ok()?,
|
|
u8::from_str_radix(&s[4..6], 16).ok()?,
|
|
u8::from_str_radix(&s[6..8], 16).ok()?,
|
|
),
|
|
_ => return None,
|
|
};
|
|
Some((a as u32) << 24 | (r as u32) << 16 | (g as u32) << 8 | (b as u32) << 0)
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_color_literal() {
|
|
assert_eq!(parse_color_literal("#abc"), Some(0xffaabbcc));
|
|
assert_eq!(parse_color_literal("#ABC"), Some(0xffaabbcc));
|
|
assert_eq!(parse_color_literal("#AbC"), Some(0xffaabbcc));
|
|
assert_eq!(parse_color_literal("#AbCd"), Some(0xddaabbcc));
|
|
assert_eq!(parse_color_literal("#01234567"), Some(0x67012345));
|
|
assert_eq!(parse_color_literal("#012345"), Some(0xff012345));
|
|
assert_eq!(parse_color_literal("_01234567"), None);
|
|
assert_eq!(parse_color_literal("→↓←"), None);
|
|
assert_eq!(parse_color_literal("#→↓←"), None);
|
|
assert_eq!(parse_color_literal("#1234567890"), None);
|
|
}
|
|
|
|
fn unescape_string(string: &str) -> Option<String> {
|
|
if !string.starts_with('"') || !string.ends_with('"') {
|
|
return None;
|
|
}
|
|
let string = &string[1..(string.len() - 1)];
|
|
// TODO: remove slashes
|
|
return Some(string.into());
|
|
}
|
|
|
|
fn parse_number_literal(s: String) -> Result<Expression, String> {
|
|
let bytes = s.as_bytes();
|
|
let mut end = 0;
|
|
while end < bytes.len() && matches!(bytes[end], b'0'..=b'9' | b'.') {
|
|
end += 1;
|
|
}
|
|
let val = s[..end].parse().map_err(|_| "Cannot parse number literal".to_owned())?;
|
|
let unit = s[end..].parse().map_err(|_| "Invalid unit".to_owned())?;
|
|
Ok(Expression::NumberLiteral(val, unit))
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_number_literal() {
|
|
fn doit(s: &str) -> Result<(f64, Unit), String> {
|
|
parse_number_literal(s.into()).map(|e| match e {
|
|
Expression::NumberLiteral(a, b) => (a, b),
|
|
_ => panic!(),
|
|
})
|
|
}
|
|
|
|
assert_eq!(doit("10"), Ok((10., Unit::None)));
|
|
assert_eq!(doit("10phx"), Ok((10., Unit::Phx)));
|
|
assert_eq!(doit("10.0phx"), Ok((10., Unit::Phx)));
|
|
assert_eq!(doit("10.0"), Ok((10., Unit::None)));
|
|
assert_eq!(doit("1.1phx"), Ok((1.1, Unit::Phx)));
|
|
assert_eq!(doit("10.10"), Ok((10.10, Unit::None)));
|
|
assert_eq!(doit("10000000"), Ok((10000000., Unit::None)));
|
|
assert_eq!(doit("10000001phx"), Ok((10000001., Unit::Phx)));
|
|
|
|
let wrong_unit = Err("Invalid unit".to_owned());
|
|
let cannot_parse = Err("Cannot parse number literal".to_owned());
|
|
assert_eq!(doit("10000001 phx"), wrong_unit);
|
|
assert_eq!(doit("12.10.12phx"), cannot_parse);
|
|
assert_eq!(doit("12.12oo"), wrong_unit);
|
|
assert_eq!(doit("12.12€"), wrong_unit);
|
|
}
|