Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.4
// swift-tools-version:5.6
import PackageDescription

let package = Package(
Expand Down
12 changes: 10 additions & 2 deletions Sources/LeafKit/Exports.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@
extension Character {

// MARK: - LeafToken specific identities (Internal)
static var tagIndicator: Character = .octothorpe

private static let _tagIndicator = SendableBox(Character.octothorpe)
static var tagIndicator: Character {
get {
_tagIndicator.value
}
set(newValue) {
_tagIndicator.value = newValue
}
}

var isValidInTagName: Bool {
return self.isLowercaseLetter
|| self.isUppercaseLetter
Expand Down
2 changes: 1 addition & 1 deletion Sources/LeafKit/LeafAST.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import NIO

/// `LeafAST` represents a "compiled," grammatically valid Leaf template (which may or may not be fully resolvable or erroring)
public struct LeafAST: Hashable {
public struct LeafAST: Hashable, Sendable {
// MARK: - Public

public func hash(into hasher: inout Hasher) { hasher.combine(name) }
Expand Down
30 changes: 26 additions & 4 deletions Sources/LeafKit/LeafCache/DefaultLeafCache.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
import NIOConcurrencyHelpers
import NIO

public final class DefaultLeafCache: SynchronousLeafCache {
/// `@unchecked Sendable` because uses locks to guarantee Sendability.
public final class DefaultLeafCache: SynchronousLeafCache, @unchecked Sendable {
// MARK: - Public - `LeafCache` Protocol Conformance


var __isEnabled = true
/// Global setting for enabling or disabling the cache
public var isEnabled: Bool = true
public var _isEnabled: Bool {
get {
self.lock.withLock {
self.__isEnabled
}
}
set(newValue) {
self.lock.withLock {
self.__isEnabled = newValue
}
}
}
/// Global setting for enabling or disabling the cache
public var isEnabled: Bool {
get {
self._isEnabled
}
set(newValue) {
self._isEnabled = newValue
}
}
/// Current count of cached documents
Comment on lines 7 to 31
Copy link
Member

Choose a reason for hiding this comment

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

Why two layers of indirection?

Copy link
Author

Choose a reason for hiding this comment

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

i assume compiler will take care of that anyway... two levels to not use self.lock.withLock 2 more times, which is also less code.

public var count: Int { self.lock.withLock { cache.count } }

Expand Down Expand Up @@ -73,7 +95,7 @@ public final class DefaultLeafCache: SynchronousLeafCache {

// MARK: - Internal Only

internal let lock: Lock
internal let lock: NIOLock
internal var cache: [String: LeafAST]

/// Blocking file load behavior
Expand Down
3 changes: 2 additions & 1 deletion Sources/LeafKit/LeafCache/LeafCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import NIO
/// adherents where the cache store itself is not a bottleneck.
///
/// `LeafAST.name` is to be used in all cases as the key for retrieving cached documents.
public protocol LeafCache {
@preconcurrency
public protocol LeafCache: Sendable {
/// Global setting for enabling or disabling the cache
var isEnabled : Bool { get set }
/// Current count of cached documents
Expand Down
80 changes: 41 additions & 39 deletions Sources/LeafKit/LeafConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
/// General configuration of Leaf
/// - Sets the default View directory where templates will be looked for
/// - Guards setting the global tagIndicator (default `#`).
public struct LeafConfiguration {
public struct LeafConfiguration: Sendable {

/// Initialize Leaf with the default tagIndicator `#` and unfound imports throwing an exception
/// - Parameter rootDirectory: Default directory where templates will be found
Expand All @@ -23,9 +23,9 @@ public struct LeafConfiguration {
/// - Parameter tagIndicator: Unique tagIndicator - may only be set once.
/// - Parameter ignoreUnfoundImports: Ignore unfound imports - may only be set once.
public init(rootDirectory: String, tagIndicator: Character, ignoreUnfoundImports: Bool) {
if !Self.started {
if !Self.started.value {
Character.tagIndicator = tagIndicator
Self.started = true
Self.started.value = true
}
self._rootDirectory = rootDirectory
self._ignoreUnfoundImports = ignoreUnfoundImports
Expand All @@ -42,53 +42,53 @@ public struct LeafConfiguration {
}

public static var encoding: String.Encoding {
get { _encoding }
set { if !Self.running { _encoding = newValue } }
get { _encoding.value }
set { if !Self.running { _encoding.value = newValue } }
}

public static var boolFormatter: (Bool) -> String {
get { _boolFormatter }
set { if !Self.running { _boolFormatter = newValue } }
get { _boolFormatter.value }
set { if !Self.running { _boolFormatter.value = newValue } }
}

public static var intFormatter: (Int) -> String {
get { _intFormatter }
set { if !Self.running { _intFormatter = newValue } }
get { _intFormatter.value }
set { if !Self.running { _intFormatter.value = newValue } }
}

public static var doubleFormatter: (Double) -> String {
get { _doubleFormatter }
set { if !Self.running { _doubleFormatter = newValue } }
get { _doubleFormatter.value }
set { if !Self.running { _doubleFormatter.value = newValue } }
}

public static var nilFormatter: () -> String {
get { _nilFormatter }
set { if !Self.running { _nilFormatter = newValue } }
get { _nilFormatter.value }
set { if !Self.running { _nilFormatter.value = newValue } }
}

public static var voidFormatter: () -> String {
get { _voidFormatter }
set { if !Self.running { _voidFormatter = newValue } }
get { _voidFormatter.value }
set { if !Self.running { _voidFormatter.value = newValue } }
}

public static var stringFormatter: (String) -> String {
get { _stringFormatter }
set { if !Self.running { _stringFormatter = newValue } }
get { _stringFormatter.value }
set { if !Self.running { _stringFormatter.value = newValue } }
}

public static var arrayFormatter: ([String]) -> String {
get { _arrayFormatter }
set { if !Self.running { _arrayFormatter = newValue } }
get { _arrayFormatter.value }
set { if !Self.running { _arrayFormatter.value = newValue } }
}

public static var dictFormatter: ([String: String]) -> String {
get { _dictFormatter }
set { if !Self.running { _dictFormatter = newValue } }
get { _dictFormatter.value }
set { if !Self.running { _dictFormatter.value = newValue } }
}

public static var dataFormatter: (Data) -> String? {
get { _dataFormatter }
set { if !Self.running { _dataFormatter = newValue } }
get { _dataFormatter.value }
set { if !Self.running { _dataFormatter.value = newValue } }
}

// MARK: - Internal/Private Only
Expand All @@ -99,27 +99,29 @@ public struct LeafConfiguration {
internal var _ignoreUnfoundImports: Bool {
willSet { assert(!accessed, "Changing property after LeafConfiguration has been read has no effect") }
}
internal static var _encoding: String.Encoding = .utf8
internal static var _boolFormatter: (Bool) -> String = { $0.description }
internal static var _intFormatter: (Int) -> String = { $0.description }
internal static var _doubleFormatter: (Double) -> String = { $0.description }
internal static var _nilFormatter: () -> String = { "" }
internal static var _voidFormatter: () -> String = { "" }
internal static var _stringFormatter: (String) -> String = { $0 }
internal static var _arrayFormatter: ([String]) -> String =

internal static let _encoding = SendableBox<String.Encoding>(.utf8)
internal static let _boolFormatter = SendableBox<(Bool) -> String>({ $0.description })
internal static let _intFormatter = SendableBox<(Int) -> String>({ $0.description })
internal static let _doubleFormatter = SendableBox<(Double) -> String>({ $0.description })
internal static let _nilFormatter = SendableBox<(() -> String)>({ "" })
internal static let _voidFormatter = SendableBox<(() -> String)>({ "" })
internal static let _stringFormatter = SendableBox<((String) -> String)>({ $0 })
internal static let _arrayFormatter = SendableBox<(([String]) -> String)>(
{ "[\($0.map {"\"\($0)\""}.joined(separator: ", "))]" }
internal static var _dictFormatter: ([String: String]) -> String =
)
internal static let _dictFormatter = SendableBox<(([String: String]) -> String)>(
{ "[\($0.map { "\($0): \"\($1)\"" }.joined(separator: ", "))]" }
internal static var _dataFormatter: (Data) -> String? =
{ String(data: $0, encoding: Self._encoding) }

)
internal static let _dataFormatter = SendableBox<((Data) -> String?)>(
{ String(data: $0, encoding: Self._encoding.value) }
)

/// Convenience flag for global write-once
private static var started = false
private static let started = SendableBox(false)
private static var running: Bool {
assert(!Self.started, "LeafKit can only be configured prior to instantiating any LeafRenderer")
return Self.started
assert(!Self.started.value, "LeafKit can only be configured prior to instantiating any LeafRenderer")
return Self.started.value
}

/// Convenience flag for local lock-after-access
Expand Down
9 changes: 5 additions & 4 deletions Sources/LeafKit/LeafData/LeafData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ public struct LeafData: CustomStringConvertible,
ExpressibleByBooleanLiteral,
ExpressibleByArrayLiteral,
ExpressibleByFloatLiteral,
ExpressibleByNilLiteral {

ExpressibleByNilLiteral,
Sendable {

/// The concrete instantiable object types for a `LeafData`
public enum NaturalType: String, CaseIterable, Hashable {
public enum NaturalType: String, CaseIterable, Hashable, Sendable {
case bool
case string
case int
Expand Down Expand Up @@ -250,7 +251,7 @@ public struct LeafData: CustomStringConvertible,

/// Creates a new `LeafData` from `() -> LeafData` if possible or `nil` if not possible.
/// `returns` must specify a `NaturalType` that the function will return
internal static func lazy(_ lambda: @escaping () -> LeafData,
internal static func lazy(_ lambda: @Sendable @escaping () -> LeafData,
returns type: LeafData.NaturalType,
invariant sideEffects: Bool) throws -> LeafData {
LeafData(.lazy(f: lambda, returns: type, invariant: sideEffects))
Expand Down
5 changes: 3 additions & 2 deletions Sources/LeafKit/LeafData/LeafDataStorage.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import NIO
import Foundation

internal indirect enum LeafDataStorage: Equatable, CustomStringConvertible {
internal indirect enum LeafDataStorage: Equatable, CustomStringConvertible, Sendable {
// MARK: - Cases

// Static values
Expand All @@ -20,7 +20,8 @@ internal indirect enum LeafDataStorage: Equatable, CustomStringConvertible {

// Lazy resolvable function
// Must specify return tuple giving (returnType, invariance)
case lazy(f: () -> (LeafData),
@preconcurrency
case lazy(f: @Sendable () -> (LeafData),
returns: LeafData.NaturalType,
invariant: Bool)

Expand Down
4 changes: 2 additions & 2 deletions Sources/LeafKit/LeafError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
///
public struct LeafError: Error {
/// Possible cases of a LeafError.Reason, with applicable stored values where useful for the type
public enum Reason {
public enum Reason: Sendable {
// MARK: Errors related to loading raw templates
/// Attempted to access a template blocked for security reasons
case illegalAccess(String)
Expand Down Expand Up @@ -109,7 +109,7 @@ public struct LeafError: Error {
public struct LexerError: Error {
// MARK: - Public

public enum Reason {
public enum Reason: Sendable {
// MARK: Errors occuring during Lexing
/// A character not usable in parameters is present when Lexer is not expecting it
case invalidParameterToken(Character)
Expand Down
8 changes: 4 additions & 4 deletions Sources/LeafKit/LeafLexer/LeafParameterTypes.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// MARK: - `Parameter` Token Type

/// An associated value enum holding data, objects or values usable as parameters to a `.tag`
public enum Parameter: Equatable, CustomStringConvertible {
public enum Parameter: Equatable, CustomStringConvertible, Sendable {
case stringLiteral(String)
case constant(Constant)
case variable(name: String)
Expand Down Expand Up @@ -44,7 +44,7 @@ public enum Parameter: Equatable, CustomStringConvertible {
/// `Keyword`s are identifiers which take precedence over syntax/variable names - may potentially have
/// representable state themselves as value when used with operators (eg, `true`, `false` when
/// used with logical operators, `nil` when used with equality operators, and so forth)
public enum LeafKeyword: String, Equatable {
public enum LeafKeyword: String, Equatable, Sendable {
// MARK: Public - Cases

// Eval -> Bool / Other
Expand Down Expand Up @@ -79,7 +79,7 @@ extension LeafKeyword {
// MARK: - Operator Symbols

/// Mathematical and Logical operators
public enum LeafOperator: String, Equatable, CustomStringConvertible, CaseIterable {
public enum LeafOperator: String, Equatable, CustomStringConvertible, CaseIterable, Sendable {
// MARK: Public - Cases

// Operator types: Logic Exist. UnPre Scope
Expand Down Expand Up @@ -157,7 +157,7 @@ public enum LeafOperator: String, Equatable, CustomStringConvertible, CaseIterab
}

/// An integer or double constant value parameter (eg `1_000`, `-42.0`)
public enum Constant: CustomStringConvertible, Equatable {
public enum Constant: CustomStringConvertible, Equatable, Sendable {
case int(Int)
case double(Double)

Expand Down
2 changes: 1 addition & 1 deletion Sources/LeafKit/LeafLexer/LeafToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
/// - `.whitespace`: Only generated when not at top-level, and unclear why maintaining it is useful
///

internal enum LeafToken: CustomStringConvertible, Equatable {
internal enum LeafToken: CustomStringConvertible, Equatable, Sendable {
/// Holds a variable-length string of data that will be passed through with no processing
case raw(String)

Expand Down
2 changes: 1 addition & 1 deletion Sources/LeafKit/LeafParser/LeafParameter.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import NIO

public indirect enum ParameterDeclaration: CustomStringConvertible {
public indirect enum ParameterDeclaration: CustomStringConvertible, Sendable {
case parameter(Parameter)
case expression([ParameterDeclaration])
case tag(Syntax.CustomTagDeclaration)
Expand Down
14 changes: 9 additions & 5 deletions Sources/LeafKit/LeafRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import NIO
///
/// Additional instances of LeafRenderer can then be created using these shared modules to allow
/// concurrent rendering, potentially with unique per-instance scoped data via `userInfo`.
public final class LeafRenderer {
public final class LeafRenderer: Sendable {
// MARK: - Public Only

/// An initialized `LeafConfiguration` specificying default directory and tagIndicator
Expand All @@ -25,9 +25,12 @@ public final class LeafRenderer {
public let sources: LeafSources
/// The NIO `EventLoop` on which this instance of `LeafRenderer` will operate
public let eventLoop: EventLoop
let _userInfo: SendableBox<[AnyHashable: Any]>
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't do what you think it does. The only way we can make Any Sendable is by tying this to an event loop

Copy link
Author

Choose a reason for hiding this comment

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

Makes sense ... even with a Value: Sendable constraint it is still showing warnings.
Not sure what exactly to do though.
We need to change Any -> any Sendable as well as somehow conforming AnyHashable to Sendable.

Copy link
Author

Choose a reason for hiding this comment

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

Though not sure what an event loop does that the locking mechanism can't ... I'm tempted to ask in the OpenSource slack.

Copy link
Author

Choose a reason for hiding this comment

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

The documentation of NIOLoopBound does mention why...

/// Any custom instance data to use (eg, in Vapor, the `Application` and/or `Request` data)
public let userInfo: [AnyHashable: Any]

public var userInfo: [AnyHashable: Any] {
_userInfo.value
Copy link
Author

Choose a reason for hiding this comment

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

Will this crash on access by users on another eventLoop?

}

/// Initial configuration of LeafRenderer.
public init(
configuration: LeafConfiguration,
Expand All @@ -42,7 +45,7 @@ public final class LeafRenderer {
self.cache = cache
self.sources = sources
self.eventLoop = eventLoop
self.userInfo = userInfo
self._userInfo = .init(userInfo)
}

/// The public interface to `LeafRenderer`
Expand Down Expand Up @@ -164,6 +167,7 @@ public final class LeafRenderer {

let fetchRequests = ast.unresolvedRefs.map { self.fetch(template: $0, chain: chain) }

let constantChain = chain
let results = EventLoopFuture.whenAllComplete(fetchRequests, on: self.eventLoop)
return results.flatMap { results in
let results = results
Expand All @@ -182,7 +186,7 @@ public final class LeafRenderer {
// Check new AST's unresolved refs to see if extension introduced new refs
if !new.unresolvedRefs.subtracting(ast.unresolvedRefs).isEmpty {
// AST has new references - try to resolve again recursively
return self.resolve(ast: new, chain: chain)
return self.resolve(ast: new, chain: constantChain)
} else {
// Cache extended AST & return - AST is either flat or unresolvable
return self.cache.insert(new, on: self.eventLoop, replace: true)
Expand Down
Loading