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
7 changes: 7 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Mammoth.LiteMapper Decision Log

## Issue #47: configured nullable updater captures (2026-10-09)

- Non-semantic repair under specification sections 9.4, 11.4, 14.4, 16.1 and 16.3. A patch pattern capture unwraps Nullable<T>; both automatic and configured source paths must restore the selected source type at invocation so C# overload binding matches generator resolution.
- Extend the existing direct-member cast condition to configured captures. Do not change updater selection, receiver dispatch, Throw-policy captures, update ordering, public API or specification semantics.
- Tests preserve explicit Use and Source-only default selection, direct/dotted and nullable-intermediate paths, void/returning methods, identity/replacement, null skipping, changing getters, and non-nullable-leaf/non-patch controls. The separate enum-path issue #48 is untouched.


## 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.
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 #47: configured nullable updater overload binding (2026-10-09)

- Scope: preserve the selected nullable value-type updater overload after capturing a configured direct or dotted patch source path. Retain single evaluation, null skipping, target identity, and sequential update behavior. No public API or specification semantics change.
- Test-first: reproduce competing nullable/non-nullable overloads across direct/dotted paths, void/returning updaters, missing/changing values, nullable intermediates, instance/external methods, and C# 9 deterministic output. Add a shared normally compiled and package-consumer example.
- Implementation: extend the existing nullable capture type restoration to configured source paths without changing updater resolution or emission order.
- Validation: focused/full generator, runtime/integration, source and clean-package samples, supported framework/language consumers, available packaging checks, independent review, and exact-head ordinary Windows/Linux and Roslyn CI. Disclose unavailable local stages.
- Boundary: issue #47 only, then draft PR/human review. Issue #48 remains open and untouched. No merge, release, tag, publication or workflow dispatch. AgentStack CLI is absent; the authorized connector fallback adds/readbacks readiness labels without issue comments or claim writes.

## 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.
Expand Down
11 changes: 11 additions & 0 deletions STATUS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Mammoth.LiteMapper Status

## GitHub issue #47 configured nullable updater checkpoint (2026-10-09)

- Base: isolated cloud worktree from develop 9c48bfd59fe586097ca18c4289c85f03e93e30bc after PR #46 merged. New P2 issue #47 has 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 completed 29-case addition yielded 20 failures and nine passing controls on unchanged production; all 38 prior capture cases passed, zero skips. Configured direct/dotted paths invoked the competing non-nullable updater. The normally compiled Basic sample independently failed its overload assertion. One malformed external-container fixture in the initial iteration was corrected before the final baseline.
- Implementation: remove the direct-member-only restriction from existing patch-capture type restoration. Both automatic and configured value captures retain their selected source type for C# overload binding. Resolver precedence, guards, receivers, per-member order, public API and specification semantics are unchanged.
- Core evidence: Release solution build has zero warnings/errors. All 946 generator, five runtime and one integration tests pass with zero failures/skips. The related focused group passes 111/111; all three source samples pass. Independent read-only review, consumer-skill validation and CRLF-aware diff checks pass.
- Package evidence: available packaging suite records 224 passes, two infrastructure failures, zero skips. All 12 framework/language consumers, clean-package samples, usage validation, package/API checks and benchmark smoke pass. The shared ConfiguredUpdaterOverloadExample executes in source, clean-package and matrix consumers, including real netstandard2.0/C#9 references; generated source was inspected for both nullable casts and capture reuse. Normal/trim/AOT consumers include the same example.
- Local limits: trimming and Native AOT fail before publish behavior is established at ILLink ComputeManagedAssemblies task-host creation, with MSB4216 / Unix-domain SocketException 13. PowerShell and clang are absent; 49 PowerShell-dependent cases were not run locally. No warning, assertion, dependency, prerequisite, credential or security setting was weakened. SDK 10.0.401 and runtimes 8.0.31/9.0.20/10.0.12 use workspace-local caches/temporary files and one-processor builds. Exact-head ordinary Windows/Linux and Roslyn CI evidence belongs in the draft PR.
- Coverage and boundary: direct/dotted explicit Use, Source-only default selection, missing leaf/intermediate, non-nullable leaf, void/returning/get-only/replacement, instance/external, changing getters, sequential failure and deterministic C#9 output. Issue #47 only, then human review; issue #48 remains open and untouched. No user-computer access, merge, release, tag, package publication or workflow dispatch.


