Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add atomics support in the standard library #1637

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
4 changes: 2 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ jobs:

find .ninja-build -name '*.a' -delete

ctest --parallel --test-dir .ninja-build
ctest --parallel --output-on-failure --test-dir .ninja-build
push: never

- name: Reclaim disk space
Expand Down Expand Up @@ -281,7 +281,7 @@ jobs:
run: cmake --build hylo/.ninja-build ${{ matrix.cmake_generator == 'Xcode' && format('--config {0}', matrix.cmake_build_type) || '' }}

- name: Test (CMake)
run: ctest --parallel --test-dir hylo/.ninja-build ${{ matrix.cmake_generator == 'Xcode' && format('-C {0}', matrix.cmake_build_type) || '' }}
run: ctest --parallel --output-on-failure --test-dir hylo/.ninja-build ${{ matrix.cmake_generator == 'Xcode' && format('-C {0}', matrix.cmake_build_type) || '' }}

- if: ${{ matrix.use_spm }}
name: Create LLVM pkgconfig file and make it findable
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@
**.lib
**.7z
llvm.pc
Tools/__pycache__
122 changes: 122 additions & 0 deletions StandardLibrary/Sources/LowLevel/Threading/Atomic.hylo
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@

/// Defines atomic operations for `T`.
public type Atomic<T: AtomicRepresentable>: SemiRegular {

/// The storage for the atomic operations.
var storage: T.AtomicRepresentation

/// Initializes `self` with the zero value.
public init() {
&storage = .new()
}

/// Initializes `self` with `value`.
public init(value: sink T) {
&storage = .new()
store(value, ordering: /*AtomicStoreOrdering.relaxed*/ .new(value: 0))
// TODO: using atomic ordering constants produce linker errors
}

/// Atomically load the value from the atomic, using `ordering`.
public fun load(ordering: AtomicLoadOrdering) -> T {
return T.decodeAtomicRepresentation(storage.load(ordering: ordering))
}

/// Atomically store `value` into the atomic, using `ordering`.
public fun store(_ value: sink T, ordering: AtomicStoreOrdering) inout {
storage.store(T.encodeAtomicRepresentation(value), ordering: ordering)
}

/// Atomically exchanges the value in the atomic with `desired`, using `ordering`.
public fun exchange(desired: sink T, ordering: AtomicUpdateOrdering) inout -> T {
return T.decodeAtomicRepresentation(storage.exchange(desired: T.encodeAtomicRepresentation(desired), ordering: ordering))
}

/// Atomically exchanges the atomic with `desired`, if the original value is equal to `expected`, using `ordering`.
/// Returns a tuple containing the original value and a boolean indicating whether the exchange was successful.
public fun compare_and_exchange(expected: sink T, desired: sink T, ordering: AtomicUpdateOrdering) inout -> {exchanged: Bool, original: T} {
let r = storage.compare_and_exchange(
expected: T.encodeAtomicRepresentation(expected),
desired: T.encodeAtomicRepresentation(desired),
success_ordering: ordering,
failure_ordering: failure_ordering(for: ordering))
return (exchanged: r.0, original: T.decodeAtomicRepresentation(r.1))
}

/// Atomically exchanges the atomic with `desired`, if the original value is equal to `expected`, using `success_ordering` for the update, and `failure_ordering` for loading the new value in case of failure.
/// Returns a tuple containing the original value and a boolean indicating whether the exchange was successful.
public fun compare_and_exchange(expected: sink T, desired: sink T, success_ordering: AtomicUpdateOrdering, failure_ordering: AtomicLoadOrdering) inout -> {exchanged: Bool, original: T} {
let r = storage.compare_and_exchange(
expected: T.encodeAtomicRepresentation(expected),
desired: T.encodeAtomicRepresentation(desired),
success_ordering: success_ordering,
failure_ordering: failure_ordering)
return (exchanged: r.0, original: T.decodeAtomicRepresentation(r.1))
}

/// Same as `compare_and_exchange`, but may fail spuriously.
public fun weak_compare_and_exchange(expected: sink T, desired: sink T, ordering: AtomicUpdateOrdering) inout -> {exchanged: Bool, original: T} {
let r = storage.weak_compare_and_exchange(
expected: T.encodeAtomicRepresentation(expected),
desired: T.encodeAtomicRepresentation(desired),
success_ordering: ordering,
failure_ordering: failure_ordering(for: ordering))
return (exchanged: r.0, original: T.decodeAtomicRepresentation(r.1))
}

/// Same as `compare_and_exchange`, but may fail spuriously.
public fun weak_compare_and_exchange(expected: sink T, desired: sink T, success_ordering: AtomicUpdateOrdering, failure_ordering: AtomicLoadOrdering) inout -> {exchanged: Bool, original: T} {
let r = storage.weak_compare_and_exchange(
expected: T.encodeAtomicRepresentation(expected),
desired: T.encodeAtomicRepresentation(desired),
success_ordering: success_ordering,
failure_ordering: failure_ordering)
return (exchanged: r.0, original: T.decodeAtomicRepresentation(r.1))
}

}

