Skip to main content

Build Operations

This document defines build orchestration, cleaning, and diagnostic operations for Morphir workspaces.

Overview

Build operations coordinate compilation across multiple projects:

  • Dependency-ordered builds: Compile projects in correct order
  • Parallel compilation: Build independent projects concurrently
  • Incremental builds: Only rebuild what changed
  • Diagnostic aggregation: Unified error reporting across workspace

Build Pipeline

┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│ Resolve │───►│ Order │───►│ Compile │───►│ Report │
│ Dependencies│ │ Projects │ │ (Parallel)│ │ Diagnostics │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘

Types

BuildResult

/// Result of a workspace build
pub type BuildResult {
BuildResult(
/// Overall success/failure
success: Bool,
/// Per-project results
projects: List(ProjectBuildResult),
/// Total build duration (milliseconds)
duration_ms: Int,
)
}

/// Result for a single project
pub type ProjectBuildResult {
ProjectBuildResult(
/// Project name
name: PackagePath,
/// Build status
status: BuildStatus,
/// Compiled distribution (if successful)
distribution: Option(Distribution),
/// Compilation diagnostics
diagnostics: List(Diagnostic),
/// Build duration (milliseconds)
duration_ms: Int,
)
}

/// Build status for a project
pub type BuildStatus {
/// Build succeeded with no issues
Ok
/// Build succeeded with warnings
Partial
/// Build failed
Failed
/// Build was skipped (up to date)
Skipped
}

Diagnostic

/// Compilation diagnostic
pub type Diagnostic {
Diagnostic(
/// Severity level
severity: Severity,
/// Error/warning code
code: String,
/// Human-readable message
message: String,
/// Source location
location: Option(SourceLocation),
/// Suggested fixes
hints: List(String),
)
}

pub type Severity {
Error
Warning
Info
Hint
}

pub type SourceLocation {
SourceLocation(
/// File path (relative to workspace)
file: String,
/// Start line (1-indexed)
start_line: Int,
/// Start column (1-indexed)
start_col: Int,
/// End line
end_line: Int,
/// End column
end_col: Int,
)
}

Operations

Build All

Builds all projects in the workspace.

Behavior

  1. Resolve all dependencies
  2. Compute build order (topological sort)
  3. Build projects in parallel where possible
  4. Aggregate diagnostics
  5. Return combined result

Build Order

Projects are built in dependency order:

Level 0 (no deps):     core
Level 1 (→ core): domain, utils
Level 2 (→ domain): api, cli

Projects at the same level can be built in parallel.

WIT Interface

/// Build all projects in workspace
build-all: func() -> result<list<tuple<package-path, distribution>>, workspace-error>;

JSON-RPC

Request:

{
"method": "workspace/buildAll",
"params": {}
}

Response:

{
"result": {
"success": true,
"projects": [
{
"name": "my-org/core",
"status": "ok",
"distribution": { "..." },
"diagnostics": [],
"durationMs": 523
},
{
"name": "my-org/domain",
"status": "partial",
"distribution": { "..." },
"diagnostics": [
{
"severity": "warning",
"code": "W001",
"message": "Unused import: List.Extra",
"location": {
"file": "packages/domain/src/User.elm",
"startLine": 5,
"startCol": 1,
"endLine": 5,
"endCol": 25
}
}
],
"durationMs": 1247
}
],
"durationMs": 1823
}
}

CLI

morphir build                    # Build all
morphir build --parallel 4 # Limit parallelism
morphir build --project my-org/api # Build single project

Clean

Removes build artifacts and caches.

Behavior

  1. If project specified: clean that project only
  2. If no project: clean entire workspace
  3. Remove .morphir-dist/ directories
  4. Optionally remove dependency cache

WIT Interface

/// Clean build artifacts
clean: func(
/// Specific project, or all if none
project: option<package-path>,
) -> result<_, workspace-error>;

JSON-RPC

Request (single project):

{
"method": "workspace/clean",
"params": {
"project": "my-org/domain"
}
}

Request (entire workspace):

