Skip to content

Commit 9649085

Browse files
edgarfgpclaude
andcommitted
Preserve enum type in custom attribute argument of type obj (#995)
An enum assigned to a custom-attribute argument or property of type 'obj' was stored in metadata as its underlying integer, so it round-tripped as an int instead of the enum. Carry the enum type alongside the value (new ILAttribElem.Enum) and encode it with the ECMA-335 enum tag (0x55 + type name) for the boxed-object case, matching what C# emits. Also decode that form (previously threw) and keep encode/decode symmetric so static linking preserves it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d9a937f commit 9649085

9 files changed

Lines changed: 110 additions & 28 deletions

File tree

docs/release-notes/.FSharp.Compiler.Service/11.0.100.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
* Fix `[<return: X>]` prefix attributes being silently dropped on class members, and fix false-positive `AllowMultiple=false` errors when `[<X>]` and `[<return: X>]` 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))
44
* 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))
5+
* 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))
56
* 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))
67
* 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))
78
* 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))

src/Compiler/AbstractIL/il.fs

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,7 @@ type ILAttribElem =
11921192
| Type of ILType option
11931193
| TypeRef of ILTypeRef option
11941194
| Array of ILType * ILAttribElem list
1195+
| Enum of enumType: ILType * value: ILAttribElem
11951196

11961197
type ILAttributeNamedArg = string * ILType * bool * ILAttribElem
11971198

@@ -4882,6 +4883,8 @@ let rec encodeCustomAttrElemTypeForObject x =
48824883
| ILAttribElem.Single _ -> [| et_R4 |]
48834884
| ILAttribElem.Double _ -> [| et_R8 |]
48844885
| ILAttribElem.Array(elemTy, _) -> [| yield et_SZARRAY; yield! encodeCustomAttrElemType elemTy |]
4886+
// An enum boxed in 'object' is encoded as 0x55 followed by the enum type's qualified name.
4887+
| ILAttribElem.Enum(enumTy, _) -> encodeCustomAttrElemType enumTy
48854888

48864889
let parseILVersion (vstr: string) =
48874890
// 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 =
49794982
mkILArr1DTy elemTy, sigptr
49804983
| x when x = 0x50uy -> PrimaryAssemblyILGlobals.typ_Type, sigptr
49814984
| x when x = 0x51uy -> PrimaryAssemblyILGlobals.typ_Object, sigptr // SERIALIZATION_TYPE_TAGGED_OBJECT (ECMA-335 II.23.3)
4985+
| x when x = 0x55uy ->
4986+
// SERIALIZATION_TYPE_ENUM (ECMA-335 II.23.3): the enum type's qualified name follows.
4987+
// Occurs e.g. when an enum is boxed into an 'object'-typed argument.
4988+
let qualifiedName, sigptr = sigptr_get_serstring bytes sigptr
4989+
4990+
let unqualifiedName, rest =
4991+
let pieces = qualifiedName.Split ','
4992+
4993+
if pieces.Length > 1 then
4994+
pieces[0], Some(String.concat "," pieces[1..])
4995+
else
4996+
pieces[0], None
4997+
4998+
let scoref =
4999+
match rest with
5000+
| Some aname -> ILScopeRef.Assembly(ILAssemblyRef.FromAssemblyName(AssemblyName aname))
5001+
| None -> PrimaryAssemblyILGlobals.primaryAssemblyScopeRef
5002+
5003+
ILType.Value(mkILNonGenericTySpec (mkILTyRef (scoref, unqualifiedName))), sigptr
49825004
| _ -> failwithf "decodeCustomAttrElemType ilg: unrecognized custom element type: %A" x
49835005

49845006
/// 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 =
50095031
for elem in elems do
50105032
yield! encodeCustomAttrPrimValue elem
50115033
|]
5034+
// The enum type is captured separately (in the type tag); the value is the underlying integer.
5035+
| ILAttribElem.Enum(_, value) -> encodeCustomAttrPrimValue value
50125036

50135037
and encodeCustomAttrValue ty c =
50145038
match ty, c with
@@ -5355,7 +5379,13 @@ let decodeILAttribData (ca: ILAttribute) =
53555379
ILAttribElem.Null, sigptr
53565380
else
53575381
let ty, sigptr = decodeCustomAttrElemType bytes sigptr et
5358-
parseVal ty sigptr
5382+
let v, sigptr = parseVal ty sigptr
5383+
// Preserve the enum type of a boxed enum so it re-encodes with its 0x55 enum tag
5384+
// (rather than the bare underlying integer) when the attribute is round-tripped,
5385+
// e.g. during static linking. See https://github.com/dotnet/fsharp/issues/995.
5386+
match ty with
5387+
| ILType.Value _ -> ILAttribElem.Enum(ty, v), sigptr
5388+
| _ -> v, sigptr
53595389
| ILType.Array(shape, elemTy) when shape = ILArrayShape.SingleDimensional ->
53605390
let n, sigptr = sigptr_get_i32 bytes sigptr
53615391

@@ -5371,7 +5401,11 @@ let decodeILAttribData (ca: ILAttribute) =
53715401

