Skip to main content

Morphir Extensions

The extension architecture for adding capabilities to Morphir via WASM components and the task system.

Tracking

TypeReferences
Beadsmorphir-go-772 (task execution), morphir-010 (CLI extensions)
GitHub Issues#399 (task/target execution engine)

Overview

Morphir Extensions enable:

  • Custom Code Generators: Add new backend targets (Spark, Scala, etc.)
  • Custom Frontends: Support new source languages
  • Additional Tasks: Register new intrinsic tasks
  • Build Automation: Pre/post hooks for built-in commands
  • Protocol Integration: JSON-RPC based communication

Documents

DocumentStatusDescription
WASM ComponentsDraftComponent model integration and WIT interfaces
TasksDraftTask system, dependencies, and hooks

Getting Started with Extensions

Extension development starts with a minimal "info" extension that verifies connectivity before adding features.

Minimal Extension (Hello World)

Every extension must implement the info interface - this is the only required interface:

package morphir:extension@0.4.0;

/// Required interface - all extensions must implement this
interface info {
/// Extension metadata
record extension-info {
/// Unique identifier (e.g., "spark-codegen")
id: string,
/// Human-readable name
name: string,
/// Version (semver)
version: string,
/// Description
description: string,
/// Author/maintainer
author: option<string>,
/// Homepage/repository URL
homepage: option<string>,
/// License identifier (SPDX)
license: option<string>,
}

/// Return extension metadata
get-info: func() -> extension-info;

/// Health check - return true if extension is ready
ping: func() -> bool;
}

Minimal Extension Implementation:

use morphir_extension::info::{ExtensionInfo, Info};

struct MyExtension;

impl Info for MyExtension {
fn get_info() -> ExtensionInfo {
ExtensionInfo {
id: "my-extension".to_string(),
name: "My First Extension".to_string(),
version: "0.1.0".to_string(),
description: "A minimal Morphir extension".to_string(),
author: Some("My Name".to_string()),
homepage: Some("https://github.com/me/my-extension".to_string()),
license: Some("Apache-2.0".to_string()),
}
}

fn ping() -> bool {
true // Extension is ready
}
}

Extension Discovery

The CLI can list and inspect all registered extensions:

# List all extensions
morphir extension list

# Output:
# NAME VERSION TYPE CAPABILITIES
# spark-codegen 1.2.0 codegen generate, streaming, incremental
# elm-frontend 0.19.1 frontend compile, diagnostics
# my-extension 0.1.0 unknown (info only)

# Detailed info about an extension
morphir extension info spark-codegen

# Output:
# spark-codegen v1.2.0
# Type: codegen
# Description: Generate Apache Spark DataFrame code from Morphir IR
# Author: Morphir Contributors
# Homepage: https://github.com/finos/morphir-spark
# License: Apache-2.0
#
# Capabilities:
# ✓ codegen/generate
# ✓ codegen/generate-streaming
# ✓ codegen/generate-incremental
# ✓ codegen/generate-module
# ✓ codegen/options-schema
#
# Targets: spark
#
# Options:
# spark_version string "3.5" Spark version to target
# scala_version string "2.13" Scala version to target

# Verify extension connectivity
morphir extension ping spark-codegen
# spark-codegen: OK (2ms)

# Ping all extensions
morphir extension ping --all
# spark-codegen: OK (2ms)
# elm-frontend: OK (1ms)
# my-extension: OK (1ms)

JSON-RPC Methods

List Extensions:

{
"jsonrpc": "2.0",
"id": "list-001",
"method": "extension/list",
"params": {}
}

Response:

{
"jsonrpc": "2.0",
"id": "list-001",
"result": {
"extensions": [
{
"id": "spark-codegen",
"name": "Spark Code Generator",
"version": "1.2.0",
"type": "codegen",
"source": { "path": "./extensions/spark-codegen.wasm" },
"capabilities": ["generate", "generate-streaming", "generate-incremental"]
},
{
"id": "my-extension",
"name": "My First Extension",
"version": "0.1.0",
"type": null,
"source": { "path": "./extensions/my-extension.wasm" },
"capabilities": []
}
]
}
}

