From f847e41ed68efaa5b70654985d85eb4348bbacbd Mon Sep 17 00:00:00 2001 From: Jonathan Lim Date: Fri, 8 May 2026 16:57:52 -0500 Subject: [PATCH 1/4] doc: add documentation Signed-off-by: Jonathan Lim --- crates/uplc/src/arena.rs | 26 ++- crates/uplc/src/binder/debruijn.rs | 8 + crates/uplc/src/binder/mod.rs | 20 +- crates/uplc/src/binder/name.rs | 8 + crates/uplc/src/binder/named_debruijn.rs | 7 + crates/uplc/src/bls.rs | 6 + crates/uplc/src/builtin/default_function.rs | 141 +++++++++++-- crates/uplc/src/builtin/mod.rs | 7 + crates/uplc/src/constant.rs | 39 ++++ crates/uplc/src/data.rs | 32 +++ crates/uplc/src/flat/decode/decoder.rs | 3 + crates/uplc/src/flat/decode/error.rs | 3 + crates/uplc/src/flat/encode/encoder.rs | 2 + crates/uplc/src/flat/encode/error.rs | 3 + crates/uplc/src/flat/encode/mod.rs | 1 + crates/uplc/src/flat/mod.rs | 6 + crates/uplc/src/flat/tag.rs | 3 + crates/uplc/src/lib.rs | 58 ++++++ crates/uplc/src/machine/cek.rs | 3 + .../uplc/src/machine/cost_model/ex_budget.rs | 27 +++ crates/uplc/src/machine/cost_model/mod.rs | 3 + crates/uplc/src/machine/error.rs | 11 + crates/uplc/src/machine/eval_result.rs | 6 + crates/uplc/src/machine/info.rs | 5 + crates/uplc/src/machine/mod.rs | 12 ++ crates/uplc/src/machine/runtime.rs | 18 ++ crates/uplc/src/machine/value.rs | 1 + crates/uplc/src/program.rs | 42 +++- crates/uplc/src/syn/mod.rs | 16 ++ crates/uplc/src/term.rs | 196 +++++++++++++++++- crates/uplc/src/typ.rs | 34 +++ 31 files changed, 722 insertions(+), 25 deletions(-) diff --git a/crates/uplc/src/arena.rs b/crates/uplc/src/arena.rs index 880780e2d..8b9454b38 100644 --- a/crates/uplc/src/arena.rs +++ b/crates/uplc/src/arena.rs @@ -1,3 +1,9 @@ +//! 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; @@ -5,12 +11,18 @@ 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, } impl Arena { + /// Creates a new empty arena. pub fn new() -> Self { Self { bump: Bump::new(), @@ -18,6 +30,7 @@ impl Arena { } } + /// Creates an arena reusing an existing [`bumpalo::Bump`] allocator. pub fn from_bump(bump: Bump) -> Self { Self { bump, @@ -25,6 +38,11 @@ impl Arena { } } + /// 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(&self, value: T) -> &mut T { if cfg!(debug_assertions) { assert!( @@ -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] @@ -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(); } diff --git a/crates/uplc/src/binder/debruijn.rs b/crates/uplc/src/binder/debruijn.rs index 8e300b766..b7976ee24 100644 --- a/crates/uplc/src/binder/debruijn.rs +++ b/crates/uplc/src/binder/debruijn.rs @@ -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)) } diff --git a/crates/uplc/src/binder/mod.rs b/crates/uplc/src/binder/mod.rs index 9913a670c..943946170 100644 --- a/crates/uplc/src/binder/mod.rs +++ b/crates/uplc/src/binder/mod.rs @@ -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; @@ -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; } diff --git a/crates/uplc/src/binder/name.rs b/crates/uplc/src/binder/name.rs index c0e8f6eef..4af7d9d35 100644 --- a/crates/uplc/src/binder/name.rs +++ b/crates/uplc/src/binder/name.rs @@ -1,7 +1,14 @@ +//! 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, @@ -9,6 +16,7 @@ pub struct Name<'a> { } 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 }) } diff --git a/crates/uplc/src/binder/named_debruijn.rs b/crates/uplc/src/binder/named_debruijn.rs index b25c7d52a..fd9d69312 100644 --- a/crates/uplc/src/binder/named_debruijn.rs +++ b/crates/uplc/src/binder/named_debruijn.rs @@ -1,7 +1,13 @@ +//! Named De Bruijn variable binder strategy. + use crate::arena::Arena; use super::{Binder, Eval}; +/// A hybrid variable binding that carries both a human-readable name and a De Bruijn index. +/// +/// Useful when debugging or round-tripping through a textual format: the `text` label +/// aids readability while the `index` drives CEK machine variable lookup. #[derive(Debug)] pub struct NamedDeBruijn<'a> { text: &'a str, @@ -9,6 +15,7 @@ pub struct NamedDeBruijn<'a> { } impl<'a> NamedDeBruijn<'a> { + /// Allocates a [`NamedDeBruijn`] with the given text label and De Bruijn index. pub fn new(arena: &'a Arena, text: &'a str, index: usize) -> &'a Self { arena.alloc(NamedDeBruijn { text, index }) } diff --git a/crates/uplc/src/bls.rs b/crates/uplc/src/bls.rs index dbebe919a..1167f985f 100644 --- a/crates/uplc/src/bls.rs +++ b/crates/uplc/src/bls.rs @@ -1,3 +1,9 @@ +//! BLS12-381 elliptic curve operations. +//! +//! Wraps the [`blst`] crate to provide point compression, hashing-to-curve, +//! and pairing operations used by the BLS built-in functions (Plutus V3). +#![allow(missing_docs)] + use bumpalo::collections::Vec as BumpVec; use once_cell::sync::Lazy; diff --git a/crates/uplc/src/builtin/default_function.rs b/crates/uplc/src/builtin/default_function.rs index 2befd1bb4..f488f110b 100644 --- a/crates/uplc/src/builtin/default_function.rs +++ b/crates/uplc/src/builtin/default_function.rs @@ -1,123 +1,221 @@ use crate::machine::PlutusVersion; +/// All built-in functions available in the UPLC language. +/// +/// The discriminant values match the Flat encoding used in on-chain scripts. +/// Not every function is available in every Plutus version; the runtime +/// checks availability before dispatch. +#[non_exhaustive] #[repr(u8)] #[allow(non_camel_case_types)] #[derive(Copy, Clone, Debug, PartialEq)] pub enum DefaultFunction { - // Integer functions + // --- Integer --- + /// Adds two integers. AddInteger = 0, + /// Subtracts the second integer from the first. SubtractInteger = 1, + /// Multiplies two integers. MultiplyInteger = 2, + /// Truncated division (rounds towards negative infinity). DivideInteger = 3, + /// Truncated quotient (rounds towards zero). QuotientInteger = 4, + /// Remainder after [`QuotientInteger`](Self::QuotientInteger). RemainderInteger = 5, + /// Modulo after [`DivideInteger`](Self::DivideInteger). ModInteger = 6, + /// Tests two integers for equality. EqualsInteger = 7, + /// Returns `true` if the first integer is strictly less than the second. LessThanInteger = 8, + /// Returns `true` if the first integer is less than or equal to the second. LessThanEqualsInteger = 9, - // ByteString functions + + // --- ByteString --- + /// Concatenates two byte strings. AppendByteString = 10, + /// Prepends a byte (given as an integer 0–255) to a byte string. ConsByteString = 11, + /// Extracts a sub-byte-string by offset and length. SliceByteString = 12, + /// Returns the length of a byte string. LengthOfByteString = 13, + /// Returns the byte at a given index. IndexByteString = 14, + /// Tests two byte strings for equality. EqualsByteString = 15, + /// Lexicographic less-than on byte strings. LessThanByteString = 16, + /// Lexicographic less-than-or-equal on byte strings. LessThanEqualsByteString = 17, - // Cryptography and hash functions + + // --- Cryptography --- + /// SHA-256 hash of a byte string. Sha2_256 = 18, + /// SHA3-256 hash of a byte string. Sha3_256 = 19, + /// Blake2b-256 hash of a byte string. Blake2b_256 = 20, + /// Keccak-256 hash of a byte string (Plutus V3, protocol version ≥ 9). Keccak_256 = 71, + /// Blake2b-224 hash of a byte string (Plutus V3, protocol version ≥ 9). Blake2b_224 = 72, + /// Verifies an Ed25519 signature given `(public_key, message, signature)`. VerifyEd25519Signature = 21, + /// Verifies an ECDSA secp256k1 signature given `(public_key, message, signature)`. VerifyEcdsaSecp256k1Signature = 52, + /// Verifies a Schnorr secp256k1 signature given `(public_key, message, signature)`. VerifySchnorrSecp256k1Signature = 53, - // String functions + + // --- String --- + /// Concatenates two UTF-8 strings. AppendString = 22, + /// Tests two strings for equality. EqualsString = 23, + /// Encodes a string to a byte string (UTF-8). EncodeUtf8 = 24, + /// Decodes a byte string to a string (UTF-8); errors on invalid bytes. DecodeUtf8 = 25, - // Bool function + + // --- Control --- + /// Polymorphic conditional; forces the chosen branch. IfThenElse = 26, - // Unit function + /// Evaluates a unit value and returns the provided second argument. ChooseUnit = 27, - // Tracing function + /// Logs a trace message and returns the second argument unchanged. Trace = 28, - // Pairs functions + + // --- Pairs --- + /// Returns the first element of a pair. FstPair = 29, + /// Returns the second element of a pair. SndPair = 30, - // List functions + + // --- Lists --- + /// Pattern-matches a list, selecting the nil or cons branch. ChooseList = 31, + /// Prepends an element to a typed list. MkCons = 32, + /// Returns the first element of a non-empty list. HeadList = 33, + /// Returns the list without its first element. TailList = 34, + /// Returns `true` if the list is empty. NullList = 35, - // Data functions - // It is convenient to have a "choosing" function for a data type that has more than two - // constructors to get pattern matching over it and we may end up having multiple such data - // types, hence we include the name of the data type as a suffix. + + // --- Data --- + /// Pattern-matches a `Data` value across all five constructors. ChooseData = 36, + /// Constructs a `Data` constr value from a tag and a list of fields. ConstrData = 37, + /// Lifts a map of `Data` values into a `Data` map. MapData = 38, + /// Lifts a list of `Data` values into `Data`. ListData = 39, + /// Lifts an integer into `Data`. IData = 40, + /// Lifts a byte string into `Data`. BData = 41, + /// Deconstructs a `Data` constr into `(tag, fields)`. UnConstrData = 42, + /// Extracts the map from a `Data` map value. UnMapData = 43, + /// Extracts the list from a `Data` list value. UnListData = 44, + /// Extracts the integer from a `Data` integer value. UnIData = 45, + /// Extracts the byte string from a `Data` byte-string value. UnBData = 46, + /// Tests two `Data` values for structural equality. EqualsData = 47, + /// CBOR-serialises a `Data` value to a byte string. SerialiseData = 51, - // Misc constructors - // Constructors that we need for constructing e.g. Data. Polymorphic builtin - // constructors are often problematic (See note [Representable built-in - // functions over polymorphic built-in types]) + + // --- Data constructors --- + /// Constructs a `Data` pair. MkPairData = 48, + /// Constructs an empty `Data` list. MkNilData = 49, + /// Constructs an empty `Data` map (list of pairs). MkNilPairData = 50, - // BLS Builtins + // --- BLS12-381 (Plutus V3) --- + /// Point addition in BLS12-381 G1. Bls12_381_G1_Add = 54, + /// Point negation in BLS12-381 G1. Bls12_381_G1_Neg = 55, + /// Scalar multiplication in BLS12-381 G1. Bls12_381_G1_ScalarMul = 56, + /// Equality test for BLS12-381 G1 points. Bls12_381_G1_Equal = 57, + /// Compresses a BLS12-381 G1 point to 48 bytes. Bls12_381_G1_Compress = 58, + /// Decompresses 48 bytes into a BLS12-381 G1 point. Bls12_381_G1_Uncompress = 59, + /// Hashes a byte string to a BLS12-381 G1 point using a domain-separation tag. Bls12_381_G1_HashToGroup = 60, + /// Point addition in BLS12-381 G2. Bls12_381_G2_Add = 61, + /// Point negation in BLS12-381 G2. Bls12_381_G2_Neg = 62, + /// Scalar multiplication in BLS12-381 G2. Bls12_381_G2_ScalarMul = 63, + /// Equality test for BLS12-381 G2 points. Bls12_381_G2_Equal = 64, + /// Compresses a BLS12-381 G2 point to 96 bytes. Bls12_381_G2_Compress = 65, + /// Decompresses 96 bytes into a BLS12-381 G2 point. Bls12_381_G2_Uncompress = 66, + /// Hashes a byte string to a BLS12-381 G2 point using a domain-separation tag. Bls12_381_G2_HashToGroup = 67, + /// Computes the BLS12-381 Miller loop (G1 × G2 → GT). Bls12_381_MillerLoop = 68, + /// Multiplies two BLS12-381 GT (Miller-loop result) elements. Bls12_381_MulMlResult = 69, + /// Checks equality of two BLS12-381 pairings (final-exponentiation verify). Bls12_381_FinalVerify = 70, - // Bitwise + // --- Bitwise (Plutus V3) --- + /// Converts an integer to a byte string with given endianness and size. IntegerToByteString = 73, + /// Converts a byte string to an integer with given endianness. ByteStringToInteger = 74, - + /// Bitwise AND of two byte strings, with a padding-semantics flag. AndByteString = 75, + /// Bitwise OR of two byte strings, with a padding-semantics flag. OrByteString = 76, + /// Bitwise XOR of two byte strings, with a padding-semantics flag. XorByteString = 77, + /// Bitwise complement of a byte string. ComplementByteString = 78, + /// Reads a single bit at the given index. ReadBit = 79, + /// Writes a list of `(index, bit)` pairs into a byte string. WriteBits = 80, + /// Creates a byte string of given length filled with a single byte. ReplicateByte = 81, + /// Logical shift of a byte string by a signed number of bits. ShiftByteString = 82, + /// Rotation of a byte string by a signed number of bits. RotateByteString = 83, + /// Counts the number of set bits (popcount) in a byte string. CountSetBits = 84, + /// Returns the index of the lowest set bit, or -1 if none. FindFirstSetBit = 85, + /// RIPEMD-160 hash of a byte string. Ripemd_160 = 86, + // --- van Rossem (protocol version ≥ 11) --- + /// Modular exponentiation: `base ^ exp mod modulus`. ExpModInteger = 87, + /// Drops the first `n` elements of a list. DropList = 88, + /// Returns the length of an array. LengthOfArray = 89, + /// Converts a list to an array. ListToArray = 90, + /// Returns the element at a given index in an array. IndexArray = 91, // BLS Multi-Scalar Multiplication @@ -135,6 +233,8 @@ pub enum DefaultFunction { } impl DefaultFunction { + /// Number of [`Force`](crate::term::Term::Force) applications required before + /// this built-in accepts value arguments (0 for monomorphic, 1–2 for polymorphic). pub fn force_count(&self) -> usize { match self { DefaultFunction::AddInteger => 0, @@ -241,6 +341,7 @@ impl DefaultFunction { } } + /// Number of value arguments this built-in expects after any required `Force`s. pub fn arity(&self) -> usize { match self { DefaultFunction::AddInteger => 2, diff --git a/crates/uplc/src/builtin/mod.rs b/crates/uplc/src/builtin/mod.rs index 030258888..8e068b4fe 100644 --- a/crates/uplc/src/builtin/mod.rs +++ b/crates/uplc/src/builtin/mod.rs @@ -1,3 +1,10 @@ +//! UPLC built-in functions. +//! +//! [`DefaultFunction`] enumerates every built-in function available across Plutus V1/V2/V3: +//! arithmetic, byte-string operations, cryptographic hashing and signature verification, +//! string operations, list and pair manipulation, Plutus data constructors and destructors, +//! BLS12-381 curve operations, and bitwise primitives. + mod default_function; pub use default_function::*; diff --git a/crates/uplc/src/constant.rs b/crates/uplc/src/constant.rs index 0943fad76..f40c36680 100644 --- a/crates/uplc/src/constant.rs +++ b/crates/uplc/src/constant.rs @@ -1,69 +1,102 @@ +//! Constant values in UPLC programs. +//! +//! [`Constant`] covers all ground types: arbitrary-precision integers ([`Integer`]), +//! byte strings, UTF-8 strings, booleans, unit, homogeneous lists and arrays, pairs, +//! structured [`PlutusData`], and BLS12-381 curve elements. +//! use crate::{ arena::Arena, binder::Eval, data::PlutusData, ledger_value::LedgerValue, machine::MachineError, typ::Type, }; +use crate::{arena::Arena, binder::Eval, data::PlutusData, machine::MachineError, typ::Type}; + +/// A UPLC ground-type constant. +#[non_exhaustive] #[derive(Debug, PartialEq)] pub enum Constant<'a> { + /// Arbitrary-precision integer. Integer(&'a Integer), + /// Raw byte string. ByteString(&'a [u8]), + /// UTF-8 string. String(&'a str), + /// Boolean. Boolean(bool), + /// Plutus structured data. Data(&'a PlutusData<'a>), + /// Homogeneous list. ProtoList(&'a Type<'a>, &'a [&'a Constant<'a>]), + /// Homogeneous array (Plutus V3). ProtoArray(&'a Type<'a>, &'a [&'a Constant<'a>]), + /// Pair of typed constants. ProtoPair( &'a Type<'a>, &'a Type<'a>, &'a Constant<'a>, &'a Constant<'a>, ), + /// Unit value `()`. Unit, + /// BLS12-381 G1 curve point. Bls12_381G1Element(&'a blst::blst_p1), + /// BLS12-381 G2 curve point. Bls12_381G2Element(&'a blst::blst_p2), + /// BLS12-381 Miller-loop result. Bls12_381MlResult(&'a blst::blst_fp12), Value(&'a LedgerValue<'a>), } +/// Arbitrary-precision integer (alias for [`num::BigInt`]). pub type Integer = num::BigInt; +/// Allocates a zero [`Integer`] in the arena. pub fn integer(arena: &Arena) -> &Integer { arena.alloc_integer(Integer::default()) } +/// Allocates an [`Integer`] from an `i128`. pub fn integer_from(arena: &Arena, i: i128) -> &Integer { arena.alloc_integer(Integer::from(i)) } impl<'a> Constant<'a> { + /// Allocates a [`Constant::Integer`]. pub fn integer(arena: &'a Arena, i: &'a Integer) -> &'a Constant<'a> { arena.alloc(Constant::Integer(i)) } + /// Allocates a [`Constant::Integer`] from an `i128`. pub fn integer_from(arena: &'a Arena, i: i128) -> &'a Constant<'a> { arena.alloc(Constant::Integer(integer_from(arena, i))) } + /// Allocates a [`Constant::ByteString`]. pub fn byte_string(arena: &'a Arena, bytes: &'a [u8]) -> &'a Constant<'a> { arena.alloc(Constant::ByteString(bytes)) } + /// Allocates a [`Constant::String`]. pub fn string(arena: &'a Arena, s: &'a str) -> &'a Constant<'a> { arena.alloc(Constant::String(s)) } + /// Allocates a [`Constant::Boolean`]. pub fn bool(arena: &'a Arena, v: bool) -> &'a Constant<'a> { arena.alloc(Constant::Boolean(v)) } + /// Allocates a [`Constant::Data`]. pub fn data(arena: &'a Arena, d: &'a PlutusData<'a>) -> &'a Constant<'a> { arena.alloc(Constant::Data(d)) } + /// Allocates a [`Constant::Unit`]. pub fn unit(arena: &'a Arena) -> &'a Constant<'a> { arena.alloc(Constant::Unit) } + /// Allocates a [`Constant::ProtoList`] with the given element type and values. pub fn proto_list( arena: &'a Arena, inner: &'a Type<'a>, @@ -72,6 +105,7 @@ impl<'a> Constant<'a> { arena.alloc(Constant::ProtoList(inner, values)) } + /// Allocates a [`Constant::ProtoArray`] with the given element type and values (Plutus V3). pub fn proto_array( arena: &'a Arena, inner: &'a Type<'a>, @@ -80,6 +114,7 @@ impl<'a> Constant<'a> { arena.alloc(Constant::ProtoArray(inner, values)) } + /// Allocates a [`Constant::ProtoPair`] with the given types and values. pub fn proto_pair( arena: &'a Arena, first_type: &'a Type<'a>, @@ -95,14 +130,17 @@ impl<'a> Constant<'a> { )) } + /// Allocates a [`Constant::Bls12_381G1Element`]. pub fn g1(arena: &'a Arena, g1: &'a blst::blst_p1) -> &'a Constant<'a> { arena.alloc(Constant::Bls12_381G1Element(g1)) } + /// Allocates a [`Constant::Bls12_381G2Element`]. pub fn g2(arena: &'a Arena, g2: &'a blst::blst_p2) -> &'a Constant<'a> { arena.alloc(Constant::Bls12_381G2Element(g2)) } + /// Allocates a [`Constant::Bls12_381MlResult`]. pub fn ml_result(arena: &'a Arena, ml_res: &'a blst::blst_fp12) -> &'a Constant<'a> { arena.alloc(Constant::Bls12_381MlResult(ml_res)) } @@ -121,6 +159,7 @@ impl<'a> Constant<'a> { } } + /// Returns the runtime [`Type`] of this constant. pub fn type_of(&self, arena: &'a Arena) -> &'a Type<'a> { match self { Constant::Integer(_) => Type::integer(arena), diff --git a/crates/uplc/src/data.rs b/crates/uplc/src/data.rs index 4f38ffac2..bb5d653af 100644 --- a/crates/uplc/src/data.rs +++ b/crates/uplc/src/data.rs @@ -1,3 +1,9 @@ +//! Plutus structured data. +//! +//! [`PlutusData`] is the serialisable data type passed across the Plutus script boundary. +//! It supports five constructors — `Constr`, `Map`, `List`, `Integer`, `ByteString` — +//! mirroring the Haskell `Data` type from `plutus-core`. + use crate::{ arena::Arena, binder::Eval, @@ -6,19 +12,32 @@ use crate::{ machine::MachineError, }; +/// Plutus structured data, serialisable across the script boundary. +/// +/// This is the data type passed as datum, redeemer, and script context to on-chain +/// validators. It mirrors the Haskell `PlutusCore.Data` type. +#[non_exhaustive] #[derive(Debug, PartialEq)] pub enum PlutusData<'a> { + /// Tagged constructor with positional fields. Constr { + /// Constructor tag (alternative index). tag: u64, + /// Positional field values. fields: &'a [&'a PlutusData<'a>], }, + /// Association list (key-value map). Map(&'a [(&'a PlutusData<'a>, &'a PlutusData<'a>)]), + /// Arbitrary-precision integer. Integer(&'a Integer), + /// Raw byte string. ByteString(&'a [u8]), + /// Homogeneous list. List(&'a [&'a PlutusData<'a>]), } impl<'a> PlutusData<'a> { + /// Allocates a [`PlutusData::Constr`] with the given tag and fields. pub fn constr( arena: &'a Arena, tag: u64, @@ -27,10 +46,12 @@ impl<'a> PlutusData<'a> { arena.alloc(PlutusData::Constr { tag, fields }) } + /// Allocates a [`PlutusData::List`]. pub fn list(arena: &'a Arena, items: &'a [&'a PlutusData<'a>]) -> &'a PlutusData<'a> { arena.alloc(PlutusData::List(items)) } + /// Allocates a [`PlutusData::Map`]. pub fn map( arena: &'a Arena, items: &'a [(&'a PlutusData<'a>, &'a PlutusData<'a>)], @@ -38,18 +59,22 @@ impl<'a> PlutusData<'a> { arena.alloc(PlutusData::Map(items)) } + /// Allocates a [`PlutusData::Integer`]. pub fn integer(arena: &'a Arena, i: &'a Integer) -> &'a PlutusData<'a> { arena.alloc(PlutusData::Integer(i)) } + /// Allocates a [`PlutusData::Integer`] from an `i128`. pub fn integer_from(arena: &'a Arena, i: i128) -> &'a PlutusData<'a> { arena.alloc(PlutusData::Integer(integer_from(arena, i))) } + /// Allocates a [`PlutusData::ByteString`]. pub fn byte_string(arena: &'a Arena, bytes: &'a [u8]) -> &'a PlutusData<'a> { arena.alloc(PlutusData::ByteString(bytes)) } + /// Decodes a CBOR-encoded `PlutusData` value. pub fn from_cbor( arena: &'a Arena, cbor: &'_ [u8], @@ -65,6 +90,7 @@ impl<'a> PlutusData<'a> { ) } + /// Unwraps a [`PlutusData::Constr`], returning `(tag, fields)`. pub fn unwrap_constr( &'a self, ) -> Result<(&'a u64, &'a [&'a PlutusData<'a>]), MachineError<'a, V>> @@ -77,6 +103,7 @@ impl<'a> PlutusData<'a> { } } + /// Unwraps a [`PlutusData::Map`]. pub fn unwrap_map( &'a self, ) -> Result<&'a [(&'a PlutusData<'a>, &'a PlutusData<'a>)], MachineError<'a, V>> @@ -89,6 +116,7 @@ impl<'a> PlutusData<'a> { } } + /// Unwraps a [`PlutusData::Integer`]. pub fn unwrap_integer(&'a self) -> Result<&'a Integer, MachineError<'a, V>> where V: Eval<'a>, @@ -99,6 +127,7 @@ impl<'a> PlutusData<'a> { } } + /// Unwraps a [`PlutusData::ByteString`]. pub fn unwrap_byte_string(&'a self) -> Result<&'a [u8], MachineError<'a, V>> where V: Eval<'a>, @@ -109,6 +138,7 @@ impl<'a> PlutusData<'a> { } } + /// Unwraps a [`PlutusData::List`]. pub fn unwrap_list(&'a self) -> Result<&'a [&'a PlutusData<'a>], MachineError<'a, V>> where V: Eval<'a>, @@ -119,10 +149,12 @@ impl<'a> PlutusData<'a> { } } + /// Wraps this value in a [`Constant::Data`]. pub fn constant(&'a self, arena: &'a Arena) -> &'a Constant<'a> { Constant::data(arena, self) } + /// CBOR-serialises this value into a byte slice allocated in the arena. pub fn to_bytes(&'a self, arena: &'a Arena) -> Result<&'a [u8], MachineError<'a, V>> where V: Eval<'a>, diff --git a/crates/uplc/src/flat/decode/decoder.rs b/crates/uplc/src/flat/decode/decoder.rs index 21c114607..104ff9851 100644 --- a/crates/uplc/src/flat/decode/decoder.rs +++ b/crates/uplc/src/flat/decode/decoder.rs @@ -1,3 +1,6 @@ +//! Flat binary decoder implementation. +#![allow(missing_docs)] + use bumpalo::collections::{String as BumpString, Vec as BumpVec}; use crate::{ diff --git a/crates/uplc/src/flat/decode/error.rs b/crates/uplc/src/flat/decode/error.rs index e38e3dc7c..d6ebbc75d 100644 --- a/crates/uplc/src/flat/decode/error.rs +++ b/crates/uplc/src/flat/decode/error.rs @@ -1,3 +1,6 @@ +//! Flat decode error types. +#![allow(missing_docs)] + use thiserror::Error; #[derive(Error, Debug)] diff --git a/crates/uplc/src/flat/encode/encoder.rs b/crates/uplc/src/flat/encode/encoder.rs index deddbe997..545dba391 100644 --- a/crates/uplc/src/flat/encode/encoder.rs +++ b/crates/uplc/src/flat/encode/encoder.rs @@ -2,8 +2,10 @@ use crate::{constant::Integer, flat::zigzag::ZigZag}; use super::FlatEncodeError; +/// Bit-level Flat binary encoder. #[derive(Default)] pub struct Encoder { + /// The encoded bytes accumulated so far. pub buffer: Vec, // Int used_bits: i64, diff --git a/crates/uplc/src/flat/encode/error.rs b/crates/uplc/src/flat/encode/error.rs index 5384738cf..ca7672cfa 100644 --- a/crates/uplc/src/flat/encode/error.rs +++ b/crates/uplc/src/flat/encode/error.rs @@ -1,3 +1,6 @@ +//! Flat encode error types. +#![allow(missing_docs)] + use std::convert::Infallible; use thiserror::Error; diff --git a/crates/uplc/src/flat/encode/mod.rs b/crates/uplc/src/flat/encode/mod.rs index 5b2e38560..ad0b65e79 100644 --- a/crates/uplc/src/flat/encode/mod.rs +++ b/crates/uplc/src/flat/encode/mod.rs @@ -11,6 +11,7 @@ use crate::{ use super::tag; +/// Encodes a UPLC [`Program`] into its Flat binary representation. pub fn encode<'a, V>(program: &'a Program<'a, V>) -> Result, FlatEncodeError> where V: Binder<'a>, diff --git a/crates/uplc/src/flat/mod.rs b/crates/uplc/src/flat/mod.rs index 64de6e6ec..2d1a05a8f 100644 --- a/crates/uplc/src/flat/mod.rs +++ b/crates/uplc/src/flat/mod.rs @@ -1,3 +1,9 @@ +//! Flat and CBOR binary encoding/decoding for UPLC programs and data. +//! +//! Flat is the canonical on-chain binary format for UPLC scripts; CBOR wrapping is used +//! in the Cardano transaction witness set. This module re-exports [`Encoder`], [`Decoder`], +//! and their associated error types. + mod builtin; mod data; mod decode; diff --git a/crates/uplc/src/flat/tag.rs b/crates/uplc/src/flat/tag.rs index 6646280a4..f699cdfea 100644 --- a/crates/uplc/src/flat/tag.rs +++ b/crates/uplc/src/flat/tag.rs @@ -1,3 +1,6 @@ +//! Flat binary format tag constants for UPLC terms, types, and built-ins. +#![allow(missing_docs)] + // Widths pub const TERM_TAG_WIDTH: usize = 4; pub const CONST_TAG_WIDTH: usize = 4; diff --git a/crates/uplc/src/lib.rs b/crates/uplc/src/lib.rs index 54bec029f..f704592c2 100644 --- a/crates/uplc/src/lib.rs +++ b/crates/uplc/src/lib.rs @@ -1,3 +1,61 @@ +#![warn(missing_docs)] +//! A lightning-fast [UPLC] (Untyped Plutus Language Core) evaluator implemented as a CEK machine. +//! +//! UPLC is the low-level bytecode compiled from [Plutus], the smart-contract language for the +//! [Cardano] blockchain. This crate provides a complete evaluation pipeline: parsing, +//! binary encoding/decoding (Flat/CBOR), and execution against configurable cost models for +//! Plutus V1, V2, and V3. +//! +//! # Quick start +//! +//! ```rust,no_run +//! 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::::new(&arena, version, term); +//! let result = program.eval(&arena); +//! +//! assert_eq!(result.term.unwrap(), Term::integer_from(&arena, 4)); +//! ``` +//! +//! # Modules +//! +//! | Module | Description | +//! |--------|-------------| +//! | [`arena`] | Arena allocator wrapping [`bumpalo`] with stable integer storage | +//! | [`program`] | Top-level [`Program`](program::Program) and UPLC [`Version`](program::Version) | +//! | [`term`] | [`Term`](term::Term) AST with builder helpers | +//! | [`binder`] | Variable-binding strategies: De Bruijn indices, named, and named-De Bruijn | +//! | [`constant`] | Compile-time constant values | +//! | [`data`] | Plutus structured data ([`PlutusData`](data::PlutusData)) | +//! | [`builtin`] | All built-in functions ([`DefaultFunction`](builtin::DefaultFunction)) | +//! | [`machine`] | CEK machine, cost models, and evaluation results | +//! | [`syn`] | UPLC text-format parser | +//! | [`flat`] | Flat/CBOR binary encoder and decoder | +//! | [`bls`] | BLS12-381 elliptic curve primitives | +//! | [`typ`] | UPLC type system | +//! +//! # Feature flags +//! +//! - `alloc_profiler` — enables experimental arena allocation profiling. Currently a no-op; +//! reserved for future profiling hooks. +//! +//! [UPLC]: https://plutus.readthedocs.io/en/latest/reference/uplc-introduction.html +//! [Plutus]: https://plutus.readthedocs.io/ +//! [Cardano]: https://cardano.org/ + pub mod arena; pub mod binder; pub mod bls; diff --git a/crates/uplc/src/machine/cek.rs b/crates/uplc/src/machine/cek.rs index 8ce566228..c9e0d2773 100644 --- a/crates/uplc/src/machine/cek.rs +++ b/crates/uplc/src/machine/cek.rs @@ -1,3 +1,6 @@ +//! CEK machine execution loop. +#![allow(missing_docs)] + use crate::program::Version; use bumpalo::collections::Vec as BumpVec; diff --git a/crates/uplc/src/machine/cost_model/ex_budget.rs b/crates/uplc/src/machine/cost_model/ex_budget.rs index 0cc740a43..afa44af18 100644 --- a/crates/uplc/src/machine/cost_model/ex_budget.rs +++ b/crates/uplc/src/machine/cost_model/ex_budget.rs @@ -1,6 +1,18 @@ +//! Execution budget tracking. +//! +//! [`ExBudget`] holds a CPU and memory allowance used by the CEK machine to bound evaluation. +//! The default budget mirrors the Cardano mainnet per-transaction limit. + +/// Execution budget denominated in abstract memory and CPU units. +/// +/// The default budget corresponds to the Cardano mainnet per-transaction limit +/// (`14_000_000` mem, `10_000_000_000` cpu). Use [`ExBudget::max`] for an effectively +/// unbounded budget in off-chain contexts. #[derive(Debug, Copy, Clone, PartialEq)] pub struct ExBudget { + /// Memory units available or consumed. pub mem: i64, + /// CPU step units available or consumed. pub cpu: i64, } @@ -11,19 +23,23 @@ impl Default for ExBudget { } impl ExBudget { + /// Creates an [`ExBudget`] with explicit `mem` and `cpu` values. pub fn new(mem: i64, cpu: i64) -> Self { ExBudget { mem, cpu } } + /// Returns an effectively unbounded budget for off-chain use. pub fn max() -> Self { Self::machine_max() } + /// Scales both `mem` and `cpu` by `n` (used when a cost model has an occurrence factor). pub fn occurrences(&mut self, n: i64) { self.mem *= n; self.cpu *= n; } + /// Cardano mainnet per-transaction budget (`14_000_000` mem, `10_000_000_000` cpu). pub fn machine() -> Self { ExBudget { mem: 14_000_000, @@ -31,6 +47,7 @@ impl ExBudget { } } + /// Effectively unbounded budget (`14_000_000_000_000` mem, `10_000_000_000_000_000` cpu). pub fn machine_max() -> Self { ExBudget { mem: 14_000_000_000_000, @@ -38,10 +55,12 @@ impl ExBudget { } } + /// Step cost charged once at program start-up. pub fn start_up() -> Self { ExBudget { mem: 100, cpu: 100 } } + /// Step cost for a variable lookup (`Var`) step. pub fn var() -> Self { ExBudget { mem: 100, @@ -49,6 +68,7 @@ impl ExBudget { } } + /// Step cost for a constant (`Constant`) step. pub fn constant() -> Self { ExBudget { mem: 100, @@ -56,6 +76,7 @@ impl ExBudget { } } + /// Step cost for a lambda abstraction (`Lambda`) step. pub fn lambda() -> Self { ExBudget { mem: 100, @@ -63,6 +84,7 @@ impl ExBudget { } } + /// Step cost for a `Delay` step. pub fn delay() -> Self { ExBudget { mem: 100, @@ -70,6 +92,7 @@ impl ExBudget { } } + /// Step cost for a `Force` step. pub fn force() -> Self { ExBudget { mem: 100, @@ -77,6 +100,7 @@ impl ExBudget { } } + /// Step cost for a function application (`Apply`) step. pub fn apply() -> Self { ExBudget { mem: 100, @@ -84,6 +108,7 @@ impl ExBudget { } } + /// Step cost for entering a built-in call (`Builtin`) step. pub fn builtin() -> Self { ExBudget { mem: 100, @@ -91,6 +116,7 @@ impl ExBudget { } } + /// Step cost for a constructor (`Constr`) step (Plutus V3). pub fn constr() -> Self { ExBudget { mem: 100, @@ -98,6 +124,7 @@ impl ExBudget { } } + /// Step cost for a `Case` step (Plutus V3). pub fn case() -> Self { ExBudget { mem: 100, diff --git a/crates/uplc/src/machine/cost_model/mod.rs b/crates/uplc/src/machine/cost_model/mod.rs index 5cf1482e6..e6750d312 100644 --- a/crates/uplc/src/machine/cost_model/mod.rs +++ b/crates/uplc/src/machine/cost_model/mod.rs @@ -1,3 +1,6 @@ +//! CEK machine cost models. +#![allow(missing_docs)] + pub mod builtin_costs; pub(crate) mod cost_map; mod costing; diff --git a/crates/uplc/src/machine/error.rs b/crates/uplc/src/machine/error.rs index 643e14f74..0e9a6e285 100644 --- a/crates/uplc/src/machine/error.rs +++ b/crates/uplc/src/machine/error.rs @@ -1,3 +1,12 @@ +//! Error types produced by the CEK machine during evaluation. +// The #[error("...")] Display strings on each variant serve as the primary documentation; +// per-variant /// comments would duplicate them. +#![allow(missing_docs)] +//! +//! [`MachineError`] is the top-level error enum. Most variants wrap a [`RuntimeError`] +//! which covers arithmetic faults, type mismatches, cryptographic errors, and +//! out-of-bounds accesses encountered when executing built-in functions. + use std::array::TryFromSliceError; use crate::{ @@ -13,6 +22,7 @@ use crate::{ use super::{value::Value, ExBudget}; +#[non_exhaustive] #[derive(thiserror::Error, Debug)] pub enum MachineError<'a, V> where @@ -46,6 +56,7 @@ where NoCostForBuiltin(DefaultFunction), } +#[non_exhaustive] #[derive(thiserror::Error, Debug)] pub enum RuntimeError<'a> { #[error("Byte string out of bounds")] diff --git a/crates/uplc/src/machine/eval_result.rs b/crates/uplc/src/machine/eval_result.rs index 9258f8c27..cc836eca9 100644 --- a/crates/uplc/src/machine/eval_result.rs +++ b/crates/uplc/src/machine/eval_result.rs @@ -1,12 +1,18 @@ +//! Evaluation result returned by the CEK machine. + use crate::{binder::Eval, term::Term}; use super::{info::MachineInfo, MachineError}; +/// The result of evaluating a UPLC [`Program`](crate::program::Program). +#[must_use = "evaluation result must be inspected"] #[derive(Debug)] pub struct EvalResult<'a, V> where V: Eval<'a>, { + /// The final reduced term, or the [`MachineError`] that halted evaluation. pub term: Result<&'a Term<'a, V>, MachineError<'a, V>>, + /// Budget consumed and trace log lines produced during evaluation. pub info: MachineInfo, } diff --git a/crates/uplc/src/machine/info.rs b/crates/uplc/src/machine/info.rs index b523f66fd..59781f981 100644 --- a/crates/uplc/src/machine/info.rs +++ b/crates/uplc/src/machine/info.rs @@ -1,8 +1,13 @@ +//! Metadata captured during CEK machine evaluation. + use super::ExBudget; +/// Metadata captured during CEK machine evaluation. #[derive(Debug)] pub struct MachineInfo { pub remaining_budget: ExBudget, + /// Execution budget remaining after evaluation (initial budget minus consumed). pub consumed_budget: ExBudget, + /// Lines emitted by `Trace` built-in calls, in order. pub logs: Vec, } diff --git a/crates/uplc/src/machine/mod.rs b/crates/uplc/src/machine/mod.rs index 4e079e2ac..70e852925 100644 --- a/crates/uplc/src/machine/mod.rs +++ b/crates/uplc/src/machine/mod.rs @@ -1,3 +1,15 @@ +//! CEK machine implementation and associated types. +//! +//! The main entry point is [`Program::eval`](crate::program::Program::eval). This module +//! re-exports the types needed to inspect evaluation results: +//! +//! - [`EvalResult`] — the final term (or error) plus [`MachineInfo`] +//! - [`MachineError`] — all error variants the machine can produce +//! - [`ExBudget`] — CPU/memory execution budget +//! - [`PlutusVersion`] — V1 / V2 / V3 semantics selector +//! - [`BuiltinSemantics`] — built-in behaviour variant (V1 or V2) +//! - [`CostModel`] — parameterised cost model + mod cek; mod context; pub(crate) mod cost_model; diff --git a/crates/uplc/src/machine/runtime.rs b/crates/uplc/src/machine/runtime.rs index 067fae78a..6c173ecbf 100644 --- a/crates/uplc/src/machine/runtime.rs +++ b/crates/uplc/src/machine/runtime.rs @@ -1,3 +1,5 @@ +//! Built-in function runtime dispatch and Plutus version selection. + use core::str; use std::array::TryFromSliceError; @@ -71,15 +73,30 @@ fn prepare_msm_scalar( scalar_bytes.extend_from_slice(&scalar_buf.b); } +/// Selects the semantic behaviour of certain built-in functions. +/// +/// `V2` semantics (used for Plutus V3) aligns `modInteger` / `divideInteger` with +/// Haskell's standard `div` / `mod` behaviour for negative operands. +#[non_exhaustive] pub enum BuiltinSemantics { + /// Original Plutus V1/V2 built-in semantics. V1, + /// Updated semantics introduced in Plutus V3. V2, } +/// Plutus language version selector for the CEK machine. +/// +/// Controls which built-in functions are available and which [`BuiltinSemantics`] +/// variant is used during evaluation. +#[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PlutusVersion { + /// Plutus V1 — Alonzo-era built-ins. V1, + /// Plutus V2 — Vasil-era additions (inline datums, reference inputs). V2, + /// Plutus V3 — Chang-era additions (BLS12-381, bitwise ops, sums-of-products). V3, } @@ -151,6 +168,7 @@ where } impl<'a, B: BuiltinCostModel> Machine<'a, B> { + /// Executes a fully-saturated built-in runtime call and returns the resulting value. pub fn call( &mut self, runtime: &'a Runtime<'a, V>, diff --git a/crates/uplc/src/machine/value.rs b/crates/uplc/src/machine/value.rs index 626c6f40f..10d5203c0 100644 --- a/crates/uplc/src/machine/value.rs +++ b/crates/uplc/src/machine/value.rs @@ -266,6 +266,7 @@ where } impl<'a> Constant<'a> { + /// Wraps this constant in a `Value::Con` for use as a CEK machine value. pub fn value(&'a self, arena: &'a Arena) -> &'a Value<'a, V> where V: Eval<'a>, diff --git a/crates/uplc/src/program.rs b/crates/uplc/src/program.rs index 76256b82d..d695b5343 100644 --- a/crates/uplc/src/program.rs +++ b/crates/uplc/src/program.rs @@ -1,3 +1,9 @@ +//! Top-level UPLC program representation. +//! +//! A [`Program`] wraps a [`Term`] together with a UPLC [`Version`] tag +//! and exposes the primary evaluation methods. All allocations go through the shared +//! [`Arena`]. + use crate::{ arena::Arena, binder::Eval, @@ -11,19 +17,28 @@ use crate::{ term::Term, }; +/// A versioned UPLC program ready for evaluation. +/// +/// `V` is the variable-binding strategy (typically [`DeBruijn`](crate::binder::DeBruijn)). +/// All fields are arena-allocated references; the program lifetime `'a` ties back to the +/// [`Arena`] it was built in. #[derive(Debug)] pub struct Program<'a, V> { + /// UPLC language version encoded with this program. pub version: &'a Version<'a>, + /// The root term of the program. pub term: &'a Term<'a, V>, } impl<'a, V> Program<'a, V> { + /// Allocates a new program with the given version and root term. pub fn new(arena: &'a Arena, version: &'a Version<'a>, term: &'a Term<'a, V>) -> &'a Self { let program = Program { version, term }; arena.alloc(program) } + /// Returns a new program whose root term is `self.term` applied to `term`. pub fn apply(&'a self, arena: &'a Arena, term: &'a Term<'a, V>) -> &'a Self { let term = self.term.apply(arena, term); @@ -35,11 +50,14 @@ impl<'a, V> Program<'a, V> where V: Eval<'a>, { + /// Evaluate using Plutus V3 semantics and the default mainnet budget. + #[must_use] pub fn eval(&'a self, arena: &'a Arena) -> EvalResult<'a, V> { self.eval_version(arena, PlutusVersion::V3) } - /// Evaluate with explicit Plutus version + /// Evaluate with the specified Plutus version and the default mainnet budget. + #[must_use] pub fn eval_version( &'a self, arena: &'a Arena, @@ -48,6 +66,8 @@ where self.eval_version_budget(arena, plutus_version, ExBudget::default()) } + /// Evaluate with an explicit Plutus version and a custom initial [`ExBudget`]. + #[must_use] pub fn eval_version_budget( &'a self, arena: &'a Arena, @@ -95,6 +115,11 @@ where EvalResult { term, info } } + /// Evaluate with a fully custom cost-model parameter array and execution budget. + /// + /// `cost_model` must be ordered as expected by the corresponding + /// `BuiltinCostModel` implementation for `plutus_version`. + #[must_use] pub fn eval_with_params( &'a self, arena: &'a Arena, @@ -125,40 +150,52 @@ where } } +/// UPLC program version tag (`major.minor.patch`). +/// +/// Encoded as a triple of unsigned integers in the Flat binary format. +/// Use the named constructors to obtain the canonical version for each Plutus era. #[derive(Debug, Copy, Clone)] pub struct Version<'a>(&'a (usize, usize, usize)); impl<'a> Version<'a> { + /// Allocates a version with explicit `major.minor.patch` components. pub fn new(arena: &'a Arena, major: usize, minor: usize, patch: usize) -> &'a mut Self { let version = arena.alloc((major, minor, patch)); arena.alloc(Version(version)) } + /// Canonical version for Plutus V1 scripts (1.0.0). pub fn plutus_v1(arena: &'a Arena) -> &'a mut Self { Self::new(arena, 1, 0, 0) } + /// Canonical version for Plutus V2 scripts (1.0.0). pub fn plutus_v2(arena: &'a Arena) -> &'a mut Self { Self::new(arena, 1, 0, 0) } + /// Canonical version for Plutus V3 scripts (1.1.0). pub fn plutus_v3(arena: &'a Arena) -> &'a mut Self { Self::new(arena, 1, 1, 0) } + /// Returns `true` if this version is `1.0.0`. pub fn is_v1_0_0(&'a self) -> bool { self.0 == &(1, 0, 0) } + /// Returns `true` if this version is `1.1.0`. pub fn is_v1_1_0(&'a self) -> bool { self.0 == &(1, 1, 0) } + /// Returns `true` if this version is a known valid UPLC version. pub fn is_valid_version(&'a self) -> bool { self.is_v1_0_0() || self.is_v1_1_0() } + /// Returns `true` if this version is below `1.1.0`. pub fn is_less_than_1_1_0(&'a self) -> bool { self.0 < &(1, 1, 0) } @@ -167,14 +204,17 @@ impl<'a> Version<'a> { self.0 >= &(1, 1, 0) } + /// Returns the major component. pub fn major(&'a self) -> usize { self.0 .0 } + /// Returns the minor component. pub fn minor(&'a self) -> usize { self.0 .1 } + /// Returns the patch component. pub fn patch(&'a self) -> usize { self.0 .2 } diff --git a/crates/uplc/src/syn/mod.rs b/crates/uplc/src/syn/mod.rs index be40cd2da..2a89b42c5 100644 --- a/crates/uplc/src/syn/mod.rs +++ b/crates/uplc/src/syn/mod.rs @@ -1,3 +1,15 @@ +//! UPLC text-format parser. +//! +//! Parses UPLC source text into an arena-allocated AST. Variables are represented as +//! [`DeBruijn`] indices in the output. +//! +//! # Entry points +//! +//! - [`parse_program`] — parse a complete `(program 1.0.0 )` expression +//! - [`parse_term`] — parse a single term +//! - [`parse_constant`] — parse a constant literal +//! - [`parse_data`] — parse a Plutus data value + use chumsky::{extra::SimpleState, prelude::*, ParseResult, Parser}; mod constant; @@ -14,6 +26,7 @@ use crate::{ term::Term, }; +/// Parses a complete UPLC program expression `(program )`. pub fn parse_program<'a>( arena: &'a Arena, input: &'a str, @@ -23,6 +36,7 @@ pub fn parse_program<'a>( program::parser().parse_with_state(input, &mut initial_state) } +/// Parses a single UPLC term. pub fn parse_term<'a>( arena: &'a Arena, input: &'a str, @@ -32,6 +46,7 @@ pub fn parse_term<'a>( term::parser().parse_with_state(input, &mut initial_state) } +/// Parses a UPLC constant literal. pub fn parse_constant<'a>( arena: &'a Arena, input: &'a str, @@ -41,6 +56,7 @@ pub fn parse_constant<'a>( constant::parser().parse_with_state(input, &mut initial_state) } +/// Parses a Plutus data value. pub fn parse_data<'a>( arena: &'a Arena, input: &'a str, diff --git a/crates/uplc/src/term.rs b/crates/uplc/src/term.rs index 73294364e..cd0757a9f 100644 --- a/crates/uplc/src/term.rs +++ b/crates/uplc/src/term.rs @@ -1,3 +1,10 @@ +//! UPLC term AST and builder helpers. +//! +//! [`Term`] is the central type of this crate. It covers all UPLC term constructors: +//! variables, lambdas, function application, delay/force, constants, built-ins, and the +//! Plutus V3 `Constr`/`Case` constructors. Builder methods allocate into the shared +//! [`Arena`], so all returned references carry the arena lifetime `'a`. + use crate::{ arena::Arena, builtin::DefaultFunction, @@ -5,47 +12,72 @@ use crate::{ data::PlutusData, }; +/// A UPLC term. +/// +/// All recursive sub-terms are arena-allocated references, making the structure +/// cheap to traverse without heap churn. Builder methods on `Term` allocate into +/// the shared [`Arena`]. +#[non_exhaustive] #[derive(Debug, PartialEq, Clone)] pub enum Term<'a, V> { + /// A variable reference. `V` is the binder strategy (e.g. [`DeBruijn`](crate::binder::DeBruijn)). Var(&'a V), + /// A lambda abstraction binding one variable. Lambda { + /// The bound variable. parameter: &'a V, + /// The body of the abstraction. body: &'a Term<'a, V>, }, + /// Function application. Apply { + /// The function term being applied. function: &'a Term<'a, V>, + /// The argument being passed. argument: &'a Term<'a, V>, }, + /// Wraps a term in a thunk (introduction form for `Force`). Delay(&'a Term<'a, V>), + /// Evaluates a delayed term. Force(&'a Term<'a, V>), + /// Pattern-match on a constructor value (Plutus V3 sums-of-products). Case { + /// The scrutinee constructor term. constr: &'a Term<'a, V>, + /// One branch term per constructor alternative. branches: &'a [&'a Term<'a, V>], }, + /// Construct a tagged product value (Plutus V3 sums-of-products). Constr { - // TODO: revisit what the best type is for this + /// Constructor alternative index. tag: usize, + /// Field values of the constructor. fields: &'a [&'a Term<'a, V>], }, + /// A literal constant value. Constant(&'a Constant<'a>), + /// A reference to a built-in function. Builtin(&'a DefaultFunction), + /// Explicit error term; always reduces to a runtime error. Error, } impl<'a, V> Term<'a, V> { + /// Allocates a [`Term::Var`] referencing `i`. pub fn var(arena: &'a Arena, i: &'a V) -> &'a Term<'a, V> { arena.alloc(Term::Var(i)) } + /// Applies `argument` to `self`, allocating a [`Term::Apply`]. pub fn apply(&'a self, arena: &'a Arena, argument: &'a Term<'a, V>) -> &'a Term<'a, V> { arena.alloc(Term::Apply { function: self, @@ -53,6 +85,7 @@ impl<'a, V> Term<'a, V> { }) } + /// Wraps `self` in a [`Term::Lambda`] binding `parameter`. pub fn lambda(&'a self, arena: &'a Arena, parameter: &'a V) -> &'a Term<'a, V> { arena.alloc(Term::Lambda { parameter, @@ -60,22 +93,27 @@ impl<'a, V> Term<'a, V> { }) } + /// Wraps `self` in a [`Term::Force`]. pub fn force(&'a self, arena: &'a Arena) -> &'a Term<'a, V> { arena.alloc(Term::Force(self)) } + /// Wraps `self` in a [`Term::Delay`]. pub fn delay(&'a self, arena: &'a Arena) -> &'a Term<'a, V> { arena.alloc(Term::Delay(self)) } + /// Allocates a [`Term::Constant`]. pub fn constant(arena: &'a Arena, constant: &'a Constant<'a>) -> &'a Term<'a, V> { arena.alloc(Term::Constant(constant)) } + /// Allocates a [`Term::Constr`] with the given `tag` and `fields` (Plutus V3). pub fn constr(arena: &'a Arena, tag: usize, fields: &'a [&'a Term<'a, V>]) -> &'a Term<'a, V> { arena.alloc(Term::Constr { tag, fields }) } + /// Allocates a [`Term::Case`] over `constr` with the given `branches` (Plutus V3). pub fn case( arena: &'a Arena, constr: &'a Term<'a, V>, @@ -84,588 +122,744 @@ impl<'a, V> Term<'a, V> { arena.alloc(Term::Case { constr, branches }) } + /// Allocates a constant integer term. pub fn integer(arena: &'a Arena, i: &'a Integer) -> &'a Term<'a, V> { let constant = arena.alloc(Constant::Integer(i)); Term::constant(arena, constant) } + /// Allocates a constant integer term from an `i128`. pub fn integer_from(arena: &'a Arena, i: i128) -> &'a Term<'a, V> { Self::integer(arena, integer_from(arena, i)) } + /// Allocates a constant byte-string term. pub fn byte_string(arena: &'a Arena, bytes: &'a [u8]) -> &'a Term<'a, V> { let constant = Constant::byte_string(arena, bytes); Term::constant(arena, constant) } + /// Allocates a constant UTF-8 string term. pub fn string(arena: &'a Arena, s: &'a str) -> &'a Term<'a, V> { let constant = Constant::string(arena, s); Term::constant(arena, constant) } + /// Allocates a constant boolean term. pub fn bool(arena: &'a Arena, v: bool) -> &'a Term<'a, V> { let constant = Constant::bool(arena, v); Term::constant(arena, constant) } + /// Allocates a constant [`PlutusData`] term. pub fn data(arena: &'a Arena, d: &'a PlutusData<'a>) -> &'a Term<'a, V> { let constant = Constant::data(arena, d); Term::constant(arena, constant) } + /// Allocates a constant [`PlutusData::ByteString`] term. pub fn data_byte_string(arena: &'a Arena, bytes: &'a [u8]) -> &'a Term<'a, V> { let data = PlutusData::byte_string(arena, bytes); Term::data(arena, data) } + /// Allocates a constant [`PlutusData::Integer`] term. pub fn data_integer(arena: &'a Arena, i: &'a Integer) -> &'a Term<'a, V> { let data = PlutusData::integer(arena, i); Term::data(arena, data) } + /// Allocates a constant [`PlutusData::Integer`] term from an `i128`. pub fn data_integer_from(arena: &'a Arena, i: i128) -> &'a Term<'a, V> { let data = PlutusData::integer_from(arena, i); Term::data(arena, data) } + /// Allocates a constant unit term. pub fn unit(arena: &'a Arena) -> &'a Term<'a, V> { let constant = Constant::unit(arena); Term::constant(arena, constant) } + /// Allocates a [`Term::Builtin`] for `fun`. pub fn builtin(arena: &'a Arena, fun: &'a DefaultFunction) -> &'a Term<'a, V> { arena.alloc(Term::Builtin(fun)) } + /// Allocates a [`Term::Error`]. pub fn error(arena: &'a Arena) -> &'a Term<'a, V> { arena.alloc(Term::Error) } + // --- Integer built-in shorthands --- + + /// Builtin term for [`DefaultFunction::AddInteger`]. pub fn add_integer(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::AddInteger); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::MultiplyInteger`]. pub fn multiply_integer(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::MultiplyInteger); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::DivideInteger`]. pub fn divide_integer(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::DivideInteger); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::QuotientInteger`]. pub fn quotient_integer(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::QuotientInteger); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::RemainderInteger`]. pub fn remainder_integer(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::RemainderInteger); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::ModInteger`]. pub fn mod_integer(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ModInteger); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::SubtractInteger`]. pub fn subtract_integer(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::SubtractInteger); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::EqualsInteger`]. pub fn equals_integer(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::EqualsInteger); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::LessThanEqualsInteger`]. pub fn less_than_equals_integer(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::LessThanEqualsInteger); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::LessThanInteger`]. pub fn less_than_integer(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::LessThanInteger); Term::builtin(arena, fun) } + // --- Control --- + + /// Builtin term for [`DefaultFunction::IfThenElse`]. pub fn if_then_else(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::IfThenElse); Term::builtin(arena, fun) } + // --- ByteString --- + + /// Builtin term for [`DefaultFunction::AppendByteString`]. pub fn append_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::AppendByteString); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::EqualsByteString`]. pub fn equals_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::EqualsByteString); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::ConsByteString`]. pub fn cons_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ConsByteString); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::SliceByteString`]. pub fn slice_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::SliceByteString); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::LengthOfByteString`]. pub fn length_of_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::LengthOfByteString); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::IndexByteString`]. pub fn index_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::IndexByteString); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::LessThanByteString`]. pub fn less_than_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::LessThanByteString); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::LessThanEqualsByteString`]. pub fn less_than_equals_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::LessThanEqualsByteString); Term::builtin(arena, fun) } + // --- Cryptography --- + + /// Builtin term for [`DefaultFunction::Sha2_256`]. pub fn sha2_256(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Sha2_256); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::Sha3_256`]. pub fn sha3_256(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Sha3_256); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::Blake2b_256`]. pub fn blake2b_256(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Blake2b_256); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::Keccak_256`]. pub fn keccak_256(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Keccak_256); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::Blake2b_224`]. pub fn blake2b_224(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Blake2b_224); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::VerifyEd25519Signature`]. pub fn verify_ed25519_signature(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::VerifyEd25519Signature); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::VerifyEcdsaSecp256k1Signature`]. pub fn verify_ecdsa_secp256k1_signature(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::VerifyEcdsaSecp256k1Signature); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::VerifySchnorrSecp256k1Signature`]. pub fn verify_schnorr_secp256k1_signature(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::VerifySchnorrSecp256k1Signature); Term::builtin(arena, fun) } + // --- String --- + + /// Builtin term for [`DefaultFunction::AppendString`]. pub fn append_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::AppendString); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::EqualsString`]. pub fn equals_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::EqualsString); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::EncodeUtf8`]. pub fn encode_utf8(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::EncodeUtf8); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::DecodeUtf8`]. pub fn decode_utf8(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::DecodeUtf8); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::ChooseUnit`]. pub fn choose_unit(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ChooseUnit); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::Trace`]. pub fn trace(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Trace); Term::builtin(arena, fun) } + // --- Pairs --- + + /// Builtin term for [`DefaultFunction::FstPair`]. pub fn fst_pair(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::FstPair); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::SndPair`]. pub fn snd_pair(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::SndPair); Term::builtin(arena, fun) } + // --- Lists --- + + /// Builtin term for [`DefaultFunction::ChooseList`]. pub fn choose_list(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ChooseList); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::MkCons`]. pub fn mk_cons(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::MkCons); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::HeadList`]. pub fn head_list(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::HeadList); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::TailList`]. pub fn tail_list(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::TailList); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::NullList`]. pub fn null_list(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::NullList); Term::builtin(arena, fun) } + // --- Data --- + + /// Builtin term for [`DefaultFunction::ChooseData`]. pub fn choose_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ChooseData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::ConstrData`]. pub fn constr_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ConstrData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::MapData`]. pub fn map_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::MapData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::ListData`]. pub fn list_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ListData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::IData`]. pub fn i_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::IData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::BData`]. pub fn b_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::BData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::UnConstrData`]. pub fn un_constr_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::UnConstrData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::UnMapData`]. pub fn un_map_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::UnMapData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::UnListData`]. pub fn un_list_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::UnListData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::UnIData`]. pub fn un_i_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::UnIData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::UnBData`]. pub fn un_b_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::UnBData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::EqualsData`]. pub fn equals_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::EqualsData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::MkPairData`]. pub fn mk_pair_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::MkPairData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::MkNilData`]. pub fn mk_nil_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::MkNilData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::MkNilPairData`]. pub fn mk_nil_pair_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::MkNilPairData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::SerialiseData`]. pub fn serialise_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::SerialiseData); Term::builtin(arena, fun) } + // --- BLS12-381 --- + + /// Builtin term for [`DefaultFunction::Bls12_381_G1_Add`]. pub fn bls12_381_g1_add(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G1_Add); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_G1_Neg`]. pub fn bls12_381_g1_neg(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G1_Neg); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_G1_ScalarMul`]. pub fn bls12_381_g1_scalar_mul(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G1_ScalarMul); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_G1_Equal`]. pub fn bls12_381_g1_equal(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G1_Equal); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_G1_Compress`]. pub fn bls12_381_g1_compress(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G1_Compress); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_G1_Uncompress`]. pub fn bls12_381_g1_uncompress(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G1_Uncompress); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_G1_HashToGroup`]. pub fn bls12_381_g1_hash_to_group(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G1_HashToGroup); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_G2_Add`]. pub fn bls12_381_g2_add(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G2_Add); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_G2_Neg`]. pub fn bls12_381_g2_neg(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G2_Neg); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_G2_ScalarMul`]. pub fn bls12_381_g2_scalar_mul(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G2_ScalarMul); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_G2_Equal`]. pub fn bls12_381_g2_equal(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G2_Equal); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_G2_Compress`]. pub fn bls12_381_g2_compress(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G2_Compress); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_G2_Uncompress`]. pub fn bls12_381_g2_uncompress(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G2_Uncompress); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_G2_HashToGroup`]. pub fn bls12_381_g2_hash_to_group(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G2_HashToGroup); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_MillerLoop`]. pub fn bls12_381_miller_loop(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_MillerLoop); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_MulMlResult`]. pub fn bls12_381_mul_ml_result(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_MulMlResult); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Bls12_381_FinalVerify`]. pub fn bls12_381_final_verify(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_FinalVerify); Term::builtin(arena, fun) } + + // --- Bitwise --- + + /// Builtin term for [`DefaultFunction::IntegerToByteString`]. pub fn integer_to_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::IntegerToByteString); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::ByteStringToInteger`]. pub fn byte_string_to_integer(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ByteStringToInteger); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::AndByteString`]. pub fn and_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::AndByteString); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::OrByteString`]. pub fn or_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::OrByteString); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::XorByteString`]. pub fn xor_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::XorByteString); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::ComplementByteString`]. pub fn complement_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ComplementByteString); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::ReadBit`]. pub fn read_bit(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ReadBit); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::WriteBits`]. pub fn write_bits(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::WriteBits); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::ReplicateByte`]. pub fn replicate_byte(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ReplicateByte); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::ShiftByteString`]. pub fn shift_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ShiftByteString); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::RotateByteString`]. pub fn rotate_byte_string(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::RotateByteString); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::CountSetBits`]. pub fn count_set_bits(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::CountSetBits); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::FindFirstSetBit`]. pub fn find_first_set_bit(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::FindFirstSetBit); Term::builtin(arena, fun) } + + /// Builtin term for [`DefaultFunction::Ripemd_160`]. pub fn ripemd_160(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Ripemd_160); Term::builtin(arena, fun) } + // --- van Rossem builtins --- + + /// Builtin term for [`DefaultFunction::ExpModInteger`]. pub fn exp_mod_integer(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ExpModInteger); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::DropList`]. pub fn drop_list(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::DropList); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::LengthOfArray`]. pub fn length_of_array(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::LengthOfArray); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::ListToArray`]. pub fn list_to_array(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ListToArray); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::IndexArray`]. pub fn index_array(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::IndexArray); diff --git a/crates/uplc/src/typ.rs b/crates/uplc/src/typ.rs index 6bb29b63e..d76903370 100644 --- a/crates/uplc/src/typ.rs +++ b/crates/uplc/src/typ.rs @@ -1,67 +1,101 @@ +//! UPLC type system. +//! +//! [`Type`] represents the ground types of constants in a UPLC program. +//! It is used by built-in function dispatch to check and infer argument types at runtime. + use crate::arena::Arena; +/// A UPLC constant type. +/// +/// These are the types that can appear as constant tags and in polymorphic built-in +/// function signatures. The machine uses them to type-check arguments at runtime. +#[non_exhaustive] #[derive(Debug, PartialEq)] pub enum Type<'a> { + /// Boolean. Bool, + /// Arbitrary-precision integer. Integer, + /// UTF-8 string. String, + /// Raw byte string. ByteString, + /// Unit type `()`. Unit, + /// Homogeneous list with the given element type. List(&'a Type<'a>), + /// Homogeneous array with the given element type (Plutus V3). Array(&'a Type<'a>), + /// Pair of two typed values. Pair(&'a Type<'a>, &'a Type<'a>), + /// Plutus structured data. Data, + /// BLS12-381 G1 curve point. Bls12_381G1Element, + /// BLS12-381 G2 curve point. Bls12_381G2Element, + /// BLS12-381 Miller-loop result. Bls12_381MlResult, Value, } impl<'a> Type<'a> { + /// Allocates a [`Type::Integer`]. pub fn integer(arena: &'a Arena) -> &'a Type<'a> { arena.alloc(Type::Integer) } + /// Allocates a [`Type::Bool`]. pub fn bool(arena: &'a Arena) -> &'a Type<'a> { arena.alloc(Type::Bool) } + /// Allocates a [`Type::String`]. pub fn string(arena: &'a Arena) -> &'a Type<'a> { arena.alloc(Type::String) } + /// Allocates a [`Type::ByteString`]. pub fn byte_string(arena: &'a Arena) -> &'a Type<'a> { arena.alloc(Type::ByteString) } + /// Allocates a [`Type::Unit`]. pub fn unit(arena: &'a Arena) -> &'a Type<'a> { arena.alloc(Type::Unit) } + /// Allocates a [`Type::Data`]. pub fn data(arena: &'a Arena) -> &'a Type<'a> { arena.alloc(Type::Data) } + /// Allocates a [`Type::List`] with the given element type. pub fn list(arena: &'a Arena, inner: &'a Type<'a>) -> &'a Type<'a> { arena.alloc(Type::List(inner)) } + /// Allocates a [`Type::Array`] with the given element type (Plutus V3). pub fn array(arena: &'a Arena, inner: &'a Type<'a>) -> &'a Type<'a> { arena.alloc(Type::Array(inner)) } + /// Allocates a [`Type::Pair`] with the given component types. pub fn pair(arena: &'a Arena, fst: &'a Type<'a>, snd: &'a Type<'a>) -> &'a Type<'a> { arena.alloc(Type::Pair(fst, snd)) } + /// Allocates a [`Type::Bls12_381G1Element`]. pub fn g1(arena: &'a Arena) -> &'a Type<'a> { arena.alloc(Type::Bls12_381G1Element) } + /// Allocates a [`Type::Bls12_381G2Element`]. pub fn g2(arena: &'a Arena) -> &'a Type<'a> { arena.alloc(Type::Bls12_381G2Element) } + /// Allocates a [`Type::Bls12_381MlResult`]. pub fn ml_result(arena: &'a Arena) -> &'a Type<'a> { arena.alloc(Type::Bls12_381MlResult) } From 7d86171824298de384b7a17422044b07b0906339 Mon Sep 17 00:00:00 2001 From: Jonathan Lim Date: Mon, 11 May 2026 10:26:08 -0500 Subject: [PATCH 2/4] doc: update readme Signed-off-by: Jonathan Lim --- Cargo.toml | 2 + README.md | 181 ++++++++++++++++++++++++++++++++++++++++- crates/uplc/Cargo.toml | 3 + 3 files changed, 183 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 16f3695d8..7ffc3fcb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ authors = ["PRAGMA ", "Lucas Rosa "] 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 diff --git a/README.md b/README.md index 44d25f0ca..468dd0e32 100644 --- a/README.md +++ b/README.md @@ -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.1.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::::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 @@ -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 @@ -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). diff --git a/crates/uplc/Cargo.toml b/crates/uplc/Cargo.toml index 96bb05231..1c2597e68 100644 --- a/crates/uplc/Cargo.toml +++ b/crates/uplc/Cargo.toml @@ -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] From a352bc942cd6ebc17e67f986c4f6de6d3ea83193 Mon Sep 17 00:00:00 2001 From: Jonathan Lim Date: Mon, 11 May 2026 11:49:46 -0500 Subject: [PATCH 3/4] doc: cleanup Signed-off-by: Jonathan Lim --- crates/uplc/src/lib.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/uplc/src/lib.rs b/crates/uplc/src/lib.rs index f704592c2..1b96492cd 100644 --- a/crates/uplc/src/lib.rs +++ b/crates/uplc/src/lib.rs @@ -47,11 +47,6 @@ //! | [`bls`] | BLS12-381 elliptic curve primitives | //! | [`typ`] | UPLC type system | //! -//! # Feature flags -//! -//! - `alloc_profiler` — enables experimental arena allocation profiling. Currently a no-op; -//! reserved for future profiling hooks. -//! //! [UPLC]: https://plutus.readthedocs.io/en/latest/reference/uplc-introduction.html //! [Plutus]: https://plutus.readthedocs.io/ //! [Cardano]: https://cardano.org/ From 3f9ac1c576a6cfcc38f1b9e28b559ae461eebeac Mon Sep 17 00:00:00 2001 From: Jonathan Lim Date: Wed, 3 Jun 2026 16:01:18 -0500 Subject: [PATCH 4/4] doc: doc cleanup Signed-off-by: Jonathan Lim --- README.md | 2 +- crates/uplc/src/builtin/default_function.rs | 9 ++++ crates/uplc/src/constant.rs | 7 +-- crates/uplc/src/ledger_value.rs | 59 +++++++++++++++++++++ crates/uplc/src/lib.rs | 1 + crates/uplc/src/machine/info.rs | 3 +- crates/uplc/src/program.rs | 5 +- crates/uplc/src/term.rs | 9 ++++ crates/uplc/src/typ.rs | 2 + 9 files changed, 88 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 468dd0e32..4b5c26489 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ smart-contract language for the [Cardano](https://cardano.org/) blockchain. ```toml [dependencies] -amaru-uplc = "0.1.0" +amaru-uplc = "0.4.0" ``` ## Usage diff --git a/crates/uplc/src/builtin/default_function.rs b/crates/uplc/src/builtin/default_function.rs index f488f110b..95ca46bde 100644 --- a/crates/uplc/src/builtin/default_function.rs +++ b/crates/uplc/src/builtin/default_function.rs @@ -219,16 +219,25 @@ pub enum DefaultFunction { IndexArray = 91, // BLS Multi-Scalar Multiplication + /// Multi-scalar multiplication on G1 points. Bls12_381_G1_MultiScalarMul = 92, + /// Multi-scalar multiplication on G2 points. Bls12_381_G2_MultiScalarMul = 93, // Value builtins + /// Inserts a single token quantity into a ledger value. InsertCoin = 94, + /// Looks up a token quantity in a ledger value. LookupCoin = 95, + /// Merges two ledger values by summing matching token quantities. UnionValue = 96, + /// Tests whether one ledger value contains at least the quantities of another. ValueContains = 97, + /// Serialises a ledger value into [`PlutusData`](crate::data::PlutusData). ValueData = 98, + /// Deserialises [`PlutusData`](crate::data::PlutusData) into a ledger value. UnValueData = 99, + /// Multiplies every quantity in a ledger value by a scalar. ScaleValue = 100, } diff --git a/crates/uplc/src/constant.rs b/crates/uplc/src/constant.rs index f40c36680..78a953b87 100644 --- a/crates/uplc/src/constant.rs +++ b/crates/uplc/src/constant.rs @@ -3,14 +3,12 @@ //! [`Constant`] covers all ground types: arbitrary-precision integers ([`Integer`]), //! byte strings, UTF-8 strings, booleans, unit, homogeneous lists and arrays, pairs, //! structured [`PlutusData`], and BLS12-381 curve elements. -//! +//! use crate::{ arena::Arena, binder::Eval, data::PlutusData, ledger_value::LedgerValue, machine::MachineError, typ::Type, }; -use crate::{arena::Arena, binder::Eval, data::PlutusData, machine::MachineError, typ::Type}; - /// A UPLC ground-type constant. #[non_exhaustive] #[derive(Debug, PartialEq)] @@ -44,6 +42,7 @@ pub enum Constant<'a> { Bls12_381G2Element(&'a blst::blst_p2), /// BLS12-381 Miller-loop result. Bls12_381MlResult(&'a blst::blst_fp12), + /// Cardano multi-asset ledger value. Value(&'a LedgerValue<'a>), } @@ -145,10 +144,12 @@ impl<'a> Constant<'a> { arena.alloc(Constant::Bls12_381MlResult(ml_res)) } + /// Allocates a [`Constant::Value`] wrapping a [`LedgerValue`]. pub fn ledger_value(arena: &'a Arena, v: &'a LedgerValue<'a>) -> &'a Constant<'a> { arena.alloc(Constant::Value(v)) } + /// Unwraps a [`Constant::Data`], returning the inner [`PlutusData`]. pub fn unwrap_data(&'a self) -> Result<&'a PlutusData<'a>, MachineError<'a, V>> where V: Eval<'a>, diff --git a/crates/uplc/src/ledger_value.rs b/crates/uplc/src/ledger_value.rs index ffee3a005..75a6ebc8b 100644 --- a/crates/uplc/src/ledger_value.rs +++ b/crates/uplc/src/ledger_value.rs @@ -1,3 +1,9 @@ +//! Cardano multi-asset ledger values. +//! +//! A [`LedgerValue`] is a sorted, canonical representation of a Cardano multi-asset value +//! (currency symbol → token name → quantity). It is used by the `Value`-related built-in +//! functions introduced in later Plutus versions. + use bumpalo::collections::Vec as BumpVec; use num::{Signed, Zero}; @@ -7,68 +13,100 @@ use crate::{ data::PlutusData, }; +/// Errors produced when deserialising [`PlutusData`] into a [`LedgerValue`]. #[derive(thiserror::Error, Debug)] pub enum UnValueDataError { + /// Expected a `Map` constructor but found something else. #[error("non-Map constructor")] NonMapConstructor, + /// Expected a `ByteString` constructor but found something else. #[error("non-B constructor")] NonByteStringConstructor, + /// Expected an `Integer` constructor but found something else. #[error("non-I constructor")] NonIntegerConstructor, + /// A currency symbol or token name exceeds the 32-byte limit. #[error("invalid key")] InvalidKey, + /// An inner token map is empty. #[error("empty inner map")] EmptyInnerMap, + /// Currency symbols are not in strictly ascending byte order. #[error("currency symbols not strictly ascending")] CurrencyNotAscending, + /// Token names are not in strictly ascending byte order. #[error("token names not strictly ascending")] TokenNotAscending, + /// A token quantity is zero or out of the valid range. #[error("invalid quantity")] InvalidQuantity, } +/// Errors produced by `Value` built-in function operations. #[derive(thiserror::Error, Debug)] pub enum ValueError { + /// `insertCoin` received an invalid currency symbol. #[error("insertCoin: invalid currency")] InsertCoinInvalidCurrency, + /// `insertCoin` received an invalid token name. #[error("insertCoin: invalid token")] InsertCoinInvalidToken, + /// `unionValue` produced a quantity outside the signed 128-bit range. #[error("unionValue: quantity is out of the signed 128-bit integer bounds")] UnionValueQuantityOutOfBounds, + /// `valueContains` called with a first value that has negative amounts. #[error("valueContains: first value contains negative amounts")] ValueContainsFirstNegative, + /// `valueContains` called with a second value that has negative amounts. #[error("valueContains: second value contains negative amounts")] ValueContainsSecondNegative, + /// `scaleValue` produced a quantity outside the signed 128-bit range. #[error("scaleValue: quantity out of bounds")] ScaleValueQuantityOutOfBounds, + /// `valueData` input exceeds the maximum allowed size. #[error("valueData: maximum input size ({0}) exceeded")] ValueDataMaxSizeExceeded(usize), + /// Error during `unValueData` deserialisation. #[error("unValueData: {0}")] UnValueData(#[from] UnValueDataError), + /// A quantity is outside the signed 128-bit integer bounds. #[error("Quantity out of signed 128-bit integer bounds")] QuantityOutOfBounds, } +/// A Cardano multi-asset value, sorted by currency symbol then token name. +/// +/// Entries are kept in strictly ascending order to allow efficient merging and comparison. #[derive(Debug, PartialEq)] pub struct LedgerValue<'a> { + /// Currency entries, sorted by currency symbol in ascending byte order. pub entries: &'a [CurrencyEntry<'a>], + /// Total number of individual token entries across all currencies. pub size: usize, + /// Number of token entries with a negative quantity. pub negative_count: usize, } +/// A single currency symbol and its associated token entries. #[derive(Debug, PartialEq, Clone)] pub struct CurrencyEntry<'a> { + /// Currency symbol (policy ID) as raw bytes. pub currency: &'a [u8], + /// Token entries under this currency, sorted by token name in ascending byte order. pub tokens: &'a [TokenEntry<'a>], } +/// A single token name and its quantity within a currency. #[derive(Debug, PartialEq, Clone)] pub struct TokenEntry<'a> { + /// Token name as raw bytes. pub name: &'a [u8], + /// Quantity of this token (may be negative in intermediate results). pub quantity: &'a Integer, } impl<'a> LedgerValue<'a> { + /// Returns an empty ledger value with no currency entries. pub fn empty(arena: &'a Arena) -> &'a LedgerValue<'a> { arena.alloc(LedgerValue { entries: &[], @@ -77,6 +115,7 @@ impl<'a> LedgerValue<'a> { }) } + /// Looks up the quantity for a given currency and token name, returning zero if absent. pub fn lookup_coin(&self, arena: &'a Arena, ccy: &[u8], tok: &[u8]) -> &'a Integer { for entry in self.entries { match entry.currency.cmp(ccy) { @@ -97,6 +136,9 @@ impl<'a> LedgerValue<'a> { integer(arena) } + /// Inserts or replaces a single token quantity in the value, maintaining sort order. + /// + /// If `qty` is zero, the entry is removed. pub fn insert_coin( arena: &'a Arena, ccy: &'a [u8], @@ -199,6 +241,9 @@ impl<'a> LedgerValue<'a> { }) } + /// Merges two ledger values by summing quantities for matching currency/token pairs. + /// + /// Entries with a resulting zero quantity are dropped. pub fn union_value( arena: &'a Arena, v1: &'a LedgerValue<'a>, @@ -250,6 +295,9 @@ impl<'a> LedgerValue<'a> { })) } + /// Returns `true` if every token in `v2` is present in `v1` with at least the same quantity. + /// + /// Both values must be non-negative; returns an error otherwise. pub fn value_contains(v1: &LedgerValue<'a>, v2: &LedgerValue<'a>) -> Result { // 1. Check v1 for negatives if v1.negative_count > 0 { @@ -305,6 +353,9 @@ impl<'a> LedgerValue<'a> { Ok(true) } + /// Multiplies every quantity in the value by `scalar`. + /// + /// Returns an empty value if `scalar` is zero. pub fn scale_value( arena: &'a Arena, scalar: &'a Integer, @@ -351,6 +402,7 @@ impl<'a> LedgerValue<'a> { })) } + /// Serialises a ledger value into a [`PlutusData`] map-of-maps representation. pub fn value_data( arena: &'a Arena, v: &'a LedgerValue<'a>, @@ -383,6 +435,10 @@ impl<'a> LedgerValue<'a> { Ok(PlutusData::map(arena, outer_pairs)) } + /// Deserialises a [`PlutusData`] map-of-maps into a [`LedgerValue`]. + /// + /// Validates that keys are ≤ 32 bytes, in strictly ascending order, inner maps are + /// non-empty, quantities are non-zero, and all quantities fit in a signed 128-bit range. pub fn un_value_data( arena: &'a Arena, d: &'a PlutusData<'a>, @@ -478,6 +534,7 @@ impl<'a> LedgerValue<'a> { } } +/// Counts the total number of token entries and how many have negative quantities. pub fn count_stats(entries: &[CurrencyEntry]) -> (usize, usize) { let mut total_size = 0usize; let mut negative_count = 0usize; @@ -563,6 +620,7 @@ pub fn check_quantity_range(int: &Integer) -> Result<(), ValueError> { } } +/// Returns the approximate tree depth of the value for costing purposes. pub fn value_max_depth(v: &LedgerValue) -> i64 { let outer_size = v.entries.len(); let mut max_inner = 0usize; @@ -584,6 +642,7 @@ pub fn value_max_depth(v: &LedgerValue) -> i64 { log_outer + log_inner } +/// Counts the total number of nodes in a [`PlutusData`] tree for costing purposes. pub fn data_node_count(d: &PlutusData) -> i64 { let mut total: i64 = 0; let mut stack: Vec<&PlutusData> = vec![d]; diff --git a/crates/uplc/src/lib.rs b/crates/uplc/src/lib.rs index 1b96492cd..b771c1744 100644 --- a/crates/uplc/src/lib.rs +++ b/crates/uplc/src/lib.rs @@ -46,6 +46,7 @@ //! | [`flat`] | Flat/CBOR binary encoder and decoder | //! | [`bls`] | BLS12-381 elliptic curve primitives | //! | [`typ`] | UPLC type system | +//! | [`ledger_value`] | Cardano multi-asset ledger values | //! //! [UPLC]: https://plutus.readthedocs.io/en/latest/reference/uplc-introduction.html //! [Plutus]: https://plutus.readthedocs.io/ diff --git a/crates/uplc/src/machine/info.rs b/crates/uplc/src/machine/info.rs index 59781f981..f1cd9a927 100644 --- a/crates/uplc/src/machine/info.rs +++ b/crates/uplc/src/machine/info.rs @@ -5,8 +5,9 @@ use super::ExBudget; /// Metadata captured during CEK machine evaluation. #[derive(Debug)] pub struct MachineInfo { - pub remaining_budget: ExBudget, /// Execution budget remaining after evaluation (initial budget minus consumed). + pub remaining_budget: ExBudget, + /// Execution budget consumed during evaluation. pub consumed_budget: ExBudget, /// Lines emitted by `Trace` built-in calls, in order. pub logs: Vec, diff --git a/crates/uplc/src/program.rs b/crates/uplc/src/program.rs index d695b5343..78696da91 100644 --- a/crates/uplc/src/program.rs +++ b/crates/uplc/src/program.rs @@ -51,13 +51,11 @@ where V: Eval<'a>, { /// Evaluate using Plutus V3 semantics and the default mainnet budget. - #[must_use] pub fn eval(&'a self, arena: &'a Arena) -> EvalResult<'a, V> { self.eval_version(arena, PlutusVersion::V3) } /// Evaluate with the specified Plutus version and the default mainnet budget. - #[must_use] pub fn eval_version( &'a self, arena: &'a Arena, @@ -67,7 +65,6 @@ where } /// Evaluate with an explicit Plutus version and a custom initial [`ExBudget`]. - #[must_use] pub fn eval_version_budget( &'a self, arena: &'a Arena, @@ -119,7 +116,6 @@ where /// /// `cost_model` must be ordered as expected by the corresponding /// `BuiltinCostModel` implementation for `plutus_version`. - #[must_use] pub fn eval_with_params( &'a self, arena: &'a Arena, @@ -200,6 +196,7 @@ impl<'a> Version<'a> { self.0 < &(1, 1, 0) } + /// Returns `true` if this version is `1.1.0` or above. pub fn is_at_least_1_1_0(&'a self) -> bool { self.0 >= &(1, 1, 0) } diff --git a/crates/uplc/src/term.rs b/crates/uplc/src/term.rs index cd0757a9f..b5c3aafdf 100644 --- a/crates/uplc/src/term.rs +++ b/crates/uplc/src/term.rs @@ -866,54 +866,63 @@ impl<'a, V> Term<'a, V> { Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::Bls12_381_G1_MultiScalarMul`]. pub fn bls12_381_g1_multi_scalar_mul(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G1_MultiScalarMul); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::Bls12_381_G2_MultiScalarMul`]. pub fn bls12_381_g2_multi_scalar_mul(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::Bls12_381_G2_MultiScalarMul); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::InsertCoin`]. pub fn insert_coin(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::InsertCoin); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::LookupCoin`]. pub fn lookup_coin(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::LookupCoin); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::UnionValue`]. pub fn union_value(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::UnionValue); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::ValueContains`]. pub fn value_contains(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ValueContains); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::ValueData`]. pub fn value_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ValueData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::UnValueData`]. pub fn un_value_data(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::UnValueData); Term::builtin(arena, fun) } + /// Builtin term for [`DefaultFunction::ScaleValue`]. pub fn scale_value(arena: &'a Arena) -> &'a Term<'a, V> { let fun = arena.alloc(DefaultFunction::ScaleValue); diff --git a/crates/uplc/src/typ.rs b/crates/uplc/src/typ.rs index d76903370..f93eb97a3 100644 --- a/crates/uplc/src/typ.rs +++ b/crates/uplc/src/typ.rs @@ -36,6 +36,7 @@ pub enum Type<'a> { Bls12_381G2Element, /// BLS12-381 Miller-loop result. Bls12_381MlResult, + /// Cardano multi-asset ledger value. Value, } @@ -100,6 +101,7 @@ impl<'a> Type<'a> { arena.alloc(Type::Bls12_381MlResult) } + /// Allocates a [`Type::Value`]. pub fn value(arena: &'a Arena) -> &'a Type<'a> { arena.alloc(Type::Value) }