Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

## Fixes

- Issue #32: recursive cycle paths retain compact immutable segments and format the full path only when a cycle is detected, removing quadratic path-prefix allocations while preserving exception metadata and first-edge allocation behavior. [#32](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/32)
- Issue #31: dictionary mappings preserve compatible key comparers independently of value conversion. Keys transformed by custom converters use the destination default comparer, even when key types match. [#31](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/31)
- Issue #30: set mappings probe exact accessible HashSet capacity constructors and use portable fallbacks when absent, including on netstandard2.0. Capacity-plus-comparer support is detected independently. [#30](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/30)
- Issue #28: IgnoreCase rejects multiple case-insensitive source matches even when one is exact; nested structural ambiguity retains LITEMAPPER1004. Exact and ExactThenIgnoreCase keep their existing exact-match priority. [#28](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/28)
Expand Down
8 changes: 8 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Mammoth.LiteMapper Decision Log

## Issue #32: immutable diagnostic paths with an inline first segment (2026-10-09)

- Context: each tracked member descent concatenated the complete prefix even when mapping succeeded. Current-develop measurements confirm increasing allocation at safe depths; collection bodies and acyclic/value bridges must retain the same path without adding segments.
- Decision: pass a private readonly Path value containing its literal leaf and an immutable ancestor link. The first segment stays inline; only extension of a nonempty path allocates a link. This preserves shallow and wide first-edge allocation behavior without mutable path-stack cleanup. The existing active-reference Enter/finally/Exit scopes and single tracker field remain unchanged.
- Formatting: only duplicate-reference Enter calls Path.ToString; two iterative passes count and reverse-fill a segment array, then join once. Successful active-path storage and total chain-link allocation are O(depth), with no copied full prefixes, source-object retention, reflection, or public API. Empty root collection paths and method metadata keep their existing meaning.
- Contract: sections 5.9, 14.1, 17.2-17.6, 19.5, 22.14, 23.3, and 28.7 already permit this private representation; no specification semantics change is needed. A mutable push/pop stack was avoided because context-only bridges and failed Enter calls would require new cleanup scopes.
- Evidence: the initial 13-case suite has two allocation/storage failures and 11 controls on unchanged production. At depth 256 total allocations fall from 421,520 to 32,392 bytes per mapping; depth 1/2 and 256 shallow sibling chains are unchanged. These are allocation measurements, not throughput or general stack-safety claims. See PERFORMANCE_OPTIMIZATION_NOTES.md for method and full results.

## Issue #31: classify the selected dictionary key conversion (2026-10-09)

- Context: the shared comparer predicate compared dictionary value types, so widening or structurally mapping values discarded an otherwise compatible key comparer. It also preserved comparers for same-type keys without checking whether a custom conversion was selected.
Expand Down
8 changes: 8 additions & 0 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Mammoth.LiteMapper Implementation Plan

## Issue #32: lazy cycle diagnostic paths (2026-10-09)

- Scope: avoid copying full path prefixes on successful recursive descent, preserving exact exception paths and public entry names, active-reference semantics, acyclic bridges, collections, and value-type traversal. No public API or specification semantics change.
- Test-first: measure current allocations at safe depths 16/32/64/128/256, then add linear-storage/scaling checks and exact-path, sibling, concurrency, cleanup, deterministic C# 9, and nonrecursive allocation controls before production edits.
- Implementation: use compact private path state and format the full string only on cycle detection. Keep reference Enter/finally/Exit behavior and one tracker per recursive call.
- Validation: focused/full generator, runtime/integration, source and clean-package consumers, framework/language matrix, available packaging/API/benchmark checks, independent review, and exact-head ordinary Windows/Linux and Roslyn CI. Report allocation evidence separately from throughput.
- Boundary: issue #32 only, then draft PR/human review. No merge, release, tag, package publication, or workflow dispatch. AgentStack CLI is absent; authorized connector fallback adds/readbacks readiness labels without issue comments or claim writes.

## Issue #31: dictionary key comparer preservation (2026-10-09)

- Scope: preserve a concrete Dictionary source's compatible key comparer when key resolution selects language identity, independently of value conversion. Converted keys use the destination default comparer; retain Add collision semantics and existing set behavior.
Expand Down
20 changes: 20 additions & 0 deletions PERFORMANCE_OPTIMIZATION_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

