Skip to content

Commit

Permalink
C# v7.x: Local functions (#104)
Browse files Browse the repository at this point in the history
* Update statements.md

* Update unsafe-code.md

* Add semantics and example for local functions v7.0 feature

* removed local function unsafe grammar extension

With the consolidation of the unsafe grammar in PR #233, this text is no longer needed.

* include support for local functions

* add support for local functions

* fix build issues

* Apply suggestions from code review

Co-authored-by: KalleOlaviNiemitalo <kon@iki.fi>

* clarify definite assignment, add examples

Addresses comments in committee meeting:

- dotnet/csharpstandard#104 (comment)
- dotnet/csharpstandard#104 (comment)

* respond to feedback.

* grammar updates

* Incorporate Andy's text

In this commit, create the new clause for the definite assignment rules for local functions. Move the existing example to that clause. Add text from Andy's comments and notes.

* finish local functions and definite assignment.

Incorporate the rules for definite assignment for captured variables in local functions. This is both for local function calls and for delegate conversion.

* typo

* Apply suggestions from code review

Co-authored-by: Nigel-Ecma <perryresearch@zoot.net.nz>

* Apply suggestions from code review

* Update standard/statements.md

Co-authored-by: Jon Skeet <jonskeet@google.com>

* Update standard/statements.md

* Update standard/statements.md

Co-authored-by: Neal Gafter <neal@gafter.com>

* Update standard/statements.md

* Update standard/statements.md

* edits based on September committee meeting

* fix link text

* fix section renumbering issue

* Final edits

We resolved these two discussions during the committee meeting. Edits reflect those decisions.

* updates from code review.

* one more bit of feedback.

Co-authored-by: Bill Wagner <wiwagn@microsoft.com>
Co-authored-by: KalleOlaviNiemitalo <kon@iki.fi>
Co-authored-by: Nigel-Ecma <perryresearch@zoot.net.nz>
Co-authored-by: Jon Skeet <jonskeet@google.com>
Co-authored-by: Neal Gafter <neal@gafter.com>
  • Loading branch information
6 people committed Oct 11, 2022
1 parent 76cdfa0 commit 7fce2c2
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 8 deletions.
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.
- 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.
```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')
> {
> 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++)
> {
> 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 variable is read by the body of the local function but is not definitely assigned before each call to the function. The compiler shall determine which variables are definitely assigned on returndefinite-assignment-rules-for-local-function).
A local function may be called from a lexical point prior to its declaration. 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 function7.7).
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.
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

0 comments on commit 7fce2c2

Please sign in to comment.