finos / finos/morphir-dotnet

Implement TreeProcessor in Morphir.IR.Pipeline

Open
#375 0 comments 0 reactions 0 assignees View on GitHub
feature priority-p1
Dominant language
F#
Stars
14
Forks
12
PR merge metrics
No merged PRs in 30d

Description

# GitHub Issue: Implement TreeProcessor in Morphir.IR.Pipeline

**Type**: Feature
**Labels**: `feature`, `file-architecture`, `priority-p1`, `pipeline`
**Priority**: P1 (Enhances VFileTree support)
**Milestone**: v1.0.0
**Estimated Effort**: 3-5 days
**Project**: Morphir.IR.Pipeline (existing project - add new file)

---

## Description

Implement `TreeProcessor` - pipeline support for processing `VFileTree` structures in the `Morphir.IR.Pipeline` project. This enables multi-file transformations while maintaining the hierarchical structure and aggregating diagnostics across all files.

**Related Design**: [Unified File Architecture](./unified-file-architecture.md#layer-3-pipeline-integration)

---

## Context

### What Exists Today

✅ **VFile** (in `Morphir.IR.Pipeline/File.fs`):
- Virtual file with content, path, diagnostics, metadata
- Used by existing `MorphirProcessor` for single-file operations

✅ **MorphirProcessor** (in `Morphir.IR.Pipeline/Processor.fs`):
- Processes single `VFile` through transformation pipeline
- Supports computation expression syntax

### What's Missing

❌ **Multi-file pipeline support**:
- No way to process `VFileTree` (hierarchical multi-file structures)
- No aggregation of diagnostics across files
- No directory-level transformations

---

## Acceptance Criteria

### Core Types (Add to `Morphir.IR.Pipeline/TreeProcessor.fs`)

**Type Definitions**:
- [ ] Define `TreeProcessor` record type
- [ ] Define computation expression builder `TreePipelineBuilder`
- [ ] XML doc comments on all types

### TreeProcessor Type

**Fields**:
- [ ] `ProcessTree: VFileTree -> Result` - Process entire tree
- [ ] `ProcessFile: VFile -> Result` - Process individual files (for leaf operations)
- [ ] `Name: string option` - Processor name (for diagnostics)

### Module Functions

**Creation Functions** (`TreeProcessor` module):
- [ ] `empty: TreeProcessor` - Create empty processor (identity)
- [ ] `fromFileProcessor: (VFile -> Result) -> TreeProcessor` - Lift file processor to tree processor
- [ ] `fromMorphirProcessor: MorphirProcessor -> TreeProcessor` - Convert existing processor

**Composition Functions**:
- [ ] `compose: TreeProcessor -> TreeProcessor -> TreeProcessor` - Sequential composition
- [ ] `parallel: TreeProcessor list -> TreeProcessor` - Parallel processing (independent transformations)

**Execution Functions**:
- [ ] `run: TreeProcessor -> VFileTree -> Result` - Execute processor on tree
- [ ] `runOnFiles: TreeProcessor -> VFile list -> Result` - Execute on file list

**Diagnostics Functions**:
- [ ] `collectDiagnostics: VFileTree -> Map` - Aggregate diagnostics by file
- [ ] `hasErrors: VFileTree -> bool` - Check if any file has errors
- [ ] `summarize: VFileTree -> ProcessorSummary` - Get processing summary

### Computation Expression Builder

**Custom Operations**:
- [ ] `Yield` - Create empty processor
- [ ] `parseTree` - Parse tree structure
- [ ] `transformTree` - Transform entire tree
- [ ] `mapFiles` - Apply transformation to each file
- [ ] `filterFiles` - Filter files based on predicate
- [ ] `aggregateDiagnostics` - Collect diagnostics
- [ ] `validateTree` - Validate tree structure

### ProcessorSummary Type

**Fields**:
- [ ] `TotalFiles: int` - Total files processed
- [ ] `SuccessCount: int` - Files processed successfully
- [ ] `ErrorCount: int` - Files with errors
- [ ] `WarningCount: int` - Total warnings
- [ ] `ProcessingTime: TimeSpan option` - Time taken (optional)

---

## Implementation Tasks

### 1. Create TreeProcessor.fs

```bash
# Add new file to existing Morphir.IR.Pipeline project
touch src/Morphir.IR.Pipeline/TreeProcessor.fs
# Update Morphir.IR.Pipeline.fsproj to include TreeProcessor.fs AFTER FileTree.fs
```

### 2. Define Core Types

```fsharp
namespace Morphir.IR.Pipeline

open System

///
/// Summary of tree processing results.
///
type ProcessorSummary = {
TotalFiles: int
SuccessCount: int
ErrorCount: int
WarningCount: int
ProcessingTime: TimeSpan option
}

///
/// Processor that operates on VFileTree (multi-file projects).
/// Supports both tree-level and file-level transformations.
///
type TreeProcessor = {
/// Process entire tree
ProcessTree: VFileTree -> Result

/// Process individual file (for leaf operations)
ProcessFile: VFile -> Result

/// Processor name (for diagnostics)
Name: string option
}
```

### 3. Implement Module Functions

```fsharp
[]
module TreeProcessor =
/// Create empty (identity) processor
let empty: TreeProcessor = {
ProcessTree = Ok
ProcessFile = Ok
Name = None
}

/// Create processor from file processor (applies to each file)
let fromFileProcessor (name: string option) (proc: VFile -> Result): TreeProcessor =
{
ProcessTree = fun tree ->
// Apply processor to each file in tree
let rec processTree (t: VFileTree): Result =
let processedContent =
t.Content
|> List.map (function
| File file ->
match proc file with
| Ok processedFile -> Ok (File processedFile)
| Error errorFile -> Error (File errorFile)
| Directory subtree ->
match processTree subtree with
| Ok processedSubtree -> Ok (Directory processedSubtree)
| Error errorSubtree -> Error (Directory errorSubtree))

// Check if any errors occurred
let errors = processedContent |> List.choose (function | Error e -> Some e | _ -> None)

if errors.IsEmpty then
let successContent = processedContent |> List.choose (function | Ok c -> Some c | _ -> None)
Ok { t with Content = successContent }
else
// Return tree with errors
Error { t with Content = errors }

processTree tree

ProcessFile = proc
Name = name
}

/// Convert MorphirProcessor to TreeProcessor
let fromMorphirProcessor (processor: MorphirProcessor): TreeProcessor =
fromFileProcessor processor.Name processor.Process

/// Compose two processors sequentially
let compose (first: TreeProcessor) (second: TreeProcessor): TreeProcessor =
{
ProcessTree = fun tree ->
first.ProcessTree tree
|> Result.bind second.ProcessTree

ProcessFile = fun file ->
first.ProcessFile file
|> Result.bind second.ProcessFile

Name =
match first.Name, second.Name with
| Some n1, Some n2 -> Some $"{n1} >> {n2}"
| Some n, None | None, Some n -> Some n
| None, None -> None
}

/// Run multiple processors in parallel (all must succeed)
let parallel (processors: TreeProcessor list): TreeProcessor =
{
ProcessTree = fun tree ->
let results = processors |> List.map (fun p -> p.ProcessTree tree)

// Check if all succeeded
let errors = results |> List.choose (function | Error e -> Some e | _ -> None)

if errors.IsEmpty then
// All succeeded - return last result
results |> List.last
else
// Return first error
Error (errors |> List.head)

ProcessFile = fun file ->
let results = processors |> List.map (fun p -> p.ProcessFile file)

// Check if all succeeded
let errors = results |> List.choose (function | Error e -> Some e | _ -> None)

if errors.IsEmpty then
results |> List.last
else
Error (errors |> List.head)

Name = Some "Parallel processors"
}

/// Execute processor on tree
let run (processor: TreeProcessor) (tree: VFileTree): Result =
processor.ProcessTree tree

/// Execute processor on file list
let runOnFiles (processor: TreeProcessor) (files: VFile list): Result =
let results = files |> List.map processor.ProcessFile

let errors = results |> List.choose (function | Error e -> Some e | _ -> None)

if errors.IsEmpty then
Ok (results |> List.choose (function | Ok f -> Some f | _ -> None))
else
Error errors

/// Collect diagnostics from all files in tree
let collectDiagnostics (tree: VFileTree): Map =
tree
|> VFileTree.allFiles
|> List.map (fun file ->
let path = file.Path |> Option.defaultValue "unknown"
(path, file.Messages))
|> Map.ofList

/// Check if tree has errors
let hasErrors (tree: VFileTree): bool =
VFileTree.hasErrors tree

/// Summarize processing results
let summarize (tree: VFileTree): ProcessorSummary =
let stats = VFileTree.statistics tree
{
TotalFiles = stats.TotalFiles
SuccessCount = stats.TotalFiles - stats.ErrorCount
ErrorCount = stats.ErrorCount
WarningCount = stats.WarningCount
ProcessingTime = None
}
```

### 4. Implement Computation Expression Builder

```fsharp
///
/// Computation expression builder for tree pipelines.
/// Enables pipeline { ... } syntax for multi-file processing.
///
type TreePipelineBuilder() =
member _.Yield(_) = TreeProcessor.empty

[]
member _.ParseTree(proc: TreeProcessor, parser: VFileTree -> Result) =
{ proc with ProcessTree = parser }

[]
member _.TransformTree(proc: TreeProcessor, transformer: TreeProcessor) =
TreeProcessor.compose proc transformer

[]
member _.MapFiles(proc: TreeProcessor, mapper: VFile -> Result) =
let fileProc = TreeProcessor.fromFileProcessor None mapper
TreeProcessor.compose proc fileProc

[]
member _.FilterFiles(proc: TreeProcessor, predicate: VFile -> bool) =
let filterProc = TreeProcessor.fromFileProcessor (Some "FilterFiles") (fun file ->
if predicate file then Ok file else Error file)
TreeProcessor.compose proc filterProc

[]
member _.AggregateDiagnostics(proc: TreeProcessor) =
{ proc with
ProcessTree = fun tree ->
match proc.ProcessTree tree with
| Ok resultTree ->
// Log summary
let summary = TreeProcessor.summarize resultTree
printfn "Processed %d files (%d errors, %d warnings)"
summary.TotalFiles summary.ErrorCount summary.WarningCount
Ok resultTree
| Error errorTree ->
Error errorTree
}

///
/// Pipeline builder for tree processing.
///
let treePipeline = TreePipelineBuilder()
```

### 5. Write Tests

Create `tests/Morphir.IR.Pipeline.Tests/TreeProcessorTests.fs`:

```fsharp
module Morphir.IR.Pipeline.Tests.TreeProcessorTests

open TUnit.Core
open Morphir.IR.Pipeline

[]
let ``Empty processor returns tree unchanged`` () =
let tree = VFileTree.empty
let result = TreeProcessor.run TreeProcessor.empty tree

match result with
| Ok resultTree -> resultTree |> should equal tree
| Error _ -> failwith "Should not error"

[]
let ``fromFileProcessor applies to all files`` () =
let file1 = VFile.create "file1.fs" "content1"
let file2 = VFile.create "file2.fs" "content2"
let tree = VFileTree.fromFiles [file1; file2]

// Processor that adds metadata
let addMetadata file =
Ok (VFile.setData "processed" true file)

let processor = TreeProcessor.fromFileProcessor (Some "AddMetadata") addMetadata

match TreeProcessor.run processor tree with
| Ok resultTree ->
let files = VFileTree.allFiles resultTree
files |> List.forall (fun f -> VFile.getData "processed" f = Some (box true))
|> should be true
| Error _ -> failwith "Should not error"

[]
let ``compose chains processors`` () =
let file = VFile.create "test.fs" "content"
let tree = VFileTree.fromFiles [file]

let proc1 = TreeProcessor.fromFileProcessor (Some "Proc1") (fun f ->
Ok (VFile.setData "step1" true f))

let proc2 = TreeProcessor.fromFileProcessor (Some "Proc2") (fun f ->
Ok (VFile.setData "step2" true f))

let composed = TreeProcessor.compose proc1 proc2

match TreeProcessor.run composed tree with
| Ok resultTree ->
let files = VFileTree.allFiles resultTree
files |> List.head |> VFile.getData "step1" |> should equal (Some (box true))
files |> List.head |> VFile.getData "step2" |> should equal (Some (box true))
| Error _ -> failwith "Should not error"

[]
let ``collectDiagnostics aggregates messages`` () =
let fileWithError =
VFile.create "error.fs" "content"
|> VFile.error "Test error" None

let fileWithWarning =
VFile.create "warning.fs" "content"
|> VFile.warn "Test warning" None

let tree = VFileTree.fromFiles [fileWithError; fileWithWarning]
let diagnostics = TreeProcessor.collectDiagnostics tree

diagnostics |> Map.count |> should equal 2
diagnostics |> Map.containsKey "error.fs" |> should be true
diagnostics |> Map.containsKey "warning.fs" |> should be true

[]
let ``summarize provides processing statistics`` () =
let fileWithError =
VFile.create "error.fs" "content"
|> VFile.error "Error" None

let fileOk = VFile.create "ok.fs" "content"

let tree = VFileTree.fromFiles [fileWithError; fileOk]
let summary = TreeProcessor.summarize tree

summary.TotalFiles |> should equal 2
summary.ErrorCount |> should equal 1

[]
let ``treePipeline computation expression works`` () =
let file = VFile.create "test.fs" "content"
let tree = VFileTree.fromFiles [file]

let pipeline = treePipeline {
mapFiles (fun f -> Ok (VFile.setData "processed" true f))
aggregateDiagnostics
}

match TreeProcessor.run pipeline tree with
| Ok resultTree ->
let files = VFileTree.allFiles resultTree
files |> List.head |> VFile.getData "processed" |> should equal (Some (box true))
| Error _ -> failwith "Should not error"
```

### 6. Update Project File

Add to `src/Morphir.IR.Pipeline/Morphir.IR.Pipeline.fsproj`:

```xml





```

### 7. Documentation

- [ ] Add XML doc comments to all public types and functions
- [ ] Create usage examples in unified-file-architecture.md
- [ ] Document integration with MorphirProcessor
- [ ] Document computation expression syntax

---

## Usage Examples

### Example 1: Simple File Transformation

```fsharp
open Morphir.IR.Pipeline

// Create processor that adds metadata to all files
let addTimestamp =
TreeProcessor.fromFileProcessor (Some "AddTimestamp") (fun file ->
Ok (VFile.setData "timestamp" DateTime.UtcNow file))

// Run on tree
let tree = VFileTree.fromFiles [file1; file2]
match TreeProcessor.run addTimestamp tree with
| Ok resultTree -> printfn "Success!"
| Error errorTree -> printfn "Errors: %A" (TreeProcessor.collectDiagnostics errorTree)
```

### Example 2: Pipeline Syntax

```fsharp
let pipeline = treePipeline {
// Parse F# files
parseTree FSharpFrontend.parse

// Transform to IR
mapFiles (fun file ->
// Each file contains F# AST, transform to IR
IRMapper.mapFile file)

// Validate IR
mapFiles IRValidator.validate

// Aggregate diagnostics
aggregateDiagnostics
}

let result = TreeProcessor.run pipeline myTree
```

### Example 3: Composition

```fsharp
// Compose multiple processors
let fullPipeline =
parseProcessor
|> TreeProcessor.compose transformProcessor
|> TreeProcessor.compose validateProcessor

let result = TreeProcessor.run fullPipeline tree
```

---

## Success Criteria

- [ ] All types defined and compile successfully
- [ ] All module functions implemented
- [ ] All tests pass (≥80% coverage)
- [ ] Can process VFileTree with file-level transformations
- [ ] Can compose processors
- [ ] Computation expression syntax works
- [ ] Documentation complete
- [ ] Integrates with existing MorphirProcessor

---

## Dependencies

- VFile (exists in Morphir.IR.Pipeline/File.fs)
- VFileTree (new - see related issue)
- MorphirProcessor (exists in Morphir.IR.Pipeline/Processor.fs)

## Blocks

- F# Frontend multi-file parsing (enhanced by this)
- F# Backend tree generation (enhanced by this)

---

## Related Documents

- [Unified File Architecture](./unified-file-architecture.md) - Complete design
- [GitHub Issue: VFileTree](./github-issue-morphir-file-tree.md) - Related issue

---

**Ready for Implementation**: This issue provides complete context and code examples.

**Estimated Effort**: 3-5 developer-days

**Priority**: P1 (Enhances VFileTree functionality, not blocking but highly valuable)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.