These notes are non-authoritative follow-up candidates. `SPECIFICATION.md` remains the product contract.

## Issue #32: lazy cycle-path allocation evidence (2026-10-09)

- Baseline: unchanged develop `7b19def74753df90cb3770c7410ff5d7a3951244`. Candidate: issue #32's private inline-leaf/immutable-link diagnostic path representation.
- Environment: Debian 13 Linux x64 cloud container, AMD EPYC 9V74 host as exposed by `/proc/cpuinfo`, SDK 10.0.401, runtime 10.0.12, Roslyn 4.8.0, Release compilation, C# 9, `DOTNET_PROCESSOR_COUNT=1`. This is not a controlled release throughput benchmark.
- Harness: `LazyCyclePathTests.SuccessfulChainAllocationsScaleLinearlyAtSafeDepths`, using `GC.GetAllocatedBytesForCurrentThread`. Each depth uses 40 warmups followed by three rounds of 100 mappings. Source construction and reporting are outside measurement; each mapped chain is validated without allocating. All three rounds give identical values. The no-tracking control allocates destination nodes only.

| Chain depth | Previous ThrowOnCycle bytes/op | Lazy-path bytes/op | None bytes/op (unchanged) |
|---:|---:|---:|---:|
| 16 | 3,032 | 1,744 | 512 |
| 32 | 9,200 | 3,560 | 1,024 |
| 64 | 31,000 | 7,440 | 2,048 |
| 128 | 111,792 | 15,528 | 4,096 |
| 256 | 421,520 | 32,392 | 8,192 |

`ShallowAndWideTraversalAllocationControls` additionally measures depths 1 and 2 at 232 and 264 bytes/op, and an array of 256 sibling depth-2 chains at 18,656 bytes/op. These values are unchanged by the optimization. Runtime manual controls verify that first named edges introduce no path-object allocation and that shallow siblings share only one tracker overhead.

Generated source retains one literal segment per immutable link and no materialized prefix; formatting occurs only in the duplicate-reference throwing branch. This establishes linear active diagnostic-path storage by code structure. The numbers above are total managed allocations, including destination objects and tracker/HashSet growth, not isolated diagnostic allocations or peak-memory measurements. No latency, throughput, release acceptance, or general stack-safety improvement is claimed. Safe test depth is capped at 256.

Reproduce after explicit restore/build with `dotnet test tests/Mammoth.LiteMapper.Generator.Tests/Mammoth.LiteMapper.Generator.Tests.csproj -c Release --no-build --no-restore --filter LazyCyclePathTests --logger trx`; per-round measurements are retained in TRX standard output. The existing `RecursiveCycleAllocationBenchmarks` remains available for controlled BenchmarkDotNet throughput comparison; its historical manual tracker does not build public exception paths and is not a diagnostic-path allocation baseline.

## Flat-object benchmark observations

- Benchmark: `FlatObjectBenchmarks`.
Expand Down
12 changes: 12 additions & 0 deletions STATUS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Mammoth.LiteMapper Status

## GitHub issue #32 lazy cycle-path checkpoint (2026-10-09)

