Skip to content

Preserve enum type in custom attribute argument of type obj (#995)#19975

Open
edgarfgp wants to merge 18 commits into
dotnet:mainfrom
edgarfgp:fix-995-enum-attr-obj
Open

Preserve enum type in custom attribute argument of type obj (#995)#19975
edgarfgp wants to merge 18 commits into
dotnet:mainfrom
edgarfgp:fix-995-enum-attr-obj

Conversation

@edgarfgp

@edgarfgp edgarfgp commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Problem

Assigning an enum to a custom attribute argument of type obj loses the enum type. The value is stored in metadata as the underlying int32, so reading the attribute back gives a plain int rather than the enum. C# stores the same thing as the enum, so this is also a cross-language inconsistency.

Fixes #995.

Before

open System

type MyAttribute() =
    inherit Attribute()
    let mutable prop : obj = null
    member _.Prop with get () : obj = prop and set (v: obj) = prop <- v

type MyEnum = A = 1 | B = 2

[<My(Prop = MyEnum.B)>]
type C = class end

let v = (typeof<C>.GetCustomAttributes(false)[0] :?> MyAttribute).Prop
v.GetType()                // System.Int32
Convert.ToString v         // "2"

After

v.GetType()                // MyEnum
Convert.ToString v         // "B"

The enum type is now written into the attribute blob using the ECMA-335 enum tag (0x55 + type name), the same encoding C# produces, so it round-trips as the enum. Reading that encoding back (which previously threw) is also handled, and decode/encode are kept symmetric so static linking preserves it.

Quotations

The original triage worried this needed quotation/TAST changes. It doesn't: the fix only reads the enum type (already on the Const node) when emitting the attribute. Quotations are untouched and already keep the enum type added a test to confirm.

@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/11.0.100.md

@edgarfgp edgarfgp force-pushed the fix-995-enum-attr-obj branch from 9649085 to 013cb43 Compare June 21, 2026 17:45
@github-actions github-actions Bot added the ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen label Jun 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Compiler-Output
Affects-Compiler-Output: changes custom attribute blob encoding (enum 0x55 tag in il.fs/IlxGen.fs)

Generated by PR Tooling Safety Check · opus46 3.2M ·

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This review was generated by AI (@expert-reviewer agent). Findings may contain inaccuracies — please verify independently.

Nice, targeted fix for #995 — the ILAttribElem.Enum carrier plus the symmetric encode tag (0x55 + type name) are the right model, and consolidating the 0x55 decode into decodeCustomAttrElemType removes the duplicated named-arg branch. Two correctness concerns about the decode side, both in decodeILAttribData/parseVal in src/Compiler/AbstractIL/il.fs — see inline comments.

Summary:

  1. The encode path now writes the enum's true underlying width, but the decode fallback still reads a fixed int32, so boxed enums whose underlying type isn't int32 (e.g. the LongEnum/int64 case this PR adds) don't round-trip through decodeILAttribData and can desync the blob pointer for any following named args. The runtime test passes only because the CLR — not decodeILAttribData — reads the metadata.
  2. The new ILType.Value _ -> ILAttribElem.Enum(ty, v) wrap in the System.Object branch also fires for boxed primitives (int/bool/char/byte…), not just enums, mislabeling them as Enum on decode.

Comment thread src/Compiler/AbstractIL/il.fs Outdated
Comment thread src/Compiler/AbstractIL/il.fs Outdated
@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Jun 25, 2026
@T-Gro T-Gro self-requested a review June 25, 2026 08:49
edgarfgp added 3 commits June 25, 2026 21:17
Boxed primitives (et_I4, et_BOOLEAN, ...) also decode to ILType.Value, so the
previous match wrapped them as Enum too. Gate the wrap on the 0x55 enum tag so
only real enums become ILAttribElem.Enum; primitives stay as their primitive
element. Addresses PR review feedback.
Comment thread src/Compiler/AbstractIL/il.fs Outdated
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is this code path exercised?
Pls demonstrate with a test cases that uses long-backed enum and also has value(s) which would not fit an int32.

Comment thread src/Compiler/AbstractIL/il.fs Outdated
Comment thread src/Compiler/AbstractIL/il.fs
// 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 ->

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have a function for checking ilArgTy being object instead of a vanilla name check?

edgarfgp added 2 commits June 30, 2026 12:36
- GenAttribArg: check ilArgTy against g.ilg.typ_Object (both the enum guard and the isobj
  binding) instead of a string name check.
- decodeILAttribData: deduplicate the enum-decode comments (single source for the int32 note).
edgarfgp added 2 commits June 30, 2026 21:36
ilArgTy = g.ilg.typ_Object is not structurally equal to a System.Object arg
imported from a referenced assembly (different scope), so string/const args to
object-typed attribute members from external attributes (e.g. NUnit's
[<TestCase(...)>]) hit the "may not be used as a custom attribute value" internal
error. Use the scope-agnostic name check, as the surrounding code does.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen AI-reviewed PR reviewed by AI review council

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

Enum type is lost when used in attribute

2 participants