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.

The linked-metadata draft develops attributes.facts and scoped predicates for a future V4 minor revision. It does not change the 4.0.0 attribute encoding described here.

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)