Get Extension Info:

{
"jsonrpc": "2.0",
"id": "info-001",
"method": "extension/info",
"params": {
"extension": "spark-codegen"
}
}

Response:

{
"jsonrpc": "2.0",
"id": "info-001",
"result": {
"id": "spark-codegen",
"name": "Spark Code Generator",
"version": "1.2.0",
"description": "Generate Apache Spark DataFrame code from Morphir IR",
"author": "Morphir Contributors",
"homepage": "https://github.com/finos/morphir-spark",
"license": "Apache-2.0",
"type": "codegen",
"capabilities": {
"codegen/generate": true,
"codegen/generate-streaming": true,
"codegen/generate-incremental": true,
"codegen/generate-module": true,
"codegen/options-schema": true
},
"targets": ["spark"],
"options": {
"spark_version": {
"type": "string",
"default": "3.5",
"description": "Spark version to target"
},
"scala_version": {
"type": "string",
"default": "2.13",
"description": "Scala version to target"
}
}
}
}

Ping Extension:

{
"jsonrpc": "2.0",
"id": "ping-001",
"method": "extension/ping",
"params": {
"extension": "spark-codegen"
}
}

Response:

{
"jsonrpc": "2.0",
"id": "ping-001",
"result": {
"extension": "spark-codegen",
"status": "ok",
"latency_ms": 2
}
}

Extension Development Workflow

  1. Start minimal: Implement only info interface
  2. Verify connectivity: morphir extension ping my-extension
  3. Check registration: morphir extension list
  4. Add capabilities incrementally: One interface at a time
  5. Test each capability: Verify with CLI before adding more

Extension Formats

Extensions can be distributed in several formats:

FormatExtensionDescription
WASM Component.wasmWebAssembly component (sandboxed)
ExecutablePlatform-specificJSON-RPC over stdio executable
Package.morphir-ext.tgzTar gzipped bundle with manifest
Directory*/extension.tomlUnpacked extension with manifest

WASM Component Extensions

Sandboxed WebAssembly components using the Component Model:

extensions/
└── spark-codegen.wasm # Self-contained WASM component

Executable Extensions (JSON-RPC)

Native executables that communicate via JSON-RPC over stdio:

extensions/
├── my-backend # Unix executable
├── my-backend.exe # Windows executable
└── my-frontend/
├── extension.toml
└── bin/
├── frontend-linux-amd64 # Linux
├── frontend-darwin-amd64 # macOS Intel
├── frontend-darwin-arm64 # macOS Apple Silicon
└── frontend-windows-amd64.exe

Platform Resolution:

The daemon selects the appropriate executable based on OS and architecture:

OSArchitectureSearch Pattern
Linuxamd64*-linux-amd64, *-linux-x86_64, *
Linuxarm64*-linux-arm64, *-linux-aarch64, *
macOSamd64*-darwin-amd64, *-darwin-x86_64, *
macOSarm64*-darwin-arm64, *-darwin-aarch64, *
Windowsamd64*.exe, *-windows-amd64.exe

Executable Manifest:

# extension.toml
[extension]
id = "my-backend"
name = "My Backend"
version = "1.0.0"
type = "codegen"

# Executable configuration
[extension.executable]
# Platform-specific binaries
[extension.executable.bin]
"linux-amd64" = "bin/backend-linux-amd64"
"linux-arm64" = "bin/backend-linux-arm64"
"darwin-amd64" = "bin/backend-darwin-amd64"
"darwin-arm64" = "bin/backend-darwin-arm64"
"windows-amd64" = "bin/backend-windows-amd64.exe"

# Or single cross-platform binary (e.g., Go, Java)
# command = "bin/backend"

# Arguments passed to executable
args = ["--mode", "jsonrpc"]

# Environment variables
[extension.executable.env]
LOG_LEVEL = "info"

Package Format (.morphir-ext.tgz)

Distributable tar gzipped packages:

# Package structure (when extracted)
spark-codegen-1.2.0/
├── extension.toml # Required: manifest
├── codegen.wasm # WASM component
├── README.md # Documentation
├── LICENSE
└── examples/
└── basic.elm

