Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/bubble_sort.inputs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"operand_stack": ["16"],
"advice_stack": ["7", "1", "8", "4", "3", "5", "19", "13", "0", "4", "7", "2", "5", "1", "5", "7"]
}

384 changes: 384 additions & 0 deletions examples/bubble_sort.masm
Original file line number Diff line number Diff line change
@@ -0,0 +1,384 @@
# Bubble sort - but provable!
#
# Bubble sort is a sorting algorithm that repeatedly steps through the array,
# compares adjacent elements, and swaps them if they are in the wrong order.
# The process is repeated until the array is sorted, with smaller elements "bubbling" to the top of the array.
#
# Time Complexity:
# - Best Case: O(n) (when the array is already sorted).
# - Average Case: O(n^2) (for a randomly ordered array).
# - Worst Case: O(n^2) (when the array is sorted in reverse order).
#
# Space Complexity:
# - O(1), requiring only a constant amount of space regardless of the input size. (generally yes; not the case here)
#
# Benefits:
# - Simplicity: Easy to understand and implement, suitable for small arrays.
# - In-place: Requires only a constant amount O(1) of additional memory space. (generally yes; not the case here)
# - Adaptive: Efficient for datasets that are already substantially sorted, as it can
# detect if the array is already sorted and stop early.
#
# Trade-offs:
# - Inefficiency: Performance degrades quickly with large datasets compared to more
# advanced algorithms like quicksort or mergesort.
# - High Operation Count: Requires multiple passes through the entire array,
# leading to a higher number of overall operations.
#
# Usage Guidelines:
# - Do's:
# - Add the length of the array as the only public input element in the `operand_stack`.
# - Add the array to be sorted as private input elements in the `advice_stack`.
# - Dont's:
# - Avoid running the program without correctly populating the `operand_stack` and `advice_stack`.
# - Do not input a length greater than the actual length of the array.
# - Note that the VM does not support negative numbers.
# - Additional Information:
# - The VM will output only the first 16 elements of your sorted array.
# - The program processes 'n' elements equal to the number added as public input,
# regardless of the actual length of the array.
#
# Example Usage:
# To sort an array of 10 elements, add '10' to the `operand_stack` as public input,
# and add the array elements onto the `advice_stack` as private input.
# Execute the program to get the sorted array as output.

# GLOBAL INPUTS
# -------------------------------------------------------------------------------------------------

# The memory address at which the `swapped` boolean is stored
const.SWAPPED_ADDRESS=0

# The memory address at which the `i_pointer` variable is stored
const.I_POINTER_ADDRESS=1

# The memory address at which the `j_pointer` variable is stored
const.J_POINTER_ADDRESS=2

# The memory address at which the `counter` variable is stored
const.COUNTER_ADDRESS=3

# The memory address at which the `length` variable is stored
const.LENGTH_ADDRESS=4

#! Initialises memory for sorting.
#!
#! Input:
#! operand_stack: [n, ...]
#! advice_stack: [d0, d1, .., dn]
#! Output: [...]
#!
#! Where:
#! n: is the count of elements to be sorted.
proc.initialise
# initialise `swapped` boolean - initialised at 0 / false
push.0
push.SWAPPED_ADDRESS
mem_store

# initialise `i` index - initialised at 5 because mem[5] is the array start index
push.5
push.I_POINTER_ADDRESS
mem_store

# initialise `j` index - initialised at 6 because mem[6] is the second array index
push.6
push.J_POINTER_ADDRESS
mem_store

# initialise `counter` - initialised at 0
push.0
push.COUNTER_ADDRESS
mem_store

# initialise `length` - initialised as the array length using the public inputs
push.LENGTH_ADDRESS
mem_store

# Loop over the `advice_stack` pushing values 1-by-1 on the `operand_stack`
push.1
while.true
# Push 1 element from the array into the `operand_stack`
adv_push.1
Copy link
Contributor

