Extension Development Tutorial
Learn how to create Morphir extensions for new languages or targets.
Overview
Morphir extensions are WASM modules that implement the Frontend and/or Backend traits. Extensions can be:
- Builtin: Bundled with the CLI (like Gleam)
- Registry: Installed from a registry
- Local: Referenced by path in
morphir.toml
Extension Types
Frontend Extension
Converts source code → Morphir IR V4
impl Frontend for MyExtension {
fn compile(&self, request: CompileRequest) -> Result<CompileResult> {
let ir = serde_json::json!({"formatVersion": 4});
Ok(CompileResult {
success: true,
ir_version: Some(request.options.ir_version),
ir: Some(ir),
diagnostics: vec![],
modules: request.package.exposed_modules,
})
}
}
Backend Extension
Converts Morphir IR V4 → Generated code
impl Backend for MyExtension {
fn generate(&self, request: GenerateRequest) -> Result<GenerateResult> {
// Read Morphir IR
// Generate target language code
// Return GenerateResult
}
}
Creating a Simple Extension
Step 1: Create Extension Crate
cargo new --lib my-morphir-extension
cd my-morphir-extension
Step 2: Configure for WASM
In Cargo.toml:
[lib]
crate-type = ["cdylib"]
[dependencies]
morphir-extension-sdk = { path = "../../morphir-extension-sdk" }
extism-pdk = "1.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Step 3: Implement Extension
In src/lib.rs:
use morphir_extension_sdk::prelude::*;
#[derive(Default)]
pub struct MyExtension;
impl Extension for MyExtension {
fn info() -> ExtensionInfo {
ExtensionInfo {
id: "my-extension".into(),
name: "My Extension".into(),
version: env!("CARGO_PKG_VERSION").into(),
types: vec![ExtensionType::Frontend],
..ExtensionInfo::default()
}
}
fn capabilities() -> ExtensionCapabilities {
ExtensionCapabilities {
frontend: Some(FrontendCapability {
languages: vec![LanguageCapability {
id: "my-language".into(),
file_extensions: vec![".my".into()],
}],
ir_versions: vec!["4".into()],
compile: true,
incremental: false,
fragments: false,
}),
..ExtensionCapabilities::default()
}
}
}
impl Frontend for MyExtension {
fn compile(&self, request: CompileRequest) -> Result<CompileResult> {
let ir = serde_json::json!({"formatVersion": 4});
Ok(CompileResult {
success: true,
ir_version: Some(request.options.ir_version),
ir: Some(ir),
diagnostics: vec![],
modules: request.package.exposed_modules,
})
}
fn supported_languages() -> Vec<String> {
vec!["my-language".into()]
}
fn file_extensions() -> Vec<String> {
vec![".my".into()]
}
}
Step 4: Build WASM
cargo build --target wasm32-unknown-unknown --release
Step 5: Use Extension
Add to morphir.toml:
[extensions.my-extension]
path = "./extensions/my-extension.wasm"
enabled = true
Builtin Extensions
Builtin extensions (like Gleam) are:
- Compiled as part of the CLI build
- Bundled in the CLI binary or resources
- Automatically discovered
To make an extension builtin:
- Add to
morphir-devkit/src/extensions.rs - Bundle WASM in build process
- Update extension discovery
Extension Discovery
Extensions are discovered in this order:
- Builtin extensions (highest priority)
- Extensions from
morphir.toml - Registry extensions
Extension Configuration
Extensions can have custom configuration:
[extensions.my-extension]
path = "./extensions/my-extension.wasm"
enabled = true
[extensions.my-extension.config]
custom_setting = "value"
Access in extension:
let custom_setting = request.options.extra
.get("custom_setting")
.and_then(|v| v.as_str());
Testing Extensions
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compile() {
let ext = MyExtension;
let request = CompileRequest {
language_id: "my-language".into(),
documents: vec![SourceDocument {
uri: "file:///workspace/Example.my".into(),
language_id: "my-language".into(),
version: 1,
text: "module Example".into(),
}],
package: CompilePackage {
name: "local/example".into(),
exposed_modules: vec!["Example".into()],
},
dependencies: vec![],
options: CompileOptions {
types_only: false,
ir_version: "4".into(),
extra: Default::default(),
},
};
let result = ext.compile(request).unwrap();
assert!(result.success);
assert_eq!(result.ir_version.as_deref(), Some("4"));
assert!(result.ir.is_some());
}
}
Integration Tests
Use the CLI to test extensions:
morphir compile --language my-language --input src/
Best Practices
- Error Handling: Provide clear diagnostics with source locations
- Performance: Optimize parsing and conversion for large codebases
- Testing: Include comprehensive test coverage
- Documentation: Document language-specific features and limitations
Next Steps
- Read Extension System Design
- See Architecture Overview
- Check Development Guide