Files
GacUI/.github/KnowledgeBase/Learning.md
T
2026-05-20 02:08:57 -07:00

13 KiB
Raw Blame History

!!!LEARNING!!!

Orders

  • Process staged tasks one by one with verification [12]
  • Crash early instead of adding error-tolerance fallbacks [6]
  • Use WString::IndexOf with wchar_t (not const wchar_t*) [4]
  • Use collections::BinarySearchLambda on contiguous buffers (guard empty) [4]
  • Capture dependent lambdas explicitly [2]
  • Don't assume observable changes are batched [2]
  • Use ERROR_MESSAGE_PREFIX for meaningful CHECK_ERROR / CHECK_FAIL messages [2]
  • Prefer simple calls before interface casts [2]
  • Validate expectations against implementation and existing tests [2]
  • Use vl::Exception for expected semantic failures and CHECK_ERROR for invariants [2]
  • Do not assume async callback owners are heap allocated [1]
  • Extract abstractions only for real shared behavior [1]
  • Make Stop() drain asynchronous work before returning [1]
  • Port fixes from imports to source repositories [1]
  • Prefer well-defined tests over ambiguous edge cases [1]
  • Prefer operator<=> = default for lexicographic key structs [1]
  • Prefer two-pointer merge for sorted range maps [1]
  • Treat Debug memory leak dumps as required failures [1]
  • Use named sentinel constants instead of raw values [1]
  • Use Variant<T...>::Index() to check active alternative [1]
  • Avoid references into containers when mutating them [1]
  • Prefer designated initializers for aggregate-like structs [1]
  • Construct Nullable<WString> explicitly in function calls [1]
  • Sort serialization metadata by deterministic keys, not pointer addresses [1]
  • collections::Dictionary copy assignment is deleted (use move/swap) [1]
  • Dereference Ptr<T> via .Obj() (not *ptr) [1]
  • vl::regex separator regex: L"[\\/\\\\]+" [1]
  • Use 2-space indentation in embedded XML/JSON literals [1]
  • collections::List has deleted copy constructor; use std::move() for structs with List members [1]

Refinements

Capture dependent lambdas explicitly

When a C++ lambda uses another local lambda (or any local variable), capture it explicitly (e.g. [&] or [CreateBindableTree]). Lambdas do not implicitly capture other lambdas, and missing captures can show up as confusing compile errors.

Don't assume observable changes are batched

When verifying callbacks from an observable collection, do not assume multiple operations collapse into a single notification. For example, for vl::collections::ObservableList<T>, Clear() followed by multiple Add() calls triggers one callback pair per operation; test expectations should match the actual per-operation granularity.

Crash early instead of adding error-tolerance fallbacks

For public API arguments that callers can validate with query methods, fail with CHECK_ERROR when the argument is invalid instead of accepting impossible protocol states.

When an invariant says a value must exist or a conversion must succeed, prefer using it directly or using a strong cast so a violation crashes or throws immediately. Do not add speculative null checks, weak casts, or silent fallbacks that hide protocol or ownership bugs. Fix the real cause instead of making the code tolerant of states that should be impossible.

Process staged tasks one by one with verification

When a request is split into explicit tasks, complete and verify each task before starting the next one. This keeps commits easy to understand and review, limits side effects to the current task, and avoids having to diagnose many unrelated issues at the same time. If a task has its own finishing instructions, finish that task properly before moving on.

When a task boundary says to commit and push, do that before starting the next task so each task remains independently reviewable.

Use ERROR_MESSAGE_PREFIX for meaningful CHECK_ERROR / CHECK_FAIL messages

For source-code CHECK_ERROR / CHECK_FAIL calls with real diagnostic messages, follow the repo pattern: define ERROR_MESSAGE_PREFIX at the beginning of the function with the full class/function context, use it in the message, and undefine it at the end. Simple unsupported stubs such as CHECK_FAIL(L"Not Supported!") can stay as-is.

Prefer simple calls before interface casts

Do not add explicit interface casting (e.g. dynamic_cast<IFoo*>(...)) just because a similar file does it. Start with the simplest direct call, and only introduce interface casts when the compiler or access rules actually require it; unnecessary casts make tests noisier and can obscure which API surface is being exercised.

Prefer well-defined tests over ambiguous edge cases

When a scenarios expected behavior is unclear or undocumented (e.g. calling an operation while an object is in an “invisible”/non-effective state), avoid turning it into a test requirement. Prefer fewer tests that validate the public contract and real-world usage; if an edge case is important, first clarify the intended semantics from implementation and/or documentation.

Do not assume async callback owners are heap allocated

Asynchronous callbacks and handle-close paths must not rely on the owning object being allocated with new or outliving callbacks by convention. Track active operations explicitly and make shutdown drain or detach every callback path that can reference the object.

Extract abstractions only for real shared behavior

When refactoring client/server or similar paired implementations, extract common state and helper behavior only when it genuinely simplifies both sides. Preserve intentional differences in small derived redirects or callbacks instead of forcing an abstraction just to increase reuse.

Make Stop() drain asynchronous work before returning

If an API exposes Stop(), callers should be able to rely on it as the shutdown boundary: after it returns, no pending action, wait callback, overlapped I/O, or async completion should still touch the object. Do not paper over a broken Stop() with sleeps in tests; fix the stop path.

Port fixes from imports to source repositories

Do not treat files copied into Import or generated release files as the source of truth. When a fix affects imported Vlpp files, make the upstream change in Vlpp, regenerate its release output, and then copy the generated files downstream. When a .github instruction or script fix is needed, port it through Tools/Copilot.

Validate expectations against implementation and existing tests

