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:
Tobias Hunger 2025-03-26 11:20:41 +00:00 committed by Tobias Hunger
parent e613ffb319
commit aaeb4a0df5
16 changed files with 147 additions and 23 deletions

View file

@ -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}\")")
}
}
}

View file

@ -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(),

View file

@ -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),
}
}

View file

@ -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,

View file

@ -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);

View file

@ -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));

View file

@ -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());

View file

@ -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!(

View 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;
}

View file

@ -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 {

View file

@ -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);

View file

@ -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

View file

@ -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),

View file

@ -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() {

View file

@ -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),