Creating a Package:

# Package an extension
morphir extension pack ./spark-codegen/
# → spark-codegen-1.2.0.morphir-ext.tgz

# Package with specific output
morphir extension pack ./spark-codegen/ -o dist/

Installing a Package:

# Install from package file
morphir extension install spark-codegen-1.2.0.morphir-ext.tgz
# Extracts to: .morphir/extensions/spark-codegen/

# Install from URL
morphir extension install https://example.com/spark-codegen-1.2.0.morphir-ext.tgz

Package Manifest:

# extension.toml in package
[extension]
id = "spark-codegen"
name = "Spark Code Generator"
version = "1.2.0"
description = "Generate Apache Spark DataFrame code from Morphir IR"
author = "Morphir Contributors"
license = "Apache-2.0"
homepage = "https://github.com/finos/morphir-spark"

# Component type and file
type = "codegen"
component = "codegen.wasm" # WASM component
# OR
# executable = "bin/codegen" # Native executable

targets = ["spark"]

# Dependencies on other extensions (optional)
[extension.dependencies]
morphir-ir = "^4.0.0"

# Configuration schema
[extension.options]
spark_version = { type = "string", default = "3.5", description = "Spark version" }
scala_version = { type = "string", default = "2.13", description = "Scala version" }

Extension Discovery Locations

Extensions are discovered from multiple locations, in order of precedence:

Discovery Order

  1. Explicit configuration (morphir.toml)
  2. Workspace extensions (.morphir/extensions/)
  3. User extensions ($XDG_DATA_HOME/morphir/extensions/)
  4. System extensions (/usr/share/morphir/extensions/ or platform equivalent)

Workspace Extensions

my-workspace/
├── morphir.toml
├── .morphir/
│ ├── extensions/ # Auto-discovered
│ │ ├── spark-codegen.wasm # WASM component
│ │ ├── my-backend # Executable (Unix)
│ │ ├── my-backend.exe # Executable (Windows)
│ │ ├── flink-codegen/ # Directory extension
│ │ │ ├── extension.toml
│ │ │ └── codegen.wasm
│ │ └── custom-frontend/ # Executable extension
│ │ ├── extension.toml
│ │ └── bin/
│ │ ├── frontend-linux-amd64
│ │ └── frontend-darwin-arm64
│ ├── cache/
│ └── deps/

User Extensions (Global)

# Linux/macOS
$XDG_DATA_HOME/morphir/extensions/
~/.local/share/morphir/extensions/ # Fallback

# macOS alternative
~/Library/Application Support/morphir/extensions/

# Windows
%LOCALAPPDATA%\morphir\extensions\

System Extensions

# Linux
/usr/share/morphir/extensions/
/usr/local/share/morphir/extensions/

# macOS
/Library/Application Support/morphir/extensions/

# Windows
%PROGRAMDATA%\morphir\extensions\

Explicit Configuration

Override or supplement auto-discovery in morphir.toml:

[extensions]
# WASM component (explicit path)
spark-codegen = { path = "./custom/spark-codegen.wasm" }

# Executable with command
my-backend = { command = "./bin/my-backend", args = ["--mode", "jsonrpc"] }

# URL (downloaded and cached)
flink-codegen = { url = "https://extensions.morphir.dev/flink-codegen-1.0.0.morphir-ext.tgz" }

# Disable auto-discovered extension
legacy-ext = { enabled = false }

# Override options for auto-discovered extension
[extensions.spark-codegen.config]
spark_version = "3.4"

Discovery Resolution

# Show where each extension was discovered from
morphir extension list --show-source

# Output:
# NAME VERSION FORMAT SOURCE
# spark-codegen 1.2.0 wasm .morphir/extensions/spark-codegen.wasm
# my-backend 1.0.0 executable .morphir/extensions/my-backend/
# flink-codegen 1.0.0 package morphir.toml (url → cached)
# elm-frontend 0.19.1 wasm ~/.local/share/morphir/extensions/

