From 013cb437ee47cee432aab7c4ff9c942b3dd6e56f Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Sun, 21 Jun 2026 12:42:29 -0500 Subject: [PATCH 1/5] Add PR link to release note --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/AbstractIL/il.fs | 64 +++++++++++-------- src/Compiler/AbstractIL/il.fsi | 5 ++ src/Compiler/AbstractIL/ilmorph.fs | 1 + src/Compiler/Checking/AttributeChecking.fs | 4 +- src/Compiler/Checking/NicePrint.fs | 5 +- src/Compiler/CodeGen/IlxGen.fs | 13 ++++ .../CustomAttributes/Basic/Basic.fs | 7 ++ .../Basic/EnumValueAsObjectArg01.fs | 38 +++++++++++ 9 files changed, 110 insertions(+), 28 deletions(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/Basic/EnumValueAsObjectArg01.fs diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 7e9cd230a4c..f2ca3aea1fe 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -2,6 +2,7 @@ * Fix `[]` prefix attributes being silently dropped on class members, and fix false-positive `AllowMultiple=false` errors when `[]` and `[]` are applied to the same binding. ([Issue #17904](https://github.com/dotnet/fsharp/issues/17904), [Issue #19020](https://github.com/dotnet/fsharp/issues/19020), [PR #19738](https://github.com/dotnet/fsharp/pull/19738)) * Fix attributes on return type of unparenthesized tuple methods being silently dropped from IL. ([Issue #462](https://github.com/dotnet/fsharp/issues/462), [PR #19714](https://github.com/dotnet/fsharp/pull/19714)) +* Fix enum values losing their type when used in a custom attribute argument of type `obj` (they were stored as the underlying integer instead of the enum). ([Issue #995](https://github.com/dotnet/fsharp/issues/995), [PR #19975](https://github.com/dotnet/fsharp/pull/19975)) * Fix internal error FS0073 "Undefined or unsolved type variable" in IlxGen when nested inline SRTP functions with multiple overloads leave unsolved typars in the non-witness codegen path. ([Issue #19709](https://github.com/dotnet/fsharp/issues/19709), [PR #19710](https://github.com/dotnet/fsharp/pull/19710)) * Fix NRE when calling virtual Object methods on value types through inline SRTP functions. ([Issue #8098](https://github.com/dotnet/fsharp/issues/8098), [PR #19511](https://github.com/dotnet/fsharp/pull/19511)) * Fix attributes not resolved from opened namespaces in `namespace rec` / `module rec` scopes. ([Issue #7931](https://github.com/dotnet/fsharp/issues/7931), [PR #19502](https://github.com/dotnet/fsharp/pull/19502)) diff --git a/src/Compiler/AbstractIL/il.fs b/src/Compiler/AbstractIL/il.fs index 5a80b12fbdc..11832ae17ad 100644 --- a/src/Compiler/AbstractIL/il.fs +++ b/src/Compiler/AbstractIL/il.fs @@ -1192,6 +1192,7 @@ type ILAttribElem = | Type of ILType option | TypeRef of ILTypeRef option | Array of ILType * ILAttribElem list + | Enum of enumType: ILType * value: ILAttribElem type ILAttributeNamedArg = string * ILType * bool * ILAttribElem @@ -4882,6 +4883,8 @@ let rec encodeCustomAttrElemTypeForObject x = | ILAttribElem.Single _ -> [| et_R4 |] | ILAttribElem.Double _ -> [| et_R8 |] | ILAttribElem.Array(elemTy, _) -> [| yield et_SZARRAY; yield! encodeCustomAttrElemType elemTy |] + // An enum boxed in 'object' is encoded as 0x55 followed by the enum type's qualified name. + | ILAttribElem.Enum(enumTy, _) -> encodeCustomAttrElemType enumTy let parseILVersion (vstr: string) = // matches "v1.2.3.4" or "1.2.3.4". Note, if numbers are missing, returns -1 (not 0). @@ -4979,6 +4982,25 @@ let rec decodeCustomAttrElemType bytes sigptr x = mkILArr1DTy elemTy, sigptr | x when x = 0x50uy -> PrimaryAssemblyILGlobals.typ_Type, sigptr | x when x = 0x51uy -> PrimaryAssemblyILGlobals.typ_Object, sigptr // SERIALIZATION_TYPE_TAGGED_OBJECT (ECMA-335 II.23.3) + | x when x = 0x55uy -> + // SERIALIZATION_TYPE_ENUM (ECMA-335 II.23.3): the enum type's qualified name follows. + // Occurs e.g. when an enum is boxed into an 'object'-typed argument. + let qualifiedName, sigptr = sigptr_get_serstring bytes sigptr + + let unqualifiedName, rest = + let pieces = qualifiedName.Split ',' + + if pieces.Length > 1 then + pieces[0], Some(String.concat "," pieces[1..]) + else + pieces[0], None + + let scoref = + match rest with + | Some aname -> ILScopeRef.Assembly(ILAssemblyRef.FromAssemblyName(AssemblyName aname)) + | None -> PrimaryAssemblyILGlobals.primaryAssemblyScopeRef + + ILType.Value(mkILNonGenericTySpec (mkILTyRef (scoref, unqualifiedName))), sigptr | _ -> failwithf "decodeCustomAttrElemType ilg: unrecognized custom element type: %A" x /// Given a custom attribute element, encode it to a binary representation according to the rules in Ecma 335 Partition II. @@ -5009,6 +5031,8 @@ let rec encodeCustomAttrPrimValue c = for elem in elems do yield! encodeCustomAttrPrimValue elem |] + // The enum type is captured separately (in the type tag); the value is the underlying integer. + | ILAttribElem.Enum(_, value) -> encodeCustomAttrPrimValue value and encodeCustomAttrValue ty c = match ty, c with @@ -5355,7 +5379,13 @@ let decodeILAttribData (ca: ILAttribute) = ILAttribElem.Null, sigptr else let ty, sigptr = decodeCustomAttrElemType bytes sigptr et - parseVal ty sigptr + let v, sigptr = parseVal ty sigptr + // Preserve the enum type of a boxed enum so it re-encodes with its 0x55 enum tag + // (rather than the bare underlying integer) when the attribute is round-tripped, + // e.g. during static linking. See https://github.com/dotnet/fsharp/issues/995. + match ty with + | ILType.Value _ -> ILAttribElem.Enum(ty, v), sigptr + | _ -> v, sigptr | ILType.Array(shape, elemTy) when shape = ILArrayShape.SingleDimensional -> let n, sigptr = sigptr_get_i32 bytes sigptr @@ -5371,7 +5401,11 @@ let decodeILAttribData (ca: ILAttribute) = let elems, sigptr = parseElems [] n sigptr ILAttribElem.Array(elemTy, elems), sigptr - | ILType.Value _ -> (* assume it is an enumeration *) + | ILType.Value _ -> + // Assume an enumeration. Note: the underlying integer width is not present in the + // blob, so this reads it as int32. Enums with a non-int32 underlying type (byte, + // int16, int64, ...) are therefore not read correctly here; resolving that would + // require materializing the enum type, which this blob parser does not do. let n, sigptr = sigptr_get_i32 bytes sigptr ILAttribElem.Int32 n, sigptr | _ -> failwith "decodeILAttribData: attribute data involves an enum or System.Type value" @@ -5394,29 +5428,9 @@ let decodeILAttribData (ca: ILAttribute) = let isPropByte, sigptr = sigptr_get_u8 bytes sigptr let isProp = (int isPropByte = 0x54) let et, sigptr = sigptr_get_u8 bytes sigptr - // We have a named value - let ty, sigptr = - if ( (* 0x50 = (int et) || *) 0x55 = (int et)) then - let qualified_tname, sigptr = sigptr_get_serstring bytes sigptr - - let unqualified_tname, rest = - let pieces = qualified_tname.Split ',' - - if pieces.Length > 1 then - pieces[0], Some(String.concat "," pieces[1..]) - else - pieces[0], None - - let scoref = - match rest with - | Some aname -> ILScopeRef.Assembly(ILAssemblyRef.FromAssemblyName(AssemblyName aname)) - | None -> PrimaryAssemblyILGlobals.primaryAssemblyScopeRef - - let tref = mkILTyRef (scoref, unqualified_tname) - let tspec = mkILNonGenericTySpec tref - ILType.Value tspec, sigptr - else - decodeCustomAttrElemType bytes sigptr et + // We have a named value. The type tag (including the 0x55 enum form) is decoded by + // decodeCustomAttrElemType. + let ty, sigptr = decodeCustomAttrElemType bytes sigptr et let nm, sigptr = sigptr_get_serstring bytes sigptr let v, sigptr = parseVal ty sigptr diff --git a/src/Compiler/AbstractIL/il.fsi b/src/Compiler/AbstractIL/il.fsi index 3539e235518..da816c5b02b 100644 --- a/src/Compiler/AbstractIL/il.fsi +++ b/src/Compiler/AbstractIL/il.fsi @@ -849,6 +849,11 @@ type ILAttribElem = | Type of ILType option | TypeRef of ILTypeRef option | Array of ILType * ILAttribElem list + /// Represents an enum value together with its enum type. Used when an enum is stored in a + /// custom-attribute argument of type 'object', so the enum type is preserved in the encoded + /// blob (ECMA-335 II.23.3) instead of being collapsed to its underlying integer. The second + /// element is the underlying integer value (e.g. ILAttribElem.Int32). + | Enum of enumType: ILType * value: ILAttribElem /// Named args: values and flags indicating if they are fields or properties. type ILAttributeNamedArg = string * ILType * bool * ILAttribElem diff --git a/src/Compiler/AbstractIL/ilmorph.fs b/src/Compiler/AbstractIL/ilmorph.fs index 31adf4a364a..9b0a0ecb768 100644 --- a/src/Compiler/AbstractIL/ilmorph.fs +++ b/src/Compiler/AbstractIL/ilmorph.fs @@ -166,6 +166,7 @@ let rec celem_ty2ty f celem = | ILAttribElem.Type(Some ty) -> ILAttribElem.Type(Some(f ty)) | ILAttribElem.TypeRef(Some tref) -> ILAttribElem.TypeRef(Some (f (mkILBoxedType (mkILNonGenericTySpec tref))).TypeRef) | ILAttribElem.Array(elemTy, elems) -> ILAttribElem.Array(f elemTy, List.map (celem_ty2ty f) elems) + | ILAttribElem.Enum(enumTy, value) -> ILAttribElem.Enum(f enumTy, celem_ty2ty f value) | _ -> celem let cnamedarg_ty2ty f ((nm, ty, isProp, elem): ILAttributeNamedArg) = (nm, f ty, isProp, celem_ty2ty f elem) diff --git a/src/Compiler/Checking/AttributeChecking.fs b/src/Compiler/Checking/AttributeChecking.fs index a7352914720..8a4baf1eba0 100755 --- a/src/Compiler/Checking/AttributeChecking.fs +++ b/src/Compiler/Checking/AttributeChecking.fs @@ -44,8 +44,10 @@ let rec private evalILAttribElem elem = | ILAttribElem.Double x -> box x | ILAttribElem.Null -> null | ILAttribElem.Array (_, a) -> box [| for i in a -> evalILAttribElem i |] + // An enum value: evaluate to its underlying integer value (the enum type itself is not materialized here). + | ILAttribElem.Enum (_, value) -> evalILAttribElem value // TODO: typeof<..> in attribute values - | ILAttribElem.Type (Some _t) -> fail() + | ILAttribElem.Type (Some _t) -> fail() | ILAttribElem.Type None -> null | ILAttribElem.TypeRef (Some _t) -> fail() | ILAttribElem.TypeRef None -> null diff --git a/src/Compiler/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index de8daffa7c5..1cff6256eda 100644 --- a/src/Compiler/Checking/NicePrint.fs +++ b/src/Compiler/Checking/NicePrint.fs @@ -644,11 +644,12 @@ module PrintTypes = | ILAttribElem.Type (Some ty) -> LeftL.keywordTypeof ^^ SepL.leftAngle ^^ PrintIL.layoutILType denv [] ty ^^ RightL.rightAngle | ILAttribElem.Type None -> wordL (tagText "") - | ILAttribElem.TypeRef (Some ty) -> + | ILAttribElem.TypeRef (Some ty) -> LeftL.keywordTypedefof ^^ SepL.leftAngle ^^ PrintIL.layoutILTypeRef denv ty ^^ RightL.rightAngle | ILAttribElem.TypeRef None -> emptyL + | ILAttribElem.Enum (_, value) -> layoutILAttribElement denv value - and layoutILAttrib denv (ty, args) = + and layoutILAttrib denv (ty, args) = let argsL = bracketL (sepListL RightL.comma (List.map (layoutILAttribElement denv) args)) PrintIL.layoutILType denv [] ty ++ argsL diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index 939e3da324e..0de59ec748a 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -10322,6 +10322,19 @@ and GenAttribArg amap (g: TcGlobals) eenv x (ilArgTy: ILType) = let tynm = ilArgTy.TypeSpec.Name let isobj = (tynm = "System.Object") + // An enum value stored into an 'object'-typed argument must keep its enum type in the + // custom-attribute blob (ECMA-335 II.23.3), otherwise it round-trips as the underlying + // integer (e.g. 'Prop = MyEnum.B' surfaces as boxed int32). See + // https://github.com/dotnet/fsharp/issues/995. The enum type is carried alongside the + // underlying integer value, which is computed by recursing with the underlying IL type. + if isobj && isEnumTy g ty then + let enumIlTy = GenType amap m eenv.tyenv ty + let underlyingTy = underlyingTypeOfEnumTy g ty + let underlyingIlTy = GenType amap m eenv.tyenv underlyingTy + let underlyingElem = GenAttribArg amap g eenv (Expr.Const(c, m, underlyingTy)) underlyingIlTy + ILAttribElem.Enum(enumIlTy, underlyingElem) + else + match c with | Const.Bool b -> ILAttribElem.Bool b | Const.Int32 i when isobj || tynm = "System.Int32" -> ILAttribElem.Int32 i diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/Basic/Basic.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/Basic/Basic.fs index 93148810451..c142c92dcd3 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/Basic/Basic.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/Basic/Basic.fs @@ -34,6 +34,13 @@ module CustomAttributes_Basic = |> verifyCompileAndRun |> shouldSucceed + // Regression for https://github.com/dotnet/fsharp/issues/995 + [] + let ``EnumValueAsObjectArg01_fs`` compilation = + compilation + |> verifyCompileAndRun + |> shouldSucceed + // SOURCE=E_AttributeApplication01.fs # E_AttributeApplication01.fs [] let ``E_AttributeApplication01_fs`` compilation = diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/Basic/EnumValueAsObjectArg01.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/Basic/EnumValueAsObjectArg01.fs new file mode 100644 index 00000000000..44e23d7ff42 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/Basic/EnumValueAsObjectArg01.fs @@ -0,0 +1,38 @@ +// #Conformance #DeclarationElements #Attributes +// Regression test for https://github.com/dotnet/fsharp/issues/995 +// An enum assigned to an attribute argument of type 'obj' must keep its enum type in the +// emitted metadata, instead of being stored as the underlying int32. + +open System + +type MyAttribute() = + inherit Attribute() + let mutable prop : obj = null + member _.Prop + with get () : obj = prop + and set (value: obj) = prop <- value + +type MyEnum = + | A = 1 + | B = 2 + +// An enum with a non-int32 underlying type, to exercise the encoded value width. +type LongEnum = + | P = 1L + | Q = 2L + +[] +type MyClass = class end + +[] +type MyClassLong = class end + +let propOf<'T> () = (typeof<'T>.GetCustomAttributes(false)[0] :?> MyAttribute).Prop + +let intProp = propOf () +if intProp.GetType() <> typeof then failwith "MyEnum type was lost" +if Convert.ToString(intProp, Globalization.CultureInfo.InvariantCulture) <> "B" then failwith "expected \"B\"" + +let longProp = propOf () +if longProp.GetType() <> typeof then failwith "LongEnum type was lost" +if Convert.ToString(longProp, Globalization.CultureInfo.InvariantCulture) <> "Q" then failwith "expected \"Q\"" From 7c298d0324362b65f933b42eff9ae45c5725e614 Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Sun, 21 Jun 2026 13:03:28 -0500 Subject: [PATCH 2/5] Remove unrelated whitespace changes --- src/Compiler/Checking/AttributeChecking.fs | 2 +- src/Compiler/Checking/NicePrint.fs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Compiler/Checking/AttributeChecking.fs b/src/Compiler/Checking/AttributeChecking.fs index f9d54310a91..87621466329 100755 --- a/src/Compiler/Checking/AttributeChecking.fs +++ b/src/Compiler/Checking/AttributeChecking.fs @@ -47,7 +47,7 @@ let rec private evalILAttribElem elem = // An enum value: evaluate to its underlying integer value (the enum type itself is not materialized here). | ILAttribElem.Enum (_, value) -> evalILAttribElem value // TODO: typeof<..> in attribute values - | ILAttribElem.Type (Some _t) -> fail() + | ILAttribElem.Type (Some _t) -> fail() | ILAttribElem.Type None -> null | ILAttribElem.TypeRef (Some _t) -> fail() | ILAttribElem.TypeRef None -> null diff --git a/src/Compiler/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index 1cff6256eda..2f09053367a 100644 --- a/src/Compiler/Checking/NicePrint.fs +++ b/src/Compiler/Checking/NicePrint.fs @@ -644,12 +644,12 @@ module PrintTypes = | ILAttribElem.Type (Some ty) -> LeftL.keywordTypeof ^^ SepL.leftAngle ^^ PrintIL.layoutILType denv [] ty ^^ RightL.rightAngle | ILAttribElem.Type None -> wordL (tagText "") - | ILAttribElem.TypeRef (Some ty) -> + | ILAttribElem.TypeRef (Some ty) -> LeftL.keywordTypedefof ^^ SepL.leftAngle ^^ PrintIL.layoutILTypeRef denv ty ^^ RightL.rightAngle | ILAttribElem.TypeRef None -> emptyL | ILAttribElem.Enum (_, value) -> layoutILAttribElement denv value - and layoutILAttrib denv (ty, args) = + and layoutILAttrib denv (ty, args) = let argsL = bracketL (sepListL RightL.comma (List.map (layoutILAttribElement denv) args)) PrintIL.layoutILType denv [] ty ++ argsL From fa334198eac982d5f5d260c6c9fb6864cdc5fba6 Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Sun, 21 Jun 2026 13:58:20 -0500 Subject: [PATCH 3/5] Fix formatting and add quotation regression test for enum preservation --- src/Compiler/CodeGen/IlxGen.fs | 28 ++++++++++--------- .../FSharpQuotations.fs | 14 ++++++++++ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index ea7513e0050..4cf4f2633b3 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -10387,24 +10387,26 @@ and GenAttribArg amap (g: TcGlobals) eenv x (ilArgTy: ILType) = // Detect 'null' used for an array argument | Expr.Const(Const.Zero, _, _), ILType.Array _ -> ILAttribElem.Null + // An enum value stored into an 'object'-typed argument must keep its enum type in the + // custom-attribute blob (ECMA-335 II.23.3), otherwise it round-trips as the underlying + // integer (e.g. 'Prop = MyEnum.B' surfaces as boxed int32). See + // https://github.com/dotnet/fsharp/issues/995. The enum type is carried alongside the + // underlying integer value, which is computed by recursing with the underlying IL type. + | Expr.Const(c, m, ty), _ when ilArgTy.TypeSpec.Name = "System.Object" && isEnumTy g ty -> + let enumIlTy = GenType amap m eenv.tyenv ty + let underlyingTy = underlyingTypeOfEnumTy g ty + let underlyingIlTy = GenType amap m eenv.tyenv underlyingTy + + let underlyingElem = + GenAttribArg amap g eenv (Expr.Const(c, m, underlyingTy)) underlyingIlTy + + ILAttribElem.Enum(enumIlTy, underlyingElem) + // Detect standard constants | Expr.Const(c, m, ty), _ -> let tynm = ilArgTy.TypeSpec.Name let isobj = (tynm = "System.Object") - // An enum value stored into an 'object'-typed argument must keep its enum type in the - // custom-attribute blob (ECMA-335 II.23.3), otherwise it round-trips as the underlying - // integer (e.g. 'Prop = MyEnum.B' surfaces as boxed int32). See - // https://github.com/dotnet/fsharp/issues/995. The enum type is carried alongside the - // underlying integer value, which is computed by recursing with the underlying IL type. - if isobj && isEnumTy g ty then - let enumIlTy = GenType amap m eenv.tyenv ty - let underlyingTy = underlyingTypeOfEnumTy g ty - let underlyingIlTy = GenType amap m eenv.tyenv underlyingTy - let underlyingElem = GenAttribArg amap g eenv (Expr.Const(c, m, underlyingTy)) underlyingIlTy - ILAttribElem.Enum(enumIlTy, underlyingElem) - else - match c with | Const.Bool b -> ILAttribElem.Bool b | Const.Int32 i when isobj || tynm = "System.Int32" -> ILAttribElem.Int32 i 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 8be4cb639c9..bec73a21957 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 @@ -18,6 +18,10 @@ type E = Microsoft.FSharp.Quotations.Expr;; type StaticIndexedPropertyTest() = static member IdxProp with get (n : int) = n + 1 +type QuotationEnum = + | A = 1 + | B = 2 + module Check = let argumentException f = let mutable ex = false @@ -103,6 +107,16 @@ type FSharpQuotationsTests() = | NewTuple [ Value(:? int as i, _) ; Value(:? string as s, _) ] when i = 1 && s = "" -> () | _ -> Assert.Fail() + [] + member x.``Quotation of an enum value preserves the enum type`` () = + // Related to https://github.com/dotnet/fsharp/issues/995: an enum literal is quoted as a + // Value node carrying the enum type, not the bare underlying integer. + match <@ QuotationEnum.B @> with + | Value(v, t) -> + Assert.Equal(typeof, t) + Assert.Equal(box QuotationEnum.B, v) + | _ -> Assert.Fail() + [] member x.``NewTuple literal should not be recognized by NewStructTuple active pattern`` () = match <@ (1, "") @> with From 21727a7b6e51ef5c6739701b081987079b192770 Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Sun, 21 Jun 2026 14:15:18 -0500 Subject: [PATCH 4/5] Add cross-language test: C#-defined enum in an F# attribute arg of type obj --- .../CustomAttributes/Basic/Basic.fs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/Basic/Basic.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/Basic/Basic.fs index c142c92dcd3..dde97126f13 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/Basic/Basic.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/Basic/Basic.fs @@ -41,6 +41,41 @@ module CustomAttributes_Basic = |> verifyCompileAndRun |> shouldSucceed + // Cross-language: the same scenario as EnumValueAsObjectArg01.fs, but with the enum and the + // attribute defined in C#. See https://github.com/dotnet/fsharp/issues/995. + [] + let ``Enum defined in C# used in an F# attribute arg of type obj keeps its type`` () = + let csLib = + CSharp """ +namespace CSharpLib +{ + public enum MyEnum { A = 1, B = 2 } + + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute : System.Attribute + { + public object Prop { get; set; } + } +} +""" + |> withName "CSharpLib" + + FSharp """ +module Test +open System +open CSharpLib + +[] +type MyClass = class end + +let prop = (typeof.GetCustomAttributes(false)[0] :?> MyAttribute).Prop +if prop.GetType() <> typeof then failwith "enum type was lost" +if Convert.ToString(prop, Globalization.CultureInfo.InvariantCulture) <> "B" then failwith "expected \"B\"" +""" + |> withReferences [csLib] + |> compileExeAndRun + |> shouldSucceed + // SOURCE=E_AttributeApplication01.fs # E_AttributeApplication01.fs [] let ``E_AttributeApplication01_fs`` compilation = From a3110005880915785b7e893072fb216f23312bef Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Sun, 21 Jun 2026 15:12:39 -0500 Subject: [PATCH 5/5] Update FCS surface area baseline for ILAttribElem.Enum --- ...FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index 4034ba8064d..66e746d1e68 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -144,6 +144,10 @@ FSharp.Compiler.AbstractIL.IL+ILAttribElem+Char: Char Item FSharp.Compiler.AbstractIL.IL+ILAttribElem+Char: Char get_Item() FSharp.Compiler.AbstractIL.IL+ILAttribElem+Double: Double Item FSharp.Compiler.AbstractIL.IL+ILAttribElem+Double: Double get_Item() +FSharp.Compiler.AbstractIL.IL+ILAttribElem+Enum: ILAttribElem get_value() +FSharp.Compiler.AbstractIL.IL+ILAttribElem+Enum: ILAttribElem value +FSharp.Compiler.AbstractIL.IL+ILAttribElem+Enum: ILType enumType +FSharp.Compiler.AbstractIL.IL+ILAttribElem+Enum: ILType get_enumType() FSharp.Compiler.AbstractIL.IL+ILAttribElem+Int16: Int16 Item FSharp.Compiler.AbstractIL.IL+ILAttribElem+Int16: Int16 get_Item() FSharp.Compiler.AbstractIL.IL+ILAttribElem+Int32: Int32 Item @@ -161,6 +165,7 @@ FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Bool FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Byte FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Char FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Double +FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Enum FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Int16 FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Int32 FSharp.Compiler.AbstractIL.IL+ILAttribElem+Tags: Int32 Int64 @@ -192,6 +197,7 @@ FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsBool FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsByte FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsChar FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsDouble +FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsEnum FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsInt16 FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsInt32 FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean IsInt64 @@ -209,6 +215,7 @@ FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsBool() FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsByte() FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsChar() FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsDouble() +FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsEnum() FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsInt16() FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsInt32() FSharp.Compiler.AbstractIL.IL+ILAttribElem: Boolean get_IsInt64() @@ -226,6 +233,7 @@ FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttr FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Byte FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Char FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Double +FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Enum FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Int16 FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Int32 FSharp.Compiler.AbstractIL.IL+ILAttribElem: FSharp.Compiler.AbstractIL.IL+ILAttribElem+Int64 @@ -243,6 +251,7 @@ FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewBool(Boolean) FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewByte(Byte) FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewChar(Char) FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewDouble(Double) +FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewEnum(ILType, ILAttribElem) FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewInt16(Int16) FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewInt32(Int32) FSharp.Compiler.AbstractIL.IL+ILAttribElem: ILAttribElem NewInt64(Int64)