/// Atomic operations for integer types
public extension Atomic where T.AtomicRepresentation: IntegerPlatformAtomic, T: Numeric {

/// Atomically updates `this` by adding `value` to it, using `ordering`, and return the original value.
public fun fetch_add(_ value: sink T, ordering: AtomicUpdateOrdering) inout -> T {
return T.decodeAtomicRepresentation(storage.fetch_add(T.encodeAtomicRepresentation(value), ordering: ordering))
}

/// Atomically updates `this` by subtracting `value` from it, using `ordering`, and return the original value.
public fun fetch_sub(_ value: sink T, ordering: AtomicUpdateOrdering) inout -> T {
return T.decodeAtomicRepresentation(storage.fetch_sub(T.encodeAtomicRepresentation(value), ordering: ordering))
}

/// Atomically updates `this` by using the maximum of `value` and itself, using `ordering`, and return the original value.
public fun fetch_max(_ value: sink T, ordering: AtomicUpdateOrdering) inout -> T {
return T.decodeAtomicRepresentation(storage.fetch_max(T.encodeAtomicRepresentation(value), ordering: ordering))
}

/// Atomically updates `this` by using the minimum of `value` and itself, using `ordering`, and return the original value.
public fun fetch_min(_ value: sink T, ordering: AtomicUpdateOrdering) inout -> T {
return T.decodeAtomicRepresentation(storage.fetch_min(T.encodeAtomicRepresentation(value), ordering: ordering))
}

/// Atomically updates `this` by performing a bitwise AND of `value` and itself, using `ordering`, and return the original value.
public fun fetch_and(_ value: sink T, ordering: AtomicUpdateOrdering) inout -> T {
return T.decodeAtomicRepresentation(storage.fetch_and(T.encodeAtomicRepresentation(value), ordering: ordering))
}

/// Atomically updates `this` by performing a bitwise NAND of `value` and itself, using `ordering`, and return the original value.
public fun fetch_nand(_ value: sink T, ordering: AtomicUpdateOrdering) inout -> T {
return T.decodeAtomicRepresentation(storage.fetch_nand(T.encodeAtomicRepresentation(value), ordering: ordering))
}

/// Atomically updates `this` by performing a bitwise OR of `value` and itself, using `ordering`, and return the original value.
public fun fetch_or(_ value: sink T, ordering: AtomicUpdateOrdering) inout -> T {
return T.decodeAtomicRepresentation(storage.fetch_or(T.encodeAtomicRepresentation(value), ordering: ordering))
}

/// Atomically updates `this` by performing a bitwise XOR of `value` and itself, using `ordering`, and return the original value.
public fun fetch_xor(_ value: sink T, ordering: AtomicUpdateOrdering) inout -> T {
return T.decodeAtomicRepresentation(storage.fetch_xor(T.encodeAtomicRepresentation(value), ordering: ordering))
}

}
167 changes: 167 additions & 0 deletions StandardLibrary/Sources/LowLevel/Threading/AtomicOrderings.hylo
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/// Specifies the memory ordering semantics for an atomic load operation.
public type AtomicLoadOrdering: Regular {

public memberwise init

internal let value: Int

}

public extension AtomicLoadOrdering {

/// Guarantees the atomicity of the load operation, but does not impose any ordering constraints
/// on other memory operations.
///
/// Corresponds to the `memory_order_relaxed` C++ memory ordering.
public static let relaxed: AtomicLoadOrdering = .new(value: 0)

/// An acquiring load operation syncrhonizes with a releasing store operation on the same atomic
/// variable. The thread performing the acquiring operation agrees with the thread performing the
/// releasing operation that all the subsequent load operations (atomic or not) on the acquiring
/// thread happen after the atomic operation itself.
///
/// Provides a barrier that prevents read/write memory operations to be reordered before the
/// atomic load. This barrier can be used to acquire a lock.
///
/// Corresponds to the `memory_order_acquire` C++ memory ordering.
public static let acquiring: AtomicLoadOrdering = .new(value: 1)

/// A sequentially consistent load operation provides the same guarantees as an acquiring load
/// and also guarantees that all sequentially consistent operations on the atomic variable will
/// have a total squential order, across all threads.
///
/// Provides a barrier that prevents read/write memory operations to be reordered before and
/// after the atomic load.
///
/// Corresponds to the `memory_order_seq_cst` C++ memory ordering.
public static let sequentially_consistent: AtomicLoadOrdering = .new(value: 4)

}

/// Specifies the memory ordering semantics for an atomic store operation.
public type AtomicStoreOrdering: Regular {

public memberwise init

internal let value: Int

}

