Skip to content

testing/mcp: read declared properties back through debug info - #13372

Open
tilladam wants to merge 6 commits into
slint-ui:masterfrom
tilladam:declared-property-reads
Open

tilladam wants to merge 6 commits into
slint-ui:masterfrom
tilladam:declared-property-reads

Conversation

@tilladam

Copy link
Copy Markdown
Collaborator

An MCP client (and any ElementHandle user) can now read an element's declared in/out/in-out properties back, so verifying UI state no longer means diffing screenshots.

Closes #13251. Addresses gap 1 of #13244.

Design

  • Under the existing debug-info flag (SLINT_EMIT_DEBUG_INFO=1 / with_debug_info), the compiler records each element's readable declared properties at LLR lowering (SubComponent::element_properties).
    move_declarations captures the source name and root key of every declaration it hoists, ranking the effective shadowing declaration first; the LLR optimizer filters and remaps the table like every other reference holder.
  • Two new ItemTreeVTable entries expose the table, following the accessible_string_property stringify convention: element_declared_properties enumerates name:type pairs, element_property_value reads one value in the string encoding of the new i_slint_core::debug_info module.
    The contract is tri-state: false means unsupported, true with an empty result means nothing declared, so a runtime without the feature is distinguishable from an element without properties.
  • All three runtimes serve the same table with the same encoding: generated Rust bakes match arms mirroring item_element_infos, the interpreter evaluates through eval::load_property, and generated C++ dispatches through slint::private_api helpers.
    C++ delegates float encoding to the Rust runtime over FFI: GCC 10 (a supported toolchain) has no floating-point std::to_chars, and no local formatter reproduces Rust's f32 Display digits for values like 1e20.
  • ElementHandle gains declared_properties() and declared_property_value() in the Rust and C++ testing APIs.
    get_element_properties and get_element_tree report declaredProperties with typed JSON values, and say so through a note when debug info is missing instead of degrading silently.

Example response fragment:

"declaredProperties": [
  { "name": "selected", "typeName": "bool", "value": true },
  { "name": "len", "typeName": "length", "value": 12 },
  { "name": "tint", "typeName": "color", "value": "#112233ff" }
]

Properties the compiler optimized out (constant, or never read) are not listed; private declarations are excluded. Writing declared properties is left to a follow-up.

Testing

  • One source, one set of expected strings, run on all three runtimes: tests/cases/testing/declared_properties.slint (generated Rust and C++) and internal/interpreter/tests/declared_properties.rs (interpreter), covering every encodable type, float edge values (0.1, 1e20), aliases, inherited and use-site declarations, repeated rows, reactivity, and the exclusions.
  • Shadow resolution (@shadowable) is pinned by a compiler unit test.
  • Full runs: interpreter driver (891), Rust driver (808), C++ driver (862), syntax tests, and the testing backend's MCP tests, all green.

Cost

Measured on a release build of the gallery (macOS arm64):

no flag with SLINT_EMIT_DEBUG_INFO=1
master 19,282,672 B 19,374,720 B
this branch 19,372,304 B 19,598,960 B

Flag off, the two vtable entries and their stubs cost +89,632 B (+0.46%); flag on, the tables and value-read code cost +224,240 B (+1.16%).

@tilladam
tilladam force-pushed the declared-property-reads branch from e23e59e to 46f35af Compare September 13, 2026 10:11
tilladam and others added 6 commits September 13, 2026 13:45
Under the existing debug-info flag, record each element's readable
in/out/in-out properties at LLR lowering (SubComponent::element_properties):
move_declarations captures the source name and root key of every declaration
it hoists, ranking the effective shadowing declaration of a name first (the
shadowing_members index is cleared by the move, so this is the last point the
information exists), and collect_element_properties resolves the captured and
inherited declarations through the regular property-reference mapping. The
LLR optimizer filters and remaps the table like every other reference holder.

Two new ItemTreeVTable entries expose the table: element_declared_properties
enumerates 'name:type' pairs, element_property_value reads one value in the
string encoding of the new i-slint-core debug_info module. Their contract is
tri-state: false means unsupported, true with an empty result means nothing
declared. Generated Rust bakes match arms mirroring item_element_infos; the
interpreter serves both entries from the LLR table via eval::load_property;
generated C++ emits stubs reporting unsupported until its own implementation.
ElementHandle gains declared_properties() and declared_property_value().

