Skip to content

feat: AOT-safe property access: [KumikoGridItem] generator, column delegates, guarded reflection (#17) - #137

Merged
michaelstonis merged 2 commits into
mainfrom
feat/17-aot-accessors
Oct 25, 2026
Merged

michaelstonis merged 2 commits into
mainfrom
feat/17-aot-accessors

Conversation

@michaelstonis

@michaelstonis michaelstonis commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Closes #17. Supersedes #1 (not closed here; see the note at the end).

How the grid reads and writes item properties

For every column, the grid tries these in order:

  1. Column delegates: KumikoColumn.ValueAccessor / ValueSetter, and EditorDisplaySelector / EditorValueSelector for ComboBox items (DrawnComboBox.DisplaySelector / ValueSelector). Delegates are used for display, sorting, grouping, filtering, summaries and edits.
  2. Generated accessors: types marked [KumikoGridItem] (KumikoUI.Core.Data). A new source generator, KumikoUI.SourceGenerators, ships inside the KumikoUI.Core package under analyzers/dotnet/cs. It writes a typed IKumikoAccessorProvider per type and registers it with a module initializer.
    • Covers: public instance properties including inherited ones, new and override members, structs (written through the box), records, nullable and generic property types, and keyword-named properties.
    • Skips: init-only and private setters, protected getters, static properties, indexers, and [Obsolete(error: true)] members.
    • Warns: KUMIKO001 for generic types and KUMIKO002 for types it cannot reference (private, protected or file-local).
  3. Registered providers: KumikoAccessorRegistry.Register(type, provider) or AddProvider(provider), for example to read dictionaries or data rows. A provider registered for a type also covers its subclasses.
  4. Reflection: the fallback sits behind a feature switch ([FeatureSwitchDefinition] + [FeatureGuard(RequiresUnreferencedCode)], plus an IL4000 suppression with its justification). KumikoUI.Core's buildTransitive targets set the switch to false for PublishAot=true and TrimMode=full, so the trimmer removes the fallback. Partial trimming (the MAUI default) leaves it on. The KumikoReflectionPropertyAccess MSBuild property overrides the automatic choice. The fallback compiles expression trees only where dynamic code is supported, and uses PropertyInfo otherwise.

With reflection off, a property that no tier can serve reads as null, edits to it are ignored, and a warning is traced once per type and property.

Paths are resolved one segment at a time. A null value part-way along a path now reads as null instead of throwing an NRE (Address.City with a null Address). Resolved paths are cached on the column, keyed by item type and registry version.

AOT compatibility

  • KumikoUI.Core, .SkiaSharp, .Maui and .Uno are all IsAotCompatible, with zero IL2xxx/IL3xxx warnings. I checked Maui on catalyst, android and ios, and Uno on the desktop head.
  • The new Directory.Build.targets makes every IL2xxx/IL3xxx analyzer warning an error in those libraries. It also forces the trim analyzer on, because the Apple workloads turn it off for library builds. I confirmed a planted GetType().GetProperty call now fails the MAUI build.
  • End-to-end check: I packed KumikoUI.Core and published a Native AOT console app (osx-arm64) against the package.
    • No IL or AOT warnings.
    • The switch turned off automatically.
    • [KumikoGridItem] types read, sort, write with conversion and walk nested paths.
    • An unmarked type reads null with the traced warning, and a ValueAccessor on it still works.

Benchmarks (bench/KumikoUI.Benchmarks, 10 000 rows via GetCellValue, Apple M4 Max, BenchmarkDotNet ShortRun)

Path Time vs pre-#17 Allocated
Pre-#17 compiled expression + string-key lookup 178 µs 1.00 1172 KB
[KumikoGridItem] generated 55 µs 0.31 234 KB
ValueAccessor delegate 36 µs 0.20 234 KB
Reflection fallback (JIT) 57 µs 0.32 234 KB

Generated and delegate reads meet the acceptance criterion: both are at least as fast as compiled expressions. The remaining allocation is the boxed int each read returns. Under Native AOT, generated accessors avoid the expression interpreter entirely.

Other changes

  • Fix: the ComboBox editor set EditorDisplayMemberPath/EditorValueMemberPath after building its items. So items showed ToString() and the current value was never selected. A test now covers it.
  • Kept behaviour: a setter that throws (for example item-side validation) still leaves the item unchanged, as the reflection path always did. Typed value conversion, validation and error UI following WPF/WinUI DataGrid conventions #32 will surface these as validation errors. A ValueSetter delegate's own exceptions propagate.
  • Dependabot: ignores Microsoft.CodeAnalysis.CSharp. The generator is built against Roslyn 4.14 on purpose, the oldest compiler it supports.
  • CI and publish: both run the new tests/KumikoUI.SourceGenerators.Tests, and both new projects are in the solution.
  • Docs: README has a new "Native AOT & Trimming" section, CONTRIBUTING covers the AOT rule and the generator tests, bench/README.md explains the benchmarks, and the CHANGELOG has Added, Changed and Fixed entries.

Tests

  • Core: 487 passed, 3 skipped. The 21 new tests cover:
    • generated accessors
    • delegate precedence and an unconverted ValueSetter
    • nested null reads and writes
    • write-only and read-only properties
    • SetCellValue conversion
    • throwing setters
    • struct setters
    • records and keyword-named properties
    • inheritance and overrides
    • providers
    • the ComboBox selectors and the member-path fix
    • a non-parallel reflection-disabled collection
  • Source generator: 9 passed. Covers diagnostics, the emitted code compiling with no warnings (this caught a nullable cast bug, now fixed), the accessor rules above, struct boxing, the module initializer, and incremental caching on unrelated edits.
  • Uno: 63 passed.
  • Builds: Maui catalyst, android and ios; Uno desktop; both samples. The name check is clean for Core plus each host.
  • Not built locally: the Windows heads and the Uno android, ios and wasm heads. CI covers them.

PR #1

This PR supersedes #1, which swapped in plain reflection with no tests or measurements. Closing #1 is your call; I left it open.

🤖 Generated with Claude Code

…elegates, providers, guarded reflection (#17)

- KumikoUI.Core.Data: KumikoPropertyAccessor, IKumikoAccessorProvider, KumikoAccessorRegistry
  (type and global providers, cached per type and path) and [KumikoGridItem].
- KumikoUI.SourceGenerators (shipped in the Core package under analyzers/): a typed provider per
  [KumikoGridItem] type, registered by a module initializer; KUMIKO001/002 for unsupported types.
- KumikoColumn.ValueAccessor/ValueSetter and EditorDisplaySelector/EditorValueSelector;
  DrawnComboBox.DisplaySelector/ValueSelector. Delegates win over every other source.
- Reflection fallback behind a feature switch (FeatureSwitchDefinition + FeatureGuard); the package's
  build targets turn it off for PublishAot and TrimMode=full. Compiled expressions only where dynamic
  code is supported.
- Paths resolve per segment: a null intermediate value reads as null instead of throwing; resolved
  paths are cached on the column (3.2x faster reads than before, 80% less allocation).
- IsAotCompatible on Core, SkiaSharp, Maui and Uno; IL2xxx/IL3xxx are errors (Directory.Build.targets).
- Fix: the ComboBox editor applied member paths after building its items.
- Tests for the accessors, the switch-off mode and the generator; bench/KumikoUI.Benchmarks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 25, 2026 15:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing. Review the Copilot code review billing settings.

@michaelstonis
michaelstonis requested a balanced review from Copilot October 25, 2026 15:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing. Review the Copilot code review billing settings.

Comment thread src/KumikoUI.Core/Data/KumikoAccessorRegistry.cs Fixed
Comment thread src/KumikoUI.SourceGenerators/KumikoGridItemGenerator.cs Fixed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@michaelstonis
michaelstonis merged commit 4911373 into main Oct 25, 2026
5 checks passed
@michaelstonis
michaelstonis deleted the feat/17-aot-accessors branch October 25, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

AOT-safe property access: source generator, delegate accessors, annotated reflection fallback; IsAotCompatible everywhere

3 participants