Skip to content

forms, table: pair reflected keys and values through one shared helper - #909

Merged
Yaraslaut merged 1 commit into
masterfrom
fix/907-meta-member-pairing
Oct 10, 2026
Merged

Yaraslaut merged 1 commit into
masterfrom
fix/907-meta-member-pairing

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Closes #907

The defect

forms::detail::forEachNamedMember named member I with glz::reflect<A>::keys[I] but read the value at get<I>(glz::to_tie(action)). to_tie follows declaration order and ignores glz::meta. Under a reordered or partial meta, one member's value was handed to another member's name. That could offer a hidden member under a public key, or bind a key to a value of a different type. reconcileDeclaredPrecision walked the same tie, so it could skip a Quantity that the meta lists.

The shared helper

The table engine already had the correct pairing as a private copy (table::detail::memberAt). Two copies of one defect is the "missing framework seam" case, so this PR promotes that pairing to one helper instead of writing a second fix:

  • morph::detail::reflectedMember<I>(object) in include/morph/detail/reflected_member.hpp, next to fixed_string.hpp, the other cross-module detail utility. It reads the declaration-order tie for a type glaze reflects by itself, and the meta's I-th value otherwise. It returns a reference into the object, const when the object is const. A computed meta entry yields whatever glz::get_member yields for it.

Call sites:

  • forms: forEachNamedMember, so every caller inherits the fix: memberWireName/describe<>(), schema derivation, views' column walk, and the session walks. reconcileDeclaredPrecision also uses the helper directly. Under a meta it rounds only listed members, and it skips a computed (by-value) entry because there is no stored member behind it.
  • table: cellReaders and reflectedColumns. memberAt is deleted.

Spec: docs/spec/forms/forms.md (rows for forEachNamedMember and reconcileDeclaredPrecision). The table spec already describes reading through the meta, so its contract is unchanged.

Verification (measured, clang-debug, Homebrew clang 23, pinned glaze 719ef1be)

New tests:

  • tests/test_forms_meta_members.cpp: partial meta (each key gets its own member, and the hidden internalNote is never offered), reordered meta (meta order, and writes reach the named member), plain aggregate (declaration order), memberWireName under reordered and partial metas, and the helper's reference/constness types.
  • tests/test_quantity_forms.cpp: ReconcileDeclaredPrecision::FollowsAPartialMeta.

Results: morph_tests 1845 test cases, all pass (the one "failed as expected" is a pre-existing [!shouldfail]). morph_table_tests all pass (26071 assertions in 125 test cases).

Mutation 1: forms reverted to master's forms.hpp, new tests kept. Excerpt:

test_forms_meta_members.cpp:125: FAILED:
  CHECK( morph::forms::detail::memberWireName<&fmm::ReorderedMeta::b>() == "b" )
with expansion:
  "a" == "b"
test_forms_meta_members.cpp:127: FAILED:
  CHECK( morph::forms::detail::memberWireName<&fmm::PartialMeta::internalNote>().empty() )
with expansion:
  false
test_forms_meta_members.cpp:119: FAILED:
  CHECK( action.a == "a-written" )
with expansion:
  "b-written" == "a-written"
test_forms_meta_members.cpp:93: FAILED:
  CHECK( visit.value != "SECRET" )
with expansion:
  "SECRET" != "SECRET"
===============================================================================
test cases:  7 | 2 passed | 5 failed
assertions: 18 | 9 passed | 9 failed
test_quantity_forms.cpp:1176: FAILED:
  CHECK( (*action.moisture).getDecimalPlaces() == dp1 )
test_quantity_forms.cpp:1177: FAILED:
  CHECK( *action.moisture == Rational{Numerator{6}, Denominator{5}, dp1} )
test cases: 1 | 1 failed

The two [reflection] cases that still pass under the mutation are the plain-aggregate walk and the helper's own type checks, which do not go through the forms code. Restored, all pass.

Mutation 2: helper forced to the to_tie path (if constexpr (true)). This shows the table really reads through the shared helper:

test_table_server.cpp:88: FAILED:
with expansion:
  "secret-z" == "Arsenic"
test_table_server.cpp:103: FAILED:
with expansion:
  "bolt" == "4"
test cases:   8 |   7 passed | 1 failed
assertions: 677 | 672 passed | 5 failed

Restored, all pass.

Not verified:

  • The ladder rungs that declare glz::meta (crm, lims, ledger) were configured with MORPH_BUILD_LADDER=ON. The final rebuild was stopped at 87/113 because the machine ran low on memory; it had no compile errors up to that point. Their full builds and tests are left to CI.
  • MSVC, GCC, clang 22 -Wdocumentation, and Doxygen were not run locally.

Review notes

  • A glz::meta entry that wraps or computes a value (glz::quoted_num, a lambda) now reaches forms visitors as glaze's wrapper or value, not the underlying member, because that is what glaze serialises under the key. No forms action in tree uses such an entry; the only glz::custom is Rational's value meta, which is not an object meta. reconcileDeclaredPrecision explicitly skips by-value entries so that every registered action still compiles.
  • The helper takes T&, so a temporary cannot bind and leave a dangling reference.

🤖 Generated with Claude Code

@Yaraslaut Yaraslaut added bug Something isn't working area: forms Subsystem: forms labels Oct 10, 2026
@Yaraslaut
Yaraslaut force-pushed the fix/907-meta-member-pairing branch from b9fc524 to c75a0b0 Compare October 10, 2026 10:08
forms::detail::forEachNamedMember named member I with glz::reflect<A>::keys[I]
but read the value at get<I>(glz::to_tie(action)). to_tie follows declaration
order and ignores glz::meta, so under a reordered or partial meta one member's
value was handed to another member's name: a hidden member offered under a
public key, or a key bound to a value of a different type. reconcileDeclaredPrecision
walked the same tie and could miss a Quantity the meta lists.

The table engine carried its own copy of the correct pairing (memberAt).
Promote it to morph::detail::reflectedMember<I>(object) in
include/morph/detail/reflected_member.hpp: the declaration-order tie for a
type glaze reflects by itself, the meta's I-th value otherwise. forms
(forEachNamedMember, reconcileDeclaredPrecision) and table (cellReaders,
reflectedColumns) now both read through it.

Closes #907

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Yaraslaut
Yaraslaut force-pushed the fix/907-meta-member-pairing branch from c75a0b0 to d35b784 Compare October 10, 2026 10:36
@Yaraslaut
Yaraslaut merged commit 2f42a07 into master Oct 10, 2026
17 checks passed
@codecov

codecov Bot commented Oct 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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

Labels

area: forms Subsystem: forms bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

forms: forEachNamedMember pairs glz::reflect names with to_tie positions, mismatching members under a glz::meta

1 participant