mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 18:58:36 +00:00
compiler: Add a DebugHook expression
You can not create this expression manually, but there is a pass in the compiler that adds it to all set properties in a compilation run. All it does is basically associate an id with an expression, so that we can then in a later step have the interpreter do something with that information. Apart from that, it tries to be as transparent as possible. The LLR lowering removes that expression again, just so we can be sure it does not end up in the generated live code.
This commit is contained in:
parent
e613ffb319
commit
aaeb4a0df5
16 changed files with 147 additions and 23 deletions
|
@ -716,6 +716,11 @@ pub enum Expression {
|
|||
rhs: Box<Expression>,
|
||||
},
|
||||
|
||||
DebugHook {
|
||||
expression: Box<Expression>,
|
||||
id: SmolStr,
|
||||
},
|
||||
|
||||
EmptyComponentFactory,
|
||||
}
|
||||
|
||||
|
@ -837,6 +842,7 @@ impl Expression {
|
|||
Expression::SolveLayout(..) => Type::LayoutCache,
|
||||
Expression::MinMax { ty, .. } => ty.clone(),
|
||||
Expression::EmptyComponentFactory => Type::ComponentFactory,
|
||||
Expression::DebugHook { expression, .. } => expression.ty(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -931,6 +937,7 @@ impl Expression {
|
|||
visitor(rhs);
|
||||
}
|
||||
Expression::EmptyComponentFactory => {}
|
||||
Expression::DebugHook { expression, .. } => visitor(expression),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1027,6 +1034,7 @@ impl Expression {
|
|||
visitor(rhs);
|
||||
}
|
||||
Expression::EmptyComponentFactory => {}
|
||||
Expression::DebugHook { expression, .. } => visitor(expression),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1108,6 +1116,7 @@ impl Expression {
|
|||
Expression::SolveLayout(..) => false,
|
||||
Expression::MinMax { lhs, rhs, .. } => lhs.is_constant() && rhs.is_constant(),
|
||||
Expression::EmptyComponentFactory => true,
|
||||
Expression::DebugHook { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1409,6 +1418,14 @@ impl Expression {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unwrap DebugHook expressions to their contained sub-expression
|
||||
pub fn ignore_debug_hooks(&self) -> &Expression {
|
||||
match self {
|
||||
Expression::DebugHook { expression, .. } => expression.as_ref(),
|
||||
_ => return self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn model_inner_type(model: &Expression) -> Type {
|
||||
|
@ -1738,5 +1755,10 @@ pub fn pretty_print(f: &mut dyn std::fmt::Write, expression: &Expression) -> std
|
|||
write!(f, ")")
|
||||
}
|
||||
Expression::EmptyComponentFactory => write!(f, "<empty-component-factory>"),
|
||||
Expression::DebugHook { expression, id } => {
|
||||
write!(f, "debug-hook(")?;
|
||||
pretty_print(f, expression)?;
|
||||
write!(f, "\"{id}\")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,6 +148,9 @@ pub struct CompilerConfiguration {
|
|||
/// Generate debug information for elements (ids, type names)
|
||||
pub debug_info: bool,
|
||||
|
||||
/// Generate debug hooks to inspect/override properties.
|
||||
pub debug_hooks: bool,
|
||||
|
||||
pub components_to_generate: ComponentSelection,
|
||||
|
||||
#[cfg(feature = "software-renderer")]
|
||||
|
@ -226,6 +229,7 @@ impl CompilerConfiguration {
|
|||
translation_domain: None,
|
||||
cpp_namespace,
|
||||
debug_info,
|
||||
debug_hooks: false,
|
||||
components_to_generate: ComponentSelection::ExportedWindows,
|
||||
#[cfg(feature = "software-renderer")]
|
||||
font_cache: Default::default(),
|
||||
|
|
|
@ -252,6 +252,7 @@ pub fn lower_expression(
|
|||
rhs: Box::new(lower_expression(rhs, ctx)),
|
||||
},
|
||||
tree_Expression::EmptyComponentFactory => llr_Expression::EmptyComponentFactory,
|
||||
tree_Expression::DebugHook { expression, .. } => lower_expression(expression, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -641,6 +641,9 @@ pub struct ElementDebugInfo {
|
|||
// The id qualified with the enclosing component name. Given `foo := Bar {}` this is `EnclosingComponent::foo`
|
||||
pub qualified_id: Option<SmolStr>,
|
||||
pub type_name: String,
|
||||
// Hold an id for each element that is unique during this build.
|
||||
// It helps to cross-reference the element in the different build stages the LSP has to deal with.
|
||||
pub element_id: u64,
|
||||
pub node: syntax_nodes::Element,
|
||||
// Field to indicate whether this element was a layout that had
|
||||
// been lowered into a rectangle in the lower_layouts pass.
|
||||
|
@ -997,6 +1000,7 @@ impl Element {
|
|||
base_type,
|
||||
debug: vec![ElementDebugInfo {
|
||||
qualified_id,
|
||||
element_id: 0,
|
||||
type_name,
|
||||
node: node.clone(),
|
||||
layout: None,
|
||||
|
|
|
@ -25,6 +25,7 @@ mod flickable;
|
|||
mod focus_handling;
|
||||
pub mod generate_item_indices;
|
||||
pub mod infer_aliases_types;
|
||||
mod inject_debug_hooks;
|
||||
mod inlining;
|
||||
mod lower_absolute_coordinates;
|
||||
mod lower_accessibility;
|
||||
|
@ -56,6 +57,16 @@ use crate::expression_tree::Expression;
|
|||
use crate::namedreference::NamedReference;
|
||||
use smol_str::SmolStr;
|
||||
|
||||
pub fn ignore_debug_hooks(expr: &Expression) -> &Expression {
|
||||
let mut expr = expr;
|
||||
loop {
|
||||
match expr {
|
||||
Expression::DebugHook { expression, .. } => expr = expression.as_ref(),
|
||||
_ => return expr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_passes(
|
||||
doc: &mut crate::object_tree::Document,
|
||||
type_loader: &mut crate::typeloader::TypeLoader,
|
||||
|
@ -81,6 +92,9 @@ pub async fn run_passes(
|
|||
};
|
||||
|
||||
let global_type_registry = type_loader.global_type_registry.clone();
|
||||
|
||||
inject_debug_hooks::inject_debug_hooks(doc, type_loader);
|
||||
|
||||
run_import_passes(doc, type_loader, diag);
|
||||
check_public_api::check_public_api(doc, &type_loader.compiler_config, diag);
|
||||
|
||||
|
|
|
@ -207,6 +207,7 @@ fn simplify_expression(expr: &mut Expression) -> bool {
|
|||
Expression::LayoutCacheAccess { .. } => false,
|
||||
Expression::SolveLayout { .. } => false,
|
||||
Expression::ComputeLayoutInfo { .. } => false,
|
||||
Expression::DebugHook { .. } => false, // This is not const by design
|
||||
_ => {
|
||||
let mut result = true;
|
||||
expr.visit_mut(|expr| result &= simplify_expression(expr));
|
||||
|
|
|
@ -82,7 +82,8 @@ impl<'a> LocalFocusForwards<'a> {
|
|||
return;
|
||||
};
|
||||
|
||||
let Expression::ElementReference(focus_target) = &forward_focus_binding.expression
|
||||
let Expression::ElementReference(focus_target) =
|
||||
super::ignore_debug_hooks(&forward_focus_binding.expression)
|
||||
else {
|
||||
// resolve expressions pass has produced type errors
|
||||
debug_assert!(diag.has_errors());
|
||||
|
|
|
@ -82,7 +82,7 @@ fn resolve_alias(
|
|||
assert!(diag.has_errors());
|
||||
return;
|
||||
};
|
||||
let nr = match &binding.borrow().expression {
|
||||
let nr = match super::ignore_debug_hooks(&binding.borrow().expression) {
|
||||
Expression::Uncompiled(node) => {
|
||||
let Some(node) = syntax_nodes::TwoWayBinding::new(node.clone()) else {
|
||||
assert!(
|
||||
|
|
55
internal/compiler/passes/inject_debug_hooks.rs
Normal file
55
internal/compiler/passes/inject_debug_hooks.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
// 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
|
||||
|
||||
//! Hooks properties for live inspection.
|
||||
|
||||
use crate::{expression_tree, object_tree, typeloader};
|
||||
|
||||
pub fn inject_debug_hooks(
|
||||
doc: &mut object_tree::Document,
|
||||
type_loader: &mut typeloader::TypeLoader,
|
||||
) {
|
||||
if !type_loader.compiler_config.debug_info {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut counter = 1_u64;
|
||||
|
||||
doc.visit_all_used_components(|component| {
|
||||
object_tree::recurse_elem_including_sub_components(component, &(), &mut |e, &()| {
|
||||
process_element(e, counter, &type_loader.compiler_config);
|
||||
counter += 1;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn property_id(counter: u64, name: &smol_str::SmolStr) -> smol_str::SmolStr {
|
||||
smol_str::format_smolstr!("?{counter}-{name}")
|
||||
}
|
||||
|
||||
fn process_element(
|
||||
element: &object_tree::ElementRc,
|
||||
counter: u64,
|
||||
config: &crate::CompilerConfiguration,
|
||||
) {
|
||||
let mut elem = element.borrow_mut();
|
||||
assert_eq!(elem.debug.len(), 1); // We did not merge Elements yet and we have debug info!
|
||||
|
||||
if config.debug_hooks {
|
||||
elem.bindings.iter().for_each(|(name, be)| {
|
||||
let expr = std::mem::take(&mut be.borrow_mut().expression);
|
||||
be.borrow_mut().expression = {
|
||||
let stripped = super::ignore_debug_hooks(&expr);
|
||||
if matches!(stripped, expression_tree::Expression::Invalid) {
|
||||
stripped.clone()
|
||||
} else {
|
||||
expression_tree::Expression::DebugHook {
|
||||
expression: Box::new(expr),
|
||||
id: property_id(counter, name),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
elem.debug.first_mut().expect("There was one element a moment ago").element_id = counter;
|
||||
}
|
|
@ -22,7 +22,9 @@ pub fn lower_accessibility_properties(component: &Rc<Component>, diag: &mut Buil
|
|||
apply_builtin(elem);
|
||||
let accessible_role_set = match elem.borrow().bindings.get("accessible-role") {
|
||||
Some(role) => {
|
||||
if let Expression::EnumerationValue(val) = &role.borrow().expression {
|
||||
if let Expression::EnumerationValue(val) =
|
||||
super::ignore_debug_hooks(&role.borrow().expression)
|
||||
{
|
||||
debug_assert_eq!(val.enumeration.name, "AccessibleRole");
|
||||
debug_assert_eq!(val.enumeration.values[0], "none");
|
||||
if val.value == 0 {
|
||||
|
|
|
@ -60,7 +60,8 @@ pub fn lower_layouts(
|
|||
fn check_preferred_size_100(elem: &ElementRc, prop: &str, diag: &mut BuildDiagnostics) -> bool {
|
||||
let ret = if let Some(p) = elem.borrow().bindings.get(prop) {
|
||||
if p.borrow().expression.ty() == Type::Percent {
|
||||
if !matches!(p.borrow().expression, Expression::NumberLiteral(val, _) if val == 100.) {
|
||||
if !matches!(p.borrow().expression.ignore_debug_hook(), Expression::NumberLiteral(val, _) if val == 100.)
|
||||
{
|
||||
diag.push_error(
|
||||
format!("{prop} must either be a length, or the literal '100%'"),
|
||||
&*p.borrow(),
|
||||
|
@ -523,7 +524,9 @@ fn lower_dialog_layout(
|
|||
layout_child.borrow_mut().bindings.remove("dialog-button-role");
|
||||
let is_button = if let Some(role_binding) = dialog_button_role_binding {
|
||||
let role_binding = role_binding.into_inner();
|
||||
if let Expression::EnumerationValue(val) = &role_binding.expression {
|
||||
if let Expression::EnumerationValue(val) =
|
||||
super::ignore_debug_hooks(&role_binding.expression)
|
||||
{
|
||||
let en = &val.enumeration;
|
||||
debug_assert_eq!(en.name, "DialogButtonRole");
|
||||
button_roles.push(en.values[val.value].clone());
|
||||
|
@ -550,7 +553,9 @@ fn lower_dialog_layout(
|
|||
),
|
||||
Some(binding) => {
|
||||
let binding = &*binding.borrow();
|
||||
if let Expression::EnumerationValue(val) = &binding.expression {
|
||||
if let Expression::EnumerationValue(val) =
|
||||
super::ignore_debug_hooks(&binding.expression)
|
||||
{
|
||||
let en = &val.enumeration;
|
||||
debug_assert_eq!(en.name, "StandardButtonKind");
|
||||
let kind = &en.values[val.value];
|
||||
|
@ -583,7 +588,7 @@ fn lower_dialog_layout(
|
|||
let clicked_ty =
|
||||
layout_child.borrow().lookup_property("clicked").property_type;
|
||||
if matches!(&clicked_ty, Type::Callback { .. })
|
||||
&& layout_child.borrow().bindings.get("clicked").map_or(true, |c| {
|
||||
&& layout_child.borrow().bindings.get("clicked").is_none_or(|c| {
|
||||
matches!(c.borrow().expression, Expression::Invalid)
|
||||
})
|
||||
{
|
||||
|
@ -798,7 +803,7 @@ fn eval_const_expr(
|
|||
span: &dyn crate::diagnostics::Spanned,
|
||||
diag: &mut BuildDiagnostics,
|
||||
) -> Option<u16> {
|
||||
match expression {
|
||||
match super::ignore_debug_hooks(expression) {
|
||||
Expression::NumberLiteral(v, Unit::None) => {
|
||||
if *v < 0. || *v > u16::MAX as f64 || !v.trunc().approx_eq(v) {
|
||||
diag.push_error(format!("'{name}' must be a positive integer"), span);
|
||||
|
|
|
@ -48,7 +48,7 @@ fn lower_popup_window(
|
|||
diag: &mut BuildDiagnostics,
|
||||
) {
|
||||
if let Some(binding) = popup_window_element.borrow().bindings.get(CLOSE_ON_CLICK) {
|
||||
if popup_window_element.borrow().bindings.get(CLOSE_POLICY).is_some() {
|
||||
if popup_window_element.borrow().bindings.contains_key(CLOSE_POLICY) {
|
||||
diag.push_error(
|
||||
"close-policy and close-on-click cannot be set at the same time".into(),
|
||||
&binding.borrow().span,
|
||||
|
@ -59,12 +59,18 @@ fn lower_popup_window(
|
|||
CLOSE_POLICY,
|
||||
&binding.borrow().span,
|
||||
);
|
||||
if !matches!(binding.borrow().expression, Expression::BoolLiteral(_)) {
|
||||
if !matches!(
|
||||
super::ignore_debug_hooks(&binding.borrow().expression),
|
||||
Expression::BoolLiteral(_)
|
||||
) {
|
||||
report_const_error(CLOSE_ON_CLICK, &binding.borrow().span, diag);
|
||||
}
|
||||
}
|
||||
} else if let Some(binding) = popup_window_element.borrow().bindings.get(CLOSE_POLICY) {
|
||||
if !matches!(binding.borrow().expression, Expression::EnumerationValue(_)) {
|
||||
if !matches!(
|
||||
super::ignore_debug_hooks(&binding.borrow().expression),
|
||||
Expression::EnumerationValue(_)
|
||||
) {
|
||||
report_const_error(CLOSE_POLICY, &binding.borrow().span, diag);
|
||||
}
|
||||
}
|
||||
|
@ -110,12 +116,12 @@ fn lower_popup_window(
|
|||
}
|
||||
|
||||
let map_close_on_click_value = |b: &BindingExpression| {
|
||||
let Expression::BoolLiteral(v) = b.expression else {
|
||||
let Expression::BoolLiteral(v) = super::ignore_debug_hooks(&b.expression) else {
|
||||
assert!(diag.has_errors());
|
||||
return None;
|
||||
};
|
||||
let enum_ty = crate::typeregister::BUILTIN.with(|e| e.enums.PopupClosePolicy.clone());
|
||||
let s = if v { "close-on-click" } else { "no-auto-close" };
|
||||
let s = if *v { "close-on-click" } else { "no-auto-close" };
|
||||
Some(EnumerationValue {
|
||||
value: enum_ty.values.iter().position(|v| v == s).unwrap(),
|
||||
enumeration: enum_ty,
|
||||
|
@ -125,8 +131,8 @@ fn lower_popup_window(
|
|||
let close_policy =
|
||||
popup_window_element.borrow_mut().bindings.remove(CLOSE_POLICY).and_then(|b| {
|
||||
let b = b.into_inner();
|
||||
if let Expression::EnumerationValue(v) = b.expression {
|
||||
Some(v)
|
||||
if let Expression::EnumerationValue(v) = super::ignore_debug_hooks(&b.expression) {
|
||||
Some(v.clone())
|
||||
} else {
|
||||
assert!(diag.has_errors());
|
||||
None
|
||||
|
|
|
@ -39,6 +39,9 @@ fn process_expression(
|
|||
ty: &Type,
|
||||
) -> ExpressionResult {
|
||||
match e {
|
||||
Expression::DebugHook { expression, .. } => {
|
||||
process_expression(*expression, toplevel, ctx, ty)
|
||||
}
|
||||
Expression::ReturnStatement(expr) => ExpressionResult::Return(expr.map(|e| *e)),
|
||||
Expression::CodeBlock(expr) => {
|
||||
process_codeblock(expr.into_iter().peekable(), toplevel, ty, ctx)
|
||||
|
@ -69,14 +72,14 @@ fn process_expression(
|
|||
}
|
||||
(ExpressionResult::Return(te), ExpressionResult::Return(fe)) => {
|
||||
ExpressionResult::Return(Some(Expression::Condition {
|
||||
condition: condition.into(),
|
||||
condition,
|
||||
true_expr: te.unwrap_or(Expression::CodeBlock(vec![])).into(),
|
||||
false_expr: fe.unwrap_or(Expression::CodeBlock(vec![])).into(),
|
||||
}))
|
||||
}
|
||||
(te, fe) => {
|
||||
let te = te.into_return_object(&ty, &ctx.ret_ty);
|
||||
let fe = fe.into_return_object(&ty, &ctx.ret_ty);
|
||||
let te = te.into_return_object(ty, &ctx.ret_ty);
|
||||
let fe = fe.into_return_object(ty, &ctx.ret_ty);
|
||||
ExpressionResult::ReturnObject {
|
||||
has_value: has_value(ty),
|
||||
has_return_value: has_value(&ctx.ret_ty),
|
||||
|
|
|
@ -35,7 +35,7 @@ fn resolve_expression(
|
|||
type_loader: &crate::typeloader::TypeLoader,
|
||||
diag: &mut BuildDiagnostics,
|
||||
) {
|
||||
if let Expression::Uncompiled(node) = expr {
|
||||
if let Expression::Uncompiled(node) = expr.ignore_debug_hooks() {
|
||||
let mut lookup_ctx = LookupCtx {
|
||||
property_name,
|
||||
property_type,
|
||||
|
@ -77,7 +77,10 @@ fn resolve_expression(
|
|||
Expression::Invalid
|
||||
}
|
||||
};
|
||||
*expr = new_expr;
|
||||
match expr {
|
||||
Expression::DebugHook { expression, .. } => *expression = Box::new(new_expr),
|
||||
_ => *expr = new_expr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1600,7 +1603,9 @@ fn resolve_two_way_bindings(
|
|||
&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 Expression::Uncompiled(node) =
|
||||
binding.expression.ignore_debug_hooks().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() {
|
||||
|
|
|
@ -74,7 +74,7 @@ fn eval_const_expr(
|
|||
span: &dyn crate::diagnostics::Spanned,
|
||||
diag: &mut BuildDiagnostics,
|
||||
) -> Option<f64> {
|
||||
match expression {
|
||||
match super::ignore_debug_hooks(expression) {
|
||||
Expression::NumberLiteral(v, Unit::None) => Some(*v),
|
||||
Expression::Cast { from, .. } => eval_const_expr(from, name, span, diag),
|
||||
Expression::UnaryOp { sub, op: '-' } => eval_const_expr(sub, name, span, diag).map(|v| -v),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue