Skip to content

Commit

Permalink
Initial commit; WIP; non-stable API
Browse files Browse the repository at this point in the history
  • Loading branch information
Riley-Kilgore committed Dec 18, 2024
1 parent 7cedf9a commit f5bdea9
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 0 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Continuous Integration

on:
push:
branches: ["main"]
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: aiken-lang/setup-aiken@v1
with:
version: v1.1.9
- run: aiken fmt --check
- run: aiken check -D
- run: aiken build
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Aiken compilation artifacts
artifacts/
# Aiken's project working directory
build/
# Aiken's default documentation export
docs/
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div align="center">
<hr />
<h2 align="center" style="border-bottom: none"><img style="position: relative; top: 0.25rem;" src="https://raw.githubusercontent.com/aiken-lang/branding/main/assets/icon.png" alt="Aiken" height="30" /> aiken/bench</h2>

[![Licence](https://img.shields.io/github/license/aiken-lang/bench?style=for-the-badge)](https://github.com/aiken-lang/bench/blob/main/LICENSE)
[![Continuous Integration](https://img.shields.io/github/actions/workflow/status/aiken-lang/bench/continuous-integration.yml?style=for-the-badge)](https://github.com/aiken-lang/bench/actions/workflows/continuous-integration.yml)
<hr/>
</div>

The official library for writing _samplers_ (a.k.a Scaled Fuzzers) for the [Aiken](https://aiken-lang.org) Cardano smart-contract language.

> ### ⚠️ WARNING
>
> **IMPORTANT:** This is a work in progress and the API is not stable yet; additionally Samplers are not yet supported in the current version of Aiken (v1.1.9).
## Installation

```
aiken add aiken-lang/bench --version v0.0.0
```

## Getting started

First, make sure you have the [Aiken's user manual about tests](https://aiken-lang.org/language-tour/tests#property-based-test); in particular the section about benchmarking functions.

In many situations, you can use primitives from this library out-of-the-box, composing them inline when necessary. For example, if you need a growing non-empty list of values, you can simply write:

```
use aiken/bench
bench my_bench(xs via bench.list(bench.int(Linear(1)), Linear(1))) {
// some function
}
```

You can also write your own more complex sampler. Note that writing good samplers can be complicated, so here are a few guiding principles you should follow.. TODO
212 changes: 212 additions & 0 deletions lib/aiken/sample.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
use aiken/builtin
use cardano/assets.{Value}
use aiken/interval.{Interval}
use cardano/script_context.{ScriptContext}
use cardano/transaction.{Input, Output, ScriptPurpose, Transaction}
use aiken/crypto.{Blake2b_224, Hash}
use cardano/address.{Address, Credential, Inline, VerificationKey}
use aiken/collection/dict

/// A growth pattern determines how complexity scales with input size
pub type Growth {
/// Constant growth: f(n) = c
Constant
/// Linear growth: f(n) = n
Linear(Int)
/// Exponential growth: f(n) = 2^n
Exponential(Int)
/// Logarithmic growth: f(n) = log(n)
Logarithmic(Int)
}

/// Core random number generator that produces a byte (0-255)
fn rand(prng: PRNG) -> Option<(PRNG, Int)> {
when prng is {
Seeded { seed, choices } -> {
let choice = builtin.index_bytearray(seed, 0)
Some((
Seeded {
seed: crypto.blake2b_256(seed),
choices: builtin.cons_bytearray(choice, choices),
},
choice,
))
}
Replayed { cursor, choices } -> {
if cursor >= 1 {
let cursor = cursor - 1
Some((
Replayed { cursor, choices },
builtin.index_bytearray(choices, cursor),
))
} else {
None
}
}
}
}

/// Apply a growth pattern to scale an input size
pub fn apply_growth(pattern: Growth, n: Int) -> Int {
when pattern is {
Constant -> 1
Linear(base) -> base * n
Exponential(base) ->
if n <= 0 {
0
} else {
exp_int_helper(base, n, 1)
}
Logarithmic(base) ->
if n <= 1 {
0
} else {
count_divisions(base, n, 0)
}
}
}

// TODO: Probably need to use a builtin in the future.
fn exp_int_helper(base: Int, n: Int, acc: Int) -> Int {
if base <= 0 {
acc
} else {
exp_int_helper(base - 1, n, acc * n)
}
}

// TODO: Probably need to use a builtin in the future.
fn count_divisions(base: Int, n: Int, acc: Int) -> Int {
if n <= 1 {
acc
} else {
count_divisions(base, n / base, acc + 1)
}
}

// SAMPLERS:

/// Create a constant sampler that always returns the same value
pub fn constant(x: a) -> Sampler<a> {
fn(_n) { fn(prng) { Some((prng, x)) } }
}

/// Create a sampler that generates integers with configurable growth
pub fn int(growth: Growth) -> Sampler<Int> {
fn(n) -> Fuzzer<Int> {
let scaled_multiplier = apply_growth(growth, n)
fn(prng) {
when rand(prng) is {
Some((new_prng, value)) -> Some((new_prng, value * scaled_multiplier))
None -> None
}
}
}
}

/// Generate bytestrings with length scaling according to the growth pattern
pub fn bytestring(growth: Growth) -> Sampler<ByteArray> {
fn(n) {
let length_fuzzer = int(growth)(n)
fn(prng) {
when length_fuzzer(prng) is {
Some((prng2, len)) -> {
todo
}
None -> None
}
}
}
}

fn list_handler(items_so_far: List<a>, i: Int, scaled_length: Int, fuzzer: Fuzzer<a>, prng: PRNG) -> Option<(PRNG, List<a>)> {
if i > scaled_length {
Some((prng, items_so_far))
} else {
when fuzzer(prng) is {
Some((new_prng, item)) ->
list_handler([item, ..items_so_far], i + 1, scaled_length, fuzzer, new_prng)
None -> None
}
}
}

/// Create a sampler that generates lists with configurable growth
pub fn list(element_sampler: Sampler<a>, growth: Growth) -> Sampler<List<a>> {
fn(n) {
let scaled_length = apply_growth(growth, n)
let element_fuzzer = element_sampler(n)
fn(prng) {
list_handler([], 0, scaled_length, element_fuzzer, prng)
}
}
}

pub fn pair(first_sampler: Sampler<a>, second_sampler: Sampler<b>) -> Sampler<Pair<a, b>> {
fn(n) {
let first_fuzzer = first_sampler(n)
let second_fuzzer = second_sampler(n)
fn(prng) {
when first_fuzzer(prng) is {
Some((prng2, first_val)) -> {
when second_fuzzer(prng2) is {
Some((prng3, second_val)) ->
Some((prng3, Pair(first_val, second_val)))
None -> None
}
}
None -> None
}
}
}
}

pub fn pairs(first_sampler: Sampler<a>, second_sampler: Sampler<b>, growth: Growth) {
fn (n) {
let scaled_length = apply_growth(growth, n)
let pair_sampler = pair(first_sampler, second_sampler)(n)

fn(prng) {
list_handler([], 0, scaled_length, pair_sampler, prng)
}
}
}

pub fn dict(key_sampler: Sampler<ByteArray>, value_sampler: Sampler<a>, growth: Growth) {
fn (n) {
let scaled_length = apply_growth(growth, n)
map(pairs(key_sampler, value_sampler, growth), fn(pairs) {
dict.from_pairs(pairs)
})
}
}

pub fn map(sampler: Sampler<a>, f: fn(a) -> b) -> Sampler<b> {
fn(n) {
let fuzzer = sampler(n)
fn(prng) {
when fuzzer(prng) is {
Some((prng2, a)) -> Some((prng2, f(a)))
None -> None
}
}
}
}

pub fn map2(sampler: Sampler<a>, sampler2: Sampler<b>, f: fn(a, b) -> c) -> Sampler<c> {
fn(n) {
let fuzzer1 = sampler(n)
let fuzzer2 = sampler2(n)
fn(prng) {
when fuzzer1(prng) is {
Some((prng2, a)) -> {
when fuzzer2(prng2) is {
Some((prng3, b)) -> Some((prng3, f(a, b)))
None -> None
}
}
None -> None
}
}
}
}

0 comments on commit f5bdea9

Please sign in to comment.