Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ConsumePlugin/ConsumePlugin.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<Compile Include="GeneratedRecord.fs">
<MyriadFile>RecordFile.fs</MyriadFile>
</Compile>
<Compile Include="RecordConstructor.fs"/>
<Compile Include="GeneratedRecordConstructor.fs">
<MyriadFile>RecordConstructor.fs</MyriadFile>
</Compile>
<Compile Include="JsonRecord.fs"/>
<Compile Include="GeneratedJson.fs">
<MyriadFile>JsonRecord.fs</MyriadFile>
Expand Down
162 changes: 162 additions & 0 deletions ConsumePlugin/GeneratedRecordConstructor.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
//------------------------------------------------------------------------------
// This code was generated by myriad.
// Changes to this file will be lost when the code is regenerated.
//------------------------------------------------------------------------------
namespace ConsumePlugin

open System
open WoofWare.Myriad.Plugins

/// Functions for constructing the MixedConstructorRecord record.
[<RequireQualifiedAccess; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module MixedConstructorRecord =
/// Create a value of type MixedConstructorRecord with every optional field set to None.
let create (arg_0: string) (arg_1: string) (arg_2: string) : MixedConstructorRecord =
{ FirstRequired = arg_0
FirstOptional = None
SecondRequired = arg_1
SecondOptional = None
ThirdRequired = arg_2 }
namespace ConsumePlugin

open System
open WoofWare.Myriad.Plugins

/// Functions for constructing the AllRequiredConstructorRecord record.
[<RequireQualifiedAccess; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module AllRequiredConstructorRecord =
/// Create a value of type AllRequiredConstructorRecord with every optional field set to None.
let create (arg_0: string) (arg_1: string) (arg_2: string) : AllRequiredConstructorRecord =
{ First = arg_0
Second = arg_1
Third = arg_2 }
namespace ConsumePlugin

open System
open WoofWare.Myriad.Plugins

/// Functions for constructing the AllOptionalConstructorRecord record.
[<RequireQualifiedAccess; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module AllOptionalConstructorRecord =
/// Create a value of type AllOptionalConstructorRecord with every optional field set to None.
let create () : AllOptionalConstructorRecord = { Number = None; Text = None }
namespace ConsumePlugin

open System
open WoofWare.Myriad.Plugins

/// Functions for constructing the NestedOptionConstructorRecord record.
[<RequireQualifiedAccess; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module NestedOptionConstructorRecord =
/// Create a value of type NestedOptionConstructorRecord with every optional field set to None.
let create (arg_0: Guid) : NestedOptionConstructorRecord =
{ Id = arg_0
PostfixNested = None
PrefixOption = None }
namespace ConsumePlugin

open System
open WoofWare.Myriad.Plugins

/// Functions for constructing the GenericConstructorRecord record.
[<RequireQualifiedAccess; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module GenericConstructorRecord =
/// Create a value of type GenericConstructorRecord with every optional field set to None.
let create (arg_0: 'required) (arg_1: 'required list) : GenericConstructorRecord<'required, 'optional> =
{ Required = arg_0
Optional = None
RequiredList = arg_1 }
namespace ConsumePlugin

open System
open WoofWare.Myriad.Plugins

/// Functions for constructing the GenericAllOptionalConstructorRecord record.
[<RequireQualifiedAccess; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module GenericAllOptionalConstructorRecord =
/// Create a value of type GenericAllOptionalConstructorRecord with every optional field set to None.
let create () : GenericAllOptionalConstructorRecord<'value> = { Value = None }
namespace ConsumePlugin

open System
open WoofWare.Myriad.Plugins

/// Functions for constructing the InternalConstructorRecord record.
[<RequireQualifiedAccess; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module internal InternalConstructorRecord =
/// Create a value of type InternalConstructorRecord with every optional field set to None.
let create (arg_0: string) : InternalConstructorRecord = { Required = arg_0; Optional = None }
namespace ConsumePlugin

open System
open WoofWare.Myriad.Plugins

/// Functions for constructing the TupleAndFunctionConstructorRecord record.
[<RequireQualifiedAccess; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module TupleAndFunctionConstructorRecord =
/// Create a value of type TupleAndFunctionConstructorRecord with every optional field set to None.
let create (arg_0: int * string) (arg_1: int -> string) : TupleAndFunctionConstructorRecord =
{ Pair = arg_0
Transform = arg_1
OptionalTransform = None }
namespace ConsumePlugin

open System
open WoofWare.Myriad.Plugins

/// Functions for constructing the NonSyntacticOptionConstructorRecord record.
[<RequireQualifiedAccess; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module NonSyntacticOptionConstructorRecord =
/// Create a value of type NonSyntacticOptionConstructorRecord with every optional field set to None.
let create
(arg_0: ConstructorOptionAlias<int>)
(arg_1: string voption)
(arg_2: Nullable<int>)
: NonSyntacticOptionConstructorRecord =
{ Alias = arg_0
ValueOption = arg_1
Nullable = arg_2
Optional = None }
namespace ConsumePlugin

open System
open WoofWare.Myriad.Plugins

/// Functions for constructing the NameHazardConstructorRecord record.
[<RequireQualifiedAccess; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module NameHazardConstructorRecord =
/// Create a value of type NameHazardConstructorRecord with every optional field set to None.
let create (arg_0: string) (arg_1: string) (arg_2: string) (arg_3: string) : NameHazardConstructorRecord =
{ ``type`` = arg_0
arg_0 = arg_1
Arg_0 = arg_2
``two words`` = arg_3
``two-words`` = None }
namespace ConsumePlugin.FirstScopedOpen

open RecordConstructorFirstInput

/// Functions for constructing the ScopedOpenRecord record.
[<RequireQualifiedAccess; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module ScopedOpenRecord =
/// Create a value of type ScopedOpenRecord with every optional field set to None.
let create (arg_0: Token) : ScopedOpenRecord = { Token = arg_0; Optional = None }
namespace ConsumePlugin.SecondScopedOpen

open RecordConstructorSecondInput

/// Functions for constructing the ScopedOpenRecord record.
[<RequireQualifiedAccess; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module ScopedOpenRecord =
/// Create a value of type ScopedOpenRecord with every optional field set to None.
let create (arg_0: Token) : ScopedOpenRecord = { Token = arg_0; Optional = None }










117 changes: 117 additions & 0 deletions ConsumePlugin/RecordConstructor.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
namespace ConsumePlugin

open System
open WoofWare.Myriad.Plugins

type ConstructorOptionAlias<'value> = 'value option

[<GenerateRecordConstructor>]
type MixedConstructorRecord =
{
FirstRequired : string
FirstOptional : int option
SecondRequired : string
SecondOptional : bool option
ThirdRequired : string
}

[<GenerateRecordConstructor>]
type AllRequiredConstructorRecord =
{
First : string
Second : string
Third : string
}

[<GenerateRecordConstructor>]
type AllOptionalConstructorRecord =
{
Number : int option
Text : string option
}

[<GenerateRecordConstructor>]
type NestedOptionConstructorRecord =
{
Id : Guid
PostfixNested : int option option
PrefixOption : Option<string>
}

[<GenerateRecordConstructor>]
type GenericConstructorRecord<'required, 'optional> =
{
Required : 'required
Optional : 'optional option
RequiredList : 'required list
}

[<GenerateRecordConstructor>]
type GenericAllOptionalConstructorRecord<'value> =
{
Value : 'value option
}

[<GenerateRecordConstructor>]
type internal InternalConstructorRecord =
{
Required : string
Optional : int option
}

[<GenerateRecordConstructor>]
type TupleAndFunctionConstructorRecord =
{
Pair : int * string
Transform : int -> string
OptionalTransform : (int -> string) option
}

[<GenerateRecordConstructor>]
type NonSyntacticOptionConstructorRecord =
{
Alias : ConstructorOptionAlias<int>
ValueOption : string voption
Nullable : Nullable<int>
Optional : decimal option
}

[<GenerateRecordConstructor>]
type NameHazardConstructorRecord =
{
``type`` : string
arg_0 : string
Arg_0 : string
``two words`` : string
``two-words`` : string option
}

namespace RecordConstructorFirstInput

type Token = | FirstToken of int

namespace RecordConstructorSecondInput

type Token = | SecondToken of string

namespace ConsumePlugin.FirstScopedOpen

open RecordConstructorFirstInput

[<WoofWare.Myriad.Plugins.GenerateRecordConstructor>]
type ScopedOpenRecord =
{
Token : Token
Optional : int option
}

namespace ConsumePlugin.SecondScopedOpen

open RecordConstructorSecondInput

[<WoofWare.Myriad.Plugins.GenerateRecordConstructor>]
type ScopedOpenRecord =
{
Token : Token
Optional : int option
}
74 changes: 74 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Generated record constructors

## Problem

Constructing an F# record with many `option` fields is needlessly repetitive when the usual initial state for every optional field is `None`. The generator should make the required part of construction explicit while retaining ordinary record-update syntax for later optional values.

## Public API

Annotating a record with `GenerateRecordConstructorAttribute`:

```fsharp
[<WoofWare.Myriad.Plugins.GenerateRecordConstructor>]
type Request =
{
Name : string
RetryCount : int option
Enabled : bool
}
```

generates a companion module and a curried `create` function:

```fsharp
[<RequireQualifiedAccess>]
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Request =
/// Create a Request with every optional field set to None.
let create (arg_0 : string) (arg_1 : bool) : Request =
{
Name = arg_0
RetryCount = None
Enabled = arg_1
}
```

Required arguments appear in record declaration order. If every field is optional, `create` takes `unit`, so the generated API remains a function and generic records do not encounter the value restriction. The generated module has the source type's accessibility.

Parameters are named `arg_0`, `arg_1`, and so on in required-field order. These names are deterministic, legal for every record-label spelling, and cannot collide when labels differ only by casing or identifier sanitisation.

The Myriad generator is exposed as `RecordConstructorGenerator` with the identifier `record-constructor`.

## Meaning of optional

A field is optional exactly when its outermost syntactic type constructor is F# `option`, including the ordinary postfix and prefix spellings understood by the parsed syntax. Such a field is initialized with `None`, even when its element type is itself optional.

Type aliases cannot be resolved from the untyped syntax tree. An alias of `option`, `voption`, `System.Nullable<_>`, and all other types are therefore required arguments. This boundary keeps generation deterministic and reflection-free.

## Functional core and generator shell

The implementation is split into three parts:

1. A syntax-independent constructor planner accepts an ordered list of fields classified as required or optional. It returns inert data describing ordered parameters and every record-field initializer (`FromParameter` or `UseNone`).
2. A thin Fantomas/FCS adapter classifies parsed record fields and renders that plan as an AST. It also constructs the applied return type for generic records.
3. The Myriad shell parses the input file, finds annotated types, rejects a use on a non-record, and emits the rendered modules.

The planner is the semantic core. It has no IO, compiler service, formatting, or Myriad dependencies, and its output can be interpreted directly in property tests. The syntax adapter is checked by parsing generated record shapes and observing their plans, while a checked-in consumer project supplies the renderer and compilation boundary.

## Guarantees

- Every source field appears exactly once in the generated record expression and remains in declaration order.
- Every direct `option` field is initialized with `None` and creates no parameter.
- Every other field has exactly one typed parameter, and its initializer references that parameter.
- Parameter order is the declaration order of required fields.
- Parameter names are unique and are the consecutive sequence `arg_0` through `arg_(n-1)`.
- Generic parameters used only by optional fields remain represented in the result type.
- Generated source is deterministic for a given input syntax tree.

## Deliberate limits

- The attribute applies only to records; placing it on another type is an error.
- The record must be declared directly in a namespace. F# modules cannot be reopened from another file, so an annotation nested inside a module is rejected explicitly.
- There is no attribute-free `MyriadParams` spelling or extension-member mode in this first version.
- A record with private type or representation accessibility cannot be constructed from a separate generated file and is rejected with an explicit error.
- A pre-existing companion module can collide with the generated module, as it can for the non-extension JsonParse generator.
Loading
Loading