JSON-RPC Extension Info (with Location)

Request:

{
"jsonrpc": "2.0",
"id": "info-001",
"method": "extension/info",
"params": {
"extension": "my-backend"
}
}

Response (Executable Extension):

{
"jsonrpc": "2.0",
"id": "info-001",
"result": {
"id": "my-backend",
"name": "My Backend",
"version": "1.0.0",
"type": "codegen",
"format": "executable",
"source": {
"type": "workspace",
"path": ".morphir/extensions/my-backend/",
"manifest": ".morphir/extensions/my-backend/extension.toml"
},
"executable": {
"resolved": ".morphir/extensions/my-backend/bin/backend-darwin-arm64",
"platform": "darwin-arm64",
"args": ["--mode", "jsonrpc"]
},
"capabilities": {
"codegen/generate": true,
"codegen/generate-streaming": true
}
}
}

Response (WASM Extension):

{
"jsonrpc": "2.0",
"id": "info-002",
"result": {
"id": "spark-codegen",
"name": "Spark Code Generator",
"version": "1.2.0",
"type": "codegen",
"format": "wasm",
"source": {
"type": "workspace",
"path": ".morphir/extensions/spark-codegen.wasm"
},
"component": {
"path": ".morphir/extensions/spark-codegen.wasm",
"size": 245760
},
"capabilities": {
"codegen/generate": true,
"codegen/generate-streaming": true,
"codegen/generate-incremental": true
}
}
}

Extension Installation

# Install WASM component (to workspace)
morphir extension install spark-codegen.wasm
# → .morphir/extensions/spark-codegen.wasm

# Install package (extracts)
morphir extension install spark-codegen-1.2.0.morphir-ext.tgz
# → .morphir/extensions/spark-codegen/

# Install globally
morphir extension install --global spark-codegen.wasm
# → ~/.local/share/morphir/extensions/spark-codegen.wasm

# Install from URL
morphir extension install https://releases.example.com/spark-codegen-1.2.0.morphir-ext.tgz
# Downloads, verifies, extracts to .morphir/extensions/spark-codegen/

Extension Types

WASM Components

Extensions implemented as WASM components using the Component Model:

package morphir:extension@0.4.0;

interface codegen {
/// Generate code for a target language
generate: func(ir: distribution, options: codegen-options) -> result<generated-files, codegen-error>;
}

Benefits:

  • Language agnostic (Rust, Go, C, etc.)
  • Sandboxed execution
  • Capability-based permissions
  • Hot-reloadable

Tasks

User-defined or extension-provided commands:

# Built-in tasks work automatically
# Extensions can register additional intrinsic tasks

[tasks.ci]
description = "Run CI pipeline"
depends = ["check", "test", "build"]

[tasks."post:build"]
run = "prettier --write .morphir-dist/"

Task Types:

  • Built-in: build, test, check, codegen, pack, publish
  • Extension-provided: Registered by WASM components
  • User-defined: Shell commands in [tasks]

Architecture

┌─────────────────────────────────────────────────────────┐
│ Morphir CLI/Daemon │
├─────────────────────────────────────────────────────────┤
│ Extension Host │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ WASM Runtime│ │ Task Runner │ │ JSON-RPC │ │
│ │ (wasmtime) │ │ │ │ Protocol │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
└─────────┼────────────────┼────────────────┼─────────────┘
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Codegen │ │ Custom │ │ External │
│ Extension │ │ Tasks │ │ Process │
└───────────┘ └───────────┘ └───────────┘

Extension Points

PointMechanismUse Case
Code GenerationWASM componentCustom backend targets
FrontendWASM componentNew source languages
ValidationWASM componentCustom analyzers
TasksTask definitionBuild automation
HooksPre/post tasksExtend built-in commands

Task System

Built-in Tasks

These work automatically without configuration:

TaskDescription
buildCompile project to IR
testRun tests
checkLint and validate
codegenGenerate code for targets
packCreate distributable package
publishPublish to registry

Pre/Post Hooks

Extend built-in tasks with hooks:

[tasks."pre:build"]
run = "echo 'Starting build...'"

