Skip to main content

Attributes System

This document defines the attributes system for Morphir IR v4, replacing the generic type parameter approach with structured, semantic attributes.

Overview

Problem

In IR v3, types and values are parameterized by a generic attribute type:

// v3 approach
pub type Type(a) {
Variable(attributes: a, name: TypeVariable)
Reference(attributes: a, fqname: FQName, args: List(Type(a)))
// ...
}

This has limitations:

  • No standard structure for common metadata (source locations, constraints)
  • Backends must pattern-match on arbitrary attribute types
  • No way to express semantic constraints (numeric bounds, encodings)
  • Extension points are ad-hoc

Solution

Replace the generic parameter with structured attribute types that have:

  1. Well-known fields for common concerns (source location, constraints)
  2. Extension map for user-defined metadata (RDF-like triples)
  3. Separate types for Type vs. Value attributes (different semantic needs)

Design

TypeAttributes

Attributes attached to type expressions.

pub type TypeAttributes {
TypeAttributes(
/// Source location for error reporting
source: Option(SourceLocation),
/// Semantic constraints on values of this type
constraints: Option(TypeConstraints),
/// User-defined extensions (predicate -> value)
extensions: Dict(FQName, Value),
)
}

/// Empty attributes (replaces unit `()` from v3)
pub const empty_type_attributes = TypeAttributes(
source: None,
constraints: None,
extensions: dict.new(),
)

ValueAttributes

Attributes attached to value expressions.

pub type ValueAttributes {
ValueAttributes(
/// Source location for error reporting
source: Option(SourceLocation),
/// Inferred or annotated type
inferred_type: Option(Type),
/// Value-level constraints/properties
properties: Option(ValueProperties),
/// User-defined extensions
extensions: Dict(FQName, Value),
)
}

pub type ValueProperties {
ValueProperties(
/// Is this a compile-time constant?
is_constant: Bool,
/// Purity: does this have side effects?
purity: Purity,
)
}

pub type Purity {
Pure // No side effects
Effectful // Has effects (IO, state, etc.)
Unknown // Not yet analyzed
}

pub const empty_value_attributes = ValueAttributes(
source: None,
inferred_type: None,
properties: None,
extensions: dict.new(),
)

TypeConstraints

Semantic constraints on types, particularly for numeric and string types.

pub type TypeConstraints {
TypeConstraints(
numeric: Option(NumericConstraint),
string: Option(StringConstraint),
collection: Option(CollectionConstraint),
custom: List(CustomConstraint),
)
}

pub type NumericConstraint {
/// Arbitrary precision (default for Int)
Arbitrary
/// Fixed-width signed integer
Signed(bits: IntWidth)
/// Fixed-width unsigned integer
Unsigned(bits: IntWidth)
/// Floating point
FloatingPoint(bits: FloatWidth)
/// Bounded range (JSON Schema style)
Bounded(min: Option(BigInt), max: Option(BigInt))
/// Fixed-point decimal
Decimal(precision: Int, scale: Int)
}

pub type IntWidth {
I8
I16
I32
I64
}

pub type FloatWidth {
F32
F64
}

pub type StringConstraint {
StringConstraint(
encoding: Option(StringEncoding),
min_length: Option(Int),
max_length: Option(Int),
pattern: Option(String), // Regex pattern
)
}

pub type StringEncoding {
UTF8
UTF16
ASCII
Latin1
}

pub type CollectionConstraint {
CollectionConstraint(
min_length: Option(Int),
max_length: Option(Int),
unique_items: Bool,
)
}

pub type CustomConstraint {
CustomConstraint(
predicate: FQName,
arguments: List(Value),
)
}

Updated Type Definition

With attributes, the Type definition becomes non-generic:

// v4 approach (no generic parameter)
pub type Type {
Variable(attributes: TypeAttributes, name: TypeVariable)
Reference(attributes: TypeAttributes, fqname: FQName, args: List(Type))
Tuple(attributes: TypeAttributes, elements: List(Type))
Record(attributes: TypeAttributes, fields: List(Field))
ExtensibleRecord(attributes: TypeAttributes, variable: TypeVariable, fields: List(Field))
Function(attributes: TypeAttributes, arg: Type, result: Type)
Unit(attributes: TypeAttributes)
}

pub type Field {
Field(name: Name, field_type: Type)
}

Extensions as RDF-like Triples

The extensions field in attributes acts as an RDF-like graph where:

  • Subject: The IR node being annotated (implicit)
  • Predicate: The FQName key
  • Object: The Value

This enables:

  • User-defined decorators stored in IR
  • Tool-specific metadata
  • Custom linting/analysis rules

Example:

{
"Reference": {
"attributes": {
"extensions": {
"my-org/decorators:deprecated#reason": {
"Literal": { "literal": { "StringLiteral": { "value": "Use NewType instead" } } }
},
"my-org/decorators:since#version": {
"Literal": { "literal": { "StringLiteral": { "value": "2.0.0" } } }
}
}
},
"fqname": "my-org/domain:types#old-type"
}
}