@hackaugusto hackaugusto Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: For production code almost always the data should always be validated, here we only know an array of length n was sorted, but we know nothing about the contents. To validate the contents the input should be a hash of the array instead of its length (this would not leak information because we use cryptographic secure hashes).

To implement this, the stack input would be a commitment instead of a counter. The length would go in the advice_stack (or the advice_map which has native support for length). The loop would use adv_pipe instead of adv_push. The adv_pipe instruction does a few things

  1. reads the data from the advice stack
  2. hashes the data read
  3. saves it to memory

Here is an example of that: https://github.com/0xPolygonMiden/examples/blob/main/examples/matrix_multiplication.masm#L108-L146


# Load and increment `counter` to keep track of added elements
mem_load.3
add.1
dup
mem_store.3

# Load `length` array length and check if `counter` == `length`
# signifying that all elements from array have been added to `operand_stack`
mem_load.4
eq
if.true
# Stop looping and reset `counter` to 0
push.0
push.0
mem_store.3
else
# Continue looping
push.1
end
end

# Loop over the `operand_stack` pushing values 1-by-1 into memory starting at mem[5]
push.1
while.true
# Offset by 5 considering that we have 5 pre-set variables in `memory`
# and that memory is linear meaning that array starts at mem[5]
push.5

# Load `counter` counter and add to 5 enabling incremental insertion
mem_load.3
add

# store element at that mem[5 + g]
mem_store

# increment `counter` by 1 and overwrite `memory`
mem_load.3
add.1
mem_store.3

# Load `counter` counter and `length` array length;
# check if `counter` == `length` signifying that all elements
# have been placed in `memory`
mem_load.3
mem_load.4
eq
if.true
# Stop looping and reset `counter` to 0
push.0
push.0
mem_store.3
else
# Continue looping
push.1
end
end

# Prepare the `operand_stack` for sort loop
push.1
end

# swapped() -> void
#
# Sets the `swapped` boolean variable to 1 - true
proc.swapped
push.1
mem_store.0
end

# notSwapped() -> void
#
# Sets the `swapped` boolean variable to 0 - false
#
proc.notSwapped
push.0
mem_store.0
end

# incrementCounters() -> void
#
# Increments the `i` and `j` counter variables by 1
proc.incrementCounters
# load `i` counter, increment it by 1 and store it in mem[1]
mem_load.1
add.1
mem_store.1

# load `j` counter, increment it by 1 and store it in mem[2]
mem_load.2
add.1
mem_store.2
end

# decrementCounters() -> void
#
# Decrements the `i` and `j` counter variables by 1
proc.decrementCounters
# load `i` counter, decrement it by 1 and store it in mem[1]
mem_load.1
sub.1
mem_store.1

# load `j` counter, decrement by 1 and store it in mem[2]
mem_load.2
sub.1
mem_store.2
end
Comment on lines +181 to +209
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: For production code, the j would have to be removed. Since it is the same as i + 1. All the instructions manipulating it are extra cycle cost that can be removed.


# resetCounters() -> void
#
# Resets the `i` and `j` counter variables to their initial state if required conditions are met
proc.resetCounters
# It is needed to reset the counters if `i` and `j`
# are pointing the end of the array. Resetting the counters
# enables us to start over from the initial mem[i], mem[j]

# Load `j` counter and `length` array length
mem_load.2
mem_load.4

# Offset by 5 considering that we have 5 pre-set variables in `memory`
# and that memory is linear meaning that array starts at mem[5]
add.5

# Check if they are equal
eq
if.true
# Reset `i`, `j` counters and `swapped` boolean to their initial values
push.6
mem_store.2
push.5
mem_store.1
exec.notSwapped
end
end

# loadValues() -> void
#
# Loads counters `i` and `j` and loads [a,b,...] `operand_stack` elements from mem[i], mem[j]
proc.loadValues
# load `j` counter
mem_load.2

# load value `b` at index mem[j]
mem_load

# load `i` counter
mem_load.1

# load value `a` at index mem[i]
mem_load
end

