Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C# v7.x: Local functions #104

Merged
merged 27 commits into from
Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
32f8a88
Update statements.md
RexJaeschke Dec 27, 2020
6e64ec2
Update unsafe-code.md
RexJaeschke Dec 27, 2020
917d9a5
Add semantics and example for local functions v7.0 feature
RexJaeschke Apr 19, 2021
202efa2
removed local function unsafe grammar extension
RexJaeschke Apr 19, 2021
0c6149d
include support for local functions
RexJaeschke Apr 23, 2021
f10d5bb
add support for local functions
RexJaeschke Apr 23, 2021
5daeb7c
fix build issues
BillWagner Apr 2, 2022
8b4c9f2
Apply suggestions from code review
BillWagner May 12, 2022
a54993b
clarify definite assignment, add examples
BillWagner May 12, 2022
5b75036
respond to feedback.
BillWagner Jun 7, 2022
3173218
grammar updates
BillWagner Jun 7, 2022
9cab99c
Incorporate Andy's text
BillWagner Jul 12, 2022
792668f
finish local functions and definite assignment.
BillWagner Jul 12, 2022
e36eb5a
typo
BillWagner Jul 12, 2022
070b067
Apply suggestions from code review
BillWagner Sep 7, 2022
99d6a49
Apply suggestions from code review
BillWagner Sep 7, 2022
600c1e3
Update standard/statements.md
BillWagner Sep 7, 2022
ffc7c80
Update standard/statements.md
jskeet Sep 7, 2022
9792da1
Update standard/statements.md
jskeet Sep 7, 2022
0d3135a
Update standard/statements.md
jskeet Sep 7, 2022
dbb1ee0
Update standard/statements.md
jskeet Sep 7, 2022
9cf2cb0
edits based on September committee meeting
BillWagner Sep 27, 2022
3a4fc3d
fix link text
BillWagner Sep 27, 2022
d3574e1
fix section renumbering issue
BillWagner Oct 2, 2022
ffac293
Final edits
BillWagner Oct 5, 2022
5e65122
updates from code review.
BillWagner Oct 11, 2022
a5d8436
one more bit of feedback.
BillWagner Oct 11, 2022
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
5 changes: 3 additions & 2 deletions standard/basic-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ There are several different types of declaration spaces, as described in the fol
- Each non-partial class, struct, or interface declaration creates a new declaration space. Each partial class, struct, or interface declaration contributes to a declaration space shared by all matching parts in the same program ([§15.2.3](structs.md#1523-partial-modifier)).Names are introduced into this declaration space through *class_member_declaration*s, *struct_member_declaration*s, *interface_member_declaration*s, or *type_parameter*s. Except for overloaded instance constructor declarations and static constructor declarations, a class or struct cannot contain a member declaration with the same name as the class or struct. A class, struct, or interface permits the declaration of overloaded methods and indexers. Furthermore, a class or struct permits the declaration of overloaded instance constructors and operators. For example, a class, struct, or interface may contain multiple method declarations with the same name, provided these method declarations differ in their signature ([§7.6](basic-concepts.md#76-signatures-and-overloading)). Note that base classes do not contribute to the declaration space of a class, and base interfaces do not contribute to the declaration space of an interface. Thus, a derived class or interface is allowed to declare a member with the same name as an inherited member. Such a member is said to ***hide*** the inherited member.
- Each delegate declaration creates a new declaration space. Names are introduced into this declaration space through formal parameters (*fixed_parameter*s and *parameter_array*s) and *type_parameter*s.
- Each enumeration declaration creates a new declaration space. Names are introduced into this declaration space through *enum_member_declarations*.
- Each method declaration, property declaration, property accessor declaration, indexer declaration, indexer accessor declaration, operator declaration, instance constructor declaration and anonymous function creates a new declaration space called a ***local variable declaration space***. Names are introduced into this declaration space through formal parameters (*fixed_parameter*s and *parameter_array*s) and *type_parameter*s. The set accessor for a property or an indexer introduces the name `value` as a formal parameter. The body of the function member or anonymous function, if any, is considered to be nested within the local variable declaration space. It is an error for a local variable declaration space and a nested local variable declaration space to contain elements with the same name. Thus, within a nested declaration space it is not possible to declare a local variable or constant with the same name as a local variable or constant in an enclosing declaration space. It is possible for two declaration spaces to contain elements with the same name as long as neither declaration space contains the other.
- Each *block* or *switch_block*, as well as a `for`, `foreach`, and `using` statement, creates a local variable declaration space for local variables and local constants. Names are introduced into this declaration space through *local_variable_declaration*s and *local_constant_declaration*s. Note that blocks that occur as or within the body of a function member or anonymous function are nested within the local variable declaration space declared by those functions for their parameters. Thus, it is an error to have, for example, a method with a local variable and a parameter of the same name.
- Each method declaration, property declaration, property accessor declaration, indexer declaration, indexer accessor declaration, operator declaration, instance constructor declaration, anonymous function, and local function creates a new declaration space called a ***local variable declaration space***. Names are introduced into this declaration space through formal parameters (*fixed_parameter*s and *parameter_array*s) and *type_parameter*s. The set accessor for a property or an indexer introduces the name `value` as a formal parameter. The body of the function member, anonymous function, or local function, if any, is considered to be nested within the local variable declaration space. It is an error for a local variable declaration space and a nested local variable declaration space to contain elements with the same name. Thus, within a nested declaration space it is not possible to declare a local variable or constant with the same name as a local variable or constant in an enclosing declaration space. It is possible for two declaration spaces to contain elements with the same name as long as neither declaration space contains the other.
jskeet marked this conversation as resolved.
Show resolved Hide resolved
- Each *block* or *switch_block*, as well as a `for`, `foreach`, and `using` statement, creates a local variable declaration space for local variables and local constants. Names are introduced into this declaration space through *local_variable_declaration*s and *local_constant_declaration*s. Note that blocks that occur as or within the body of a function member, anonymous function, or local function are nested within the local variable declaration space declared by those functions for their parameters. Thus, it is an error to have, for example, a method with a local variable and a parameter of the same name.

- Each *block* or *switch_block* creates a separate declaration space for labels. Names are introduced into this declaration space through *labeled_statement*s, and the names are referenced through *goto_statement*s. The ***label declaration space*** of a block includes any nested blocks. Thus, within a nested block it is not possible to declare a label with the same name as a label in an enclosing block.

The textual order in which names are declared is generally of no significance. In particular, textual order is not significant for the declaration and use of namespaces, constants, methods, properties, events, indexers, operators, instance constructors, finalizers, static constructors, and types. Declaration order is significant in the following ways:
Expand Down
99 changes: 98 additions & 1 deletion standard/statements.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,15 +270,18 @@ In addition to the reachability provided by normal flow of control, a labeled st

### 12.6.1 General

A *declaration_statement* declares a local variable or constant. Declaration statements are permitted in blocks, but are not permitted as embedded statements.
A *declaration_statement* declares a local variable, local constant, or local function. Declaration statements are permitted in blocks, but are not permitted as embedded statements.
jskeet marked this conversation as resolved.
Show resolved Hide resolved

```ANTLR
declaration_statement
: local_variable_declaration ';'
| local_constant_declaration ';'
| local_function_declaration
;
```

A local variable is declared using a *local_variable_declaration* ([§12.6.2](statements.md#1262-local-variable-declarations)). A local constant is declared using a *local_constant_declaration* ([§12.6.3](statements.md#1263-local-constant-declarations)). A local function is declared using a *local_function_declaration* (§local-function-declarations-new-clause).

### 12.6.2 Local variable declarations

A *local_variable_declaration* declares one or more local variables.
Expand Down Expand Up @@ -414,6 +417,100 @@ The scope of a local constant is the block in which the declaration occurs. It i

A local constant declaration that declares multiple constants is equivalent to multiple declarations of single constants with the same type.

### §local-function-declarations-new-clause Local function declarations

A *local_function_declaration* declares a local function.

```ANTLR
local_function_declaration
: local_function_header local_function_body
;

local_function_header
: local_function_modifier* return_type identifier type_parameter_list?
( formal_parameter_list? ) type_parameter_constraints_clause*
;
local_function_modifier
: 'async'
| 'unsafe'
;

local_function_body
: block
| '=>' null_conditional_invocation_expression ';'
| '=>' expression ';'
;
```

Grammar note: When recognising a *local_function_body* if both the *null_conditional_invocation_expression* and *expression* alternatives are applicable then the former shall be chosen. (§14.6.1)

> *Example*: There are two common use cases for local functions: iterator methods and async methods. In iterator methods, any exceptions are observed only when calling code that enumerates the returned sequence. In async methods, any exceptions are only observed when the returned Task is awaited. The following example demonstrates separating parameter validation from the iterator implementation using a local function:
>
> ```csharp
> public static IEnumerable<char> AlphabetSubset(char start, char end)
> {
> if (start < 'a' || start > 'z')
BillWagner marked this conversation as resolved.
Show resolved Hide resolved
> {
> throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
> }
> if (end < 'a' || end > 'z')
> {
> throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");
> }
> if (end <= start)
> {
> throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");
> }
> return AlphabetSubsetImplementation();
>
> IEnumerable<char> AlphabetSubsetImplementation()
> {
> for (var c = start; c < end; c++)
jskeet marked this conversation as resolved.
Show resolved Hide resolved
> {
> yield return c;
> }
> }
> }
> ```
>
> *end example*

Unless specified otherwise below, the semantics of all grammar elements is the same as for *method_declaration* ([§14.6.1](classes.md#1461-general)), read in the context of a local function instead of a method.

The *identifier* of a *local_function_declaration* must be unique in its declared block scope. One consequence of this is that overloaded *local_function_declaration*s are not allowed.

A *local_function_declaration* may include one `async` ([§14.15](classes.md#1415-async-functions)) modifier and one `unsafe` ([§22.1](unsafe-code.md#221-general)) modifier. If the declaration includes the `async` modifier then the return type shall be `void` or a task type ([§14.15.1](classes.md#14151-general)). The `unsafe` modifier uses the containing lexical scope. The `async` modifier does not use the containing lexical scope. It is a compile-time error for *type_parameter_list* or *formal_parameter_list* to contain *attributes*.

A local function is declared at block scope, and that function may capture variables from the enclosing scope. It is a compile time error if a captured variables is read by the body of the local function if it is not definitely assigned before each call to the function. The compiler shall determine which variables are definitely assigned on return (§definite-assignment-rules-for-local-function).
BillWagner marked this conversation as resolved.
Show resolved Hide resolved
BillWagner marked this conversation as resolved.
Show resolved Hide resolved

A local function may be called from a lexical point prior to its definition. However, it is a compile-time error for the function to be declared lexically prior to the declaration of a variable used in the local function (§7.7).
BillWagner marked this conversation as resolved.
Show resolved Hide resolved

It is a compile-time error for a local function to declare a parameter or local variable with the same name as one declared in the enclosing scope.
BillWagner marked this conversation as resolved.
Show resolved Hide resolved

Local function bodies are always reachable. The endpoint of a local function declaration is reachable if the beginning point of the local function declaration is reachable.

> *Example*: In the following example, the body of `L` is reachable even though the beginning point of `L` is not reachable. Because the beginning point of `L` isn't reachable, the statement following the endpoint of `L` is not reachable:
>
> ```csharp
> class C
> {
> int M()
> {
> L();
> return 1; // Beginning of L is not reachable
> int L()
> {
> return 2; // The body of L is reachable
> }
> return 3; // Not reachable, because beginning point of L is not reachable
> }
> }
> ```
>
> In other words, the location of a local function declaration doesn't affect the reachability of any statements in the containing function. *end example*

If the argument to a local function is dynamic, the function to be called must be resolved at compile time, not runtime.

## 12.7 Expression statements

An *expression_statement* evaluates a given expression. The value computed by the expression, if any, is discarded.
Expand Down
6 changes: 3 additions & 3 deletions standard/unsafe-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ An implementation that does not support unsafe code is required to diagnose any

## 22.2 Unsafe contexts

The unsafe features of C# are available only in unsafe contexts. An unsafe context is introduced by including an `unsafe` modifier in the declaration of a type or member, or by employing an *unsafe_statement*:
The unsafe features of C# are available only in unsafe contexts. An unsafe context is introduced by including an `unsafe` modifier in the declaration of a type, member, or local function, or by employing an *unsafe_statement*:

- A declaration of a class, struct, interface, or delegate may include an `unsafe` modifier, in which case, the entire textual extent of that type declaration (including the body of the class, struct, or interface) is considered an unsafe context.
> *Note*: If the *type_declaration* is partial, only that part is an unsafe context. *end note*
- A declaration of a field, method, property, event, indexer, operator, instance constructor, finalizer, or static constructor may include an `unsafe` modifier, in which case, the entire textual extent of that member declaration is considered an unsafe context.
- An *unsafe_statement* enables the use of an unsafe context within a *block*. The entire textual extent of the associated *block* is considered an unsafe context.
- A declaration of a field, method, property, event, indexer, operator, instance constructor, finalizer, static constructor, or local function may include an `unsafe` modifier, in which case, the entire textual extent of that member declaration is considered an unsafe context.
- An *unsafe_statement* enables the use of an unsafe context within a *block*. The entire textual extent of the associated *block* is considered an unsafe context. A local function declared within an unsafe context is itself unsafe.

The associated grammar extensions are shown below and in subsequent subclauses.

Expand Down
61 changes: 59 additions & 2 deletions standard/variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ For the purpose of definite-assignment checking, a value parameter is considered

A parameter declared with a `ref` modifier is a ***reference parameter***.

A reference parameter does not create a new storage location. Instead, a reference parameter represents the same storage location as the variable given as the argument in the function member or anonymous function invocation. Thus, the value of a reference parameter is always the same as the underlying variable.
A reference parameter does not create a new storage location. Instead, a reference parameter represents the same storage location as the variable given as the argument in the function member, anonymous function, or local function invocation. Thus, the value of a reference parameter is always the same as the underlying variable.

The following definite-assignment rules apply to reference parameters.

Expand All @@ -106,7 +106,7 @@ The following definite-assignment rules apply to output parameters.
- A variable need not be definitely assigned before it can be passed as an output parameter in a function member or delegate invocation.
- Following the normal completion of a function member or delegate invocation, each variable that was passed as an output parameter is considered assigned in that execution path.
- Within a function member or anonymous function, an output parameter is considered initially unassigned.
- Every output parameter of a function member or anonymous function shall be definitely assigned ([§9.4](variables.md#94-definite-assignment)) before the function member or anonymous function returns normally.
- Every output parameter of a function member, anonymous function, or local function shall be definitely assigned ([§9.4](variables.md#94-definite-assignment)) before the function member, anonymous function, or local function returns normally.

Within an instance constructor of a struct type, the `this` keyword behaves exactly as an output or reference parameter of the struct type, depending on whether the constructor declaration includes a constructor initializer ([§11.7.12](expressions.md#11712-this-access)).

Expand Down Expand Up @@ -802,6 +802,63 @@ For a *lambda_expression* or *anonymous_method_expression* *expr* with a body (e
>
> *end example*

#### §definite-assignment-rules-for-local-function Rules for variables in local functions

Local functions are analyzed in the context of their parent method. There are two control flow paths that matter for local functions: function calls and delegate conversions.

Definite assignment for the body of each local function is defined separately for each call site. At each invocation, variables captured by the local function are considered definitely assigned if they were definitely assigned at the point of call. A control flow path also exists to the local function body at this point and is considered reachable. After a call to the local function, captured variables that were definitely assigned at every control point leaving the function (`return` statements, `yield` statements, `await` expressions) are considered definitely assigned after the call location.

Delegate conversions have a control flow path to the local function body. Captured variables are definitely assigned for the body if they are definitely assigned before the conversion. Variables assigned by the local function are not considered assigned after the conversion.

> *Note:* the above implies that bodies are re-analyzed for definite assignment at every local function invocation or delegate conversion. Compilers are not required to re-analyze the body of a local function at each invocation or delegate conversion. The implementation must produce results equivalent to that description.
<!-- markdownlint-disable MD028 -->

<!-- markdownlint-enable MD028 -->
> *Example*: The following example demonstrates definite assignment for captured variables in local functions. If a local function reads a captured variable before writing it, the captured variable must be definitely assigned before calling the local function. The local function `F1` reads `s` without assigning it. It is an error if `F1` is called before `s` is definitely assigned. `F2` assigns `i` before reading it. It may be called before `i` is definitely assigned. Furthermore, `F3` may be called after `F2` because `s2` is definitely assigned in `F2`.
>
> ```csharp
> void M()
> {
> string s;
> int i;
> string s2;
>
> // Error: Use of unassigned local variable s:
> F1();
> // OK, F2 assigns i before reading it.
> F2();
>
> // OK, i is definitely assigned in the body of F2:
> s = i.ToString();
>
> // OK. s is now definitely assigned.
> F1();
>
> // OK, F3 reads s2, which is definitely assigned in F2.
> F3();
>
> void F1()
> {
> Console.WriteLine(s);
> }
>
> void F2()
> {
> i = 5;
> // OK. i is definitely assigned.
> Console.WriteLine(i);
> s2 = i.ToString();
> }
>
> void F3()
> {
> Console.WriteLine(s2);
> }
> }
> ```
>
> *end example*

## 9.5 Variable references

A *variable_reference* is an *expression* that is classified as a variable. A *variable_reference* denotes a storage location that can be accessed both to fetch the current value and to store a new value.
Expand Down