diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md new file mode 100644 index 00000000000..3ecca5bfacb --- /dev/null +++ b/BREAKING_CHANGES.md @@ -0,0 +1,67 @@ +# Breaking Changes in Query Expression Fixes + +## AnonymousObject Structural Equality 🔴 + +`AnonymousObject` types now implement `Equals`/`GetHashCode` with structural equality. + +**Impact:** Code using these as Dictionary keys with identity semantics will see different behavior. + +```fsharp +let a = AnonymousObject(1, 2) +let b = AnonymousObject(1, 2) +a.Equals(b) // Was: false, Now: true +``` + +**Why:** Fixes #7885, #47 - join/groupBy with tuple keys now work correctly. + +--- + +## Expression Tree Structure Changes 🟡 + +### Let-bindings use Block instead of Lambda.Invoke + +```fsharp +<@ let x = 1 in x + 1 @> +// Was: Lambda(x => x+1).Invoke(1) +// Now: Block({ x = 1; x + 1 }) +``` + +**Migration:** If you detected let-bindings via `Lambda.Invoke`: +```csharp +// Old: if (node.Method.Name == "Invoke" && node.Object is LambdaExpression) +// New: if (node.NodeType == ExpressionType.Block) +``` + +### Array access uses ArrayIndex + +```fsharp +<@ arr.[0] @> +// Was: Call(GetArray, arr, 0) +// Now: ArrayIndex(arr, 0) +``` + +**Migration:** +```csharp +// Old: if (node.Method.Name == "GetArray") +// New: if (node.NodeType == ExpressionType.ArrayIndex) +``` + +--- + +## IQueryable Type Preservation 🟡 + +Tuple projections now preserve the provider's `IQueryable` type. + +```fsharp +query { for p in db.Products do select (p.Id, p.Name) } +// Was: EnumerableQuery<_> +// Now: Provider's IQueryable<_> +``` + +**Impact:** Code checking `is EnumerableQuery<_>` may need adjustment. + +--- + +## FCS API: Query Variables 🟢 + +Query variables now report `IsCompilerGenerated = true`. Fixes #422 (FS1182 false positives). diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md index c247da5870b..6ecb6235225 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md @@ -1,5 +1,7 @@ ### Fixed +* Fix false FS1182 (unused variable) warning for query expression variables used in where, let, join, and select clauses. ([Issue #422](https://github.com/dotnet/fsharp/issues/422)) + ### Added ### Changed diff --git a/docs/release-notes/.FSharp.Core/10.0.300.md b/docs/release-notes/.FSharp.Core/10.0.300.md index c247da5870b..efb9618522b 100644 --- a/docs/release-notes/.FSharp.Core/10.0.300.md +++ b/docs/release-notes/.FSharp.Core/10.0.300.md @@ -1,5 +1,12 @@ ### Fixed +* Fix anonymous record field ordering in LINQ expression conversion to produce consistent expression trees regardless of field declaration order. ([Issue #11131](https://github.com/dotnet/fsharp/issues/11131), [Issue #15648](https://github.com/dotnet/fsharp/issues/15648)) +* Fix array indexing in LINQ expressions to generate proper array index expressions instead of GetArray method calls, enabling LINQ providers like Azure Cosmos DB to translate array access. ([Issue #16918](https://github.com/dotnet/fsharp/issues/16918)) +* Fix tuple join conditions and groupBy operations to properly compare tuple keys using structural equality. AnonymousObject types now implement Equals and GetHashCode, enabling inline tuple joins like `join b on ((a.Id1, a.Id2) = (b.Id1, b.Id2))` to work correctly. ([Issue #7885](https://github.com/dotnet/fsharp/issues/7885), [Issue #47](https://github.com/dotnet/fsharp/issues/47)) +* Fix tuple/multi-value projections in queries to use Queryable.Select instead of Enumerable.Select when the source is IQueryable, preserving query composition and enabling async operations like ToListAsync() in Entity Framework Core. ([Issue #3782](https://github.com/dotnet/fsharp/issues/3782), [Issue #15133](https://github.com/dotnet/fsharp/issues/15133)) +* Fix EvaluateQuotation to handle Sequential expressions, void method calls (unit return), and other patterns that were previously throwing NotSupportedException. Also properly handles unit-returning expressions by using Action delegates instead of Func delegates. ([Issue #19099](https://github.com/dotnet/fsharp/issues/19099)) +* Fix query conditionals without else branch (if-then only) that were causing type mismatch errors. Now properly extracts element type from IQueryable for creating empty sequences. ([Issue #3445](https://github.com/dotnet/fsharp/issues/3445)) + ### Added ### Changed diff --git a/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs b/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs index 1ab85391132..d7ae5317c51 100644 --- a/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs @@ -123,11 +123,29 @@ let mkSimplePatForVarSpace m (patvs: Val list) = let spats = match patvs with | [] -> [] - | [ v ] -> [ mkSynSimplePatVar false v.Id ] - | vs -> vs |> List.map (fun v -> mkSynSimplePatVar false v.Id) + // Use mkSynCompGenSimplePatVar to mark these synthetic lambda parameters as compiler-generated. + // This prevents false FS1182 warnings for query variables that are logically used in query clauses. + // See Issue #422. + | [ v ] -> [ mkSynCompGenSimplePatVar v.Id ] + | vs -> vs |> List.map (fun v -> mkSynCompGenSimplePatVar v.Id) SynSimplePats.SimplePats(spats, [], m) +/// Mark all SynSimplePat.Id nodes in a SynSimplePats as compiler-generated. +/// This is used to prevent false FS1182 warnings for join/groupJoin/zip patterns +/// that are used in synthetic lambdas for key selectors. +let rec markSimplePatAsCompilerGenerated (pat: SynSimplePat) = + match pat with + | SynSimplePat.Id(ident, altNameRefCell, _isCompilerGenerated, isThisVal, isOptional, range) -> + SynSimplePat.Id(ident, altNameRefCell, true, isThisVal, isOptional, range) + | SynSimplePat.Typed(p, ty, range) -> SynSimplePat.Typed(markSimplePatAsCompilerGenerated p, ty, range) + | SynSimplePat.Attrib(p, attribs, range) -> SynSimplePat.Attrib(markSimplePatAsCompilerGenerated p, attribs, range) + +let markSimplePatsAsCompilerGenerated (pats: SynSimplePats) = + match pats with + | SynSimplePats.SimplePats(patList, commaRanges, range) -> + SynSimplePats.SimplePats(patList |> List.map markSimplePatAsCompilerGenerated, commaRanges, range) + let mkPatForVarSpace m (patvs: Val list) = match patvs with | [] -> SynPat.Const(SynConst.Unit, m) @@ -1063,10 +1081,12 @@ let rec TryTranslateComputationExpression | None -> varSpace let firstSourceSimplePats, later1 = - SimplePatsOfPat cenv.synArgNameGenerator firstSourcePat + let pats, later = SimplePatsOfPat cenv.synArgNameGenerator firstSourcePat + markSimplePatsAsCompilerGenerated pats, later let secondSourceSimplePats, later2 = - SimplePatsOfPat cenv.synArgNameGenerator secondSourcePat + let pats, later = SimplePatsOfPat cenv.synArgNameGenerator secondSourcePat + markSimplePatsAsCompilerGenerated pats, later if Option.isSome later1 then errorR (Error(FSComp.SR.tcJoinMustUseSimplePattern nm.idText, firstSourcePat.Range)) @@ -1152,7 +1172,8 @@ let rec TryTranslateComputationExpression // groupJoin | Some secondResultPat, Some relExpr when customOperationIsLikeGroupJoin ceenv nm -> let secondResultSimplePats, later3 = - SimplePatsOfPat cenv.synArgNameGenerator secondResultPat + let pats, later = SimplePatsOfPat cenv.synArgNameGenerator secondResultPat + markSimplePatsAsCompilerGenerated pats, later if Option.isSome later3 then errorR (Error(FSComp.SR.tcJoinMustUseSimplePattern nm.idText, secondResultPat.Range)) diff --git a/src/FSharp.Core/Linq.fs b/src/FSharp.Core/Linq.fs index f6bb324f4cc..3c1daca94b9 100644 --- a/src/FSharp.Core/Linq.fs +++ b/src/FSharp.Core/Linq.fs @@ -34,10 +34,21 @@ module LeafExpressionConverter = { varEnv : Map } let asExpr x = (x :> Expression) - let instanceBindingFlags = BindingFlags.Instance ||| BindingFlags.Public ||| BindingFlags.NonPublic ||| BindingFlags.DeclaredOnly + let _instanceBindingFlags = BindingFlags.Instance ||| BindingFlags.Public ||| BindingFlags.NonPublic ||| BindingFlags.DeclaredOnly let isNamedType(typ:Type) = not (typ.IsArray || typ.IsByRef || typ.IsPointer) + /// Determines if a property access on a derived union case type needs to be coerced to the declaring type. + /// This handles the case where a property is defined on a specific union case type that inherits from the union type. + let getUnionCaseCoercionType (objOpt: Expr option) (declaringType: Type) = + if objOpt.IsSome && + FSharpType.IsUnion declaringType && + not (isNull declaringType.BaseType) && + FSharpType.IsUnion declaringType.BaseType then + Some declaringType + else + None + let equivHeadTypes (ty1:Type) (ty2:Type) = isNamedType(ty1) && if ty1.IsGenericType then @@ -475,11 +486,7 @@ module LeafExpressionConverter = build arg.Type argP n | PropertyGet(objOpt, propInfo, args) -> - let coerceTo = - if objOpt.IsSome && FSharpType.IsUnion propInfo.DeclaringType && FSharpType.IsUnion propInfo.DeclaringType.BaseType then - Some propInfo.DeclaringType - else - None + let coerceTo = getUnionCaseCoercionType objOpt propInfo.DeclaringType match args with | [] -> Expression.Property(ConvObjArg env objOpt coerceTo, propInfo) |> asExpr @@ -623,7 +630,10 @@ module LeafExpressionConverter = | CheckedConvUInt16Q (_, _, [x]) | CheckedConvUInt32Q (_, _, [x]) | CheckedConvUInt64Q (_, _, [x]) | CheckedConvIntPtrQ (_, _, [x]) -> transConv inp env true x | CheckedConvUIntPtrQ (_, _, [x]) -> transConv inp env true x - | ArrayLookupQ (_, GenericArgs [|_; _; _|], [x1; x2]) -> + // Issue #16918: ArrayLookupQ pattern expects GenericArgs [|_|] (1 type param) not [|_; _; _|] (3 params) + // because GetArray only has 1 type parameter. This enables proper Expression.ArrayIndex generation + // that LINQ providers can translate, instead of falling through to method call handling. + | ArrayLookupQ (_, GenericArgs [|_|], [x1; x2]) -> Expression.ArrayIndex(ConvExprToLinqInContext env x1, ConvExprToLinqInContext env x2) |> asExpr // Throw away markers inserted to satisfy C#'s design where they pass an argument @@ -770,13 +780,16 @@ module LeafExpressionConverter = |> asExpr | Let (v, e, b) -> + // Use Expression.Block to properly scope the variable. This: + // 1. Evaluates the expression exactly once (preserving reference semantics) + // 2. Avoids Lambda.Invoke which EF Core cannot translate + // 3. Works for both mutable and immutable variables let vP = ConvVarToLinq v + let eP = ConvExprToLinqInContext env e let envinner = { varEnv = Map.add v (vP |> asExpr) env.varEnv } let bodyP = ConvExprToLinqInContext envinner b - let eP = ConvExprToLinqInContext env e - let ty = Expression.GetFuncType [| v.Type; b.Type |] - let lam = Expression.Lambda(ty, bodyP, [| vP |]) |> asExpr - Expression.Call(lam, ty.GetMethod("Invoke", instanceBindingFlags), [| eP |]) |> asExpr + // Create a block with the variable declaration, initial assignment, and body + Expression.Block([| vP |], Expression.Assign(vP, eP), bodyP) |> asExpr | Lambda(v, body) -> let vP = ConvVarToLinq v @@ -792,6 +805,41 @@ module LeafExpressionConverter = let convType = lambdaTy.MakeGenericType tyargs let convDelegate = Expression.Lambda(convType, bodyP, [| vP |]) |> asExpr Expression.Call(typeof, "ToFSharpFunc", tyargs, [| convDelegate |]) |> asExpr + + // Issue #19099: Handle Sequential (e1; e2) expressions + | Sequential(e1, e2) -> + let e1P = ConvExprToLinqInContext env e1 + let e2P = ConvExprToLinqInContext env e2 + Expression.Block(e1P, e2P) |> asExpr + + // Issue #19099: Handle VarSet (v <- value) expressions + | VarSet(v, value) -> + let vP = + try Map.find v env.varEnv + with :? KeyNotFoundException -> invalidOp ("The variable '"+ v.Name + "' was not found in the translation context'") + let valueP = ConvExprToLinqInContext env value + Expression.Assign(vP, valueP) |> asExpr + + // Issue #19099: Handle FieldSet (obj.field <- value) expressions + | FieldSet(objOpt, fieldInfo, value) -> + let objP = ConvObjArg env objOpt None + let valueP = ConvExprToLinqInContext env value + Expression.Assign(Expression.Field(objP, fieldInfo), valueP) |> asExpr + + // Issue #19099: Handle PropertySet (obj.prop <- value) expressions + | PropertySet(objOpt, propInfo, args, value) -> + let coerceTo = getUnionCaseCoercionType objOpt propInfo.DeclaringType + let valueP = ConvExprToLinqInContext env value + match args with + | [] -> + let propExpr = Expression.Property(ConvObjArg env objOpt coerceTo, propInfo) + Expression.Assign(propExpr, valueP) |> asExpr + | _ -> + // For indexed property sets, use the setter method directly + let argsP = ConvExprsToLinq env args + let allArgsP = Array.append argsP [| valueP |] + Expression.Call(ConvObjArg env objOpt coerceTo, propInfo.GetSetMethod(true), allArgsP) |> asExpr + | _ -> failConvert inp @@ -910,11 +958,23 @@ module LeafExpressionConverter = | Value (obj, _) -> obj | _ -> let ty = e.Type - let e = Expr.NewDelegate (Expression.GetFuncType([|typeof; ty |]), [new Var("unit", typeof)], e) - let linqExpr = (ConvExprToLinq e:?> LambdaExpression) - let d = linqExpr.Compile () - try - d.DynamicInvoke [| box () |] - with :? TargetInvocationException as exn -> - raise exn.InnerException + // Helper to compile and invoke a delegate, re-raising inner exceptions + let compileAndInvoke (delegateExpr: Expr) = + let linqExpr = ConvExprToLinq delegateExpr :?> LambdaExpression + let d = linqExpr.Compile() + try + d.DynamicInvoke [| box () |] + with :? TargetInvocationException as exn -> + raise exn.InnerException + + // Issue #19099: Handle unit/void return types by wrapping in an Action instead of Func + // When the expression returns unit, the LINQ expression will have type System.Void which cannot + // be a return type of Func. We use Action instead and return box(). + if ty = typeof then + let unitVar = new Var("unit", typeof) + compileAndInvoke (Expr.NewDelegate(typeof>, [unitVar], e)) |> ignore + box () + else + let unitVar = new Var("unit", typeof) + compileAndInvoke (Expr.NewDelegate(Expression.GetFuncType([|typeof; ty|]), [unitVar], e)) #endif \ No newline at end of file diff --git a/src/FSharp.Core/MutableTuple.fs b/src/FSharp.Core/MutableTuple.fs index 0d3d4d998b0..06690137b46 100644 --- a/src/FSharp.Core/MutableTuple.fs +++ b/src/FSharp.Core/MutableTuple.fs @@ -2,6 +2,11 @@ namespace Microsoft.FSharp.Linq.RuntimeHelpers +open System +open Microsoft.FSharp.Core +open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators +open System.Collections.Generic + #nowarn "49" // no warning for uppercase variable names // ---------------------------------------------------------------------------- // Mutable Tuples - used when translating queries that use F# tuples @@ -22,193 +27,213 @@ namespace Microsoft.FSharp.Linq.RuntimeHelpers /// This type shouldn't be used directly from user code. /// -type AnonymousObject<'T1> = - val private item1: 'T1 - member x.Item1 = x.item1 +[] +type AnonymousObject<'T1>(Item1: 'T1) = + member _.Item1 = Item1 + + override this.Equals(other: obj) = + match other with + | :? AnonymousObject<'T1> as o -> EqualityComparer<'T1>.Default.Equals(this.Item1, o.Item1) + | _ -> false - new(Item1) = { item1 = Item1 } + override this.GetHashCode() = + EqualityComparer<'T1>.Default.GetHashCode(this.Item1) /// This type shouldn't be used directly from user code. /// -type AnonymousObject<'T1, 'T2> = - val private item1: 'T1 - member x.Item1 = x.item1 - - val private item2: 'T2 - member x.Item2 = x.item2 - - new(Item1, Item2) = { item1 = Item1; item2 = Item2 } +[] +type AnonymousObject<'T1, 'T2>(Item1: 'T1, Item2: 'T2) = + member _.Item1 = Item1 + member _.Item2 = Item2 + + override this.Equals(other: obj) = + match other with + | :? AnonymousObject<'T1, 'T2> as o -> + EqualityComparer<'T1>.Default.Equals(this.Item1, o.Item1) + && EqualityComparer<'T2>.Default.Equals(this.Item2, o.Item2) + | _ -> false + + override this.GetHashCode() = + let h1 = EqualityComparer<'T1>.Default.GetHashCode(this.Item1) + let h2 = EqualityComparer<'T2>.Default.GetHashCode(this.Item2) + ((h1 <<< 5) + h1) ^^^ h2 /// This type shouldn't be used directly from user code. /// -type AnonymousObject<'T1, 'T2, 'T3> = - val private item1: 'T1 - member x.Item1 = x.item1 - - val private item2: 'T2 - member x.Item2 = x.item2 - - val private item3: 'T3 - member x.Item3 = x.item3 - - new(Item1, Item2, Item3) = - { - item1 = Item1 - item2 = Item2 - item3 = Item3 - } +[] +type AnonymousObject<'T1, 'T2, 'T3>(Item1: 'T1, Item2: 'T2, Item3: 'T3) = + member _.Item1 = Item1 + member _.Item2 = Item2 + member _.Item3 = Item3 + + override this.Equals(other: obj) = + match other with + | :? AnonymousObject<'T1, 'T2, 'T3> as o -> + EqualityComparer<'T1>.Default.Equals(this.Item1, o.Item1) + && EqualityComparer<'T2>.Default.Equals(this.Item2, o.Item2) + && EqualityComparer<'T3>.Default.Equals(this.Item3, o.Item3) + | _ -> false + + override this.GetHashCode() = + let mutable hash = EqualityComparer<'T1>.Default.GetHashCode(this.Item1) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T2>.Default.GetHashCode(this.Item2) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T3>.Default.GetHashCode(this.Item3) + hash /// This type shouldn't be used directly from user code. /// -type AnonymousObject<'T1, 'T2, 'T3, 'T4> = - val private item1: 'T1 - member x.Item1 = x.item1 - - val private item2: 'T2 - member x.Item2 = x.item2 - - val private item3: 'T3 - member x.Item3 = x.item3 - - val private item4: 'T4 - member x.Item4 = x.item4 - - new(Item1, Item2, Item3, Item4) = - { - item1 = Item1 - item2 = Item2 - item3 = Item3 - item4 = Item4 - } +[] +type AnonymousObject<'T1, 'T2, 'T3, 'T4>(Item1: 'T1, Item2: 'T2, Item3: 'T3, Item4: 'T4) = + member _.Item1 = Item1 + member _.Item2 = Item2 + member _.Item3 = Item3 + member _.Item4 = Item4 + + override this.Equals(other: obj) = + match other with + | :? AnonymousObject<'T1, 'T2, 'T3, 'T4> as o -> + EqualityComparer<'T1>.Default.Equals(this.Item1, o.Item1) + && EqualityComparer<'T2>.Default.Equals(this.Item2, o.Item2) + && EqualityComparer<'T3>.Default.Equals(this.Item3, o.Item3) + && EqualityComparer<'T4>.Default.Equals(this.Item4, o.Item4) + | _ -> false + + override this.GetHashCode() = + let mutable hash = EqualityComparer<'T1>.Default.GetHashCode(this.Item1) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T2>.Default.GetHashCode(this.Item2) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T3>.Default.GetHashCode(this.Item3) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T4>.Default.GetHashCode(this.Item4) + hash /// This type shouldn't be used directly from user code. /// -type AnonymousObject<'T1, 'T2, 'T3, 'T4, 'T5> = - val private item1: 'T1 - member x.Item1 = x.item1 - - val private item2: 'T2 - member x.Item2 = x.item2 - - val private item3: 'T3 - member x.Item3 = x.item3 - - val private item4: 'T4 - member x.Item4 = x.item4 - - val private item5: 'T5 - member x.Item5 = x.item5 - - new(Item1, Item2, Item3, Item4, Item5) = - { - item1 = Item1 - item2 = Item2 - item3 = Item3 - item4 = Item4 - item5 = Item5 - } +[] +type AnonymousObject<'T1, 'T2, 'T3, 'T4, 'T5>(Item1: 'T1, Item2: 'T2, Item3: 'T3, Item4: 'T4, Item5: 'T5) = + member _.Item1 = Item1 + member _.Item2 = Item2 + member _.Item3 = Item3 + member _.Item4 = Item4 + member _.Item5 = Item5 + + override this.Equals(other: obj) = + match other with + | :? AnonymousObject<'T1, 'T2, 'T3, 'T4, 'T5> as o -> + EqualityComparer<'T1>.Default.Equals(this.Item1, o.Item1) + && EqualityComparer<'T2>.Default.Equals(this.Item2, o.Item2) + && EqualityComparer<'T3>.Default.Equals(this.Item3, o.Item3) + && EqualityComparer<'T4>.Default.Equals(this.Item4, o.Item4) + && EqualityComparer<'T5>.Default.Equals(this.Item5, o.Item5) + | _ -> false + + override this.GetHashCode() = + let mutable hash = EqualityComparer<'T1>.Default.GetHashCode(this.Item1) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T2>.Default.GetHashCode(this.Item2) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T3>.Default.GetHashCode(this.Item3) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T4>.Default.GetHashCode(this.Item4) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T5>.Default.GetHashCode(this.Item5) + hash /// This type shouldn't be used directly from user code. /// -type AnonymousObject<'T1, 'T2, 'T3, 'T4, 'T5, 'T6> = - val private item1: 'T1 - member x.Item1 = x.item1 - - val private item2: 'T2 - member x.Item2 = x.item2 - - val private item3: 'T3 - member x.Item3 = x.item3 - - val private item4: 'T4 - member x.Item4 = x.item4 - - val private item5: 'T5 - member x.Item5 = x.item5 - - val private item6: 'T6 - member x.Item6 = x.item6 - - new(Item1, Item2, Item3, Item4, Item5, Item6) = - { - item1 = Item1 - item2 = Item2 - item3 = Item3 - item4 = Item4 - item5 = Item5 - item6 = Item6 - } +[] +type AnonymousObject<'T1, 'T2, 'T3, 'T4, 'T5, 'T6> + (Item1: 'T1, Item2: 'T2, Item3: 'T3, Item4: 'T4, Item5: 'T5, Item6: 'T6) = + member _.Item1 = Item1 + member _.Item2 = Item2 + member _.Item3 = Item3 + member _.Item4 = Item4 + member _.Item5 = Item5 + member _.Item6 = Item6 + + override this.Equals(other: obj) = + match other with + | :? AnonymousObject<'T1, 'T2, 'T3, 'T4, 'T5, 'T6> as o -> + EqualityComparer<'T1>.Default.Equals(this.Item1, o.Item1) + && EqualityComparer<'T2>.Default.Equals(this.Item2, o.Item2) + && EqualityComparer<'T3>.Default.Equals(this.Item3, o.Item3) + && EqualityComparer<'T4>.Default.Equals(this.Item4, o.Item4) + && EqualityComparer<'T5>.Default.Equals(this.Item5, o.Item5) + && EqualityComparer<'T6>.Default.Equals(this.Item6, o.Item6) + | _ -> false + + override this.GetHashCode() = + let mutable hash = EqualityComparer<'T1>.Default.GetHashCode(this.Item1) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T2>.Default.GetHashCode(this.Item2) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T3>.Default.GetHashCode(this.Item3) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T4>.Default.GetHashCode(this.Item4) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T5>.Default.GetHashCode(this.Item5) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T6>.Default.GetHashCode(this.Item6) + hash /// This type shouldn't be used directly from user code. /// -type AnonymousObject<'T1, 'T2, 'T3, 'T4, 'T5, 'T6, 'T7> = - val private item1: 'T1 - member x.Item1 = x.item1 - - val private item2: 'T2 - member x.Item2 = x.item2 - - val private item3: 'T3 - member x.Item3 = x.item3 - - val private item4: 'T4 - member x.Item4 = x.item4 - - val private item5: 'T5 - member x.Item5 = x.item5 - - val private item6: 'T6 - member x.Item6 = x.item6 - - val private item7: 'T7 - member x.Item7 = x.item7 - - new(Item1, Item2, Item3, Item4, Item5, Item6, Item7) = - { - item1 = Item1 - item2 = Item2 - item3 = Item3 - item4 = Item4 - item5 = Item5 - item6 = Item6 - item7 = Item7 - } +[] +type AnonymousObject<'T1, 'T2, 'T3, 'T4, 'T5, 'T6, 'T7> + (Item1: 'T1, Item2: 'T2, Item3: 'T3, Item4: 'T4, Item5: 'T5, Item6: 'T6, Item7: 'T7) = + member _.Item1 = Item1 + member _.Item2 = Item2 + member _.Item3 = Item3 + member _.Item4 = Item4 + member _.Item5 = Item5 + member _.Item6 = Item6 + member _.Item7 = Item7 + + override this.Equals(other: obj) = + match other with + | :? AnonymousObject<'T1, 'T2, 'T3, 'T4, 'T5, 'T6, 'T7> as o -> + EqualityComparer<'T1>.Default.Equals(this.Item1, o.Item1) + && EqualityComparer<'T2>.Default.Equals(this.Item2, o.Item2) + && EqualityComparer<'T3>.Default.Equals(this.Item3, o.Item3) + && EqualityComparer<'T4>.Default.Equals(this.Item4, o.Item4) + && EqualityComparer<'T5>.Default.Equals(this.Item5, o.Item5) + && EqualityComparer<'T6>.Default.Equals(this.Item6, o.Item6) + && EqualityComparer<'T7>.Default.Equals(this.Item7, o.Item7) + | _ -> false + + override this.GetHashCode() = + let mutable hash = EqualityComparer<'T1>.Default.GetHashCode(this.Item1) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T2>.Default.GetHashCode(this.Item2) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T3>.Default.GetHashCode(this.Item3) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T4>.Default.GetHashCode(this.Item4) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T5>.Default.GetHashCode(this.Item5) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T6>.Default.GetHashCode(this.Item6) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T7>.Default.GetHashCode(this.Item7) + hash /// This type shouldn't be used directly from user code. /// -type AnonymousObject<'T1, 'T2, 'T3, 'T4, 'T5, 'T6, 'T7, 'T8> = - val private item1: 'T1 - member x.Item1 = x.item1 - - val private item2: 'T2 - member x.Item2 = x.item2 - - val private item3: 'T3 - member x.Item3 = x.item3 - - val private item4: 'T4 - member x.Item4 = x.item4 - - val private item5: 'T5 - member x.Item5 = x.item5 - - val private item6: 'T6 - member x.Item6 = x.item6 - - val private item7: 'T7 - member x.Item7 = x.item7 - - val private item8: 'T8 - member x.Item8 = x.item8 - - new(Item1, Item2, Item3, Item4, Item5, Item6, Item7, Item8) = - { - item1 = Item1 - item2 = Item2 - item3 = Item3 - item4 = Item4 - item5 = Item5 - item6 = Item6 - item7 = Item7 - item8 = Item8 - } +[] +type AnonymousObject<'T1, 'T2, 'T3, 'T4, 'T5, 'T6, 'T7, 'T8> + (Item1: 'T1, Item2: 'T2, Item3: 'T3, Item4: 'T4, Item5: 'T5, Item6: 'T6, Item7: 'T7, Item8: 'T8) = + member _.Item1 = Item1 + member _.Item2 = Item2 + member _.Item3 = Item3 + member _.Item4 = Item4 + member _.Item5 = Item5 + member _.Item6 = Item6 + member _.Item7 = Item7 + member _.Item8 = Item8 + + override this.Equals(other: obj) = + match other with + | :? AnonymousObject<'T1, 'T2, 'T3, 'T4, 'T5, 'T6, 'T7, 'T8> as o -> + EqualityComparer<'T1>.Default.Equals(this.Item1, o.Item1) + && EqualityComparer<'T2>.Default.Equals(this.Item2, o.Item2) + && EqualityComparer<'T3>.Default.Equals(this.Item3, o.Item3) + && EqualityComparer<'T4>.Default.Equals(this.Item4, o.Item4) + && EqualityComparer<'T5>.Default.Equals(this.Item5, o.Item5) + && EqualityComparer<'T6>.Default.Equals(this.Item6, o.Item6) + && EqualityComparer<'T7>.Default.Equals(this.Item7, o.Item7) + && EqualityComparer<'T8>.Default.Equals(this.Item8, o.Item8) + | _ -> false + + override this.GetHashCode() = + let mutable hash = EqualityComparer<'T1>.Default.GetHashCode(this.Item1) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T2>.Default.GetHashCode(this.Item2) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T3>.Default.GetHashCode(this.Item3) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T4>.Default.GetHashCode(this.Item4) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T5>.Default.GetHashCode(this.Item5) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T6>.Default.GetHashCode(this.Item6) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T7>.Default.GetHashCode(this.Item7) + hash <- ((hash <<< 5) + hash) ^^^ EqualityComparer<'T8>.Default.GetHashCode(this.Item8) + hash diff --git a/src/FSharp.Core/Query.fs b/src/FSharp.Core/Query.fs index 88d4781ec22..715894dd34a 100644 --- a/src/FSharp.Core/Query.fs +++ b/src/FSharp.Core/Query.fs @@ -1168,14 +1168,13 @@ module Query = let isIQ = IsIQueryableTy mutExpr.Type assert (IsIEnumerableTy mutExpr.Type || IsIQueryableTy mutExpr.Type) let mutElemTy = mutExpr.Type.GetGenericArguments().[0] - let mutExpr = if isIQ then Expr.Coerce (mutExpr, MakeIEnumerableTy mutElemTy) else mutExpr // Generate "source.Select(fun v -> ...)" (remembering that Select is an extension member, i.e. static) let mutVar = new Var("v", mutElemTy) let mutToImmutConvExpr = ConvMutableToImmutable conv (Expr.Var mutVar) - let immutExpr = MakeSelect (CanEliminate.Yes, false, mutExpr, mutVar, mutToImmutConvExpr) - let immutElemTy = mutToImmutConvExpr.Type - let immutExprCoerced = if isIQ then MakeAsQueryable(immutElemTy, immutExpr) else immutExpr - immutExprCoerced + // Use Queryable.Select when source is IQueryable, Enumerable.Select otherwise + // This preserves the IQueryable type, enabling query composition and EF Core async ops + let immutExpr = MakeSelect (CanEliminate.Yes, isIQ, mutExpr, mutVar, mutToImmutConvExpr) + immutExpr | GroupingConv (immutKeyTy, immutElemTy, conv) -> @@ -1387,8 +1386,15 @@ module Query = | IfThenElse (g, t, e) -> match MacroExpand e with | ZeroOnElseBranch -> - let t, tConv = TransInnerAndCommit CanEliminate.Yes check t - TransInnerResult.Other(Expr.IfThenElse (g, t, MakeEmpty t.Type)), tConv + let (t: Expr), tConv = TransInnerAndCommit CanEliminate.Yes check t + // Issue #3445: The result type 't.Type' is IQueryable or IEnumerable. + // MakeEmpty expects the element type, not the collection type. + // Extract the element type to create a properly typed empty sequence. + let tType = t.Type + // t.Type must be IQueryable or IEnumerable - always generic. + // Fail fast if not, rather than silently producing wrong types. + let elemTy = tType.GetGenericArguments().[0] + TransInnerResult.Other(Expr.IfThenElse (g, t, MakeEmpty elemTy)), tConv | _ -> if check then raise (NotSupportedException (SR.GetString(SR.unsupportedIfThenElse)) ) TransInnerResult.Other e, NoConv @@ -1682,14 +1688,12 @@ module Query = immutSource.Type.GetGenericArguments().[0] let immutVar = Var("after", immutSourceElemTy) let mutVar, mutToImmutSelector = ConvertImmutableConsumerToMutableConsumer sourceConv (immutVar, Expr.Var immutVar) - let immutExprEnumerable = MakeSelect(CanEliminate.Yes, false, mutSource, mutVar, mutToImmutSelector) + // Determine if we need to use IQueryable-preserving Select let mustReturnIQueryable = IsQuerySourceTy immutSourceTy && qTyIsIQueryable (immutSourceTy.GetGenericArguments().[1]) || IsIQueryableTy immutSourceTy - let immutExprFinal = - if mustReturnIQueryable then MakeAsQueryable(immutSourceElemTy, immutExprEnumerable) - else immutExprEnumerable - immutExprFinal + // Use Queryable.Select when source should be IQueryable to preserve query composition + MakeSelect(CanEliminate.Yes, mustReturnIQueryable, mutSource, mutVar, mutToImmutSelector) /// Like TransInnerApplicativeAndCommit but (a) assumes the query is nested and (b) throws away the conversion information, /// i.e. assumes that the function "(fun immutConsumingVar -> immutConsumingExpr)" is the only consumption of the query. diff --git a/tests/FSharp.Compiler.ComponentTests/Language/CodeQuotationTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/CodeQuotationTests.fs index 525a124a4d6..29556381221 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/CodeQuotationTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/CodeQuotationTests.fs @@ -57,4 +57,111 @@ match expr with |> asExe |> withLangVersion80 |> compileAndRun + |> shouldSucceed + + // Tests for issues #11131 and #15648 - anonymous record field ordering + // Note: The fix is in FSharp.Core/Linq.fs - these tests verify that queries + // with anonymous records work correctly regardless of field order. + // The expression tree structure tests are in FSharp.Core.UnitTests which directly + // references the modified FSharp.Core. + + [] + let ``Anonymous records with both field orders produce equivalent results - issue 11131 and 15648`` () = + Fsx """ +open System.Linq + +type Person = { Name: string; Id : int } +type Wrapper = { Person: Person } + +let data = [ + { Person = { Name = "One"; Id = 1 } } + { Person = { Name = "Two"; Id = 2 } } + ] + +// Both orders should produce same results when executed +let resultsAlpha = + data.AsQueryable().Select(fun x -> {| A = x.Person.Name; B = x.Person.Id |}).ToList() + +let resultsNonAlpha = + data.AsQueryable().Select(fun x -> {| B = x.Person.Id; A = x.Person.Name |}).ToList() + +// Verify results are equivalent +if resultsAlpha.Count <> resultsNonAlpha.Count then + failwith "Result counts don't match" + +for i in 0 .. resultsAlpha.Count - 1 do + if resultsAlpha.[i].A <> resultsNonAlpha.[i].A then + failwithf "A values don't match at index %d" i + if resultsAlpha.[i].B <> resultsNonAlpha.[i].B then + failwithf "B values don't match at index %d" i + +printfn "Both field orders produce equivalent results: %d items" resultsAlpha.Count + """ + |> asExe + |> withLangVersionPreview + |> compileAndRun + |> shouldSucceed + + [] + let ``Nested anonymous records work correctly`` () = + Fsx """ +open System.Linq + +type Person = { Name: string; Id : int } +type Wrapper = { Person: Person } + +let data = [ + { Person = { Name = "One"; Id = 1 } } + ] + +// Nested anonymous records should work +let queryNested = + data.AsQueryable().Select(fun x -> {| Other = {| Name = x.Person.Name; Id = x.Person.Id |} |}).ToList() + +if queryNested.Count <> 1 then + failwith "Expected 1 result" + +if queryNested.[0].Other.Name <> "One" then + failwithf "Expected Name='One', got '%s'" queryNested.[0].Other.Name + +if queryNested.[0].Other.Id <> 1 then + failwithf "Expected Id=1, got %d" queryNested.[0].Other.Id + +printfn "Nested anonymous record works correctly" + """ + |> asExe + |> withLangVersionPreview + |> compileAndRun + |> shouldSucceed + + [] + let ``F# record with non-declaration field order works correctly`` () = + Fsx """ +open System.Linq + +type Person = { Name: string; Id : int } +type PartialPerson = { LastName: string; ID : int } + +let data = [ { Name = "One"; Id = 1 }; { Name = "Two"; Id = 2 } ] + +// Declaration order +let query1 = data.AsQueryable().Select(fun p -> { LastName = p.Name; ID = p.Id }).ToList() + +// Non-declaration order (swapped) +let query2 = data.AsQueryable().Select(fun p -> { ID = p.Id; LastName = p.Name }).ToList() + +if query1.Count <> query2.Count then + failwith "Result counts don't match" + +for i in 0 .. query1.Count - 1 do + if query1.[i].LastName <> query2.[i].LastName then + failwithf "LastName values don't match at index %d" i + if query1.[i].ID <> query2.[i].ID then + failwithf "ID values don't match at index %d" i + +printfn "Both F# record field orderings produce equivalent results" + """ + |> asExe + |> withLangVersionPreview + |> compileAndRun |> shouldSucceed \ No newline at end of file diff --git a/tests/FSharp.Compiler.ComponentTests/Language/ComputationExpressionTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/ComputationExpressionTests.fs index 4c9e371a5d0..e5b687996f0 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/ComputationExpressionTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/ComputationExpressionTests.fs @@ -2258,4 +2258,109 @@ but here has type |> getCompilation |> asFsx |> runFsi - |> shouldSucceed \ No newline at end of file + |> shouldSucceed + + // ===================================================================== + // Issue #422: FS1182 false positive in query expressions + // The fix marks synthetic lambda parameters in query translation as + // compiler-generated, which prevents the FS1182 warning from firing. + // ===================================================================== + + [] + let ``Query expression variable with underscore prefix should not warn FS1182 - issue 422`` () = + FSharp """ +module Test + +let result = + query { for _x in [1;2;3] do + select 1 } + """ + |> withOptions ["--warnon:FS1182"] + |> asLibrary + |> compile + |> shouldSucceed + |> ignore + + [] + let ``Query expression variable used in select should not warn FS1182 - issue 422`` () = + FSharp """ +module Test + +let result = + query { for x in [1;2;3] do + where (x > 2) + select x } + """ + |> withOptions ["--warnon:FS1182"] + |> asLibrary + |> compile + |> shouldSucceed + |> ignore + + [] + let ``Query variable used in where does not trigger FS1182 - issue 422`` () = + FSharp """ +module Test + +let result = + query { for x in [1;2;3;4;5] do + where (x > 0) + select 1 } + """ + |> withOptions ["--warnon:FS1182"] + |> asLibrary + |> compile + |> shouldSucceed + |> ignore + + [] + let ``Query variable used in let binding does not trigger FS1182 - issue 422`` () = + FSharp """ +module Test + +let result = + query { for x in [1;2;3] do + let y = x * 2 + select y } + """ + |> withOptions ["--warnon:FS1182"] + |> asLibrary + |> compile + |> shouldSucceed + |> ignore + + [] + let ``Join variable used in select does not trigger FS1182 - issue 422`` () = + FSharp """ +module Test + +let data1 = [1;2;3] +let data2 = [(1, "one"); (2, "two"); (3, "three")] + +let result = + query { for x in data1 do + join (y, name) in data2 on (x = y) + select name } + """ + |> withOptions ["--warnon:FS1182"] + |> asLibrary + |> compile + |> shouldSucceed + |> ignore + + [] + let ``Multiple query variables in nested for do not trigger FS1182 - issue 422`` () = + FSharp """ +module Test + +let result = + query { for a in [1;2;3] do + for b in [4;5;6] do + where (a < b) + select (a + b) } + """ + |> withOptions ["--warnon:FS1182"] + |> asLibrary + |> compile + |> shouldSucceed + |> ignore \ No newline at end of file diff --git a/tests/FSharp.Compiler.Service.Tests/ProjectAnalysisTests.fs b/tests/FSharp.Compiler.Service.Tests/ProjectAnalysisTests.fs index 50258738045..b3e09b588eb 100644 --- a/tests/FSharp.Compiler.Service.Tests/ProjectAnalysisTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/ProjectAnalysisTests.fs @@ -2161,9 +2161,11 @@ let ``Test Project12 all symbols`` () = ("val op_Range", "(..)", "file1", ((5, 28), (5, 30)), [], ["val"]); ("val i", "i", "file1", ((5, 21), (5, 22)), ["defn"], []); ("val op_Equality", "(=)", "file1", ((6, 26), (6, 27)), [], ["val"]); - ("val i", "i", "file1", ((6, 24), (6, 25)), [], []); - ("val i", "i", "file1", ((7, 25), (7, 26)), [], []); - ("val i", "i", "file1", ((7, 27), (7, 28)), [], []); + // Note: Query variable usages in where/select lambdas are now marked as + // compiler-generated to suppress false FS1182 warnings. See Issue #422 fix. + ("val i", "i", "file1", ((6, 24), (6, 25)), [], ["compgen"]); + ("val i", "i", "file1", ((7, 25), (7, 26)), [], ["compgen"]); + ("val i", "i", "file1", ((7, 27), (7, 28)), [], ["compgen"]); ("val x2", "x2", "file1", ((5, 4), (5, 6)), ["defn"], ["val"]); ("ComputationExpressions", "ComputationExpressions", "file1", ((2, 7), (2, 29)), ["defn"], ["module"])|] diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl index 5bd39b09a39..6a88718e82c 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl @@ -2327,14 +2327,20 @@ Microsoft.FSharp.Linq.QueryRunExtensions.LowPriority: T RunQueryAsValue[T](Micro Microsoft.FSharp.Linq.QuerySource`2[T,Q]: System.Collections.Generic.IEnumerable`1[T] Source Microsoft.FSharp.Linq.QuerySource`2[T,Q]: System.Collections.Generic.IEnumerable`1[T] get_Source() Microsoft.FSharp.Linq.QuerySource`2[T,Q]: Void .ctor(System.Collections.Generic.IEnumerable`1[T]) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: Void .ctor(T1) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T2 Item2 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T2 get_Item2() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: Void .ctor(T1, T2) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T2 Item2 @@ -2342,6 +2348,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T2 get_Item2() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T3 Item3 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T3 get_Item3() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: Void .ctor(T1, T2, T3) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T2 Item2 @@ -2351,6 +2359,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T3 get_Item Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T4 Item4 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T4 get_Item4() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: Void .ctor(T1, T2, T3, T4) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T2 Item2 @@ -2362,6 +2372,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T4 get_I Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T5 Item5 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T5 get_Item5() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: Void .ctor(T1, T2, T3, T4, T5) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T2 Item2 @@ -2375,6 +2387,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T5 ge Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T6 Item6 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T6 get_Item6() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: Void .ctor(T1, T2, T3, T4, T5, T6) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T2 Item2 @@ -2390,6 +2404,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T6 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T7 Item7 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T7 get_Item7() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: Void .ctor(T1, T2, T3, T4, T5, T6, T7) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: T2 Item2 diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl index 8096acb15a1..301cb5ef98e 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl @@ -2326,14 +2326,20 @@ Microsoft.FSharp.Linq.QueryRunExtensions.LowPriority: T RunQueryAsValue[T](Micro Microsoft.FSharp.Linq.QuerySource`2[T,Q]: System.Collections.Generic.IEnumerable`1[T] Source Microsoft.FSharp.Linq.QuerySource`2[T,Q]: System.Collections.Generic.IEnumerable`1[T] get_Source() Microsoft.FSharp.Linq.QuerySource`2[T,Q]: Void .ctor(System.Collections.Generic.IEnumerable`1[T]) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: Void .ctor(T1) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T2 Item2 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T2 get_Item2() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: Void .ctor(T1, T2) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T2 Item2 @@ -2341,6 +2347,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T2 get_Item2() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T3 Item3 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T3 get_Item3() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: Void .ctor(T1, T2, T3) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T2 Item2 @@ -2350,6 +2358,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T3 get_Item Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T4 Item4 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T4 get_Item4() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: Void .ctor(T1, T2, T3, T4) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T2 Item2 @@ -2361,6 +2371,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T4 get_I Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T5 Item5 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T5 get_Item5() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: Void .ctor(T1, T2, T3, T4, T5) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T2 Item2 @@ -2374,6 +2386,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T5 ge Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T6 Item6 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T6 get_Item6() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: Void .ctor(T1, T2, T3, T4, T5, T6) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T2 Item2 @@ -2389,6 +2403,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T6 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T7 Item7 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T7 get_Item7() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: Void .ctor(T1, T2, T3, T4, T5, T6, T7) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: T2 Item2 diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl index 95ebeea8a37..c7e8d9ca4da 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl @@ -2330,14 +2330,20 @@ Microsoft.FSharp.Linq.QueryRunExtensions.LowPriority: T RunQueryAsValue[T](Micro Microsoft.FSharp.Linq.QuerySource`2[T,Q]: System.Collections.Generic.IEnumerable`1[T] Source Microsoft.FSharp.Linq.QuerySource`2[T,Q]: System.Collections.Generic.IEnumerable`1[T] get_Source() Microsoft.FSharp.Linq.QuerySource`2[T,Q]: Void .ctor(System.Collections.Generic.IEnumerable`1[T]) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: Void .ctor(T1) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T2 Item2 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T2 get_Item2() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: Void .ctor(T1, T2) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T2 Item2 @@ -2345,6 +2351,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T2 get_Item2() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T3 Item3 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T3 get_Item3() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: Void .ctor(T1, T2, T3) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T2 Item2 @@ -2354,6 +2362,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T3 get_Item Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T4 Item4 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T4 get_Item4() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: Void .ctor(T1, T2, T3, T4) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T2 Item2 @@ -2365,6 +2375,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T4 get_I Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T5 Item5 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T5 get_Item5() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: Void .ctor(T1, T2, T3, T4, T5) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T2 Item2 @@ -2378,6 +2390,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T5 ge Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T6 Item6 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T6 get_Item6() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: Void .ctor(T1, T2, T3, T4, T5, T6) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T2 Item2 @@ -2393,6 +2407,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T6 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T7 Item7 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T7 get_Item7() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: Void .ctor(T1, T2, T3, T4, T5, T6, T7) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: T2 Item2 diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl index c4106d360c4..606217f9fc5 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl @@ -2329,14 +2329,20 @@ Microsoft.FSharp.Linq.QueryRunExtensions.LowPriority: T RunQueryAsValue[T](Micro Microsoft.FSharp.Linq.QuerySource`2[T,Q]: System.Collections.Generic.IEnumerable`1[T] Source Microsoft.FSharp.Linq.QuerySource`2[T,Q]: System.Collections.Generic.IEnumerable`1[T] get_Source() Microsoft.FSharp.Linq.QuerySource`2[T,Q]: Void .ctor(System.Collections.Generic.IEnumerable`1[T]) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`1[T1]: Void .ctor(T1) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T2 Item2 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: T2 get_Item2() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`2[T1,T2]: Void .ctor(T1, T2) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T2 Item2 @@ -2344,6 +2350,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T2 get_Item2() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T3 Item3 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: T3 get_Item3() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`3[T1,T2,T3]: Void .ctor(T1, T2, T3) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T2 Item2 @@ -2353,6 +2361,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T3 get_Item Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T4 Item4 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: T4 get_Item4() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`4[T1,T2,T3,T4]: Void .ctor(T1, T2, T3, T4) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T2 Item2 @@ -2364,6 +2374,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T4 get_I Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T5 Item5 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: T5 get_Item5() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`5[T1,T2,T3,T4,T5]: Void .ctor(T1, T2, T3, T4, T5) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T2 Item2 @@ -2377,6 +2389,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T5 ge Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T6 Item6 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: T6 get_Item6() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`6[T1,T2,T3,T4,T5,T6]: Void .ctor(T1, T2, T3, T4, T5, T6) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T2 Item2 @@ -2392,6 +2406,8 @@ Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T6 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T7 Item7 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: T7 get_Item7() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`7[T1,T2,T3,T4,T5,T6,T7]: Void .ctor(T1, T2, T3, T4, T5, T6, T7) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: Boolean Equals(System.Object) +Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: Int32 GetHashCode() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: T1 Item1 Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: T1 get_Item1() Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject`8[T1,T2,T3,T4,T5,T6,T7,T8]: T2 Item2 diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj b/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj index 16e45542174..d4c6fd5177a 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj @@ -88,6 +88,8 @@ + + diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Linq/NullableOperators.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Linq/NullableOperators.fs index ecc1da12ba3..dc957303eaf 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Linq/NullableOperators.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Linq/NullableOperators.fs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. -namespace FSharp.Core.UnitTests.FSharp_Core.Linq.NullableOperators +namespace FSharp.Core.UnitTests.Linq +open System open Xunit open Microsoft.FSharp.Linq -[] type NullableOperators() = [] member _.CastingUint () = let expected = Nullable(12u) let actual = Nullable.uint (Nullable(12)) - Assert.AreEqual(expected, actual) \ No newline at end of file + Assert.Equal(expected, actual) \ No newline at end of file diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Linq/QueryTests.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Linq/QueryTests.fs new file mode 100644 index 00000000000..d0043ad1c7d --- /dev/null +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Linq/QueryTests.fs @@ -0,0 +1,569 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +// Suppress FS3220 warning for System.Tuple Item1/Item2 access - these tests intentionally test interop with System.Tuple +#nowarn "3220" + +namespace FSharp.Core.UnitTests.Linq + +open System +open System.Linq +open Xunit +open Microsoft.FSharp.Linq +open Microsoft.FSharp.Linq.RuntimeHelpers + +/// Test data for query tests +module TestData = + type Person = { Id: int; Name: string; Age: int; DepartmentId: int } + type Department = { Id: int; DeptName: string } + + let people = [ + { Id = 1; Name = "Alice"; Age = 30; DepartmentId = 1 } + { Id = 2; Name = "Bob"; Age = 25; DepartmentId = 1 } + { Id = 3; Name = "Charlie"; Age = 35; DepartmentId = 2 } + { Id = 4; Name = "Diana"; Age = 28; DepartmentId = 2 } + ] + + let departments = [ + { Id = 1; DeptName = "Engineering" } + { Id = 2; DeptName = "Marketing" } + ] + + type Table1 = { Id1: int; Id2: int; Value1: string } + type Table2 = { Id1: int; Id2: int; Value2: string } + + let table1Data = [ + { Table1.Id1 = 1; Id2 = 10; Value1 = "A" } + { Table1.Id1 = 2; Id2 = 20; Value1 = "B" } + { Table1.Id1 = 3; Id2 = 30; Value1 = "C" } + ] + + let table2Data = [ + { Table2.Id1 = 1; Id2 = 10; Value2 = "X" } + { Table2.Id1 = 2; Id2 = 20; Value2 = "Y" } + { Table2.Id1 = 4; Id2 = 40; Value2 = "Z" } + ] + +open TestData + +type QueryTupleJoinTests() = + + /// Issue #7885: Inline tuple join on ((t1.Id1, t1.Id2) = (t2.Id1, t2.Id2)) returns correct matches + [] + member _.``Inline tuple join returns correct matches - issue 7885``() = + let t1 = table1Data.AsQueryable() + let t2 = table2Data.AsQueryable() + + // Inline tuple join - this was broken before the fix + let inlineResult = + query { + for a in t1 do + join b in t2 on ((a.Id1, a.Id2) = (b.Id1, b.Id2)) + select (a.Value1, b.Value2) + } |> Seq.toList + + // Expected: 2 matches (1,10) and (2,20) + Assert.Equal(2, inlineResult.Length) + Assert.Contains(("A", "X"), inlineResult) + Assert.Contains(("B", "Y"), inlineResult) + + /// Issue #7885: Verify inline and function-based tuple joins produce same results + [] + member _.``Inline tuple join matches function-based tuple join - issue 7885``() = + let t1 = table1Data.AsQueryable() + let t2 = table2Data.AsQueryable() + + let makeKey1 (a: Table1) = (a.Id1, a.Id2) + let makeKey2 (b: Table2) = (b.Id1, b.Id2) + + // Inline tuple join + let inlineResult = + query { + for a in t1 do + join b in t2 on ((a.Id1, a.Id2) = (b.Id1, b.Id2)) + select (a.Value1, b.Value2) + } |> Seq.toList + + // Function-based tuple join (this was working before) + let funcResult = + query { + for a in t1 do + join b in t2 on (makeKey1 a = makeKey2 b) + select (a.Value1, b.Value2) + } |> Seq.toList + + // Both should produce the same results + Assert.Equal(funcResult.Length, inlineResult.Length) + for item in funcResult do + Assert.Contains(item, inlineResult) + for item in inlineResult do + Assert.Contains(item, funcResult) + + +type QueryGroupByTupleTests() = + + /// Issue #47: GroupBy with tuple key works + [] + member _.``GroupBy with tuple key works - issue 47``() = + let data = + [ (1, "A", 100); (1, "A", 200); (2, "B", 300) ].AsQueryable() + + let result = + query { + for (x, y, z) in data do + groupBy (x, y) into g + select (g.Key, g.Count()) + } |> Seq.toList + + Assert.Equal(2, result.Length) + Assert.Contains(((1, "A"), 2), result) + Assert.Contains(((2, "B"), 1), result) + + /// Issue #47: Accessing tuple elements after groupBy works + [] + member _.``Accessing tuple elements after groupBy works - issue 47``() = + let data = people.AsQueryable() + + let result = + query { + for p in data do + groupBy (p.DepartmentId, p.Age > 28) into g + select (fst g.Key, snd g.Key, g.Count()) + } |> Seq.toList + + // Verify we can access fst and snd on the key + Assert.True(result.Length > 0) + for (deptId, isOver28, count) in result do + Assert.True(deptId >= 1 && deptId <= 2) + Assert.True(count >= 1) + + /// Verify groupBy with complex tuple key and element access + [] + member _.``GroupBy with tuple key allows iteration over group elements``() = + let data = table1Data.AsQueryable() + + let result = + query { + for item in data do + groupBy item.Id1 into g + select (g.Key, g |> Seq.map (fun x -> x.Value1) |> Seq.toList) + } |> Seq.toList + + Assert.Equal(3, result.Length) + let group1 = result |> List.find (fun (k, _) -> k = 1) + Assert.Contains("A", snd group1) + + +type QueryGroupJoinTupleTests() = + + /// Verify groupJoin with inline tuple key works + [] + member _.``GroupJoin with inline tuple key works``() = + let t1 = table1Data.AsQueryable() + let t2 = table2Data.AsQueryable() + + let result = + query { + for a in t1 do + groupJoin b in t2 on ((a.Id1, a.Id2) = (b.Id1, b.Id2)) into bs + select (a.Value1, bs |> Seq.length) + } |> Seq.toList + + // t1 has 3 items, each should have a group (possibly empty) + Assert.Equal(3, result.Length) + // First two should have 1 match each, third should have 0 + Assert.Contains(("A", 1), result) + Assert.Contains(("B", 1), result) + Assert.Contains(("C", 0), result) + + +type AnonymousObjectEqualityTests() = + + /// Verify AnonymousObject has structural equality + [] + member _.``AnonymousObject with same values are equal``() = + let ao1 = Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject(1, "test") + let ao2 = Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject(1, "test") + + Assert.True(ao1.Equals(ao2)) + Assert.Equal(ao1.GetHashCode(), ao2.GetHashCode()) + + /// Verify AnonymousObject with different values are not equal + [] + member _.``AnonymousObject with different values are not equal``() = + let ao1 = Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject(1, "test") + let ao2 = Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject(2, "test") + + Assert.False(ao1.Equals(ao2)) + + /// Verify AnonymousObject hash codes differ for different values + [] + member _.``AnonymousObject hash codes are consistent with equality``() = + let ao1 = Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject(1, "test") + let ao2 = Microsoft.FSharp.Linq.RuntimeHelpers.AnonymousObject(2, "other") + + // The important thing is that equal objects have equal hash codes + Assert.False(ao1.Equals(ao2)) + + +/// Tests for tuple select preserving IQueryable type - Issues #3782 and #15133 +type QueryTupleSelectTests() = + + /// Issue #3782: Tuple select should produce composable IQueryable + /// The query expression should contain proper Select calls that can be extended + [] + member _.``Tuple select preserves IQueryable type - issue 3782``() = + let data = TestData.people.AsQueryable() + + let result = + query { + for p in data do + select (p.Id, p.Name) + } + + // The result should be IQueryable - use box to avoid FS0067 warning + Assert.NotNull(box result) + Assert.True(typeof>.IsAssignableFrom(result.GetType()), + sprintf "Expected IQueryable but got %s" (result.GetType().FullName)) + + // The expression tree should have Select calls that produce tuples + let queryable = result + let exprStr = queryable.Expression.ToString() + // Verify the expression has the tuple conversion + Assert.True(exprStr.Contains("Tuple") && exprStr.Contains("Select"), + sprintf "Expression should contain Select with Tuple, but got: %s" exprStr) + + // Most importantly: verify the query can be composed (this is what the issue is about) + // If the query was incorrectly using Enumerable.Select, adding Where would fail or produce wrong results + let composed = result.Where(fun (id, _name) -> id > 0) + let items = composed |> Seq.toList + Assert.True(items.Length > 0) + + /// Issue #3782: System.Tuple select should also preserve IQueryable type + [] + member _.``System.Tuple select preserves IQueryable type``() = + let data = TestData.people.AsQueryable() + + let result = + query { + for p in data do + select (System.Tuple.Create(p.Id, p.Name)) + } + + // System.Tuple should also produce IQueryable - use box to avoid FS0067 warning + Assert.NotNull(box result) + Assert.True(typeof>>.IsAssignableFrom(result.GetType()), + sprintf "Expected IQueryable> but got %s" (result.GetType().FullName)) + + // Verify composability + let composed = result.Where(fun t -> t.Item1 > 0) + let items = composed |> Seq.toList + Assert.True(items.Length > 0) + + /// Issue #3782: F# tuple and System.Tuple should produce equivalent query types + [] + member _.``F# tuple and System.Tuple produce equivalent query behavior``() = + let data = TestData.people.AsQueryable() + + // F# tuple projection + let fsharpTupleResult = + query { + for p in data do + select (p.Id, p.Name) + } |> Seq.toList + + // Explicit System.Tuple projection + let systemTupleResult = + query { + for p in data do + select (System.Tuple.Create(p.Id, p.Name)) + } |> Seq.map (fun t -> (t.Item1, t.Item2)) |> Seq.toList + + // Both should produce the same results + Assert.Equal(fsharpTupleResult.Length, systemTupleResult.Length) + for (expected, actual) in List.zip fsharpTupleResult systemTupleResult do + Assert.Equal(expected, actual) + + /// Issue #15133: Queries with tuple select can be extended with Where + [] + member _.``Tuple select query can be composed with Where - issue 15133``() = + let data = TestData.people.AsQueryable() + + let baseQuery = + query { + for p in data do + select (p.Id, p.Name, p.Age) + } + + // Should be able to add Where after tuple select + let filtered = baseQuery.Where(fun (id, name, age) -> age > 25) + + // Verify the query can be executed + let result = filtered |> Seq.toList + + // All results should have age > 25 + Assert.True(result.Length > 0) + for (id, name, age) in result do + Assert.True(age > 25, sprintf "Expected age > 25 but got %d" age) + + /// Issue #15133: Queries with tuple select can be extended with OrderBy + [] + member _.``Tuple select query can be composed with OrderBy - issue 15133``() = + let data = TestData.people.AsQueryable() + + let baseQuery = + query { + for p in data do + select (p.Id, p.Name, p.Age) + } + + // Should be able to add OrderBy after tuple select + let sorted = baseQuery.OrderBy(fun (id, name, age) -> age) + + // Verify the query can be executed and is sorted + let result = sorted |> Seq.toList + + Assert.True(result.Length > 0) + // Verify sorting + let ages = result |> List.map (fun (_, _, age) -> age) + let sortedAges = ages |> List.sort + Assert.Equal(sortedAges, ages) + + /// Record projections should also be composable + [] + member _.``Record projection query is composable``() = + let data = TestData.people.AsQueryable() + + let baseQuery = + query { + for p in data do + select { TestData.Person.Id = p.Id; Name = p.Name; Age = p.Age; DepartmentId = p.DepartmentId } + } + + // Should be able to compose with Where + let filtered = baseQuery.Where(fun p -> p.Age > 28) + + let result = filtered |> Seq.toList + Assert.True(result.Length > 0) + for p in result do + Assert.True(p.Age > 28) + + /// Multiple tuple elements should all be accessible in composed queries + [] + member _.``Multi-element tuple select preserves all elements for composition``() = + let data = TestData.people.AsQueryable() + + let baseQuery = + query { + for p in data do + select (p.Id, p.Name, p.Age, p.DepartmentId) + } + + // Filter by tuple element + let filtered = baseQuery.Where(fun (id, name, age, deptId) -> deptId = 1 && age > 20) + let result = filtered |> Seq.toList + + Assert.True(result.Length > 0) + for (_, _, _, deptId) in result do + Assert.Equal(1, deptId) + + +/// Helper types for mutation tests +module MutationTestHelpers = + /// Type with a mutable field for FieldSet testing + type TypeWithMutableField() = + [] + val mutable Field: int + + /// Type with a settable property for PropertySet testing + type TypeWithSettableProperty() = + let mutable value = 0 + member this.Prop + with get() = value + and set(v) = value <- v + +/// Tests for EvaluateQuotation edge cases - Issue #19099 +type EvaluateQuotationEdgeCaseTests() = + + /// Issue #19099: EvaluateQuotation should handle Sequential expressions + [] + member _.``EvaluateQuotation handles Sequential expressions - issue 19099``() = + // Test sequential expression: (ignore 1; 42) - evaluates first expr for side effects, returns second + let result = LeafExpressionConverter.EvaluateQuotation <@ ignore 1; 42 @> + Assert.Equal(42, result :?> int) + + /// Issue #19099: EvaluateQuotation should handle void method calls (unit return) + [] + member _.``EvaluateQuotation handles void method calls - issue 19099``() = + // This should not throw - it should execute and return unit + let result = LeafExpressionConverter.EvaluateQuotation <@ System.Console.Write("") @> + Assert.Equal(box (), result) + + /// Issue #19099: EvaluateQuotation should handle unit-returning expressions + [] + member _.``EvaluateQuotation handles unit return - issue 19099``() = + // Test that unit-returning expressions work (previously failed with System.Void issue) + let result = LeafExpressionConverter.EvaluateQuotation <@ ignore 1; () @> + Assert.Equal(box (), result) + + /// Issue #19099 T1.1: EvaluateQuotation should handle VarSet (mutable variable assignment) + [] + member _.``EvaluateQuotation handles VarSet - issue 19099``() = + // Test mutable variable assignment: let mutable x = 1; x <- 2; x should return 2 + let result = LeafExpressionConverter.EvaluateQuotation <@ let mutable x = 1 in x <- 2; x @> + Assert.Equal(2, result :?> int) + + /// Issue #19099 T1.2: EvaluateQuotation should handle FieldSet (mutable field assignment) + [] + member _.``EvaluateQuotation handles FieldSet - issue 19099``() = + // Test mutable field assignment + let obj = MutationTestHelpers.TypeWithMutableField() + let result = LeafExpressionConverter.EvaluateQuotation <@ obj.Field <- 42; obj.Field @> + Assert.Equal(42, result :?> int) + + /// Issue #19099 T1.3: EvaluateQuotation should handle PropertySet (settable property assignment) + [] + member _.``EvaluateQuotation handles PropertySet - issue 19099``() = + // Test settable property assignment + let obj = MutationTestHelpers.TypeWithSettableProperty() + let result = LeafExpressionConverter.EvaluateQuotation <@ obj.Prop <- 99; obj.Prop @> + Assert.Equal(99, result :?> int) + + /// Issue #19099 T1.4: EvaluateQuotation should handle indexed PropertySet (array index assignment) + [] + member _.``EvaluateQuotation handles indexed PropertySet - issue 19099``() = + // Test array index assignment: arr.[0] <- value + let arr = [| 1; 2; 3 |] + let result = LeafExpressionConverter.EvaluateQuotation <@ arr.[0] <- 10; arr.[0] @> + Assert.Equal(10, result :?> int) + + /// Q3.5: EvaluateQuotation should handle deeply nested let bindings + /// This tests the let-binding inlining logic doesn't break with deep nesting + [] + member _.``EvaluateQuotation handles deeply nested let bindings``() = + // Test: let a = x in let b = a in let c = b in c + let result = LeafExpressionConverter.EvaluateQuotation <@ let a = 42 in let b = a in let c = b in c @> + Assert.Equal(42, result :?> int) + + /// Q3.5: Additional test for deeply nested let with computation at each level + [] + member _.``EvaluateQuotation handles deeply nested let with computation``() = + // Test with actual computation at each level + let result = LeafExpressionConverter.EvaluateQuotation <@ let a = 1 in let b = a + 1 in let c = b + 1 in let d = c + 1 in d @> + Assert.Equal(4, result :?> int) + + /// Q3.6: Verify no performance regression with 15+ nested let bindings + /// This tests the inlining approach doesn't cause exponential blowup + [] + member _.``EvaluateQuotation handles 15 nested let bindings``() = + // 15 levels of nested lets with computation at each level + // This verifies the inlining is O(n) not O(2^n) + let result = LeafExpressionConverter.EvaluateQuotation <@ + let v1 = 1 in + let v2 = v1 + 1 in + let v3 = v2 + 1 in + let v4 = v3 + 1 in + let v5 = v4 + 1 in + let v6 = v5 + 1 in + let v7 = v6 + 1 in + let v8 = v7 + 1 in + let v9 = v8 + 1 in + let v10 = v9 + 1 in + let v11 = v10 + 1 in + let v12 = v11 + 1 in + let v13 = v12 + 1 in + let v14 = v13 + 1 in + let v15 = v14 + 1 in + v15 @> + Assert.Equal(15, result :?> int) + + +/// Tests for conditional without else branch in queries - Issue #3445 +type QueryConditionalTests() = + + /// Issue #3445: Query with if-then (no else) should compile and run + [] + member _.``Query with if-then no else compiles and runs - issue 3445``() = + // This was throwing: Type mismatch when building 'cond' + let result = + query { + for i in [1; 2; 3].AsQueryable() do + if i > 1 then + select i + } |> Seq.toList + + Assert.Equal(2, result.Length) + Assert.Contains(2, result) + Assert.Contains(3, result) + + /// Issue #3445: Query with if-then (no else) with false condition returns empty + [] + member _.``Query with if-then no else with false condition returns empty - issue 3445``() = + let result = + query { + for i in [1; 2; 3].AsQueryable() do + if false then + select i + } |> Seq.toList + + Assert.Empty(result) + + /// Issue #3445: Query with complex conditional filter + [] + member _.``Query with complex if-then condition works - issue 3445``() = + let data = TestData.people.AsQueryable() + let result = + query { + for p in data do + if p.Age > 25 && p.DepartmentId = 1 then + select p.Name + } |> Seq.toList + + // Alice (30, dept 1) matches; Bob (25, dept 1) doesn't (not >25) + Assert.Single(result) |> ignore + Assert.Contains("Alice", result) + + +/// Tests for headOrDefault with tuples - Issue #3845 +/// Note: This issue requires a compiler warning for proper fix. +/// These tests document the current behavior. +type QueryHeadOrDefaultTests() = + + /// headOrDefault returns null for empty sequence with reference type + [] + member _.``headOrDefault with empty sequence returns default``() = + let data = [1; 2; 3].AsQueryable() + let result = + query { + for x in data do + where (x > 100) + headOrDefault + } + // For int (value type), default is 0 + Assert.Equal(0, result) + + /// headOrDefault with matching element returns that element + [] + member _.``headOrDefault with matching element returns first match``() = + let data = [1; 2; 3].AsQueryable() + let result = + query { + for x in data do + where (x > 1) + headOrDefault + } + Assert.Equal(2, result) + + /// Document: headOrDefault with tuple and no match returns null + /// This test documents the known limitation (issue #3845) + [] + member _.``headOrDefault with tuple and no match returns null - issue 3845 known limitation``() = + let data = [(1, "a"); (2, "b")].AsQueryable() + let result = + query { + for x in data do + where (fst x > 100) + headOrDefault + } + // The result is null (default for reference type tuple) + // Accessing .Item1/.Item2 would cause NRE - this is the known issue + Assert.True(obj.ReferenceEquals(null, result)) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Quotations/FSharpQuotations.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Quotations/FSharpQuotations.fs index a61c8a97e2e..8be4cb639c9 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Quotations/FSharpQuotations.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Quotations/FSharpQuotations.fs @@ -11,6 +11,7 @@ open FSharp.Core.UnitTests.LibraryTestFx open Xunit open FSharp.Quotations open FSharp.Quotations.Patterns +open FSharp.Linq.RuntimeHelpers type E = Microsoft.FSharp.Quotations.Expr;; @@ -260,3 +261,88 @@ module TestConditionalConstraints = equate.Invoke (null, [| ThingWithNoEquality.NoEquality ; anotherOne |]) |> unbox |> Assert.False + + // Tests for issues #11131 and #15648 - anonymous record field ordering + // When anonymous records have fields in non-alphabetical order, the LINQ expression + // should not contain Invoke patterns that LINQ providers can't translate. + [] + let ``Anonymous record with non-alphabetical field order produces clean LINQ expression - issues 11131 and 15648`` () = + // Non-alphabetical order - B before A + let q = <@ fun (x: int) -> {| B = x; A = x + 1 |} @> + + let linqExpr = LeafExpressionConverter.QuotationToExpression q + let exprStr = linqExpr.ToString() + + Assert.DoesNotContain(".Invoke(", exprStr) + + [] + let ``Nested anonymous record produces clean LINQ expression`` () = + // Nested anonymous record with non-alphabetical field order + let q = <@ fun (x: int) -> {| Outer = {| B = x; A = x + 1 |} |} @> + + let linqExpr = LeafExpressionConverter.QuotationToExpression q + let exprStr = linqExpr.ToString() + + Assert.DoesNotContain(".Invoke(", exprStr) + + [] + let ``Both anonymous record field orders produce equivalent results`` () = + // Alphabetical order + let qAlpha = <@ fun (x: int) -> {| A = x + 1; B = x |} @> + // Non-alphabetical order + let qNonAlpha = <@ fun (x: int) -> {| B = x; A = x + 1 |} @> + + let linqAlpha = LeafExpressionConverter.QuotationToExpression qAlpha + let linqNonAlpha = LeafExpressionConverter.QuotationToExpression qNonAlpha + + let exprAlpha = linqAlpha.ToString() + let exprNonAlpha = linqNonAlpha.ToString() + + // Neither should contain Invoke + Assert.DoesNotContain(".Invoke(", exprAlpha) + Assert.DoesNotContain(".Invoke(", exprNonAlpha) + + // Tests for issue #16918 - Array indexing generates GetArray instead of proper array index expression + // When array indexing is used in LINQ expressions, it should generate proper array index + // expressions that LINQ providers can translate, not GetArray method calls. + type ArrayTestUnfold = { c: string } + type ArrayTestDoc = { p: string; u: ArrayTestUnfold[] } + + [] + let ``Array indexing produces ArrayIndex expression not GetArray - issue 16918`` () = + // Array access like x.u.[0] should NOT produce GetArray call + let q = <@ fun (x: ArrayTestDoc) -> x.u.[0] @> + + let linqExpr = LeafExpressionConverter.QuotationToExpression q + let exprStr = linqExpr.ToString() + + // Should NOT contain GetArray + Assert.DoesNotContain("GetArray", exprStr) + // Should produce x.u[0] style array index expression + Assert.Contains("[0]", exprStr) + + [] + let ``Nested array member access produces clean LINQ expression - issue 16918`` () = + // x.u[0].c should generate proper expression tree without GetArray + let q = <@ fun (x: ArrayTestDoc) -> x.u.[0].c @> + + let linqExpr = LeafExpressionConverter.QuotationToExpression q + let exprStr = linqExpr.ToString() + + // Should NOT contain GetArray + Assert.DoesNotContain("GetArray", exprStr) + // Should contain array index + Assert.Contains("[0]", exprStr) + // Should contain property access + Assert.Contains(".c", exprStr) + + [] + let ``Array indexing with variable index produces clean expression`` () = + // Array access with variable index + let q = <@ fun (x: int[]) (i: int) -> x.[i] @> + + let linqExpr = LeafExpressionConverter.QuotationToExpression q + let exprStr = linqExpr.ToString() + + // Should NOT contain GetArray + Assert.DoesNotContain("GetArray", exprStr)