# storeValues() -> void
#
# Loads counters `i` and `j` and stores [a,b,...] `operand_stack` elements in mem[i], mem[j]
proc.storeValues
# load `i` counter
mem_load.1

# store value `a` at index mem[i]
mem_store

# load `j` counter
mem_load.2

# store value `b` at index mem[j]
mem_store
end

# sort() -> void
#
# Loads values [a,b,...] from `memory` into the `operand_stack` before sorting them
proc.sort
# Load values `a` and `b` into the `operand_stack`
exec.loadValues

# Duplicate and compare them
dup.1
dup.1
lt

# If a > b then swap and store else store values as is
if.true
swap
exec.swapped
exec.storeValues
else
drop drop
end

# Increment counters to point to the next two elements of the array
exec.incrementCounters
end

# sorted() -> bool
#
# Checks if the array has been sorted
proc.sorted
# Array is sorted if counters `i` and `j` are at end of array && `swapped` == 0
# We don't need to load both `i` and `j`, using only `j` is sufficient
# Load `j` counter and `length` array length
mem_load.2
mem_load.4

# Offset by 5 considering that we have 5 pre-set variables in `memory`
# and that memory is linear meaning that array starts at mem[5]
add.5

# Check if they are equal; signifying end of array
eq

# Load `swapped` boolean
mem_load.0
not

# Check if both requirements are true
and
if.true
push.0
else
push.1
end
end


# printResult() -> void
#
# Adds the array elements to the `operand_stack` before program completion and output
proc.printResult
push.1
while.true
# Load the `i` counter; at this moment will be equal to `length`
mem_load.1

# Load from `memory` to `operand_stack` element at location mem[i]
mem_load

# Load `counter` counter and increment by 1; signifying that 1 element
# has been added to the `operand_stack` from `memory`
mem_load.3
add.1
mem_store.3

# Decrement counters - going through `memory` in reverse order
exec.decrementCounters

# Load `length` and `counter`
mem_load.4
mem_load.3

# Check if `counter` == `length` meaning that we have added all elements to the `operand_stack`
eq

# Exits the loop
if.true
push.0
else
push.1
end
end
end

begin
# Initialise the VM
exec.initialise

# Loop over the array and sort repeatedly
while.true
# If looping has been done over the whole array reset counters to start over from the beginning
exec.resetCounters

# Sort elements two-by-two repeatedly
exec.sort

# Check if the array is sorted if so then stop looping
exec.sorted
end
Comment on lines +367 to +380
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: for production code we also want to have a nice interface. This would be better wrapper around a single sort procedure that people can use. Things like initialise, resetCounters, sorted should be treated as implementation details.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the code would be both more efficient and more readable if it used the stack more often. Here is one suggestion:

#! Sort data
#!
#! Input:
#!   operand_stack: [addr, C, ...]
#!   advice_stack: [n, d0, d1, .., dn, ..]
#! Output: [...]
#!
#! Where:
#!   addr: address were the data will be loaded and sorted in-place
#!   C: a commitment to the data to be sorted
#!   n: the number of elements to be sorted
#!   d0..dn: the data to be sorted
#!
#! Cycles: X
proc.sort
  # save a copy of addr
  dup movdn.6
  # => [C, B, A, addr, C, addr ..]

  # initialize the hasher state
  # TODO: handle padding / input array with odd number of elements
  padw padw padw
  # => [C, B, A, addr, C, addr, ..]

  # load length
  adv_push.1
  # => [len, C, B, A, addr, C, addr, ..]

  # load data 
  # optimization: negate the counter, we are going to loop from `[-n,0)`
  neg
  dup neq.0
  while.true
    movdn.13 # save len (TODO: movup?)
    adv_pipe
    movup.13 add.2 # update len
    dup neq.0
  end

  # clear counter and array end 
  drop movup.13 drop

  # TODO: verify commitment

  # sort
  # 1. copy the addr
  # 2. have a boolean to check if any swaps happened
  # 3. stop when the boolean is false
end


# Add array to operand_stack for output - printing result
exec.printResult
end