[tasks."post:build"]
run = "prettier --write .morphir-dist/"

[tasks."post:codegen"]
run = "./scripts/post-codegen.sh"

Task Dependencies

Chain tasks together:

[tasks.ci]
description = "Full CI pipeline"
depends = ["check", "test", "build", "pack"]

[tasks.release]
depends = ["ci"]
run = "morphir publish --backend github"

Configuration

Registering Extensions

# morphir.toml

[extensions]
# WASM component extensions
codegen-spark = { path = "./extensions/spark-codegen.wasm" }
codegen-scala = { url = "https://extensions.morphir.dev/scala-codegen-1.0.0.wasm" }

# Extension configuration
[extensions.codegen-spark.config]
spark_version = "3.5"

Extension Capabilities

Extensions are capability-based - they declare which features they implement, allowing incremental development. An extension doesn't need to implement everything; it exports only what it supports.

world codegen-extension {
// Required imports (what the extension needs)
import morphir:ir/types;
import morphir:ir/values;

// Provided exports (what the extension offers)
export morphir:extension/codegen;

// Optional exports (implement incrementally)
// export morphir:extension/codegen-streaming;
// export morphir:extension/codegen-incremental;
}

Capability-Based Design

Extensions implement features incrementally through optional interfaces:

Frontend Capabilities

CapabilityInterfaceDescription
Basicfrontend/compileCompile source to IR (required)
Streamingfrontend/compile-streamingStream module-by-module results
Incrementalfrontend/compile-incrementalRecompile only changed files
Fragmentfrontend/compile-fragmentCompile code fragments (IDE)
Diagnosticsfrontend/diagnosticsRich error messages with fixes

Minimal Frontend:

world minimal-frontend {
import morphir:ir/types;
export frontend/compile; // Only basic compilation
}

Full-Featured Frontend:

world full-frontend {
import morphir:ir/types;
export frontend/compile;
export frontend/compile-streaming;
export frontend/compile-incremental;
export frontend/compile-fragment;
export frontend/diagnostics;
}

Backend/Codegen Capabilities

CapabilityInterfaceDescription
Basiccodegen/generateGenerate code for target (required)
Streamingcodegen/generate-streamingStream file-by-file output
Incrementalcodegen/generate-incrementalRegenerate only changed modules
Module-levelcodegen/generate-moduleGenerate single module
Optionscodegen/options-schemaDeclare configurable options

Minimal Backend:

world minimal-backend {
import morphir:ir/distributions;
export codegen/generate; // Only basic generation
}

Capability Negotiation

The daemon queries extension capabilities at load time:

JSON-RPC:

{
"jsonrpc": "2.0",
"id": "caps-001",
"method": "extension/capabilities",
"params": {
"extension": "codegen-spark"
}
}

Response:

{
"jsonrpc": "2.0",
"id": "caps-001",
"result": {
"extension": "codegen-spark",
"type": "codegen",
"capabilities": {
"codegen/generate": true,
"codegen/generate-streaming": true,
"codegen/generate-incremental": true,
"codegen/generate-module": true,
"codegen/options-schema": true
},
"targets": ["spark"],
"options": {
"spark_version": { "type": "string", "default": "3.5" },
"scala_version": { "type": "string", "default": "2.13" }
}
}
}

Graceful Degradation

When an extension lacks a capability, Morphir falls back gracefully:

Missing CapabilityFallback Behavior
compile-streamingCompile all at once, return single result
compile-incrementalFull recompilation on every change
generate-streamingGenerate all files, return at end
generate-moduleGenerate full distribution

CLI Feedback:

$ morphir codegen --target spark --stream
Warning: spark-codegen does not support streaming, generating all at once...

Incremental Implementation Path

Recommended order for implementing extension capabilities:

Frontend:

  1. compile - Basic compilation (MVP)
  2. diagnostics - Better error messages
  3. compile-incremental - Watch mode support
  4. compile-streaming - Large project support
  5. compile-fragment - IDE integration

