[await?] Semantic model and IOperation tests#83236
Draft
CyrusNajmabadi wants to merge 58 commits intodotnet:features/null-conditional-awaitfrom
Draft
[await?] Semantic model and IOperation tests#83236CyrusNajmabadi wants to merge 58 commits intodotnet:features/null-conditional-awaitfrom
CyrusNajmabadi wants to merge 58 commits intodotnet:features/null-conditional-awaitfrom
Conversation
added 2 commits
April 18, 2026 16:17
Extends AwaitExpressionSyntax with an optional QuestionToken child, and teaches the parser to eat the `?` that immediately follows `await`. No binder or lowering changes: await? t is currently bound identically to await t, ignoring the ?. Feature-availability gating and real semantics come in a follow-up.
Tests live in src/Compilers/CSharp/Test/CSharp15/NullConditionalAwaitParsingTests.cs. Adds file-links for ParsingTests and SyntaxExtensions so the CSharp15 test project can use UsingTree / UsingExpression / N-walks like the existing Syntax/Parsing tests. Coverage: - Tree-shape tests for await / await? in async, sync, and top-level-statements contexts. - Whitespace and trivia between `await` and `?`. - Every positive case in the non-async IsAwaitExpression lookahead switch, both with and without `?`, including the peek-past-? path. - Negative cases: `with` identifier, tokens not in the switch (`;`, `(`, `[`, unary ops, sizeof/stackalloc/switch/throw), and after-`?` tokens that fall out of the switch. - @await escaped identifier (sync and async contexts). - field / value contextual-keyword operands inside property accessors. - Precedence / disambiguation: ternary interactions, member-access, binary ops, unary !, nested await? await?, pattern + ternary. - Error recovery: missing operand, double question. - Regression: `await foreach` / `await using` (no `?`) still parse as their statement forms; `await? foreach` / `await? using` do NOT steal those forms, they fall through to expression-statement parsing. - Expression-bodied async method, async lambda, async local function, parenthesized operand in async. - All assertions roundtrip-check that the tree's ToFullString equals the source.
Removes references to internal parser helpers (IsAwaitExpression, IsInAsync) from test comments and region headers. The observable behavior (what the parser produces in async vs non-async context for each operand kind) is unchanged; the comments now describe that behavior directly.
6c79262 to
57d139d
Compare
This was referenced Apr 18, 2026
Draft
Draft
4f3eff5 to
09b4597
Compare
The lookahead is deliberately limited to `await <tok>`, not `await ? <tok>`. In non-async context `await ? expr1 : expr2` is a legal ternary on the identifier `await`, and one token of lookahead can't tell the two forms apart (the disambiguator is the matching `:`, arbitrarily far away). Pessimistically leaving `await ?` to parse as a ternary is the only correct choice without committing to much deeper lookahead.
09b4597 to
57e4f9d
Compare
added 12 commits
April 24, 2026 12:31
A user writing `await?` who naturally extends it to `await? foreach` or
`await? using` got a cascade of several expression-parsing errors: the
`await?` would commit to an expression statement with a missing operand,
a semicolon-expected error would fire on the following keyword, and then
the remaining tokens would parse as a separate (non-await) foreach/using
statement.
Detect `await ? foreach` / `await ? using` at the same statement-start
lookahead that already drives `await foreach` and `await using`. Eat the
stray `?` with a single ERR_UnexpectedToken diagnostic, attach it as
trailing skipped syntax on the `await` keyword, and parse the rest as a
normal await-foreach / await-using statement. Covers the parenthesized
using form (`await? using (d) { }`) and the declaration form
(`await? using var d = x;`) via ParseLocalDeclarationStatement.
Updates the two existing AwaitQuestion_ForEach / AwaitQuestion_Using
parsing tests to pin the new single-diagnostic shape, and adds a third
covering `await? using var`.
Extends BindAwait to handle the `await? e` form. The operand-nullability rule rejects non-nullable value-type operands (concrete structs, T:struct, T:unmanaged) at the ? token, mirroring BindConditionalAccessReceiver. The awaitable pattern is resolved against the underlying type U (V when the operand is Nullable<V>, otherwise the operand type). The result type is lifted per the same rule as `?.`: non-nullable value-type R becomes Nullable<R>; already-nullable, reference, or dynamic R stays unchanged; unconstrained-T-at-result-position produces ERR_CannotBeMadeNullable when the result is used. The lifting logic moves into a shared helper that both BindConditionalAccessExpression and BindAwait call. BoundAwaitExpression gets an IsNullConditional bool field. The nullable flow walker uses node.Type for the lifted result (MaybeDefault state) and strips Nullable<> off the placeholder replacement so the AwaitableInfo placeholder's type invariant holds when the operand is Nullable<V>. Phase 2 produces the correct bound tree and semantic model; emit and state-machine lowering for the new form come in phase 3.
Covers: - Feature availability (preview vs C#14 rejection). - Operand-nullability errors: concrete ValueTask/ValueTask<T> and user structs, T:struct, T:unmanaged. - Operand-nullability successes and result-type rule across the cross product: Task<int>, Task<string>, Nullable<ValueTask>, Nullable<ValueTask<int>>, Task<int?>, Task<T> for T:class and T:struct, unconstrained T result types (both value and statement position), dynamic. - Existing await-context restrictions: non-async parse-as-ternary, lock, unsafe, missing GetAwaiter. - Interactions: extension GetAwaiter picked up on the underlying type, bare ConfigureAwait rejected, t?.ConfigureAwait(false) accepted. Each success case declares `var v = await? <operand>;` and asserts the inferred type of `v` via the semantic model, the user-visible observation of the result-type rule.
Removes references to internal helpers (ComputeConditionalAccessResultType, ResultIsUsed, IsRestrictedType, etc.) and a verbatim quote of a source comment from the binder. The observable assertions — diagnostics, inferred result types, NRT annotations — are unchanged; comments now describe them in spec-level terms.
The nullable walker was reporting `WRN_NullReferenceReceiver` when `await?` was applied to an operand the flow analysis considered possibly null — e.g. `await? (Task<int>?)t`, a nested `await? await? t`, or any call result that evaluates to a `MaybeNull` reference. But `await?` is specifically designed to accept a null receiver; the GetAwaiter call only ever runs on the non-null branch, so the warning was always wrong. Force the awaitable-info placeholder's flow state to NotNull whenever IsNullConditional, for both reference-type operands and `Nullable<V>` operands (the existing Nullable<V> branch already did this, just not for refs). Five binding tests that were pinning the spurious warning as "expected" are updated to assert no warning on the NRT-on side. The NRT-off CS8632 warnings (which flag the `?` annotation on the parameter type, not the await) stay.
The previous commit forced the awaitable placeholder's flow state to NotNull so the GetAwaiter receiver check doesn't warn. This test confirms the suppression is scoped correctly: passing a null literal to a non-nullable reference-type parameter of the operand expression still produces CS8625. If we ever over-broaden the suppression, this test will fail loudly.
Pin that await? produces its lifted result type during generic method type inference and during overload resolution. Exercises int, string, ref-type, and mixed int/int? overload shapes. Also pins that an -only overload fails to convert from int? with ERR_BadArgType.
Pin pattern-matching behavior on the lifted result (constant/type/null/ not-null/property patterns, switch expression), plus tuple deconstruction. Deconstructing the lifted Nullable<ValueTuple> fails with the standard type-inference-failed + missing-Deconstruct cascade; the workaround via GetValueOrDefault() compiles cleanly.
Pin null-forgiving behavior (`(await? t)!.Length` suppresses CS8602; `(await? t)!.Value` works on lifted int?) and `out var` definite- assignment through an await? operand. When the operand receiver is itself null-conditional (`h?.M(out var x)`) and can short-circuit, out var is NOT definitely assigned — CS0165 fires.
Pin rejection of await? in non-async contexts where `await` is likewise forbidden: default parameter value, field initializer, constructor body, and finalizer. Each one produces the ternary- disambiguation cascade we document for the standalone outside-async case.
Pin that `async` lambdas using await? cannot be converted to an expression tree, same as plain await.
Pin that await? surfaces the same binding errors as plain await on malformed awaitable patterns: missing GetResult, awaiter not implementing INotifyCompletion, GetResult with arguments, and static GetAwaiter (which is outside the pattern).
added 6 commits
April 24, 2026 12:32
Many of the earlier binding tests verified types and diagnostics but not runtime behavior. Add execution-level counterparts for every positive (no-diagnostic) binding scenario where runtime semantics are the real thing being verified: - Overload resolution (int? overload actually runs; string overload handles null) - Generic method type inference (lifted T actually flows to callee) - Pattern matching: constant / type / null / not-null / property / switch-expression arms on await? results - Null-forgiving on reference + lifted value results (member access / .Value at runtime) - out var captured through await? (x visible after the statement) - Tuple deconstruction of lifted Nullable<ValueTuple> via GetValueOrDefault
Covers the public SemanticModel surface (GetTypeInfo, GetAwaitExpressionInfo, GetSymbolInfo, GetOperation, GetDeclaredSymbol, GetConversion, AnalyzeDataFlow, AnalyzeControlFlow, GetEnclosingSymbol, LookupSymbols, GetSpeculativeTypeInfo) and pins the IOperation tree shape for `await? e` (IAwaitOperation.Type lifts to Nullable<R>, tree is flat, error operand maps to IInvalidOperation, CFG has no short-circuit branch).
Addresses adversarial audit findings: adds coverage for Task<int?> (already- nullable result), unconstrained T in statement vs value position, ConfigureAwait misuse, generic Task<T:struct>, Nullable<ValueTask> void result, and `await?` as a call argument. Tightens GetAwaitExpressionInfo assertions to cover GetResult and IsCompleted. Adds AnalyzeDataFlow tests for `await?` as argument and inside a try/catch.
Removes references to internal compiler types and phase numbering (BoundAwaitExpression, BoundBadExpression, SpillSequenceSpiller, ControlFlowGraphBuilder, phase 3/4) from test comments. Observable assertions are unchanged; the comments now describe the public API shape each test pins and the spec-level invariant behind it.
…Annotated The test's own comment claimed the flow state was preserved across the NRT context, but the assertion only covered Type and Annotation — a regression to a different FlowState would have been silently accepted. Add the missing assertion (actual value is None in NRT-disabled mode, which matches "nullable flow analysis doesn't run here").
- DebugEmit smoke tests (straight-line and try/finally) confirming PDB + sequence-point emission doesn't choke on AwaitExpressionSyntax.QuestionToken. - NRT flow tests pinning that (a) narrowing established before await? (e.g. `if (a is null) return;`) survives the await? expression, and (b) `(await? t) ?? default` narrows to non-null in the coalesce result.
57e4f9d to
fb9ac5d
Compare
The previous consolidation in TryParseStatementStartingWithIdentifier broke plain `await expr;` in async methods: the outer `if (await)` block always returned, making the `else if (IsPossibleAwaitExpressionStatement)` branch below unreachable. `await foo();` fell through to ParseStatementCoreRest and was (mis)parsed as a local declaration with `await` as the type. Let the `if (await)` block fall through when the token isn't the foreach/using statement form, so the chain below (including IsPossibleAwaitExpressionStatement) runs. Route the non-parenthesized `await [?] using Type ...` declaration form directly to ParseLocalDeclarationStatement from the dispatcher, bypassing the non-async await-retry in ParseStatementCoreRest that would otherwise trip on the ERR_UnexpectedToken we attach to the stray `?` and produce a cascading diagnostic. Teach IsPossibleAwaitExpressionStatement to reject `await [?] using` / `await [?] foreach` so it doesn't hijack those statement forms as plain expression statements once the chain is reachable again. Adds three non-async-method parsing tests covering `await? foreach`, `await? using (...)`, and `await? using var` recovery, pinning the same single-diagnostic shape as the async variants.
Callers of ParseForEachStatement / ParseUsingStatement had to peek for `await` (and recover a stray `?`) externally and pass the eaten token in via an `awaitTokenOpt` parameter. Move that logic into each statement parser: on entry they check whether CurrentToken is the `await` contextual keyword and eat it (plus any stray `?` as skipped syntax) themselves. Rename the helper to TryEatAwaitKeywordWithOptionalSkippedQuestion so it returns null when not at `await`, letting callers assign its result unconditionally. This removes the `awaitTokenOpt` parameter from both statement parsers and collapses the three `ParseForEachStatement(..., awaitTokenOpt: null)` callers plus the `ParseUsingStatement(..., awaitTokenOpt: null)` caller to single-argument calls. The two await-aware dispatcher callers no longer pre-eat the `await` token; they just delegate.
The previous consolidation in TryParseStatementStartingWithIdentifier broke plain `await expr;` in async methods: the outer `if (await)` block always returned, making the `else if (IsPossibleAwaitExpressionStatement)` branch below unreachable. `await foo();` fell through to ParseStatementCoreRest and was (mis)parsed as a local declaration with `await` as the type. Let the `if (await)` block fall through when the token isn't the foreach/using statement form, so the chain below (including IsPossibleAwaitExpressionStatement) runs. Route the non-parenthesized `await [?] using Type ...` declaration form directly to ParseLocalDeclarationStatement from the dispatcher, bypassing the non-async await-retry in ParseStatementCoreRest that would otherwise trip on the ERR_UnexpectedToken we attach to the stray `?` and produce a cascading diagnostic. Teach IsPossibleAwaitExpressionStatement to reject `await [?] using` / `await [?] foreach` so it doesn't hijack those statement forms as plain expression statements once the chain is reachable again. Adds three non-async-method parsing tests covering `await? foreach`, `await? using (...)`, and `await? using var` recovery, pinning the same single-diagnostic shape as the async variants.
Callers of ParseForEachStatement / ParseUsingStatement had to peek for `await` (and recover a stray `?`) externally and pass the eaten token in via an `awaitTokenOpt` parameter. Move that logic into each statement parser: on entry they check whether CurrentToken is the `await` contextual keyword and eat it (plus any stray `?` as skipped syntax) themselves. Rename the helper to TryEatAwaitKeywordWithOptionalSkippedQuestion so it returns null when not at `await`, letting callers assign its result unconditionally. This removes the `awaitTokenOpt` parameter from both statement parsers and collapses the three `ParseForEachStatement(..., awaitTokenOpt: null)` callers plus the `ParseUsingStatement(..., awaitTokenOpt: null)` caller to single-argument calls. The two await-aware dispatcher callers no longer pre-eat the `await` token; they just delegate.
Adds ExperimentalUrl to the new QuestionToken field on AwaitExpressionSyntax, pointing at dotnet#83237. Extends the syntax generator so that a field-level ExperimentalUrl also marks the generated Update method and SyntaxFactory overloads as experimental. Those signatures all include the new field as a parameter and are therefore themselves new public API. The pre-field signatures remain non-experimental via the hand-written forwarders. Mirrors the pattern established in dotnet#83197 for labeled break/continue.
The previous pass routed the three SyntaxFactory variant writers through GetCreationExperimentalUrl, which returned a field-level ExperimentalUrl for any factory when the node had one. That over-broadened the annotation: a minimal-factory whose signature doesn't include the experimental field (because the field is optional and omitted from the shorthand) still got marked experimental. That made a shipped, signature-unchanged API appear as a newly-experimental one, tripping RS0016 / RS0017 (the generator emits [Experimental] on a method that is still listed in Shipped.txt without the attribute). Split the factory-side logic out into GetFactorySignatureExperimentalUrl, which takes the specific fields that appear as factory parameters, and propagates field-level ExperimentalUrl only if one of those fields is actually in the signature. Node-level and kind-level marks still apply regardless. Leaves GetFieldExperimentalUrl (used by Update, which always takes every field) alone.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tests for the public SemanticModel surface on
await? e(GetTypeInfo,GetAwaitExpressionInfo,GetSymbolInfo,GetOperation,GetDeclaredSymbol,GetConversion,AnalyzeDataFlow,AnalyzeControlFlow,GetEnclosingSymbol,LookupSymbols,GetSpeculativeTypeInfo) plusIAwaitOperationtree-shape and CFG tests inIOperationTests_IAwaitExpression.cs. Test-only PR — no compiler changes.Part of the
await?/ null-conditional-await language feature implementation.Stacked: