Skip to content
Open
10 changes: 10 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/whisperkit-Package.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ArgmaxCoreMacrosTests"
BuildableName = "ArgmaxCoreMacrosTests"
BlueprintName = "ArgmaxCoreMacrosTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
12 changes: 11 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 36 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
// swift-tools-version: 6.2
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
import CompilerPluginSupport
import Foundation
import PackageDescription

let approachableConcurrencySettings: [SwiftSetting] = [
.enableUpcomingFeature("InferIsolatedConformances"),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
]

let macroPlugin = Target.macro(
name: "ArgmaxCoreMacroPlugin",
dependencies: [
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax")
]
)

let macroTarget = Target.target(
name: "ArgmaxCoreMacros",
dependencies: [
"ArgmaxCore",
"ArgmaxCoreMacroPlugin",
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
],
swiftSettings: approachableConcurrencySettings
)

let macroTestTarget = Target.testTarget(
name: "ArgmaxCoreMacrosTests",
dependencies: [
"ArgmaxCoreMacros",
"ArgmaxCoreMacroPlugin",
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax")
]
)

Comment thread
naykutguven marked this conversation as resolved.
let package = Package(
name: "whisperkit",
platforms: [
Expand All @@ -34,6 +65,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/huggingface/swift-transformers.git", .upToNextMinor(from: "1.1.6")),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.7.0"),
.package(url: "https://github.com/swiftlang/swift-syntax", from: "602.0.0"),
] + (isServerEnabled() ? [
.package(url: "https://github.com/vapor/vapor.git", from: "4.115.1"),
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.10.2"),
Expand All @@ -45,6 +77,8 @@ let package = Package(
name: "ArgmaxCore",
swiftSettings: approachableConcurrencySettings
),
macroPlugin,
macroTarget,
.target(
name: "WhisperKit",
dependencies: [
Expand All @@ -63,6 +97,7 @@ let package = Package(
],
swiftSettings: approachableConcurrencySettings
),
macroTestTarget,
.testTarget(
name: "WhisperKitTests",
dependencies: [
Expand Down
54 changes: 54 additions & 0 deletions Sources/ArgmaxCore/ConcurrencyUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,57 @@ public actor EarlyStopActor {
return shouldStop.removeValue(forKey: uuid)
}
}

// MARK: - Mutex

@dynamicMemberLookup
public final class Mutex<Value: Sendable>: Sendable {
private let lock: OSAllocatedUnfairLock<Value>

public init(_ value: Value) {
lock = .init(initialState: value)
}

public var value: Value {
lock.withLock { $0 }
}

@discardableResult
public func mutate<Result: Sendable>(_ mutation: @Sendable (inout Value) throws -> Result) rethrows -> Result {
try lock.withLock { try mutation(&$0) }
}

/// Set to the new value and return the old value.
@discardableResult
public func set(_ newValue: Value) -> Value {
lock.withLock { value in
let oldValue = value
value = newValue
return oldValue
}
}

/// Set property to the new value and return the old value.
@discardableResult
public func set<T: Sendable>(_ keyPath: WritableKeyPath<Value, T>, to newValue: T) -> T {
lock.withLock { value in
let oldValue = value[keyPath: keyPath]
value[keyPath: keyPath] = newValue
return oldValue
}
}

public subscript<T: Sendable>(dynamicMember keyPath: WritableKeyPath<Value, T>) -> T {
get { value[keyPath: keyPath] }
set { lock.withLock { value in value[keyPath: keyPath] = newValue } }
}

public subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T {
value[keyPath: keyPath]
}
}

// MARK: - KeyPath + @unchecked @retroactive Sendable

extension KeyPath: @unchecked @retroactive Sendable where Root: Sendable, Value: Sendable { }

14 changes: 14 additions & 0 deletions Sources/ArgmaxCoreMacroPlugin/ArgmaxCoreMacroPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// For licensing see accompanying LICENSE.md file.
// Copyright © 2026 Argmax, Inc. All rights reserved.

import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct ArgmaxCoreMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
ThreadSafeMacro.self,
ThreadSafeInitializerMacro.self,
ThreadSafePropertyMacro.self,
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// For licensing see accompanying LICENSE.md file.
// Copyright © 2026 Argmax, Inc. All rights reserved.

import Foundation
import SwiftCompilerPlugin
import SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
import RegexBuilder

extension ClassDeclSyntax {
private static let trailingCallSuffixRegex = Regex {
"("
ZeroOrMore {
CharacterClass.anyOf(")").inverted
}
")"
Anchor.endOfLine
}

private static let integerLiteralRegex = Regex {
Anchor.startOfLine
Optionally { "-" }
OneOrMore(.digit)
Anchor.endOfLine
}

private static let doubleLiteralRegex = Regex {
Anchor.startOfLine
Optionally { "-" }
OneOrMore(.digit)
Optionally { "." }
ZeroOrMore(.digit)
Anchor.endOfLine
}

private static let quotedStringRegex = Regex {
Anchor.startOfLine
"\""
ZeroOrMore {
CharacterClass.anyOf("\n").inverted
}
"\""
Anchor.endOfLine
}

/// Returns the list of mutable stored properties in the class.
var storedVariables: [(name: String, type: String, defaultValue: String?)] {
var storedVars = [(String, String, String?)]()

for member in memberBlock.members {
guard
let varDecl = member.decl.as(VariableDeclSyntax.self),
varDecl.isMutable
else { continue }

for binding in varDecl.bindings {
if
binding.accessorBlock == nil,
let pattern = binding.pattern.as(IdentifierPatternSyntax.self)
{
let name = pattern.identifier.text.trimmingCharacters(in: .whitespacesAndNewlines)
let defaultValue = binding.initializer?.value.trimmedDescription
.trimmingCharacters(in: .whitespacesAndNewlines) ?? binding
.typeAnnotation?.type.defaultValueForOptional

if let typeAnnotation = binding.typeAnnotation {
let type = typeAnnotation.type.trimmedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
storedVars.append((name, type, defaultValue))
} else if let defaultValue {
// Heuristically tries to infer the type from the default value
let value = stripTrailingCallSuffix(from: defaultValue)
.trimmingCharacters(in: .whitespacesAndNewlines)

let type: String =
if value == "true" || value == "false" {
"Bool"
} else if value.wholeMatch(of: Self.integerLiteralRegex) != nil {
"Int"
} else if value.wholeMatch(of: Self.doubleLiteralRegex) != nil {
"Double"
} else if value.wholeMatch(of: Self.quotedStringRegex) != nil {
"String"
} else {
value
}
storedVars.append((name, type, defaultValue))
Comment on lines +71 to +88
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

The type inference fallback uses the default value text as the inferred type when it doesn’t match a simple literal (e.g. .init(), .someCase(), [], [:]). This can produce invalid types like .init in generated code when the property has no explicit type annotation. Consider detecting these patterns and emitting a diagnostic requiring an explicit type annotation (or expanding inference to handle common cases).

Copilot uses AI. Check for mistakes.
}
}
}
}
return storedVars
}

private func stripTrailingCallSuffix(from value: String) -> String {
guard let match = value.firstMatch(of: Self.trailingCallSuffixRegex) else {
return value
}
return String(value[..<match.range.lowerBound])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// For licensing see accompanying LICENSE.md file.
// Copyright © 2026 Argmax, Inc. All rights reserved.

import SwiftSyntax

extension TypeSyntax {
var defaultValueForOptional: String? {
if self.as(OptionalTypeSyntax.self) != nil {
return "nil"
}
return nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// For licensing see accompanying LICENSE.md file.
// Copyright © 2026 Argmax, Inc. All rights reserved.

import Foundation
import SwiftSyntax

extension VariableDeclSyntax {
var isMutable: Bool {
guard
bindingSpecifier.text == "var",
attributes.isEmpty,
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

isMutable currently returns false whenever a var has any attributes (attributes.isEmpty). This prevents @ThreadSafe from tracking properties that already have attributes (including @ThreadSafeProperty itself, property wrappers, etc.) and makes the later “already tracked” attribute check in ThreadSafeMacro unreachable. Consider relaxing this to allow attributes and instead filter only unsupported cases (e.g. computed/accessor vars, multiple bindings, etc.).

Suggested change
attributes.isEmpty,

Copilot uses AI. Check for mistakes.
bindings.count == 1,
Comment on lines +8 to +12
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

VariableDeclSyntax.isMutable requires attributes.isEmpty, which means a stored property already annotated with @ThreadSafeProperty will be excluded from ClassDeclSyntax.storedVariables. As a result, @ThreadSafe can generate an _InternalState that omits those fields, and the accessor expansion will then reference missing members (compile-time error). Consider splitting this into (a) “is mutable stored identifier binding” and (b) “eligible for auto-annotation”, and ensure storedVariables includes vars that already have @ThreadSafeProperty even if they have attributes.

Copilot uses AI. Check for mistakes.
let binding = bindings.first,
binding.accessorBlock == nil,
let _ = binding.pattern.as(IdentifierPatternSyntax.self)
else {
return false
}
return true
}
}
Loading
Loading