53725402
let elems, sigptr = parseElems [] n sigptr
53735403
ILAttribElem.Array(elemTy, elems), sigptr
5374-
| ILType.Value _ -> (* assume it is an enumeration *)
5404+
| ILType.Value _ ->
5405+
// Assume an enumeration. Note: the underlying integer width is not present in the
5406+
// blob, so this reads it as int32. Enums with a non-int32 underlying type (byte,
5407+
// int16, int64, ...) are therefore not read correctly here; resolving that would
5408+
// require materializing the enum type, which this blob parser does not do.
53755409
let n, sigptr = sigptr_get_i32 bytes sigptr
53765410
ILAttribElem.Int32 n, sigptr
53775411
| _ -> failwith "decodeILAttribData: attribute data involves an enum or System.Type value"
@@ -5394,29 +5428,9 @@ let decodeILAttribData (ca: ILAttribute) =
53945428
let isPropByte, sigptr = sigptr_get_u8 bytes sigptr
53955429
let isProp = (int isPropByte = 0x54)
53965430
let et, sigptr = sigptr_get_u8 bytes sigptr
5397-
// We have a named value
5398-
let ty, sigptr =
5399-
if ( (* 0x50 = (int et) || *) 0x55 = (int et)) then
5400-
let qualified_tname, sigptr = sigptr_get_serstring bytes sigptr
5401-
5402-
let unqualified_tname, rest =
5403-
let pieces = qualified_tname.Split ','
5404-
5405-
if pieces.Length > 1 then
5406-
pieces[0], Some(String.concat "," pieces[1..])
5407-
else
5408-
pieces[0], None
5409-
5410-
let scoref =
5411-
match rest with
5412-
| Some aname -> ILScopeRef.Assembly(ILAssemblyRef.FromAssemblyName(AssemblyName aname))
5413-
| None -> PrimaryAssemblyILGlobals.primaryAssemblyScopeRef
5414-
5415-
let tref = mkILTyRef (scoref, unqualified_tname)
5416-
let tspec = mkILNonGenericTySpec tref
5417-
ILType.Value tspec, sigptr
5418-
else
5419-
decodeCustomAttrElemType bytes sigptr et
5431+
// We have a named value. The type tag (including the 0x55 enum form) is decoded by
5432+
// decodeCustomAttrElemType.
5433+
let ty, sigptr = decodeCustomAttrElemType bytes sigptr et
54205434

54215435
let nm, sigptr = sigptr_get_serstring bytes sigptr
54225436
let v, sigptr = parseVal ty sigptr

src/Compiler/AbstractIL/il.fsi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,11 @@ type ILAttribElem =
849849
| Type of ILType option
850850
| TypeRef of ILTypeRef option
851851
| Array of ILType * ILAttribElem list
852+
/// Represents an enum value together with its enum type. Used when an enum is stored in a
853+
/// custom-attribute argument of type 'object', so the enum type is preserved in the encoded
854+
/// blob (ECMA-335 II.23.3) instead of being collapsed to its underlying integer. The second
855+
/// element is the underlying integer value (e.g. ILAttribElem.Int32).
856+
| Enum of enumType: ILType * value: ILAttribElem
852857

853858
/// Named args: values and flags indicating if they are fields or properties.
854859
type ILAttributeNamedArg = string * ILType * bool * ILAttribElem

src/Compiler/AbstractIL/ilmorph.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ let rec celem_ty2ty f celem =
166166
| ILAttribElem.Type(Some ty) -> ILAttribElem.Type(Some(f ty))
167167
| ILAttribElem.TypeRef(Some tref) -> ILAttribElem.TypeRef(Some (f (mkILBoxedType (mkILNonGenericTySpec tref))).TypeRef)
168168
| ILAttribElem.Array(elemTy, elems) -> ILAttribElem.Array(f elemTy, List.map (celem_ty2ty f) elems)
169+
| ILAttribElem.Enum(enumTy, value) -> ILAttribElem.Enum(f enumTy, celem_ty2ty f value)
169170
| _ -> celem
170171

171172
let cnamedarg_ty2ty f ((nm, ty, isProp, elem): ILAttributeNamedArg) = (nm, f ty, isProp, celem_ty2ty f elem)

src/Compiler/Checking/AttributeChecking.fs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ let rec private evalILAttribElem elem =
4444
| ILAttribElem.Double x -> box x
4545
| ILAttribElem.Null -> null
4646
| ILAttribElem.Array (_, a) -> box [| for i in a -> evalILAttribElem i |]
47+
// An enum value: evaluate to its underlying integer value (the enum type itself is not materialized here).
48+
| ILAttribElem.Enum (_, value) -> evalILAttribElem value
4749
// TODO: typeof<..> in attribute values
48-
| ILAttribElem.Type (Some _t) -> fail()
50+
| ILAttribElem.Type (Some _t) -> fail()
4951
| ILAttribElem.Type None -> null
5052
| ILAttribElem.TypeRef (Some _t) -> fail()
5153
| ILAttribElem.TypeRef None -> null

