Skip to main content

MLIR Intermediate Representation

Overview

The Ora compiler includes a comprehensive MLIR (Multi-Level Intermediate Representation) lowering system that provides an intermediate representation for advanced analysis, optimization, and alternative code generation paths. This system complements the primary Yul backend and enables sophisticated compiler transformations.

What is MLIR?

MLIR (Multi-Level Intermediate Representation) is a compiler infrastructure developed by the LLVM community (https://mlir.llvm.org) that provides:

  • Multi-level representation: Support for different abstraction levels
  • Extensible dialects: Custom operation and type definitions
  • Optimization passes: Pluggable transformation framework
  • Analysis infrastructure: Data flow and control flow analysis

The Ora compiler integrates with the existing MLIR framework to provide Ora-specific lowering and analysis capabilities.

Ora MLIR Integration

The Ora compiler integrates with the LLVM MLIR framework (https://mlir.llvm.org) to:

  1. Represent Ora semantics in a structured intermediate form using MLIR operations and types
  2. Enable advanced analysis for verification and optimization using MLIR's pass infrastructure
  3. Support alternative backends beyond Yul by leveraging MLIR's code generation capabilities
  4. Provide debugging information with source location preservation using MLIR's location tracking

Our implementation provides Ora-specific dialects, operations, and lowering passes that work within the existing MLIR ecosystem.

Type System Mapping

Primitive Types

Ora primitive types are mapped to MLIR as follows:

Ora TypeMLIR TypeNotes
u8 - u256iNN-bit unsigned integers
i8 - i256iNN-bit signed integers
booli1Single bit boolean
addressi160With ora.address attribute
string!ora.stringOra dialect type
bytes!ora.bytesOra dialect type
void()MLIR void type

Complex Types

Ora TypeMLIR TypeDescription
[T; N]memref<NxT, space>Fixed-size arrays with memory space
slice[T]!ora.slice<T>Dynamic slices
map[K, V]!ora.map<K, V>Key-value mappings
doublemap[K1, K2, V]!ora.doublemap<K1, K2, V>Nested mappings
struct { ... }!llvm.struct<...>Struct types
enum!ora.enum<name, repr>Enumeration types
!T!ora.error<T>Error types
!T1 | T2!ora.error_union<T1, T2>Error unions

Memory Region Semantics

Ora's memory regions are represented in MLIR using memory spaces:

Storage (Space 1)

%storage_var = memref.alloca() {ora.region = "storage"} : memref<1xi256, 1>
  • Persistent contract state
  • Transactional semantics
  • Gas costs for access

Memory (Space 0)

%memory_var = memref.alloca() : memref<1xi256, 0>
  • Transient execution memory
  • Cleared between calls
  • Lower gas costs

TStore (Space 2)

%tstore_var = memref.alloca() {ora.region = "tstore"} : memref<1xi256, 2>
  • Transient storage window
  • Temporary persistence
  • Intermediate gas costs

Expression Lowering

Arithmetic Operations

Ora arithmetic expressions are lowered to MLIR arith dialect operations:

let result = a + b * c;
%mul = arith.muli %b, %c : i256
%add = arith.addi %a, %mul : i256

Comparison Operations

let is_greater = x > y;
%cmp = arith.cmpi sgt, %x, %y : i256

Logical Operations

Logical operators use short-circuit evaluation:

let result = condition1 && condition2;
%result = scf.if %condition1 -> i1 {
scf.yield %condition2 : i1
} else {
%false = arith.constant false
scf.yield %false : i1
}

Field Access

Struct field access uses LLVM operations:

let value = my_struct.field;
%value = llvm.extractvalue %my_struct[0] : !llvm.struct<(i256, i256)>

Function Calls

let result = my_function(arg1, arg2);
%result = func.call @my_function(%arg1, %arg2) : (i256, i256) -> i256

Statement Lowering

Control Flow

If Statements

if (condition) {
// then block
} else {
// else block
}
scf.if %condition {
// then region
} else {
// else region
}

While Loops

while (condition) {
// body
}
scf.while (%arg0 = %initial) : (i256) -> () {
%cond = // evaluate condition
scf.condition(%cond)
} do {
// loop body
scf.yield %next_value : i256
}

For Loops

for (array) |item| {
// body
}
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%len = // array length
scf.for %i = %c0 to %len step %c1 {
%item = memref.load %array[%i] : memref<?xi256>
// loop body
}

Switch Statements

switch (value) {
case 1 => // handle 1
case 2...5 => // handle range
else => // default case
}
cf.switch %value : i256, [
default: ^default,
1: ^case1,
2: ^case2_5,
3: ^case2_5,
4: ^case2_5,
5: ^case2_5
]

Ora-Specific Features

Move Statements

move 100 from sender to receiver;
%amount = arith.constant 100 : i256
%move_op = ora.move %amount from %sender to %receiver {ora.move = true}

Log Statements

log Transfer(from: sender, to: receiver, amount: value);
ora.log "Transfer"(%sender, %receiver, %value) {
indexed = [true, true, false]
} : (i160, i160, i256) -> ()

Try-Catch Blocks

try {
risky_operation();
} catch (error) {
handle_error(error);
}
%result = scf.execute_region -> !ora.error<()> {
%success = func.call @risky_operation() : () -> !ora.error<()>
scf.yield %success : !ora.error<()>
}
%is_error = ora.is_error %result : !ora.error<()>
scf.if %is_error {
%error = ora.unwrap_error %result : !ora.error<()>
func.call @handle_error(%error) : (()) -> ()
}

Verification Features

Old Expressions

ensures balance == old(balance) + amount;
%old_balance = // captured at function entry
%postcond = arith.cmpi eq, %balance, %expected : i256
ora.assert %postcond {ora.ensures = true, ora.old = %old_balance}

Quantified Expressions

requires forall(i in 0...array.length) array[i] > 0;
%all_positive = ora.forall %i in %range {
%elem = memref.load %array[%i] : memref<?xi256>
%zero = arith.constant 0 : i256
%positive = arith.cmpi sgt, %elem, %zero : i256
ora.yield %positive : i1
} : (index) -> i1
ora.assert %all_positive {ora.requires = true}

Function Contracts

Requires Clauses

fn transfer(amount: u256) 
requires amount > 0
requires balance >= amount
{
// function body
}
func.func @transfer(%amount: i256) {
%zero = arith.constant 0 : i256
%amount_positive = arith.cmpi sgt, %amount, %zero : i256
ora.assert %amount_positive {ora.requires = true}

%balance = // load balance
%sufficient = arith.cmpi uge, %balance, %amount : i256
ora.assert %sufficient {ora.requires = true}

// function body
}

Ensures Clauses

fn transfer(amount: u256)
ensures balance == old(balance) - amount
{
// function body
}
func.func @transfer(%amount: i256) {
%old_balance = memref.load %balance_ref : memref<1xi256>

// function body

%new_balance = memref.load %balance_ref : memref<1xi256>
%expected = arith.subi %old_balance, %amount : i256
%postcond = arith.cmpi eq, %new_balance, %expected : i256
ora.assert %postcond {ora.ensures = true}
}

Compiler Integration

CLI Usage

Generate MLIR output:

ora compile --emit-mlir contract.ora

Verify MLIR correctness:

ora compile --mlir-verify contract.ora

Run optimization passes:

ora compile --mlir-passes="canonicalize,cse" contract.ora

Build Integration

The MLIR lowering integrates with the existing compilation pipeline:

  1. Lexing and Parsing: Source code → AST
  2. Semantic Analysis: Type checking and validation
  3. MLIR Lowering: AST → MLIR module
  4. MLIR Passes: Optimization and analysis
  5. Backend Selection: MLIR → Yul (primary) or other targets

Debugging and Analysis

Source Location Preservation

All MLIR operations preserve source location information:

%result = arith.addi %a, %b : i256 loc("contract.ora":15:8)

Error Reporting

MLIR lowering provides detailed error messages with source context:

error: Type mismatch in binary operation
--> contract.ora:15:8
|
15 | let result = balance + "invalid";
| ^ expected numeric type, found string

Verification Passes

Custom verification passes check Ora-specific constraints:

  • Memory region usage validation
  • Contract invariant checking
  • Gas usage analysis
  • Security pattern detection

Performance Characteristics

The MLIR lowering system is designed for:

  • Fast compilation: Efficient lowering algorithms
  • Memory efficiency: Minimal overhead during compilation
  • Scalability: Handles large smart contracts
  • Deterministic output: Consistent results for testing

Future Enhancements

Planned improvements include:

  1. Enhanced Dialect: Complete Ora dialect with custom operations
  2. Advanced Analysis: Data flow and control flow analysis
  3. Optimization Passes: Ora-specific optimizations
  4. Alternative Backends: LLVM, WebAssembly, and native targets
  5. Formal Verification: Integration with verification tools

Examples

Complete Contract Example

contract Token {
storage balance: map[address, u256];
storage total_supply: u256;

fn transfer(to: address, amount: u256)
requires balance[msg.sender] >= amount
ensures balance[to] == old(balance[to]) + amount
ensures balance[msg.sender] == old(balance[msg.sender]) - amount
{
move amount from msg.sender to to;
log Transfer(from: msg.sender, to: to, amount: amount);
}
}

This generates MLIR with:

  • Memory space annotations for storage variables
  • Precondition and postcondition assertions
  • Move operation with transfer semantics
  • Event logging with indexed parameters
  • Source location preservation throughout

The MLIR representation enables advanced analysis and optimization while maintaining the semantic integrity of the original Ora code.

MLIR Resources

Learn More About MLIR

Ora MLIR Implementation

  • Source Code: Available in the src/mlir/ directory of the Ora repository
  • Technical Documentation: See docs/mlir-lowering.md for implementation details
  • API Reference: See docs/mlir-api.md for developer documentation
  • Troubleshooting: See docs/mlir-troubleshooting.md for common issues

The Ora MLIR integration leverages the powerful MLIR framework developed by the LLVM community to provide advanced compiler capabilities for smart contract development.