Skip to content
This repository was archived by the owner on Jun 11, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ authors = ["PRAGMA <hello@pragma.io>", "Lucas Rosa <x@rvcas.dev>"]
repository = "https://github.com/pragma-org/uplc"
homepage = "https://github.com/pragma-org/uplc"
documentation = "https://docs.rs/amaru-uplc"
keywords = ["uplc", "plutus", "cardano", "smart-contracts", "cek-machine"]
categories = ["cryptography::cryptocurrencies", "compilers", "parser-implementations"]

[workspace.dependencies]
# Internal
Expand Down
181 changes: 178 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,97 @@
# A Lighting Fast UPLC Evaluator
# amaru-uplc

## Dev
[![Crates.io](https://img.shields.io/crates/v/amaru-uplc.svg)](https://crates.io/crates/amaru-uplc)
[![docs.rs](https://img.shields.io/docsrs/amaru-uplc)](https://docs.rs/amaru-uplc)
[![License](https://img.shields.io/crates/l/amaru-uplc.svg)](LICENSE)

A lightning-fast [UPLC](https://plutus.readthedocs.io/en/latest/reference/uplc-introduction.html)
(Untyped Plutus Language Core) evaluator implemented as a
[CEK machine](https://en.wikipedia.org/wiki/CEK_Machine) in Rust.

UPLC is the low-level bytecode compiled from [Plutus](https://plutus.readthedocs.io/), the
smart-contract language for the [Cardano](https://cardano.org/) blockchain.

## Features

- Full CEK machine evaluation for Plutus V1, V2, and V3
- Arena-allocated term representation for minimal allocator overhead
- Built-in UPLC text-format parser
- Flat and CBOR binary encoding/decoding
- Configurable cost models with `ExBudget` tracking
- Comprehensive built-in functions:
- Arithmetic and integer operations
- Byte-string and UTF-8 string operations
- Cryptographic hashing (SHA-256, SHA-3, Blake2b-256/224, Keccak-256, RIPEMD-160)
- Signature verification (Ed25519, ECDSA secp256k1, Schnorr secp256k1)
- BLS12-381 elliptic curve operations (G1, G2, Miller loop, pairing)
- Bitwise operations (Plutus V3)
- Plutus data constructors and destructors

## Installation

```toml
[dependencies]
amaru-uplc = "0.4.0"
```

## Usage

### Building and evaluating a program

```rust
use amaru_uplc::{
arena::Arena,
binder::DeBruijn,
program::{Program, Version},
term::Term,
};

let arena = Arena::new();

// Build a term: addInteger 1 3
let term = Term::add_integer(&arena)
.apply(&arena, Term::integer_from(&arena, 1))
.apply(&arena, Term::integer_from(&arena, 3));

let version = Version::plutus_v3(&arena);
let program = Program::<DeBruijn>::new(&arena, version, term);
let result = program.eval(&arena);

assert_eq!(result.term.unwrap(), Term::integer_from(&arena, 4));
```

### Parsing UPLC source text

```rust
use amaru_uplc::{arena::Arena, syn::parse_program};

let arena = Arena::new();
let result = parse_program(&arena, "(program 1.1.0 (addInteger 1 3))");
```

### Selecting Plutus version and budget

```rust
use amaru_uplc::machine::{ExBudget, PlutusVersion};

// Evaluate under Plutus V1 semantics with an unlimited budget
let result = program.eval_version_budget(&arena, PlutusVersion::V1, ExBudget::max());

// Inspect consumed budget and trace logs
println!("CPU: {}", result.info.consumed_budget.cpu);
println!("Mem: {}", result.info.consumed_budget.mem);
for line in &result.info.logs {
println!("TRACE: {line}");
}
```

## Development

### Prerequisites

- [Rust](https://rustup.rs/) (stable toolchain)
- [just](https://github.com/casey/just) — task runner used for common workflows
- [cargo-nextest](https://nexte.st/) — faster test runner (`cargo install cargo-nextest`)

### Conformance tests

Expand All @@ -17,7 +108,7 @@ cargo test -p amaru-uplc --tests

### Refreshing the textual suite

Install [just](https://github.com/casey/just), then:
The conformance test suite is not vendored. Download it before running tests:

```bash
just download-plutus-tests
Expand All @@ -26,3 +117,87 @@ just download-plutus-tests
This replaces `crates/uplc/tests/conformance/textual/` with the latest fixtures from [IntersectMBO/plutus](https://github.com/IntersectMBO/plutus)'s `plutus-conformance/test-cases/uplc/evaluation/`. The `flat/` suite is untouched; it isn't guaranteed to track upstream byte-for-byte and has no automated sync.

See `crates/uplc/tests/conformance/flat/README.md` for the flat layout, hand-crafted negatives, and which classes of upstream fixtures live only in `textual/`.

### Testing

Run the full test suite (unit tests + conformance tests):

```bash
# Using the built-in test harness
cargo test

# Or directly with nextest
cargo nextest run


```

Run only the unit tests (skip conformance):

```bash
cargo nextest run --lib
```

Run only the conformance tests:

```bash
cargo nextest run --test conformance
```

Run a specific test by name:

```bash
cargo nextest run add_integer
cargo nextest run fibonacci
```

Run tests matching a pattern:

```bash
cargo nextest run encode_cbor
```

Run tests in release mode:

```bash
cargo nextest run --release
```

List all available tests without running them:

```bash
cargo test -- --list
```

### Benchmarks

Run all benchmarks:

```bash
cargo bench
```

Run a specific benchmark suite:

```bash
# Microbenchmarks (addInteger, fibonacci)
cargo bench --bench simple

# Real-world Plutus use-case scripts
cargo bench --bench use_cases

# High-throughput bulk evaluation over a large script corpus
cargo bench --bench turbo
```

### Documentation

Build and open the API docs locally:

```bash
cargo doc -p amaru-uplc --open
```

## License

Apache-2.0 — see [LICENSE](LICENSE).
3 changes: 3 additions & 0 deletions crates/uplc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
keywords.workspace = true
categories.workspace = true
readme = "../../README.md"
publish = true

[build-dependencies]
Expand Down
26 changes: 25 additions & 1 deletion crates/uplc/src/arena.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,48 @@
//! Arena allocator for zero-copy UPLC term construction.
//!
//! [`Arena`] wraps [`bumpalo::Bump`] for general term allocation and stores
//! [`Integer`] values in a stable append-only vector so that
//! raw references into them remain valid across further allocations.

use std::any::type_name;

use append_only_vec::AppendOnlyVec;
use bumpalo::Bump;

use crate::constant::Integer;

/// Arena allocator for zero-copy UPLC term construction.
///
/// General allocations go through the bump allocator; [`Integer`]
/// values are stored separately in a stable append-only vector so raw references into them
/// remain valid across further allocations.
pub struct Arena {
bump: Bump,
integers: AppendOnlyVec<Integer>,
}

impl Arena {
/// Creates a new empty arena.
pub fn new() -> Self {
Self {
bump: Bump::new(),
integers: AppendOnlyVec::new(),
}
}

/// Creates an arena reusing an existing [`bumpalo::Bump`] allocator.
pub fn from_bump(bump: Bump) -> Self {
Self {
bump,
integers: AppendOnlyVec::new(),
}
}

/// Allocates `value` in the arena and returns a mutable reference to it.
///
/// # Panics
///
/// Panics in debug builds if `T` is [`Integer`]; use [`Arena::alloc_integer`] instead.
pub fn alloc<T>(&self, value: T) -> &mut T {
if cfg!(debug_assertions) {
assert!(
Expand All @@ -35,6 +53,10 @@ impl Arena {
self.bump.alloc(value)
}

/// Allocates an [`Integer`] with a stable address.
///
/// Unlike the bump allocator, integers are stored in an append-only vector
/// so that existing references remain valid after subsequent allocations.
pub fn alloc_integer(&self, value: Integer) -> &Integer {
let idx = self.integers.push(value);
&self.integers[idx]
Expand All @@ -44,8 +66,10 @@ impl Arena {
&self.bump
}

/// Resets the arena, freeing all allocated values.
///
/// All references previously returned by this arena are invalidated.
pub fn reset(&mut self) {
// Drop all allocated integers
self.integers = AppendOnlyVec::new();
self.bump.reset();
}
Expand Down
8 changes: 8 additions & 0 deletions crates/uplc/src/binder/debruijn.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
//! De Bruijn index binder strategy.

use crate::arena::Arena;

use super::{Binder, Eval};

/// A De Bruijn index variable reference.
///
/// The index represents the number of lambda abstractions between the variable occurrence
/// and its binding site (1-based: index 1 refers to the immediately enclosing lambda).
#[derive(Debug, Eq, PartialEq)]
pub struct DeBruijn(usize);

impl DeBruijn {
/// Allocates a De Bruijn index.
pub fn new(arena: &Arena, i: usize) -> &Self {
arena.alloc(DeBruijn(i))
}

/// Allocates a De Bruijn index of 0 (used as a placeholder for lambda parameters).
pub fn zero(arena: &Arena) -> &Self {
arena.alloc(DeBruijn(0))
}
Expand Down
20 changes: 18 additions & 2 deletions crates/uplc/src/binder/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
//! Variable-binding strategies for UPLC terms.
//!
//! UPLC supports multiple ways to represent variables:
//!
//! - [`DeBruijn`] — the canonical on-chain representation using De Bruijn indices.
//! - [`Name`] — human-readable named bindings used during parsing and pretty-printing.
//! - [`NamedDeBruijn`] — a hybrid that carries both a name and a De Bruijn index.
//!
//! The [`Binder`] trait abstracts over the Flat encoding/decoding of each strategy,
//! while [`Eval`] adds the index lookup required by the CEK machine.

mod debruijn;
mod name;
mod named_debruijn;
Expand All @@ -8,22 +19,27 @@ pub use named_debruijn::*;

use crate::{arena::Arena, flat};

/// Abstracts over variable-binding strategies for Flat encoding and decoding.
pub trait Binder<'a>: std::fmt::Debug {
// this might not need to return a Result
/// Encodes a variable occurrence (reference site) into the Flat stream.
fn var_encode(&self, e: &mut flat::Encoder) -> Result<(), flat::FlatEncodeError>;
/// Decodes a variable occurrence from the Flat stream.
fn var_decode(
arena: &'a Arena,
d: &mut flat::Decoder,
) -> Result<&'a Self, flat::FlatDecodeError>;

// this might not need to return a Result
/// Encodes a lambda parameter (binding site) into the Flat stream.
fn parameter_encode(&self, e: &mut flat::Encoder) -> Result<(), flat::FlatEncodeError>;
/// Decodes a lambda parameter from the Flat stream.
fn parameter_decode(
arena: &'a Arena,
d: &mut flat::Decoder,
) -> Result<&'a Self, flat::FlatDecodeError>;
}

/// Extends [`Binder`] with the De Bruijn index lookup required by the CEK machine.
pub trait Eval<'a>: Binder<'a> {
/// Returns the De Bruijn index (1-based distance to the enclosing lambda).
fn index(&self) -> usize;
}
8 changes: 8 additions & 0 deletions crates/uplc/src/binder/name.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
//! Named variable binder strategy.

use crate::arena::Arena;

use super::Binder;

/// A human-readable named variable binding.
///
/// Used during parsing and pretty-printing where variable names are preserved.
/// Each binding carries a `text` label and a `unique` integer to disambiguate
/// shadowed names.
#[derive(Debug)]
pub struct Name<'a> {
text: &'a str,
unique: usize,
}

impl<'a> Name<'a> {
/// Allocates a [`Name`] with the given text label and uniqueness index.
pub fn new(arena: &'a Arena, text: &'a str, unique: usize) -> &'a Self {
arena.alloc(Name { text, unique })
}
Expand Down
Loading
Loading