{
"method": "workspace/clean",
"params": {}
}

Request (include dependency cache):

{
"method": "workspace/clean",
"params": {
"includeDeps": true
}
}

CLI

morphir clean                    # Clean all
morphir clean my-org/domain # Clean one project
morphir clean --deps # Also clean dependency cache

Get Diagnostics

Returns all current diagnostics across the workspace.

WIT Interface

/// Get workspace-wide diagnostics
get-diagnostics: func() -> list<tuple<package-path, list<diagnostic>>>;

JSON-RPC

Request:

{
"method": "workspace/getDiagnostics",
"params": {}
}

Response:

{
"result": {
"my-org/core": [],
"my-org/domain": [
{
"severity": "error",
"code": "E001",
"message": "Type mismatch: expected Int, got String",
"location": {
"file": "packages/domain/src/Order.elm",
"startLine": 42,
"startCol": 15,
"endLine": 42,
"endCol": 28
},
"hints": ["Try using String.toInt to convert"]
}
],
"my-org/api": [
{
"severity": "warning",
"code": "W002",
"message": "Function 'oldHelper' is deprecated",
"location": { "..." }
}
]
}
}

Incremental Builds

Change Detection

Changes are detected via:

  1. File modification time: Compare against last build
  2. Content hash: SHA-256 of source files
  3. Dependency changes: Rebuild if dependency was rebuilt

Build Cache

.morphir/
└── cache/
├── my-org/
│ └── domain/
│ ├── manifest.json # Build metadata
│ ├── source-hash # Hash of all sources
│ └── ir-cache/ # Cached IR fragments

Manifest Format

{
"version": "1.0.0",
"lastBuild": "2026-01-16T12:00:00Z",
"sourceHash": "sha256:abc123...",
"files": {
"src/User.elm": {
"hash": "sha256:def456...",
"lastModified": "2026-01-16T11:30:00Z"
}
},
"dependencies": {
"my-org/core": "sha256:ghi789..."
}
}

Parallel Execution

Strategy

Sequential (dependencies):    core ──► domain ──► api

Parallel (independent): core ──►─┬► domain ──► api
└► utils ───►─┘

Configuration

# morphir.toml
[build]
parallel = true # Enable parallel builds
max-workers = 4 # Maximum parallel compilations
fail-fast = false # Continue on errors (or stop immediately)

Error Recovery

Partial Builds

When a project fails, dependent projects can still attempt to build using the last successful IR:

core (ok) ──► domain (FAILED) ──► api (uses cached domain IR)

Diagnostic-Only Mode

Build without generating artifacts (fast validation):

morphir build --check-only

Build Events

workspace/onBuildStarted

{
"method": "workspace/onBuildStarted",
"params": {
"projects": ["my-org/core", "my-org/domain"],
"incremental": true
}
}

workspace/onBuildProgress

{
"method": "workspace/onBuildProgress",
"params": {
"project": "my-org/domain",
"phase": "compiling",
"progress": 0.45,
"currentFile": "src/Domain/User.elm"
}
}

workspace/onBuildComplete

{
"method": "workspace/onBuildComplete",
"params": {
"success": true,
"projects": ["my-org/core", "my-org/domain"],
"durationMs": 2341,
"diagnosticCount": {
"errors": 0,
"warnings": 3
}
}
}

Streaming Builds

Large projects benefit from streaming compilation where results are produced incrementally rather than in one shot. This enables:

  • Early feedback: Errors appear as soon as they're discovered
  • Progressive output: Generated artifacts stream as modules complete
  • Memory efficiency: Don't hold entire project in memory
  • Interruptibility: Cancel long builds without losing partial progress

Streaming Model

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ Source │────►│ Compile │────►│ Stream │
│ Files │ │ Module │ │ Results │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ ▼ ▼
│ ┌─────────────┐ ┌─────────────┐
└───────────►│ Compile │─────►│ Stream │
│ Module │ │ Results │
└─────────────┘ └─────────────┘

Module-Level Streaming

Compilation streams results at the module level:

{
"jsonrpc": "2.0",
"method": "build/moduleCompiled",
"params": {
"project": "my-org/domain",
"module": ["Domain", "User"],
"status": "ok",
"ir": { "...module IR..." },
"diagnostics": []
}
}

Streaming Build Request

Request:

{
"jsonrpc": "2.0",
"id": "build-001",
"method": "workspace/buildStreaming",
"params": {
"projects": ["my-org/domain"],
"streaming": {
"granularity": "module",
"includeIR": true,
"includeDiagnostics": true
}
}
}

Stream of Notifications:

{ "method": "build/started", "params": { "project": "my-org/domain", "modules": 12 } }
{ "method": "build/moduleCompiled", "params": { "module": ["Domain", "Types"], "status": "ok", "ir": {...} } }
{ "method": "build/moduleCompiled", "params": { "module": ["Domain", "User"], "status": "ok", "ir": {...} } }
{ "method": "build/moduleCompiled", "params": { "module": ["Domain", "Order"], "status": "partial", "diagnostics": [...] } }
...
{ "method": "build/completed", "params": { "success": true, "modulesCompiled": 12 } }

Final Response:

{
"jsonrpc": "2.0",
"id": "build-001",
"result": { "success": true, "modulesCompiled": 12, "durationMs": 3421 }
}

CLI Streaming Output

morphir build --stream

Output:

Building my-org/domain (12 modules)
✓ Domain.Types [42ms]
✓ Domain.User [38ms]
⚠ Domain.Order [51ms] (2 warnings)
✓ Domain.Product [29ms]
...
✓ Domain.Api [67ms]

Build complete: 12 modules in 3.4s (2 warnings)

Incremental Module Compilation

For watch mode and IDE integration, individual modules can be recompiled:

Request:

{
"jsonrpc": "2.0",
"id": "compile-001",
"method": "compile/module",
"params": {
"project": "my-org/domain",
"module": ["Domain", "User"],
"source": "module Domain.User exposing (..)\n\nimport Domain.Types...",
"existingIR": { "...previous module IR for merge..." }
}
}

Dependency-Aware Streaming

When a module changes, dependent modules are recompiled in order:

User.elm changed
└─► Recompile Domain.User
└─► Recompile Domain.Api (depends on User)
└─► Recompile Domain.Service (depends on Api)

Each recompilation streams its result immediately:

{ "method": "build/moduleCompiled", "params": { "module": ["Domain", "User"], "trigger": "source-change" } }
{ "method": "build/moduleCompiled", "params": { "module": ["Domain", "Api"], "trigger": "dependency-change" } }
{ "method": "build/moduleCompiled", "params": { "module": ["Domain", "Service"], "trigger": "dependency-change" } }

Code Generation

Code generation transforms compiled IR into target language code using backend extensions.

Target Selection

The --target flag selects which backend to use for code generation:

# Generate Spark/Scala code
morphir codegen --target spark

# Generate TypeScript code
morphir codegen --target typescript

# Generate multiple targets
morphir codegen --target spark --target typescript

# List available targets
morphir codegen --list-targets

Built-in Targets:

TargetFlagOutputNotes
Spark--target sparkScala (Spark DataFrame API)Default for data pipelines
Scala--target scalaPure ScalaGeneral-purpose
TypeScript--target typescriptTypeScriptWeb/Node.js
JSON Schema--target json-schemaJSON SchemaType definitions only

Extension Targets:

WASM-based backend extensions register additional targets:

# morphir.toml
[extensions]
codegen-flink = { path = "./extensions/flink-codegen.wasm" }
# Registers --target flink
# Use extension-provided target
morphir codegen --target flink

Target Configuration

Targets can be configured in morphir.toml:

[codegen.spark]
spark_version = "3.5"
scala_version = "2.13"
output_dir = "src/main/scala"

[codegen.typescript]
module_system = "esm"
output_dir = "src/generated"

Or via CLI flags:

morphir codegen --target spark --option spark_version=3.5 --output src/main/scala

Automatic Target Association

Projects can configure default targets that run automatically on build:

# morphir.toml
[project]
name = "my-org/domain"

# Default targets for this project
[codegen]
targets = ["spark", "typescript"]

# Target-specific configuration
[codegen.spark]
output_dir = "src/main/scala"

[codegen.typescript]
output_dir = "src/generated/ts"

With this configuration, morphir build automatically generates code for all configured targets:

# Builds IR and generates code for spark and typescript
morphir build

# Skip codegen
morphir build --no-codegen

# Override targets
morphir build --codegen-targets spark

Module-Level Target Association

Associate specific modules with specific targets:

# morphir.toml
[codegen]
# Default targets for all modules
targets = ["spark"]

# Override for specific modules
[codegen.modules."Domain.Api"]
targets = ["typescript", "json-schema"]

[codegen.modules."Domain.Internal"]
targets = [] # No codegen for internal modules

Pattern-Based Target Association

Use glob patterns for target association:

[codegen]
targets = ["spark"]

# All Api modules get TypeScript
[[codegen.rules]]
pattern = "**/Api/**"
targets = ["typescript"]

# Test modules don't get codegen
[[codegen.rules]]
pattern = "**/Test/**"
targets = []

Extension-Declared Targets

Extensions can declare which targets they provide and their capabilities:

// In extension's WIT interface
world spark-codegen {
export codegen-target {
// Target metadata
name: func() -> string; // "spark"
description: func() -> string; // "Apache Spark DataFrame API"
file-extension: func() -> string; // ".scala"

// Capability flags
supports-streaming: func() -> bool;
supports-incremental: func() -> bool;
}

export morphir:extension/codegen;
}

Extension Registration:

# morphir.toml
[extensions]
codegen-spark = { path = "./extensions/spark-codegen.wasm" }

# Extension automatically registers its target
# Now --target spark is available

Querying Available Targets:

{
"jsonrpc": "2.0",
"id": "targets-001",
"method": "codegen/listTargets",
"params": {}
}

Response:

{
"jsonrpc": "2.0",
"id": "targets-001",
"result": {
"targets": [
{
"name": "spark",
"description": "Apache Spark DataFrame API",
"fileExtension": ".scala",
"source": "builtin",
"supportsStreaming": true,
"supportsIncremental": true
},
{
"name": "flink",
"description": "Apache Flink DataStream API",
"fileExtension": ".scala",
"source": "extension:codegen-flink",
"supportsStreaming": true,
"supportsIncremental": false
}
]
}
}

JSON-RPC Method

{
"jsonrpc": "2.0",
"id": "codegen-001",
"method": "codegen/generate",
"params": {
"project": "my-org/domain",
"target": "spark",
"options": {
"spark_version": "3.5",
"output_dir": "src/main/scala"
}
}
}

Response:

{
"jsonrpc": "2.0",
"id": "codegen-001",
"result": {
"target": "spark",
"filesGenerated": 24,
"outputDir": "src/main/scala"
}
}

Streaming Code Generation

Code generation also supports streaming to avoid generating all output at once.

Streaming Codegen Request

Request:

{
"jsonrpc": "2.0",
"id": "codegen-001",
"method": "codegen/generateStreaming",
"params": {
"project": "my-org/domain",
"target": "spark",
"streaming": {
"granularity": "module",
"writeImmediately": true
}
}
}

Stream of Notifications:

{ "method": "codegen/started", "params": { "target": "spark", "modules": 12 } }
{ "method": "codegen/moduleGenerated", "params": { "module": ["Domain", "Types"], "files": ["Types.scala"] } }
{ "method": "codegen/moduleGenerated", "params": { "module": ["Domain", "User"], "files": ["User.scala", "UserCodecs.scala"] } }
...
{ "method": "codegen/completed", "params": { "filesGenerated": 24 } }

Incremental Codegen

Only regenerate code for changed modules:

morphir codegen --target spark --incremental

Behavior:

  1. Compare module IR hashes against last codegen
  2. Regenerate only changed modules
  3. Stream generated files as they're produced

Codegen Manifest

Track what was generated for incremental updates:

{
"target": "spark",
"generatedAt": "2026-01-16T12:00:00Z",
"modules": {
"Domain.User": {
"irHash": "sha256:abc123...",
"files": [
{ "path": "src/main/scala/domain/User.scala", "hash": "sha256:def456..." }
]
}
}
}

Parallel Codegen

Generate code for independent modules in parallel:

# morphir.toml
[codegen]
parallel = true
max-workers = 4
streaming = true # Enable streaming output

Ad-Hoc Compilation

For quick experimentation and integration workloads, Morphir supports compiling code without a full project setup. This is useful for:

  • Quick prototyping: Test ideas without creating a project
  • Shell pipelines: Integrate with Unix-style workflows
  • CI validation: Check snippets in automated tests
  • Code generation testing: Validate codegen output

Snippet Compilation

Compile source code from stdin or inline. The input language must be specified (or inferred from file extension):

# From stdin (language required)
echo 'module Example exposing (add)
add a b = a + b' | morphir compile --lang elm -

# From a single file (language inferred from .elm extension)
morphir compile snippet.elm

# Explicit language
morphir compile --lang elm snippet.elm

# Multiple files
morphir compile types.elm logic.elm

Supported Languages:

LanguageFlagExtensionsNotes
Elm--lang elm.elmDefault frontend
Morphir DSL--lang morphir-dsl.morphir, .mdslNative DSL
(Extensions)--lang <name>Per extensionVia WASM frontends

JSON-RPC Method:

{
"jsonrpc": "2.0",
"id": "snippet-001",
"method": "compile/snippet",
"params": {
"language": "elm",
"source": "module Example exposing (add)\nadd a b = a + b",
"options": {
"moduleName": "Example",
"packageName": "adhoc"
}
}
}

Expression Evaluation

Compile or evaluate standalone expressions:

# Type-check an expression
morphir check --expr "\\x -> x + 1"

# Evaluate an expression
morphir eval "List.map (\\x -> x * 2) [1, 2, 3]"

JSON-RPC Method:

{
"jsonrpc": "2.0",
"id": "expr-001",
"method": "compile/expression",
"params": {
"expression": "\\x -> x + 1",
"context": {
"imports": []
}
}
}

Ad-Hoc with Dependencies

Specify dependencies inline for snippets that need external packages:

morphir compile snippet.elm --with-dep morphir/sdk --with-dep morphir/json

JSON-RPC:

{
"jsonrpc": "2.0",
"id": "snippet-002",
"method": "compile/snippet",
"params": {
"language": "elm",
"source": "module Example exposing (..)\nimport Json.Decode...",
"options": {
"dependencies": [
{ "name": "morphir/sdk" },
{ "name": "morphir/json", "version": "1.0.0" }
]
}
}
}

Pipeline Compilation

Chain compilation and codegen in Unix pipelines:

# Compile then generate
cat snippet.elm | morphir compile - | morphir codegen --target spark -

# Compile multiple, stream codegen
morphir compile "src/*.elm" --stream | morphir codegen --target typescript --stream -

Fragment Compilation

For IDE integration, compile a code fragment within an existing module context:

{
"jsonrpc": "2.0",
"id": "frag-001",
"method": "compile/fragment",
"params": {
"language": "elm",
"fragment": "\\x -> x + 1",
"context": {
"modulePath": ["Domain", "User"],
"imports": ["Domain.Types"],
"localBindings": {
"currentUser": "User"
}
}
}
}

This enables features like:

  • Hover type information
  • Autocomplete with context
  • Inline error checking

See CLI Interaction for complete CLI documentation.

Best Practices

  1. Use Incremental Builds: Avoid clean unless necessary
  2. Parallelize: Enable parallel builds for faster compilation
  3. Fail Fast in CI: Use --fail-fast in CI to stop on first error
  4. Cache Dependencies: Keep dependency cache for faster rebuilds
  5. Check Before Push: Run morphir build --check-only before committing
  6. Stream Large Builds: Use --stream for projects with many modules
  7. Incremental Codegen: Use --incremental to only regenerate changed modules