- Base: isolated cloud worktree from develop 7b19def74753df90cb3770c7410ff5d7a3951244 after PR #45 merged. Issue #32 was the only open issue, with no blocked-by dependencies, sub-issues, or competing PRs. Authorized exec:agent and ready:agent labels were added/read back. AgentStack CLI is absent; connector fallback is used without issue comments or claim writes.
- Red-first: the initial 13-case C# 9 suite produced two allocation/storage failures and 11 passing controls, zero skips, on unchanged production. The tracked chain retained quadratic full-string prefixes. Exact metadata, sibling/concurrent calls, same-tracker cleanup, and None controls were already passing; no expected runtime contract was changed.
- Implementation: private readonly Path values keep the first literal segment inline and allocate immutable ancestor links only when extending a nonempty path. Duplicate-reference Enter formats the path with two iterative passes and one join. Active-reference Enter/finally/Exit scopes, public names, collection forwarding, and the one-field tracker remain unchanged. No public API, reflection, runtime dispatch, or specification semantics change.
- Allocation evidence: depths 16/32/64/128/256 improve from 3,032/9,200/31,000/111,792/421,520 to 1,744/3,560/7,440/15,528/32,392 total bytes per mapping in three identical rounds. None remains 512/1,024/2,048/4,096/8,192; shallow depths 1/2 remain 232/264 and 256 sibling depth-2 chains remain 18,656. Runtime manual controls verify no added first-edge path allocation. PERFORMANCE_OPTIMIZATION_NOTES.md records environment and method; no throughput, peak-memory, release, or general stack-safety claim is made.
- Core evidence: final Release solution build has zero warnings/errors. All 917 generator tests, five runtime tests, and one integration test pass with zero failures/skips. All 13 focused cases and three source samples pass. The new shared Basic example covers depth 256, exact direct/indirect/bridged collection paths, public method names, shared siblings, and later calls after exceptions. Independent read-only review, consumer-skill validation, and CRLF-aware diff checks pass.
- Package evidence: Available packaging retry: 224 passed, two infrastructure failures, zero skips. All 12 framework/language combinations, normal/clean-package consumers, usage validation, package/API checks, and benchmark smoke pass. The shared cycle example executes in each framework/language consumer, including genuine netstandard2.0 references; actual netstandard2.0/C#9 generated source was inspected. Trimming and Native AOT fail before publish behavior is established at ILLink ComputeManagedAssemblies task-host creation (MSB4216 / Unix-domain SocketException 13). No local trimming/AOT success is claimed.
- Validation recovery: the first candidate passed 111 of 112 focused cases; one existing string-specific source regex was updated solely for the new representation while retaining its runtime assertions. Strengthening shallow allocation controls exposed CS0762 in the test input before source generation; a lambda wrapper corrected the fixture without changing production or weakening the clean-input check. A complete 917-test rerun then passed. The first available-package attempt was interrupted by automatic approval-review cancellation and produced no final TRX; it is not completion evidence.
- Environment: existing official SDK 10.0.401, runtimes 8.0.31/9.0.20/10.0.12, task-local temporary files/caches, and one-processor builds. PowerShell and clang are absent; 49 PowerShell-dependent cases are unavailable locally. No assertion, warning, dependency, prerequisite, credential, or security setting was weakened. Exact-head ordinary Windows/Linux and Roslyn CI evidence belongs in the draft PR.
- Boundary: issue #32 only, then human review. The previously documented nullable-intermediate enum defect remains outside scope. No user-computer access, other issue, merge, release, tag push, package publication, or workflow dispatch.

## GitHub issue #31 dictionary comparer checkpoint (2026-10-09)

- Base: isolated cloud worktree from develop c973aac8e457cc40a4069889aeab49d8278ccca3 after PR #44 merged. Issue #31 was the older remaining P3, with no blocked-by dependencies, sub-issues, or competing PR. Explicitly authorized exec:agent and ready:agent labels were added and read back. AgentStack CLI is absent; connector fallback is used without issue comments or claim writes.
Expand Down
2 changes: 2 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,8 @@ public static partial class CycleMapper

`ReferenceHandling.ThrowOnCycle` enables generated cycle tracking for recursive type graphs. A detected cycle throws `LiteMapperCycleException`. Non-recursive mappings do not allocate cycle-tracker state.

Successful recursive traversal retains compact diagnostic path segments rather than allocating a full path string at every level. The complete `MemberPath` is formatted only when a cycle is detected; active diagnostic-path storage grows linearly with depth. This does not remove the call-stack limit of recursive mapping. The [Basic cycle example](../samples/Mammoth.LiteMapper.Samples.Basic/LazyCyclePathExample.cs) exercises finite chains, exact direct/indirect/collection paths, shared siblings, and later calls after exceptions.

Acyclic object and collection wrappers can lead into a recursive graph. The same tracker is forwarded through those wrappers, while only recursive reference objects participate in cycle detection. The Basic sample maps `WrappedSource.Envelope.Nodes`; a self-cycle in a node reports `Envelope.Nodes.Next`, and a shared node in separate collection positions maps independently.

`LiteMapperCycleException` exposes `SourceType`, `DestinationType`, `MappingMethod`, and `MemberPath` for the detected active-path cycle. It does not expose `SourcePath` or `DestinationPath` properties.
Expand Down
Loading
Loading