slint-ui/slint · error
model two-way bindings only exist on sub-components
Error message
model two-way bindings only exist on sub-components
What it means
This panic fires in the C++ code generator while emitting a model two-way binding (`generate_model_two_way_binding`). The generator needs the name of the current sub-component to compute the C++ class name for casting back from a type-erased VRc, but `ctx.current_sub_component()` returned None. Model two-way bindings are only lowered when attached to a sub-component (repeater), so hitting this means the code generator encountered the binding outside that context.
Solutions
- Check how the binding reached this code path; model two-way bindings must be emitted from `generate_sub_component` where the context has a current sub-component
- Ensure the EvaluationContext was built with `new_sub_component` and the correct sub-component index before compiling the binding
- If the binding legitimately belongs elsewhere, handle it explicitly instead of relying on the expect
- File/report a compiler bug with the .slint input that reproduces it
Example fix
// before
let self_type = ident(&ctx.current_sub_component().expect("...").name);
// after
let Some(sub) = ctx.current_sub_component() else {
panic!("model two-way binding generated outside sub-component context: {:?}", info.repeater_index);
};
let self_type = ident(&sub.name); Defensive patterns
Strategy: validation
Validate before calling
debug_assert!(
ctx.current_sub_component().is_some(),
"model two-way binding must be generated inside a sub-component context"
); Type guard
let sub_component = match ctx.current_sub_component() {
Some(sub) => sub,
None => panic!("model two-way binding generated outside sub-component context"),
}; Prevention
- Only emit model two-way bindings from generate_sub_component
- Always build EvaluationContext with new_sub_component for binding compilation
- Add a debug assertion in the binding compilation entry point
- Write a regression test per generator path that compiles model two-way bindings
When it happens
Trigger: A model two-way binding expression is generated for a context whose current entity is a global or the root without a sub-component frame on the EvaluationContext stack — i.e. a compiler-internal scheduling/pass-ordering bug, not something a .slint author writes directly.
Common situations: Slint compiler development: adding a new lowering pass or generator path that routes model two-way binding expressions through `compile_expression` without setting up a sub-component context; regressions after refactoring EvaluationContext creation.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Expression::ObjectAccess: Cannot find a key in an object
- ShowPopupWindow is invoked on a sub-component
- there should be a popup menu if we want to show it
- a setter belongs to a field
- an identifier
AI-assisted analysis of slint-ui/slint@3a7e700487 (2026-09-16).
Data as JSON: /api/errors/8cf48e1f0b727b44.
Report an issue: GitHub.
Appendix: source
Thrown at internal/compiler/generator/cpp.rs:675
/// Emit a `link_two_way_to_model_data` call wiring `p1` to a row of the
/// model described by `info`, optionally through a struct `field_access`.
fn generate_model_two_way_binding(
ctx: &EvaluationContext,
info: &llr::ResolvedModelTwoWayBinding,
p1: &str,
field_access: &[SmolStr],
) -> String {
let body_sc = &ctx.compilation_unit.sub_components[info.body_sub_component];
let data_prop_name = field_name(&body_sc.properties[info.data_prop].name);
let index_prop_name = field_name(&body_sc.properties[info.index_prop].name);
let repeater_index = usize::from(info.repeater_index);
// Determine the C++ class name of `self` so we can cast back from
// the type-erased VRc obtained by locking the weak pointer.
let self_type = ident(
&ctx.current_sub_component()
.expect("model two-way bindings only exist on sub-components")
.name,
);
// Walk the parent chain in a single expression so the intermediate
// `lock().value()` temporaries live until we assign to `body_rc`.
let (body_setup, body) = if info.parent_level == 0 {
(String::new(), "self")
} else {
let chain: String = (0..info.parent_level).map(|_| "->parent.lock().value()").collect();
(format!("auto body_rc = self{chain}; "), "body_rc")
};
let (getter_expr, ty) = lower_field_access_chain(
format!("{body}->{data_prop_name}.get()"),
info.data_prop_ty,
field_access,
);
let (setter_lvalue, _) =View on GitHub (pinned to 3a7e700487)