Record constructors (FS-1073)#19974
Conversation
Records compile to a class whose all-fields constructor is callable from C#
(new MyRecord(a, b)) but not from F#, which only allows { Field = ... } syntax.
This adds a RecordConstructorSyntax preview feature that surfaces that constructor
to F# too, supporting positional and named arguments.
Implemented via a new MethInfo.RecdAllFieldsCtor case surfaced by InfoReader for
record tycons; it elaborates through the existing mkRecordExpr path, so there is
no codegen or overload-resolution change. Accessibility mirrors { } construction
(the ctor is no more accessible than the record's representation/fields), so the
C# behaviour of a public IL constructor bypassing a private record is not inherited.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
|
Shorter name; a record has exactly one synthesized constructor, so the 'AllFields' qualifier is not needed to disambiguate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| LanguageFeature.MethodOverloadsCache, previewVersion // Performance optimization for overload resolution | ||
| LanguageFeature.ImplicitDIMCoverage, languageVersion110 | ||
| LanguageFeature.ErrorOnMissingSignatureAttribute, previewVersion // Opt-in: turn FS3888 from warning into error | ||
| LanguageFeature.RecordConstructorSyntax, previewVersion // Allow constructing a record via its all-fields constructor, e.g. MyRecord(a, b) |
There was a problem hiding this comment.
Can you pls also add tests to https://github.com/dotnet/fsharp/tree/main/tests/projects/CompilerCompat project to see how this works with two dependent projects, one compiled with the feature and other without (both directions).
Since the codegen has been always there and this feature did not alter pickled representation, it should work as long as the consuming project has the LanguageFeature enabled, independent on what the type-authoring project had.
What about the opposite direction, type-authoring project exposes records and also inline functions with the record constructions. And consuming project is on an older compiler. Will the consuming project work with the inlined calls correctly?
There was a problem hiding this comment.
Both directions work because the pickled representations stay the same.
This is verified in the this commit but I don't like the code quality here. The new code adds complexity, the existing structure isn't designed to handle it, and overall I don't think it's maintainable. I suggest I revert it and write a clear argument here instead about why it will work.
There was a problem hiding this comment.
Its not that bad - the define can be generalized (so that there isn't one per feature eventually) and when the comments are removed, it is just a few lines.
Thanks for adding it 👍
The feature surfaces only the all-fields constructor. A struct record's zero-init default and a [<CLIMutable>] record's IL parameterless .ctor must remain non-callable from F#; both 'Point()' and 'R()' are rejected (FS0501). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…terless ctor A struct record's 'Point()' is default (zero) initialization, not a real constructor; only [<CLIMutable>] emits an actual parameterless .ctor. Name the two tests accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rangeOfMethInfo had no RecdCtor case, so it fell through to ArbitraryValRef (None) and GoToDefinition on a positional record-constructor call navigated nowhere. Add a RecdCtor arm returning the record type's range, mirroring DefaultStructCtor. Adds FSharp.Compiler.Service.Tests smoke tests: go-to-definition lands on the record type, the tooltip mentions the type, and find-all-references links the constructor call to the type declaration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regression guard: the positional constructor has no field labels, so a [<RequireQualifiedAccess>] record must construct without a spurious diagnostic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
🔍 Tooling Safety Check — Affects-Compiler-Output
|
…073)
Library exposes a record and an inline constructor function; the app constructs
records positionally and via the inline function. The new syntax is gated behind
RECORD_CTOR_FEATURE / --langversion:preview for local builds, with a classic { }
fallback so SDK-compiler scenarios still build. Both branches elaborate to the
same record-allocation node, so the pickled representation is unchanged and the
matrix exercises both directions (feature-enabled consumer, older consumer).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The constructor is not a declared member, so it rides on the record's
representation visibility through a signature: available when the .fsi exposes
the representation, rejected (FS1133, like { }) when the .fsi hides it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| <!-- When built with the local compiler, exercise the FS-1073 positional record constructor (preview). --> | ||
| <PropertyGroup Condition="'$(LoadLocalFSharpBuild)' == 'True'"> | ||
| <LangVersion>preview</LangVersion> | ||
| <DefineConstants>$(DefineConstants);RECORD_CTOR_FEATURE</DefineConstants> |
There was a problem hiding this comment.
Can you please generalize the define to USES_PREVIEW_COMPILER ?
| else | ||
| printfn "SUCCESS: All compiler compatibility tests passed" | ||
| 0 | ||
| // FS-1073 record-constructor cross-compiler checks. |
There was a problem hiding this comment.
Please get rid of all the comments, then this addition gets fairly compact 👍
…ents Per review: rename the per-feature RECORD_CTOR_FEATURE define to the reusable USES_PREVIEW_COMPILER, and remove the explanatory comments so the addition is compact. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements FS-1073. F# records compile to a class whose all-fields constructor is callable from C# (
new MyRecord(a, b)) but not from F#, which only permits{ Field = … }. This adds aRecordConstructorSyntaxpreview language feature that surfaces that constructor to F#, with positional and named arguments.Notes:
{ }construction, so aprivate/internalrecord's representation is not bypassed (unlike C#'s public IL constructor).--langversion:preview.