Skip to content
Merged
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
158 changes: 158 additions & 0 deletions Sources/SkipLib/Duration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ public struct Duration: Hashable, Comparable, Sendable, CustomStringConvertible
/// The attoseconds component of the duration (0 ..< 1_000_000_000_000_000_000).
public let attoseconds: Int64

/// The seconds and attoseconds components, matching Swift's `Duration.components`.
public var components: (seconds: Int64, attoseconds: Int64) {
fatalError()
}

/// Creates a duration from seconds and attoseconds components.
public init(secondsComponent: Int64, attosecondsComponent: Int64) {
fatalError()
Expand Down Expand Up @@ -103,4 +108,157 @@ public struct Duration: Hashable, Comparable, Sendable, CustomStringConvertible
}
}

/// A type that defines a specific point in time for a given `Clock`.
public protocol InstantProtocol<Duration>: Hashable, Comparable, Sendable {
associatedtype Duration

func advanced(by duration: Duration) -> Self
func duration(to other: Self) -> Duration
}

extension InstantProtocol {
public static func + (lhs: Self, rhs: Duration) -> Self {
fatalError()
}

public static func - (lhs: Self, rhs: Duration) -> Self {
fatalError()
}

public static func - (lhs: Self, rhs: Self) -> Duration {
fatalError()
}

public static func += (lhs: inout Self, rhs: Duration) {
fatalError()
}

public static func -= (lhs: inout Self, rhs: Duration) {
fatalError()
}
}

/// A mechanism in which to measure time, and delay work until a given point in time.
public protocol Clock<Duration>: Sendable {
associatedtype Duration
associatedtype Instant: InstantProtocol where Self.Instant.Duration == Self.Duration

var now: Instant { get }
var minimumResolution: Duration { get }

func sleep(until deadline: Instant, tolerance: Duration?) async throws
}

extension Clock {
public func sleep(for duration: Duration) async throws {
fatalError()
}

public func measure(_ work: () async throws -> Void) async rethrows -> Duration {
fatalError()
}
}

/// A clock that measures time that always increments and does not stop
/// incrementing while the system is asleep.
public struct ContinuousClock: Clock, Sendable {
public struct Instant: InstantProtocol, Hashable, Comparable, Sendable, CustomStringConvertible {
public func advanced(by duration: Duration) -> Instant {
fatalError()
}

public func duration(to other: Instant) -> Duration {
fatalError()
}

public static func < (lhs: Instant, rhs: Instant) -> Bool {
fatalError()
}

public var description: String {
fatalError()
}

public static var now: Instant {
fatalError()
}
}

public init() {
}

public var now: Instant {
fatalError()
}

public var minimumResolution: Duration {
fatalError()
}

public func sleep(until deadline: Instant, tolerance: Duration? = nil) async throws {
fatalError()
}

public static var now: Instant {
fatalError()
}
}

extension Clock where Self == ContinuousClock {
public static var continuous: ContinuousClock {
fatalError()
}
}

/// A clock that measures time that always increments, but stops incrementing
/// while the system is asleep.
public struct SuspendingClock: Clock, Sendable {
public struct Instant: InstantProtocol, Hashable, Comparable, Sendable, CustomStringConvertible {
public func advanced(by duration: Duration) -> Instant {
fatalError()
}

public func duration(to other: Instant) -> Duration {
fatalError()
}

public static func < (lhs: Instant, rhs: Instant) -> Bool {
fatalError()
}

public var description: String {
fatalError()
}

public static var now: Instant {
fatalError()
}
}

public init() {
}

public var now: Instant {
fatalError()
}

public var minimumResolution: Duration {
fatalError()
}

public func sleep(until deadline: Instant, tolerance: Duration? = nil) async throws {
fatalError()
}

public static var now: Instant {
fatalError()
}
}

extension Clock where Self == SuspendingClock {
public static var suspending: SuspendingClock {
fatalError()
}
}

#endif
192 changes: 192 additions & 0 deletions Sources/SkipLib/Skip/Clock.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Copyright 2023–2026 Skip
// SPDX-License-Identifier: MPL-2.0
package skip.lib

import kotlin.time.TimeSource
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import kotlin.time.Duration as KtDuration

/**
* Kotlin representation of Swift's `InstantProtocol`.
*
* Concrete clock `Instant` types implement this marker interface and provide
* `advanced(by:)`, `duration(to:)`, and `Comparable` semantics directly.
*/
interface InstantProtocol

/**
* Kotlin representation of Swift's `Clock` protocol.
*
* Implementations measure time and offer a `sleep(until:tolerance:)` primitive.
* `Instant` is the type returned by `now`; the associated duration is always
* `skip.lib.Duration`.
*/
interface Clock<I : InstantProtocol> {
val now: I
val minimumResolution: Duration

suspend fun sleep(until: I, tolerance: Duration? = null)

suspend fun sleep(for_: Duration) {
Task.sleep(for_ = for_)
}

suspend fun measure(work: suspend () -> Unit): Duration {
val start = now
work()
val end = now
return durationBetween(start, end)
}

fun durationBetween(start: I, end: I): Duration
}