Before encoding expectations (especially for return value conventions and error semantics), read the relevant implementation and check existing tests for established patterns. This reduces churn from mismatched assumptions (e.g. public API returning a normalized error value even if internals use different sentinel codes).

This also applies to enums and API surface: verify that enum values and method names/signatures actually exist before using them.

Prefer operator<=> = default for lexicographic key structs

When a struct is a pure lexicographic key (e.g. {begin, end}), prefer auto operator<=>(const T&) const = default; to generate a correct, consistent ordering and equality set automatically, instead of hand-writing <, ==, etc.

Use collections::BinarySearchLambda on contiguous buffers (guard empty)

collections::BinarySearchLambda expects a contiguous buffer pointer and count (e.g. &keys[0], keys.Count()), plus a search item, an out index, and an orderer that returns std::strong_ordering. When using it on Dictionary::Keys(), guard the empty-map case before taking &Keys()[0].

If you are searching for “overlap” rather than exact ordering, provide a custom orderer that defines “before / after / overlap” semantics for your ranges.

If multiple entries can satisfy “overlap”, binary search can return any matching entry. Scan backward (and/or forward) from the returned index to locate the first overlapping entry you intend to process.

For nearest-neighbor lookup on a sorted key buffer, implement helpers for largest key <= item and smallest key > item, and use those helpers for line/range iteration instead of mixing text positions with raw array indices.

Prefer two-pointer merge for sorted range maps

When combining or diffing two maps that are already sorted by range keys, iterate both in one pass using a two-pointer “merge sort merge phase” approach. This avoids nested scans and keeps merge/diff logic linear in the number of runs.

Treat Debug memory leak dumps as required failures

On Windows Debug unit-test runs with memory leak checking enabled, a test can pass all assertions and still fail engineering acceptance if the final execution log contains a CRT leak dump. Read the end of the log after tests pass and fix leaks instead of ignoring the appended report.

Use named sentinel constants instead of raw values

When a protocol needs a sentinel value such as an administrator or system client id, use a named constant and validate it explicitly. Do not scatter raw values like -1 through call sites; it makes sender/receiver semantics hard to audit and easy to misuse.

Use Variant<T...>::Index() to check active alternative

For Variant<T...>, use Index() (active alternative index) to branch on the stored type; do not assume helper methods like GetType() exist.

Avoid references into containers when mutating them

When iterating a container and performing mutations like Remove()/Add(), avoid holding references (auto&&) to elements across mutations, because internal storage can be reallocated or reordered. Copy keys/values you still need to local variables before modifying the container.

Prefer designated initializers for aggregate-like structs

For small structs used as value objects (especially those with default member initializers), prefer designated initializers like { .field = value } for clarity and to avoid compile-time issues from positional aggregate initialization.

Construct Nullable<WString> explicitly in function calls

When passing string literals to a function parameter typed as Nullable<WString>, wrap them in WString(...) (or otherwise construct a WString) to make the conversion explicit. Direct assignment to a Nullable<WString> field may compile via an assignment operator, but function-call argument conversion can require explicit construction.

Sort serialization metadata by deterministic keys, not pointer addresses

When serializing metadata into stable binary output, do not let pointer-address ordering decide indices or item order. Collect items for membership checks if needed, then sort the serialized lists by deterministic keys such as type names or owner-qualified member signatures before assigning indices and writing the stream. This applies to type descriptors, methods, properties, events, and generated custom-type lists whose order would otherwise depend on allocation order or ASLR.

collections::Dictionary copy assignment is deleted (use move/swap)

collections::Dictionary does not support copy assignment. When you need to replace one dictionary with another, use move semantics (when appropriate), or rebuild/swap explicitly instead of a = b.

Dereference Ptr<T> via .Obj() (not *ptr)

Ptr<T> is a smart pointer wrapper; to dereference it, use ptr.Obj() to get a raw pointer first (e.g. *ptr.Obj()). The *ptr syntax is not supported.

Use WString::IndexOf with wchar_t (not const wchar_t*)

vl::WString::IndexOf searches for a single character when given a character parameter; pass character literals like L'\r' / L'\n', not string literals like L"\r" / L"\n" (which are const wchar_t*).

Use 2-space indentation in embedded XML/JSON literals

When writing XML or JSON inside a C++ string literal (e.g. LR"GacUISrc(... )GacUISrc" resources), indent the XML/JSON with 2 spaces (not tabs) to match the repos formatting rules for embedded structured text.

vl::regex separator regex: L"[\\/\\\\]+"

In vl::regex::Regex, both / and \\ are escaping characters, and incorrect escaping inside [] can throw errors like Illegal character set definition.

To split paths by either / or \\, a verified pattern is L"[\\/\\\\]+", and using Regex::Split(..., keepEmptyMatch=false, ...) conveniently drops empty components (so // behaves like /).

collections::List has deleted copy constructor; use std::move() for structs with List members

When a C++ struct contains vl::collections::List fields, the struct's implicit copy constructor is deleted. Appending such structs to a vl::collections::List by copy will fail at compile time with error C2280. Use std::move() when adding these structs to destination lists (e.g. list.Add(std::move(model))). Note: there is no repo-provided Move() utility; always use std::move() from <utility>.

Use vl::Exception for expected semantic failures and CHECK_ERROR for invariants

When a failure is part of the public or script-visible semantics and tests are expected to catch it as a recoverable error, throw vl::Exception. Reserve CHECK_ERROR / CHECK_FAIL / vl::Error for internal invariant violations and states that indicate implementation corruption. For example, duplicate RPC registration can remain a catchable semantic exception when samples intentionally verify it, while impossible local type ids should fail as invariants.