Constraint Entailment (Subtyping)

Constraints form a partial order for subtype checking:

Signed { bits: 8 }  ⊑  Signed { bits: 16 }  ⊑  Signed { bits: 32 }  ⊑  Signed { bits: 64 }  ⊑  Arbitrary
Unsigned { bits: 8 } ⊑ Unsigned { bits: 16 } ⊑ ... ⊑ Arbitrary

Bounded { min: a, max: b } ⊑ Signed { bits: n } when a >= -(2^(n-1)) && b < 2^(n-1)
Bounded { min: a, max: b } ⊑ Unsigned { bits: n } when a >= 0 && b < 2^n

This enables:

  • Implicit widening: Int32 value assigned to Int variable
  • Explicit narrowing: Int to Int32 requires conversion function with Maybe result

JSON Serialization

Attributes are serialized with omission of empty/None fields for compactness:

// Minimal (all defaults)
{ "Reference": { "fqname": "morphir/sdk:basics#int" } }

// With source location only
{
"Reference": {
"attributes": { "source": { "file": "src/Domain.elm", "line": 42, "col": 5 } },
"fqname": "morphir/sdk:basics#int"
}
}

// With numeric constraint
{
"Reference": {
"attributes": {
"constraints": {
"numeric": { "Signed": { "bits": 32 } }
}
},
"fqname": "morphir/sdk:int#int32"
}
}

// With bounded constraint (JSON Schema style)
{
"Reference": {
"attributes": {
"constraints": {
"numeric": { "Bounded": { "min": 0, "max": 100 } }
}
},
"fqname": "morphir/sdk:basics#int"
}
}

Migration from v3

Backward Compatibility

  • Decoders accept Type(a) format (v3) and convert to Type with TypeAttributes.empty
  • The attributes field is optional in JSON; missing = empty

Forward Path

  1. Phase 1: Add attributes types, keep generic parameter
  2. Phase 2: Default generic parameter to TypeAttributes/ValueAttributes
  3. Phase 3: Remove generic parameter, use concrete attribute types

Boundary Extensions

For functions that cross component boundaries (Wasm imports/exports), the frontend inserts boundary metadata into the extensions field. This enables backends to generate appropriate ABI code.

Well-Known Boundary Extension Keys

Extension FQNamePurpose
morphir/wasm:boundary#exportMark function as Wasm export
morphir/wasm:boundary#importMark function as Wasm import
morphir/wasm:boundary#abi-signatureComputed Canonical ABI signature
morphir/wasm:boundary#memory-layoutsMemory layouts for complex types

Export Example

// Source (with attribute)
@wasm_export
pub fn calculate_premium(policy: Policy, risk: Risk) -> Premium {
// ...
}

IR representation:

{
"ValueDefinition": {
"attributes": {
"extensions": {
"morphir/wasm:boundary#export": {
"Literal": { "literal": { "BoolLiteral": { "value": true } } }
},
"morphir/wasm:boundary#abi-signature": {
"Record": {
"fields": {
"params": { "List": { "items": [
{ "Record": { "fields": { "name": "policy_ptr", "type": "i32" } } },
{ "Record": { "fields": { "name": "risk_ptr", "type": "i32" } } }
] } },
"results": { "List": { "items": [
{ "Record": { "fields": { "type": "i32" } } }
] } }
}
}
}
}
},
"body": { "..." : "..." }
}
}

Import Example

// Source (with attribute)
@wasm_import("pricing-engine")
pub fn get_base_rate(product_code: ProductCode) -> Rate

IR representation:

{
"ValueDefinition": {
"attributes": {
"extensions": {
"morphir/wasm:boundary#import": {
"Literal": { "literal": { "StringLiteral": { "value": "pricing-engine" } } }
}
}
},
"body": { "..." : "..." }
}
}

Boundary Validation Rules

Functions marked as boundary functions are validated:

  1. No type variables — generics cannot cross boundaries
  2. No extensible records — row polymorphism not allowed
  3. All types must be boundary-compatible — must map to Canonical ABI
  4. Arbitrary-precision Int handling — configurable (error/warn/default to i64)

Validation behavior is configured in morphir.toml:

[wasm.boundary]
arbitrary_int = "error" # "error" | "warn" | "i64" | "i32"
arbitrary_float = "f64" # "error" | "warn" | "f64"

Open Questions

  1. Constraint inference: Should frontends infer constraints from type aliases?

  2. Constraint composition: How do constraints combine when multiple apply?

  3. Custom constraint validation: How are CustomConstraint predicates evaluated?

  4. Boundary ABI storage: Should computed ABI signatures be:

    • Stored in IR extensions (current approach)
    • Computed on-demand by backends
    • Stored in a separate artifact (.abi.json)