Part of slint-ui#13251.
Adds declaredProperties to ElementPropertiesResponse: each element's
in/out/in-out properties with name, type, and current value read through the
debug-info channel. The MCP layer retypes the string values into JSON booleans
and numbers for both get_element_properties and get_element_tree, and reports
missing debug info or an unsupported code generator through a note instead of
an empty list. Protobuf clients get the raw strings plus an availability flag.

Part of slint-ui#13251.

ChangeLog: The MCP server's get_element_properties and get_element_tree report declaredProperties — an element's declared in/out/in-out properties with typed current values — and ElementHandle gains declared_properties() and declared_property_value() in the Rust and C++ testing APIs. Requires SLINT_EMIT_DEBUG_INFO=1.
The C++ generator replaces its element_declared_properties and
element_property_value stubs with real dispatch. Bool, integer, color, brush,
and enum values are encoded by slint::private_api::debug_info_format_*
helpers matching i_slint_core::debug_info; float values are formatted by the
Rust runtime through a new slint_debug_info_format_float FFI function — GCC 10
(a supported toolchain) has no floating-point std::to_chars, and no local
formatter reproduces Rust's f32 Display digits for values like 1e20.
The C++ testing API gains ElementHandle::declared_properties() and
declared_property_value().

Part of slint-ui#13251.
One source, one set of expected strings: the driver case runs it through
generated Rust and generated C++, the interpreter test through the runtime
interpretation, so the three value encodings cannot drift apart. Covers every
encodable type plus a struct (listed, value unavailable), float edge values
(0.1 and 1e20), two-way aliases, inherited and use-site declarations,
repeated rows, reactivity, and the exclusions: private declarations and
optimizer-removed properties.

Part of slint-ui#13251.
@tronical

Copy link
Copy Markdown
Member

Properties the compiler optimized out (constant, or never read) are not listed; private declarations are excluded.

In the past, this has been indeed the primary concern about this feature. If a new Slint version introduces a new optimisation that optimises a property away that was previously accessible and now isn't anymore, then that would constitute a breaking change - preventing us from making the optimisation. That's why we were thinking of introducing an extra attribute to a property to indicate that this particular property is required for the purpose of system testing. So not necessarily part of the component's public API, but relevant to testing.

Writing declared properties is left to a follow-up.

Can you elaborate on the follow-up plans?

@tilladam

Copy link
Copy Markdown
Collaborator Author

Right, the PR leans on that limitation being acceptable for now, and the docs' "anchor the property with a changed callback" advice is exactly that: a workaround, not a contract. The attribute you describe is the missing contract, and I think it layers cleanly on top of this PR:

  • the attribute's only job is to keep the property alive through the optimizer: exempt it from const-propagation and unused-removal when debug info is on, the way a changed callback already does implicitly;
  • everything downstream is unchanged: the lowering-time table, the vtable contract, and the encoding don't change shape. A pinned property just reliably appears in the table it already flows through.

So tests written against pinned properties would get a stability guarantee instead of today's "hope it survives the optimizer", and the introspection channel doesn't need touching when the attribute lands. I'd do it as the next PR, spelling to be bikeshed (@testable?), gated experimental like @shadowable to start.

On writing: the sketch is a third vtable entry mirroring the read side. It parses back the same string encoding element_property_value emits, same tri-state, same debug-info gate. Generated code dispatches through the same table to a compiled setter; the interpreter goes through store_property; MCP gains a set_element_property tool. The one semantic question is binding replacement: a set from the test channel would behave like a set from the language APIs (replace the binding with a value), which is what UI tests generally want. I kept it out of this PR so the read contract stays reviewable on its own.

@tronical

Copy link
Copy Markdown
Member

The essence:

I'd do it as the next PR, spelling to be bikeshed (@testable?), gated experimental like @Shadowable to start.

I can't believe I'm saying this, but perhaps this would go well in a single PR :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP: declared property values can't be read back, so verifying state means diffing pixels

2 participants