public extension AtomicStoreOrdering {

/// Guarantees the atomicity of the store operation, but does not impose any ordering constraints
/// on other memory operations.
///
/// Corresponds to the `memory_order_relaxed` C++ memory ordering.
public static let relaxed: AtomicStoreOrdering = .new(value: 0)

/// A releasing store operation syncrhonizes with an acquiring load operation on the same atomic
/// variable. The thread performing the releasing operation agrees with the thread performing the
/// acquiring operation that all the previous store operations (atomic or not) on the releasing
/// thread will be seen before the atomic operation itself.
///
/// Provides a barrier that prevents read/write memory operations to be reordered after the
/// atomic store. This barrier can be used to release a lock.
///
/// Corresponds to the `memory_order_release` C++ memory ordering.
public static let releasing: AtomicStoreOrdering = .new(value: 2)

/// A sequentially consistent store operation provides the same guarantees as a releasing store
/// and also guarantees that all sequentially consistent operations on the atomic variable will
/// have a total squential order, across all threads.
///
/// Provides a barrier that prevents read/write memory operations to be reordered before and
/// after the atomic store.
///
/// Corresponds to the `memory_order_seq_cst` C++ memory ordering.
public static let sequentially_consistent: AtomicStoreOrdering = .new(value: 4)

}

/// Specifies the memory ordering semantics for an atomic read-modify-write (update) operation.
public type AtomicUpdateOrdering: Regular {

public memberwise init

internal let value: Int

}

public extension AtomicUpdateOrdering {

/// Guarantees the atomicity of the load operation, but does not impose any ordering constraints
/// on other memory operations.
///
/// Corresponds to the `memory_order_relaxed` C++ memory ordering.
public static let relaxed: AtomicUpdateOrdering = .new(value: 0)

/// An acquiring update operation syncrhonizes with a releasing store operation on the same
/// atomic variable whose value it reads. The thread performing the acquiring operation agrees
/// with the thread performing the releasing operation that all the subsequent load operations
/// (atomic or not) on the acquiring thread happen after the atomic operation itself.
///
/// Provides a barrier that prevents read/write memory operations to be reordered before the
/// atomic operation. This barrier can be used to acquire a lock.
///
/// Corresponds to the `memory_order_acquire` C++ memory ordering.
public static let acquiring: AtomicUpdateOrdering = .new(value: 1)

/// A releasing update operation syncrhonizes with an acquiring load operation on the same atomic
/// variable that reads the updated value. The thread performing the releasing operation agrees
/// with the thread performing the acquiring operation that all the previous store operations
/// (atomic or not) on the releasing thread will be seen before the atomic operation itself.
///
/// Provides a barrier that prevents read/write memory operations to be reordered after the
/// atomic update. This barrier can be used to release a lock.
///
/// Corresponds to the `memory_order_release` C++ memory ordering.
public static let releasing: AtomicUpdateOrdering = .new(value: 2)

/// An acquiring and releasing update operation syncrhonizes with both acquiring loads that read
/// the updated value and with the releasing stores that update the value that this update reads.
/// The thread performing the update operation agrees with the threads performing the acquiring
/// load that all the previous store operations (atomic or not) on the releasing thread will be
/// seen before the atomic operation itself. The thread performing the update operation also
/// agrees with the threads performing the releasing store of the value read by the update that
/// all the previous store operations (atomic or not) on the updating thread happen before the
/// update.
///
/// Provides a barrier that prevents read/write memory operations to be reordered before and
/// after the atomic update.
///
/// Corresponds to the `memory_order_seq_cst` C++ memory ordering.
public static let acquiring_and_releasing: AtomicUpdateOrdering = .new(value: 3)

/// A sequentially consistent update operation provides the same guarantees as an acquiring and
/// releasing update and also guarantees that all sequentially consistent operations on the
/// atomic variable will have a total squential order, across all threads.
///
/// Provides a barrier that prevents read/write memory operations to be reordered before and
/// after the atomic update.
///
/// Corresponds to the `memory_order_seq_cst` C++ memory ordering.
public static let sequentially_consistent: AtomicUpdateOrdering = .new(value: 4)

}

/// Transforms from an atomic update ordering to a corresponding atomic load ordering, to be used
/// in `compare_and_exchange` operations to get the failure ordering from the success ordering.
// TODO: this should be internal, but we have a bug that generates a linker error
public fun failure_ordering(for ordering: AtomicUpdateOrdering) -> AtomicLoadOrdering {
if ordering == /*AtomicUpdateOrdering.relaxed*/ AtomicUpdateOrdering(value: 0) {
return /*AtomicLoadOrdering.relaxed.copy()*/ AtomicLoadOrdering(value: 0)
}
if ordering == /*AtomicUpdateOrdering.acquiring*/ AtomicUpdateOrdering(value: 1) {
return /*AtomicLoadOrdering.acquiring.copy()*/ AtomicLoadOrdering(value: 1)
}
if ordering == /*AtomicUpdateOrdering.releasing*/ AtomicUpdateOrdering(value: 2) {
return /*AtomicLoadOrdering.relaxed.copy()*/ AtomicLoadOrdering(value: 0)
}
if ordering == /*AtomicUpdateOrdering.acquiring_and_releasing*/ AtomicUpdateOrdering(value: 3) {
return /*AtomicLoadOrdering.acquiring.copy()*/ AtomicLoadOrdering(value: 1)
}
if ordering == /*AtomicUpdateOrdering.sequentially_consistent*/ AtomicUpdateOrdering(value: 4) {
return /*AtomicLoadOrdering.sequentially_consistent.copy()*/ AtomicLoadOrdering(value: 4)
}
trap()
}
Loading
Loading