Create Morphir.Internal.CodeGeneration Project - Myriad Plugin for morphir-dotnet
- Dominant language
- F#
- Stars
- 14
- Forks
- 12
- PR merge metrics
- No merged PRs in 30d
Description
# Create Morphir.Internal.CodeGeneration Project
## Summary
Create a new **Morphir.Internal.CodeGeneration** F# project focused on utility code generation to supplement morphir-dotnet developers. This project will function as a Myriad plugin package, providing compile-time code generation for Morphir IR types, JSON codecs, visitors, lenses, and other boilerplate reduction.
**Key Goal**: Enable reflection-free, AOT-compatible F# code through compile-time code generation.
## Motivation
The morphir-dotnet project requires extensive code generation for:
1. **JSON Encoders/Decoders** - AOT-compatible serialization without reflection
2. **IR Type Boilerplate** - Visitors, traversals, folds for IR manipulation
3. **Lenses** - Type-safe nested updates for deeply nested IR structures
4. **Active Patterns** - Generated from discriminated unions for pattern matching
5. **Type-Safe Builders** - Fluent APIs for IR construction
6. **Exhaustiveness Helpers** - Ensure all DU cases are handled
By creating a dedicated code generation project with Myriad plugin capabilities, we can:
- ✅ **Eliminate reflection** - All code generated at compile-time
- ✅ **Ensure AOT compatibility** - Generated code is trimming-friendly
- ✅ **Reduce boilerplate** - Automate repetitive patterns
- ✅ **Improve type safety** - Generated code is strongly typed
- ✅ **Accelerate development** - Less manual code to write and maintain
- ✅ **Support Elm→F# migration** - Generate equivalents to Elm encoders/decoders
## Project Structure
```
src/Morphir.Internal.CodeGeneration/
├── Morphir.Internal.CodeGeneration.fsproj
├── README.md
├── Generators/
│ ├── JsonCodecGenerator.fs # JSON encoder/decoder generation
│ ├── VisitorGenerator.fs # IR visitor pattern generation
│ ├── LensGenerator.fs # Lens generation for nested updates
│ ├── ActivePatternGenerator.fs # Active pattern generation from DUs
│ └── BuilderGenerator.fs # Type-safe builder generation
├── Attributes/
│ ├── Attributes.fs # Generator marker attributes
│ └── ConfigurationAttributes.fs # Configuration attributes
├── Core/
│ ├── AstHelpers.fs # AST manipulation utilities
│ ├── TypeHelpers.fs # Type analysis utilities
│ └── CodeGenHelpers.fs # Common code generation patterns
└── Plugin/
├── MyriadPlugin.fs # Myriad plugin registration
└── build/
└── Morphir.Internal.CodeGeneration.props # MSBuild integration
```
## Package Configuration
### Project File (.fsproj)
```xml
net9.0
true
Morphir.Internal.CodeGeneration
0.1.0
FINOS
FINOS
Myriad code generation utilities for morphir-dotnet development
Apache-2.0
https://github.com/finos/morphir-dotnet
https://github.com/finos/morphir-dotnet
morphir;myriad;codegen;fsharp
false
true
true
build
true
lib/net9.0
true
```
### MSBuild Props File
**`build/Morphir.Internal.CodeGeneration.props`:**
```xml
```
## Core Components
### 1. Attributes (Marker Attributes for Generators)
**`Attributes/Attributes.fs`:**
```fsharp
namespace Morphir.Internal.CodeGeneration
open System
/// Marker attribute for JSON codec generation
[]
type GenerateJsonCodecAttribute() =
inherit Attribute()
/// Namespace for generated code (optional, defaults to current namespace + ".Generated")
member val Namespace: string = null with get, set
/// Property naming policy (e.g., "camelCase", "PascalCase")
member val PropertyNamingPolicy: string = "camelCase" with get, set
/// Marker attribute for visitor pattern generation
[]
type GenerateVisitorAttribute() =
inherit Attribute()
member val Namespace: string = null with get, set
member val VisitorName: string = null with get, set
/// Marker attribute for lens generation
[]
type GenerateLensesAttribute() =
inherit Attribute()
member val Namespace: string = null with get, set
/// Marker attribute for active pattern generation
[]
type GenerateActivePatternsAttribute() =
inherit Attribute()
member val Namespace: string = null with get, set
/// Marker attribute for builder generation
[]
type GenerateBuilderAttribute() =
inherit Attribute()
member val Namespace: string = null with get, set
member val BuilderName: string = null with get, set
```
### 2. JSON Codec Generator
**`Generators/JsonCodecGenerator.fs`:**
```fsharp
namespace Morphir.Internal.CodeGeneration.Generators
open Myriad.Core
open FSharp.Compiler.Syntax
open FSharp.Compiler.SyntaxTree
open Morphir.Internal.CodeGeneration
/// Generates JSON encoder and decoder functions for F# types
///
/// Example input:
/// []
/// type User = { Id: int; Name: string }
///
/// Example output:
/// module User.JsonCodec =
/// open System.Text.Json
///
/// let encode (value: User) : JsonElement =
/// // Generated encoder without reflection
/// ...
///
/// let decode (json: JsonElement) : Result =
/// // Generated decoder without reflection
/// ...
[]
type JsonCodecGenerator() =
interface IMyriadGenerator with
member _.ValidInputExtensions = seq { ".fs" }
member _.Generate(context: GeneratorContext) =
// 1. Extract types marked with []
let markedTypes =
Ast.fromFilename context.InputFilename
|> Ast.extractRecords
|> List.filter (fun (_, attrs) ->
attrs |> List.exists (fun attr ->
attr.TypeName.AsString = "GenerateJsonCodec"))
// 2. Generate encoder/decoder for each type
let generatedModules =
markedTypes
|> List.map (fun (record, attrs) ->
let config = Generator.getConfigFromAttribute attrs
let ns = config.Namespace ?? $"{context.Namespace}.Generated"
// Generate module with encoder/decoder
generateCodecModule ns record config)
// 3. Return generated AST
Output.Ast generatedModules
// Helper: Generate codec module for a record type
static member private generateCodecModule ns record config =
let recordName = record.Name
// Create module: {RecordName}.JsonCodec
let moduleName = $"{recordName}.JsonCodec"
// Generate encoder function
let encoder = generateEncoderFunction record config
// Generate decoder function
let decoder = generateDecoderFunction record config
SynModuleOrNamespace.createNamespace
[ Ident.create ns ]
[
SynModuleDecl.createNestedModule
(Ident.create moduleName)
[ encoder; decoder ]
]
// Implementation details for encoder/decoder generation...
```
### 3. Visitor Generator
**`Generators/VisitorGenerator.fs`:**
```fsharp
namespace Morphir.Internal.CodeGeneration.Generators
open Myriad.Core
open FSharp.Compiler.Syntax
/// Generates visitor pattern for discriminated unions
///
/// Example input:
/// []
/// type TypeExpr =
/// | TInt
/// | TString
/// | TFunc of input: TypeExpr * output: TypeExpr
///
/// Example output:
/// type TypeExprVisitor<'Result> = {
/// VisitTInt: unit -> 'Result
/// VisitTString: unit -> 'Result
/// VisitTFunc: input: TypeExpr -> output: TypeExpr -> 'Result
/// }
///
/// module TypeExpr =
/// let accept (visitor: TypeExprVisitor<'Result>) (expr: TypeExpr) : 'Result =
/// match expr with
/// | TInt -> visitor.VisitTInt()
/// | TString -> visitor.VisitTString()
/// | TFunc(input, output) -> visitor.VisitTFunc input output
[]
type VisitorGenerator() =
interface IMyriadGenerator with
member _.ValidInputExtensions = seq { ".fs" }
member _.Generate(context: GeneratorContext) =
// Extract DUs marked with []
// Generate visitor record type
// Generate accept function
Output.Ast []
```
### 4. Lens Generator
**`Generators/LensGenerator.fs`:**
```fsharp
namespace Morphir.Internal.CodeGeneration.Generators
open Myriad.Core
/// Generates lenses for nested record updates
///
/// Example input:
/// []
/// type Config = { Port: int; Host: string }
///
/// Example output:
/// module Config.Lenses =
/// let port = {
/// Get = fun (c: Config) -> c.Port
/// Set = fun (value: int) (c: Config) -> { c with Port = value }
/// }
/// let host = {
/// Get = fun (c: Config) -> c.Host
/// Set = fun (value: string) (c: Config) -> { c with Host = value }
/// }
[]
type LensGenerator() =
interface IMyriadGenerator with
member _.ValidInputExtensions = seq { ".fs" }
member _.Generate(context: GeneratorContext) =
// Generate lens record type
// Generate lens for each field
Output.Ast []
```
## Usage Examples
### In morphir-dotnet Projects
**1. Install Package:**
```xml
```
**2. Mark Types for Code Generation:**
```fsharp
namespace Morphir.IR
open Morphir.Internal.CodeGeneration
/// IR Type definition
[]
[]
type TypeExpr =
| TInt
| TString
| TBool
| TFunc of input: TypeExpr * output: TypeExpr
| TRecord of fields: Map
/// Package definition
[]
[]
type Package =
{ Name: PackageName
Modules: Map }
```
**3. Build Project - Code Generated Automatically:**
```bash
dotnet build # Myriad runs automatically, generates code
```
**4. Use Generated Code:**
```fsharp
open Morphir.IR.TypeExpr.JsonCodec
open Morphir.IR.Package.JsonCodec
// Use generated encoder/decoder
let json = TypeExpr.encode (TFunc(TInt, TString))
let result = TypeExpr.decode jsonElement
// Use generated lenses
let updated =
package
|> Package.Lenses.name.Set newName
// Use generated visitor
let visitor = {
VisitTInt = fun () -> "integer"
VisitTString = fun () -> "string"
// ...
}
let description = TypeExpr.accept visitor myType
```
## Implementation Phases
### Phase 1: Foundation (Week 1)
- [x] Create project structure
- [x] Set up Myriad plugin packaging
- [x] Implement core attributes
- [x] Create AST helper utilities
- [x] Set up build integration
- [x] Write README and documentation
### Phase 2: JSON Codec Generator (Week 2)
- [ ] Implement JsonCodecGenerator
- [ ] Support records, DUs, tuples
- [ ] Handle nested types
- [ ] Property naming policy support
- [ ] Write unit tests
- [ ] Create usage examples
### Phase 3: Visitor Generator (Week 2-3)
- [ ] Implement VisitorGenerator
- [ ] Support discriminated unions
- [ ] Generate visitor record type
- [ ] Generate accept function
- [ ] Write tests and examples
### Phase 4: Lens & Active Pattern Generators (Week 3)
- [ ] Implement LensGenerator
- [ ] Implement ActivePatternGenerator
- [ ] Support nested records
- [ ] Write tests and examples
### Phase 5: Builder Generator (Week 4)
- [ ] Implement BuilderGenerator
- [ ] Fluent API generation
- [ ] Validation support
- [ ] Tests and examples
### Phase 6: Integration & Testing (Week 4)
- [ ] Integration with Morphir.Core
- [ ] End-to-end testing
- [ ] Performance benchmarks
- [ ] Documentation and guides
## Acceptance Criteria
### For Initial Release (v0.1.0)
- [ ] Project structure created and building
- [ ] Packaged as Myriad plugin (NuGet)
- [ ] JsonCodecGenerator implemented and working
- [ ] At least one other generator (Visitor or Lens)
- [ ] Unit tests with >= 80% coverage
- [ ] Integration tested with Morphir.Core types
- [ ] README with usage examples
- [ ] Published to local NuGet feed
### For Production Release (v1.0.0)
- [ ] All 5 generators implemented
- [ ] Comprehensive test coverage
- [ ] Performance benchmarked
- [ ] Used in at least 3 morphir-dotnet projects
- [ ] Documentation complete
- [ ] Published to NuGet.org
- [ ] Integrated with CI/CD
## AOT Compatibility Verification
All generated code must:
- ✅ Compile with `PublishAot=true` without warnings
- ✅ Not use reflection (`typeof`, `Type.GetType`, etc.)
- ✅ Not trigger IL2026, IL3050 warnings
- ✅ Work with trimming (`PublishTrimmed=true`)
- ✅ Be verifiable with AOT Guru skill
## Integration with Other Skills
### Elm to F# Guru
- Provides code generation for Elm encoder/decoder equivalents
- Generates boilerplate for migrated types
- Accelerates migration workflow
### AOT Guru
- Ensures all generated code is AOT-compatible
- Validates no reflection usage
- Tests with PublishAot=true
### QA Tester
- Tests generated code thoroughly
- BDD scenarios for generators
- Property-based tests for correctness
## Testing Strategy
### Unit Tests
```fsharp
module JsonCodecGeneratorTests
open NUnit.Framework
open Morphir.Internal.CodeGeneration.Generators
[]
let ``generates encoder for simple record`` () =
// Arrange
let input = """
[]
type User = { Id: int; Name: string }
"""
// Act
let output = JsonCodecGenerator().Generate(...)
// Assert
// Verify encoder function generated
// Verify decoder function generated
// Verify no reflection usage
```
### Integration Tests
- Test with real Morphir.Core types
- Verify JSON roundtrip (encode → decode)
- Compare with manual implementations
- Test AOT compilation
### BDD Tests
```gherkin
Feature: JSON Codec Generation
Scenario: Generate codec for simple record
Given a record type with primitive fields
When I mark it with GenerateJsonCodec
And I build the project
Then a JsonCodec module should be generated
And it should contain encode and decode functions
And the code should compile without reflection warnings
```
## Success Metrics
1. **Code Reduction**: 50%+ less manual codec code
2. **AOT Compatibility**: 100% of generated code AOT-compatible
3. **Performance**: Generated codecs within 10% of manual implementations
4. **Adoption**: Used in 5+ morphir-dotnet modules
5. **Reliability**: >= 80% test coverage
6. **Developer Experience**: Positive feedback from team
## Related Resources
### Myriad
- [Myriad Repository](https://github.com/MoiraeSoftware/myriad)
- [Myriad Templates](https://www.nuget.org/packages/Myriad.Templates/)
- [Example: Fields Generator](https://github.com/MoiraeSoftware/myriad/blob/master/src/Myriad.Plugins/FieldsGenerator.fs)
### morphir-dotnet
- [AGENTS.md](../AGENTS.md) - Agent guidance
- [Elm to F# Guru Skill](https://github.com/finos/morphir-dotnet/issues/240)
- [AOT Guru Skill](./.claude/skills/aot-guru/)
- [F# Coding Guide](../docs/contributing/fsharp-coding-guide.md)
## Labels
- `enhancement`
- `myriad`
- `code-generation`
- `fsharp`
- `tooling`
- `infrastructure`
## Related Issues
- #240 - Elm to F# Guru Skill (will use this for code generation)
- [Link to AOT optimization issues]
- [Link to JSON serialization issues]
Contributor guide
Assessment
This issue has not been assessed yet.