src/Compiler/Checking/NicePrint.fs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -644,11 +644,12 @@ module PrintTypes =
644644
| ILAttribElem.Type (Some ty) ->
645645
LeftL.keywordTypeof ^^ SepL.leftAngle ^^ PrintIL.layoutILType denv [] ty ^^ RightL.rightAngle
646646
| ILAttribElem.Type None -> wordL (tagText "")
647-
| ILAttribElem.TypeRef (Some ty) ->
647+
| ILAttribElem.TypeRef (Some ty) ->
648648
LeftL.keywordTypedefof ^^ SepL.leftAngle ^^ PrintIL.layoutILTypeRef denv ty ^^ RightL.rightAngle
649649
| ILAttribElem.TypeRef None -> emptyL
650+
| ILAttribElem.Enum (_, value) -> layoutILAttribElement denv value
650651

651-
and layoutILAttrib denv (ty, args) =
652+
and layoutILAttrib denv (ty, args) =
652653
let argsL = bracketL (sepListL RightL.comma (List.map (layoutILAttribElement denv) args))
653654
PrintIL.layoutILType denv [] ty ++ argsL
654655

src/Compiler/CodeGen/IlxGen.fs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10322,6 +10322,19 @@ and GenAttribArg amap (g: TcGlobals) eenv x (ilArgTy: ILType) =
1032210322
let tynm = ilArgTy.TypeSpec.Name
1032310323
let isobj = (tynm = "System.Object")
1032410324

10325+
// An enum value stored into an 'object'-typed argument must keep its enum type in the
10326+
// custom-attribute blob (ECMA-335 II.23.3), otherwise it round-trips as the underlying
10327+
// integer (e.g. 'Prop = MyEnum.B' surfaces as boxed int32). See
10328+
// https://github.com/dotnet/fsharp/issues/995. The enum type is carried alongside the
10329+
// underlying integer value, which is computed by recursing with the underlying IL type.
10330+
if isobj && isEnumTy g ty then
10331+
let enumIlTy = GenType amap m eenv.tyenv ty
10332+
let underlyingTy = underlyingTypeOfEnumTy g ty
10333+
let underlyingIlTy = GenType amap m eenv.tyenv underlyingTy
10334+
let underlyingElem = GenAttribArg amap g eenv (Expr.Const(c, m, underlyingTy)) underlyingIlTy
10335+
ILAttribElem.Enum(enumIlTy, underlyingElem)
10336+
else
10337+
1032510338
match c with
1032610339
| Const.Bool b -> ILAttribElem.Bool b
1032710340
| Const.Int32 i when isobj || tynm = "System.Int32" -> ILAttribElem.Int32 i

tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/Basic/Basic.fs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ module CustomAttributes_Basic =
3434
|> verifyCompileAndRun
3535
|> shouldSucceed
3636

37+
// Regression for https://github.com/dotnet/fsharp/issues/995
38+
[<Theory; Directory(__SOURCE_DIRECTORY__, Includes=[|"EnumValueAsObjectArg01.fs"|])>]
39+
let ``EnumValueAsObjectArg01_fs`` compilation =
40+
compilation
41+
|> verifyCompileAndRun
42+
|> shouldSucceed
43+
3744
// SOURCE=E_AttributeApplication01.fs # E_AttributeApplication01.fs
3845
[<Theory; Directory(__SOURCE_DIRECTORY__, Includes=[|"E_AttributeApplication01.fs"|])>]
3946
let ``E_AttributeApplication01_fs`` compilation =
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// #Conformance #DeclarationElements #Attributes
2+
// Regression test for https://github.com/dotnet/fsharp/issues/995
3+
// An enum assigned to an attribute argument of type 'obj' must keep its enum type in the
4+
// emitted metadata, instead of being stored as the underlying int32.
5+
6+
open System
7+
8+
type MyAttribute() =
9+
inherit Attribute()
10+
let mutable prop : obj = null
11+
member _.Prop
12+
with get () : obj = prop
13+
and set (value: obj) = prop <- value
14+
15+
type MyEnum =
16+
| A = 1
17+
| B = 2
18+
19+
// An enum with a non-int32 underlying type, to exercise the encoded value width.
20+
type LongEnum =
21+
| P = 1L
22+
| Q = 2L
23+
24+
[<My(Prop = MyEnum.B)>]
25+
type MyClass = class end
26+
27+
[<My(Prop = LongEnum.Q)>]
28+
type MyClassLong = class end
29+
30+
let propOf<'T> () = (typeof<'T>.GetCustomAttributes(false)[0] :?> MyAttribute).Prop
31+
32+
let intProp = propOf<MyClass> ()
33+
if intProp.GetType() <> typeof<MyEnum> then failwith "MyEnum type was lost"
34+
if Convert.ToString(intProp, Globalization.CultureInfo.InvariantCulture) <> "B" then failwith "expected \"B\""
35+
36+
let longProp = propOf<MyClassLong> ()
37+
if longProp.GetType() <> typeof<LongEnum> then failwith "LongEnum type was lost"
38+
if Convert.ToString(longProp, Globalization.CultureInfo.InvariantCulture) <> "Q" then failwith "expected \"Q\""

0 commit comments

Comments
 (0)