Backend:

  1. generate - Basic codegen (MVP)
  2. options-schema - Configurable output
  3. generate-module - Granular generation
  4. generate-incremental - Efficient rebuilds
  5. generate-streaming - Large project support

Extension Development Playbook

A step-by-step guide for building Morphir extensions from scratch.

Phase 1: Hello World (Day 1)

Goal: Verify your development setup and basic connectivity.

  1. Create project structure:
mkdir my-morphir-extension && cd my-morphir-extension
cargo new --lib .
cargo add wit-bindgen
  1. Implement minimal info interface:
// src/lib.rs
use morphir_extension::info::{ExtensionInfo, Info};

struct MyExtension;

impl Info for MyExtension {
fn get_info() -> ExtensionInfo {
ExtensionInfo {
id: "my-extension".into(),
name: "My Extension".into(),
version: "0.1.0".into(),
description: "My first Morphir extension".into(),
author: Some("My Name".into()),
homepage: None,
license: Some("Apache-2.0".into()),
types: vec![], // Will add later
}
}

fn ping() -> bool { true }
}
  1. Build and install:
# Build WASM component
cargo build --target wasm32-unknown-unknown --release
wasm-tools component new target/wasm32-unknown-unknown/release/my_extension.wasm \
-o my-extension.wasm

# Install to workspace
cp my-extension.wasm /path/to/workspace/.morphir/extensions/
  1. Verify installation:
morphir extension list
# Should show: my-extension 0.1.0 unknown (info only)

morphir extension ping my-extension
# Should show: my-extension: OK (Xms)

morphir extension info my-extension
# Should show full extension metadata

Checkpoint: Extension appears in list, responds to ping.

Phase 2: Basic Functionality (Week 1)

Goal: Implement core functionality (compile or generate).

For a Backend (Code Generator):

  1. Add generator interface:
use morphir_backend::generator::{Generator, GeneratorInfo, GenerationResult, Artifact};

impl Generator for MyExtension {
fn info() -> GeneratorInfo {
GeneratorInfo {
name: "my-generator".into(),
description: "Generates MyLang from Morphir IR".into(),
version: "0.1.0".into(),
target: TargetLanguage::Custom,
custom_target: Some("mylang".into()),
supported_granularities: vec![Granularity::Distribution],
}
}

fn generate_distribution(dist: Distribution, options: GenerationOptions) -> GenerationResult {
let artifacts = vec![
Artifact {
path: "output.mylang".into(),
content: transform_to_mylang(&dist),
source_map: None,
}
];
GenerationResult::Ok(artifacts)
}
}
  1. Update extension type:
fn get_info() -> ExtensionInfo {
ExtensionInfo {
// ... other fields
types: vec![ExtensionType::Codegen],
}
}
  1. Test with real IR:
# Create test IR
morphir build /path/to/test-project

# Run your generator
morphir codegen --target mylang

# Check output
ls .morphir-dist/

Checkpoint: Generator produces valid output files.

Phase 3: Production Quality (Month 1)

Goal: Add robustness features.

  1. Add capability discovery:
impl Capabilities for MyExtension {
fn list_capabilities() -> Vec<CapabilityInfo> {
vec![
CapabilityInfo {
id: "codegen/generate".into(),
description: "Basic code generation".into(),
available: true,
},
CapabilityInfo {
id: "codegen/options-schema".into(),
description: "Configurable options".into(),
available: true,
},
]
}

fn get_targets() -> Vec<String> {
vec!["mylang".into()]
}

fn get_options_schema() -> Vec<OptionSchema> {
vec![
OptionSchema {
name: "indent".into(),
option_type: OptionType::String,
default_value: Some("\" \"".into()),
description: "Indentation string".into(),
required: false,
},
]
}
}
  1. Add error handling:
fn generate_distribution(dist: Distribution, options: GenerationOptions) -> GenerationResult {
match try_generate(&dist, &options) {
Ok(artifacts) => GenerationResult::Ok(artifacts),
Err(e) => GenerationResult::Failed(vec![
Diagnostic {
severity: Severity::Error,
code: "GEN001".into(),
message: format!("Generation failed: {}", e),
location: None,
}
]),
}
}
  1. Add module-level generation:
fn generate_module(path: ModulePath, module: ModuleDefinition, ...) -> GenerationResult {
// Generate for single module
}

Checkpoint: Extension handles errors gracefully, shows in morphir extension info.

Phase 4: Advanced Features (Month 2+)

Goal: Add streaming and incremental support.

  1. Add streaming generation:
impl backend::Streaming for MyExtension {
fn generate_streaming(dist: Distribution, options: GenerationOptions) -> StreamHandle {
// Start background generation
let handle = start_generation_thread(dist, options);
handle.id
}

fn poll_result(handle: StreamHandle) -> Option<ModuleGenerationResult> {
// Return next completed module or None
}
}
  1. Add incremental generation:
impl backend::Incremental for MyExtension {
fn generate_incremental(
dist: Distribution,
changed_modules: Vec<ModuleChange>,
options: GenerationOptions,
) -> IncrementalGenerationResult {
// Only regenerate affected modules
}
}
  1. Update capabilities:
fn list_capabilities() -> Vec<CapabilityInfo> {
vec![
// ... existing
CapabilityInfo {
id: "codegen/generate-streaming".into(),
description: "Stream results as modules complete".into(),
available: true,
},
CapabilityInfo {
id: "codegen/generate-incremental".into(),
description: "Regenerate only changed modules".into(),
available: true,
},
]
}

Checkpoint: morphir codegen --target mylang --stream shows incremental progress.

Phase 5: Distribution (Month 3+)

Goal: Package and distribute your extension.

  1. Create package manifest:
# extension.toml
[extension]
id = "my-extension"
name = "My Extension"
version = "1.0.0"
description = "Generate MyLang from Morphir IR"
author = "My Name"
license = "Apache-2.0"
homepage = "https://github.com/me/my-extension"

type = "codegen"
component = "my-extension.wasm"

targets = ["mylang"]

[extension.options]
indent = { type = "string", default = " ", description = "Indentation" }
  1. Create distributable package:
morphir extension pack ./
# Creates: my-extension-1.0.0.morphir-ext.tgz
  1. Test installation:
# In a new workspace
morphir extension install my-extension-1.0.0.morphir-ext.tgz
morphir extension list
morphir codegen --target mylang

Checkpoint: Package installs cleanly on other machines.

Development Tips

TipDescription
Start minimalGet info working before adding features
Test earlyUse morphir extension ping after each change
Check capabilitiesmorphir extension info <name> shows what's detected
Watch logsmorphir daemon logs shows extension loading
Iterate fastHot-reload works with WASM components

Common Issues

IssueCauseFix
Extension not foundWrong locationCheck .morphir/extensions/
Ping failsMissing info implImplement info interface
No capabilitiesNot exportedExport capabilities interface
Codegen not triggeredWrong targetCheck get_targets() returns correct value
Missing optionsNo schemaImplement get_options_schema()

Testing Checklist

  • morphir extension list shows your extension
  • morphir extension ping <name> returns OK
  • morphir extension info <name> shows correct metadata
  • Capabilities appear in info output
  • Target/language appears in info output
  • Options schema appears in info output
  • Basic generation produces valid output
  • Errors produce helpful diagnostics
  • Streaming shows incremental progress (if implemented)
  • Incremental skips unchanged modules (if implemented)

Future: Alternative Extension Runtimes

Beyond WASM components and native executables, several alternative extension runtimes are worth exploring for different trade-offs in performance, ease of authoring, and sandboxing.

QuickJS

QuickJS is a small, embeddable JavaScript engine that could enable JavaScript/TypeScript extensions without WASM compilation.

Potential Benefits:

  • Familiar language for web developers
  • No compilation step (interpret directly)
  • Small runtime footprint (~210KB)
  • ES2020 support

Example:

// extensions/my-codegen.js
export function getInfo() {
return {
id: "my-codegen",
name: "My Code Generator",
version: "1.0.0",
type: "codegen"
};
}

export function generate(ir, options) {
// Transform IR to target code
return {
files: [
{ path: "output.ts", content: generateTypeScript(ir) }
]
};
}