## 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.
Expand Down
2 changes: 1 addition & 1 deletion docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ For a writable non-null child, select the updater explicitly with `[MapProperty(

Omitting `Source` in `[MapProperty(Target = nameof(Target.Child), Use = nameof(ApplyChild))]` passes the root source to `void ApplyChild(Source source, ChildTarget target)`. No source member named `Child` is required: the updater can copy root `Value == 3` into `target.Child.Value`.

The patch guard and nested updater share one captured source value: a nullable getter is evaluated once, even if another read would return a different object or null. The [Basic sample](../samples/Mammoth.LiteMapper.Samples.Basic/Program.cs) demonstrates this with `PatchUpdaterExample`.
The patch guard and nested updater share one captured source value: a nullable getter is evaluated once, even if another read would return a different object or null. The [Basic sample](../samples/Mammoth.LiteMapper.Samples.Basic/Program.cs) demonstrates this with `PatchUpdaterExample`. Nullable value captures retain the selected updater overload for automatic direct members and configured direct or dotted source paths. The [configured overload sample](../samples/Mammoth.LiteMapper.Samples.Basic/ConfiguredUpdaterOverloadExample.cs) selects `ApplyChild(ConfiguredUpdaterValue?, ConfiguredUpdaterChild)` even when a competing non-nullable overload exists, and preserves the target for a null leaf or intermediate.

Patch mode also applies to nested updater calls. If `source.Child` is null and `IgnoreNullSourceMembers` is enabled, the updater is skipped and the existing target child remains unchanged. With the default mismatch policy, passing a nullable child to a non-null updater parameter reports `LITEMAPPER2001`; `Throw` raises `InvalidOperationException` whose message includes the `Child` path.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System;

namespace Mammoth.LiteMapper.Samples.Basic
{
public static class ConfiguredUpdaterOverloadExample
{
public static void Run()
{
var direct = new ConfiguredUpdaterSource(new ConfiguredUpdaterValue { Value = 7 });
var target = new ConfiguredUpdaterTarget();
var child = target.Child;
ConfiguredUpdaterMapper.ApplyDirect(direct, target);
if (direct.Reads != 1 || target.Child.Value != 7 || !ReferenceEquals(child, target.Child))
throw new InvalidOperationException("The configured direct path must preserve the nullable updater overload.");

var leaf = new ConfiguredUpdaterSource(new ConfiguredUpdaterValue { Value = 11 });
var path = new ConfiguredUpdaterPath(leaf);
var returned = ConfiguredUpdaterMapper.ApplyPath(path, target);
if (path.Reads != 1 || leaf.Reads != 1 || target.Child.Value != 11 ||
!ReferenceEquals(target, returned) || !ReferenceEquals(child, target.Child))
throw new InvalidOperationException("The configured dotted path must consume its selected nullable value once.");

var missingLeaf = new ConfiguredUpdaterSource(null);
ConfiguredUpdaterMapper.ApplyDirect(missingLeaf, target);
var missingPath = new ConfiguredUpdaterPath(null);
ConfiguredUpdaterMapper.ApplyPath(missingPath, target);
if (missingLeaf.Reads != 1 || missingPath.Reads != 1 || target.Child.Value != 11 ||
!ReferenceEquals(child, target.Child))
throw new InvalidOperationException("A missing configured value must preserve the existing nested target.");
}
}

[LiteMapper(IgnoreNullSourceMembers = true)]
public static partial class ConfiguredUpdaterMapper
{
[MapProperty(Source = "Child", Target = nameof(ConfiguredUpdaterTarget.Child), Use = nameof(ApplyChild))]
public static partial void ApplyDirect(ConfiguredUpdaterSource source, ConfiguredUpdaterTarget target);

[MapProperty(Source = "Container.Child", Target = nameof(ConfiguredUpdaterTarget.Child), Use = nameof(ApplyChild))]
public static partial ConfiguredUpdaterTarget ApplyPath(ConfiguredUpdaterPath source, ConfiguredUpdaterTarget target);

private static ConfiguredUpdaterChild ApplyChild(ConfiguredUpdaterValue? source, ConfiguredUpdaterChild target)
{
target.Value = source?.Value ?? -1;
return target;
}

private static ConfiguredUpdaterChild ApplyChild(ConfiguredUpdaterValue source, ConfiguredUpdaterChild target)
{
target.Value = 99;
return target;
}
}

public sealed class ConfiguredUpdaterSource
{
private readonly ConfiguredUpdaterValue? first;
public ConfiguredUpdaterSource(ConfiguredUpdaterValue? first) { this.first = first; }
public int Reads { get; private set; }
public ConfiguredUpdaterValue? Child { get { Reads++; return Reads == 1 ? first : null; } }
}

public sealed class ConfiguredUpdaterPath
{
private readonly ConfiguredUpdaterSource? first;
public ConfiguredUpdaterPath(ConfiguredUpdaterSource? first) { this.first = first; }
public int Reads { get; private set; }
public ConfiguredUpdaterSource? Container { get { Reads++; return Reads == 1 ? first : null; } }
}

public struct ConfiguredUpdaterValue { public int Value { get; set; } }
public sealed class ConfiguredUpdaterChild { public int Value { get; set; } }
public sealed class ConfiguredUpdaterTarget { public ConfiguredUpdaterChild Child { get; set; } = new ConfiguredUpdaterChild(); }
}
1 change: 1 addition & 0 deletions samples/Mammoth.LiteMapper.Samples.Basic/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ internal static class Program
private static void Main()
{
LazyCyclePathExample.Run();
ConfiguredUpdaterOverloadExample.Run();
EnumPathExample.Run();
PatchUpdaterExample.Run();
var staticTarget = StaticMapper.Map(new StaticSource
Expand Down
2 changes: 1 addition & 1 deletion skills/mammoth-litemapper/references/mapping-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,4 @@ For a writable non-null child, select the updater explicitly with `[MapProperty(

Omitting `Source` in `[MapProperty(Target = nameof(Target.Child), Use = nameof(ApplyChild))]` passes the root source to `void ApplyChild(Source source, ChildTarget target)`. No source member named `Child` is required: the updater can copy root `Value == 3` into `target.Child.Value`.

Patch guards and nested updater calls share the same captured source value, so direct nullable getters are read once and null skips the updater. Direct nullable value captures preserve the selected updater overload. Nested updater calls obey patch and nullability rules. With `IgnoreNullSourceMembers`, null `source.Child` skips the updater and preserves the destination. Under the default mismatch policy, nullable child to a non-null updater parameter reports `LITEMAPPER2001`; `Throw` identifies the member path. A returning updater may create and return a nullable writable child, and the parent assigns it. A nullable get-only child cannot store a returned replacement and reports `LITEMAPPER5005`.
Patch guards and nested updater calls share the same captured source value, so direct nullable getters are read once and null skips the updater. Nullable value captures preserve the selected updater overload for both automatic direct members and configured direct or dotted source paths. Nested updater calls obey patch and nullability rules. With `IgnoreNullSourceMembers`, null `source.Child` skips the updater and preserves the destination. Under the default mismatch policy, nullable child to a non-null updater parameter reports `LITEMAPPER2001`; `Throw` identifies the member path. A returning updater may create and return a nullable writable child, and the parent assigns it. A nullable get-only child cannot store a returned replacement and reports `LITEMAPPER5005`.
2 changes: 1 addition & 1 deletion src/Mammoth.LiteMapper.Generator/LiteMapperGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ private static MappingModel CreateUpdateMappingModel(IMethodSymbol method, Compi
: selected != null
? sourcePathCaptureName ?? throwCaptureName ?? BuildNullSafeSourcePathExpression(source.Name, selected)
: sourcePathCaptureName ?? throwCaptureName ?? escapedSourceName + "." + EscapeIdentifier(match.Member.Name);
if (selected == null && sourcePathCaptureName != null && nestedSourceType.IsValueType)
if (sourcePathCaptureName != null && nestedSourceType.IsValueType)
{
// A pattern capture unwraps Nullable<T>; retain the selected input type for overload binding.
argument = "(" + DisplayType(nestedSourceType) + ")" + argument;
Expand Down
Loading
Loading