/// Convert a `skip.lib.Duration` to a `kotlin.time.Duration`.
internal fun Duration.toKotlinDuration(): KtDuration =
toNanoseconds().toDuration(DurationUnit.NANOSECONDS)

/// Convert a `kotlin.time.Duration` to a `skip.lib.Duration`.
internal fun KtDuration.toSkipDuration(): Duration =
Duration.nanoseconds(inWholeNanoseconds)

/**
* Kotlin representation of Swift's `ContinuousClock`.
*
* Backed by `kotlin.time.TimeSource.Monotonic`, which on the JVM is
* implemented by `System.nanoTime()` and continues to advance while the host
* is idle. The Swift distinction between continuous and suspending clocks
* does not exist on Android; the two clock classes share an implementation
* but Swift code that names `ContinuousClock` explicitly continues to work.
*/
class ContinuousClock : Clock<ContinuousClock.Instant> {

/**
* A point in time on a `ContinuousClock`.
*
* Backed by a `kotlin.time.TimeSource.Monotonic.ValueTimeMark`.
*/
class Instant internal constructor(internal val mark: TimeSource.Monotonic.ValueTimeMark) : InstantProtocol, Comparable<Instant> {

fun advanced(by: Duration): Instant {
return Instant(mark + by.toKotlinDuration())
}

fun duration(to: Instant): Duration {
return (to.mark - mark).toSkipDuration()
}

operator fun plus(duration: Duration): Instant = advanced(by = duration)
operator fun minus(duration: Duration): Instant = Instant(mark - duration.toKotlinDuration())
operator fun minus(other: Instant): Duration = other.duration(to = this)

override fun compareTo(other: Instant): Int = mark.compareTo(other.mark)

override fun equals(other: Any?): Boolean {
val o = other as? Instant ?: return false
return mark == o.mark
}

override fun hashCode(): Int = mark.hashCode()
override fun toString(): String = "ContinuousClock.Instant($mark)"

companion object {
val now: Instant
get() = Instant(TimeSource.Monotonic.markNow())
}
}

override val now: Instant
get() = Instant.now

override val minimumResolution: Duration
get() = Duration.nanoseconds(1L)

override suspend fun sleep(until: Instant, tolerance: Duration?) {
val remaining = -until.mark.elapsedNow()
if (remaining <= KtDuration.ZERO) return
Task.sleep(for_ = remaining.toSkipDuration())
}

override fun durationBetween(start: Instant, end: Instant): Duration {
return start.duration(to = end)
}

companion object {
val now: Instant
get() = Instant.now

// Mirror Swift's `Clock.continuous` shorthand.
val continuous: ContinuousClock = ContinuousClock()
}
}

/**
* Kotlin representation of Swift's `SuspendingClock`.
*
* On Android there is no first-class distinction between a continuous and a
* suspending clock: both are backed by `kotlin.time.TimeSource.Monotonic`.
* The class exists so Swift code that names `SuspendingClock` transpiles
* cleanly.
*/
class SuspendingClock : Clock<SuspendingClock.Instant> {

/**
* A point in time on a `SuspendingClock`.
*
* Backed by a `kotlin.time.TimeSource.Monotonic.ValueTimeMark`.
*/
class Instant internal constructor(internal val mark: TimeSource.Monotonic.ValueTimeMark) : InstantProtocol, Comparable<Instant> {

fun advanced(by: Duration): Instant {
return Instant(mark + by.toKotlinDuration())
}

fun duration(to: Instant): Duration {
return (to.mark - mark).toSkipDuration()
}

operator fun plus(duration: Duration): Instant = advanced(by = duration)
operator fun minus(duration: Duration): Instant = Instant(mark - duration.toKotlinDuration())
operator fun minus(other: Instant): Duration = other.duration(to = this)

override fun compareTo(other: Instant): Int = mark.compareTo(other.mark)

override fun equals(other: Any?): Boolean {
val o = other as? Instant ?: return false
return mark == o.mark
}

override fun hashCode(): Int = mark.hashCode()
override fun toString(): String = "SuspendingClock.Instant($mark)"

companion object {
val now: Instant
get() = Instant(TimeSource.Monotonic.markNow())
}
}

override val now: Instant
get() = Instant.now

override val minimumResolution: Duration
get() = Duration.nanoseconds(1L)

override suspend fun sleep(until: Instant, tolerance: Duration?) {
val remaining = -until.mark.elapsedNow()
if (remaining <= KtDuration.ZERO) return
Task.sleep(for_ = remaining.toSkipDuration())
}

override fun durationBetween(start: Instant, end: Instant): Duration {
return start.duration(to = end)
}

companion object {
val now: Instant
get() = Instant.now

// Mirror Swift's `Clock.suspending` shorthand.
val suspending: SuspendingClock = SuspendingClock()
}
}
4 changes: 4 additions & 0 deletions Sources/SkipLib/Skip/Duration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ data class Duration private constructor(
}
}

/** Seconds and attoseconds components, matching Swift's `Duration.components`. */
val components: Tuple2<Long, Long>
get() = Tuple2(seconds, attoseconds)

/** Total duration in nanoseconds. */
fun toNanoseconds(): Long {
return seconds * 1_000_000_000L + attoseconds / ATTOSECONDS_PER_NANOSECOND
Expand Down
Loading
Loading