Discovery:

.morphir/extensions/
└── my-codegen.js # Detected as QuickJS extension

Javy (JavaScript → WASM)

Javy compiles JavaScript to WASM, combining JavaScript's ease of authoring with WASM's sandboxing.

Potential Benefits:

  • Write in JavaScript, run as sandboxed WASM
  • Leverage existing JS ecosystem
  • Same security model as WASM components
  • AOT compilation for better performance than interpretation

Workflow:

# Author in JavaScript
cat > my-extension.js << 'EOF'
export function generate(ir) { ... }
EOF

# Compile to WASM
javy compile my-extension.js -o my-extension.wasm

# Install as normal WASM extension
morphir extension install my-extension.wasm

Lua

Lua is a lightweight, embeddable scripting language often used for game scripting and configuration.

Potential Benefits:

  • Extremely lightweight (~300KB)
  • Fast startup time
  • Simple, learnable syntax
  • Battle-tested embedding API
  • Good for simple transformations

Example:

-- extensions/my-transform.lua
function get_info()
return {
id = "my-transform",
name = "My Transform",
version = "1.0.0",
type = "validator"
}
end

function validate(ir)
-- Check IR constraints
local errors = {}
for _, module in ipairs(ir.modules) do
if not module.doc then
table.insert(errors, {
module = module.path,
message = "Missing module documentation"
})
end
end
return { valid = #errors == 0, errors = errors }
end

Morphir IR Extensions (Self-Hosting)

The most interesting possibility: extensions written in Morphir itself, compiled to IR, and interpreted or compiled by the daemon.

Potential Benefits:

  • Dogfooding: Use Morphir to extend Morphir
  • Type-safe extension authoring
  • Extensions benefit from Morphir's guarantees
  • Can generate extensions to multiple targets
  • Ultimate validation of Morphir's expressiveness

Example:

-- extensions/MyCodegen.elm
module MyCodegen exposing (generate)

import Morphir.IR.Distribution exposing (Distribution)
import Morphir.Extension exposing (GeneratedFile)

generate : Distribution -> List GeneratedFile
generate distribution =
distribution.modules
|> List.concatMap generateModule

generateModule : Module -> List GeneratedFile
generateModule mod =
[ { path = modulePath mod ++ ".scala"
, content = moduleToScala mod
}
]

Compilation:

# Compile extension to IR
morphir build extensions/my-codegen/

# The IR itself becomes the extension
# Daemon interprets or JIT-compiles the IR

Architecture:

┌─────────────────────────────────────────────────────┐
│ Morphir Daemon │
├─────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ WASM Runtime│ │ IR Interp. │ │ Native │ │
│ │ (wasmtime) │ │ (Morphir IR)│ │ (exec) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │
│ │ .wasm │ │ .mir │ │ bin/* │ │
│ │ codegen │ │ codegen │ │ codegen │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────┘

Considerations:

  • Requires IR interpreter or JIT in the daemon
  • Bootstrap problem: need initial extensions to build Morphir extensions
  • Performance may vary (interpreted vs compiled)
  • Could compile IR extensions to WASM for production

Runtime Comparison

RuntimeSandboxedStartupAuthoringPerformanceSize
WASM Component✓ StrongMediumRust/Go/CHighMedium
Native Executable✗ ProcessFastAnyHighestVaries
QuickJS✓ EmbeddedFastJS/TSMediumSmall
Javy✓ WASMMediumJS/TSMedium-HighMedium
Lua✓ EmbeddedFastestLuaMediumTiny
Morphir IR✓ SemanticVariesMorphirVariesSmall

Exploration Status

RuntimeStatusPriority
WASM ComponentSupportedPrimary
Native ExecutableSupportedPrimary
QuickJSFutureMedium
JavyFutureMedium
LuaFutureLow
Morphir IRFutureHigh (dogfooding)

Note: Alternative runtimes are documented for future exploration. The current implementation focuses on WASM components and native executables. Morphir IR extensions are particularly interesting as they would validate Morphir's expressiveness and enable self-hosting.