ADR-0003: Toolchain Integration Framework
Status: Proposed Date: 2026-01-08 Authors: Morphir Team
Context
Morphir needs to integrate with both external tools (like morphir-elm) and native Go implementations (like WIT bindings) to provide a complete development experience.
External tools (morphir-elm):
morphir-elm make: Compiles Elm sources to Morphir IR (frontend)morphir-elm gen: Generates target language code from IR (backend)
Native implementations (WIT bindings - already implemented):
morphir wit make: Compiles WIT to Morphir IRmorphir wit gen: Generates WIT from Morphir IRmorphir wit build: Full pipeline with round-trip validation- JSONL batch processing support
We want to:
- Orchestrate both external tools and native implementations through a unified CLI
- Support future toolchains (Haskell, PureScript frontends; Spark, Scala backends)
- Provide a unified, inspectable build experience
- Enable caching and incremental builds
- Adapt the existing WIT pipeline as the first native toolchain
Decision
We will implement a Toolchain Integration Framework with the following architecture:
Core Abstractions
- Toolchains: Tool adapters (native or external) that provide tasks and can hook into execution lifecycle
- Native toolchains: In-process Go implementations (e.g., WIT bindings)
- External toolchains: Process-based tools (e.g., morphir-elm via npx)
- Targets: CLI-facing capabilities (make, gen, test) that tasks fulfill
- Tasks: Concrete implementations that produce artifacts (via Go code or process execution)
- Workflows: Named compositions of targets with staged execution
Key Design Choices
Hybrid Pipeline Model
- Targets declare capabilities and artifact contracts
- Workflows define orchestration (stages, parallelism, conditions)
- Execution plan is computed by merging workflow order with target dependencies
- Plan is validated, optimized, and inspectable via
morphir plan
Artifact-Based Communication
- Tasks produce outputs to
.morphir/out/{toolchain}/{task}/ - Artifacts are JSONC files (human-readable with comments)
- Diagnostics stream as JSONL/NDJSON
- Tasks reference other outputs via logical paths (
@toolchain/task:artifact)
Middleware Execution
- Task execution follows a pipeline: RESOLVE → CACHE → PREPARE → EXECUTE → COLLECT → REPORT
- Toolchains can inject hooks at any stage
- Task system owns error reporting; toolchains contribute diagnostics
Configuration Inheritance
- Workflows support
extendsfor inheritance - Precedence: project > toolchain > built-in defaults
- Similar to tsconfig/eslint extends pattern
CLI Mapping
morphir make # Run target
morphir gen:scala # Run target variant
morphir build # Run workflow
morphir plan build # Show execution plan
morphir doctor # Diagnose configuration issues
Consequences
Positive
- Unified CLI regardless of underlying toolchain
- Inspectable builds via
morphir plan - Cacheable tasks based on input hashing
- Extensible to new frontends/backends
- Familiar patterns (Mill, Bazel, Make)
Negative
- Complexity of merged execution model (mitigated by plan visualization)
- Two concepts (targets + workflows) to understand
- File-based communication may be slow for large artifacts (future: streaming)
Neutral
- External tools must be acquired/installed
- Initial focus on file-based I/O (LSP/gRPC deferred)
Alternatives Considered
A: Pipeline-only (no targets)
- Simpler but less composable
- Hard to express "any tool that produces IR"
- Rejected: loses flexibility
B: Targets-only (no workflows)
- Target dependencies form implicit pipeline
- Simpler but less control over staging
- Rejected: need explicit stages for complex builds
C: Current choice (hybrid)
- Targets for capabilities, workflows for orchestration
- Plan merges both with validation
- Accepted: best balance of power and safety