Skip to content

core: process the active case of a C# 15 union command result - #199

Merged
carldebilly merged 2 commits into
mainfrom
dev/cdb/union-results
Nov 11, 2026
Merged

carldebilly merged 2 commits into
mainfrom
dev/cdb/union-results

Conversation

@carldebilly

@carldebilly carldebilly commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

Closes #177.

What changed

  • CommandInvoker.InvokeAsync replaces a union result by its active case (System.Runtime.CompilerServices.IUnion.Value) after the await, for sync, Task<T> and ValueTask<T> handlers. Everything downstream (middleware's ReplExecutionContext.Result, classification, exit codes, every output format, Repl.Testing) sees the case.
  • A case that is itself a Task<T>/ValueTask<T> is awaited, as the same task returned directly would be; a faulted task fails the run the same way. Only values that come out of a union are awaited, so a handler's own Task<Task<T>> keeps its single await.
  • A union in a union is unwrapped (bounded at 16 steps, a guard against a custom IUnion that leads back to itself); a default union or a case holding null behaves like null.
  • A union element of a returned tuple is processed the same way, and a union a middleware sets as ReplExecutionContext.Result is processed once the pipeline completes.
  • Compiled for net11.0 only (#if NET11_0_OR_GREATER): native unions need the .NET 11 runtime. net10.0 is unchanged.
  • docs/commands.md: a "Union return values (.NET 11)" section with a working example, the semantics and the limits, plus a row in the return-pattern table.

Verified on SDK 11.0.100

IUnion is in corelib with object? Value; value-type cases come out boxed; default(union).Value is null; a union holding a union yields an IUnion.

Tests

Given_UnionResults (net11.0 only), written first. The awaitable-case fix added 9 more tests that failed before it (Task and ValueTask cases, an IExitResult behind a task, a faulted task compared with the task returned directly, a middleware setting a union, a tuple element whose case is a task). Also covered: sync/Task/ValueTask handlers, value-type, default and null cases, nested unions, IExitResult and IReplResult cases compared with the same result returned directly, JSON and human output equal to the bare payload, the depth bound, a middleware observing the active case, and a union parameter (usage error from command input, injected from the services).

Full suite on net10.0 and net11.0: 5594 total, 0 failed, 74 skipped (Windows); -warnaserror and markdownlint clean.

Limits, documented

  • A union gets no conversion from command input: like any type Repl cannot convert, a value given for a union parameter is a usage error (exit code 2). A union parameter can still come from the services. A general Map-time check for unconvertible input types is Refuse at Map a command-input parameter whose type cannot be converted #202; union input support only on a real use case.
  • A union passed as the payload of a Results helper (Results.Exit(3, lookup)) is rendered as an ordinary object.
  • A union built on a polyfilled IUnion (an assembly compiled for net10.0) gets no unwrapping, even on the .NET 11 runtime.
  • McpAppResourceInvoker calls its handlers without going through CommandInvoker, so a UI resource handler returning a union is not unwrapped; those handlers return strings.
  • Discovery does not look inside a union: the command is not described as paged even when every case is a page. The MCP anyOf output schema is mcp: describe union command results with anyOf in the output schema #185.

@carldebilly carldebilly added this to the 0.12.1 milestone Nov 11, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-11T06:04:57.685492Z f273279 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@autocarl autocarl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested at exact head 9e80985, against merge-base 00b35aa. One introduced P2 confirmed by public-API execution: awaitable union cases bypass awaiting, leak task wrappers into output and can lose an explicit exit code. See inline reproduction and controls. Independent read-only review covered all five changed files; parent inspected the delta/callers and executed the candidate. No other concrete introduced P0-P2 established. Documented deferred MCP schemas/UI resources, paging discovery and net10 polyfills are not reported as defects.

Validation: exact-head Release unit-test project build with warnings as errors passed; both unit-test targets passed, 1348 net10.0 plus 1360 net11.0 (2708 passed, 0 failed/skipped). These existing tests do not cover the reproduced awaitable-case failure. External SDK11 probe built and executed against this checkout; no product source edits or package publication.

Separate unresolved CI gate: Build, Test, Pack failed in Repl.RemoteTests/net11.0, When_TheHostStops_Then_CommandCleanupCannotDisableItsDeadline(blocking-callback,1): exit 0 instead of expected 1. This is not attributed to the union delta without introduction evidence; it remains unvalidated rather than being dismissed as flaky. Local unit-test success is not a claim of green full CI.

Comment thread src/Repl.Core/Parsing/CommandInvoker.cs Resolved
Base automatically changed from dev/cdb/net11-multitarget to main November 11, 2026 02:38
@carldebilly
carldebilly force-pushed the dev/cdb/union-results branch from 46ed50a to 806bc8c Compare November 11, 2026 02:58
On net11.0 a handler may return a union (sync, Task<T> or ValueTask<T>). The
command invoker replaces it with the case it holds, after the await, so exit
codes, error classification, rendering and middleware see the value the handler
meant to return. A union nested in a union, or placed as an element of a returned
tuple, is unwrapped too; a default union behaves like null.

The unwrapping compiles for net11.0 only: native unions need the .NET 11 runtime,
and net10.0 behaviour is unchanged. A union is not a bindable input and is
refused as a usage error, which a test now pins.
A union whose active case was a Task<T> or ValueTask<T> reached rendering as
the task: an IExitResult behind it was lost (exit 0) and a faulted task passed
as success. The case is now awaited as the same task returned directly would
be, inside the bounded unwrap; only values that come out of a union are
awaited, so a handler's own Task<Task<T>> keeps its single await.

A union a middleware sets as ReplExecutionContext.Result is unwrapped before
rendering, and a union element of a returned tuple gets the same await.

A union handler parameter keeps the generic behaviour of any type Repl cannot
convert from command input (a usage error), and can still come from the
services; the union-specific message is gone, and #202 tracks a Map-time check
for every unconvertible input type. The docs list the union semantics and
limits.
@carldebilly
carldebilly force-pushed the dev/cdb/union-results branch from 806bc8c to f273279 Compare November 11, 2026 06:03

@autocarl autocarl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved at exact head f273279, against unchanged base cc5a043. This supersedes my earlier request-changes review: the awaitable union-case blocker is fixed and no other concrete introduced P0-P2 was found in the four-file delta, including independent read-only revalidation.

Previously completed exact-head validation: warning-as-error Release unit-test project/dependency build; 1373 net10.0 and 1396 net11.0 unit tests passed (2769, no failures/skips); original nine-case external public-API reproduction rebuilt against this checkout. Task/ValueTask union cases match direct-return controls, faulted cases preserve the error/exit behavior, and the original union Task now exits 7 instead of 0. No product source edits or full-solution local rebuild.

Refreshed live head/base, review threads and CI before approval. The macOS rerun now passes, including its Test step, closing the previously missing validation gate. Linux/Windows builds/tests, Build Test Pack, container/shell/MCP smoke, signal stress and documentation/security checks are successful; GitHub Release is skipped. The prior macOS Actions environment-file failure remains historical, not attributed to the union change. No new local rebuild was needed for this CI-only transition. No merge performed.

@carldebilly
carldebilly merged commit 0c9e0af into main Nov 11, 2026
28 of 29 checks passed
@carldebilly
carldebilly deleted the dev/cdb/union-results branch November 11, 2026 12:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support C# 15 union command results and assess related .NET 11 features

2 participants