Skip to content
Open
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
3 changes: 3 additions & 0 deletions docs/release-notes/.FSharp.Core/11.0.200.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@

* Correct `FSharpValue.PreComputeTuplePropertyInfo` to report that struct tuples store elements in fields, not properties, and recommend supported tuple readers instead of reporting a valid index as out of range. ([Issue #20801](https://github.com/dotnet/fsharp/issues/20801), [PR #20815](https://github.com/dotnet/fsharp/pull/20815))
* Quotation debug text from `Expr.ToString()`, `Expr.ToString(full)`, and `%A` now prints struct tuple construction as `NewStructTuple`; reference tuples remain `NewTuple`. ([Issue #20800](https://github.com/dotnet/fsharp/issues/20800), [PR #20814](https://github.com/dotnet/fsharp/pull/20814))
* Fix struct tuples in query expressions and in lambdas converted to LINQ expression trees: selecting one no longer fails with "unreachable", and taking one apart no longer fails with a tuple index out of range. ([Issue #15522](https://github.com/dotnet/fsharp/issues/15522), [PR #20894](https://github.com/dotnet/fsharp/pull/20894))
* Fix `let` bindings reaching LINQ providers as `Expression.Block`, which no provider translates: in lambdas handed to a provider, records and anonymous records with fields written out of order, tupled lambda parameters, struct copies such as `x.Nullable.HasValue`, and a `let` that only names a member, a sum, difference or product of members, or is read once now produce plain expressions. ([Issue #11131](https://github.com/dotnet/fsharp/issues/11131), [Issue #15648](https://github.com/dotnet/fsharp/issues/15648), [Issue #11332](https://github.com/dotnet/fsharp/issues/11332), [PR #20895](https://github.com/dotnet/fsharp/pull/20895))
* Fix a lambda nested in another lambda that goes to a LINQ provider, such as `db.Orders.Any(fun o -> o.CustomerId = c.Id)` inside a query `where`: when it reads a parameter of the outer lambda, it becomes a quoted lambda, as in C#, instead of a run-time call to `QuotationToLambdaExpression` that no provider can translate. ([Issue #2758](https://github.com/dotnet/fsharp/issues/2758), [PR #20896](https://github.com/dotnet/fsharp/pull/20896))

### Changed
29 changes: 29 additions & 0 deletions docs/release-notes/PENDING_BREAKING_CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,35 @@ a.Equals(b) // Was: false, Now: true
// New: if (node.NodeType == ExpressionType.Block)
```

Inside a lambda handed to a LINQ provider (a query operator over `IQueryable`, or an F# lambda passed as `Expression<Func<..>>`) a let no longer makes a `Block` where leaving it out computes the same:

- its value only reads: a constant, the lambda's parameter or a member of it (`let age = p.Age`), what a member or method of a string, number or date makes of those (`let upper = p.Name.ToUpper()`), or an addition, subtraction or multiplication of those (`let next = p.Age + 1`);
- or the variable is read once, as the first thing the rest of the lambda evaluates (`let orders = p.Orders.Count() in orders > 2`).

This covers the lets the compiler introduces for tupled lambda parameters and struct copies. For record and anonymous record fields written out of order, the lets are left out as well:

```fsharp
db.People.Select(fun p -> {| Name = p.Name; Age = p.Age |})
// Was: p => { var Name = p.Name; new <>f__AnonymousType(p.Age, Name) }
// Now: p => new <>f__AnonymousType(p.Age, p.Name)
```

**Impact:** When such a lambda is compiled and run, the fields of a record written out of order are evaluated in declaration order instead of the order they are written in, which shows when they have side effects. A let that only reads, such as `let age = p.Age`, `let upper = p.Name.ToUpper()` or `let next = p.Age + 1`, is computed where the variable is used: once for each use, at the moment of that use, and not at all on a path that does not use it.

Every other let keeps its `Block` and is evaluated once, where it is written: a value that is computed by a call and read more than once, by a nested lambda or after something else has run, a struct of your own or a boxed value, a `mutable` variable, and every let of a lambda that assigns, sets a property or field, or runs a statement. `EvaluateQuotation` and `QuotationToExpression` are not affected.

### Nested lambdas that read a parameter of the outer lambda are quoted

Inside a lambda handed to a LINQ provider, a lambda passed as `Expression<Func<..>>` that reads a parameter of the outer lambda is now a `Quote` of the converted lambda, as in C#, instead of a call that converts its quotation when the outer tree runs.

```fsharp
db.Customers.Where(fun c -> db.Orders.Any(fun o -> o.CustomerId = c.Id))
// Was: c => Orders.Any(QuotationToLambdaExpression(SubstHelper(<quotation>, [c], [c])))
// Now: c => Orders.Any(o => o.CustomerId == c.Id)
```

**Impact:** When such a tree is compiled and run, the nested lambda reads the outer parameter through a closure where it used to get its value as a constant. A let of the outer lambda that names a member of its parameter is read by the nested lambda when that lambda runs. A nested lambda that reads no parameter of the outer lambda is converted as before, and so is one that cannot be converted.

### Array access uses ArrayIndex

```fsharp
Expand Down
Loading
Loading