diff --git a/Package.swift b/Package.swift index a903d5e9..c6dd68a4 100644 --- a/Package.swift +++ b/Package.swift @@ -4,25 +4,20 @@ import PackageDescription let package = Package( name: "leaf-kit", platforms: [ - .macOS(.v10_15) + .macOS(.v10_15) ], products: [ .library(name: "LeafKit", targets: ["LeafKit"]), - .library(name: "XCTLeafKit", targets: ["XCTLeafKit"]) ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.20.2"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.2.0"), ], targets: [ .target(name: "LeafKit", dependencies: [ .product(name: "NIO", package: "swift-nio"), - .product(name: "NIOFoundationCompat", package: "swift-nio") - ]), - .target(name: "XCTLeafKit", dependencies: [ - .target(name: "LeafKit") ]), .testTarget(name: "LeafKitTests", dependencies: [ - .target(name: "XCTLeafKit") - ]) + .target(name: "LeafKit"), + ]), ] ) diff --git a/Sources/LeafKit/Deprecated.swift b/Sources/LeafKit/Deprecated.swift new file mode 100644 index 00000000..178f65b6 --- /dev/null +++ b/Sources/LeafKit/Deprecated.swift @@ -0,0 +1,57 @@ +// MARK: Subject to change prior to 1.0.0 release +// MARK: - + +extension LeafRenderer { + /// Deprecated in Leaf-Kit 1.0.0rc-1.2 + @available(*, deprecated, message: "Use files instead of fileio") + public var fileio: NonBlockingFileIO { + guard let source = self.sources.sources["default"], + let nio = source as? NIOLeafFiles else { + fatalError("Unexpected non-NIO LeafFiles.") + } + return nio.fileio + } + + /// Deprecated in Leaf-Kit 1.0.0rc-1.2 + @available(*, deprecated, message: "Use files instead of fileio") + public convenience init( + configuration: LeafConfiguration, + cache: LeafCache = DefaultLeafCache(), + fileio: NonBlockingFileIO, + eventLoop: EventLoop + ) { + let sources = LeafSources() + try! sources.register(using: NIOLeafFiles(fileio: fileio)) + + self.init( + configuration: configuration, + cache: cache, + sources: sources, + eventLoop: eventLoop + ) + } +} + +extension LeafSource { + /// Default implementation for non-adhering protocol implementations mimicing older LeafRenderer expansion + /// This wrapper will be removed in a future release. + @available(*, deprecated, message: "Update to adhere to `file(template, escape, eventLoop)`") + func file(template: String, escape: Bool, on eventLoop: EventLoop) throws -> EventLoopFuture { + var path = template + if path.split(separator: "/").last?.split(separator: ".").count ?? 1 < 2, + !path.hasSuffix(".leaf") { path += ".leaf" } + if !path.hasPrefix("/") { path = "/" + path } + return try self.file(path: path, on: eventLoop) + } + + /// Deprecated in Leaf-Kit 1.0.0rc-1.11 + /// Default implementation for newer adherants to allow older adherents to be called until upgraded + @available(*, deprecated, message: "This default implementation should never be called") + public func file(path: String, on eventLoop: EventLoop) throws -> EventLoopFuture { + fatalError("This default implementation should never be called") + } +} + +/// Deprecated in Leaf-Kit 1.0.0rc-1.11 +@available(*, deprecated, renamed: "LeafSource") +typealias LeafFiles = LeafSource diff --git a/Sources/LeafKit/Exports+Helpers.swift b/Sources/LeafKit/Exports+Helpers.swift deleted file mode 100644 index f5d3bc7c..00000000 --- a/Sources/LeafKit/Exports+Helpers.swift +++ /dev/null @@ -1,30 +0,0 @@ -@_exported import NIO - -// MARK: Public Type Shorthands - -// Can't alias until deprecated version totally removed -public typealias LeafContext = LeafRenderer.Context -public typealias LeafOptions = LeafRenderer.Options -public typealias LeafOption = LeafRenderer.Option - -public typealias ParseSignatures = [String: [LeafParseParameter]] - -// MARK: - Static Conveniences - -/// Public helper identities -public extension Character { - static var tagIndicator: Self { LKConf.tagIndicator } - static var octothorpe: Self { "#".first! } -} - -public extension String { - /// Whether the string is valid as an identifier (variable part or function name) in LeafKit - var isValidLeafIdentifier: Bool { - !isEmpty && !isLeafKeyword - && first?.canStartIdentifier ?? false - && allSatisfy({$0.isValidInIdentifier}) - } - - /// Whether the string is a (protected) Leaf keyword - var isLeafKeyword: Bool { LeafKeyword(rawValue: self) != nil } -} diff --git a/Sources/LeafKit/Exports.swift b/Sources/LeafKit/Exports.swift new file mode 100644 index 00000000..bdee096f --- /dev/null +++ b/Sources/LeafKit/Exports.swift @@ -0,0 +1,119 @@ +// MARK: Subject to change prior to 1.0.0 release +// MARK: - + +@_exported import NIO + +/// Various helper identities for convenience +extension Character { + // MARK: - Leaf-Kit specific static identities (Public) + + /// Global setting of `tagIndicator` for Leaf-Kit - by default, `#` + public internal(set) static var tagIndicator: Character = .octothorpe + + // MARK: - LeafToken specific identities (Internal) + + var isValidInTagName: Bool { + return self.isLowercaseLetter + || self.isUppercaseLetter + } + + var isValidInParameter: Bool { + return self.isValidInTagName + || self.isValidOperator + || self.isValidInNumeric + } + + var canStartNumeric: Bool { + return (.zero ... .nine) ~= self + } + + var isValidInNumeric: Bool { + return self.canStartNumeric + || self == .underscore + || self == .binaryNotation + || self == .octalNotation + || self == .hexNotation + || self.isHexadecimal + || self == .period + } + + var isValidOperator: Bool { + switch self { + case .plus, + .minus, + .star, + .forwardSlash, + .equals, + .exclamation, + .lessThan, + .greaterThan, + .ampersand, + .vertical: return true + default: return false + } + } + + // MARK: - General group-membership identities (Internal) + + var isHexadecimal: Bool { + return (.zero ... .nine).contains(self) + || (.A ... .F).contains(self.uppercased().first!) + || self == .hexNotation + } + + var isOctal: Bool { + return (.zero ... .seven).contains(self) + || self == .octalNotation + } + + var isBinary: Bool { + return (.zero ... .one).contains(self) + || self == .binaryNotation + } + + var isUppercaseLetter: Bool { + return (.A ... .Z).contains(self) + } + + var isLowercaseLetter: Bool { + return (.a ... .z).contains(self) + } + + // MARK: - General static identities (Internal) + + static let newLine = "\n".first! + static let quote = "\"".first! + static let octothorpe = "#".first! + static let leftParenthesis = "(".first! + static let backSlash = "\\".first! + static let rightParenthesis = ")".first! + static let comma = ",".first! + static let space = " ".first! + static let colon = ":".first! + static let period = ".".first! + static let A = "A".first! + static let F = "F".first! + static let Z = "Z".first! + static let a = "a".first! + static let z = "z".first! + + static let zero = "0".first! + static let one = "1".first! + static let seven = "7".first! + static let nine = "9".first! + static let binaryNotation = "b".first! + static let octalNotation = "o".first! + static let hexNotation = "x".first! + + static let plus = "+".first! + static let minus = "-".first! + static let star = "*".first! + static let forwardSlash = "/".first! + static let equals = "=".first! + static let exclamation = "!".first! + static let lessThan = "<".first! + static let greaterThan = ">".first! + static let ampersand = "&".first! + static let vertical = "|".first! + static let underscore = "_".first! +} diff --git a/Sources/LeafKit/LKHelpers/RWLock.swift b/Sources/LeafKit/LKHelpers/RWLock.swift deleted file mode 100644 index 32cbfaed..00000000 --- a/Sources/LeafKit/LKHelpers/RWLock.swift +++ /dev/null @@ -1,85 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// Derived from SwiftNIO open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftNIO project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) -import Darwin -#else -import Glibc -#endif - -/// A threading lock based on `libpthread` instead of `libdispatch`. -/// -/// This object provides a lock on top of a single `pthread_mutex_t`. This kind -/// of lock is safe to use with `libpthread`-based threading models, such as the -/// one used by NIO. -internal final class RWLock { - private let rwlock: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) - - /// Create a new lock. - init() { - var attr = pthread_rwlockattr_t() - pthread_rwlockattr_init(&attr) - let err = pthread_rwlock_init(self.rwlock, &attr) - precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") - } - - deinit { - let err = pthread_rwlock_destroy(self.rwlock) - precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") - rwlock.deallocate() - } - - func lock(forWrite: Bool = false) { - let err = forWrite ? pthread_rwlock_wrlock(self.rwlock) - : pthread_rwlock_rdlock(self.rwlock) - precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") - } - - func unlock() { - let err = pthread_rwlock_unlock(self.rwlock) - precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") - } -} - -internal extension RWLock { - /// Acquire the lock for the duration of the given block. - /// - /// This convenience method should be preferred to `lock` and `unlock` in - /// most situations, as it ensures that the lock will be released regardless - /// of how `body` exits. - /// - /// - Parameter body: The block to execute while holding the lock. - /// - Returns: The value returned by the block. - @inlinable - func readWithLock(_ body: () throws -> T) rethrows -> T { - lock(forWrite: false) - defer { unlock() } - return try body() - } - - @inlinable - func writeWithLock(_ body: () throws -> T) rethrows -> T { - lock(forWrite: true) - defer { unlock() } - return try body() - } - - @inlinable - func writeWithLock(_ body: () throws -> Void) rethrows -> Void { - lock(forWrite: true) - defer { unlock() } - try body() - } -} diff --git a/Sources/LeafKit/LKHelpers/SwiftExtensions.swift b/Sources/LeafKit/LKHelpers/SwiftExtensions.swift deleted file mode 100644 index ae0cfde3..00000000 --- a/Sources/LeafKit/LKHelpers/SwiftExtensions.swift +++ /dev/null @@ -1,90 +0,0 @@ -import Foundation - -/// Internal conveniences on native Swift types/protocols - -internal extension Comparable { - /// Conditional shorthand for lhs = max(lhs, rhs) - mutating func maxAssign(_ rhs: Self) { self = max(self, rhs) } -} - -internal extension Double { - /// Convenience for formatting Double to a s/ms/µs String - func formatSeconds(places: Int = 2) -> String { - let abs = self.magnitude - if abs * 10 > 1 { return String(format: "%.\(places)f%", abs) + " s"} - if abs * 1_000 > 1 { return String(format: "%.\(places)f%", abs * 1_000) + " ms" } - return String(format: "%.\(places)f%", abs * 1_000_000) + " µs" - } -} - -internal extension Int { - /// Convenience for formatting Ints to a B/kB/mB String - func formatBytes(places: Int = 2) -> String { "\(signum() == -1 ? "-" : "")\(magnitude.formatBytes(places: places))" } -} - -internal extension UnsignedInteger { - /// Convenience for formatting UInts to a B/kB/mB String - func formatBytes(places: Int = 2) -> String { - if self > 1024 * 1024 * 512 { return String(format: "%.\(places)fGB", Double(self)/1024.0/1024.0/1024.0) } - if self > 1024 * 512 { return String(format: "%.\(places)fMB", Double(self)/1024.0/1024.0) } - if self > 512 { return String(format: "%.\(places)fKB", Double(self)/1024.0) } - return "\(self)B" - } -} - -internal extension CaseIterable { - static var terse: String { "[\(Self.allCases.map {"\($0)"}.joined(separator: ", "))]" } -} - -extension Array: LKPrintable where Element == LeafCallParameter { - var description: String { short } - var short: String { "(\(map {$0.short}.joined(separator: ", ")))" } -} - -/// Tired of writing `distance(to: ...` -infix operator +-> : AdditionPrecedence - -internal extension Date { - /// Tired of writing `distance(to: ...` - static func +->(_ from: Self, _ to: Self) -> Double { - from.timeIntervalSinceReferenceDate.distance(to: to.timeIntervalSinceReferenceDate) - } -} - -internal extension String { - /// Validate that the String chunk is processable by Leaf for a given LeafEntities config. Can be used on chunks. - /// - /// If return value is .success, holds true if the file is possibly Leaf parseable (has valid tags) or false if - /// no tag marks of any kind exist in the string. - /// - /// Failure return implies the file *IS* Leaf parseable but definitely has invalid tag entities. If the string - /// value is non-empty, the string ends in an open tag identifier. - func isLeafProcessable(_ entities: LeafEntities) -> Result { - if isEmpty { return .success(false) } - - var i = startIndex - var lastTagMark: String.Index? = nil - var last: String.Index { index(before: i) } - var peek: String.Element { self[i] } - - func advance() { i = index(after: i) } - var chunk: String { String(self[lastTagMark!...indices.last!]) } - - while i != endIndex { - defer { if i != endIndex { advance() } } - if peek != .tagIndicator { continue } - if i != indices.first, self[last] == .backSlash { continue } - lastTagMark = i - if i == indices.last { return .failure(chunk) } - advance() - if peek == .leftParenthesis { return .success(true) } - var possibleID = "" - while i != endIndex, peek.isValidInIdentifier { possibleID.append(peek); advance() } - if i == endIndex { return .failure(chunk) } - if peek == .leftParenthesis { - if !entities.openers.contains(possibleID) { return .failure("") } } - else { if !entities.closers.contains(possibleID) { return .failure("") } } - } - return .success(lastTagMark != nil) - } -} diff --git a/Sources/LeafKit/LKHelpers/Typealiases.swift b/Sources/LeafKit/LKHelpers/Typealiases.swift deleted file mode 100644 index f8228991..00000000 --- a/Sources/LeafKit/LKHelpers/Typealiases.swift +++ /dev/null @@ -1,13 +0,0 @@ -internal typealias LKConf = LeafConfiguration -internal typealias ELF = EventLoopFuture -/// `Leaf(Kit)Data` -internal typealias LKData = LeafData -/// `Leaf(Kit)DataType` -internal typealias LKDType = LeafDataType -/// Set of LKDTypes -internal typealias LKDTypeSet = Set -/// `[LKParameter]` - no special bounds enforced, used to pass to `LKTuple` which validates -internal typealias LKParams = [LKParameter] - -internal typealias LKRContext = LeafRenderer.Context -internal typealias LKROption = LeafRenderer.Option diff --git a/Sources/LeafKit/LeafAST.swift b/Sources/LeafKit/LeafAST.swift new file mode 100644 index 00000000..7209141d --- /dev/null +++ b/Sources/LeafKit/LeafAST.swift @@ -0,0 +1,96 @@ +// MARK: Subject to change prior to 1.0.0 release +// MARK: - + + +/// `LeafAST` represents a "compiled," grammatically valid Leaf template (which may or may not be fully resolvable or erroring) +public struct LeafAST: Hashable { + // MARK: - Public + + public func hash(into hasher: inout Hasher) { hasher.combine(name) } + public static func == (lhs: LeafAST, rhs: LeafAST) -> Bool { lhs.name == rhs.name } + + // MARK: - Internal/Private Only + let name: String + + init(name: String, ast: [Syntax]) { + self.name = name + self.ast = ast + self.rawAST = nil + self.flat = false + + updateRefs([:]) + } + + init(from: LeafAST, referencing externals: [String: LeafAST]) { + self.name = from.name + self.ast = from.ast + self.rawAST = from.rawAST + self.externalRefs = from.externalRefs + self.unresolvedRefs = from.unresolvedRefs + self.flat = from.flat + + updateRefs(externals) + } + + internal private(set) var ast: [Syntax] + internal private(set) var externalRefs = Set() + internal private(set) var unresolvedRefs = Set() + internal private(set) var flat: Bool + + // MARK: - Private Only + + private var rawAST: [Syntax]? + + mutating private func updateRefs(_ externals: [String: LeafAST]) { + var firstRun = false + if rawAST == nil, flat == false { rawAST = ast; firstRun = true } + unresolvedRefs.removeAll() + var pos = ast.startIndex + + // inline provided externals + while pos < ast.endIndex { + // get desired externals for this Syntax - if none, continue + let wantedExts = ast[pos].externals() + if wantedExts.isEmpty { + pos = ast.index(after: pos) + continue + } + // see if we can provide any of them - if not, continue + let providedExts = externals.filter { wantedExts.contains($0.key) } + if providedExts.isEmpty { + unresolvedRefs.formUnion(wantedExts) + pos = ast.index(after: pos) + continue + } + + // replace the original Syntax with the results of inlining, potentially 1...n + let replacementSyntax = ast[pos].inlineRefs(providedExts, [:]) + ast.replaceSubrange(pos...pos, with: replacementSyntax) + // any returned new inlined syntaxes can't be further resolved at this point + // but we need to add their unresolvable references to the global set + var offset = replacementSyntax.startIndex + while offset < replacementSyntax.endIndex { + unresolvedRefs.formUnion(ast[pos].externals()) + offset = replacementSyntax.index(after: offset) + pos = ast.index(after: pos) + } + } + + // compress raws + pos = ast.startIndex + while pos < ast.index(before: ast.endIndex) { + if case .raw(var syntax) = ast[pos] { + if case .raw(var add) = ast[ast.index(after: pos)] { + var buffer = ByteBufferAllocator().buffer(capacity: syntax.readableBytes + add.readableBytes) + buffer.writeBuffer(&syntax) + buffer.writeBuffer(&add) + ast[pos] = .raw(buffer) + ast.remove(at: ast.index(after: pos) ) + } else { pos = ast.index(after: pos) } + } else { pos = ast.index(after: pos) } + } + + flat = unresolvedRefs.isEmpty ? true : false + if firstRun && flat { rawAST = nil } + } +} diff --git a/Sources/LeafKit/LeafCache/DefaultLeafCache.swift b/Sources/LeafKit/LeafCache/DefaultLeafCache.swift index 504a60df..4f2e7d0f 100644 --- a/Sources/LeafKit/LeafCache/DefaultLeafCache.swift +++ b/Sources/LeafKit/LeafCache/DefaultLeafCache.swift @@ -1,129 +1,91 @@ +// MARK: Subject to change prior to 1.0.0 release +// MARK: - + + import NIOConcurrencyHelpers -/// The default implementation of `LeafCache` -public final class DefaultLeafCache { +public final class DefaultLeafCache: SynchronousLeafCache { + // MARK: - Public - `LeafCache` Protocol Conformance + + /// Global setting for enabling or disabling the cache + public var isEnabled: Bool = true + /// Current count of cached documents + public var count: Int { self.lock.withLock { cache.count } } + /// Initializer public init() { - self.locks = (.init(), .init()) + self.lock = .init() self.cache = [:] - self.touches = [:] } - - // MARK: - Stored Properties - Private Only - private let locks: (cache: RWLock, touch: RWLock) - /// NOTE: internal read-only purely for test access validation - not assured - private(set) var cache: [LeafAST.Key: LeafAST] - private var touches: [LeafAST.Key: LeafAST.Touch] -} - -// MARK: - Public - LeafCache -extension DefaultLeafCache: LeafCache { - public var count: Int { locks.cache.readWithLock { cache.count } } - - public var isEmpty: Bool { locks.cache.readWithLock { cache.isEmpty } } - - public var keys: Set { .init(locks.cache.readWithLock { cache.keys }) } /// - Parameters: /// - document: The `LeafAST` to store /// - loop: `EventLoop` to return futures on /// - replace: If a document with the same name is already cached, whether to replace or not. /// - Returns: The document provided as an identity return - /// - /// Use `LeafAST.key` as the - public func insert(_ document: LeafAST, - on loop: EventLoop, - replace: Bool = false) -> EventLoopFuture { - switch insert(document, replace: replace) { - case .success(let ast): return succeed(ast, on: loop) - case .failure(let err): return fail(err, on: loop) + public func insert( + _ document: LeafAST, + on loop: EventLoop, + replace: Bool = false + ) -> EventLoopFuture { + // future fails if caching is enabled + guard isEnabled else { return loop.makeSucceededFuture(document) } + + self.lock.lock() + defer { self.lock.unlock() } + // return an error if replace is false and the document name is already in cache + switch (self.cache.keys.contains(document.name),replace) { + case (true, false): return loop.makeFailedFuture(LeafError(.keyExists(document.name))) + default: self.cache[document.name] = document } + return loop.makeSucceededFuture(document) } - + /// - Parameters: - /// - key: Name of the `LeafAST` to try to return + /// - documentName: Name of the `LeafAST` to try to return /// - loop: `EventLoop` to return futures on /// - Returns: `EventLoopFuture` holding the `LeafAST` or nil if no matching result - public func retrieve(_ key: LeafAST.Key, - on loop: EventLoop) -> EventLoopFuture { - succeed(retrieve(key), on: loop) + public func retrieve( + documentName: String, + on loop: EventLoop + ) -> EventLoopFuture { + guard isEnabled == true else { return loop.makeSucceededFuture(nil) } + self.lock.lock() + defer { self.lock.unlock() } + return loop.makeSucceededFuture(self.cache[documentName]) } /// - Parameters: - /// - key: Name of the `LeafAST` to try to purge from the cache + /// - documentName: Name of the `LeafAST` to try to purge from the cache /// - loop: `EventLoop` to return futures on /// - Returns: `EventLoopFuture` - If no document exists, returns nil. If removed, /// returns true. If cache can't remove because of dependencies (not yet possible), returns false. - public func remove(_ key: LeafAST.Key, - on loop: EventLoop) -> EventLoopFuture { - return succeed(remove(key), on: loop) } + public func remove( + _ documentName: String, + on loop: EventLoop + ) -> EventLoopFuture { + guard isEnabled == true else { return loop.makeFailedFuture(LeafError(.cachingDisabled)) } - public func touch(_ key: LeafAST.Key, - with values: LeafAST.Touch) { - locks.touch.writeWithLock { touches[key]?.aggregate(values: values) } + self.lock.lock() + defer { self.lock.unlock() } + + guard self.cache[documentName] != nil else { return loop.makeSucceededFuture(nil) } + self.cache[documentName] = nil + return loop.makeSucceededFuture(true) } - public func info(for key: LeafAST.Key, - on loop: EventLoop) -> EventLoopFuture { - succeed(info(for: key), on: loop) - } + // MARK: - Internal Only - public func dropAll() { - locks.cache.writeWithLock { - locks.touch.writeWithLock { - cache.removeAll() - touches.removeAll() - } - } - } -} - -// MARK: - Internal - LKSynchronousCache -extension DefaultLeafCache: LKSynchronousCache { - /// Blocking file load behavior - func insert(_ document: LeafAST, replace: Bool) -> Result { - /// Blind failure if caching is disabled - var e: Bool = false - locks.cache.writeWithLock { - if replace || !cache.keys.contains(document.key) { - cache[document.key] = document - locks.touch.writeWithLock { touches[document.key] = .empty } - } else { e = true } - } - guard !e else { return .failure(err(.keyExists(document.name))) } - return .success(document) - } - - /// Blocking file load behavior - func retrieve(_ key: LeafAST.Key) -> LeafAST? { - return locks.cache.readWithLock { - guard cache.keys.contains(key) else { return nil } - locks.touch.writeWithLock { - if touches[key]!.count >= 128, - let touch = touches.updateValue(.empty, forKey: key), - touch != .empty { - cache[key]!.touch(values: touch) } - } - return cache[key] - } - } - - /// Blocking file load behavior - func remove(_ key: LeafAST.Key) -> Bool? { - if locks.touch.writeWithLock({ touches.removeValue(forKey: key) == nil }) { return nil } - locks.cache.writeWithLock { _ = cache.removeValue(forKey: key) } - return true - } + internal let lock: Lock + internal var cache: [String: LeafAST] - func info(for key: LeafAST.Key) -> LeafAST.Info? { - locks.cache.readWithLock { - guard cache.keys.contains(key) else { return nil } - locks.touch.writeWithLock { - if let touch = touches.updateValue(.empty, forKey: key), - touch != .empty { - cache[key]!.touch(values: touch) } - } - return cache[key]!.info - } + /// Blocking file load behavior + internal func retrieve(documentName: String) throws -> LeafAST? { + guard isEnabled == true else { throw LeafError(.cachingDisabled) } + self.lock.lock() + defer { self.lock.unlock() } + let result = self.cache[documentName] + guard result != nil else { throw LeafError(.noValueForKey(documentName)) } + return result } } diff --git a/Sources/LeafKit/LeafCache/LKSynchronousCache.swift b/Sources/LeafKit/LeafCache/LKSynchronousCache.swift deleted file mode 100644 index c580c4ee..00000000 --- a/Sources/LeafKit/LeafCache/LKSynchronousCache.swift +++ /dev/null @@ -1,22 +0,0 @@ -/// A `LeafCache` that provides certain blocking methods for non-future access to the cache -/// -/// Adherents *MUST* be thread-safe and *SHOULD NOT* be blocking simply to avoid futures - -/// only adhere to this protocol if using futures is needless overhead. Currently restricted to LeafKit internally. -internal protocol LKSynchronousCache: LeafCache { - /// - Parameters: - /// - document: The `LeafAST` to store - /// - replace: If a document with the same name is already cached, whether to replace or not - /// - Returns: The document provided as an identity return when success, or a failure error - func insert(_ document: LeafAST, replace: Bool) -> Result - - /// - Parameter key: Name of the `LeafAST` to try to return - /// - Returns: The requested `LeafAST` or nil if not found - func retrieve(_ key: LeafAST.Key) -> LeafAST? - - /// - Parameter key: Name of the `LeafAST` to try to purge from the cache - /// - Returns: `Bool?` If removed, returns true. If cache can't remove because of dependencies - /// (not yet possible), returns false. Nil if no such cached key exists. - func remove(_ key: LeafAST.Key) -> Bool? - - func info(for key: LeafAST.Key) -> LeafAST.Info? -} diff --git a/Sources/LeafKit/LeafCache/LeafCache.swift b/Sources/LeafKit/LeafCache/LeafCache.swift index e5cb8a7d..e84aee03 100644 --- a/Sources/LeafKit/LeafCache/LeafCache.swift +++ b/Sources/LeafKit/LeafCache/LeafCache.swift @@ -1,3 +1,6 @@ +// MARK: Subject to change prior to 1.0.0 release +// MARK: - + /// `LeafCache` provides blind storage for compiled `LeafAST` objects. /// /// The stored `LeafAST`s may or may not be fully renderable templates, and generally speaking no @@ -7,63 +10,72 @@ /// return values. For performance, an adherent may optionally provide additional, corresponding interfaces /// where returns are direct values and not future-based by adhering to `SynchronousLeafCache` and /// providing applicable option flags indicating which methods may be used. This should only used for -/// adherents where the cache store itself is not a bottleneck. *NOTE* `SynchronousLeafCache` is -/// currently internal-only to LeafKit. +/// adherents where the cache store itself is not a bottleneck. /// -/// `LeafAST.key: LeafAST.Key` is to be used in all cases as the key for storing and retrieving cached documents. +/// `LeafAST.name` is to be used in all cases as the key for retrieving cached documents. public protocol LeafCache { + /// Global setting for enabling or disabling the cache + var isEnabled : Bool { get set } /// Current count of cached documents var count: Int { get } - /// If cache is empty - var isEmpty: Bool { get } - /// Keys for all currently cached ASTs - var keys: Set { get } - + /// - Parameters: /// - document: The `LeafAST` to store /// - loop: `EventLoop` to return futures on /// - replace: If a document with the same name is already cached, whether to replace or not. /// - Returns: The document provided as an identity return (or a failed future if it can't be inserted) - func insert(_ document: LeafAST, - on loop: EventLoop, - replace: Bool) -> EventLoopFuture - + func insert( + _ document: LeafAST, + on loop: EventLoop, + replace: Bool + ) -> EventLoopFuture + /// - Parameters: - /// - key: `LeafAST.key` to try to return + /// - documentName: Name of the `LeafAST` to try to return /// - loop: `EventLoop` to return futures on /// - Returns: `EventLoopFuture` holding the `LeafAST` or nil if no matching result - func retrieve(_ key: LeafAST.Key, - on loop: EventLoop) -> EventLoopFuture + func retrieve( + documentName: String, + on loop: EventLoop + ) -> EventLoopFuture /// - Parameters: - /// - key: `LeafAST.key` to try to purge from the cache + /// - documentName: Name of the `LeafAST` to try to purge from the cache /// - loop: `EventLoop` to return futures on /// - Returns: `EventLoopFuture` - If no document exists, returns nil. If removed, /// returns true. If cache can't remove because of dependencies (not yet possible), returns false. - func remove(_ key: LeafAST.Key, - on loop: EventLoop) -> EventLoopFuture - - /// Retrieve info for AST requested, if it's cached - func info(for key: LeafAST.Key, - on loop: EventLoop) -> EventLoopFuture - - /// Touch the stored AST for `key` with the provided `LeafAST.Touch` object via - /// `LeafAST.touch(values: LeafAST.Touch)`, if document exists - /// + func remove( + _ documentName: String, + on loop: EventLoop + ) -> EventLoopFuture +} + +/// A `LeafCache` that provides certain blocking methods for non-future access to the cache +/// +/// Adherents *MUST* be thread-safe and *SHOULD NOT* be blocking simply to avoid futures - +/// only adhere to this protocol if using futures is needless overhead +internal protocol SynchronousLeafCache: LeafCache { /// - Parameters: - /// - key: `LeafAST.key` of the stored AST to touch - /// - value: `LeafAST.Touch` to provide to the AST via `LeafAST.touch(value)` - /// - /// If document doesn't exist, can be ignored; adherent may queue touches and aggregate them via - /// `a.aggregate(b)`, and only touch when document or info is requested. As such, no event loop - /// is provided - method should still not block. - func touch(_ key: LeafAST.Key, - with value: LeafAST.Touch) + /// - document: The `LeafAST` to store + /// - replace: If a document with the same name is already cached, whether to replace or not + /// - Returns: The document provided as an identity return, or nil if it can't guarantee completion rapidly + /// - Throws: `LeafError` .keyExists if replace is false and document already exists + func insert(_ document: LeafAST, replace: Bool) throws -> LeafAST? + + /// - Parameter documentName: Name of the `LeafAST` to try to return + /// - Returns: The requested `LeafAST` or nil if it can't guarantee completion rapidly + /// - Throws: `LeafError` .noValueForKey if no such document is cached + func retrieve(documentName: String) throws -> LeafAST? - /// Drop the cache contents - func dropAll() + /// - Parameter documentName: Name of the `LeafAST` to try to purge from the cache + /// - Returns: `Bool?` If removed, returns true. If cache can't remove because of dependencies + /// (not yet possible), returns false. Nil if it can't guarantee completion rapidly. + /// - Throws: `LeafError` .noValueForKey if no such document is cached + func remove(documentName: String) throws -> Bool? } -public extension LeafCache { - var isEmpty: Bool { count == 0 } +internal extension SynchronousLeafCache { + func insert(_ document: LeafAST, replace: Bool) throws -> LeafAST? { nil } + func retrieve(documentName: String) throws -> LeafAST? { nil } + func remove(documentName: String) throws -> Bool? { nil } } diff --git a/Sources/LeafKit/LeafCache/LeafCacheBehavior.swift b/Sources/LeafKit/LeafCache/LeafCacheBehavior.swift deleted file mode 100644 index 486d68b7..00000000 --- a/Sources/LeafKit/LeafCache/LeafCacheBehavior.swift +++ /dev/null @@ -1,34 +0,0 @@ -/// Behaviors for how render calls will use the configured `LeafCache` for compiled templates -public struct LeafCacheBehavior: OptionSet, Hashable { - public private(set) var rawValue: UInt8 - - public init(rawValue: RawValue) { self.rawValue = rawValue } - - /// - Prefer reading cached (compiled) templates over checking source - /// - Always store compiled templates and/or further-resovled templates - /// - Cache `raw` inlines up to the configured limit size - public static let `default`: Self = [ - .read, .store, .embedRawInlines, .limitRawInlines, .autoUpdate - ] - - /// Avoid using caching entirely - public static let bypass: Self = [] - - /// Never read cached template, but cache compiled template. - /// Disregards `raw` inline configuration as if it's followed by a subsequent update, there's no point - /// and if it's followed by a default behavior, embedding raws will happen then. - public static let update: Self = [.store] - - /// Whether to prefer reading an available cached version over checking `LeafSources` - static let read: Self = .init(rawValue: 1 << 0) - /// Whether to store a valid compiled template, when parsing has occurred - static let store: Self = .init(rawValue: 1 << 1) - /// Embed `#inline(...)` in cached ASTs - // static let embedLeafInlines: Self = .init(rawValue: 1 << 2) - /// Embed `#inline(..., as: raw)` in cached ASTs - static let embedRawInlines: Self = .init(rawValue: 1 << 3) - /// Limit the filesize of raws to associated limit, if `cacheRawInlines` is set. Controls nothing by itself. - static let limitRawInlines: Self = .init(rawValue: 1 << 4) - - static let autoUpdate: Self = .init(rawValue: 1 << 5) -} diff --git a/Sources/LeafKit/LeafConfiguration.swift b/Sources/LeafKit/LeafConfiguration.swift index e44f6830..3df901e4 100644 --- a/Sources/LeafKit/LeafConfiguration.swift +++ b/Sources/LeafKit/LeafConfiguration.swift @@ -3,72 +3,110 @@ import Foundation -/// `LeafConfiguration` provides global storage of properites that must be consistent across -/// `LeafKit` while running. Alter the global configuration of LeafKit by setting the static properties -/// of the structure prior to calling `LeafRenderer.render()`; any changes subsequently will be ignored. +/// General configuration of Leaf +/// - Sets the default View directory where templates will be looked for +/// - Guards setting the global tagIndicator (default `#`). public struct LeafConfiguration { - // MARK: - Global-Only Options - /// The character used to signal tag processing - @LeafRuntimeGuard public static var tagIndicator: Character = .octothorpe - - /// Entities (functions, blocks, raw blocks, types) the LeafKit engine recognizes - @LeafRuntimeGuard public static var entities: LeafEntities = .leaf4Core - - // MARK: - State - - /// Convenience to check state of LeafKit - public static var isRunning: Bool { started } - - // MARK: - Internal Only - /// Convenience for getting running state of LeafKit that will assert with a fault message for soft-failing things - static func running(fault message: String) -> Bool { - assert(!started, "\(message) after LeafRenderer has instantiated") - return started + /// Initialize Leaf with the default tagIndicator `#` + /// - Parameter rootDirectory: Default directory where templates will be found + public init(rootDirectory: String) { + self.init(rootDirectory: rootDirectory, tagIndicator: .octothorpe) } - /// Flag for global write lock after LeafKit has started - static var started = false -} + /// Initialize Leaf with a specific tagIndicator + /// - Parameter rootDirectory: Default directory where templates will be found + /// - Parameter tagIndicator: Unique tagIndicator - may only be set once. + public init(rootDirectory: String, tagIndicator: Character) { + if !Self.started { + Character.tagIndicator = tagIndicator + Self.started = true + } + self._rootDirectory = rootDirectory + } + + public var rootDirectory: String { + mutating get { accessed = true; return _rootDirectory } + set { _rootDirectory = newValue } + } -/// `LeafRuntimeGuard` secures a value against being changed once a `LeafRenderer` is active -/// -/// Attempts to change the value secured by the runtime guard will assert in debug to warn against -/// programmatic changes to a value that needs to be consistent across the running state of LeafKit. -/// Such attempts to change will silently fail in production builds. -@propertyWrapper public struct LeafRuntimeGuard { - public var wrappedValue: T { - get { _unsafeValue } - set { if !LKConf.running(fault: "Cannot configure \(object)") { - assert(condition(newValue), "\(object) failed conditional check") - _unsafeValue = newValue } } + public static var encoding: String.Encoding { + get { _encoding } + set { if !Self.running { _encoding = newValue } } + } + + public static var boolFormatter: (Bool) -> String { + get { _boolFormatter } + set { if !Self.running { _boolFormatter = newValue } } + } + + public static var intFormatter: (Int) -> String { + get { _intFormatter } + set { if !Self.running { _intFormatter = newValue } } } - public var projectedValue: Self { self } + public static var doubleFormatter: (Double) -> String { + get { _doubleFormatter } + set { if !Self.running { _doubleFormatter = newValue } } + } + public static var nilFormatter: () -> String { + get { _nilFormatter } + set { if !Self.running { _nilFormatter = newValue } } + } - /// `condition` may be used to provide an asserting validation closure that will assert if false - /// when setting; *WILL FATAL IF FAILING AT INITIAL SETTING TIME* - public init(wrappedValue: T, - module: String = #file, - component: String = #function, - condition: @escaping (T) -> Bool = {_ in true}) { - precondition(condition(wrappedValue), "\(wrappedValue) failed conditional check") - let module = String(module.split(separator: "/").last?.split(separator: ".").first ?? "") - self.object = module.isEmpty ? component : "\(module).\(component)" - self.condition = condition - self._unsafeValue = wrappedValue + public static var voidFormatter: () -> String { + get { _voidFormatter } + set { if !Self.running { _voidFormatter = newValue } } } - /// T/F evaluation of condition, and if T is Hashable, nil if the values are the same - internal func validate(_ other: T) -> Bool? { - if let a = other as? AnyHashable, - let b = _unsafeValue as? AnyHashable, - a == b { return nil } - return condition(other) + public static var stringFormatter: (String) -> String { + get { _stringFormatter } + set { if !Self.running { _stringFormatter = newValue } } + } + + public static var arrayFormatter: ([String]) -> String { + get { _arrayFormatter } + set { if !Self.running { _arrayFormatter = newValue } } + } + + public static var dictFormatter: ([String: String]) -> String { + get { _dictFormatter } + set { if !Self.running { _dictFormatter = newValue } } + } + + public static var dataFormatter: (Data) -> String? { + get { _dataFormatter } + set { if !Self.running { _dataFormatter = newValue } } + } + + // MARK: - Internal/Private Only + internal var _rootDirectory: String { + 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 = + { "[\($0.map {"\"\($0)\""}.joined(separator: ", "))]" } + internal static var _dictFormatter: ([String: String]) -> String = + { "[\($0.map { "\($0): \"\($1)\"" }.joined(separator: ", "))]" } + internal static var _dataFormatter: (Data) -> String? = + { String(data: $0, encoding: Self._encoding) } + + + /// Convenience flag for global write-once + private static var started = false + private static var running: Bool { + assert(!Self.started, "LeafKit can only be configured prior to instantiating any LeafRenderer") + return Self.started } - internal var _unsafeValue: T - internal let condition: (T) -> Bool - private let object: String + /// Convenience flag for local lock-after-access + private var accessed = false } diff --git a/Sources/LeafKit/LeafContext/LKContextDictionary.swift b/Sources/LeafKit/LeafContext/LKContextDictionary.swift deleted file mode 100644 index 4fe15c20..00000000 --- a/Sources/LeafKit/LeafContext/LKContextDictionary.swift +++ /dev/null @@ -1,82 +0,0 @@ -/// Storage setup equivalent for `$aContext` and its various parts in a Leaf file. Entire dictionary may be -/// set `literal` or discrete value entries inside a variable dictionary could be literal; eg `$context` is -/// a potentially variable context, but `$context.aLiteral` will be set literal (and in ASTs, resolved to its -/// actual value when parsing a template). -internal struct LKContextDictionary { - /// Scope parent - let parent: LKVariable - /// Only returns top level scope & atomic variables to defer evaluation of values - private(set) var values: [String: LKDataValue] = [:] - private(set) var allVariables: Set - private(set) var literal: Bool = false - private(set) var frozen: Bool = false - private var cached: LKVarTable = [:] - - init(_ parent: LKVariable, _ literal: Bool = false) { - self.parent = parent - self.literal = literal - self.allVariables = [parent] - } - - /// Only settable while not frozen - subscript(key: String) -> LKDataValue? { - get { values[key] } - set { - guard !frozen else { return } - defer { cached[parent] = nil } - guard let newValue = newValue else { - values[key] = nil; allVariables.remove(parent.extend(with: key)); return } - guard values[key] == nil else { - values[key] = newValue; return } - if key.isValidLeafIdentifier { allVariables.insert(parent.extend(with: key)) } - values[key] = newValue - } - } - - /// Set all values, overwriting any that already exist - mutating func setValues(_ values: [String: LeafDataRepresentable], - allLiteral: Bool = false) { - literal = allLiteral - values.forEach { - if $0.isValidLeafIdentifier { allVariables.insert(parent.extend(with: $0)) } - self[$0] = allLiteral ? .literal($1) : .variable($1) - } - } - - /// With empty string, set entire object & all values to literal; with key string, set value to literal - mutating func setLiteral(_ key: String? = nil) { - if let key = key { return self[key]?.flatten() ?? () } - for (key, val) in values where !val.cached { self[key]!.flatten() } - literal = true - } - - /// Obtain `[LKVariable: LeafData]` for variable; freezes state of context as soon as accessed - /// - /// If a specific variable, flatten result if necessary and return that element - /// If the parent variable, return a dictionary data elelement for the entire scope, and cached copies of - /// individually referenced objects - mutating func match(_ key: LKVariable) -> Optional { - if let hit = cached[key] { return hit } - - if key.isPathed { - let root = key.ancestor - if !allVariables.contains(root) || match(root) == nil { return .none } - return cached.match(key) - } - else if !allVariables.contains(key) { return .none } - - frozen = true - - let value: Optional - if key == parent { - for (key, value) in values where !value.cached { values[key]!.flatten() } - value = .dictionary(values.mapValues {$0.leafData}) - } else { - let member = key.member! - if !values[member]!.cached { values[member]!.flatten() } - value = values[member]!.leafData - } - cached[key] = value - return value - } -} diff --git a/Sources/LeafKit/LeafContext/LKDataValue.swift b/Sources/LeafKit/LeafContext/LKDataValue.swift deleted file mode 100644 index 888d1085..00000000 --- a/Sources/LeafKit/LeafContext/LKDataValue.swift +++ /dev/null @@ -1,56 +0,0 @@ -/// Wrapper for what is essentially deferred evaluation of `LDR` values to `LeafData` as an intermediate -/// structure to allow general assembly of a contextual database that can be used/reused by `LeafRenderer` -/// in various render calls. Values are either `variable` and can be updated/refreshed to their `LeafData` -/// value, or `literal` and are considered globally fixed; ie, once literal, they can/should not be converted -/// back to `variable` as resolved ASTs will have used the pre-existing literal value. -internal struct LKDataValue: LeafDataRepresentable { - static func variable(_ value: LeafDataRepresentable) -> Self { .init(value) } - static func literal(_ value: LeafDataRepresentable) -> Self { .init(value, true) } - - init(_ value: LeafDataRepresentable, _ literal: Bool = false) { - container = literal ? .literal(value.leafData) : .variable(value, .none) } - - var isVariable: Bool { container.isVariable } - var leafData: LeafData { container.leafData } - - var cached: Bool { - if case .variable(_, .none) = container { return false } - if case .literal(let d) = container, d.isLazy { return false } - return true - } - - /// Coalesce to a literal - mutating func flatten() { - let flat: LKDContainer - switch container { - case .variable(let v, let d): flat = d?.container ?? v.leafData.container - case .literal(let d): flat = d.container - } - container = .literal(flat.evaluate) - } - - /// Update stored `LeafData` value for variable values - mutating func refresh() { - if case .variable(let t, _) = container { container = .variable(t, t.leafData) } } - - /// Uncache stored `LeafData` value for variable values - mutating func uncache() { - if case .variable(let t, .some) = container { container = .variable(t, .none) } } - - // MARK: - Private Only - - private enum Container: LeafDataRepresentable { - case literal(LeafData) - case variable(LeafDataRepresentable, Optional) - - var isVariable: Bool { if case .variable = self { return true } else { return false } } - var leafData: LeafData { - switch self { - case .variable(_, .some(let v)), .literal(let v) : return v - case .variable(_, .none) : return .error(internal: "Value was not refreshed") - } - } - } - - private var container: Container -} diff --git a/Sources/LeafKit/LeafContext/LKEncoder.swift b/Sources/LeafKit/LeafContext/LKEncoder.swift deleted file mode 100644 index 8366635d..00000000 --- a/Sources/LeafKit/LeafContext/LKEncoder.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Foundation - -internal class LKEncoder: LeafDataRepresentable, Encoder { - init(_ codingPath: [CodingKey] = [], _ softFail: Bool = true) { - self.codingPath = codingPath - self.softFail = softFail - self.root = nil - } - - var codingPath: [CodingKey] - - let softFail: Bool - var leafData: LeafData { root?.leafData ?? err } - var err: LeafData { softFail ? .trueNil : .error("No Encodable Data", function: "LKEncoder") } - - var root: LKEncoder? - - func container(keyedBy type: K.Type) -> KeyedEncodingContainer where K : CodingKey { - root = LKEncoderKeyed(codingPath, softFail) - return .init(root as! LKEncoderKeyed) - } - func unkeyedContainer() -> UnkeyedEncodingContainer { - root = LKEncoderUnkeyed(codingPath, softFail) - return root as! LKEncoderUnkeyed - } - func singleValueContainer() -> SingleValueEncodingContainer { - root = LKEncoderAtomic(codingPath, softFail) - return root as! LKEncoderAtomic - } - - /// Ignored - var userInfo: [CodingUserInfoKey : Any] {[:]} - - @inline(__always) - func _encode(_ value: T) throws -> LeafData where T: Encodable { - if let v = value as? LeafDataRepresentable { return state(v.leafData) } - let e = LKEncoder(codingPath, softFail) - try value.encode(to: e) - return state(e.leafData) - } - - @inline(__always) - func state(_ value: LeafData) -> LeafData { value.errored && softFail ? .trueNil : value } -} - -internal final class LKEncoderAtomic: LKEncoder, SingleValueEncodingContainer { - lazy var container: LeafData = err - override var leafData: LeafData { container } - - func encodeNil() throws { container = .trueNil } - func encode(_ value: T) throws where T: Encodable { container = try _encode(value) } -} - -internal final class LKEncoderUnkeyed: LKEncoder, UnkeyedEncodingContainer { - var array: [LeafDataRepresentable] = [] - var count: Int { array.count } - - override var leafData: LeafData { .array(array.map {$0.leafData}) } - - func encodeNil() throws { array.append(LeafData.trueNil) } - func encode(_ value: T) throws where T : Encodable { try array.append(_encode(value)) } - - func nestedContainer(keyedBy keyType: K.Type) -> KeyedEncodingContainer where K: CodingKey { - let c = LKEncoderKeyed(codingPath, softFail) - array.append(c) - return .init(c) - } - func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { - let c = LKEncoderUnkeyed(codingPath, softFail) - array.append(c) - return c - } - - func superEncoder() -> Encoder { fatalError() } -} - -internal final class LKEncoderKeyed: LKEncoder, - KeyedEncodingContainerProtocol where K: CodingKey { - var dictionary: [String: LeafDataRepresentable] = [:] - var count: Int { dictionary.count } - - override var leafData: LeafData { .dictionary(dictionary.mapValues {$0.leafData}) } - - func encodeNil(forKey key: K) throws { dictionary[key.stringValue] = LeafData.trueNil } - func encodeIfPresent(_ value: T?, forKey key: K) throws where T : Encodable { - dictionary[key.stringValue] = try value.map { try _encode($0) } } - func encode(_ value: T, forKey key: K) throws where T : Encodable { - dictionary[key.stringValue] = try _encode(value) } - - func nestedContainer(keyedBy keyType: NK.Type, forKey key: K) -> KeyedEncodingContainer where NK: CodingKey { - let c = LKEncoderKeyed(codingPath, softFail) - dictionary[key.stringValue] = c - return .init(c) - } - func nestedUnkeyedContainer(forKey key: K) -> UnkeyedEncodingContainer { - let c = LKEncoderUnkeyed(codingPath, softFail) - dictionary[key.stringValue] = c - return c - } - - func superEncoder() -> Encoder { fatalError() } - func superEncoder(forKey key: K) -> Encoder { fatalError() } -} diff --git a/Sources/LeafKit/LeafContext/LeafContextPublisher.swift b/Sources/LeafKit/LeafContext/LeafContextPublisher.swift deleted file mode 100644 index e59d1719..00000000 --- a/Sources/LeafKit/LeafContext/LeafContextPublisher.swift +++ /dev/null @@ -1,54 +0,0 @@ -/// An object that can be registered to a `LeafRenderer.Context` and automatically have its published -/// variables inserted to the scope specified by an API user. -/// -/// Example usage: in the below, a struct for tracking API versioning contains the API identifier and semver -/// values. By registering the object with the `LeafRenderer.Context` object, a custom tag will have -/// access to `externalObjects["api"]` (the actual `APIVersioning` object) during its evaluation -/// call, if unsafe access is enabled, and the `.ObjectMode` contains `.unsafe` -/// -/// Additionally, because the object adheres to `LeafContextPublisher`, the values returned by -/// `variables()` will be registered as variables available *in the serialized template*... eg, -/// `#($api.version.major)` will serialize as `0`, if `.ObjectMode` contains `.contextualized` -/// -/// If the context object is further directed to lock `api` scope as a global literal value, `$api` and its -/// values *will be automatically flattened* and available during parsing of a template; inlining their values -/// and thus optimize performance during serializing -/// ``` -/// // A core object adhering to `LeafContextPublisher` -/// class APIVersioning: LeafContextPublisher { -/// init(_ a: String, _ b: (Int, Int, Int)) { self.identifier = a; self.version = b } -/// -/// let identifier: String -/// let version: (major: Int, minor: Int, patch: Int) -/// -/// lazy var variables: [String: LeafDataGenerator] = [ -/// "identifier" : .immediate(identifier), -/// "version" : .lazy(["major": self.version.major, -/// "minor": self.version.minor, -/// "patch": self.version.patch]) -/// ] -/// } -/// -/// // An example extension of the object allowing an additional set of -/// // user-configured additional generators -/// extension APIVersioning { -/// var extendedVariables: [String: LeafDataGenerator] {[ -/// "isRelease": { .lazy(version.major > 0) } -/// ]} -/// } -/// -/// let myAPI = APIVersioning("api", (0,0,1)) -/// -/// var aContext: LeafRenderer.Context = [:] -/// try aContext.register(object: myAPI, toScope: "api") -/// try aContext.register(generators: myAPI.extendedVariables, toScope: "api") -/// // Result of `#($api.version)` in Leaf: -/// // ["major": 0, "minor": 0, "patch": 1] -/// myAPI.version.major = 1 -/// // Result of `#($api.version)` in Leaf in subequent render: -/// // ["major": 1, "minor": 0, "patch": 1] -/// ``` -public protocol LeafContextPublisher { - /// First-level API provider that adheres an object *it owns* to this protocol must implement `variables` - var leafVariables: [String: LeafDataGenerator] { get } -} diff --git a/Sources/LeafKit/LeafCore/ArrayReturn.swift b/Sources/LeafKit/LeafCore/ArrayReturn.swift deleted file mode 100644 index 05371412..00000000 --- a/Sources/LeafKit/LeafCore/ArrayReturn.swift +++ /dev/null @@ -1,26 +0,0 @@ -internal extension LeafEntities { - func registerArrayReturns() { - use(ArrayToArrayMap.indices , asMethod: "indices") - use(DictionaryToArrayMap.keys , asMethod: "keys") - use(DictionaryToArrayMap.values , asMethod: "values") - } -} - -internal struct ArrayToArrayMap: LKMapMethod, ArrayParam, ArrayReturn { - func evaluate(_ params: LeafCallValues) -> LKData { .array(f(params[0].array!)) } - - static let indices: Self = .init({$0.indices.map {$0.leafData}}) - - private init(_ map: @escaping ([LKData]) -> [LKData]) { f = map } - private let f: ([LKData]) -> [LKData] -} - -internal struct DictionaryToArrayMap: LKMapMethod, DictionaryParam, ArrayReturn { - func evaluate(_ params: LeafCallValues) -> LKData { .array(f(params[0].dictionary!)) } - - static let keys: Self = .init({Array($0.keys.map {$0.leafData})}) - static let values: Self = .init({Array($0.values)}) - - private init(_ map: @escaping ([String: LKData]) -> [LKData]) { f = map } - private let f: ([String: LKData]) -> [LKData] -} diff --git a/Sources/LeafKit/LeafCore/BoolReturn.swift b/Sources/LeafKit/LeafCore/BoolReturn.swift deleted file mode 100644 index e310d730..00000000 --- a/Sources/LeafKit/LeafCore/BoolReturn.swift +++ /dev/null @@ -1,67 +0,0 @@ -internal extension LeafEntities { - func registerBoolReturns() { - use(StrStrToBoolMap.hasPrefix , asMethod: "hasPrefix") - use(StrStrToBoolMap.hasSuffix , asMethod: "hasSuffix") - - use(CollectionToBoolMap.isEmpty , asMethod: "isEmpty") - use(StrToBoolMap.isEmpty , asMethod: "isEmpty") - - use(CollectionElementToBoolMap.contains , asMethod: "contains") - use(StrStrToBoolMap.contains , asMethod: "contains") - } -} - -/// (Array || Dictionary.values) -> Bool -internal struct CollectionToBoolMap: LKMapMethod, CollectionsParam, BoolReturn { - func evaluate(_ params: LeafCallValues) -> LKData { - switch params[0].container { - case .dictionary(let x) : return .bool(f(.init(x.values))) - case .array(let x) : return .bool(f(.init(x))) - default : return .error(internal: "Only supports collections") } - } - - static let isEmpty: Self = .init({ $0.isEmpty }) - - private init(_ map: @escaping (AnyCollection) -> Bool) { f = map } - private let f: (AnyCollection) -> Bool -} - -/// (Array | Dictionary, Any) -> Bool -internal struct CollectionElementToBoolMap: LKMapMethod, BoolReturn { - static var callSignature: [LeafCallParameter] { [.collections, .any] } - - func evaluate(_ params: LeafCallValues) -> LKData { - switch params[0].container { - case .dictionary(let x) : return .bool(f(.init(x.values), params[1])) - case .array(let x) : return .bool(f(.init(x), params[1])) - default : return .error(internal: "Only supports collections") } - } - - static let contains: Self = .init({for x in $0 where x.storedType == $1.storedType {if x == $1 { return true }}; return false}) - - private init(_ map: @escaping (AnyCollection, LKData) -> Bool) { f = map } - private let f: (AnyCollection, LKData) -> Bool -} - -/// (String, String) -> Bool -internal struct StrStrToBoolMap: LKMapMethod, StringStringParam, BoolReturn { - func evaluate(_ params: LeafCallValues) -> LKData { .bool(f(params[0].string!, params[1].string!)) } - - static let hasPrefix: Self = .init({ $0.hasPrefix($1) }) - static let hasSuffix: Self = .init({ $0.hasSuffix($1) }) - static let contains: Self = .init({ $0.contains($1) }) - - private init(_ map: @escaping (String, String) -> Bool) { f = map } - private let f: (String, String) -> Bool - -} - -/// (String) -> Bool -internal struct StrToBoolMap: LKMapMethod, StringParam, BoolReturn { - func evaluate(_ params: LeafCallValues) -> LKData { .bool(f(params[0].string!)) } - - static let isEmpty: Self = .init({ $0.isEmpty }) - - private init(_ map: @escaping (String) -> Bool) { f = map } - private let f: (String) -> Bool -} diff --git a/Sources/LeafKit/LeafCore/ByteBuffer+LKRawBlock.swift b/Sources/LeafKit/LeafCore/ByteBuffer+LKRawBlock.swift deleted file mode 100644 index b9cb1acb..00000000 --- a/Sources/LeafKit/LeafCore/ByteBuffer+LKRawBlock.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation -import NIOFoundationCompat - -extension ByteBuffer: LKRawBlock { - var error: Error? { nil } - var encoding: String.Encoding { .utf8 } - - static var recall: Bool { false } - - static func instantiate(size: UInt32, - encoding: String.Encoding) -> LKRawBlock { - ByteBufferAllocator().buffer(capacity: Int(size)) } - - /// Always identity return and valid - var serialized: (buffer: ByteBuffer, valid: Bool?) { (self, true) } - - /// Always takes either the serialized view of a `LKRawBlock` or the direct result if it's a `ByteBuffer` - mutating func append(_ block: inout LKRawBlock) { - var input = block as? Self ?? block.serialized.buffer - let inputEncoding = block.encoding - - guard encoding != inputEncoding else { writeBuffer(&input); return } - fatalError() - } - - mutating func append(_ data: LeafData) { _append(data) } - - /// Appends data using configured serializer views - mutating func _append(_ data: LeafData, wrapString: Bool = false) { - guard !data.isNil else { - write(LeafBuffer.nilFormatter(data.storedType.short)) - return - } - switch data.storedType { - case .bool : write(LeafBuffer.boolFormatter(data.bool!)) - case .data : write(LeafBuffer.dataFormatter(data.data!, .utf8) ?? "") - case .double : write(LeafBuffer.doubleFormatter(data.double!)) - case .int : write(LeafBuffer.intFormatter(data.int!)) - case .string : write(LeafBuffer.stringFormatter(wrapString ? "\"\(data.string!)\"": data.string!)) - case .void : break - case .array : let a = data.array! - write("[") - for index in a.indices { - _append(a[index], wrapString: true) - if index != a.indices.last! { write(", ") } - } - write("]") - case .dictionary : let d = data.dictionary!.sorted { $0.key < $1.key } - write("[") - for index in d.indices { - write("\"\(d[index].key)\": ") - _append(d[index].value, wrapString: true) - if index != d.indices.last! { write(", ") } - } - if d.isEmpty { write(":") } - write("]") - } - } - - mutating func voidAction() {} - mutating func close() {} - - var byteCount: UInt32 { UInt32(readableBytes) } - var contents: String { getString(at: readerIndex, length: readableBytes) ?? "" } - - mutating func write(_ str: String) { try! writeString(str, encoding: encoding) } -} diff --git a/Sources/LeafKit/LeafCore/ControlFlow.swift b/Sources/LeafKit/LeafCore/ControlFlow.swift deleted file mode 100644 index 01cfb7bc..00000000 --- a/Sources/LeafKit/LeafCore/ControlFlow.swift +++ /dev/null @@ -1,211 +0,0 @@ -// MARK: Subject to change prior to 1.0.0 release -// - -// MARK: - Control Flow: Looping - -internal extension LeafEntities { - func registerControlFlow() { - use(ForLoop.self , asBlock: "for") - use(WhileLoop.self , asBlock: "while") - - use(IfBlock.self , asBlock: "if") - use(ElseIfBlock.self , asBlock: "elseif") - use(ElseBlock.self , asBlock: "else") - } -} - -internal protocol CoreBlock: LeafBlock {} -internal extension CoreBlock { - static var invariant: Bool { true } - static var evaluable: Bool { false } - static var returns: Set { .void } -} - -internal protocol Nonscoping { init() } -internal extension Nonscoping { - static var parseSignatures: ParseSignatures? { nil } - var scopeVariables: [String]? { nil } - - static func instantiate(_ signature: String?, - _ params: [String]) throws -> Self { Self.init() } -} - -/// `#for(value in collection):` or `#for((index, value) in collection):` -struct ForLoop: CoreBlock { - static var parseSignatures: ParseSignatures? {[ - /// `#for(_ in collection)` - "discard": [.expression([.keyword([._]), - .keyword([.in]), - .callParameter])], - /// `#for(value in collection)` where `index, key` set variable to collection index - "single": [.expression([.unscopedVariable, - .keyword([.in]), - .callParameter])], - /// `#for((index, value) in collection)` - "tuple": [.expression([.tuple([.unscopedVariable, .unscopedVariable]), - .keyword([.in]), - .callParameter])] - ]} - static var callSignature: [LeafCallParameter] { [.types([.array, .dictionary, .string, .int])] } - - private(set) var scopeVariables: [String]? = nil - - static func instantiate(_ signature: String?, - _ params: [String]) throws -> ForLoop { - switch signature { - case "tuple" : return ForLoop(key: params[0], value: params[1]) - case "discard" : return ForLoop() - case "single" : return ["index", "key"].contains(params[0]) - ? ForLoop(key: params[0]) - : ForLoop(value: params[0]) - default : __Unreachable("ForLoop called with no signature") - } - } - - init(key: String? = nil, value: String? = nil) { - self.set = key != nil || value != nil - self.setKey = key != nil ? true : false - self.setValue = value != nil ? true : false - if set { - self.first = "isFirst" - self.last = "isLast" - self.scopeVariables = [first, last] - } - if setKey { - self.key = key! - self.scopeVariables?.append(self.key) - } - if setValue { - self.value = value! - self.scopeVariables?.append(self.value) - } - } - - mutating func evaluateScope(_ params: LeafCallValues, - _ variables: inout [String: LeafData]) -> EvalCount { - if set { - switch params[0].container { - case .array(let a) : cache = a.enumerated().map { (o, e) in - (setKey ? .int(o) : .trueNil, setValue ? e : .trueNil) } - case .dictionary(let d) : cache = d.sorted(by: {$0.key < $1.key}).map { (k, v) in - (setKey ? .string(k) : .trueNil, setValue ? v : .trueNil) } - case .int(let i) : passes = i > 0 ? UInt32(i) : 0 - for i in 0.. 0 ? UInt32(i) : 0 - case .string(let s) : passes = UInt32(s.count) - default: __MajorBug("Non-container provided as parameter") - } - } - return passes != 0 ? reEvaluateScope(&variables) : .discard - } - - mutating func reEvaluateScope(_ variables: inout [String: LeafData]) -> EvalCount { - guard pass < passes else { return .discard } - guard set else { pass += 1; return .repeating(passes + 1 - UInt32(pass)) } - if set { variables[first] = .bool(pass == 0) - variables[last] = .bool(pass == passes - 1) } - if setKey { variables[key] = cache[pass].0 } - if setValue { variables[value] = cache[pass].1 } - pass += 1 - return .repeating(passes + 1 - UInt32(pass)) - } - - var first: String = "#first" - var last: String = "#last" - var key: String = "#key" - var value: String = "#value" - var set: Bool - var setKey: Bool - var setValue: Bool - - var pass: Int = 0 - var passes: UInt32 = 0 - var cache: [(LeafData, LeafData)] = [] -} - -/// `#while(bool):` - 0...n while -internal struct WhileLoop: CoreBlock, Nonscoping { - static var callSignature: [LeafCallParameter] { [.bool] } - static func instantiate(_ signature: String?, - _ params: [String]) throws -> WhileLoop {.init()} - - mutating func evaluateScope(_ params: LeafCallValues, - _ variables: inout [String: LeafData]) -> EvalCount { - params[0].bool! ? .indefinite : .discard } - - mutating func reEvaluateScope(_ variables: inout [String: LeafData]) -> EvalCount { - __Unreachable("While loops never return non-nil") } -} - -/// `#repeat(while: bool):` 1...n+1 while -/// Note - can't safely be used if the while condition is mutating - a flag would be needed to defer evaluation -internal struct RepeatLoop: CoreBlock, Nonscoping { - // FIXME: Can't be used yet - static var callSignature:[LeafCallParameter] { [.bool(labeled: "while")] } - var cache: Bool? = nil - - static func instantiate(_ signature: String?, _ params: [String]) throws -> RepeatLoop {.init()} - - mutating func evaluateScope(_ params: LeafCallValues, - _ variables: inout [String: LeafData]) -> EvalCount { - let result: EvalCount = cache != false ? .indefinite : .discard - cache = params[0].bool! - return result - } - - mutating func reEvaluateScope(_ variables: inout [String: LeafData]) -> EvalCount { __Unreachable("Repeat loops never return non-nil") } -} - -// MARK: - Control Flow: Branching - -/// `#if(bool)` - accepts `elseif, else` -struct IfBlock: ChainedBlock, CoreBlock, Nonscoping { - static var callSignature: [LeafCallParameter] { [.bool] } - static var chainsTo: [ChainedBlock.Type] { [] } - static var chainAccepts: [ChainedBlock.Type] { [ElseIfBlock.self, ElseBlock.self] } - - static func instantiate(_ signature: String?, - _ params: [String]) throws -> IfBlock { .init() } - - mutating func evaluateScope(_ params: LeafCallValues, - _ variables: inout [String: LeafData]) -> EvalCount { - params[0].bool! ? .once : .discard } -} - -/// `#elseif(bool)` - chains to `if, elseif`, accepts `elseif, else` -struct ElseIfBlock: ChainedBlock, CoreBlock, Nonscoping { - static var callSignature: [LeafCallParameter] { [.bool] } - static var chainsTo: [ChainedBlock.Type] { [ElseIfBlock.self, IfBlock.self] } - static var chainAccepts: [ChainedBlock.Type] { [ElseIfBlock.self, ElseBlock.self] } - - static func instantiate(_ signature: String?, - _ params: [String]) throws -> ElseIfBlock {.init()} - - mutating func evaluateScope(_ params: LeafCallValues, - _ variables: inout [String: LeafData]) -> EvalCount { - params[0].bool! ? .once : .discard } -} - -/// `#elseif(bool)` - chains to `if, elseif` - end of chain -struct ElseBlock: ChainedBlock, CoreBlock, Nonscoping, EmptyParams { - static var chainsTo: [ChainedBlock.Type] { [ElseIfBlock.self, IfBlock.self] } - static var chainAccepts: [ChainedBlock.Type] { [] } - - static func instantiate(_ signature: String?, - _ params: [String]) throws -> ElseBlock {.init()} - - mutating func evaluateScope(_ params: LeafCallValues, - _ variables: inout [String: LeafData]) -> EvalCount { .once } -} diff --git a/Sources/LeafKit/LeafCore/DoubleReturn.swift b/Sources/LeafKit/LeafCore/DoubleReturn.swift deleted file mode 100644 index adae97a4..00000000 --- a/Sources/LeafKit/LeafCore/DoubleReturn.swift +++ /dev/null @@ -1,29 +0,0 @@ -internal extension LeafEntities { - func registerDoubleReturns() { - use(DoubleIntToDoubleMap.rounded, asMethod: "rounded") - } -} - -/// (Array || Dictionary.values) -> Int -internal struct DoubleIntToDoubleMap: LKMapMethod, DoubleReturn { - static var callSignature: [LeafCallParameter] { [.double, .int(labeled: "places")] } - - func evaluate(_ params: LeafCallValues) -> LKData { .double(f(params[0].double!, params[1].int!)) } - - static let rounded: Self = .init({let x = pow(10, Double($1)); return ($0*x).rounded(.toNearestOrAwayFromZero)/x}) - - private init(_ map: @escaping (Double, Int) -> Double) { f = map } - private let f: (Double, Int) -> Double -} - -internal struct DoubleDoubleToDoubleMap: LKMapMethod, DoubleReturn { - static var callSignature: [LeafCallParameter] = [.double, .double] - - func evaluate(_ params: LeafCallValues) -> LKData { .double(f(params[0].double!, params[1].double!)) } - - static let _min: Self = .init({ min($0, $1) }) - static let _max: Self = .init({ max($0, $1) }) - - private init(_ map: @escaping (Double, Double) -> Double) { f = map } - private let f: (Double, Double) -> Double -} diff --git a/Sources/LeafKit/LeafCore/Error.swift b/Sources/LeafKit/LeafCore/Error.swift deleted file mode 100644 index 74909b70..00000000 --- a/Sources/LeafKit/LeafCore/Error.swift +++ /dev/null @@ -1,22 +0,0 @@ -internal extension LeafEntities { - func registerErroring() { - use(LDErrorIdentity(), asFunction: "Error") - use(LDThrow(), asFunction: "throw") - } -} - -internal protocol LDError: Invariant, VoidReturn {} -extension LDError { - func evaluate(_ params: LeafCallValues) -> LKData { - .error(params[0].string!, function: String(describing: self)) } -} - -internal struct LDErrorIdentity: LDError { - static var callSignature: [LeafCallParameter] { - [.init(types: .string, defaultValue: .string("Unknown serialize error"))] } -} - -internal struct LDThrow: LDError { - static var callSignature: [LeafCallParameter] { - [.string(labeled: "reason", defaultValue: .string("Unknown serialize error"))] } -} diff --git a/Sources/LeafKit/LeafCore/IntReturn.swift b/Sources/LeafKit/LeafCore/IntReturn.swift deleted file mode 100644 index a6959a9e..00000000 --- a/Sources/LeafKit/LeafCore/IntReturn.swift +++ /dev/null @@ -1,43 +0,0 @@ -internal extension LeafEntities { - func registerIntReturns() { - use(CollectionToIntMap.count , asMethod: "count") - use(StrToIntMap.count , asMethod: "count") - } -} - -/// (String) -> Int -internal struct StrToIntMap: LKMapMethod, StringParam, IntReturn { - func evaluate(_ params: LeafCallValues) -> LKData { .int(f(params[0].string!)) } - - static let count: Self = .init({ $0.count }) - - private init(_ map: @escaping (String) -> Int) { f = map } - private let f: (String) -> Int -} - -/// (Array || Dictionary.values) -> Int -internal struct CollectionToIntMap: LKMapMethod, CollectionsParam, IntReturn { - func evaluate(_ params: LeafCallValues) -> LKData { - switch params[0].container { - case .dictionary(let x) : return .int(f(.init(x.values))) - case .array(let x) : return .int(f(.init(x))) - default : return .error(internal: "Non-collection parameter") } - } - - static let count: Self = .init({ $0.count }) - - private init(_ map: @escaping (AnyCollection) -> Int) { f = map } - private let f: (AnyCollection) -> Int -} - -internal struct IntIntToIntMap: LKMapMethod, IntReturn { - static var callSignature: [LeafCallParameter] = [.int, .int] - - func evaluate(_ params: LeafCallValues) -> LKData { .int(f(params[0].int!, params[1].int!)) } - - static let _min: Self = .init({ min($0, $1) }) - static let _max: Self = .init({ max($0, $1) }) - - private init(_ map: @escaping (Int, Int) -> Int) { f = map } - private let f: (Int, Int) -> Int -} diff --git a/Sources/LeafKit/LeafCore/LKMetaBlock.swift b/Sources/LeafKit/LeafCore/LKMetaBlock.swift deleted file mode 100644 index b325fbb7..00000000 --- a/Sources/LeafKit/LeafCore/LKMetaBlock.swift +++ /dev/null @@ -1,113 +0,0 @@ -// MARK: Subject to change prior to 1.0.0 release -// - -// MARK: - LKMetaBlock - -internal extension LeafEntities { - func registerMetaBlocks() { - use(RawSwitch.self , asMeta: "raw") - use(Define.self , asMeta: "define") - //use(Define.self , asMeta: "def") - use(Evaluate.self , asMeta: "evaluate") - //use(Evaluate.self , asMeta: "eval") - use(Inline.self , asMeta: "inline") - use(Declare.self , asMeta: "var") - use(Declare.self , asMeta: "let") - } -} - -internal protocol LKMetaBlock: LeafBlock { static var form: LKMetaForm { get } } - -internal enum LKMetaForm: Int, Hashable { - case rawSwitch - case define - case evaluate - case inline - case declare -} - -// MARK: - Define/Evaluate/Inline/RawSwitch - -/// `Define` blocks will be followed by a normal scope table reference or an atomic syntax -internal struct Define: LKMetaBlock, EmptyParams, VoidReturn, Invariant { - static var form: LKMetaForm { .define } - - var identifier: String - var param: LKParameter? - var table: Int - var row: Int - - mutating func remap(offset: Int) { table += offset } - - static let warning = "call signature is (identifier) when a block, (identifier = evaluableParameter) when a function" -} - -/// `Evaluate` blocks will be followed by either a nil scope syntax or a passthrough syntax if it has a defaulted value -internal struct Evaluate: LKMetaBlock, EmptyParams, AnyReturn { - static var form: LKMetaForm { .evaluate } - static var invariant: Bool { false } - - let identifier: String - let defaultValue: LKParameter? - - static let warning = "call signature is (identifier) or (identifier ?? evaluableParameter)" -} - -/// `Inline` is always followed by a rawBlock with the current rawHandler state, and a nil scope syntax if processing -/// -/// When resolving, if processing, inlined template's AST will be appended to the AST, `Inline` block's +2 -/// scope syntax will point to the inlined file's remapped entry table. -/// If inlined file is not being processed, rawBlock will be replaced with one of the same type with the inlined -/// raw document's contents. -internal struct Inline: LKMetaBlock, EmptyParams, VoidReturn, Invariant { - static var form: LKMetaForm { .inline } - - var file: String - var process: Bool - var rawIdentifier: String? - var availableVars: Set? - - static let literalWarning = "requires a string literal argument for the file" - static let warning = "call signature is (\"file\", as: type) where type is `leaf`, `raw`, or a named raw handler" -} - -/// `RawSwitch` either alters the current raw handler when by itself, or produces an isolated raw handling block with an attached scope -internal struct RawSwitch: LKMetaBlock, EmptyParams, AnyReturn, Invariant { - static var form: LKMetaForm { .rawSwitch } - - init(_ factory: LKRawBlock.Type, _ tuple: LKTuple) { - self.factory = factory - self.params = .init(tuple.values.map {$0.data!} , tuple.labels) - } - - var factory: LKRawBlock.Type - var params: LeafCallValues -} - -/// Variable declaration -internal struct Declare: LKMetaBlock, EmptyParams, VoidReturn, Invariant { - static var form: LKMetaForm { .declare } - - let variable: Bool - - static let warning = "call signature is (identifier) or (identifier = evaluableParameter)" -} - -// MARK: Default Implementations - -extension LKMetaBlock { - static var parseSignatures: ParseSignatures? { __Unreachable("LKMetaBlock") } - static var evaluable: Bool { false } - - var form: LKMetaForm { Self.form } - var scopeVariables: [String]? { nil } - - static func instantiate(_ signature: String?, - _ params: [String]) throws -> Self { __Unreachable("LKMetaBlock") } - - mutating func evaluateScope(_ params: LeafCallValues, - _ variables: inout [String: LeafData]) -> EvalCount { .once } - mutating func reEvaluateScope(_ variables: inout [String: LeafData]) -> EvalCount { - __Unreachable("Metablocks only called once") } -} - diff --git a/Sources/LeafKit/LeafCore/LeafBuffer.swift b/Sources/LeafKit/LeafCore/LeafBuffer.swift deleted file mode 100644 index 2f6845fa..00000000 --- a/Sources/LeafKit/LeafCore/LeafBuffer.swift +++ /dev/null @@ -1,154 +0,0 @@ -// MARK: Subject to change prior to 1.0.0 release -// - -// MARK: - Raw Handlers -import Foundation -import NIOFoundationCompat - -/// The default output object used by LeafKit to stream serialized data to. -public struct LeafBuffer { - @LeafRuntimeGuard public static var boolFormatter: (Bool) -> String = { $0.description } - @LeafRuntimeGuard public static var intFormatter: (Int) -> String = { $0.description } - @LeafRuntimeGuard public static var doubleFormatter: (Double) -> String = { $0.description } - @LeafRuntimeGuard public static var nilFormatter: (_ type: String) -> String = { _ in "" } - @LeafRuntimeGuard public static var stringFormatter: (String) -> String = { $0 } - @LeafRuntimeGuard public static var dataFormatter: (Data, String.Encoding) -> String? = - { String(data: $0, encoding: $1) } - - private(set) var error: Error? = nil - private(set) var encoding: String.Encoding - private var output: ByteBuffer - - /// Strip leading blank lines in appended raw blocks (for cropping after `voidAction()`) - private var stripLeadingBlanklines: Bool = false - /// Index of last trailing blank line of raw (for cropping extra whitespace at `close()`) - private var trailingBlanklineIndex: Int? = nil -} - -extension LeafBuffer: LKRawBlock { - init(_ output: ByteBuffer, _ encoding: String.Encoding) { - self.output = output - self.encoding = encoding - } - - static var recall: Bool { false } - - static func instantiate(size: UInt32, - encoding: String.Encoding) -> LKRawBlock { - Self.init(ByteBufferAllocator().buffer(capacity: Int(size)), encoding) } - - /// Always identity return and valid - var serialized: (buffer: ByteBuffer, valid: Bool?) { (output, true) } - - /// Always takes either the serialized view of a `LKRawBlock` or the direct result if it's a `ByteBuffer` - mutating func append(_ block: inout LKRawBlock) { - close() - var input = (block as? Self)?.output ?? block.serialized.buffer -// guard encoding != block.encoding || stripBlanklines else { -// output.writeBuffer(&input) -// return -// } - - guard var x = input.readString(length: input.readableBytes, - encoding: block.encoding) else { - self.error = "Couldn't transcode input raw block"; return - } - - if stripLeadingBlanklines { - while let nonWhitespace = x.firstIndex(where: {!$0.isWhitespace}), - let newline = x.firstIndex(where: {$0.isNewline}), - newline < nonWhitespace { - x.removeSubrange(x.startIndex...newline) - stripLeadingBlanklines = false - } - if x.firstIndex(where: {!$0.isWhitespace}) == nil, - let newline = x.firstIndex(where: {$0.isNewline}) { - x.removeSubrange(x.startIndex...newline) } - } - - guard !x.isEmpty else { return } - - var cropped = "" - - if let lastNonWhitespace = x.lastIndex(where: {!$0.isWhitespace}), - let lastNewline = x[lastNonWhitespace.. (mutate: LKData?, result: LKData) { - let cache = params[0].string! - var operand = cache - f(&operand, params[1].string!) - return (operand != cache ? operand.leafData : nil, .trueNil) - } - - static let append: Self = .init({$0.append($1)}) - - private init(_ map: @escaping (inout String, String) -> ()) { f = map } - private let f: (inout String, String) -> () -} - -/// Mutating (String) -> String -internal struct MutatingStrToStrMap: LeafMutatingMethod, StringParam, StringReturn { - func mutatingEvaluate(_ params: LeafCallValues) -> (mutate: Optional, result: LKData) { - let cache = params[0].string! - var operand = cache - let result = f(&operand) - return (operand != cache ? operand.leafData : .none, .string(result)) - } - - static let popLast: Self = .init({ $0.popLast().map{String($0)} }) - - private init(_ map: @escaping (inout String) -> String?) { f = map } - private let f: (inout String) -> String? -} - -/// Mutating (Array) -> Any -internal struct MutatingArrayToAnyMap: LeafMutatingMethod, ArrayParam, AnyReturn { - func mutatingEvaluate(_ params: LeafCallValues) -> (mutate: Optional, result: LKData) { - let cache = params[0].array! - var operand = cache - let result = f(&operand) - return (operand != cache ? .array(operand) : .none, - result != nil ? result! : .trueNil) - } - - static let popLast: Self = .init({$0.popLast()}) - - private init(_ map: @escaping (inout [LeafData]) -> Optional) { f = map } - private let f: (inout [LeafData]) -> Optional -} - -/// Mutating (Array, Any) -internal struct MutatingArrayAnyMap: LeafMutatingMethod, VoidReturn { - static var callSignature: [LeafCallParameter] { [.array, .any] } - - func mutatingEvaluate(_ params: LeafCallValues) -> (mutate: Optional, result: LKData) { - let cache = params[0].array! - var operand = cache - f(&operand, params[1]) - return (operand != cache ? .array(operand) : .none, .trueNil) - } - - static let append: Self = .init({$0.append($1)}) - - private init(_ map: @escaping (inout [LeafData], LeafData) -> ()) { f = map } - private let f: (inout [LeafData], LeafData) -> () -} diff --git a/Sources/LeafKit/LeafCore/NumericFormatters.swift b/Sources/LeafKit/LeafCore/NumericFormatters.swift deleted file mode 100644 index cdaddd64..00000000 --- a/Sources/LeafKit/LeafCore/NumericFormatters.swift +++ /dev/null @@ -1,31 +0,0 @@ -public struct DoubleFormatterMap: LKMapMethod, StringReturn { - @LeafRuntimeGuard public static var defaultPlaces: UInt8 = 2 - - public static var callSignature: [LeafCallParameter] {[ - .double, .int(labeled: "places", defaultValue: Int(Self.defaultPlaces).leafData) - ]} - - public func evaluate(_ params: LeafCallValues) -> LeafData { - .string(f(params[0].double!, params[1].int!)) } - - static let seconds: Self = .init({$0.formatSeconds(places: Int($1))}) - - private init(_ map: @escaping (Double, Int) -> String) { f = map } - private let f: (Double, Int) -> String -} - -public struct IntFormatterMap: LKMapMethod, StringReturn { - @LeafRuntimeGuard public static var defaultPlaces: UInt8 = 2 - - public static var callSignature: [LeafCallParameter] {[ - .int, .int(labeled: "places", defaultValue: Int(Self.defaultPlaces).leafData) - ]} - - public func evaluate(_ params: LeafCallValues) -> LeafData { - .string(f(params[0].int!, params[1].int!)) } - - internal static let bytes: Self = .init({$0.formatBytes(places: Int($1))}) - - private init(_ map: @escaping (Int, Int) -> String) { f = map } - private let f: (Int, Int) -> String -} diff --git a/Sources/LeafKit/LeafCore/StringReturn.swift b/Sources/LeafKit/LeafCore/StringReturn.swift deleted file mode 100644 index 94634b36..00000000 --- a/Sources/LeafKit/LeafCore/StringReturn.swift +++ /dev/null @@ -1,40 +0,0 @@ -internal extension LeafEntities { - func registerStringReturns() { - use(StrToStrMap.uppercased, asMethod: "uppercased") - use(StrToStrMap.lowercased, asMethod: "lowercased") - use(StrToStrMap.capitalized, asMethod: "capitalized") - } -} - -/// (String) -> String -internal struct StrToStrMap: LKMapMethod, StringParam, StringReturn { - func evaluate(_ params: LeafCallValues) -> LKData { .string(f(params[0].string!)) } - - static let uppercased: Self = .init({ $0.uppercased() }) - static let lowercased: Self = .init({ $0.lowercased() }) - static let capitalized: Self = .init({ $0.capitalized }) - static let reversed: Self = .init({ String($0.reversed()) }) - static let randomElement: Self = .init({ $0.isEmpty ? nil : String($0.randomElement()!) }) - static let escapeHTML: Self = .init({ $0.reduce(into: "", {$0.append(basicHTML[$1] ?? $1.description)}) }) - - private init(_ map: @escaping (String) -> String?) { f = map } - private let f: (String) -> String? - - private static let basicHTML: [Character: String] = [ - .lessThan: "<", .greaterThan: ">", .ampersand: "&", .quote: """, .apostrophe: "'" - ] -} - -internal struct StrStrStrToStrMap: LKMapMethod, StringReturn { - static var callSignature:[LeafCallParameter] {[ - .string, .string(labeled: "occurencesOf"), .string(labeled: "with") - ]} - - func evaluate(_ params: LeafCallValues) -> LKData { - .string(f(params[0].string!, params[1].string!, params[2].string!)) } - - static let replace: Self = .init({ $0.replacingOccurrences(of: $1, with: $2) }) - - private init(_ map: @escaping (String, String, String) -> String) { f = map } - private let f: (String, String, String) -> String -} diff --git a/Sources/LeafKit/LeafCore/Timestamp+Date.swift b/Sources/LeafKit/LeafCore/Timestamp+Date.swift deleted file mode 100644 index 86d74802..00000000 --- a/Sources/LeafKit/LeafCore/Timestamp+Date.swift +++ /dev/null @@ -1,215 +0,0 @@ -import Foundation -import NIOConcurrencyHelpers - -internal extension LeafEntities { - func registerMisc() { - use(LeafTimestamp(), asFunction: "Timestamp") - use(LeafDateFormatters.ISO8601(), asFunction: "Date") - use(LeafDateFormatters.Fixed(), asFunction: "Date") - use(LeafDateFormatters.Localized(), asFunction: "Date") - } -} - -// MARK: - LeafTimestamp - -/// A time interval relative to the specificed base date -/// -/// Default value for the reference date is the Swift Date `referenceDate` (2001-01-01 00:00:00 +0000) -public struct LeafTimestamp: LeafFunction, DoubleReturn { - /// The date used as the reference base for all interpretations of Double timestamps - @LeafRuntimeGuard public static var referenceBase: ReferenceBase = .referenceDate - - public static var callSignature: [LeafCallParameter] {[ - .init(types: [.int, .double, .string], defaultValue: "now"), - .string(labeled: "since", defaultValue: referenceBase.leafData), - ]} - - public func evaluate(_ params: LeafCallValues) -> LeafData { - guard let base = ReferenceBase(rawValue: params[1].string!) else { - return .error("\(params[1].string!) is not a valid reference base; must be one of \(ReferenceBase.allCases.description)") } - let offset = base.interval - if LKDTypeSet.numerics.contains(params[0].storedType) { - return .double(Date(timeIntervalSinceReferenceDate: offset + params[0].double!) - .timeIntervalSinceReferenceDate) } - guard let x = ReferenceBase(rawValue: params[0].string!) else { return ReferenceBase.fault(params[0].string!) } - return .double(base == x ? 0 : x.interval - offset) - } - - public static var invariant: Bool { false } - - public enum ReferenceBase: String, RawRepresentable, CaseIterable, LeafDataRepresentable { - case now - case unixEpoch - case referenceDate - case distantPast - case distantFuture - - public var leafData: LeafData { .string(rawValue) } - } -} - -public struct LeafDateFormatters { - /// Used by `ISO8601`, `Fixed`, & `.Custom` - @LeafRuntimeGuard(condition: {TimeZone(identifier: $0) != nil}) - public static var defaultTZIdentifier: String = "UTC" - - /// Used by `ISO8601` - @LeafRuntimeGuard - public static var defaultFractionalSeconds: Bool = false - - /// Used by `Custom` - @LeafRuntimeGuard(condition: {Locale.availableIdentifiers.contains($0)}) - public static var defaultLocale: String = "en_US_POSIX" - - /// ISO8601 Date strings - public struct ISO8601: LeafFunction, StringReturn, Invariant { - public static var callSignature: [LeafCallParameter] {[ - .init(types: [.int, .double, .string], defaultValue: .lazy(now, returns: .double)), - .string(labeled: "timeZone", defaultValue: defaultTZIdentifier.leafData), - .bool(labeled: "fractionalSeconds", defaultValue: defaultFractionalSeconds.leafData) - ]} - - public func evaluate(_ params: LeafCallValues) -> LeafData { - let timestamp = params[0] - let zone = params[1].string! - let fractional = params[2].bool! - var interval: Double - - if timestamp.isNumeric { interval = timestamp.double! } - else { - guard let t = LeafTimestamp.ReferenceBase(rawValue: timestamp.string!) else { - return LeafTimestamp.ReferenceBase.fault(timestamp.string!) } - interval = t.interval - } - - var formatter = LeafDateFormatters[zone, fractional] - if formatter == nil { - guard let tZ = TimeZone(identifier: zone) else { - return .error("\(zone) is not a valid time zone identifier") } - formatter = ISO8601DateFormatter() - formatter!.timeZone = tZ - if fractional { formatter!.formatOptions.update(with: .withFractionalSeconds) } - LeafDateFormatters[zone, fractional] = formatter - } - return .string(formatter!.string(from: base.addingTimeInterval(interval))) - } - } - - /// Fixed format DateFormatter strings - public struct Fixed: LeafFunction, StringReturn, Invariant { - public static var callSignature: [LeafCallParameter] {[ - .init(label: "timeStamp", types: [.int, .double, .string]), - .string(labeled: "fixedFormat"), - .string(labeled: "timeZone", defaultValue: defaultTZIdentifier.leafData), - ]} - - public func evaluate(_ params: LeafCallValues) -> LeafData { - LeafDateFormatters.evaluate(params, fixed: true) } - } - - /// Variable format localized DateFormatter strings - public struct Localized: LeafFunction, StringReturn, Invariant { - public static var callSignature: [LeafCallParameter] {[ - .init(label: "timeStamp", types: [.int, .double, .string]), - .string(labeled: "localizedFormat"), - .string(labeled: "timeZone", defaultValue: defaultTZIdentifier.leafData), - .string(labeled: "locale", defaultValue: defaultLocale.leafData) - ]} - - public func evaluate(_ params: LeafCallValues) -> LeafData { - LeafDateFormatters.evaluate(params, fixed: false) } - } - - // MARK: Internal Only - - internal struct Key: Hashable { - let format: String - let tZ: String - let locale: String? - - init(_ format: String, _ tZ: String, _ locale: String? = nil) { - self.format = format - self.tZ = tZ - self.locale = locale } - } - - internal static var iso8601: [String: ISO8601DateFormatter] = [:] - internal static var locale: [Key: DateFormatter] = [:] - - private static let lock: RWLock = .init() -} - -// MARK: Internal implementations - -internal extension LeafTimestamp.ReferenceBase { - var interval: Double { - switch self { - case .now: return Date().timeIntervalSinceReferenceDate - case .unixEpoch: return -1 * Date.timeIntervalBetween1970AndReferenceDate - case .referenceDate: return 0 - case .distantFuture: return Date.distantFuture.timeIntervalSinceReferenceDate - case .distantPast: return Date.distantPast.timeIntervalSinceReferenceDate - } - } - - static func fault(_ str: String) -> LeafData { - .error("\(str) is not a valid reference base; must be one of \(Self.terse)]") } -} - -internal extension LeafDateFormatters { - static subscript(timezone: String, fractional: Bool) -> ISO8601DateFormatter? { - get { lock.readWithLock { iso8601[timezone + (fractional ? "T" : "F")] } } - set { lock.writeWithLock { iso8601[timezone + (fractional ? "T" : "F")] = newValue } } - } - - static subscript(key: Key) -> DateFormatter? { - get { lock.readWithLock { locale[key] } } - set { lock.writeWithLock { locale[key] = newValue } } - } - - static var base: Date { - Date(timeIntervalSinceReferenceDate: LeafTimestamp.referenceBase.interval) } - - static var now: () -> LeafData = { - LeafTimestamp().evaluate(.init(["now", LeafTimestamp.referenceBase.leafData], ["since": 1])) } - - static func timeZone(_ from: String) -> TimeZone? { - if let tz = TimeZone(abbreviation: from) { - return tz } - else if let tz = TimeZone(identifier: from) { - return tz } - return nil - } - - static func evaluate(_ params: LeafCallValues, fixed: Bool) -> LeafData { - let f = params[1].string! - let z = params[2].string! - let l = params[3].string - let key = Key(f, z, l) - var formatter = Self[key] - - let timestamp = params[0] - let interval: Double - - if timestamp.isNumeric { interval = timestamp.double! } - else { - guard let t = LeafTimestamp.ReferenceBase(rawValue: timestamp.string!) else { - return LeafTimestamp.ReferenceBase.fault(timestamp.string!) } - interval = t.interval - } - - if formatter == nil { - guard let zone = TimeZone(identifier: z) else { - return .error("\(z) is not a valid time zone identifier") } - if let l = l, !Locale.availableIdentifiers.contains(l) { - return .error("\(l) is not a known locale identifier") } - formatter = DateFormatter() - formatter!.dateFormat = fixed ? f : DateFormatter.dateFormat(fromTemplate: f, options: 0, locale: Locale(identifier: l!)) - formatter!.timeZone = zone - if !fixed { formatter!.locale = Locale(identifier: l!) } - Self[key] = formatter - } - - return .string(formatter!.string(from: base.addingTimeInterval(interval))) - } -} diff --git a/Sources/LeafKit/LeafCore/TypeCasts.swift b/Sources/LeafKit/LeafCore/TypeCasts.swift deleted file mode 100644 index b2e9c026..00000000 --- a/Sources/LeafKit/LeafCore/TypeCasts.swift +++ /dev/null @@ -1,32 +0,0 @@ -extension LeafEntities { - func registerTypeCasts() { - use(Double.self, asType: "Double", storeAs: .double) - use(Int.self, asType: "Int", storeAs: .int) - use(Bool.self, asType: "Bool", storeAs: .bool) - use(String.self, asType: "String", storeAs: .string) - use([LeafData].self, asType: "Array", storeAs: .array) - use([String: LeafData].self, asType: "Dictionary", storeAs: .dictionary) - - /// See `LeafData` - use(LKDSelfMethod(), asMethod: "type") - use(LKDSelfFunction(), asFunction: "type") - } -} - -internal protocol TypeCast: LKMapMethod {} -internal extension TypeCast { func evaluate(_ params: LeafCallValues) -> LKData { params[0] } } - -internal struct BoolIdentity: TypeCast, BoolReturn { - static var callSignature: [LeafCallParameter] { [.bool] } } -internal struct IntIdentity: TypeCast, IntReturn { - static var callSignature: [LeafCallParameter] { [.int] } } -internal struct DoubleIdentity: TypeCast, DoubleReturn { - static var callSignature: [LeafCallParameter] { [.double] } } -internal struct StringIdentity: TypeCast, StringParam, StringReturn {} -internal struct ArrayIdentity: TypeCast, ArrayParam, ArrayReturn {} -internal struct DictionaryIdentity: TypeCast, DictionaryParam, DictionaryReturn {} - -internal struct DataIdentity: TypeCast, DataReturn { - static var callSignature: [LeafCallParameter] { [.data] } -} - diff --git a/Sources/LeafKit/LeafData/Encodable+LeafData.swift b/Sources/LeafKit/LeafData/Encodable+LeafData.swift deleted file mode 100644 index a31e8f40..00000000 --- a/Sources/LeafKit/LeafData/Encodable+LeafData.swift +++ /dev/null @@ -1,8 +0,0 @@ -public extension Encodable { - func encodeToLeafData() -> LeafData { - let encoder = LKEncoder() - do { try encode(to: encoder) } - catch { return .error(internal: "Could not encode \(String(describing: self)) to `LeafData`)") } - return encoder.leafData - } -} diff --git a/Sources/LeafKit/LeafData/LKDContainer.swift b/Sources/LeafKit/LeafData/LKDContainer.swift deleted file mode 100644 index ad68fa49..00000000 --- a/Sources/LeafKit/LeafData/LKDContainer.swift +++ /dev/null @@ -1,194 +0,0 @@ -import Foundation - -/// `LKDContainer` provides the tangible storage for concrete Swift values, representations of -/// collections, optional value wrappers, and lazy data generators. -internal indirect enum LKDContainer: Equatable, LKPrintable { - // MARK: - Cases - case bool(Bool) - case string(String) - case int(Int) - case double(Double) - case data(Data) - - // FIXME: Dictionary & Array should store a bool to signal homogenous/heterogenous - - /// `[String: LeafData]` - case dictionary([String: LKData]) - /// `[LeafData]` - case array([LKData]) - - /// Wrapped `Optional` - case `nil`(_ type: LKDType) - - /// Lazy resolvable `() -> LeafData` where return is of `LeafDataType` - case lazy(f: () -> LeafData, returns: LKDType) - - /// A lazy evaluation of the param - Must be generated *only* during `LKSerialize`(and - /// subsequently stored in `VarStack`) to defer evaluation - case evaluate(param: LKParameter.Container) - - case error(_ reason: String, _ function: String, _ location: SourceLocation?) - case unset - - // MARK: - Properties - - /// The LeafDataType the container will evaluate to - var baseType: LKDType { - switch self { - // Concrete Types - case .array : return .array - case .bool : return .bool - case .data : return .data - case .dictionary : return .dictionary - case .double : return .double - case .int : return .int - case .string : return .string - // Internal Wrapped Types - case .lazy(_, let t), - .nil(let t) : return t - case .evaluate : return .void - case .error : return .void - case .unset : return .void - } - } - - /// Will resolve anything but variant Lazy data (99% of everything), and unwrap optionals - var evaluate: LKData { - if case .lazy(let f, _) = self { return f() } - if self == .unset { return .error(internal: "Variable used before being initialized") } - return .init(self) - } - - // MARK: - Equatable Conformance - /// Strict equality comparision, with .nil/.void being equal - will fail on Lazy data that is variant - static func ==(lhs: Self, rhs: Self) -> Bool { - /// If either side is optional and nil... - if lhs.isNil || rhs.isNil { - /// Both sides must be nil - if lhs.isNil != rhs.isNil { return false } - /// And either side can be trueNil - if lhs.baseType == .void || rhs.baseType == .void { return true } - /// And concrete type must match - return lhs.baseType == rhs.baseType } - /// Both sides must be invariant or we won't test at all - guard (lhs.isLazy || rhs.isLazy) == false else { return false } - - /// Direct tests on two concrete values of the same concrete type - switch (lhs, rhs) { - /// Direct concrete type comparisons - case ( .array(let a), .array(let b)) : return a == b - case (.dictionary(let a), .dictionary(let b)) : return a == b - case ( .bool(let a), .bool(let b)) : return a == b - case ( .string(let a), .string(let b)) : return a == b - case ( .int(let a), .int(let b)) : return a == b - case ( .double(let a), .double(let b)) : return a == b - case ( .data(let a), .data(let b)) : return a == b - default : return false - } - } - - var description: String { short } - var short: String { - switch self { - case .array(let a) : return "array(count: \(a.count))" - case .bool(let b) : return "bool(\(b))" - case .data(let d) : return "data(\(d.count.formatBytes())" - case .dictionary(let d) : return "dictionary(count: \(d.count))" - case .double(let d) : return "double(\(d))" - case .int(let i) : return "int(\(i))" - case .lazy(_, let r) : return "lazy(() -> \(r)?)" - case .nil(let t) : return "\(t)?" - case .string(let s) : return "string(\(s))" - case .evaluate : return "evaluate(deferred)" - case .error : return "error(\(error!))" - case .unset : return "unset" - } - } - var terse: String { - switch self { - case .array(let a) : return "array(\(a.count))" - case .bool(let b) : return b.description - case .data(let d) : return "data(\(d.count.formatBytes())" - case .dictionary(let d) : return "dictionary(\(d.count))" - case .double(let d) : return d.description - case .int(let i) : return i.description - case .lazy(_, let r) : return "\(r.short.capitalized)" - case .nil(let t) : return "\(t)?" - case .string(let s) : return "\"\(s)\"" - case .error : return error! - default : return "()" - } - } - - // MARK: - Other - var isNil: Bool { if case .nil = self { return true } else { return false } } - var isLazy: Bool { if case .lazy = self { return true } else { return false } } - - /// Nil if not errored, or errored function/reason - var error: String? { - guard case .error(let r, let f, let l) = self else { return nil } - return (l.map { "Serialize Error in template \"\($0.0)\" - \($0.1):\($0.2)" } ?? "") + "\(f): \(r)" - } - var isUnset: Bool { self == .unset } - - - var state: LKDState { - var state: LKDState - if case .error = self { return .error } - - switch baseType { - case .array : state = .array - case .bool : state = .bool - case .data : state = .data - case .dictionary : state = .dictionary - case .double : state = .double - case .int : state = .int - case .string : state = .string - case .void : state = .void - } - switch self { - case .lazy : state.formUnion(.variant) - case .nil : state.formUnion(.nil) - case .unset : state.formUnion(.variant) - default : break - } - return state - } -} - -/// Various conveniences for bit ops on LKDContainers -/// -/// Note: rawValue of 0 is implicit `Error` type -internal struct LKDState: OptionSet { - let rawValue: UInt16 - init(rawValue: UInt16) { self.rawValue = rawValue } - - /// Top 4 bits for container case - static let celfMask = Self(rawValue: 0xF000) - static let _void = Self(rawValue: 1 << 12) - static let _bool = Self(rawValue: 2 << 12) - static let _int = Self(rawValue: 3 << 12) - static let _double = Self(rawValue: 4 << 12) - static let _string = Self(rawValue: 5 << 12) - static let _array = Self(rawValue: 6 << 12) - static let _dictionary = Self(rawValue: 7 << 12) - static let _data = Self(rawValue: 8 << 12) - - static let numeric = Self(rawValue: 1 << 0) - static let comparable = Self(rawValue: 1 << 1) - static let collection = Self(rawValue: 1 << 2) - static let variant = Self(rawValue: 1 << 3) - static let `nil` = Self(rawValue: 1 << 4) - - static let error: Self = Self(rawValue: 0) - - static let void: Self = [_void] - static let bool: Self = [_bool, comparable] - static let int: Self = [_int, comparable, numeric] - static let double: Self = [_double, comparable, numeric] - static let string: Self = [_string, comparable] - static let array: Self = [_array, collection] - static let dictionary: Self = [_dictionary, collection] - static let data: Self = [_data] - static let trueNil: Self = [_void, `nil`] -} diff --git a/Sources/LeafKit/LeafData/LKDConverters.swift b/Sources/LeafKit/LeafData/LKDConverters.swift deleted file mode 100644 index e85b4bbb..00000000 --- a/Sources/LeafKit/LeafData/LKDConverters.swift +++ /dev/null @@ -1,121 +0,0 @@ -// MARK: Stable?!! - -import Foundation - -// MARK: - Data Converter Static Mapping - -/// Stages of convertibility -internal enum LKDConversion: UInt8, Hashable, Comparable { - /// Not implicitly convertible automatically - case ambiguous = 0 - /// A coercion with a clear meaning in one direction - case coercible = 1 - /// A conversion with a well-defined bi-directional casting possibility - case castable = 2 - /// An exact type match; identity - case identity = 3 - - static func <(lhs: Self, rhs: Self) -> Bool { lhs.rawValue < rhs.rawValue } -} - -/// Map of functions for converting between concrete, non-nil LeafData -/// -/// Purely for pass-through identity, casting, or coercing between the concrete types (Bool, Int, Double, -/// String, Array, Dictionary, Data) and will never attempt to handle optionals, which must *always* -/// be unwrapped to concrete types before being called. -/// -/// Converters are guaranteed to be provided non-nil input. Failable converters must return LeafData.trueNil -internal enum LKDConverters { - typealias ArrayMap = (`is`: LKDConversion, via: ([LKData]) -> LKData) - static let arrayMaps: [LKDType: ArrayMap] = [ - .array : (is: .identity, via: { .array($0) }), - - .bool : (is: .coercible, via: { _ in .bool(true) }), - - .data : (is: .ambiguous, via: { _ in .trueNil }), - .double : (is: .ambiguous, via: { _ in .trueNil }), - .dictionary : (is: .ambiguous, via: { _ in .trueNil }), - .int : (is: .ambiguous, via: { _ in .trueNil }), - .string : (is: .ambiguous, via: { _ in .trueNil }) - ] - - typealias BoolMap = (`is`: LKDConversion, via: (Bool) -> LKData) - static let boolMaps: [LKDType: BoolMap] = [ - .bool : (is: .identity, via: { .bool($0) }), - - .double : (is: .castable, via: { .double($0 ? 1.0 : 0.0) }), - .int : (is: .castable, via: { .int($0 ? 1 : 0) }), - .string : (is: .castable, via: { .string($0.description) }), - - .array : (is: .ambiguous, via: { _ in .trueNil }), - .data : (is: .ambiguous, via: { _ in .trueNil }), - .dictionary : (is: .ambiguous, via: { _ in .trueNil }) - ] - - typealias DataMap = (`is`: LKDConversion, via: (Data) -> LKData) - static let dataMaps: [LKDType: DataMap] = [ - .data : (is: .identity, via: { .data($0) }), - - .bool : (is: .coercible, via: { _ in .bool(true) }), - - .array : (is: .ambiguous, via: { _ in .trueNil }), - .dictionary : (is: .ambiguous, via: { _ in .trueNil }), - .double : (is: .ambiguous, via: { _ in .trueNil }), - .int : (is: .ambiguous, via: { _ in .trueNil }), - .string : (is: .ambiguous, via: { _ in .trueNil }) - ] - - typealias DictionaryMap = (`is`: LKDConversion, via: ([String: LKData]) -> LKData) - static let dictionaryMaps: [LKDType: DictionaryMap] = [ - .dictionary : (is: .identity, via: { .dictionary($0) }), - - .bool : (is: .coercible, via: { _ in .bool(true) }), - - .array : (is: .ambiguous, via: { _ in .trueNil }), - .data : (is: .ambiguous, via: { _ in .trueNil }), - .double : (is: .ambiguous, via: { _ in .trueNil }), - .int : (is: .ambiguous, via: { _ in .trueNil }), - .string : (is: .ambiguous, via: { _ in .trueNil }) - ] - - typealias DoubleMap = (`is`: LKDConversion, via: (Double) -> LKData) - static let doubleMaps: [LKDType: DoubleMap] = [ - .double : (is: .identity, via: { $0.leafData }), - - .bool : (is: .castable, via: { .bool($0 != 0.0) }), - .string : (is: .castable, via: { .string($0.description) }), - - .int : (is: .coercible, via: { .int(Int(exactly: $0.rounded())) }), - - .array : (is: .ambiguous, via: { _ in .trueNil }), - .data : (is: .ambiguous, via: { _ in .trueNil }), - .dictionary : (is: .ambiguous, via: { _ in .trueNil }), - ] - - typealias IntMap = (`is`: LKDConversion, via: (Int) -> LKData) - static let intMaps: [LKDType: IntMap] = [ - .int : (is: .identity, via: { $0.leafData }), - - .bool : (is: .castable, via: { .bool($0 != 0) }), - .double : (is: .castable, via: { .double(Double($0)) }), - .string : (is: .castable, via: { .string($0.description) }), - - .array : (is: .ambiguous, via: { _ in .trueNil }), - .data : (is: .ambiguous, via: { _ in .trueNil }), - .dictionary : (is: .ambiguous, via: { _ in .trueNil }), - ] - - typealias StringMap = (`is`: LKDConversion, via: (String) -> LKData) - static let stringMaps: [LKDType: StringMap] = [ - .string : (is: .identity, via: { $0.leafData }), - - .bool : (is: .castable, via: { - .bool(LeafKeyword(rawValue: $0.lowercased())?.bool ?? true) }), - .double : (is: .castable, via: { .double(Double($0)) }), - .int : (is: .castable, via: { .int(Int($0)) } ), - - .array : (is: .ambiguous, via: { _ in .trueNil }), - .data : (is: .ambiguous, via: { _ in .trueNil }), - .dictionary : (is: .ambiguous, via: { _ in .trueNil }), - ] -} diff --git a/Sources/LeafKit/LeafData/LeafData.swift b/Sources/LeafKit/LeafData/LeafData.swift index 92ee111f..a88309f3 100644 --- a/Sources/LeafKit/LeafData/LeafData.swift +++ b/Sources/LeafKit/LeafData/LeafData.swift @@ -1,32 +1,67 @@ -import Foundation +// MARK: Subject to change prior to 1.0.0 release +// MARK: - -// MARK: - LeafData Public Definition +import Foundation /// `LeafData` is a "pseudo-protocol" wrapping the physically storable Swift data types /// Leaf can use directly /// - `(Bool, Int, Double, String, Array, Dictionary, Data)` are the inherent root types /// supported, all of which may also be representable as `Optional` values. -/// - `CaseType` presents these cases plus `Void` as a case for functional `LeafSymbols` +/// - `NaturalType` presents these cases plus `Void` as a case for functional `LeafSymbols` /// - `nil` is creatable, but only within context of a root base type - eg, `.nil(.bool)` == `Bool?` -public struct LeafData: LeafDataRepresentable, Equatable { - - // MARK: - Stored Properties - - /// Concrete stored type of the data - public let storedType: LeafDataType - /// Actual storage - let container: LKDContainer - /// State storage flags - let state: LKDState - - // MARK: - Custom String Convertible Conformance - public var description: String { container.description } - +public struct LeafData: CustomStringConvertible, + Equatable, + ExpressibleByDictionaryLiteral, + ExpressibleByStringLiteral, + ExpressibleByIntegerLiteral, + ExpressibleByBooleanLiteral, + ExpressibleByArrayLiteral, + ExpressibleByFloatLiteral, + ExpressibleByNilLiteral { + + /// The concrete instantiable object types for a `LeafData` + public enum NaturalType: String, CaseIterable, Hashable { + case bool + case string + case int + case double + case data + case dictionary + case array + case void + } + /// The case-self identity + public var celf: NaturalType { storage.concreteType! } + + /// Returns `true` if the data is `nil` or `void`. + public var isNil: Bool { storage.isNil } + /// Returns `true` if the data can hold other data - we don't consider `Optional` for this purpose + public var isCollection: Bool { [.array, .dictionary].contains(storage.concreteType) } + + /// Returns `true` if concrete object can be exactly or losslessly cast to a second type + /// - EG: `.nil ` -> `.string("")`, `.int(1)` -> `.double(1.0)`, + /// `.bool(true)` -> `.string("true")` are all one-way lossless conversions + /// - This does not imply it's not possible to *coerce* data - handle with `coerce(to:)` + /// EG: `.string("")` -> `.nil`, `.string("1")` -> ` .bool(true)` + public func isCastable(to type: LeafData.NaturalType) -> Bool { + let conversion = _ConverterMap.symbols.get(storage.concreteType!, type)! + return conversion.is >= DataConvertible.castable + } + + /// Returns `true` if concrete object is potentially directly coercible to a second type in some way + /// - EG: `.array()` -> `.dictionary()` where array indices become keys + /// or `.int(1)` -> `.bool(true)` + /// - This does *not* validate the data itself in coercion + public func isCoercible(to type: LeafData.NaturalType) -> Bool { + let conversion = _ConverterMap.symbols.get(storage.concreteType!, type)! + return conversion.is >= DataConvertible.coercible + } + // MARK: - Equatable Conformance public static func ==(lhs: LeafData, rhs: LeafData) -> Bool { // Strict compare of invariant stored values; considers .nil & .void equal - guard !(lhs.container == rhs.container) else { return true } - // If either side is nil, false - container == would have returned false + guard !(lhs.storage == rhs.storage) else { return true } + // If either side is nil, false - storage == would have returned false guard !lhs.isNil && !rhs.isNil else { return false } // - Lazy variant data should never be tested due to potential side-effects guard lhs.invariant && rhs.invariant else { return false } @@ -36,250 +71,437 @@ public struct LeafData: LeafDataRepresentable, Equatable { let lhs = lhs.string, let rhs = rhs.string else { return false } return lhs == rhs } + + // MARK: - CustomStringConvertible + public var description: String { storage.description } + public var short: String { storage.short } + + /// Returns `true` if the object has a single uniform type + /// - Always true for invariant non-containers + /// - True or false for containers if determinable + /// - Nil if the object is variant lazy data, or invariant lazy producing a container, or a container holding such + public var hasUniformType: Bool? { + // Default case - anything that doesn't return a container + if !isCollection { return true } + // A container-returning lazy (unknowable) - specific test to avoid invariant check + if storage.isLazy && isCollection { return nil } + // A non-lazy container - somewhat expensive to check + if case .array(let a) = storage { + guard a.count > 1, let first = a.first?.concreteType else { return true } + return a.allSatisfy { $0.celf == first && $0.hasUniformType ?? false } + } else if case .dictionary(let d) = storage { + guard d.count > 1, let first = d.values.first?.concreteType else { return true } + return d.values.allSatisfy { $0.celf == first && $0.hasUniformType ?? false } + } else { return nil } + } + + /// Returns the uniform type of the object, or nil if it can't be determined/is a non-uniform container + public var uniformType: NaturalType? { + guard let determinable = hasUniformType, determinable else { return nil } + if !isCollection { return storage.concreteType } + if case .array(let a) = storage { + return a.isEmpty ? .void : a.first?.concreteType ?? nil + } else if case .dictionary(let d) = storage { + return d.values.isEmpty ? .void : d.values.first?.concreteType ?? nil + } else { return nil } + } + + // MARK: - Generic `LeafDataRepresentable` Initializer + public init(_ leafData: LeafDataRepresentable) { self = leafData.leafData } - // MARK: - State - - /// Returns `true` if concrete object can be exactly or losslessly cast to a second type - /// - EG: `.nil ` -> `.string("")`, `.int(1)` -> `.double(1.0)`, - /// `.bool(true)` -> `.string("true")` are all one-way lossless conversions - /// - This does not imply it's not possible to *coerce* data - handle with `coerce(to:)` - /// EG: `.string("")` -> `.nil`, `.string("1")` -> ` .bool(true)` - public func isCastable(to type: LeafDataType) -> Bool { storedType.casts(to: type) >= .castable } + // MARK: - Static Initializer Conformances + /// Creates a new `LeafData` from a `Bool`. + public static func bool(_ value: Bool?) -> LeafData { + return value.map { LeafData(.bool($0)) } ?? LeafData(.optional(nil, .bool)) + } + /// Creates a new `LeafData` from a `String`. + public static func string(_ value: String?) -> LeafData { + return value.map { LeafData(.string($0)) } ?? LeafData(.optional(nil, .string)) + } + /// Creates a new `LeafData` from am `Int`. + public static func int(_ value: Int?) -> LeafData { + return value.map { LeafData(.int($0)) } ?? LeafData(.optional(nil, .int)) + } + /// Creates a new `LeafData` from a `Double`. + public static func double(_ value: Double?) -> LeafData { + return value.map { LeafData(.double($0)) } ?? LeafData(.optional(nil, .double)) + } + /// Creates a new `LeafData` from `Data`. + public static func data(_ value: Data?) -> LeafData { + return value.map { LeafData(.data($0)) } ?? LeafData(.optional(nil, .data)) + } + /// Creates a new `LeafData` from `[String: LeafData]`. + public static func dictionary(_ value: [String: LeafData]?) -> LeafData { + return value.map { LeafData(.dictionary($0)) } ?? LeafData(.optional(nil, .dictionary)) + } + /// Creates a new `LeafData` from `[LeafData]`. + public static func array(_ value: [LeafData]?) -> LeafData { + return value.map { LeafData(.array($0)) } ?? LeafData(.optional(nil, .array)) + } + /// Creates a new `LeafData` for `Optional` + public static func `nil`(_ type: LeafData.NaturalType) -> LeafData { + return .init(.optional(nil, type)) + } - /// Returns `true` if concrete object is potentially directly coercible to a second type in some way - /// - EG: `.array()` -> `.dictionary()` where array indices become keys - /// or `.int(1)` -> `.bool(true)` - /// - This does *not* validate the data itself in coercion - public func isCoercible(to type: LeafDataType) -> Bool { storedType.casts(to: type) >= .coercible } + // MARK: - Literal Initializer Conformances + public init(nilLiteral: ()) { self = .trueNil } + public init(stringLiteral value: StringLiteralType) { self = value.leafData } + public init(integerLiteral value: IntegerLiteralType) { self = value.leafData } + public init(floatLiteral value: FloatLiteralType) { self = value.leafData } + public init(booleanLiteral value: BooleanLiteralType) { self = value.leafData } + public init(arrayLiteral elements: LeafData...) { self = .array(elements) } + public init(dictionaryLiteral elements: (String, LeafData)...) { + self = .dictionary(.init(uniqueKeysWithValues: elements)) + } - // MARK: - LeafDataRepresentable - public var leafData: LeafData { self } - - // MARK: - Swift Type Extraction + // MARK: - Fuzzy Conversions from Storage to Types - /// Attempts to convert to `Bool`: if a nil optional Bool, returns `nil` - returns t/f if bool-evaluated. - /// Anything that is tangible but not evaluable to bool reports on its optional-ness as truth. + /// Attempts to convert to `Bool` or returns `nil`. public var bool: Bool? { - if case .bool(let b) = container { return b } - guard case .bool(let b) = coerce(to: .bool).container else { return nil } + if case .bool(let b) = storage { return b } + guard case .bool(let b) = convert(to: .bool).storage else { return nil } return b } /// Attempts to convert to `String` or returns `nil`. public var string: String? { - if case .string(let s) = container { return s } - guard case .string(let s) = coerce(to: .string).container else { return nil } + if case .string(let s) = storage { return s } + guard case .string(let s) = convert(to: .string).storage else { return nil } return s } /// Attempts to convert to `Int` or returns `nil`. public var int: Int? { - if case .int(let i) = container { return i } - guard case .int(let i) = coerce(to: .int).container else { return nil } + if case .int(let i) = storage { return i } + guard case .int(let i) = convert(to: .int).storage else { return nil } return i } /// Attempts to convert to `Double` or returns `nil`. public var double: Double? { - if case .double(let d) = container { return d } - guard case .double(let d) = coerce(to: .double).container else { return nil } + if case .double(let d) = storage { return d } + guard case .double(let d) = convert(to: .double).storage else { return nil } return d } /// Attempts to convert to `Data` or returns `nil`. public var data: Data? { - if case .data(let d) = container { return d } - guard case .data(let d) = coerce(to: .data).container else { return nil } + if case .data(let d) = storage { return d } + guard case .data(let d) = convert(to: .data).storage else { return nil } return d } /// Attempts to convert to `[String: LeafData]` or returns `nil`. public var dictionary: [String: LeafData]? { - if case .dictionary(let d) = container { return d } - guard case .dictionary(let d) = coerce(to: .dictionary).container else { return nil } + if case .dictionary(let d) = storage { return d } + guard case .dictionary(let d) = convert(to: .dictionary).storage else { return nil } return d } /// Attempts to convert to `[LeafData]` or returns `nil`. public var array: [LeafData]? { - if case .array(let a) = container { return a } - guard case .array(let a) = coerce(to: .array).container else { return nil } + if case .array(let a) = storage { return a } + guard case .array(let a) = convert(to: .array).storage else { return nil } return a } - + /// For convenience, `trueNil` is stored as `.optional(nil, .void)` - public static let trueNil: LeafData = .nil(.void) -} - -// MARK: - Public Initializers -extension LeafData: ExpressibleByDictionaryLiteral, - ExpressibleByStringLiteral, - ExpressibleByIntegerLiteral, - ExpressibleByBooleanLiteral, - ExpressibleByArrayLiteral, - ExpressibleByFloatLiteral, - ExpressibleByNilLiteral, - ExpressibleByStringInterpolation { - // MARK: Generic `LeafDataRepresentable` Initializer - public init(_ leafData: LeafDataRepresentable) { self = leafData.leafData } - - // MARK: Static Initializer Conformances - /// Creates a new `LeafData` from a `Bool`. - public static func bool(_ value: Bool?) -> Self { - value.map { Self(.bool($0)) } ?? .nil(.bool) } - /// Creates a new `LeafData` from a `String`. - public static func string(_ value: String?) -> Self { - value.map { Self(.string($0)) } ?? .nil(.string) } - /// Creates a new `LeafData` from am `Int`. - public static func int(_ value: Int?) -> Self { - value.map { Self(.int($0)) } ?? .nil(.int) } - /// Creates a new `LeafData` from a `Double`. - public static func double(_ value: Double?) -> Self { - value.map { Self(.double($0)) } ?? .nil(.double) } - /// Creates a new `LeafData` from `Data`. - public static func data(_ value: Data?) -> Self { - value.map { Self(.data($0)) } ?? .nil(.data) } - /// Creates a new `LeafData` from `[String: LeafData]`. - public static func dictionary(_ value: [String: LeafData]?) -> Self { - value.map { Self(.dictionary($0)) } ?? .nil(.dictionary) } - /// Creates a new `LeafData` from `[String: LeafDataRepresentable]`. - public static func dictionary(_ value: [String: LeafDataRepresentable]?) -> Self { - dictionary(value?.mapValues { $0.leafData }) } - /// Creates a new `LeafData` from `[LeafData]`. - public static func array(_ value: [LeafData]?) -> Self { - value.map { Self(.array($0)) } ?? .nil(.array) } - /// Creates a new `LeafData` from `[LeafDataRepresentable]`. - public static func array(_ value: [LeafDataRepresentable]?) -> Self { - array(value?.map {$0.leafData}) } - /// Creates a new `LeafData` for `Optional` - public static func `nil`(_ type: LeafDataType) -> Self { - Self(.nil(type)) } + public static var trueNil: LeafData { .init(.optional(nil, .void)) } + + public func cast(to: LeafData.NaturalType) -> LeafData { convert(to: to, .castable) } + public func coerce(to: LeafData.NaturalType) -> LeafData { convert(to: to, .coercible) } + + // MARK: - Internal Only - public static func error(_ reason: String, - function: String = #function) -> Self { - Self(.error(reason, function, nil)) + /// Actual storage. + internal private(set) var storage: LeafDataStorage + + // MARK: - LeafSymbol Conformance + internal var resolved: Bool { storage.resolved } + internal var invariant: Bool { storage.invariant } + internal var symbols: Set { .init() } + internal var isAtomic: Bool { true } + internal var isExpression: Bool { false } + internal var isConcrete: Bool { false } + internal var isAny: Bool { true } + internal var concreteType: NaturalType? { nil } + internal func resolve() -> LeafData { + LeafData(storage.resolve()) } - - // MARK: Literal Initializer Conformances - public init(nilLiteral: ()) { self = .trueNil } - public init(stringLiteral value: StringLiteralType) { self = value.leafData } - public init(integerLiteral value: IntegerLiteralType) { self = value.leafData } - public init(floatLiteral value: FloatLiteralType) { self = value.leafData } - public init(booleanLiteral value: BooleanLiteralType) { self = value.leafData } - public init(arrayLiteral elements: LeafData...) { self = .array(elements) } - public init(dictionaryLiteral elements: (String, LeafData)...) { - self = .dictionary(.init(uniqueKeysWithValues: elements)) } -} - -// MARK: - Internal Only -extension LeafData: LKSymbol { - /// Creates a new `LeafData`. - init(_ raw: LKDContainer) { - self.container = raw - self.storedType = raw.baseType - self.state = raw.state + internal func serialize() throws -> String? { + try storage.serialize() + } + internal func serialize(buffer: inout ByteBuffer) throws { + try storage.serialize(buffer: &buffer) } - static func error(internal reason: String, - _ function: String = "LeafKit", - _ location: SourceLocation? = nil) -> Self { - Self(.error(reason, "LeafKit", location)) + // Hard resolve data (remove invariants), remaining optional if nil + internal var evaluate: LeafData { + if case .lazy(let f, _, _) = self.storage { return f() } + if case .dictionary(let d) = self.storage { + return .dictionary(d.mapValues { $0.evaluate }) + } + if case .array(let a) = self.storage { + return .array(a.map { $0.evaluate }) + } + return self } + + /// Creates a new `LeafData`. + internal init(_ storage: LeafDataStorage) { self.storage = storage } /// Creates a new `LeafData` from `() -> LeafData` if possible or `nil` if not possible. - /// `returns` must specify a `CaseType` that the function will return - static func lazy(_ lambda: @escaping () -> LeafData, returns type: LKDType) -> Self { - Self(.lazy(f: lambda, returns: type)) + /// `returns` must specify a `NaturalType` that the function will return + internal static func lazy(_ lambda: @escaping () -> LeafData, + returns type: LeafData.NaturalType, + invariant sideEffects: Bool) throws -> LeafData { + LeafData(.lazy(f: lambda, returns: type, invariant: sideEffects)) } - var errored: Bool { state.rawValue == 0 } - var error: String? { !errored ? nil : container.error! } - - /// Note - we don't consider an optional container true for this purpose - var isTrueNil : Bool { state == .trueNil } - var isCollection : Bool { state.contains(.collection) } - var isNil : Bool { state.contains(.nil) } - var isNumeric : Bool { state.contains(.numeric) } - var isComparable : Bool { state.contains(.comparable) } - var isLazy : Bool { state.contains(.variant) } - - /// Returns `true` if the object has a single uniform type - /// - Always true for invariant non-containers - /// - True or false for containers if determinable - /// - Nil if the object is variant lazy data, or invariant lazy producing a container, or a container holding such - var hasUniformType: Bool? { !isCollection ? true : uniformType.map {_ in true } ?? false } - - /// Returns the uniform type of the object, or nil if it can't be determined/is a non-uniform container - var uniformType: LKDType? { - // Default case - anything that doesn't return a container, or lazy containers - if !isCollection { return storedType } else if isLazy { return nil } - // A non-lazy container - somewhat expensive to check. 0 or 1 element - // is always uniform of that type. Only 1 layer deep, considers collection - // elements, even if all the same type, unequal - if case .array(let a) = container { - guard a.count > 1 else { return a.first?.storedType } - if a.first!.isCollection { return nil } - let types = a.reduce(into: Set.init(), { $0.insert($1.storedType) }) - return types.count == 1 ? types.first! : nil - } else if case .dictionary(let d) = container { - guard d.count > 1 else { return d.values.first?.storedType } - if d.values.first!.isCollection { return nil } - let types = d.values.reduce(into: Set.init(), { $0.insert($1.storedType) }) - return types.count == 1 ? types.first! : nil - } else { return nil } - } - - func cast(to: LKDType) -> Self { convert(to: to, .castable) } - func coerce(to: LKDType) -> Self { convert(to: to, .coercible) } - - /// Try to convert one concrete object to a second type. Special handling for optional converting to bool. - func convert(to output: LKDType, _ level: LKDConversion = .castable) -> Self { - typealias _Map = LKDConverters - /// If celf is identity, return directly if invariant or return lazy evaluation - if storedType == output { return invariant ? self : container.evaluate } - /// If nil, no casting is possible between types *Except* special case of void -> bool(false) - if isNil { return output == .bool && storedType == .void ? .bool(false) : .trueNil } - - let input = !container.isLazy ? container : container.evaluate.container + /// Try to convert one concrete object to a second type. + internal func convert(to output: NaturalType, _ level: DataConvertible = .castable) -> LeafData { + guard celf != output else { return self } + if case .lazy(let f,_,_) = self.storage { return f().convert(to: output, level) } + guard let input = storage.unwrap, + let conversion = _ConverterMap.symbols.get(input.concreteType!, output), + conversion.is >= level else { return nil } switch input { - case .array(let a) : let m = _Map.arrayMaps[output]! - return m.is >= level ? m.via(a) : .trueNil - case .bool(let b) : let m = _Map.boolMaps[output]! - return m.is >= level ? m.via(b) : .trueNil - case .data(let d) : let m = _Map.dataMaps[output]! - return m.is >= level ? m.via(d) : .trueNil - case .dictionary(let d) : let m = _Map.dictionaryMaps[output]! - return m.is >= level ? m.via(d) : .trueNil - case .double(let d) : let m = _Map.doubleMaps[output]! - return m.is >= level ? m.via(d) : .trueNil - case .int(let i) : let m = _Map.intMaps[output]! - return m.is >= level ? m.via(i) : .trueNil - case .string(let s) : let m = _Map.stringMaps[output]! - return m.is >= level ? m.via(s) : .trueNil - default : return .trueNil + case .array(let any as Any), + .bool(let any as Any), + .data(let any as Any), + .dictionary(let any as Any), + .double(let any as Any), + .int(let any as Any), + .string(let any as Any): return conversion.via(any) + default: return nil } } - - // MARK: - LKSymbol Conformance - var short: String { container.short } - var resolved: Bool { !state.contains(.variant) } - var invariant: Bool { !state.contains(.variant) } - var symbols: Set { [] } - - func resolve(_ symbols: inout LKVarStack) -> Self { self } - func evaluate(_ symbols: inout LKVarStack) -> Self { state.contains(.variant) ? container.evaluate : self } } -internal protocol LKDSelf: LeafNonMutatingMethod, Invariant, StringReturn {} -internal extension LKDSelf { - func evaluate(_ params: LeafCallValues) -> LeafData { - .string("\(params[0].storedType.short.capitalized)\(params[0].isNil ? "?" : "")") } +// MARK: - Data Converter Static Mapping + +/// Stages of convertibility +internal enum DataConvertible: Int, Equatable, Comparable { + /// Not implicitly convertible automatically + case ambiguous = 0 + /// A coercioni with a clear meaning in one direction + case coercible = 1 + /// A conversion with a well-defined bi-directional casting possibility + case castable = 2 + /// An exact type match; identity + case identity = 3 + + static func < (lhs: DataConvertible, rhs: DataConvertible) -> Bool { + lhs.rawValue < rhs.rawValue + } } -internal struct LKDSelfMethod: LKDSelf { - static var callSignature: [LeafCallParameter] { [.init(types: .any, optional: true)] } +/// Wrapper for associating types and conversion tuple +fileprivate struct Converter: Equatable, Hashable { + typealias Conversion = (is: DataConvertible, via: (Any) -> LeafData) + + let from: LeafData.NaturalType + let to: LeafData.NaturalType + let conversion: Conversion? + + static func == (lhs: Converter, rhs: Converter) -> Bool { + (lhs.from == rhs.from) && (lhs.to == rhs.to) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(from) + hasher.combine(to) + } + + /// Full initializer + init(_ from: LeafData.NaturalType, _ to: LeafData.NaturalType, + `is`: DataConvertible, via: @escaping (Any) -> LeafData) { + self.from = from + self.to = to + self.conversion = (`is`, via) + } + + /// Initializer for the "key" only + init(_ from: LeafData.NaturalType, _ to: LeafData.NaturalType) { + self.from = from + self.to = to + self.conversion = nil + } } -internal struct LKDSelfFunction: LKDSelf { - static var callSignature: [LeafCallParameter] { [.init(label: "of", types: .any, optional: true)] } +fileprivate extension Set where Element == Converter { + func get(_ from: LeafData.NaturalType, _ to: LeafData.NaturalType) -> Converter.Conversion? { + self.first(where: { $0 == .init(from, to) })?.conversion + } +} +/// Map of functions for converting between concrete, non-nil LeafData +/// +/// Purely for pass-through identity, casting, or coercing between the concrete types (Bool, Int, Double, +/// String, Array, Dictionary, Data) and will never attempt to handle optionals, which must *always* +/// be unwrapped to concrete types before being called. +/// +/// Converters are guaranteed to be provided non-nil input. Failable converters must return LeafData.trueNil +fileprivate enum _ConverterMap { + private static let c = LeafConfiguration.self + fileprivate static var symbols: Set { [ + // MARK: - .identity (Passthrough) + Converter(.array , .array , is: .identity, via: { .array($0 as? [LeafData]) }), + Converter(.bool , .bool , is: .identity, via: { .bool($0 as? Bool) }), + Converter(.data , .data , is: .identity, via: { .data($0 as? Data) }), + Converter(.dictionary, .dictionary, is: .identity, via: { .dictionary($0 as? [String : LeafData]) }), + Converter(.double , .double , is: .identity, via: { .double($0 as? Double) }), + Converter(.int , .int , is: .identity, via: { .int($0 as? Int) }), + Converter(.string , .string , is: .identity, via: { .string($0 as? String) }), + + // MARK: - .castable (Well-defined bi-directional conversions) + // Double in [0,1] == truthiness & value + Converter(.double , .bool , is: .castable, via: { + ($0 as? Double).map { [0.0, 1.0].contains($0) ? $0 == 1.0 : nil}? + .map { .bool($0) } ?? .trueNil + }), + // Int in [0,1] == truthiness & value + Converter(.int , .bool , is: .castable, via: { + ($0 as? Int).map { [0, 1].contains($0) ? $0 == 1 : nil }? + .map { .bool($0) } ?? .trueNil + }), + // String == "true" || "false" + Converter(.string , .bool , is: .castable, via: { + ($0 as? String).map { Bool($0) }?.map { .bool($0) } ?? .trueNil + }), + // True = 1; False = 0 + Converter(.bool , .double , is: .castable, via: { + ($0 as? Bool).map { $0 ? 1.0 : 0.0 }.map { .double($0) } ?? .trueNil + }), + // Direct conversion + Converter(.int , .double , is: .castable, via: { + ($0 as? Int).map { Double($0) }.map { .double($0) } ?? .trueNil + }), + // Using default string-init + Converter(.string , .double , is: .castable, via: { + ($0 as? String).map { Double($0) }?.map { .double($0) } ?? .trueNil + }), + // True = 1; False = 0 + Converter(.bool , .int , is: .castable, via: { + ($0 as? Bool).map { $0 ? 1 : 0 }.map { .int($0) } ?? .trueNil + }), + // Base10 formatted Strings + Converter(.string , .int , is: .castable, via: { + ($0 as? String).map { Int($0) }?.map { .int($0) } ?? .trueNil + }), + // .description + Converter(.bool , .string , is: .castable, via: { + ($0 as? Bool).map { $0.description }.map { .string($0) } ?? .trueNil + }), + // Using configured encoding + Converter(.data , .string , is: .castable, via: { + ($0 as? Data).map { String(data: $0, encoding: c.encoding) }? + .map { .string($0) } ?? .trueNil + }), + // .description + Converter(.double , .string , is: .castable, via: { + ($0 as? Double).map { $0.description }.map { .string($0) } ?? .trueNil + }), + // .description + Converter(.int , .string , is: .castable, via: { + ($0 as? Int).map { $0.description }.map { .string($0) } ?? .trueNil + }), + + // MARK: - .coercible (One-direction defined conversion) + + // Array.isEmpty == truthiness + Converter(.array , .bool , is: .coercible, via: { + ($0 as? [LeafData]).map { $0.isEmpty }.map { .bool($0) } ?? .trueNil + }), + // Data.isEmpty == truthiness + Converter(.data , .bool , is: .coercible, via: { + ($0 as? Data).map { $0.isEmpty }.map { .bool($0) } ?? .trueNil + }), + // Dictionary.isEmpty == truthiness + Converter(.dictionary , .bool , is: .coercible, via: { + ($0 as? [String: LeafData]).map { $0.isEmpty }.map { .bool($0) } ?? .trueNil + }), + // Use the configured formatter + Converter(.array , .data , is: .coercible, via: { + ($0 as? [LeafData]).map { + try? LeafDataStorage.array($0).serialize()?.data(using: c.encoding) + }?.map { .data($0) } ?? .trueNil + }), + // Use the configured formatter + Converter(.bool , .data , is: .coercible, via: { + ($0 as? Bool).map { c.boolFormatter($0).data(using: c.encoding) }? + .map { .data($0) } ?? .trueNil + }), + // Use the configured formatter + Converter(.dictionary , .data , is: .coercible, via: { + ($0 as? [String: LeafData]).map { + try? LeafDataStorage.dictionary($0).serialize()?.data(using: c.encoding) + }?.map { .data($0) } ?? .trueNil + }), + // Use the configured formatter + Converter(.double , .data , is: .coercible, via: { + ($0 as? Double).map { + c.doubleFormatter($0) + .data(using: c.encoding) + }?.map { .data($0) } ?? .trueNil + }), + // Use the configured formatter + Converter(.int , .data , is: .coercible, via: { + ($0 as? Int).map { c.intFormatter($0) + .data(using: c.encoding) + }?.map { .data($0) } ?? .trueNil + }), + // Use the configured formatter + Converter(.string , .data , is: .coercible, via: { + ($0 as? String).map { c.stringFormatter($0) + .data(using: c.encoding) + }?.map { .data($0) } ?? .trueNil + }), + // Schoolbook rounding + Converter(.double , .int , is: .coercible, via: { + ($0 as? Double).map { Int(exactly: $0.rounded()) }?.map { .int($0) } ?? .trueNil + }), + + // FIXME: Questionable coercion possibilities - Currently disabled + + // Transform with array indices as keys + Converter(.array , .dictionary , is: .ambiguous, via: { + ($0 as? [LeafData]).map { + Dictionary(uniqueKeysWithValues: $0.enumerated().map { + (String($0), $1) }) } + .map { .dictionary($0) } ?? .trueNil + }), + // Conversion using the formatter + Converter(.array , .string , is: .ambiguous, via: { + ($0 as? [LeafData]).map { + let stringified: String? = try? LeafData.array($0).serialize() + return .string(stringified) + } ?? .trueNil + }), + // Conversion using the formatter + Converter(.dictionary , .string , is: .ambiguous, via: { + ($0 as? [String: LeafData]).map { + let stringified: String? = try? LeafData.dictionary($0).serialize() + return .string(stringified) + } ?? .trueNil + }), + + // MARK: - .ambiguous (Unconvertible) + Converter(.bool , .array, is: .ambiguous, via: { _ in nil }), + Converter(.data , .array, is: .ambiguous, via: { _ in nil }), + Converter(.dictionary, .array, is: .ambiguous, via: { _ in nil }), + Converter(.double , .array, is: .ambiguous, via: { _ in nil }), + Converter(.int , .array, is: .ambiguous, via: { _ in nil }), + Converter(.string , .array, is: .ambiguous, via: { _ in nil }), + Converter(.bool , .dictionary, is: .ambiguous, via: { _ in nil }), + Converter(.data , .dictionary, is: .ambiguous, via: { _ in nil }), + Converter(.double , .dictionary, is: .ambiguous, via: { _ in nil }), + Converter(.int , .dictionary, is: .ambiguous, via: { _ in nil }), + Converter(.string , .dictionary, is: .ambiguous, via: { _ in nil }), + Converter(.array , .double, is: .ambiguous, via: { _ in nil }), + Converter(.data , .double, is: .ambiguous, via: { _ in nil }), + Converter(.dictionary, .double, is: .ambiguous, via: { _ in nil }), + Converter(.array , .int, is: .ambiguous, via: { _ in nil }), + Converter(.data , .int, is: .ambiguous, via: { _ in nil }), + Converter(.dictionary, .int, is: .ambiguous, via: { _ in nil }), + ] } } diff --git a/Sources/LeafKit/LeafData/LeafDataGenerator.swift b/Sources/LeafKit/LeafData/LeafDataGenerator.swift deleted file mode 100644 index 8c557cb2..00000000 --- a/Sources/LeafKit/LeafData/LeafDataGenerator.swift +++ /dev/null @@ -1,35 +0,0 @@ -/// `LeafDataGenerator` is a wrapper for passing `LeafDataRepresentable` objects to -/// `LeafRenderer.Context` while deferring conversion to `LeafData` until being accessed -/// -/// In all cases, conversion of the `LeafDataRepresentable`-adhering parameter to concrete -/// `LeafData` is deferred until it is actually accessed by `LeafRenderer` (when a template has -/// accessed its value). -/// -/// Can be created as either immediate storage of the parameter, or lazy generation of the -/// `LeafDataRepresentable` object itself in order to provide an additional lazy level in the case of items -/// that may have costly conversion procedures (eg, `Encodable` auto-conformance), or to allow a -/// a generally-shared global `.Context` object to be used repeatedly. -public struct LeafDataGenerator { - /// Produce a generator that immediate stores the parameter - public static func immediate(_ value: LeafDataRepresentable) -> Self { - .init(.immediate(value)) } - - /// Produce a generator that defers evaluation of the parameter until `LeafRenderer` accesses it - public static func lazy(_ value: @escaping @autoclosure () -> LeafDataRepresentable) -> Self { - .init(.lazy(.lazy(f: {value().leafData}, returns: .void))) } - - init(_ value: Container) { self.container = value } - let container: Container - - enum Container: LeafDataRepresentable { - case immediate(LeafDataRepresentable) - case lazy(LKDContainer) - - var leafData: LeafData { - switch self { - case .immediate(let ldr): return ldr.leafData - case .lazy(let lkd): return .init(lkd.evaluate) - } - } - } -} diff --git a/Sources/LeafKit/LeafData/LeafDataRepresentable.swift b/Sources/LeafKit/LeafData/LeafDataRepresentable.swift index 4f8aabed..a01f381b 100644 --- a/Sources/LeafKit/LeafData/LeafDataRepresentable.swift +++ b/Sources/LeafKit/LeafData/LeafDataRepresentable.swift @@ -1,92 +1,77 @@ -import Foundation +// MARK: Subject to change prior to 1.0.0 release +// MARK: - -// MARK: - LeafDataRepresentable Public Definition +import Foundation /// Capable of being encoded as `LeafData`. -/// -/// As `LeafData` has no direct initializers, adherants must implement `leafData` by using a public -/// static factory method from `LeafData` to produce itself. -/// -/// - WARNING: If adherant is a reference-type object, *BE AWARE OF THREADSAFETY* public protocol LeafDataRepresentable { /// Converts `self` to `LeafData`, returning `nil` if the conversion is not possible. var leafData: LeafData { get } - - /// If the adherent has a single, specified `LeafDataType` that is *always* returned, non-nil - /// - /// Default implementation provided - static var leafDataType: LeafDataType? { get } } -public extension LeafDataRepresentable { - static var leafDataType: LeafDataType? { nil } -} - -// MARK: - Default Conformances +// MARK: Default Conformances extension String: LeafDataRepresentable { - public static var leafDataType: LeafDataType? { .string } public var leafData: LeafData { .string(self) } } extension FixedWidthInteger { - public var leafData: LeafData { .int(Int(exactly: self)) } + public var leafData: LeafData { + guard let valid = Int(exactly: self) else { return .int(nil) } + return .int(Int(valid)) + } } -extension Int8: LeafDataRepresentable { public static var leafDataType: LeafDataType? { .int } } -extension Int16: LeafDataRepresentable { public static var leafDataType: LeafDataType? { .int } } -extension Int32: LeafDataRepresentable { public static var leafDataType: LeafDataType? { .int } } -extension Int64: LeafDataRepresentable { public static var leafDataType: LeafDataType? { .int } } -extension Int: LeafDataRepresentable { public static var leafDataType: LeafDataType? { .int } } -extension UInt8: LeafDataRepresentable { public static var leafDataType: LeafDataType? { .int } } -extension UInt16: LeafDataRepresentable { public static var leafDataType: LeafDataType? { .int } } -extension UInt32: LeafDataRepresentable { public static var leafDataType: LeafDataType? { .int } } -extension UInt64: LeafDataRepresentable { public static var leafDataType: LeafDataType? { .int } } -extension UInt: LeafDataRepresentable { public static var leafDataType: LeafDataType? { .int } } +extension Int8: LeafDataRepresentable {} +extension Int16: LeafDataRepresentable {} +extension Int32: LeafDataRepresentable {} +extension Int64: LeafDataRepresentable {} +extension Int: LeafDataRepresentable {} +extension UInt8: LeafDataRepresentable {} +extension UInt16: LeafDataRepresentable {} +extension UInt32: LeafDataRepresentable {} +extension UInt64: LeafDataRepresentable {} +extension UInt: LeafDataRepresentable {} extension BinaryFloatingPoint { - public var leafData: LeafData { .double(Double(exactly: self)) } + public var leafData: LeafData { + guard let valid = Double(exactly: self) else { return .double(nil) } + return .double(Double(valid)) + } } -extension Float: LeafDataRepresentable { public static var leafDataType: LeafDataType? { .double } } -extension Double: LeafDataRepresentable { public static var leafDataType: LeafDataType? { .double } } -#if arch(i386) || arch(x86_64) -extension Float80: LeafDataRepresentable { public static var leafDataType: LeafDataType? { .double } } -#endif +extension Float: LeafDataRepresentable {} +extension Double: LeafDataRepresentable {} +extension Float80: LeafDataRepresentable {} extension Bool: LeafDataRepresentable { - public static var leafDataType: LeafDataType? { .bool } public var leafData: LeafData { .bool(self) } } extension UUID: LeafDataRepresentable { - public static var leafDataType: LeafDataType? { .string } - public var leafData: LeafData { .string(description) } + public var leafData: LeafData { .string(LeafConfiguration.stringFormatter(description)) } } extension Date: LeafDataRepresentable { - public static var leafDataType: LeafDataType? { .double } - /// `Date` conversion is reliant on the configured `LeafTimestamp.referenceBase` - public var leafData: LeafData { - .double( Date(timeIntervalSinceReferenceDate: LeafTimestamp.referenceBase.interval) +-> self ) } + public var leafData: LeafData { .double(timeIntervalSince1970) } +} + +extension Array where Element == LeafData { + public var leafData: LeafData { .array(self.map { $0 }) } } -extension Set: LeafDataRepresentable where Element: LeafDataRepresentable { - public static var leafDataType: LeafDataType? { .array } - public var leafData: LeafData { .array(map {$0.leafData}) } +extension Dictionary where Key == String, Value == LeafData { + public var leafData: LeafData { .dictionary(self.mapValues { $0 }) } } -extension Array: LeafDataRepresentable where Element: LeafDataRepresentable { - public static var leafDataType: LeafDataType? { .array } - public var leafData: LeafData { .array(map {$0.leafData}) } +extension Set where Element: LeafDataRepresentable { + public var leafData: LeafData { .array(self.map { $0.leafData }) } } -extension Dictionary: LeafDataRepresentable where Key == String, Value: LeafDataRepresentable { - public static var leafDataType: LeafDataType? { .dictionary } - public var leafData: LeafData { .dictionary(mapValues {$0.leafData}) } +extension Array where Element: LeafDataRepresentable { + public var leafData: LeafData { .array(self.map { $0.leafData }) } } -extension Optional: LeafDataRepresentable where Wrapped: LeafDataRepresentable { - public static var leafDataType: LeafDataType? { Wrapped.leafDataType } - public var leafData: LeafData { self?.leafData ?? .init(.nil(Self.leafDataType ?? .void)) } +extension Dictionary where Key == String, Value: LeafDataRepresentable { + public var leafData: LeafData { .dictionary(self.mapValues { $0.leafData }) } } diff --git a/Sources/LeafKit/LeafData/LeafDataStorage.swift b/Sources/LeafKit/LeafData/LeafDataStorage.swift new file mode 100644 index 00000000..f2bf415d --- /dev/null +++ b/Sources/LeafKit/LeafData/LeafDataStorage.swift @@ -0,0 +1,233 @@ +// MARK: Subject to change prior to 1.0.0 release +// MARK: - + +import Foundation + +internal indirect enum LeafDataStorage: Equatable, CustomStringConvertible { + // MARK: - Cases + + // Static values + case bool(Bool) + case string(String) + case int(Int) + case double(Double) + case data(Data) + + // Collections (potentially holding lazy values) + case dictionary([String: LeafData]) + case array([LeafData]) + + // Wrapped `Optional` + case optional(_ wrapped: LeafDataStorage?, _ type: LeafData.NaturalType) + + // Lazy resolvable function + // Must specify return tuple giving (returnType, invariance) + case lazy(f: () -> (LeafData), + returns: LeafData.NaturalType, + invariant: Bool) + + // MARK: - LeafSymbol Conformance + + // MARK: Properties + internal var resolved: Bool { true } + internal var invariant: Bool { + switch self { + case .bool(_), + .data(_), + .double(_), + .int(_), + .string(_): return true + case .lazy(_, _, let invariant): return invariant + case .optional(let o, _): return o?.invariant ?? true + case .array(let a): + let stored = a.map { $0.storage }.filter { $0.isLazy } + return stored.allSatisfy { $0.invariant } + case .dictionary(let d): + let stored = d.values.map { $0.storage }.filter { $0.isLazy } + return stored.allSatisfy { $0.invariant } + } + } + internal var symbols: Set { .init() } + internal var isAtomic: Bool { true } + internal var isExpression: Bool { false } + internal var isAny: Bool { false } + internal var isConcrete: Bool { true } + /// Note: Will *always* return a value - can be force-unwrapped safely + internal var concreteType: LeafData.NaturalType? { + switch self { + // Concrete Types + case .array(_) : return .array + case .bool(_) : return .bool + case .data(_) : return .data + case .dictionary(_) : return .dictionary + case .double(_) : return .double + case .int(_) : return .int + case .string(_) : return .string + // Internal Wrapped Types + case .lazy(_, let t, _), + .optional(_, let t) : return t + } + } + + internal var isNumeric: Bool { Self.numerics.contains(concreteType!) } + internal static let comparable: Set = [ + .double, .int, .string + ] + + internal static let numerics: Set = [ + .double, .int + ] + // MARK: Functions + + /// Will resolve anything but variant Lazy data (99% of everything), and unwrap optionals + internal func resolve() -> LeafDataStorage { + guard invariant else { return self } + switch self { + case .lazy(let f, _, _): return f().storage + case .optional(let o, _): + if let unwrapped = o { return unwrapped } + return self + case .array(let a): + let resolved: [LeafData] = a.map { + LeafData($0.storage.resolve()) + } + return .array(resolved) + case .dictionary(let d): + let resolved: [String: LeafData] = d.mapValues { + LeafData($0.storage.resolve()) + } + return .dictionary(resolved) + default: return self + } + } + + /// Will serialize anything to a String except Lazy -> Lazy + internal func serialize() throws -> String? { + let c = LeafConfiguration.self + switch self { + // Atomic non-containers + case .bool(let b) : return c.boolFormatter(b) + case .int(let i) : return c.intFormatter(i) + case .double(let d) : return c.doubleFormatter(d) + case .string(let s) : return c.stringFormatter(s) + // Data + case .data(let d) : return c.dataFormatter(d) + // Wrapped + case .optional(let o, _) : + guard let wrapped = o else { return c.nilFormatter() } + return try wrapped.serialize() + // Atomic containers + case .array(let a) : + let result = try a.map { try $0.storage.serialize() ?? c.nilFormatter() } + return c.arrayFormatter(result) + case .dictionary(let d) : + let result = try d.mapValues { try $0.storage.serialize() ?? c.nilFormatter()} + return c.dictFormatter(result) + case .lazy(let f, _, _) : + guard let result = f() as LeafData?, + !result.storage.isLazy else { + // Silently fail lazy -> lazy... a better option would be nice + return c.nilFormatter() + } + return try result.storage.serialize() ?? c.nilFormatter() + } + } + + /// Final serialization to a shared buffer + internal func serialize(buffer: inout ByteBuffer) throws { + let encoding = LeafConfiguration.encoding + var data: Data? = nil + switch self { + case .bool(_), + .int(_), + .double(_), + .string(_), + .lazy(_,_,_), + .optional(_,_), + .array(_), + .dictionary(_) : data = try serialize()!.data(using: encoding) + case .data(let d) : data = d + } + guard let validData = data else { throw "Serialization Error" } + buffer.writeBytes(validData) + } + + // MARK: - Equatable Conformance + + /// Strict equality comparision, with .nil/.void being equal - will fail on Lazy data that is variant + internal static func == (lhs: LeafDataStorage, rhs: LeafDataStorage) -> Bool { + // If both sides are optional and nil, equal + guard !lhs.isNil || !rhs.isNil else { return true } + // Both sides must be non-nil and same concrete type, or unequal + guard !lhs.isNil && !rhs.isNil, + lhs.concreteType == rhs.concreteType else { return false } + // As long as both are static types, test them + if !lhs.isLazy && !rhs.isLazy { + switch (lhs, rhs) { + // Direct concrete type comparisons + case ( .array(let a), .array(let b)) : return a == b + case (.dictionary(let a), .dictionary(let b)) : return a == b + case ( .bool(let a), .bool(let b)) : return a == b + case ( .string(let a), .string(let b)) : return a == b + case ( .int(let a), .int(let b)) : return a == b + case ( .double(let a), .double(let b)) : return a == b + case ( .data(let a), .data(let b)) : return a == b + // Both sides are optional, unwrap and compare + case (.optional(let l,_), .optional(let r,_)) : + if let l = l, let r = r, + l == r { return true } else { return false } + // ... or unwrap just one side + case (.optional(let l,_), _) : + if let l = l { return l == rhs } else { return false } + case ( _, .optional(let r,_)) : + if let r = r { return r == lhs } else { return false } + default : return false + } + } else if case .lazy(let lhsF, let lhsR, let lhsI) = lhs, + case .lazy(let rhsF, let rhsR, let rhsI) = rhs { + // Only compare lazy equality if invariant to avoid side-effects + guard lhsI && rhsI, lhsR == rhsR else { return false } + return lhsF() == rhsF() + } else { return false } + } + + // MARK: - CustomStringConvertible + internal var description: String { + switch self { + case .array(let a) : return "array(\(a.count))" + case .bool(let b) : return "bool(\(b))" + case .data(let d) : return "data(\(d.count))" + case .dictionary(let d) : return "dictionary(\(d.count))" + case .double(let d) : return "double(\(d))" + case .int(let i) : return "int(\(i))" + case .lazy(_, let r, _) : return "lazy(() -> \(r)?)" + case .optional(_, let t) : return "\(t)()?" + case .string(let s) : return "string(\(s))" + } + } + + internal var short: String { (try? self.serialize()) ?? "" } + + // MARK: - Other + internal var isNil: Bool { + switch self { + case .optional(let o, _) where o == nil : return true + default : return false + } + } + + internal var isLazy: Bool { + if case .lazy(_,_,_) = self { return true } else { return false } + } + + /// Flat mapping behavior - will never re-wrap .optional + internal var wrap: LeafDataStorage { + if case .optional(_,_) = self { return self } + return .optional(self, concreteType!) + } + + internal var unwrap: LeafDataStorage? { + guard case .optional(let optional, _) = self else { return self } + return optional + } +} diff --git a/Sources/LeafKit/LeafData/LeafDataType.swift b/Sources/LeafKit/LeafData/LeafDataType.swift deleted file mode 100644 index ac42cb28..00000000 --- a/Sources/LeafKit/LeafData/LeafDataType.swift +++ /dev/null @@ -1,66 +0,0 @@ -/// The concrete instantiable object types for `LeafData` -public enum LeafDataType: UInt8, CaseIterable, Hashable { - // MARK: Cases - case bool - case string - case int - case double - case data - case dictionary - case array - - case void - - public var description: String { short } -} - -public extension Set where Element == LeafDataType { - /// Any `LeafDataType` but `.void` - static var any: Self { Set(LeafDataType.allCases.filter {$0.returnable}) } - /// `LeafDataType` == `Collection` - static var collections: Self { [.array, .dictionary] } - /// `LeafDataType` == `SignedNumeric` - static var numerics: Self { [.int, .double] } - - static var string: Self { [.string] } - static var int: Self { [.int] } - static var double: Self { [.double] } - static var void: Self { [.void] } - static var bool: Self { [.bool] } - static var array: Self { [.array] } - static var dictionary: Self { [.dictionary] } - static var data: Self { [.data] } -} - -// MARK: - Internal Only -extension LKDType: LKPrintable { - internal var short: String { - switch self { - case .array : return "array" - case .bool : return "bool" - case .data : return "data" - case .dictionary : return "dictionary" - case .double : return "double" - case .int : return "int" - case .string : return "string" - case .void : return "void" - } - } - - /// Get the casting level for two types - internal func casts(to type: Self) -> LKDConversion { - typealias _Map = LKDConverters - switch self { - case .array : return _Map.arrayMaps[type]!.is - case .bool : return _Map.boolMaps[type]!.is - case .data : return _Map.dataMaps[type]!.is - case .dictionary : return _Map.dictionaryMaps[type]!.is - case .double : return _Map.doubleMaps[type]!.is - case .int : return _Map.intMaps[type]!.is - case .string : return _Map.stringMaps[type]!.is - case .void : return .ambiguous - } - } - - internal var returnable: Bool { self != .void } -} diff --git a/Sources/LeafKit/LeafEntities/LKRawBlock.swift b/Sources/LeafKit/LeafEntities/LKRawBlock.swift deleted file mode 100644 index 4e7ece31..00000000 --- a/Sources/LeafKit/LeafEntities/LKRawBlock.swift +++ /dev/null @@ -1,71 +0,0 @@ -// MARK: Subject to change prior to 1.0.0 release - -/// A `LKRawBlock` is a specialized `LeafBlock` that handles the output stream of Leaf processing. -/// -/// It may optionally process in another language and maintain its own state. -internal protocol LKRawBlock: LeafFunction { - /// If the raw handler needs be signalled after it has been provided the contents of its entire block - static var recall: Bool { get } - - /// Generate a `.raw` block - /// - Parameters: - /// - size: Expected minimum byte count required - /// - encoding: Encoding of the incoming string. - static func instantiate(size: UInt32, - encoding: String.Encoding) -> LKRawBlock - - /// Adherent must be able to provide a serialized view of itself in entirety while open or closed - /// - /// `valid` shall be semantic for the block type. An HTML raw block might report as follows - /// ``` - ///
// true (valid as an encapsulated block) - ///
// nil (indefinite) - ///
// false (always invalid) - var serialized: (buffer: ByteBuffer, valid: Bool?) { get } - - /// Optional error information if the handler is stateful which LeafKit may choose to report/log. - var error: Error? { get } - - /// The encoding of the contents of the block. - /// - /// Incoming data appended to the block may be in a different encoding than the block itself expects. - var encoding: String.Encoding { get } - - /// Append a second block to this one. - /// - /// If the second block is the same type, adherent should take care of maintaining state as necessary. - /// If it isn't of the same type, adherent may assume it's a completed LKRawBlock and access - /// `block.serialized` to obtain a `ByteBuffer` to append - mutating func append(_ block: inout LKRawBlock) - - /// Append a `LeafData` object to the output stream - mutating func append(_ data: LeafData) - - /// Signal that a non-outputting void Leaf action has happened - /// - /// Used to potentially strip unncessary whitespace from the template - mutating func voidAction() - - /// If type is `recall == true`, will be called when the block's scope is closed to allow cleanup/additions/validation - mutating func close() - - /// Bytes in the raw buffer - var byteCount: UInt32 { get } - var contents: String { get } -} - -/// Default implementations for typical `LKRawBlock`s -extension LKRawBlock { - /// Most blocks are not evaluable - public static var returns: Set { .void } - - public static var invariant: Bool { true } - public static var callSignature:[LeafCallParameter] { [] } - - /// RawBlocks will never be called with evaluate - public func evaluate(_ params: LeafCallValues) -> LeafData { .error(internal: "LKRawBlock called as function") } - var recall: Bool { Self.recall } - - func getError(_ location: SourceLocation) -> LeafError? { error.map { err(.serializeError(Self.self, $0, location)) } } -} - diff --git a/Sources/LeafKit/LeafEntities/LeafBlock.swift b/Sources/LeafKit/LeafEntities/LeafBlock.swift deleted file mode 100644 index 7fd9e0aa..00000000 --- a/Sources/LeafKit/LeafEntities/LeafBlock.swift +++ /dev/null @@ -1,110 +0,0 @@ -// MARK: Subject to change prior to 1.0.0 release - -/// An object that can introduce variables and/or scope into a template for anything within the block -/// -/// Example: `#for(value in dictionary)` -public protocol LeafBlock: LeafFunction { - /// Provide any relevant parse signatures, if the block must be provided data at parse time. - /// - /// Ex: `#for` needs to provide a signature for `x in y` where x is a parse parameter that sets - /// the variable name it will provide to its scope when evaluated, and y is a call parameter that it will - /// receive when being evaluated. - static var parseSignatures: ParseSignatures? { get } - - /// Generate a concrete object of this type given concrete parameters at parse time - /// - Parameters: - /// - parseParams: The parameters this object requires at parse time - static func instantiate(_ signature: String?, _ params: [String]) throws -> Self - - /// If the object can be called with function syntax via `evaluate` - static var evaluable: Bool { get } - - /// The variable names an instantiated `LeafBlock` will provide to its block, if any. - /// - /// These must be consistent throughout calls to the block. If the block type will *never* provide - /// variables, return nil rather than an empty array. - var scopeVariables: [String]? { get } - - /// The actual entry point function of a `LeafBlock` - /// - /// - Parameters: - /// - params: `CallValues` holding the Leaf data corresponding to the block's call signature - /// - variables: Dictionary of variable values the block is setting. - /// - Returns: - /// - `ScopeValue` signals whether the block should be re-evaluated; 0 if discard, - /// 1...n if a known amount, nil if unknown how many times it will need to be re-evaluated - /// - `.discard` or `.once` are the predominant returns for most blocks - /// - `.indefinite` or `.repeating(x)` for looping blocks. - /// - If returning anything but `.indefinite`, further calls will go to `reEvaluateScope` - /// - /// If the block is setting any scope variable values, assign them to the corresponding key previously - /// reported in `scopeVariables` - any variable keys not previously reported in that property will - /// be ignored and not available inside the block's scope. - mutating func evaluateScope(_ params: LeafCallValues, - _ variables: inout [String: LeafData]) -> EvalCount - - /// Re-entrant point for `LeafBlock`s that previously reported a finite scope count. - /// - /// If a block has previously reported a fixed number, it must continue to report a fixed number and may - /// not return to reporting `.indefinite`. While it is not prohibited to *increase* the number of times - /// upon re-evaluation, doing so should be done carefully. Count does not need to change in single - /// increments. - mutating func reEvaluateScope(_ variables: inout [String: LeafData]) -> EvalCount -} - -/// An object that can be chained to other `ChainedBlock` objects -/// -/// - Ex: `#if(): #elseif(): #else: #endif` -/// When evaluating, the first block to return a non-discard state will have its scope evaluated and further -/// blocks in the chain will be immediately discarded. -public protocol ChainedBlock: LeafBlock { - static var chainsTo: [ChainedBlock.Type] { get } - static var chainAccepts: [ChainedBlock.Type] { get } -} - -/// `EvalCount` dictates how many times a block will be evaluated -/// -/// - `.discard` if immediately bypass the block's scope -/// - `.once` if only called once -/// - `.repeating(x)` if called a finite number of times -/// - `.indefinite` if number of calls is indeterminable -public typealias EvalCount = UInt32? -public extension EvalCount { - static let discard: Self = 0 - static let once: Self = 1 - static let indefinite: Self = nil - static func repeating(_ times: UInt32) -> Self { times } -} - -/// A representation of a block's parsing parameters -public indirect enum LeafParseParameter: Hashable { - /// A mapping of this position to a raw string `instantiate` will receive - case unscopedVariable - /// A mapping of a literal value - case literal(String) - /// A mapping of this position to the function signature parameters - case callParameter - /// A set of keywords the block accepts at this position - case keyword(Set) - - /// A tuple - `(x, y)` where contents are *not* `.expression` or `.tuple` - case tuple([Self]) - /// An expression - `(x in y)` where contents are *not* `.expression` - case expression([Self]) -} - -// MARK: - Default Implementations - -/// Default implementations for typical `LeafBlock`s -public extension LeafBlock { - /// Default implementation of LeafFunction.evaluate() - func evaluate(_ parameters: LeafCallValues) -> LeafData { - if Self.evaluable { __MajorBug("LeafBlock called as a function: implement `evaluate`") } - else { __MajorBug("Catachall default implementation for non-evaluable block") } - } -} - -public extension ChainedBlock { - mutating func reEvaluateScope(_ variables: inout [String: LeafData]) -> EvalCount { - __MajorBug("ChainedBlocks are only called once") } -} diff --git a/Sources/LeafKit/LeafEntities/LeafCallParameter.swift b/Sources/LeafKit/LeafEntities/LeafCallParameter.swift deleted file mode 100644 index 33eba88e..00000000 --- a/Sources/LeafKit/LeafEntities/LeafCallParameter.swift +++ /dev/null @@ -1,132 +0,0 @@ -/// A representation of a function parameter defintion - equivalent to a Swift parameter defintion -public struct LeafCallParameter: LKPrintable, Equatable { - let label: String? - let types: Set - let optional: Bool - let defaultValue: Optional - - /// Direct equivalency to a Swift parameter - see examples below - /// - /// For "func aFunction(`myLabel 0: String? = nil`)" (parameter will be available at `params["myLabel"]` or `params[0]`: - /// - `.init(label: "myLabel", types: [.string], optional: true, defaultValue: nil)` - /// - /// For "func aFunction(`_ 0: LeafData")` (parameter will be available at `params[0]`: - /// - `.init(types: Set(LeafDataType.allCases)) ` - public init(label: String? = nil, - types: Set, - optional: Bool = false, - defaultValue: Optional = .none) { - self.label = label - self.types = types - self.optional = optional - self.defaultValue = defaultValue - _sanity() - } - - /// `(value(1), isValid: bool(true), ...)` - public var description: String { short } - var short: String { - "\(label ?? "_"): \(types.map {$0.short.capitalized}.sorted(by: <).joined(separator: "|"))\(optional ? "?" : "")\(defaulted ? " = \(defaultValue!.container.terse)" : "")" - } - - var defaulted: Bool { defaultValue != .none } - var labeled: Bool { label != nil } -} - -public extension LeafCallParameter { - /// Shorthand convenience for an unlabled, non-optional, undefaulted parameter of a single type - static func type(_ type: LeafDataType) -> Self { .init(types: [type]) } - - /// Shorthand convenience for an unlabled, non-optional, undefaulted parameter of `[types]` - static func types(_ types: Set) -> Self { .init(types: types) } - - /// Shorthand convenience for an unlabled, undefaulted parameter of `[types]?` - static func optionalTypes(_ types: Set) -> Self { .init(types: types, optional: true) } - - /// Any `LeafDataType` but `.void` (and implicitly not an errored state value) - static var any: Self { .types(.any) } - - /// `LeafDataType` == `Collection` - static var collections: Self { .types(.collections) } - - /// `LeafDataType` == `SignedNumeric` - static var numerics: Self { .types(.numerics) } - - /// Unlabeled, non-optional, undefaulted `.string` - static var string: Self { .type(.string) } - - /// Unlabeled, non-optional, undefaulted `.int` - static var int: Self { .type(.int) } - - /// Unlabeled, non-optional, undefaulted `.double` - static var double: Self { .type(.double) } - - /// Unlabeled, non-optional, undefaulted `.bool` - static var bool: Self { .type(.bool) } - - /// Unlabeled, non-optional, undefaulted `.data` - static var data: Self { .type(.data) } - - /// Unlabeled, non-optional, undefaulted `.array` - static var array: Self { .type(.array) } - - /// Unlabeled, non-optional, undefaulted `.dictionary` - static var dictionary: Self { .type(.dictionary) } - - /// string-only with conveniences for various options - static func string(labeled: String?, optional: Bool = false, defaultValue: Optional = .none) -> Self { - .init(label: labeled, types: .string, optional: optional, defaultValue: defaultValue) } - - /// double-only with conveniences for various options - static func double(labeled: String?, optional: Bool = false, defaultValue: Optional = .none) -> Self { - .init(label: labeled, types: .double, optional: optional, defaultValue: defaultValue) } - - /// int-only with conveniences for various options - static func int(labeled: String?, optional: Bool = false, defaultValue: Optional = .none) -> Self { - .init(label: labeled, types: .int, optional: optional, defaultValue: defaultValue) } - - /// bool-only with conveniences for various options - static func bool(labeled: String?, optional: Bool = false, defaultValue: Optional = .none) -> Self { - .init(label: labeled, types: .bool, optional: optional, defaultValue: defaultValue) } -} - -internal extension LeafCallParameter { - /// Verify the `CallParameter` is valid - func _sanity() { - precondition(!types.isEmpty, - "Parameter must specify at least one type") - precondition(!types.contains(.void), - "Parameters cannot take .void types") - precondition(!(label?.isEmpty ?? false) && label != "_", - "Use nil for unlabeled parameters, not empty strings or _") - precondition(label?.isValidLeafIdentifier ?? true, - "Label must be a valid, non-keyword Leaf identifier") - precondition(types.contains(defaultValue?.storedType ?? types.first!), - "Default value is not a match for the argument types") - } - - /// Return the parameter value if it's valid, coerce if possible, nil if not an interpretable match. - func match(_ value: LeafData) -> Optional { - /// 1:1 expected match, valid as long as expectation isn't non-optional with optional value - if types.contains(value.storedType) { return !value.isNil || optional ? value : .none } - /// If value is still nil but no match... - if value.isNil { - /// trueNil param - if value.storedType == .void || !optional { - /// param accepts optional, coerce nil type to an expected type - return optional ? .init(.nil(types.first!)) - /// or if it takes bool, coerce to a false boolean or fail - : types.contains(.bool) ? .bool(false) : .none - } - /// Remaining nil values are failures - return .none - } - /// If only one type, return coerced value as long as it doesn't coerce to .trueNil (and for .bool always true) - if types.count == 1 { - let coerced = value.coerce(to: types.first!) - return coerced != .trueNil ? coerced : types.first! == .bool ? .bool(true) : .none - } - /// Otherwise assume function will handle coercion itself as long as one potential match exists - return types.first(where: {value.isCoercible(to: $0)}) != nil ? value : .none - } -} diff --git a/Sources/LeafKit/LeafEntities/LeafCallValues.swift b/Sources/LeafKit/LeafEntities/LeafCallValues.swift deleted file mode 100644 index b285b783..00000000 --- a/Sources/LeafKit/LeafEntities/LeafCallValues.swift +++ /dev/null @@ -1,47 +0,0 @@ -/// The concrete object a `LeafFunction` etc. will receive holding its call parameter values -/// -/// Values for all parameters in function's call signature are guaranteed to be present and accessible via -/// subscripting using the 0-based index of the parameter position, or the label if one was specified. Data -/// is guaranteed to match at least one of the data types that was specified, and will only be optional if -/// the parameter specified that it accepts optionals at that position. -/// -/// `.trueNil` is a unique case that never is an actual parameter value the function has received - it -/// signals out-of-bounds indexing of the parameter value object. -public struct LeafCallValues { - let values: [LeafData] - let labels: [String: Int] -} - -public extension LeafCallValues { - /// Get the value at the specified 0-based index. - /// - /// Out of bounds positions return `.trueNil` - subscript(index: Int) -> LeafData { (0.. LeafData { labels[index] != nil ? self[labels[index]!] : .trueNil } - - var count: Int { values.count } -} - -internal extension LeafCallValues { - /// Generate fulfilled LeafData call values from symbols in incoming tuple - init?(_ sig: [LeafCallParameter], - _ tuple: LKTuple?, - _ symbols: inout LKVarStack) { - if tuple == nil && !sig.isEmpty { return nil } - guard let tuple = tuple else { values = []; labels = [:]; return } - self.labels = tuple.labels - self.values = tuple.values.enumerated().compactMap { - sig[$0.offset].match($0.element.evaluate(&symbols)) } - /// Some values not matched - call fails - if count < tuple.count { return nil } - } - - init(_ values: [LeafData], _ labels: [String: Int]) { - self.values = values - self.labels = labels - } -} diff --git a/Sources/LeafKit/LeafEntities/LeafEntities.swift b/Sources/LeafKit/LeafEntities/LeafEntities.swift deleted file mode 100644 index d6509ea9..00000000 --- a/Sources/LeafKit/LeafEntities/LeafEntities.swift +++ /dev/null @@ -1,402 +0,0 @@ -public final class LeafEntities { - // MARK: Internal Only Properties - private(set) var identifiers: Set = [] - private(set) var openers: Set = [] - private(set) var closers: Set = [] - private(set) var assignment: Set = [] - - /// Factories that produce `.raw` Blocks - private(set) var rawFactories: [String: LKRawBlock.Type] - /// Factories that produce named Blocks - private(set) var blockFactories: [String: LeafBlock.Type] - /// Function registry - private(set) var functions: [String: [LeafFunction]] - /// Method registry - private(set) var methods: [String: [LeafMethod]] - - /// Type registery - private(set) var types: [String: (LeafDataRepresentable.Type, LeafDataType)] - - /// Initializer - /// - Parameter rawHandler: The default factory for `.raw` blocks - init(rawHandler: LKRawBlock.Type = LeafBuffer.self) { - self.rawFactories = [Self.defaultRaw: rawHandler] - self.blockFactories = [:] - self.functions = [:] - self.methods = [:] - self.types = [:] - } - - public static var leaf4Core: LeafEntities { ._leaf4Core } -} - -public extension LeafEntities { - // MARK: Entity Registration Methods - - /// Register a Block factory - /// - Parameters: - /// - block: A `LeafBlock` adherent (which is not a `LKRawBlock` adherent) - /// - name: The name used to choose this factory - "name: `for`" == `#for():` - func use(_ block: LeafBlock.Type, - asBlock name: String) { - if !LKConf.running(fault: "Cannot register new Block factories") { - name._sanity() - block.callSignature._sanity() - if let parseSigs = block.parseSignatures { parseSigs._sanity() } - precondition(block == RawSwitch.self || - block as? LKRawBlock.Type == nil, - "Register LKRawBlock factories using `registerRaw(...)`") - precondition(!openers.contains(name), - "A block named `\(name)` already exists") - blockFactories[name] = block - identifiers.insert(name) - openers.insert(name) - if let chained = block as? ChainedBlock.Type { - precondition(chained.chainsTo.filter { $0 != block.self} - .allSatisfy({ b in blockFactories.values.contains(where: {$0 == b})}), - "All types this block chains to must be registered.") - if chained.chainsTo.isEmpty { closers.insert("end" + name) } - else if chained.callSignature.isEmpty { closers.insert(name) } - } else { closers.insert("end" + name) } - } - } - - /// Register a LeafFunction - /// - Parameters: - /// - function: An instance of a `LeafFunction` adherant which is not a mutating `LeafMethod` - /// - name: "name: `date`" == `#date()` - func use(_ function: LeafFunction, - asFunction name: String) { - if !LKConf.running(fault: "Cannot register new function \(name)") { - name._sanity() - function.sig._sanity() - precondition(!((function as? LeafMethod)?.mutating ?? false), - "Mutating method \(type(of: function)) may not be used as direct functions") - if functions.keys.contains(name) { - functions[name]!.forEach { - precondition(!function.sig.confusable(with: $0.sig), - "Function overload is ambiguous with \(type(of: $0))") - } - functions[name]!.append(function) - } else { functions[name] = [function] } - identifiers.insert(name) - openers.insert(name) - } - } - - /// Register a LeafMethod - /// - Parameters: - /// - method: An instance of a `LeafMethod` adherant - /// - name: "name: `hasPrefix`" == `#(a.hasPrefix(b))` - /// - Throws: If a function for name is already registered, or name is empty - func use(_ method: LeafMethod, - asMethod name: String) { - if !LKConf.running(fault: "Cannot register new method \(name)") { - name._sanity() - method.sig._sanity() - type(of: method)._sanity() - if methods.keys.contains(name) { - methods[name]!.forEach { - precondition(!method.sig.confusable(with: $0.sig), - "Method overload is ambiguous with \(type(of: $0))") - } - methods[name]!.append(method) - } else { methods[name] = [method] } - identifiers.insert(name) - } - } - - /// Register a non-mutating `LeafMethod` as both a Function and a Method - /// - Parameters: - /// - method: An instance of a `LeafMethod` adherant - /// - name: "name: `hasPrefix`" == `#hasPrefix(a,b)` && `#(a.hasPrefix(b)` - /// - Throws: If a function for name is already registered, or name is empty - func use(_ method: LeafMethod, - asFunctionAndMethod name: String) { - use(method, asFunction: name) - use(method, asMethod: name) - } - - /// Lightweight validator for a string that may be a Leaf template source. - /// - /// - Returns: True if all tag marks in the string are valid entities, but does not guarantee rendering will not error - /// False if there are no tag marks in the string - /// Nil if there are tag marks that are inherently erroring due to invalid entities. - func validate(in string: String) -> Bool? { - switch string.isLeafProcessable(self) { - case .success(true): return true - case .success(false): return false - case .failure: return nil - } - } - - /// Register optional Metablocks prior to starting LeafKit - /// - /// `import`, `export`, `extend` synonyms for `define`, `evaluate`, `inline` - func registerLeaf4Transitional() { - use(Define.self , asMeta: "export") - use(Evaluate.self , asMeta: "import") - use(Inline.self , asMeta: "extend") - } - - /// Register optional Metablocks prior to starting LeafKit - /// - /// `def`, `eval` synonyms for `define`, `evaluate` - func registerLazyShorthands() { - use(Define.self , asMeta: "def") - use(Evaluate.self , asMeta: "eval") - } - - /// Register optional entities prior to starting LeafKit - func registerExtendedEntities() { - //use(IntIntToIntMap._min , asFunction: "min") - //use(IntIntToIntMap._max , asFunction: "max") - //use(DoubleDoubleToDoubleMap._min, asFunction: "min") - //use(DoubleDoubleToDoubleMap._max, asFunction: "max") - //use(StrToStrMap.reversed, asMethod: "reversed") - //use(StrToStrMap.randomElement, asMethod: "randomElement") - //use(StrStrStrToStrMap.replace, asMethod: "replace") - //use(StrToStrMap.escapeHTML, asFunctionAndMethod: "escapeHTML") - use(DoubleFormatterMap.seconds, asFunctionAndMethod: "formatSeconds") - use(IntFormatterMap.bytes, asFunctionAndMethod: "formatBytes") - } -} - -// MARK: - Internal Only -internal extension LeafEntities { - // MARK: Entity Registration Methods - - /// Register a type - func use(_ swiftType: T.Type, - asType name: String, - storeAs: LKDType) where T: LeafDataRepresentable { - if !LKConf.running(fault: "Cannot register new types") { - precondition(storeAs != .void, "Void is not a valid storable type") - precondition(!types.keys.contains(name), - "\(name) is already registered for \(String(describing: types[name]))") - switch storeAs { - case .array : use(ArrayIdentity(), asFunction: name) - case .bool : use(BoolIdentity(), asFunction: name) - case .data : use(DataIdentity(), asFunction: name) - case .dictionary : use(DictionaryIdentity(), asFunction: name) - case .double : use(DoubleIdentity(), asFunction: name) - case .int : use(IntIdentity(), asFunction: name) - case .string : use(StringIdentity(), asFunction: name) - case .void : __MajorBug("Void is not a valid storable type") - } - identifiers.insert(name) - openers.insert(name) - } - } - - /// Register a LKRawBlock factory - /// - Parameters: - /// - block: A `LKRawBlock` adherent - /// - name: The name used to choose this factory - "name: `html`" == `#raw(html, ....):` - func use(_ block: LKRawBlock.Type, - asRaw name: String) { - if !LKConf.running(fault: "Cannot register new Raw factory \(name)") { - name._sanity() - block.callSignature._sanity() - precondition(!openers.contains(name), - "A block named `\(name)` already exists") - rawFactories[name] = block - } - } - - /// Register a metablock - func use(_ meta: LKMetaBlock.Type, - asMeta name: String) { - if !LKConf.running(fault: "Cannot register new Metablock factory \(name)") { - if meta.form != .declare { name._sanity() } - precondition(!openers.contains(name), - "A block named `\(name)` already exists") - blockFactories[name] = meta - identifiers.insert(name) - openers.insert(name) - if [.define, .rawSwitch].contains(meta.form) { closers.insert("end" + name) } - if [.define, .declare].contains(meta.form) { assignment.insert(name) } - } - } - - // MARK: Validators - - /// Return all valid matches. - func validateFunction(_ name: String, - _ params: LKTuple?) -> Result<[(LeafFunction, LKTuple?)], ParseErrorCause> { - guard let functions = functions[name] else { return .failure(.noEntity(type: "function", name: name)) } - var valid: [(LeafFunction, LKTuple?)] = [] - for function in functions { - if let tuple = try? validateTupleCall(params, function.sig).get() - { valid.append((function, tuple.isEmpty ? nil : tuple)) } else { continue } - } - if !valid.isEmpty { return .success(valid) } - return .failure(.sameName(type: "function", name: name, params: (params ?? .init()).description, matches: functions.map {$0.sig.short} )) - } - - func validateMethod(_ name: String, - _ params: LKTuple?, - _ mutable: Bool) -> Result<[(LeafFunction, LKTuple?)], ParseErrorCause> { - guard let methods = methods[name] else { - return .failure(.noEntity(type: "method", name: name)) } - var valid: [(LeafFunction, LKTuple?)] = [] - var mutatingMismatch = false - for method in methods { - if method.mutating && !mutable { mutatingMismatch = true; continue } - if let tuple = try? validateTupleCall(params, method.sig).get() - { valid.append((method, tuple.isEmpty ? nil : tuple)) } else { continue } - } - if valid.isEmpty { - return .failure(mutatingMismatch ? .mutatingMismatch(name: name) - : .sameName(type: "function", name: name, params: (params ?? .init()).description, matches: methods.map {$0.sig.short} ) ) - } - return .success(valid) - } - - func validateBlock(_ name: String, - _ params: LKTuple?) -> Result<(LeafFunction, LKTuple?), ParseErrorCause> { - guard blockFactories[name] != RawSwitch.self else { return validateRaw(params) } - guard let factory = blockFactories[name] else { return .failure(.noEntity(type: "block", name: name)) } - let block: LeafFunction? - var call: LKTuple = .init() - - validate: - if let parseSigs = factory.parseSignatures { - for (name, sig) in parseSigs { - guard let match = sig.splitTuple(params ?? .init()) else { continue } - guard let created = try? factory.instantiate(name, match.0) else { - return .failure(.parameterError(name: name, reason: "Parse signature matched but couldn't instantiate")) } - block = created - call = match.1 - break validate - } - block = nil - } else if (params?.count ?? 0) == factory.callSignature.count { - if let params = params { call = params } - block = try? factory.instantiate(nil, []) - } else { return .failure(.parameterError(name: name, reason: "Takes no parameters")) } - - guard let function = block else { - return .failure(.parameterError(name: name, reason: "Parameters don't match parse signature") )} - let validate = validateTupleCall(call, function.sig) - switch validate { - case .failure(let message): return .failure(.parameterError(name: name, reason: message)) - case .success(let tuple): return .success((function, !tuple.isEmpty ? tuple : nil)) - } - } - - func validateRaw(_ params: LKTuple?) -> Result<(LeafFunction, LKTuple?), ParseErrorCause> { - var name = Self.defaultRaw - var call: LKTuple - - if let params = params { - if case .variable(let v) = params[0]?.container, v.isAtomic { name = String(v.member!) } - else { return .failure(.unknownError("Specify raw handler with unquoted name")) } - call = params - call.values.removeFirst() - call.labels = call.labels.mapValues { $0 - 1 } - } else { call = .init() } - - guard let factory = rawFactories[name] else { return .failure(.unknownError("\(name) is not a raw handler"))} - guard call.values.allSatisfy({ $0.data != nil }) else { - return .failure(.unknownError("Raw handlers currently require literal data parameters")) } - let validate = validateTupleCall(call, factory.callSignature) - switch validate { - case .failure(let message): return .failure(.parameterError(name: name, reason: message)) - case .success(let tuple): return .success((RawSwitch(factory, tuple), nil)) - } - } - - func validateTupleCall(_ tuple: LKTuple?, _ expected: [LeafCallParameter]) -> Result { - /// True if actual parameter matches expected parameter value type, or if actual parameter is uncertain type - func matches(_ actual: LKParameter, _ expected: LeafCallParameter) -> Bool { - guard let t = actual.baseType else { return true } - if case .value(let literal) = actual.container, literal.isNil { return expected.optional } - return expected.types.contains(t) ? true - : expected.types.first(where: {t.casts(to: $0) != .ambiguous}) != nil - } - - func output() -> Result { - for i in 0 ..< count.out { - if temp[i] == nil { return .failure("Missing parameter \(expected[i].description)") } - tuples.out.values.append(temp[i]!) - } - return .success(tuples.out) - } - - guard expected.count < 256 else { return .failure("Can't have more than 255 params") } - - var tuples = (in: tuple ?? LKTuple(), out: LKTuple()) - - /// All input must be valued types - guard tuples.in.values.allSatisfy({$0.isValued}) else { - return .failure("Parameters must all be value types") } - - let count = (in: tuples.in.count, out: expected.count) - let defaults = expected.compactMap({ $0.defaultValue }).count - /// Guard that `in.count <= out.count` && `in.count + default >= out.count` - if count.in > count.out { return .failure("Too many parameters") } - if Int(count.in) + defaults < count.out { return .failure("Not enough parameters") } - - /// guard that if the signature has labels, input is fully contained and in order - let labels = (in: tuples.in.enumerated.compactMap {$0.label}, out: expected.compactMap {$0.label}) - guard labels.out.filter({labels.in.contains($0)}).elementsEqual(labels.in), - Set(labels.out).isSuperset(of: labels.in) else { return .failure("Label mismatch") } - - var temp: [LKParameter?] = .init(repeating: nil, count: expected.count) - - /// Copy all labels to out and labels and/or default values to temp - for (i, p) in expected.enumerated() { - if let label = p.label { tuples.out.labels[label] = i } - if let data = p.defaultValue { temp[i] = .value(data) } - } - - /// If input is empty, all default values are already copied and we can output - if count.in == 0 { return output() } - - /// Map labeled input parameters to their correct position in the temp array - for label in labels.in { temp[Int(tuples.out.labels[label]!)] = tuples.in[label] } - - /// At this point any nil value in the temp array is undefaulted, and - /// the only values uncopied from tuple.in are unlabeled values - var index = 0 - let last = (in: (tuples.in.labels.values.min() ?? count.in) - 1, - out: (tuples.out.labels.values.min() ?? count.out) - 1) - while index <= last.in, index <= last.out { - let param = tuples.in.values[index] - /// apply all unlabeled input params to temp, unsetting if not matching expected - temp[index] = matches(param, expected[index]) ? param : nil - if temp[index] == nil { break } - index += 1 - } - return output() - } - - /// Convenience referent to the default `.raw` Block factory - static var defaultRaw: String { "raw" } - var raw: LKRawBlock.Type { rawFactories[Self.defaultRaw]! } -} - -// MARK: - Private Only -private extension LeafEntities { - private static var _leaf4Core: LeafEntities { - let entities = LeafEntities() - - entities.registerMetaBlocks() - entities.registerControlFlow() - - entities.registerTypeCasts() - entities.registerErroring() - - entities.registerArrayReturns() - entities.registerBoolReturns() - entities.registerIntReturns() - entities.registerDoubleReturns() - entities.registerStringReturns() - entities.registerMutatingMethods() - - entities.registerMisc() - - return entities - } -} diff --git a/Sources/LeafKit/LeafEntities/LeafFunction.swift b/Sources/LeafKit/LeafEntities/LeafFunction.swift deleted file mode 100644 index 7601b3f3..00000000 --- a/Sources/LeafKit/LeafEntities/LeafFunction.swift +++ /dev/null @@ -1,67 +0,0 @@ -// MARK: Subject to change prior to 1.0.0 release - -/// An object that can take `LeafData` parameters and returns a single `LeafData` result -/// -/// Example: `#date("now", "YYYY-mm-dd")` -public protocol LeafFunction { - /// Array of the function's full call parameters - /// - /// *MUST BE STABLE AND NOT CHANGE* - static var callSignature: [LeafCallParameter] { get } - - /// The concrete type(s) of `LeafData` the function returns - /// - /// *MUST BE STABLE AND NOT CHANGE* - if multiple possible types can be returned, use .any - static var returns: Set { get } - - /// Whether the function is invariant (has no potential side effects and always produces the same - /// value given the same input) - /// - /// *MUST BE STABLE AND NOT CHANGE* - static var invariant: Bool { get } - - /// The actual evaluation function of the `LeafFunction`, which will be called with fully resolved data - func evaluate(_ params: LeafCallValues) -> LeafData -} - -// MARK: - Convenience Protocols - -public protocol EmptyParams: LeafFunction {} -public extension EmptyParams { static var callSignature: [LeafCallParameter] {[]} } - -public protocol Invariant: LeafFunction {} -public extension Invariant { static var invariant: Bool { true } } - -public protocol StringReturn: LeafFunction {} -public extension StringReturn { static var returns: Set { .string } } - -public protocol VoidReturn: LeafFunction {} -public extension VoidReturn { static var returns: Set { .void } } - -public protocol BoolReturn: LeafFunction {} -public extension BoolReturn { static var returns: Set { .bool } } - -public protocol ArrayReturn: LeafFunction {} -public extension ArrayReturn { static var returns: Set { .array } } - -public protocol DictionaryReturn: LeafFunction {} -public extension DictionaryReturn { static var returns: Set { .dictionary } } - -public protocol IntReturn: LeafFunction {} -public extension IntReturn { static var returns: Set { .int } } - -public protocol DoubleReturn: LeafFunction {} -public extension DoubleReturn { static var returns: Set { .double } } - -public protocol DataReturn: LeafFunction {} -public extension DataReturn { static var returns: Set { .data } } - -public protocol AnyReturn: LeafFunction {} -public extension AnyReturn { static var returns: Set { .any } } - -// MARK: Internal Only - -internal extension LeafFunction { - var invariant: Bool { Self.invariant } - var sig: [LeafCallParameter] { Self.callSignature } -} diff --git a/Sources/LeafKit/LeafEntities/LeafMethod.swift b/Sources/LeafKit/LeafEntities/LeafMethod.swift deleted file mode 100644 index 0a0b4b4c..00000000 --- a/Sources/LeafKit/LeafEntities/LeafMethod.swift +++ /dev/null @@ -1,62 +0,0 @@ - - -/// A `LeafFunction` that additionally can be used on a method on concrete `LeafData` types. -/// -/// Example: `#(aStringVariable.hasPrefix("prefix")` -/// The first parameter of the `.callSignature` provides the types the method can operate on. The method -/// will still be called using `LeafFunction.evaluate`, with the first parameter being the operand. -/// -/// Has the potential to mutate the first parameter it is passed; must either be mutating or non-mutating (not both). -/// -/// Convenience protocols`Leaf(Non)MutatingMethod`s preferred for adherence as they provide default -/// implementations for the enforced requirements of those variations. -public protocol LeafMethod: LeafFunction {} - -/// A `LeafMethod` that does not mutate its first parameter value. -public protocol LeafNonMutatingMethod: LeafMethod {} - -/// A `LeafMethod` that may potentially mutate its first parameter value. -public protocol LeafMutatingMethod: LeafMethod { - /// Return non-nil for `mutate` to the value the operand should now hold, or nil if it has not changed. Always return `result` - func mutatingEvaluate(_ params: LeafCallValues) -> (mutate: Optional, result: LeafData) -} - -public extension LeafMutatingMethod { - /// Mutating methods are inherently always variant - static var invariant: Bool { false } - - /// Mutating methods will never be called with the normal `evaluate` call - func evaluate(_ params: LeafCallValues) -> LeafData { - .error(internal: "Non-mutating evaluation on mutating method") } -} - -// MARK: Internal Only - -internal extension LeafMethod { - var mutating: Bool { self as? LeafMutatingMethod != nil } -} -internal protocol LKMapMethod: LeafNonMutatingMethod, Invariant {} - -internal protocol BoolParam: LeafFunction {} -internal extension BoolParam { static var callSignatures: [LeafCallParameter] { [.bool] } } - -internal protocol IntParam: LeafFunction {} -internal extension IntParam { static var callSignatures: [LeafCallParameter] { [.int] } } - -internal protocol DoubleParam: LeafFunction {} -internal extension DoubleParam { static var callSignatures: [LeafCallParameter] { [.double] } } - -internal protocol StringParam: LeafFunction {} -internal extension StringParam { static var callSignature: [LeafCallParameter] { [.string] } } - -internal protocol StringStringParam: LeafFunction {} -internal extension StringStringParam { static var callSignature: [LeafCallParameter] { [.string, .string] } } - -internal protocol DictionaryParam: LeafFunction {} -internal extension DictionaryParam { static var callSignature: [LeafCallParameter] { [.dictionary] } } - -internal protocol ArrayParam: LeafFunction {} -internal extension ArrayParam { static var callSignature: [LeafCallParameter] { [.array] } } - -internal protocol CollectionsParam: LeafFunction {} -internal extension CollectionsParam { static var callSignature: [LeafCallParameter] { [.collections] } } diff --git a/Sources/LeafKit/LeafEntities/LeafUnsafeEntity.swift b/Sources/LeafKit/LeafEntities/LeafUnsafeEntity.swift deleted file mode 100644 index 71f9ec86..00000000 --- a/Sources/LeafKit/LeafEntities/LeafUnsafeEntity.swift +++ /dev/null @@ -1,17 +0,0 @@ -/// A function, method, or block that desires access to application-provided unsafe data stores may -/// optionally adhere to `LeafUnsafeFunction` to be provided with a dictionary of data that was -/// previously registered to the `LeafRenderer` in configuration prior to it being called via any relevant -/// evaluation function appropriate for the type. If no such dictionary was registered or user has configured -/// LeafKit to disallow unsafe object access to custom tags, value will be nil. -/// -/// Any structures so passed *may be reference types* if so configured - no guarantees are made, and -/// using such an unsafe entity on a non-threadsafe stored value may cause runtime issues. -public protocol LeafUnsafeEntity: LeafFunction { - var unsafeObjects: UnsafeObjects? { get set } -} - -public typealias UnsafeObjects = [String: Any] - -public extension LeafUnsafeEntity { - static var invariant: Bool { false } -} diff --git a/Sources/LeafKit/LeafEntities/Sanity.swift b/Sources/LeafKit/LeafEntities/Sanity.swift deleted file mode 100644 index 0a8fb9b7..00000000 --- a/Sources/LeafKit/LeafEntities/Sanity.swift +++ /dev/null @@ -1,166 +0,0 @@ -// MARK: - Internal Sanity Checkers - -internal extension String { - func _sanity() { - precondition(!isLeafKeyword, "Name cannot be Leaf keyword") - precondition(isValidLeafIdentifier, "Name must be valid Leaf identifier") - } -} - -internal extension LeafMethod { - /// Verify that the method's signature isn't empty and passes sanity - static func _sanity() { - let m = Self.self is LeafMutatingMethod.Type - let nm = Self.self is LeafNonMutatingMethod.Type - precondition(m != nm, - "Adhere strictly to one and only one of LeafMutating/NonMutatingMethod") - precondition(!callSignature.isEmpty, - "Method must have at least one parameter") - precondition(callSignature.first!.label == nil, - "Method's first parameter cannot be labeled") - precondition(callSignature.first!.defaultValue == nil, - "Method's first parameter cannot be defaulted") - precondition(m ? !invariant : true, - "Mutating methods cannot be invariant") - callSignature._sanity() - } -} - -internal extension Array where Element == LeafCallParameter { - /// Veryify the `CallParameters` is valid - func _sanity() { - precondition(self.count < 256, - "Functions may not have more than 255 parameters") - precondition(0 == self.compactMap({$0.label}).count - - Set(self.compactMap { $0.label }).count, - "Labels must be unique") - precondition(self.enumerated().allSatisfy({ - $0.element.label != nil || - $0.offset < self.enumerated().first(where: - {$0.element.label != nil})? - .offset ?? endIndex}), - "All after first labeled parameter must also be labeled") - precondition(self.enumerated().allSatisfy({ - $0.element.defaultValue != nil || - $0.offset < self.enumerated().first(where: - {$0.element.defaultValue != nil})? - .offset ?? endIndex}), - "All after first defaulted parameter must also be defaulted") - } - - /// Compare two signatures and return true if they can be confused - func confusable(with: Self) -> Bool { - if isEmpty && with.isEmpty { return true } - - var s = self - var w = with - if s.count < w.count { swap(&s, &w) } - - let map = s.indices.map { (s[$0], $0 < w.count ? w[$0] : nil) } - - var index = 0 - var a: LeafCallParameter { map[index].0 } - var b: LeafCallParameter? { map[index].1 } - - while index < map.count { - defer { index += 1 } - if let b = b { - /// If both defaulted, ambiguous - if a.defaulted && b.defaulted { return true } - /// One of the two is defaulted. As long as label is different, that's ok - if a.defaulted || b.defaulted { return a.label == b.label } - /// Neither is defaulted. - /// If the labels are not the same, it's unambiguous - if a.label != b.label { return false } - /// ... or if no shared types overlap. - if a.types.intersection(b.types).isEmpty { return false } - } - /// If shorter sig is out of params, a's defaulted state determines ambiguity - else { return a.defaulted } - } - /// If we've exhausted (equal number of params, it's ambiguous - return true - } - - /// If the signature can accept an empty call signature, whether because empty or fully defaulted - var emptyParamSig: Bool { - !filter { $0.defaultValue != nil }.isEmpty - } -} - -internal extension ParseSignatures { - func _sanity() { - precondition(self.values.enumerated().allSatisfy { sig in - self.values.enumerated() - .filter { $0.offset > sig.offset } - .allSatisfy { $0 != sig } - }, - "Parse signatures must be unique") - self.values.forEach { $0.forEach { $0._sanity() } } - } -} - -internal extension LeafParseParameter { - func _sanity(_ depth: Int = 0) { - switch self { - case .callParameter, .keyword, .unscopedVariable: return - case .literal: - preconditionFailure(""" - Do not use .literal in parse signatures: - `instantiate` will receive it in place of `unscopedVariable` - """) - case .expression(let e): - precondition(depth == 0, "Expression only allowed at top level of signature ") - precondition((2...3).contains(e.count), "Expression must have 2 or 3 parts") - e.forEach { $0._sanity(1) } - case .tuple(let t): - precondition(depth == 1, "Tuple only allowed when nested in expression") - t.forEach { $0._sanity(2) } - } - } -} - -internal extension Array where Element == LeafParseParameter { - /// Given a specific parseSignature and a parsed tuple, attempt to split into parse parameters & call tuple or nil if not a match - func splitTuple(_ tuple: LKTuple) -> ([String], LKTuple)? { - var parse: [String] = [] - var call: LKTuple = .init() - - guard self.count == tuple.count else { return nil } - var index = 0 - var t: (label: String?, value: LKParameter) { tuple.enumerated[index] } - var s: LeafParseParameter { self[index] } - while index < self.count { - switch (s, t.label, t.value.container) { - /// Valued parameters where call parameter is expected - case (.callParameter, .none, _) where t.value.isValued: - call.values.append(t.value) - case (.callParameter, .some, _) where t.value.isValued: - call.labels[t.label!] = call.count - call.values.append(t.value) - /// Signature expects a keyword (can't be labeled) - case (.keyword(let kSet), nil, .keyword(let k)) - where kSet.contains(k): break - /// Signature expects an unscoped variable (can't be labeled) - case (.unscopedVariable, nil, .variable(let v)) where v.isAtomic: - parse.append(String(v.member!)) - case (.expression(let sE), nil, .expression(let tE)) - where tE.form.exp == .custom: - let extract: LKTuple = .init([tE.first, tE.second, tE.third].compactMap {$0 != nil ? (nil, $0!) : nil}) - guard let more = sE.splitTuple(extract) else { return nil } - parse.append(contentsOf: more.0) - call.append(more.1) - case (.tuple(let sT), nil, .tuple(let tT)) - where sT.count == tT.count: - guard let more = sT.splitTuple(tT) else { return nil } - parse.append(contentsOf: more.0) - call.append(more.1) - default: return nil - } - index += 1 - } - return (parse, call) - } -} - -extension String: Error {} diff --git a/Sources/LeafKit/LeafError.swift b/Sources/LeafKit/LeafError.swift index b4b7d603..0412296e 100644 --- a/Sources/LeafKit/LeafError.swift +++ b/Sources/LeafKit/LeafError.swift @@ -1,28 +1,20 @@ // MARK: Subject to change prior to 1.0.0 release // MARK: - -import Foundation - // MARK: `LeafError` Summary -public typealias LeafErrorCause = LeafError.Reason -public typealias LexErrorCause = LexError.Reason -public typealias ParseErrorCause = ParseError.Reason - /// `LeafError` reports errors during the template rendering process, wrapping more specific /// errors if necessary during Lexing and Parsing stages. /// /// #TODO /// - Implement a ParserError subtype -public struct LeafError: LocalizedError, CustomStringConvertible { +public struct LeafError: Error { /// Possible cases of a LeafError.Reason, with applicable stored values where useful for the type public enum Reason { // MARK: Errors related to loading raw templates - case noSources - case noSourceForKey(String, invalid: Bool = false) /// Attempted to access a template blocked for security reasons - case illegalAccess(String, NIOLeafFiles.Limit = .toVisibleFiles) - + case illegalAccess(String) + // MARK: Errors related to LeafCache access /// Attempt to modify cache entries when caching is globally disabled case cachingDisabled @@ -38,9 +30,6 @@ public struct LeafError: LocalizedError, CustomStringConvertible { /// Attempt to render a non-flat AST /// - Provide template name & array of unresolved references case unresolvedAST(String, [String]) - /// Attempt to render a non-flat AST - /// - Provide raw file name needed - case missingRaw(String) /// Attempt to render a non-existant template /// Provide template name case noTemplateExists(String) @@ -50,28 +39,18 @@ public struct LeafError: LocalizedError, CustomStringConvertible { // MARK: Wrapped Errors related to Lexing or Parsing /// Errors due to malformed template syntax or grammar - case lexError(LexError) + case lexerError(LexerError) /// Errors due to malformed template syntax or grammar - case parseError(ParseError) - /// Warnings from parsing, if escalated to an error - case parseWarnings([ParseError]) - /// Errors from serializing to a LKRawBlock - case serializeError(LeafFunction.Type, Error, SourceLocation) - - case invalidIdentifier(String) - - /// Error due to timeout (may or may not be permanent) - case timeout(Double) + // FIXME: Implement a specific ParserError type + // case parserError(ParserError) // MARK: Errors lacking specificity - /// General errors occuring prior to running LeafKit - case configurationError(String) /// Errors from protocol adherents that do not support newer features case unsupportedFeature(String) /// Errors only when no existing error reason is adequately clear case unknownError(String) } - + /// Source file name causing error public let file: String /// Source function causing error @@ -82,60 +61,48 @@ public struct LeafError: LocalizedError, CustomStringConvertible { public let column: UInt /// The specific reason for the error public let reason: Reason + /// Provide a custom description of the `LeafError` based on type. /// /// - Where errors are caused by toolchain faults, will report the Swift source code location of the call /// - Where errors are from Lex or Parse errors, will report the template source location of the error - public var localizedDescription: String { - var m = "\(file.split(separator: "/").last ?? "?").\(function):\(line)\n" - switch reason { - case .illegalAccess(let f, let l) : m += l.contains(.toVisibleFiles) ? "Attempted to access hidden file " - : "Attempted to escape sandbox " - m += "`\(f)`" - case .noSources : m += "No searchable sources exist" - case .noSourceForKey(let s,let b) : m += b ? "`\(s)` is invalid source key" : "No source `\(s)` exists" - case .unknownError(let r) : m += r - case .unsupportedFeature(let f) : m += "`\(f)` not implemented" - case .cachingDisabled : m += "Caching is globally disabled" - case .keyExists(let k) : m += "Existing entry `\(k)`" - case .noValueForKey(let k) : m += "No cache entry exists for `\(k)`" - case .noTemplateExists(let k) : m += "No template found for `\(k)`" - case .unresolvedAST(let k, let d) : m += "\(k) has unresolved dependencies: \(d)" - case .timeout(let d) : m += "Exceeded timeout at \(d.formatSeconds())" - case .configurationError(let d) : m += "Configuration error: `\(d)`" - case .missingRaw(let f) : m += "Missing raw inline file ``\(f)``" - case .invalidIdentifier(let i) : m += "`\(i)` is not a valid Leaf identifier" - case .cyclicalReference(let k, let c) - : m += "`\(k)` cyclically referenced in [\((c + ["!\(k)"]).joined(separator: " -> "))]" - - case .lexError(let e) : m = "Lexing error\n\(e.description)" - case .parseError(let e) : m = "Parse \(e.description)" - case .serializeError(let f, let e, let l) : - m = """ - Serializing error - Error from \(f) in template "\(l.name)" while appending data at \(l.line):\(l.column): - \(e.localizedDescription) - """ - case .parseWarnings(let w) : - guard !w.isEmpty else { break } - m = """ - Template "\(w.first!.name)" Parse Warning\(w.count > 0 ? "s" : ""): - \(w.map {"\($0.line):\($0.column) - \($0.reason.description)"}.joined(separator: "\n")) - """ + var localizedDescription: String { + let file = self.file.split(separator: "/").last + let src = "\(file ?? "?").\(function):\(line)" + + switch self.reason { + case .illegalAccess(let message): + return "\(src) - \(message)" + case .unknownError(let message): + return "\(src) - \(message)" + case .unsupportedFeature(let feature): + return "\(src) - \(feature) is not implemented" + case .cachingDisabled: + return "\(src) - Caching is globally disabled" + case .keyExists(let key): + return "\(src) - Existing entry \(key); use insert with replace=true to overrride" + case .noValueForKey(let key): + return "\(src) - No cache entry exists for \(key)" + case .unresolvedAST(let key, let dependencies): + return "\(src) - Flat AST expected; \(key) has unresolved dependencies: \(dependencies)" + case .noTemplateExists(let key): + return "\(src) - No template found for \(key)" + case .cyclicalReference(let key, let chain): + return "\(src) - \(key) cyclically referenced in [\(chain.joined(separator: " -> "))]" + case .lexerError(let e): + return "Lexing error - \(e.localizedDescription)" } - return m } - - public var errorDescription: String? { localizedDescription } - public var description: String { localizedDescription } - + /// Create a `LeafError` - only `reason` typically used as source locations are auto-grabbed - public init(_ reason: Reason, - _ file: String = #file, - _ function: String = #function, - _ line: UInt = #line, - _ column: UInt = #column) { + public init( + _ reason: Reason, + file: String = #file, + function: String = #function, + line: UInt = #line, + column: UInt = #column + ) { self.file = file self.function = function self.line = line @@ -147,207 +114,63 @@ public struct LeafError: LocalizedError, CustomStringConvertible { // MARK: - `LexerError` Summary (Wrapped by LeafError) /// `LexerError` reports errors during the stage. -public struct LexError: Error, CustomStringConvertible { +public struct LexerError: Error { // MARK: - Public - - public enum Reason: CustomStringConvertible { + + public enum Reason { // MARK: Errors occuring during Lexing /// A character not usable in parameters is present when Lexer is not expecting it case invalidParameterToken(Character) - /// An invalid operator was used - case invalidOperator(LeafOperator) - /// A string was opened but never terminated by end of line + /// A string was opened but never terminated by end of file case unterminatedStringLiteral /// Use in place of fatalError to indicate extreme issue case unknownError(String) - - public var description: String { - switch self { - case .invalidOperator(let o): return "`\(o)` is not a valid operator" - case .invalidParameterToken(let c): return "`\(c)` is not meaningful in context" - case .unknownError(let e): return e - case .unterminatedStringLiteral: return "Unterminated string literal" - } - } } - - /// Stated reason for error - public let reason: Reason - /// Name of template error occured in - public var name: String { sourceLocation.name } + /// Template source file line where error occured - public var line: Int { sourceLocation.line } + public let line: Int /// Template source column where error occured - public var column: Int { sourceLocation.column } + public let column: Int + /// Name of template error occured in + public let name: String + /// Stated reason for error + public let reason: Reason - /// Template source location where error occured - internal let sourceLocation: SourceLocation - // MARK: - Internal Only - + /// State of tokens already processed by Lexer prior to error - internal let lexed: [LKToken] - + internal let lexed: [LeafToken] + /// Flag to true if lexing error is something that may be recoverable during parsing; + /// EG, `"#anhtmlanchor"` may lex as a tag name but fail to tokenize to tag because it isn't + /// followed by a left paren. Parser may be able to recover by decaying it to `.raw`. + internal let recoverable: Bool + /// Create a `LexerError` /// - Parameters: /// - reason: The specific reason for the error /// - src: File being lexed - /// - lexed: `LKTokens` already lexed prior to error + /// - lexed: `LeafTokens` already lexed prior to error /// - recoverable: Flag to say whether the error can potentially be recovered during Parse - internal init(_ reason: Reason, - _ src: LKRawTemplate, - _ lexed: [LKToken] = []) { + internal init( + _ reason: Reason, + src: LeafRawTemplate, + lexed: [LeafToken] = [], + recoverable: Bool = false + ) { + self.line = src.line + self.column = src.column self.reason = reason self.lexed = lexed - self.sourceLocation = src.state - } - - /// Convenience description of source file name, error reason, and location in file of error source - var localizedDescription: String { "Error in template \"\(name)\" - \(line):\(column)\n\(reason.description)" } - public var description: String { localizedDescription } -} - -// MARK: - `ParserError` Summary (Wrapped by LeafError) -/// `ParserError` reports errors during the stage. -public struct ParseError: Error, CustomStringConvertible { - public enum Reason: Error, CustomStringConvertible { - case noEntity(type: String, name: String) - case sameName(type: String, name: String, params: String, matches: [String]) - case mutatingMismatch(name: String) - case cantClose(name: String, open: String?) - case parameterError(name: String, reason: String) - case unset(String) - case declared(String) - case unknownError(String) - case missingKey - case missingValue(isDict: Bool) - case noPostfixOperand - case unvaluedOperand - case missingOperator - case missingIdentifier - case invalidIdentifier(String) - case invalidDeclaration - case malformedExpression - case noSubscript - case keyMismatch - case constant(String, mutate: Bool = false) - - public var description: String { - switch self { - case .constant(let v, let m): return "Can't \(m ? "mutate" : "assign"); `\(v)` is constant" - case .keyMismatch: return "Subscripting accessor is wrong type for object" - case .noSubscript: return "Non-collection objects cannot be subscripted" - case .malformedExpression: return "Couldn't close expression" - case .invalidIdentifier(let s): return "`\(s)` is not a valid Leaf identifier" - case .invalidDeclaration: return "Variable declaration may only occur at start of top level expression" - case .missingIdentifier: return "Missing expected identifier in expression" - case .missingOperator: return "Missing valid operator between operands" - case .unvaluedOperand: return "Can't operate on non-valued operands" - case .noPostfixOperand: return "Missing operand for postfix operator" - case .missingKey: return "Collection literal missing key" - case .missingValue(let dict): return "\(dict ? "Dictionary" : "Array") literal missing value" - case .unknownError(let e): return e - case .unset(let v): return "Variable `\(v)` used before initialization" - case .declared(let v): return "Variable `\(v)` is already declared in this scope" - case .cantClose(let n, let o): - return o.map { "`\(n)` can't close `\($0)`" } - ?? "No open block matching `\(n)` to close" - case .mutatingMismatch(let name): - return "Mutating methods exist for \(name) but operand is immutable" - case .parameterError(let name, let reason): - return "\(name)(...) couldn't be parsed: \(reason)" - case .noEntity(let t, let name): - return "No \(t) named `\(name)` exists" - case .sameName(let t, let name, let params, let matches): - return "No exact match for \(t) \(name + params); \(matches.count) possible matches: \(matches.map { "\(name)\($0)" }.joined(separator: "\n"))" - } - } - } - - public let reason: Reason - public let recoverable: Bool - - /// Name of template error occured in - public var name: String { sourceLocation.name } - /// Template source file line where error occured - public var line: Int { sourceLocation.line } - /// Template source column where error occured - public var column: Int { sourceLocation.column } - - /// Template source location where error occured - let sourceLocation: SourceLocation - - - init(_ reason: Reason, - _ location: SourceLocation, - _ recoverable: Bool = false) { - self.reason = reason - self.sourceLocation = location + self.name = src.name self.recoverable = recoverable } - static func error(_ reason: String, _ location: SourceLocation) -> Self { - .init(.unknownError(reason), location, false) } - static func error(_ reason: Reason, _ location: SourceLocation) -> Self { - .init(reason, location, false) } - static func warning(_ reason: Reason, _ location: SourceLocation) -> Self { - .init(reason, location, true) } - /// Convenience description of source file name, error reason, and location in file of error source - var localizedDescription: String { "\(recoverable ? "Warning" : "Error") in template \"\(name)\"\n\(line):\(column) - \(reason.description)" } - public var description: String { localizedDescription } -} - -// MARK: - Internal Conveniences - -extension Error { - var leafError: LeafError? { self as? LeafError } -} - -@inline(__always) -func err(_ cause: LeafErrorCause, - _ file: String = #file, - _ function: String = #function, - _ line: UInt = #line, - _ column: UInt = #column) -> LeafError { .init(cause, String(file.split(separator: "/").last ?? ""), function, line, column) } - -@inline(__always) -func err(_ reason: String, - _ file: String = #file, - _ function: String = #function, - _ line: UInt = #line, - _ column: UInt = #column) -> LeafError { err(.unknownError(reason), file, function, line, column) } - -@inline(__always) -func parseErr(_ cause: ParseErrorCause, - _ location: SourceLocation, - _ recoverable: Bool = false) -> LeafError { - .init(.parseError(.init(cause, location, recoverable))) } - -@inline(__always) -func succeed(_ value: T, on eL: EventLoop) -> ELF { eL.makeSucceededFuture(value) } - -@inline(__always) -func fail(_ error: LeafError, on eL: EventLoop) -> ELF { eL.makeFailedFuture(error) } - -@inline(__always) -func fail(_ error: LeafErrorCause, on eL: EventLoop, - _ file: String = #file, _ function: String = #function, - _ line: UInt = #line, _ column: UInt = #column) -> ELF { - fail(LeafError(error, file, function, line, column), on: eL) } - -func __MajorBug(_ message: String = "Unspecified", - _ file: String = #file, - _ function: String = #function, - _ line: UInt = #line) -> Never { - fatalError(""" - LeafKit Major Bug: "\(message)" - Please File Issue Immediately at https://github.com/vapor/leaf-kit/issues - - Reference "fatalError in `\(file.split(separator: "/").last ?? "").\(function) line \(line)`" - """) + var localizedDescription: String { + return "\"\(name)\": \(reason) - \(line):\(column)" + } } -func __Unreachable(_ file: String = #file, - _ function: String = #function, - _ line: UInt = #line) -> Never { - __MajorBug("Unreachable Switch Case", file, function, line) } +// MARK: - `ParserError` Summary (Wrapped by LeafError) +// FIXME: Implement a specific ParserError type +/// `ParserError` reports errors during the stage. diff --git a/Sources/LeafKit/LeafLexer/Character.swift b/Sources/LeafKit/LeafLexer/Character.swift deleted file mode 100644 index 4828edea..00000000 --- a/Sources/LeafKit/LeafLexer/Character.swift +++ /dev/null @@ -1,72 +0,0 @@ -/// Various internal helper identities for convenience -internal extension Character { - // MARK: - LKToken specific identities - - var canStartIdentifier: Bool { isLowercaseLetter || isUppercaseLetter || self == .underscore } - var isValidInIdentifier: Bool { canStartIdentifier || isDecimal } - - var isValidInParameter: Bool { isValidInIdentifier || isValidOperator || isValidInNumeric } - - var isValidOperator: Bool { LeafOperator.validCharacters.contains(self) } - - var canStartNumeric: Bool { isDecimal } - var isValidInNumeric: Bool { - if isHexadecimal { return true } - return [.binaryNotation, .octalNotation, .hexNotation, - .underscore, .period].contains(self) - } - - var isWhiteSpace: Bool { [.newLine, .space, .tab].contains(self) } - - // MARK: - General group-membership identities - var isUppercaseLetter: Bool { (.A ... .Z ) ~= self } - var isLowercaseLetter: Bool { (.a ... .z ) ~= self } - - var isBinary: Bool { (.zero ... .one ) ~= self } - var isOctal: Bool { (.zero ... .seven ) ~= self } - var isDecimal: Bool { (.zero ... .nine ) ~= self } - var isHexadecimal: Bool { isDecimal ? true - : (.A ... .F) ~= uppercased().first! } - - // MARK: - General static identities - static let newLine = "\n".first! - static let quote = "\"".first! - static let apostrophe = "'".first! - static let backSlash = "\\".first! - static let leftParenthesis = "(".first! - static let rightParenthesis = ")".first! - static let leftBracket = "[".first! - static let rightBracket = "]".first! - static let comma = ",".first! - static let space = " ".first! - static let tab = "\t".first! - static let colon = ":".first! - static let period = ".".first! - static let A = "A".first! - static let F = "F".first! - static let Z = "Z".first! - static let a = "a".first! - static let z = "z".first! - - static let zero = "0".first! - static let one = "1".first! - static let seven = "7".first! - static let nine = "9".first! - static let binaryNotation = "b".first! - static let octalNotation = "o".first! - static let hexNotation = "x".first! - - static let plus = "+".first! - static let minus = "-".first! - static let star = "*".first! - static let forwardSlash = "/".first! - static let equals = "=".first! - static let exclamation = "!".first! - static let lessThan = "<".first! - static let greaterThan = ">".first! - static let ampersand = "&".first! - static let vertical = "|".first! - static let underscore = "_".first! - static let modulo = "%".first! - static let upcaret = "^".first! -} diff --git a/Sources/LeafKit/LeafLexer/LKLexer.swift b/Sources/LeafKit/LeafLexer/LKLexer.swift deleted file mode 100644 index b8b8668a..00000000 --- a/Sources/LeafKit/LeafLexer/LKLexer.swift +++ /dev/null @@ -1,338 +0,0 @@ -/// `LKLexer` is an opaque structure that wraps the lexing logic of Leaf-Kit. -/// -/// Initialized with a `LKRawTemplate` (raw string-providing representation of a file or other source), -/// used by evaluating with `LKLexer.lex()` and either erroring or returning `[LKToken]` -internal struct LKLexer { - // MARK: - Internal Initializers - - /// Init with `LKRawTemplate` - init(_ template: LKRawTemplate) { - self.src = template - self.state = .raw - self.entities = LKConf.entities - self.tagMark = LKConf.tagIndicator - self.lastSourceLocation = (template.state.name, 1, 1) - } - - // MARK: - Internal - - /// Lex the stored `LKRawTemplate` - /// - Throws: `LeafError` - /// - Returns: An array of fully built `LKTokens`, to then be parsed by `LKParser` - mutating func lex() throws -> [LKToken] { - while let next = try nextToken() { append(next) } - return lexed - } - - // MARK: - Private Only - Stored/Computed Properties - private enum State { - /// Parse as raw, until it finds `#` (but consuming escaped `\#`) - case raw - /// Start attempting to sequence tag-viable tokens (tagName, parameters, etc) - case tag - /// Start attempting to sequence parameters - case parameters - /// Start attempting to sequence a tag body - case body - } - - /// Current state of the Lexer - private var state: State - /// Current parameter depth, when in a Parameter-lexing state - private var depth = 0 - /// Current index in `lexed` - private var offset: Int { lexed.count - 1 } - /// Stream of `LKTokens` that have been successfully lexed - private var lexed: [LKToken] = [] - /// The originating template source content (ie, raw characters) - private var src: LKRawTemplate - /// Configured entitites - private let entities: LeafEntities - - private var lastSourceLocation: SourceLocation - - /// Convenience for the current character to read - private var current: Character? { src.peek() } - - /// Convenience to pop the current character - @discardableResult - mutating private func pop() -> Character? { src.pop() } - - private var tagMark: Character - /// Convenience for an escaped tagIndicator token - private var escapedTagMark: LKToken.Container { .raw(tagMark.description) } - - // MARK: - Private - Actual implementation of Lexer - - private mutating func nextToken() throws -> LKToken.Container? { - // if EOF, return nil - no more to read - guard let first = current else { return nil } - - switch state { - case .raw where first == tagMark - : return lexCheckTagIndicator() - case .tag where first.canStartIdentifier - : return try lexNamedTag() - case .raw : return lexRaw() - case .tag : return lexAnonymousTag() - case .parameters : var part: LKToken.Container? - repeat { part = try lexParameters() } - while part == nil && current != nil - if let paramPart = part { return paramPart } - throw unknownError("Template ended on open parameters") - default : throw unknownError("Template cannot be lexed") - } - } - - private mutating func lexAnonymousTag() -> LKToken.Container { - state = .parameters - depth = 0 - return .tag(nil) - } - - private mutating func lexNamedTag() throws -> LKToken.Container { - let id = src.readWhile { $0.isValidInIdentifier } - - /// If not a recognized identifier decay to raw and rewrite tagIndicator - guard entities.openers.contains(id) || - entities.closers.contains(id) || - current == .leftParenthesis else { - lexed.removeLast() - append(escapedTagMark) - state = .raw; - return .raw(id) - } - - /// If the tag has parameters, and if it's a "closer" (end_xxxx, chained terminal tag eg: else) - let xor = (params: current == .leftParenthesis, - terminal: entities.closers.contains(id)) - switch xor { - /// Terminal chained tags can't have params (eg, else) - case (true , true ) : throw unknownError("Closing tags can't have parameters") - /// A normal tag call *must* have parameters, even if empty - case (false, false) : throw unknownError("Tags must have parameters") - /// Atomic function/block normal case - case (true , false) : state = .parameters - depth = 0 - return .tag(id) - /// Terminal chained tag normal case - case (false, true ) where entities.openers.contains(id) - : if pop() != .colon { - throw unknownError("Chained block missing `:`") } - append(.tag(id)) - state = .raw - return .blockMark - /// End tag normal case - case (false, true ) : state = .raw - return .tag(id) - } - } - - /// Consume all data until hitting a `tagIndicator` that might open a tag/expression, escaping backslashed - private mutating func lexRaw() -> LKToken.Container { - var slice = "" - scan: - while let first = current { - let peek = src.peek(aheadBy: 1) ?? .backSlash /// Magic - \ can't be an ID start - switch first { - case tagMark where peek.canStartIdentifier || peek == .leftParenthesis - : break scan - case .backSlash where peek == tagMark - : pop() - fallthrough - case tagMark, .backSlash - : slice.append(src.pop()!) - default : slice += src.readWhileNot([tagMark, .backSlash]) - } - } - return .raw(slice) - } - - /// Consume `#`, change state to `.tag` or `.raw`, return appropriate token - private mutating func lexCheckTagIndicator() -> LKToken.Container { - pop() - let valid = current == .leftParenthesis || current?.canStartIdentifier ?? false - state = valid ? .tag : .raw - return valid ? .tagMark : escapedTagMark - } - - /// Parameter lexing - very monolithic, would be nice to break this up. - private mutating func lexParameters() throws -> LKToken.Container? { - /// Consume first character regardless of what it is - let first = pop()! - - /// Simple returning cases - .parametersStart/Delimiter/End, .literal(.string()), ParameterToken, space/comment discard - switch first { - case .tab, .newLine, .space - : let x = src.readWhile {$0.isWhitespace} - return retainWhitespace ? .whiteSpace(x) : nil - case .leftParenthesis : depth += 1 - return .paramsStart - case .rightParenthesis where depth > 1 - : depth -= 1 - return .paramsEnd - case .rightParenthesis : state = .raw - let body = current == .colon - if body { pop() - append(.paramsEnd) - return .blockMark - } else { return .paramsEnd } - case .comma : return .paramDelimit - case .colon where [.paramsStart, - .paramDelimit, - .param(.operator(.subOpen))].contains(lexed[offset - 1].token) - : return .labelMark - case .leftBracket where current == .rightBracket - : pop() - return .param(.literal(.emptyArray)) - case .leftBracket where current == .colon - : pop() - if current == .rightBracket { - pop() - return .param(.literal(.emptyDict)) - } else { throw unknownError("Expected empty dictionary literal") } - case .underscore where current?.isWhitespace ?? false - : return .param(.keyword(._)) - case .quote : - var accumulate = src.readWhileNot([.quote, .newLine]) - while accumulate.last == .backSlash && current == .quote { - accumulate.removeLast() - accumulate += pop()!.description - accumulate += src.readWhileNot([.quote, .newLine]) - } - if pop() != .quote { throw unterminatedString } - return .param(.literal(.string(accumulate))) - case tagMark : /// A comment - silently discard - var x = src.readWhileNot([tagMark]) - while x.last == .backSlash { - /// Read until hitting an unescaped tagIndicator - if current == tagMark { pop() } - if current != nil { x = src.readWhileNot([tagMark]) } - } - if current == nil { throw unknownError("Template ended in open comment") } - pop() - return nil - default : break - } - - /// Complex ParameterToken lexing situations - enhanced to allow non-space separated values - /// Complicated by overlap in acceptable isValidInParameter characters between possible types - /// Process from most restrictive options to least to help prevent overly aggressive tokens - /// Possible results, most restrictive to least - /// * Operator - /// * Constant(Int) - /// * Constant(Double) - /// * Keyword - /// * Function Identifier - /// * Variable Part Identifier - - /// If current character isn't valid for any kind of parameter, something's majorly wrong - if !first.isValidInParameter { throw badToken(first) } - /// Ensure peeking by one can always be unwrapped - if current == nil { throw unknownError("Open parameters") } - - /// Test for Operator first - this will only handle max two character operators, not ideal - /// Can't switch on this, MUST happen before trying to read tags - if first.isValidOperator { - /// Try to get a 2char Op first, then a 1 char Op if can't do 2 - let twoOp = LeafOperator(rawValue: String([first, current!])) - let op = twoOp != nil ? twoOp! : LeafOperator(rawValue: String(first))! - guard op.lexable else { throw badOperator(op) } - if twoOp != nil { pop() } - /// Handle ops that require no whitespace on one/both sides (does not handle subOpen leading) - if [.evaluate, .scopeMember, .scopeRoot].contains(op) { - if current!.isWhitespace { - throw unknownError("\(op) may not have trailing whitespace") } - if op == .scopeMember, case .whiteSpace(_) = lexed[offset].token { - throw unknownError("\(op) may not have leading whitespace") } - } - return .param(.operator(op)) - } - - /// Test for numerics next. This is not very intelligent but will read base2/8/10/16 for Ints and base - /// 10/16 for decimal through native Swift initialization. Will not adequately decay to handle things - /// like `0b0A` and recognize it as an invalid numeric. - if first.canStartNumeric { - var testInt: Int? - var testDouble: Double? - var radix: Int? = nil - var sign = 1 - - let next = current! - let peekRaw = String(first) + (src.peekWhile { $0.isValidInNumeric }) - var peekNum = peekRaw.replacingOccurrences(of: String(.underscore), with: "") - /// We must be immediately preceeded by a minus to flip the sign and only flip back if - /// immediately preceeded by a const, tag or variable (which we assume will provide a - /// numeric). Grammatical errors in the template (eg, keyword-numeric) may throw here - if case .param(let p) = lexed[offset].token, - case .operator(let op) = p, op == .minus { - switch lexed[offset - 1].token { - case .param(let p): - switch p { - case .literal, - .function, - .variable : sign = 1 - case .operator : sign = -1 - case .keyword : throw badToken(.minus) - } - default: sign = -1 - } - } - - switch (peekNum.contains(.period), next, peekNum.count > 2) { - case (true, _, _) : testDouble = Double(peekNum) - case (false, .binaryNotation, true): radix = 2 - case (false, .octalNotation, true): radix = 8 - case (false, .hexNotation, true): radix = 16 - default: testInt = Int(peekNum) - } - - if let radix = radix { - let start = peekNum.startIndex - peekNum.removeSubrange(start ... peekNum.index(after: start)) - testInt = Int(peekNum, radix: radix) - } - - if testInt != nil || testDouble != nil { - // discard the minus if negative - if sign == -1 { lexed.removeLast() } - src.popWhile { $0.isValidInNumeric } - if testInt != nil { return .param(.literal(.int(testInt! * sign))) } - else { return .param(.literal(.double(testDouble! * Double(sign)))) } - } - } - - guard first.canStartIdentifier else { throw badToken(first) } - /// At this point, just read the longest possible identifier-valid part (NO operators) - let identifier = String(first) + src.readWhile { $0.isValidInIdentifier } - - /// If it's a keyword, return that - if let kw = LeafKeyword(rawValue: identifier) { return .param(.keyword(kw)) } - /// If identifier is followed by `(` it's a function or method call - if current! == .leftParenthesis { return .param(.function(identifier)) } - /// ... otherwise, a variable part - else { return .param(.variable(identifier)) } - } - - /// Signal whether whitespace should be retained (only needed currently for `[`) - private var retainWhitespace: Bool { current == .leftBracket ? true : false } - - /// Convenience for making nested `LeafError->LexerError.unknownError` - private func unknownError(_ reason: String) -> LeafError { - err(.lexError(.init(.unknownError(reason), src, lexed))) } - /// Convenience for making nested `LeafError->LexError.invalidParameterToken` - private func badToken(_ character: Character) -> LeafError { - err(.lexError(.init(.invalidParameterToken(character), src, lexed))) } - /// Convenience for making nested `LeafError->LexerError.badOperator` - private func badOperator(_ op: LeafOperator) -> LeafError { - err(.lexError(.init(.invalidOperator(op), src, lexed))) } - /// Convenience for making nested `LeafError->LexerError.untermindatedStringLiteral` - private var unterminatedString: LeafError { - err(.lexError(.init(.unterminatedStringLiteral, src, lexed))) } - - private mutating func append(_ token: LKToken.Container) { - lexed.append(.init(token, lastSourceLocation)) - lastSourceLocation = src.state - } -} - diff --git a/Sources/LeafKit/LeafLexer/LKRawTemplate.swift b/Sources/LeafKit/LeafLexer/LKRawTemplate.swift deleted file mode 100644 index aa5907ed..00000000 --- a/Sources/LeafKit/LeafLexer/LKRawTemplate.swift +++ /dev/null @@ -1,66 +0,0 @@ -// FIXME: Should really be initializable directly from `ByteBuffer` -// TODO: Make `LeafSource` return this instead of `ByteBuffer` via extension - -public typealias SourceLocation = (name: String, line: Int, column: Int) - -/// Convenience wrapper around a `String` raw source to track line & column, pop, peek & scan. -internal struct LKRawTemplate { - // MARK: - Internal Only - let body: String - var state: SourceLocation - - init(_ name: String, _ source: String) { - self.state = (name, 1, 1) - self.body = source - self.current = body.startIndex - } - - mutating func readWhile(_ check: (Character) -> Bool) -> String { - readSliceWhile(check) } - - @discardableResult - mutating func readWhileNot(_ check: Set) -> String { - readSliceWhile({!check.contains($0)}) } - - mutating func peekWhile(_ check: (Character) -> Bool) -> String { - peekSliceWhile(check) } - - @discardableResult - mutating func popWhile(_ check: (Character) -> Bool) -> Int { - readSliceWhile(check).count } - - func peek(aheadBy idx: Int = 0) -> Character? { - let peek = body.index(current, offsetBy: idx) - return peek < body.endIndex ? body[peek] : nil - } - - @discardableResult - mutating func pop() -> Character? { - guard current < body.endIndex else { return nil } - state.column = body[current] == .newLine ? 1 : state.column + 1 - state.line += body[current] == .newLine ? 1 : 0 - defer { current = body.index(after: current) } - return body[current] - } - - // MARK: - Private Only - private var current: String.Index - - mutating private func readSliceWhile(_ check: (Character) -> Bool) -> String { - var str: [Character] = [] - str.reserveCapacity(64) - while let next = peek(), check(next) { str.append(pop()!) } - return String(str) - } - - mutating private func peekSliceWhile(_ check: (Character) -> Bool) -> String { - var str: [Character] = [] - str.reserveCapacity(64) - var index = 0 - while let next = peek(aheadBy: index), check(next) { - str.append(next) - index += 1 - } - return String(str) - } -} diff --git a/Sources/LeafKit/LeafLexer/LKToken.swift b/Sources/LeafKit/LeafLexer/LKToken.swift deleted file mode 100644 index 10460346..00000000 --- a/Sources/LeafKit/LeafLexer/LKToken.swift +++ /dev/null @@ -1,191 +0,0 @@ -/// `LKToken` represents the first stage of parsing Leaf templates - a raw file/bytestream `String` -/// will be read by `LKLexer` and interpreted into `[LKToken]` representing a stream of tokens. -/// -/// # TOKEN DEFINITIONS -/// - `.raw`: A variable-length string of data that will eventually be output directly without processing -/// - `.tagMark`: The signal at top-level that a Leaf syntax object will follow. Default is `#` and while it -/// can be configured to be something else, only rare uses cases may want to do so. -/// `.tagMark` can be escaped in source templates with a backslash, which will -/// automatically be consumed by `.raw` if so. Decays to `.raw` automatically at lexing -/// when not followed by a valid function identifier or left paren indicating anonymous expr. -/// - `.tag(String?)`: The expected function/block name - in `#for(index in array)`, equivalent -/// token is `.tag("for")`. Nil value represents the insterstitial point of an -/// anonymous tag - eg, the void between `#` and `(` in `#()` -/// - `.blockMark`: Indicates the start of a scoped block from a `LeafBlock` tag - `:` -/// - `.paramsStart`: Indicates the start of an expression/function/block's parameters - `(` -/// - `.labelMark`: Indicates that preceding identifier is a label for a following parameter- `:` -/// - `.paramDelimit`: Indicates a delimter between parameters - `,` -/// - `.param(Parameter)`: Associated value enum storing a valid tag parameter. -/// - `.paramsEnd`: Indicates the end of a tag's parameters - `)` -/// - `.whiteSpace(String)`: Currently only used inside parameters, and only preserved when needed -/// to disambiguate things Lexer can't handle (eg, `[` as subscript (no -/// whitespace) versus collection literal (can accept whitespace) -/// -/// # TODO -/// - LKTokens would ideally also store the range of their location in the original source template -/// - Tracking `.whiteSpace` in .`raw` to handle regularly formatted indentation, drop extraneous \n etc -internal struct LKToken: LKPrintable, Hashable { - internal init(_ token: LKToken.Container, _ source: (String, Int, Int)) { - self.token = token - self.source = source - } - - func hash(into hasher: inout Hasher) { hasher.combine(token) } - static func == (lhs: LKToken, rhs: LKToken) -> Bool { lhs.token == rhs.token } - - let token: Container - let source: (String, Int, Int) - - enum Container: Hashable { - /// Holds a variable-length string of data that will be passed through with no processing - case raw(String) - - /// `#` (or as configured) - Top-level signal that indicates a Leaf tag/syntax object will follow. - case tagMark - - /// Holds the name of an expected tag or syntax object (eg, `for`) in `#for(index in array)` - /// - /// - Nil: Anonymous tag (top level expression) - /// - Non-nil: A named function or block, or an endblock tag - case tag(String?) - /// `:` - Indicates the start of a scoped body-bearing block - case blockMark - - /// `(` - Indicates the start of a tag's parameters - case paramsStart - /// `:` - Indicates a delineation of `label : value` in parameters - case labelMark - /// `,` - Indicates separation of a tag's parameters - case paramDelimit - /// Holds a `ParameterToken` enum - case param(Parameter) - /// `)` - Indicates the end of a tag's parameters - case paramsEnd - - /// A stream of consecutive white space (currently only used inside parameters) - case whiteSpace(String) - } - - - /// Returns `"tokenCase"` or `"tokenCase(valueAsString)"` if associated value - var description: String { - switch token { - case .raw(let r) : return "\(short)(\(r.debugDescription))" - case .tag(.some(let t)) : return "\(short)(\"\(t)\")" - case .param(let p) : return "\(short)(\(p.description))" - default : return short - } - } - - /// Token case - var short: String { - switch token { - case .raw : return "raw" - case .tagMark : return "tagIndicator" - case .tag(.none) : return "expression" - case .tag(.some) : return "function" - case .blockMark : return "blockIndicator" - case .paramsStart : return "parametersStart" - case .labelMark : return "labelIndicator" - case .paramsEnd : return "parametersEnd" - case .paramDelimit : return "parameterDelimiter" - case .param : return "parameter" - case .whiteSpace : return "whiteSpace" - } - } - - var isTagMark: Bool { token == .tagMark } - - /// A token that represents the valid objects that will be lexed inside parameters - enum Parameter: LKPrintable, Hashable { - /// Any tokenized literal value with a native Swift type - /// - /// ``` - /// case int(Int) // A Swift `Int` - /// case double(Double) // A Swift `Double` - /// case string(String) // A Swift `String` - /// case emptyArray // A Swift `[]` - /// case emptyDict // A Swift `[:]` - case literal(Literal) - /// Any Leaf keyword with no restrictions - case keyword(LeafKeyword) - /// Any Leaf operator - case `operator`(LeafOperator) - /// A single part of a variable scope - must be non-empty - case variable(String) - /// An identifier signifying a function or method name - must be non-empty - case function(String) - - /// Returns `parameterCase(parameterValue)` - var description: String { - switch self { - case .literal(let c) : return "literal(\(c.description))" - case .variable(let v) : return "variable(part: \(v))" - case .keyword(let k) : return "keyword(.\(k.short))" - case .operator(let o) : return "operator(\(o.description))" - case .function(let f) : return "function(id: \"\(f)\")" - } - } - - /// Returns `parameterValue` or `"parameterValue"` as appropriate for type - var short: String { - switch self { - case .literal(let c) : return "lit(\(c.short))" - case .variable(let v) : return "var(\(v))" - case .keyword(let k) : return "kw(.\(k.short))" - case .operator(let o) : return "op(\(o.short))" - case .function(let f) : return "func(\(f))" - } - } - - /// Any tokenized literal value with a native Swift type - /// - /// ``` - /// case int(Int) // A Swift `Int` - /// case double(Double) // A Swift `Double` - /// case string(String) // A Swift `String` - /// case emptyArray // A Swift `[]` - /// case emptyDict // A Swift `[:]` - enum Literal: LKPrintable, LeafDataRepresentable, Hashable { - /// A Swift `Int` - case int(Int) - /// A Swift `Double` - case double(Double) - /// A Swift `String` - case string(String) - /// A Swift `Array` - only used to disambiguate empty array literal - case emptyArray - /// A Swift `Dictionary` - only used to disambiguate empty array literal - case emptyDict - - var description: String { - switch self { - case .int(let i) : return "Int: \(i.description)" - case .double(let d) : return "Double: \(d.description)" - case .string(let s) : return "String: \"\(s)\"" - case .emptyArray : return "Array (empty)" - case .emptyDict : return "Dictionary (empty)" - } - } - var short: String { - switch self { - case .int(let i) : return i.description - case .double(let d) : return d.description - case .string(let s) : return "\"\(s)\"" - case .emptyArray : return "[]" - case .emptyDict : return "[:]" - } - } - - var leafData: LeafData { - switch self { - case .int(let i) : return .int(i) - case .double(let d) : return .double(d) - case .string(let s) : return .string(s) - case .emptyArray : return .array([]) - case .emptyDict : return .dictionary([:]) - } - } - } - } -} diff --git a/Sources/LeafKit/LeafLexer/LeafLexer.swift b/Sources/LeafKit/LeafLexer/LeafLexer.swift new file mode 100644 index 00000000..e814ae2a --- /dev/null +++ b/Sources/LeafKit/LeafLexer/LeafLexer.swift @@ -0,0 +1,276 @@ +// MARK: Subject to change prior to 1.0.0 release +// MARK: - + +// MARK: `LeafLexer` Summary + +/// `LeafLexer` is an opaque structure that wraps the lexing logic of Leaf-Kit. +/// +/// Initialized with a `LeafRawTemplate` (raw string-providing representation of a file or other source), +/// used by evaluating with `LeafLexer.lex()` and either erroring or returning `[LeafToken]` +internal struct LeafLexer { + // MARK: - Internal Only + + /// Convenience to initialize `LeafLexer` with a `String` + init(name: String, template string: String) { + self.name = name + self.src = LeafRawTemplate(name: name, src: string) + self.state = .raw + } + + /// Init with `LeafRawTemplate` + init(name: String, template: LeafRawTemplate) { + self.name = name + self.src = template + self.state = .raw + } + + /// Lex the stored `LeafRawTemplate` + /// - Throws: `LexerError` + /// - Returns: An array of fully built `LeafTokens`, to then be parsed by `LeafParser` + mutating func lex() throws -> [LeafToken] { + // FIXME: Adjust to keep lexing if `try` throws a recoverable LexerError + while let next = try self.nextToken() { + lexed.append(next) + offset += 1 + } + return lexed + } + + // MARK: - Private Only + + private enum State { + /// Parse as raw, until it finds `#` (but consuming escaped `\#`) + case raw + /// Start attempting to sequence tag-viable tokens (tagName, parameters, etc) + case tag + /// Start attempting to sequence parameters + case parameters + /// Start attempting to sequence a tag body + case body + } + + /// Current state of the Lexer + private var state: State + /// Current parameter depth, when in a Parameter-lexing state + private var depth = 0 + /// Current index in `lexed` that we want to insert at + private var offset = 0 + /// Streat of `LeafTokens` that have been successfully lexed + private var lexed: [LeafToken] = [] + /// The originating template source content (ie, raw characters) + private var src: LeafRawTemplate + /// Name of the template (as opposed to file name) - eg if file = "/views/template.leaf", `template` + private var name: String + + // MARK: - Private - Actual implementation of Lexer + + private mutating func nextToken() throws -> LeafToken? { + // if EOF, return nil - no more to read + guard let current = src.peek() else { return nil } + let isTagID = current == .tagIndicator + let isTagVal = current.isValidInTagName + let isCol = current == .colon + let next = src.peek(aheadBy: 1) + + switch (state, isTagID, isTagVal, isCol, next) { + case (.raw, false, _, _, _): return lexRaw() + case (.raw, true, _, _, .some): return lexCheckTagIndicator() + case (.tag, _, true, _, _): return lexNamedTag() + case (.tag, _, false, _, _): return lexAnonymousTag() + case (.parameters, _, _, _, _): return try lexParameters() + case (.body, _, _, true, _): return lexBodyIndicator() + /// Ambiguous case - `#endTagName#` at EOF. Should this result in `tag(tagName),raw(#)`? + case (.raw, true, _, _, .none): + throw LexerError(.unknownError("Unescaped # at EOF"), src: src, lexed: lexed) + default: + throw LexerError(.unknownError("Template cannot be lexed"), src: src, lexed: lexed) + } + } + + // Lexing subroutines that can produce state changes: + // * to .raw: lexRaw, lexCheckTagIndicator + // * to .tag: lexCheckTagIndicator + // * to .parameters: lexAnonymousTag, lexNamedTag + // * to .body: lexNamedTag + + private mutating func lexAnonymousTag() -> LeafToken { + state = .parameters + depth = 0 + return .tag(name: "") + } + + private mutating func lexNamedTag() -> LeafToken { + let name = src.readWhile { $0.isValidInTagName } + let trailing = src.peek() + state = .raw + if trailing == .colon { state = .body } + if trailing == .leftParenthesis { state = .parameters; depth = 0 } + return .tag(name: name) + } + + /// Consume all data until hitting an unescaped `tagIndicator` and return a `.raw` token + private mutating func lexRaw() -> LeafToken { + var slice = "" + while let current = src.peek(), current != .tagIndicator { + slice += src.readWhile { $0 != .tagIndicator && $0 != .backSlash } + guard let newCurrent = src.peek(), newCurrent == .backSlash else { break } + if let next = src.peek(aheadBy: 1), next == .tagIndicator { + src.pop() + } + slice += src.pop()!.description + } + return .raw(slice) + } + + /// Consume `#`, change state to `.tag` or `.raw`, return appropriate token + private mutating func lexCheckTagIndicator() -> LeafToken { + // consume `#` + src.pop() + // if tag indicator is followed by an invalid token, assume that it is unrelated to leaf + let current = src.peek() + if let current = current, current.isValidInTagName || current == .leftParenthesis { + state = .tag + return .tagIndicator + } else { + state = .raw + return .raw(Character.tagIndicator.description) + } + } + + /// Consume `:`, change state to `.raw`, return `.tagBodyIndicator` + private mutating func lexBodyIndicator() -> LeafToken { + src.pop() + state = .raw + return .tagBodyIndicator + } + + /// Parameter hot mess + private mutating func lexParameters() throws -> LeafToken { + // consume first character regardless of what it is + let current = src.pop()! + + // Simple returning cases - .parametersStart/Delimiter/End, .whitespace, .stringLiteral Parameter + switch current { + case .leftParenthesis: + depth += 1 + return .parametersStart + case .rightParenthesis: + switch (depth <= 1, src.peek() == .colon) { + case (true, true): state = .body + case (true, false): state = .raw + case (false, _): depth -= 1 + } + return .parametersEnd + case .comma: + return .parameterDelimiter + case .quote: + let read = src.readWhile { $0 != .quote && $0 != .newLine } + guard src.peek() == .quote else { + throw LexerError(.unterminatedStringLiteral, src: src, lexed: lexed) + } + src.pop() // consume final quote + return .parameter(.stringLiteral(read)) + case .space: + let read = src.readWhile { $0 == .space } + return .whitespace(length: read.count + 1) + default: break + } + + // Complex Parameter lexing situations - enhanced to allow non-whitespace separated values + // Complicated by overlap in acceptable isValidInParameter characters between possible types + // Process from most restrictive options to least to help prevent overly aggressive tokens + // Possible results, most restrictive to least + // * Operator + // * Constant(Int) + // * Constant(Double) + // * Keyword + // * Tag + // * Variable + + // if current character isn't valid for any kind of parameter, something's majorly wrong + guard current.isValidInParameter else { + throw LexerError(.invalidParameterToken(current), src: src, lexed: lexed) + } + + // Test for Operator first - this will only handle max two character operators, not ideal + // Can't switch on this, MUST happen before trying to read tags + if current.isValidOperator { + // Try to get a valid 2char Op + var op = LeafOperator(rawValue: String(current) + String(src.peek()!)) + if op != nil, !op!.available { throw LeafError(.unknownError("\(op!) is not yet supported as an operator")) } + if op == nil { op = LeafOperator(rawValue: String(current)) } else { src.pop() } + if op != nil, !op!.available { throw LeafError(.unknownError("\(op!) is not yet supported as an operator")) } + return .parameter(.operator(op!)) + } + + // Test for numerics next. This is not very intelligent but will read base2/8/10/16 + // for Ints and base 10/16 for decimal through native Swift initialization + // Will not adequately decay to handle things like `0b0A` and recognize as invalid. + if current.canStartNumeric { + var testInt: Int? + var testDouble: Double? + var radix: Int? = nil + var sign = 1 + + let next = src.peek()! + let peekRaw = String(current) + (src.peekWhile { $0.isValidInNumeric }) + var peekNum = peekRaw.replacingOccurrences(of: String(.underscore), with: "") + // We must be immediately preceeded by a minus to flip the sign + // And only flip back if immediately preceeded by a const, tag or variable + // (which we assume will provide a numeric). Grammatical errors in the + // template (eg, keyword-numeric) may throw here + if case .parameter(let p) = lexed[offset - 1], case .operator(let op) = p, op == .minus { + switch lexed[offset - 2] { + case .parameter(let p): + switch p { + case .constant, + .tag, + .variable: sign = 1 + default: throw LexerError(.invalidParameterToken("-"), src: src) + } + case .stringLiteral: throw LexerError(.invalidParameterToken("-"), src: src) + default: sign = -1 + } + } + + switch (peekNum.contains(.period), next, peekNum.count > 2) { + case (true, _, _) : testDouble = Double(peekNum) + case (false, .binaryNotation, true): radix = 2 + case (false, .octalNotation, true): radix = 8 + case (false, .hexNotation, true): radix = 16 + default: testInt = Int(peekNum) + } + + if let radix = radix { + let start = peekNum.startIndex + peekNum.removeSubrange(start ... peekNum.index(after: start)) + testInt = Int(peekNum, radix: radix) + } + + if testInt != nil || testDouble != nil { + // discard the minus + if sign == -1 { self.lexed.removeLast(); offset -= 1 } + src.popWhile { $0.isValidInNumeric } + if testInt != nil { return .parameter(.constant(.int(testInt! * sign))) } + else { return .parameter(.constant(.double(testDouble! * Double(sign)))) } + } + } + + // At this point, just read anything that's parameter valid, but not an operator, + // Could be handled better and is probably way too aggressive. + let name = String(current) + (src.readWhile { $0.isValidInParameter && !$0.isValidOperator }) + + // If it's a keyword, return that + if let keyword = LeafKeyword(rawValue: name) { return .parameter(.keyword(keyword)) } + // Assume anything that matches .isValidInTagName is a tag + // Parse can decay to a variable if necessary - checking for a paren + // is over-aggressive because a custom tag may not take parameters + let tagValid = name.compactMap { $0.isValidInTagName ? $0 : nil }.count == name.count + + if tagValid && src.peek()! == .leftParenthesis { + return .parameter(.tag(name: name)) + } else { + return .parameter(.variable(name: name)) + } + } +} diff --git a/Sources/LeafKit/LeafLexer/LeafParameterTypes.swift b/Sources/LeafKit/LeafLexer/LeafParameterTypes.swift new file mode 100644 index 00000000..72d80aca --- /dev/null +++ b/Sources/LeafKit/LeafLexer/LeafParameterTypes.swift @@ -0,0 +1,175 @@ +// MARK: Subject to change prior to 1.0.0 release + +// MARK: - `Parameter` Token Type + +// FIXME: Can't be internal because of `Syntax` +/// - Does `stringLiteral` need to exist - should `Constant` have a `String` case or should +/// `Constant` be renamed `Numeric` for clarity? + +/// An associated value enum holding data, objects or values usable as parameters to a `.tag` +public enum Parameter: Equatable, CustomStringConvertible { + case stringLiteral(String) + case constant(Constant) + case variable(name: String) + case keyword(LeafKeyword) + case `operator`(LeafOperator) + case tag(name: String) + + /// Returns `parameterCase(parameterValue)` + public var description: String { + return name + "(" + short + ")" + } + + /// Returns `parameterCase` + var name: String { + switch self { + case .stringLiteral: return "stringLiteral" + case .constant: return "constant" + case .variable: return "variable" + case .keyword: return "keyword" + case .operator: return "operator" + case .tag: return "tag" + } + } + + /// Returns `parameterValue` or `"parameterValue"` as appropriate for type + var short: String { + switch self { + case .stringLiteral(let s): return "\"\(s)\"" + case .constant(let c): return "\(c)" + case .variable(let v): return "\(v)" + case .keyword(let k): return "\(k)" + case .operator(let o): return "\(o)" + case .tag(let t): return "\"\(t)\"" + } + } +} + +// MARK: - `Parameter`-Storable Types + +/// `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 { + // MARK: Public - Cases + + // Eval -> Bool / Other + // ----------------------- + case `in`, // + `true`, // X T + `false`, // X F + `self`, // X X + `nil`, // X F X + `yes`, // X T + `no` // X F + + // MARK: Internal Only + + // State booleans + internal var isEvaluable: Bool { self != .in } + internal var isBooleanValued: Bool { [.true, .false, .nil, .yes, .no].contains(self) } + // Value or value-indicating returns + internal var `nil`: Bool { self == .nil } + internal var identity: Bool { self == .`self` } + internal var bool: Bool? { + guard isBooleanValued else { return nil } + return [.true, .yes].contains(self) + } +} + +// MARK: - Operator Symbols + +/// Mathematical and Logical operators +public enum LeafOperator: String, Equatable, CustomStringConvertible, CaseIterable { + // MARK: Public - Cases + + // Operator types: Logic Exist. UnPre Scope + // | Math | Infix | UnPost | + // Logical Tests -------------------------------------------- + case not = "!" // X X + case equal = "==" // X X + case unequal = "!=" // X X + case greater = ">" // X X + case greaterOrEqual = ">=" // X X + case lesser = "<" // X X + case lesserOrEqual = "<=" // X X + case and = "&&" // X X + case or = "||" // X X + // Mathematical Calcs // ----------------------------------------- + case plus = "+" // X X + case minus = "-" // X X X X + case divide = "/" // X X + case multiply = "*" // X X + case modulo = "%" // X X + // Assignment/Existential // + case assignment = "=" // X X + case nilCoalesce = "??" // X X + case evaluate = "`" // X X + // Scoping + case scopeRoot = "$" // X X + case scopeMember = "." // X X + case subOpen = "[" // X X + case subClose = "]" // X X + + /// Raw string value of the operator - eg `!=` + public var description: String { return rawValue } + + // MARK: Internal Only + + // State booleans + internal var logical: Bool { Self.states["logical"]!.contains(self) } + internal var mathematical: Bool { Self.states["mathematical"]!.contains(self) } + internal var existential: Bool { Self.states["existential"]!.contains(self) } + internal var scoping: Bool { Self.states["scoping"]!.contains(self) } + + internal var unaryPrefix: Bool { Self.states["unaryPrefix"]!.contains(self) } + internal var unaryPostfix: Bool { Self.states["unaryPostfix"]!.contains(self) } + internal var infix: Bool { Self.states["unaryPostfix"]!.contains(self) } + + internal var available: Bool { !Self.states["unavailable"]!.contains(self) } + + internal static let precedenceMap: [(check: ((LeafOperator) -> Bool), infixed: Bool)] = [ + (check: { $0 == .not }, infixed: false), // unaryNot + (check: { $0 == .multiply || $0 == .divide }, infixed: true), // Mult/Div + (check: { $0 == .plus || $0 == .minus }, infixed: true), // Plus/Minus + (check: { $0 == .greater || $0 == .greaterOrEqual }, infixed: true), // >, >= + (check: { $0 == .lesser || $0 == .lesserOrEqual }, infixed: true), // <, <= + (check: { $0 == .equal || $0 == .unequal }, infixed: true), // !, != + (check: { $0 == .and || $0 == .or }, infixed: true), // &&, || + ] + + // MARK: Private Only + + private static let states: [String: Set] = [ + "logical" : [not, equal, unequal, greater, greaterOrEqual, + lesser, lesserOrEqual, and, or], + "mathematical" : [plus, minus, divide, multiply, modulo], + "existential" : [assignment, nilCoalesce, minus, evaluate], + "scoping" : [scopeRoot, scopeMember, subOpen, subClose], + "unaryPrefix" : [not, minus, evaluate, scopeRoot], + "unaryPostfix" : [subClose], + "infix" : [equal, unequal, greater, greaterOrEqual, lesser, + lesserOrEqual, and, or, plus, minus, divide, + multiply, modulo, assignment, nilCoalesce, + scopeMember, subOpen], + "unavailable" : [modulo, assignment, nilCoalesce, evaluate, scopeRoot, + scopeMember, subOpen, subClose] + ] +} + +/// An integer or double constant value parameter (eg `1_000`, `-42.0`) +/// +/// #TODO +/// - This is somewhat confusingly named. Possibly would be better to rename as `Numeric`, since +/// `stringLiteral` *IS* a constant type, or else `stringLiteral` should be moved into this. +public enum Constant: CustomStringConvertible, Equatable { + case int(Int) + case double(Double) + + public var description: String { + switch self { + case .int(let i): return i.description + case .double(let d): return d.description + } + } +} diff --git a/Sources/LeafKit/LeafLexer/LeafRawTemplate.swift b/Sources/LeafKit/LeafLexer/LeafRawTemplate.swift new file mode 100644 index 00000000..efa0ab6b --- /dev/null +++ b/Sources/LeafKit/LeafLexer/LeafRawTemplate.swift @@ -0,0 +1,73 @@ +// MARK: Subject to change prior to 1.0.0 release +// MARK: - + +// TODO: Make `LeafSource` return this instead of `ByteBuffer` via extension +internal struct LeafRawTemplate { + // MARK: - Internal Only + let name: String + + init(name: String, src: String) { + self.name = name + self.body = src + self.current = body.startIndex + } + + mutating func readWhile(_ check: (Character) -> Bool) -> String { + return String(readSliceWhile(pop: true, check)) + } + + mutating func peekWhile(_ check: (Character) -> Bool) -> String { + return String(peekSliceWhile(check)) + } + + @discardableResult + mutating func popWhile(_ check: (Character) -> Bool) -> Int { + return readSliceWhile(pop: true, check).count + } + + func peek(aheadBy idx: Int = 0) -> Character? { + let peekIndex = body.index(current, offsetBy: idx) + guard peekIndex < body.endIndex else { return nil } + return body[peekIndex] + } + + @discardableResult + mutating func pop() -> Character? { + guard current < body.endIndex else { return nil } + if body[current] == .newLine { line += 1; column = 0 } + else { column += 1 } + defer { current = body.index(after: current) } + return body[current] + } + + // MARK: - Private Only + + private(set) var line = 0 + private(set) var column = 0 + + private let body: String + private var current: String.Index + + mutating private func readSliceWhile(pop: Bool, _ check: (Character) -> Bool) -> [Character] { + var str = [Character]() + str.reserveCapacity(512) + while let next = peek() { + guard check(next) else { return str } + if pop { self.pop() } + str.append(next) + } + return str + } + + mutating private func peekSliceWhile(_ check: (Character) -> Bool) -> [Character] { + var str = [Character]() + str.reserveCapacity(512) + var index = 0 + while let next = peek(aheadBy: index) { + guard check(next) else { return str } + str.append(next) + index += 1 + } + return str + } +} diff --git a/Sources/LeafKit/LeafLexer/LeafToken.swift b/Sources/LeafKit/LeafLexer/LeafToken.swift new file mode 100644 index 00000000..568bfe31 --- /dev/null +++ b/Sources/LeafKit/LeafLexer/LeafToken.swift @@ -0,0 +1,80 @@ +// MARK: Subject to change prior to 1.0.0 release +// MARK: - + +// MARK: `LeafToken` Summary + +/// `LeafToken` represents the first stage of parsing Leaf templates - a raw file/bytestream `String` +/// will be read by `LeafLexer` and interpreted into `[LeafToken]` representing a stream of tokens. +/// +/// # STABLE TOKEN DEFINITIONS +/// - `.raw`: A variable-length string of data that will eventually be output directly without processing +/// - `.tagIndicator`: The signal at top-level that a Leaf syntax object will follow. Default is `#` and +/// while it can be configured to be something else, only rare uses cases may want to do so. +/// `.tagindicator` can be escaped in source templates with a backslash and will automatically +/// be consumed by `.raw` if so. May decay to `.raw` at the token parsing stage if a non- +/// tag/syntax object follows. +/// - `.tag`: The expected tag name - in `#for(index in array)`, equivalent token is `.tag("for")` +/// - `.tagBodyIndicator`: Indicates the start of a body-bearing tag - ':' +/// - `.parametersStart`: Indicates the start of a tag's parameters - `(` +/// - `.parameterDelimiter`: Indicates a delimter between parameters - `,` +/// - `.parameter`: Associated value enum storing a valid tag parameter. +/// - `.parametersEnd`: Indicates the end of a tag's parameters - `)` +/// +/// # POTENTIALLY UNSTABLE TOKENS +/// - `.stringLiteral`: Does not appear to be used anywhere? +/// - `.whitespace`: Only generated when not at top-level, and unclear why maintaining it is useful +/// +/// # TODO +/// - LeafTokens would ideally also store the range of their location in the original source template + +internal enum LeafToken: CustomStringConvertible, Equatable { + /// Holds a variable-length string of data that will be passed through with no processing + case raw(String) + + /// `#` (or as configured) - Top-level signal that indicates a Leaf tag/syntax object will follow. + case tagIndicator + /// Holds the name of an expected tag or syntax object (eg, `for`) in `#for(index in array)` + case tag(name: String) + /// `:` - Indicates the start of a body for a body-bearing tag + case tagBodyIndicator + + /// `(` - Indicates the start of a tag's parameters + case parametersStart + /// `,` - Indicates separation of a tag's parameters + case parameterDelimiter + /// Holds a `Parameter` enum + case parameter(Parameter) + /// `)` - Indicates the end of a tag's parameters + case parametersEnd + + /// To be removed if possible - avoid using + case stringLiteral(String) + /// To be removed if possible - avoid using + case whitespace(length: Int) + + /// Returns `"tokenCase"` or `"tokenCase(valueAsString)"` if holding a value + var description: String { + switch self { + case .raw(let str): + return "raw(\(str.debugDescription))" + case .tagIndicator: + return "tagIndicator" + case .tag(let name): + return "tag(name: \(name.debugDescription))" + case .tagBodyIndicator: + return "tagBodyIndicator" + case .parametersStart: + return "parametersStart" + case .parametersEnd: + return "parametersEnd" + case .parameterDelimiter: + return "parameterDelimiter" + case .parameter(let param): + return "param(\(param))" + case .stringLiteral(let string): + return "stringLiteral(\(string.debugDescription))" + case .whitespace(let length): + return "whitespace(\(length))" + } + } +} diff --git a/Sources/LeafKit/LeafParser/LKParameter.swift b/Sources/LeafKit/LeafParser/LKParameter.swift deleted file mode 100644 index 5fff0016..00000000 --- a/Sources/LeafKit/LeafParser/LKParameter.swift +++ /dev/null @@ -1,393 +0,0 @@ -/// A concrete parameter object that can be stored inside a LeafAST, either as an expression/function/block -/// parameter or as a passthrough atomic object at the top level of an AST -/// -/// ``` -/// // Atomic Invariants -/// case value(LeafData) // Parameter.literal -/// case keyword(LeafKeyword) // Parameter.keyword - unvalued -/// case `operator`(LeafOperator) // Parameter.operator - limited subset -/// // Atomic Symbols -/// case variable(LKVariable) // Parameter.variable -/// // Expression -/// case expression(LKExpression) // A constrained 2-3 value Expression -/// // Tuple -/// case tuple([LKParameter]) // A 0...n array of LeafParameters -/// // Function - Single exact match function -/// case function(String, LeafFunction, [LKParameter], LKParameter?) -/// // Dynamic - Multiple potential matching overloaded functions (filtered) -/// case dynamic(String, [(LeafFunction, LKTuple?)], [LKParameter], LKParameter?) -/// -internal struct LKParameter: LKSymbol { - // MARK: - Passthrough generators - - /// Generate a `LKParameter` holding concrete `LeafData` - static func value(_ store: LKData) -> LKParameter { .init(.value(store)) } - - /// Generate a `LKParameter` holding a valid `.variable` - static func variable(_ store: LKVariable) -> LKParameter { .init(.variable(store)) } - - /// Generate a `LKParameter` holding a validated `LKExpression` - static func expression(_ store: LKExpression) -> LKParameter { .init(.expression(store)) } - - /// Generate a `LKParameter` holding an available `LeafOperator` - static func `operator`(_ store: LeafOperator) -> LKParameter { - if store.parseable { return .init(.operator(store)) } - __MajorBug("Operator not available") - } - - /// Generate a `LKParameter` hodling a validated `LeafFunction` and its concrete parameters - /// - /// If function call is as a method, `operand` is a non nil-tuple; if it contains a var, method is mutating - static func function(_ name: String, - _ function: LeafFunction?, - _ params: LKTuple?, - _ operand: LKVariable?? = .none, - _ location: SourceLocation) -> LKParameter { - .init(.function(name, function, params, operand, location)) } - - // MARK: - Auto-reducing generators - - /// Generate a `LKParameter`, auto-reduce to a `.value` or .`.variable` or a non-evaluable`.keyword` - static func keyword(_ store: LeafKeyword, - reduce: Bool = false) -> LKParameter { - if store == .nil { return .init(.value(.trueNil)) } - if !store.isEvaluable - || !reduce { return .init(.keyword(store)) } - if store.isBooleanValued { return .init(.value(.bool(store.bool!))) } - if store == .`self` { return .init(.variable(.`self`)) } - __MajorBug("Unhandled evaluable keyword") - } - - /// Generate a `LKParameter` holding a tuple of `LeafParameters` - auto-reduce multiple nested parens and decay to trueNil if void - static func tuple(_ store: LKTuple) -> LKParameter { - if store.count > 1 || store.collection { return .init(.tuple(store)) } - var store = store - while case .tuple(let s) = store[0]?.container, s.count == 1 { store = s } - return store.isEmpty ? .init(.value(.trueNil)) : store[0]! - } - - /// `[` is always invalid in a parsed AST and is used as a magic value to avoid needing a nil LKParameter - static let invalid: LKParameter = .init(.operator(.subOpen)) - - // MARK: - Stored Properties - - /// Actual storage for the object - private(set) var container: Container { didSet { setStates() } } - - // MARK: - LKSymbol - - private(set) var resolved: Bool - private(set) var invariant: Bool - private(set) var symbols: Set - - private(set) var isLiteral: Bool - - /// Will always resolve to a new LKParameter - func resolve(_ symbols: inout LKVarStack) -> Self { isValued ? .init(container.resolve(&symbols)) : self } - /// Will always evaluate to a .value container, potentially holding trueNil - func evaluate(_ symbols: inout LKVarStack) -> LKData { container.evaluate(&symbols) } - - var description: String { container.description } - var short: String { isTuple ? container.description : container.short } - - // MARK: - Internal Only - - var `operator`: LeafOperator? { - guard case .operator(let o) = container else { return nil } - return o - } - - var data: LKData? { - switch container { - case .value(let d): return d - case .keyword(let k) where k.isBooleanValued: return .bool(k.bool) - default: return nil - } - } - - /// Not guaranteed to return a type unless it's entirely knowable from context - var baseType: LKDType? { - switch container { - case .expression(let e) : return e.baseType - case .value(let d) : return d.storedType - case .tuple(let t) where t.isEvaluable : return t.baseType - case .function(_,.some(let f),_,_,_) : - return type(of: f).returns.count == 1 ? type(of: f).returns.first : nil - case .keyword, .operator, .variable, - .function, .tuple : return nil - } - } - - /// Whether the parameter *could* return actual `LeafData` when resolved; may be true but fail to provide value in serialize - var isValued: Bool { - switch container { - case .value, .variable, - .function : return true - case .operator : return false - case .tuple(let t) : return t.isEvaluable - case .keyword(let k) : return k.isEvaluable - case .expression(let e) : return e.form.exp != .custom - } - } - - var isSubscript: Bool { - if case .expression(let e) = container, e.op == .subScript { return true } - else { return false } - } - - var isCollection: Bool? { - switch container { - case .expression(let e): - return e.baseType.map { [.dictionary, .array].contains($0) } - case .function(_,let f,_,_,_): - return f.map { - !type(of: $0).returns.intersection([.dictionary, .array]).isEmpty - && Set(arrayLiteral: .dictionary, .array).isSuperset(of: type(of: $0).returns) } - case .keyword, .operator: return false - case .tuple(let t): return t.isEvaluable - case .value(let v): return v.isCollection - case .variable(let v): return v.isCollection ? true : nil - } - } - - /// Rough estimate estimate of output size - var underestimatedSize: UInt32 { - switch container { - case .expression, .value, - .variable, .function : return 16 - case .operator, .tuple : return 0 - case .keyword(let k) : return k.isBooleanValued ? 4 : 0 - } - } - - var errored: Bool { error != nil } - var error: String? { - if case .value(let v) = container { return v.error } else { return nil } } - - // MARK: - Private Only - - /// Unchecked initializer - do not use directly except through static factories that guard conditions - private init(_ store: Container) { - self.container = store - self.symbols = .init() - self.resolved = false - self.invariant = false - self.isLiteral = false - setStates() - } - - /// Cache the stored states for `symbols, resolved, invariant` - mutating private func setStates() { - isLiteral = false - switch container { - case .operator, .keyword: - symbols = [] - resolved = true - invariant = true - case .value(let v): - symbols = [] - resolved = true - invariant = v.container.isLazy ? v.invariant : true - isLiteral = invariant && !v.errored - case .variable(let v): - symbols = [v] - resolved = false - invariant = true - case .expression(let e): - symbols = e.symbols - resolved = false - invariant = e.invariant - case .tuple(let t): - symbols = t.symbols - resolved = t.resolved - invariant = t.invariant - case .function(_,let f,let p,_,_): - resolved = p?.resolved ?? true - symbols = p?.symbols ?? [] - invariant = f?.invariant ?? false && p?.invariant ?? true - } - } - - private var isTuple: Bool { if case .tuple = container { return true } else { return false } } - - // MARK: - Internal Scoped Type - - /// Wrapped storage object for the actual value the `LKParameter` holds - enum Container: LKSymbol { - /// A concrete `LeafData` - case value(LKData) - /// A `LeafKeyword` (may previously have decayed if evaluable to a different state) - case keyword(LeafKeyword) - /// A `LeafOperator` - case `operator`(LeafOperator) - /// An `LKVariable` key - case variable(LKVariable) - /// A constrained 2-3 value `LKExpression` - case expression(LKExpression) - /// A 1...n array/dictionary of LeafParameters either all with or without labels - case tuple(LKTuple) - /// A `LeafFunction`(s) - tuple is 1...n and may have 0...n labels - nil when empty params - /// If function is nil, dynamic - too many matches were present at parse time or resolution time - /// If tuple is nil, original code call had no parameters - /// If variable is .none, original code call is as function; if .some, method - .some(nil) - nonmutating - /// SourceLocation gives original template location should dynamic lookup fail - case function(String, LeafFunction?, LKTuple?, LKVariable??, SourceLocation) - - // MARK: LKSymbol - - var description: String { - switch self { - case .value(let v) : return v.description - case .keyword(let k) : return "keyword(\(k.description))" - case .operator(let o) : return "operator(\(o.description)" - case .variable(let v) : return "variable(\(v.description))" - case .expression(let e) : return "expression(\(e.description))" - case .tuple(let t) where t.collection - : return "\(t.labels.isEmpty ? "array" : "dictionary")\(short)" - case .tuple : return "tuple\(short)" - case .function(let f,_,let p,_,_) : return "\(f)\(p?.description ?? "()")" - } - } - - var short: String { - switch self { - case .value(let d) : return d.short - case .keyword(let k) : return k.short - case .operator(let o) : return o.short - case .variable(let s) : return s.short - case .expression(let e) : return e.short - case .tuple(let t) where t.collection - : return "\(t.labels.isEmpty ? t.short : t.description)" - case .tuple(let t) : return "\(t.short)" - case .function(let f,_,let p,_,_) : return "\(f)\(p?.short ?? "()")" - } - } - - var resolved: Bool { - switch self { - case .keyword, .operator : return true - case .variable : return false - case .expression(let e) : return e.resolved - case .value(let v) : return v.resolved - case .tuple(let t), - .function(_,_,.some(let t),_,_) : return t.resolved - case .function(_,let f,_,_,_) : return f != nil - } - } - - var invariant: Bool { - switch self { - case .keyword, .operator, - .variable : return true - case .expression(let e) : return e.invariant - case .tuple(let t) : return t.invariant - case .value(let v) : return v.invariant - case .function(_,let f,let p,_,_) - : return f?.invariant ?? false && p?.invariant ?? true - } - } - - var symbols: Set { - switch self { - case .keyword, .operator, .value : return [] - case .variable(let v) : return [v] - case .expression(let e) : return e.symbols - case .tuple(let t), - .function(_,_,.some(let t),_,_) : return t.symbols - case .function : return [] - } - } - - func resolve(_ symbols: inout LKVarStack) -> Self { - if resolved && invariant { return .value(evaluate(&symbols)) } - switch self { - case .value, .keyword, - .operator : return self - case .expression(let e) : return .expression(e.resolve(&symbols)) - case .variable(let v) : let value = symbols.match(v) - return value.errored ? self : .value(value) - case .tuple(let t) - where t.isEvaluable : return .tuple(t.resolve(&symbols)) - case .function(let n, let f, var p, let m, let l) : - if p != nil { p!.values = p!.values.map { $0.resolve(&symbols) } } - guard f == nil else { return .function(n, f, p, m, l) } - let result = m != nil ? LKConf.entities.validateMethod(n, p, (m!) != nil) - : LKConf.entities.validateFunction(n, p) - switch result { - case .failure(let e): return .value(.error(e.description, function: n)) - case .success(let r) where r.count == 1: return .function(n, r[0].0, r[0].1, m, l) - default: return .function(n, nil, p, m, l) - } - case .tuple : __MajorBug("Unevaluable Tuples should not exist") - } - } - - func evaluate(_ symbols: inout LKVarStack) -> LeafData { - func softError(_ result: LKData) -> LKData { - !result.errored ? result - : symbols.context.missingVariableThrows ? result - : .trueNil } - - switch self { - case .value(let v) : return softError(v.evaluate(&symbols)) - case .variable(let v) : return softError(symbols.match(v)) - case .expression(let e) : return softError(e.evaluate(&symbols)) - case .tuple(let t) - where t.isEvaluable : return softError(t.evaluate(&symbols)) - case .function(let n, let f as Evaluate, _, _, let l) : - let x = symbols.match(.define(f.identifier)) - /// `Define` parameter was found - evaluate if non-value, and return - if case .evaluate(let x) = x.container { return softError(x.evaluate(&symbols)) } - /// Or parameter was literal - return - else if !x.errored { return x.container.evaluate } - /// Or `Evaluate` had a default - evaluate and return that - else if let x = f.defaultValue { return softError(x.evaluate(&symbols)) } - return softError(.error(internal: "\(f.identifier) is undefined and has no default value", n, l)) - case .function(let n, var f, let p, let m, let l) : - var p = p ?? .init() - /// Existing literal parameter is errored and we're throwing - return immediately - if symbols.context.missingVariableThrows, - let error = p.values.first(where: {$0.errored}) { return error.evaluate(&symbols) } - /// Check all non-literal or errored params - for i in p.values.indices where !p.values[i].isLiteral || p.values[i].errored { - let eval = p.values[i].evaluate(&symbols) - /// Return hard errors immediately if we're throwing - if eval.errored && symbols.context.missingVariableThrows { return eval } - /// If we have a concrete function and it doesn't take optional at this position, cascade void/error now - if eval.storedType == .void && !(f?.sig[i].optional ?? true) { - return eval.errored ? eval : .error(internal: "\(p.values[i].description) returned void", n, l) } - /// Evaluation checks passed but value may be decayable error - convert to truenil - p.values[i] = .value(!eval.errored ? eval : .trueNil) - } - if f == nil { - let result = m != nil ? LKConf.entities.validateMethod(n, p, (m!) != nil) - : LKConf.entities.validateFunction(n, p) - switch result { - case .success(let r) where r.count == 1: - f = r.first!.0 - p = r.first!.1 ?? p - case .failure(let e): return softError(.error(internal: e.description, n, l)) - default: - return softError(.error(internal: "Dynamic call had too many matches at evaluation", n, l)) - } - } - guard let call = LeafCallValues(f!.sig, p, &symbols) else { - return softError(.error(internal: "Couldn't validate parameter types for \(n)\(p.description)", n, l)) } - if var unsafeF = f as? LeafUnsafeEntity { - unsafeF.unsafeObjects = symbols.context.unsafeObjects - f = (unsafeF as LeafFunction) - } - if case .some(.some(let op)) = m, let f = f as? LeafMutatingMethod { - let x = f.mutatingEvaluate(call) - if let updated = x.0 { symbols.update(op, updated) } - return x.1 - } else { return softError(f!.evaluate(call)) } - case .keyword(let k) - where k.isEvaluable : let x = LKParameter.keyword(k, reduce: true) - return softError(x.container.evaluate(&symbols)) - case .keyword, .operator, - .tuple : __MajorBug("Unevaluable \(self.short) should not exist") - } - } - } -} diff --git a/Sources/LeafKit/LeafParser/LKParser.swift b/Sources/LeafKit/LeafParser/LKParser.swift deleted file mode 100644 index 26b789c0..00000000 --- a/Sources/LeafKit/LeafParser/LKParser.swift +++ /dev/null @@ -1,1107 +0,0 @@ -import Foundation -import NIO - -internal struct LKParser { - // MARK: - Internal Only - - let key: LeafAST.Key - var error: LeafError? = nil - var warnings: [ParseError] = [] - var lastTag: SourceLocation - var currentLocation: SourceLocation { offset < tokens.count ? tokens[offset].source : (key._name, 0, 0) } - - init(_ key: LeafAST.Key, - _ tokens: [LKToken], - _ context: LKRContext = .emptyContext()) { - self.entities = LKConf.$entities._unsafeValue - self.key = key - self.tokens = tokens - self.rawStack = [entities.raw.instantiate(size: 0, encoding: context.encoding)] - self.literals = .init(context: context.literalsOnly, stack: []) - self.lastTag = (key._name, 1, 1) - literals.context.options = context.options ?? [] - literals.context.options!.update(.missingVariableThrows(true)) - } - - mutating func parse() throws -> LeafAST { - literals.stack.append(([], LKVarTablePtr.allocate(capacity: 1))) - literals.stack[0].vars.initialize(to: [:]) - defer { - literals.stack[0].vars.deinitialize(count: 1) - literals.stack[0].vars.deallocate() - } - - var more = true - while more { more = advance() } - if error == nil && !openBlocks.isEmpty { - error = parseErr(.unknownError("[\(openBlocks.map { "#\($0.name)(...):" }.joined(separator: ", "))] still open at EOF"), - tokens.last!.source) - } - if let error = error { throw error } - if !warnings.isEmpty && literals.context.parseWarningThrows { - throw err(.parseWarnings(warnings)) } - return LeafAST(key, - scopes, - defines, - inlines, - requiredVars, - underestimatedSize, - scopeDepths) - } - - // MARK: - Private Only - /// The active entities reference object - private let entities: LeafEntities - /// The incoming tokens - private var tokens: [LKToken] - - /// The AST scope tables - private var scopes: [[LKSyntax]] = [[]] - /// References to all `define` blocks - private var defines: Set = [] - /// References to all `inline` blocks - private var inlines: [(inline: LeafAST.Jump, process: Bool, at: Date)] = [] - private var underestimatedSize: UInt32 = 0 - - private var scopeStack = [0] - private var depth: Int { scopeStack.count - 1 } - private var currentScope: Int { scopeStack[depth] } - - private var scopeDepths: (overallMax: UInt16, - inlineMax: UInt16) = (1,0) - - private var openBlocks: [(name: String, block: LeafFunction.Type)] = [] - private var lastBlock: Int? { openBlocks.indices.last } - - private var offset: Int = 0 - - private var peek: LKToken? { offset < tokens.count ? tokens[offset] : nil } - - private var rawStack: [LKRawBlock] - - private var requiredVars: Set = [] - /// Stack of explicitly created variables and whether they've been set yet. Nil special case for define blocks. - private var createdVars: [[LKVariable: Bool]] = [[:]] - - private var literals: LKVarStack - - /// Process the next `LKToken` or multiple tokens. - private mutating func advance() -> Bool { - guard error == nil, let token = pop() else { return false } - let current = token.token - lastTag = token.source - - /// Get the easy stuff out of the way first... - guard current == .tagMark else { - /// 1. Anything that isn't raw is invalid and Lexer shouldn't have produced it - guard case .raw(var string) = current else { __MajorBug(.malformedToken) } - /// 2. Aggregate all consecutive raws into one - while case .raw(let more) = peek?.token { string += more; pop() } - return appendRaw(string) - } - - /// 3. Off chance it's tagIndicator @ EOF - append a raw tag indicator - guard peek != nil else { return appendRaw(Character.tagIndicator.description) } - - /// 4. .tagIndicator is *always* followed by .tag - anything else is Lexer error - guard case .tag(let tag) = pop()?.token, tag != .some("") else { __MajorBug(.malformedToken) } - - /// 5. Everything now is either anonymous, function, or block: - guard let tagName = tag else { - /// 5A. Catch anonymous (nil) tags - guard let tuple = parseTuple(nil), error == nil else { return false } - if tuple.count > 1 { - return bool(.unknownError("Anonymous tag can't have multiple parameters"), lastTag) } - /// Validate tuple is single parameter, append or evaluate & append raw if invariant - if var v = tuple[0] { - if v.resolved && v.invariant { v = .value(v.evaluate(&literals)) } - guard append(v) else { return false } - } else { - append(.value(.trueNil)) - } - /// Decay trailing colon to raw : - if peek?.token == .blockMark { appendRaw(":"); offset += 1 } - return true - } - - /// 5meta. Handle unique LKMetaBlocks - if let meta = entities.blockFactories[tagName] as? LKMetaBlock.Type { - return handleMeta(tagName, meta, parseTuple(tagName)) } - - /// See if tag name exists as a block factory, and if so, whether it has parse signatures - let letRetry: Bool - if let b = entities.blockFactories[tagName] { letRetry = b.parseSignatures != nil } - else { letRetry = false } - /// 5B. Parse the parameter tuple for the tag, if parameters exist, allowing retrying on custom expressions - let tuple = parseTuple(tagName, retry: letRetry) - guard error == nil else { return false } - - /// 5C. Catch non-block tags - guard peek?.token == .blockMark else { - /// 5D. A normal function call (without a trailing colon) - if !tagName.hasPrefix("end") && !entities.blockFactories.keys.contains(tagName) { - return appendFunction(tagName, tuple) } - - /// 5E. A full-stop "end*" tag - /// - /// 5F. No open blocks on the stack. Failure to close - guard let lastBlock = lastBlock else { return bool(.cantClose(name: tagName, open: nil), lastTag) } - - var openTag = String(tagName.dropFirst(3)) - var canClose = false - var pass = 0 - - /// 5G. Last open block *must* match or it *must* be a chained block - while lastBlock - pass >= 0 && !canClose { - let currentBlock = openBlocks[lastBlock - pass] - /// Match either immediate precedent or head of chain, stop. - if openTag == currentBlock.name { canClose = true; break } - /// Not a match. Continue down stack if the current block is an - /// interstitial chained block (not an opening chainer). - if let chain = currentBlock.block as? ChainedBlock.Type, - !chain.chainsTo.isEmpty { pass += 1 } - /// Head of chain was not a match, stop. - else { openTag = currentBlock.name } - } - /// 5H. No matching open block on the stack. Failure to close. - guard canClose else { return bool(.cantClose(name: tagName, open: openTag), lastTag) } - var isRawBlock = false - /// Decay chained blocks from the open stack if we were in one. - for _ in 0...pass { isRawBlock = openBlocks.removeLast().block == RawSwitch.self } - /// 5I. Successfully (or un) closed block - only one scope will be open, even for chains... - return closeBlock(isRawBlock) - } - - /// 5J. Try to make or close/make a block (or decay to function) - let result = entities.validateBlock(tagName, tuple) - - // TODO: If the call parameters are all literal values evalute - // - EG: see/warn if scope is discarded (eg #if(false)) - - let block: (LeafFunction, LKTuple?) - switch result { - case .failure(.noEntity) : decayTokenTo(":") - return appendFunction(tagName, tuple) - case .failure(let r) : return bool(r, lastTag) - case .success(let b) : block = b; pop() // Dump scope indicator now - } - - /// 5M. If new block is chained type, ensure connection and close current scope - if let chained = type(of: block.0) as? ChainedBlock.Type, - !chained.chainsTo.isEmpty { - /// chained interstitial - must be able to close current block - guard let previous = openBlocks.last?.block as? ChainedBlock.Type, - chained.chainsTo.contains(where: {$0 == previous}) - // else { return bool(err("No open block for #\(tagName) to close")) } - else { return bool(.cantClose(name: tagName, open: openBlocks.last?.name), lastTag) } - guard closeBlock() else { return false } - } - - /// 5N. Open the new block - return openBlock(tagName, block.0 as! LeafBlock, block.1) - } - - @discardableResult - private mutating func pop() -> LKToken? { - if let next = peek { offset += 1; return next }; return nil } - - /// Decay the next token up to a specified raw string - private mutating func decayTokenTo(_ string: String) { - if peek != nil { tokens[offset] = .init(.raw(string), tokens[offset].source) } } - - /// Append a passthrough syntax object to the current scope and return true to continue parsing - @discardableResult - private mutating func append(_ syntax: LKParameter) -> Bool { - // FIXME: Check that current scope is empty and instantiate raw instead - /// If the syntax is a literal and previous is a `.raw`, immediately feed it in - if case .value(let v) = syntax.container, v.invariant, - case .raw(var previous) = scopes[currentScope].last?.container { - previous.append(v) - if let e = previous.getError(currentLocation) { error = e; return false } - scopes[currentScope][scopes[currentScope].count - 1] = .raw(previous) - return true - } - - defer { - scopes[currentScope].append(.passthrough(syntax)) - underestimatedSize += syntax.underestimatedSize - } - - /// If passthrough object is variable creation, append to current scope's set of created vars - if case .expression(let e) = syntax.container, - let declared = e.declaresVariable { - let v = declared.variable - /// Check rhs symbols first to avoid checking the declared variable since lower scope may define - guard updateVars(declared.set?.symbols) else { return false } - /// Ensure set variable wasn't already declared at this level - guard checkExplicitVariableState(v, declared.set != nil) else { return false } - return true - } - return updateVars(syntax.symbols) - } - - /// Append a new raw block from a String. - @discardableResult - private mutating func appendRaw(_ raw: String) -> Bool { - var newRaw = type(of: rawStack.last!).instantiate(size: UInt32(raw.count), - encoding: rawStack.last!.encoding) - newRaw.append(.string(raw)) - if let e = newRaw.getError(currentLocation) { error = e; return false } - underestimatedSize += newRaw.byteCount - let checkAt = scopes[currentScope].count - 2 - let blockCheck: Bool - if checkAt >= 0, case .block = scopes[currentScope][checkAt].container { blockCheck = true } - else { blockCheck = false } - // If previous is raw and it's not a scope atomic, concat or append new - if case .raw(var previous) = scopes[currentScope].last?.container, - !blockCheck, type(of: previous) == type(of: newRaw) { - previous.append(&newRaw) - if let e = previous.getError(currentLocation) { error = e; return false } - scopes[currentScope][checkAt + 1] = .raw(previous) - } else { scopes[currentScope].append(.raw(newRaw)) } - return true - } - - /// If the tag name and parameters can create a valid function call, true and append or set error - private mutating func appendFunction(_ t: String, _ p: LKTuple?) -> Bool { - let result = entities.validateFunction(t, p) - underestimatedSize += 16 - guard updateVars(p?.symbols) else { return false } - switch result { - case .success(let f) - where f.count == 1 : return append(.function(t, f[0].0, f[0].1, nil, lastTag)) - case .success : return append(.function(t, nil, p, nil, lastTag)) - case .failure where p?.values.isEmpty != false : - /// Assume is implicit `Evaluate` if empty params - let evaluate = handleEvaluateFunction("evaluate", .init([(nil, .variable(.atomic(t)))]))! - scopes[currentScope].append(.block("evaluate", evaluate, nil)) - scopes[currentScope].append(.scope(nil)) - return true - case .failure(let r) : return bool(r) - } - } - - /// Open a new block scope. - @discardableResult - private mutating func openBlock(_ n: String, _ b: LeafBlock, _ p: LKTuple?) -> Bool { - // New syntaxes in current scope - scopes[currentScope].append(.block(n, b, p)) - scopes[currentScope].append(.scope(scopes.count)) - openBlocks.append((n, type(of: b))) // Push block onto open stack - scopes.append([]) // Create new scope - scopeStack.append(scopes.count - 1) // Push new scope reference - scopeDepths.overallMax.maxAssign(UInt16(scopes.count)) - if type(of: b) == Inline.self { scopeDepths.inlineMax.maxAssign(UInt16(scopes.count)) } - guard updateVars(p?.symbols) else { return false } - createdVars.append(.init(uniqueKeysWithValues: b.scopeVariables?.map { (.atomic($0), true) } ?? [])) - return true - } - - /// Close the current scope. If the closing scope is empty or single element, remove its table and inline in place of scope. - private mutating func closeBlock(_ rawBlock: Bool = false) -> Bool { - guard currentScope > 0 else { __MajorBug("Attempted to close top scope of template") } - if scopes[currentScope].count < 2 { - let decayed = scopes.removeLast() - scopeStack.removeLast() - guard case .scope = scopes[currentScope].last?.container else { - __MajorBug("Scope change didn't find a scope reference") } - scopes[currentScope].removeLast() - scopes[currentScope].append(decayed.isEmpty ? .scope(nil) : decayed[0] ) - } else { scopeStack.removeLast() } - if rawBlock { - if rawStack.last?.recall ?? false { rawStack[rawStack.count - 1].close() } - rawStack.removeLast() - } - createdVars.removeLast() - return true - } - - private mutating func updateVars(_ vars: Set?) -> Bool { - guard var x = vars else { return true } - x = x.reduce(into: [], { $0.insert($1.ancestor)} ) - let scoped = x.filter { $0.isScoped } - requiredVars.formUnion(scoped) - x.subtract(scoped) - - /// Check that explicit variables referenced are set (or fail) - for created in createdVars.reversed() { - let unset = created.filter {$0.value == false} - let matched = x.intersection(unset.keys) - if !matched.isEmpty { return bool(.unset(matched.first!.terse)) } - x.subtract(created.keys) - } - - x.subtract(requiredVars) - x = x.filter { !requiredVars.contains($0.contextualized) } - requiredVars.formUnion(x) - return true - } - - private mutating func checkExplicitVariableState(_ v: LKVariable, _ set: Bool) -> Bool { - guard createdVars[depth][v] == nil else { return bool(.declared(v.terse)) } - createdVars[depth][v] = set - return true - } - - private mutating func handleEvaluateFunction(_ f: String, _ tuple: LKTuple, nested: Bool = false) -> Evaluate? { - guard tuple.count == 1, let param = tuple[0] else { return `nil`(.unknownError("#\(f) \(Evaluate.warning)")) } - let identifier: String - let defaultValue: LKParameter? - switch param.container { - case .expression(let e) where e.op == .nilCoalesce: - guard case .variable(let v) = e.lhs?.container, - v.isAtomic, let coalesce = e.rhs, coalesce.isValued - else { return `nil`(.unknownError("#\(f) \(Evaluate.warning)")) } - identifier = v.member! - defaultValue = coalesce - case .variable(var v) where v.isAtomic: - identifier = String(v.member!) - v.state.formUnion(.defined) - defaultValue = nil - if let match = createdVars.match(v) { - if match.1 == false { - return `nil`(.unknownError("#\(f)() explicitly does not exist and cannot be provided")) } - if match.0.state.contains(.blockDefine) && nested { - return `nil`(.unknownError("#\(f)() is a block define - cannot use as parameter")) } - } else { - /// Block eval usage can take both forms unconditionally but nested use requires non-block definition - if nested { requiredVars.update(with: v) } - else { v.state.insert(.blockDefine); requiredVars.insert(v) } - } - default: return `nil`(.unknownError("#\(f) \(Evaluate.warning)")) - } - return Evaluate(identifier: identifier, defaultValue: defaultValue) - } - - /// Arbitrate MetaBlocks - private mutating func handleMeta(_ name: String, - _ meta: LKMetaBlock.Type, - _ tuple: LKTuple?) -> Bool { - var isBlock: Bool = peek?.token == .blockMark - if isBlock { pop() } - switch meta.form { - case .define: - guard tuple?.count == 1 else { return bool(.unknownError("#\(name) \(Define.warning)"), lastTag) } - - let identifier: String - let value: LKParameter - let wasBlock = isBlock == true - let set: Bool - - switch tuple![0]!.container { - case .variable(let v) where v.isAtomic: - identifier = v.member! - value = .value(.trueNil) - set = false - case .expression(let e) where e.op == .assignment: - guard case .variable(let v) = e.lhs?.container, v.isAtomic, - e.rhs?.isValued == true else { fallthrough } - if isBlock { - isBlock = false - warn(.unknownError("#\(name)(identifier = value): is ambiguous; assumed not to be a block"), lastTag) - } - identifier = v.member! - value = e.rhs! - set = true - default: return bool(.unknownError("#\(name) \(Define.warning)")) - } - - if let functions = entities.functions[identifier], - !functions.compactMap({ $0.sig.emptyParamSig ? $0 : nil }).isEmpty { - warn(.unknownError("Defined value for \(identifier) must be called with `evaluate(\(identifier))` - A function with signature \(identifier)() already exists"), lastTag) } - - let definition = Define(identifier: identifier, - param: isBlock ? nil : value, - table: currentScope, - row: scopes[currentScope].count + 1) - var v: LKVariable = .atomic(identifier) - v.state.insert(.defined) - /// Force the current level to undefine variable so new key reflects block state as necessary - createdVars[createdVars.count - 1].removeValue(forKey: v) - if isBlock { - v.state.insert(.blockDefine) - createdVars[createdVars.count - 1][v] = true - openBlock(name, definition, nil) - } else { - if value.isLiteral { - warn(.unknownError("Definition for `\(identifier)` is a literal value - declaring as a variable may be preferred"), lastTag) - } - scopes[currentScope].append(.block(name, definition, nil)) - scopes[currentScope].append(.passthrough(definition.param!)) - if wasBlock { appendRaw(":") } - createdVars[createdVars.count - 1][v] = set - guard updateVars(definition.param!.symbols) else { return false } - } - defines.insert(identifier) - case .evaluate: - guard let valid = handleEvaluateFunction(name, tuple ?? .init()) else { - return false } - scopes[currentScope].append(.block(name, valid, nil)) - scopes[currentScope].append(valid.defaultValue == nil ? .scope(nil) - : .passthrough(valid.defaultValue!)) - if isBlock { appendRaw(":") } - case .inline: - guard let tuple = tuple, (1...2).contains(tuple.count), - case .string(let file) = tuple[0]?.data?.container - else { return bool(.unknownError("#\(name) \(Inline.literalWarning)"), lastTag) } - var process = false - var raw: String? = nil - if tuple.count == 2 { - guard tuple.labels["as"] == 1, let behavior = tuple[1]?.container - else { return bool(.unknownError("#\(name) \(Inline.warning)"), lastTag) } - if case .keyword(.leaf) = behavior { process = true } - else if case .variable(let v) = behavior, v.isAtomic, - entities.rawFactories[v.member!] != nil { - raw = v.member - } else { return bool(.unknownError("#\(name) \(Inline.warning)"), lastTag) } - } else { process = true } - let inline = Inline(file: file, - process: process, - rawIdentifier: process ? nil : raw, - availableVars: createdVars.flat) - inlines.append((inline: .init(identifier: file, - table: currentScope, - row: scopes[currentScope].count), - process: inline.process, - at: .distantFuture)) - scopes[currentScope].append(.block(name, inline, nil)) - scopes[currentScope].append(.scope(nil)) - if isBlock { appendRaw(":") } - case .rawSwitch: - return bool(.unknownError("Raw switching blocks not yet supported"), lastTag) -// guard tuple?.isEmpty ?? true else { return bool(err("Using #\(name)() with parameters is not yet supported")) } -// if isBlock { -// /// When enabled, type will be picked from parameter & params will be passed -// rawStack.append(type(of: rawStack.last!).instantiate(size: 0, encoding: LKROption.encoding)) -// return openBlock(name, RawSwitch(type(of: rawStack.last!), .init()), nil) -// } - case .declare: - guard tuple?.count == 1 else { return bool(.unknownError("#\(name) \(Declare.warning)"), lastTag) } - - var identifier: LKVariable - let value: LKParameter - - switch tuple![0]!.container { - case .variable(let v) where v.isAtomic: - identifier = v - value = .value(.trueNil) - case .expression(let e) where e.op == .assignment: - guard case .variable(let v) = e.lhs?.container, v.isAtomic, - e.rhs?.isValued == true else { fallthrough } - identifier = v - value = e.rhs! - default: return bool(.unknownError("#\(name) \(Declare.warning)"), lastTag) - } - - if name == "let" { identifier.state.formUnion(.constant) } - return append(.expression(.expressAny([.keyword(name == "var" ? .var : .let), .variable(identifier), value - ])!)) - } - return true - } - - enum VarState: UInt8, Comparable { - case start, open, chain - static func <(lhs: VarState, rhs: VarState) -> Bool { lhs.rawValue < rhs.rawValue } - } - - /// Try to read parameters. Return nil if no parameters present. Nil for `function` if for expression. - private mutating func parseTuple(_ function: String?, retry: Bool = false) -> LKTuple? { - if peek?.token == .paramsStart { pop() } else { return nil } - - /// If parsing for a block signature and normal evaluation fails, flag to retry without complex sanity - /// Set to nil when retrying is not allowed (expressions, functions, no-parse-sig blocks, etc - var retrying: Bool? = retry ? false : nil - - // Parameter parsing stacks - var functions: [(String?, SourceLocation)] = [] - var tuples: [LKTuple] = [] - var labels: [String?] = [] - var states: [VarState] = [] - var complexes: [[LKParameter]] = [] - - var variableCreation: (Bool, constant: Bool) = (false, false) - - // Conveniences to current stacks - var currentFunction: String? { - get { functions.last!.0 } - set { functions[functions.indices.last!].0 = newValue} - } - var currentFunctionLocation: SourceLocation { - get { functions.last!.1 } - set { functions[functions.indices.last!].1 = newValue } - } - var currentTuple: LKTuple { - get { tuples.last! } - set { tuples[tuples.indices.last!] = newValue } - } - var currentLabel: String? { - get { labels.last! } - set { labels[labels.indices.last!] = newValue} - } - var currentState: VarState { - get { states.last! } - set { states[states.indices.last!] = newValue } - } - var currentComplex: [LKParameter] { - get { complexes.last! } - set { complexes[complexes.indices.last!] = newValue } - } - - - // Atomic variable states - doesn't need a stack - var needIdentifier = false - var openScope: String? = nil - var openMember: String? = nil - var openPath: [String] = [] - - /// Current state is for anything but an anonymous (0,1) count expression tuple - var forFunction: Bool { currentFunction != nil } - /// Current complex antecedent being valued implies subscripting, unvalued or empty complex implies array - var subScriptOpensCollection: Bool { !(currentComplex.last?.isValued ?? false) } - /// If current tuple represents an open collection initializer - var inCollection: Bool { ["#dictionary", "#array", "#collection"].contains(currentFunction) } - /// If current state has an open tuple - eg; subscripting opens a complex but not a tuple. - var inTuple: Bool { - guard let f = currentFunction else { return false } - guard f.hasPrefix("#") else { return true } - return f != "#subscript" - } - - /// Evaluate the current complex expression into an atomic expression or fail, and close the current complex and label states - @discardableResult - func tupleAppend() -> Bool { - let l = currentFunctionLocation - guard currentComplex.count <= 1 else { return bool(.unknownError("Couldn't resolve parameter"), l) } - defer { currentState = .start; currentLabel = nil; complexes.removeLast(); complexes.append([]) } - if currentFunction == "#dictionary" { - if currentComplex.isEmpty { return bool(.missingValue(isDict: true), l) } - if currentLabel == nil { return bool(.missingKey, l) } - } - guard !currentComplex.isEmpty else { return true } - /// Get the current count of parameters in the current tuple - guard currentTuple.count != 255 else { return bool(.unknownError("Tuples are limited to 256 capacity"), l) } - /// Add the parameter to the current tuple's values - tuples[tuples.indices.last!].values.append(currentComplex[0]) - /// Get the current label if one exists, return if it's nil - guard let label = currentLabel else { return true } - /// Ensure the label isn't a duplicate - if currentTuple.labels.keys.contains(label) { return bool(.unknownError("Duplicate entry for \(label)"), l) } - /// Add the label to the tuple's labels for the new position - tuples[tuples.indices.last!].labels[label] = currentTuple.count - 1 - return true - } - - @discardableResult - func complexAppend(_ a: LKParameter) -> Bool { - var a = a - /// If an invariant function with all literal params, and not a mutating or unsafe object, evaluate immediately - if case .function(_, .some(let f), let t, _, _) = a.container, - f as? LKMetaBlock == nil, - f as? LeafMutatingMethod == nil, - f as? LeafUnsafeEntity == nil, - t?.values.allSatisfy({$0.isLiteral}) ?? true { - let values = t?.values.map {$0.data!} ?? [] - a = .value(f.evaluate(.init(values, t?.labels ?? [:]))) - } - /// `#(var x), #(var x = value)` - var flags "creation", checked on close complex - if case .keyword(let k) = a.container, k.isVariableDeclaration { - guard !forFunction, tuples.count == 1, currentTuple.isEmpty, - complexes.allSatisfy({$0.isEmpty}) else { return bool(.invalidDeclaration) } - variableCreation = (true, constant: k == .let) - return true - } - guard makeVariableIfOpen() else { return false } - if let op = a.operator { - /// Adding an infix or postfix operator - requires valued antecedent - if op.infix { - guard !currentComplex.isEmpty, currentComplex.last!.isValued else { - return bool(.unvaluedOperand) } - } else if op.unaryPostfix, !(currentComplex.last?.isValued ?? false) { - return bool(currentComplex.last != nil ? .unvaluedOperand : .noPostfixOperand) - } - } else if !currentComplex.isEmpty, retrying != true { - /// Adding anything else requires the antecedent be a non-unaryPostfix operator - guard let op = currentComplex.last?.operator, !op.unaryPostfix else { - return bool(.missingOperator) } - } - complexes[complexes.indices.last!].append(a) - return true - } - - func complexDrop(_ d: Int) { complexes[complexes.indices.last!].removeLast(d) } - - /// Open a new tuple for a function parameter - func newTuple(_ function: String? = nil) { - tuples.append(.init()) - labels.append(nil) - newComplex(function) - } - - /// Open a new complex expression - func newComplex(_ function: String? = nil) { - functions.append((function, currentLocation)) - states.append(.start) - complexes.append([]) - } - - func clearVariableState() { - currentState = .start - openScope = nil - openMember = nil - openPath = [] - needIdentifier = false - } - - func makeVariableIfOpen() -> Bool { - guard currentState == .open else { return true } - if openScope?.isValidLeafIdentifier == false { return bool(.invalidIdentifier(openScope!)) } - if openMember?.isValidLeafIdentifier == false { return bool(.invalidIdentifier(openMember!)) } - if let invalid = openPath.first(where: {!$0.isValidLeafIdentifier}) { return bool(.invalidIdentifier(invalid)) } - let variable = LKVariable(openScope, openMember, openPath.isEmpty ? nil : openPath) - guard let valid = variable else { return bool(.unknownError("Couldn't make variable identifier")) } - complexes[complexes.indices.last!].append(.variable(valid)) - clearVariableState() - return true - } - - func keyValueEntry() -> Bool { - let l = currentFunctionLocation - if currentFunction == "#collection" { currentFunction = "#dictionary" } - else if currentFunction == "#array" { return bool(.unknownError("Can't label elements of an array"), l) } - - guard case .value(let lD) = currentComplex[0].container, - lD.storedType == .string, let key = lD.string else { - return bool(.unknownError("Dictionary key must be string literal"), l) } - complexDrop(1) - currentLabel = key - return true - } - - /// State should have guaranteed we only call here when tuple is array/dict & non-zero length - @discardableResult - func resolveCollection() -> Bool { - guard closeComplex(), tupleAppend() else { return false } - - let function = functions.removeLast() - var tuple = tuples.removeLast() - labels.removeLast() - complexes.removeLast() - states.removeLast() - tuple.collection = true - - if function.0 == "#dictionary", tuple.labels.count != tuple.values.count { return bool(.missingKey, function.1) } - guard tuple.isEvaluable else { return bool(.unknownError("Unevaluable collection initializer"), function.1) } - return complexAppend(.tuple(tuple)) - } - - func resolveSubscript(with explicitAccessor: LKParameter? = nil) { - let accessor: LKParameter - - if explicitAccessor == nil { - guard closeComplex() else { return void(.malformedExpression) } - states.removeLast() - let function = functions.popLast()! - guard function.0 == "#subscript", - let identifier = complexes.popLast()!.first else { - __MajorBug("Invalid subscripting state") } - guard currentComplex.popLast()?.operator == .subOpen, - identifier.isValued else { return void(.malformedExpression, function.1) } - accessor = identifier - } else { - accessor = explicitAccessor! - } - - let l = currentFunctionLocation - - guard currentComplex.count >= 1 else { return void(.malformedExpression, l) } - let object = currentComplex.last! - complexDrop(1) - - guard object.isCollection != false else { return void(.noSubscript, l) } - var stringAccessor: Bool? = nil - if let accessorType = accessor.baseType { - switch accessorType { - case .string: stringAccessor = true - case .int: stringAccessor = false - default: return void(.keyMismatch, l) - } - } - if let objectType = object.baseType, - [.array, .dictionary].contains(objectType), - let isArray = (objectType == .array) as Bool?, - isArray != stringAccessor { return void(.keyMismatch, l) } - - let parameter = express([object, .operator(.subScript), accessor]) - - guard let accessed = parameter else { return void(.malformedExpression, l) } - complexAppend(accessed) - } - - /// only needed to close ternaryTrue - ternaryFalse won't implicitly open a new complex - func resolveTernary() { - let l = currentFunctionLocation - guard currentFunction == "#ternary" else { return void(.unknownError("Unexpected ternary :"), l) } - let whenTrue = complexes.popLast()!.first! - guard whenTrue.isValued, let last = currentComplex.indices.last, - last != 0, currentComplex[last].operator == .ternaryTrue else { - return void(.unknownError("No open ternary to close"), l) } - states.removeLast() - functions.removeLast() - complexAppend(whenTrue) - } - - func resolveExpression() { - guard let tuple = tuples.popLast(), tuple.values.count <= 1 else { - return void(.unknownError("Expressions must return a single value"), currentFunctionLocation) } - if tuple.count == 1, let value = tuple.values.first { complexAppend(value) } - } - - /// Generate a LKExpression, or if an evaluable, invariant expression, a value - func express(_ params: [LKParameter]) -> LKParameter? { - if let expression = LKExpression.express(params) { - return expression.invariant && expression.resolved - ? .value(expression.evaluate(&literals)) - : .expression(expression) - } else if let expression = LKExpression.expressTernary(params) { - return .expression(expression) - } else if let expression = LKExpression.expressAny(params) { - return .expression(expression) - } else { return nil } - } - - /// Branch on a LeafOperator - func operatorState(_ op: LeafOperator) { - if [.subOpen, .ternaryTrue].contains(op), !makeVariableIfOpen() { return } - if [.subClose, .ternaryFalse].contains(op), !closeComplex() { return } - - if op.assigning { - guard makeVariableIfOpen() else { return } - - guard !currentComplex.contains(where: {$0.isSubscript}) else { - return void(.unknownError("Assignment via subscripted access not yet supported")) } - - guard function.map({ entities.assignment.contains($0) }) ?? true, - complexes.count == 1 else { return void(.invalidDeclaration) } - guard case .variable(let assignor) = currentComplex.first?.container, - currentComplex.count == 1 else { return void(.invalidDeclaration) } - guard !assignor.isScoped else { return void(.constant(assignor.terse)) } - - // FIXME: Pathed - guard !assignor.isPathed else { - return void(.unknownError("Assignment to pathed variables is not yet supported")) } - - /// Check if variable is declared in-template - if let match = createdVars.match(assignor) { - /// ... that declared variable is not constant or, if constant, has not been set yet and is in this scope - guard !match.0.isConstant || createdVars.last![assignor] == false else { - return void(.constant(assignor.terse)) } - } - - /// Can only assign to in-template declared variables at current scope - if op == .assignment && createdVars.last![assignor] == false { - createdVars[depth][assignor] = true } - } else if op == .scopeRoot { - /// Variable scoping / Method accessor special cases - mutate the open variable state and return - guard case .param(.variable(let scope)) = pop()?.token, - currentState == .start else { return void(.unknownError("Unexpected `$`")) } - openScope = scope - currentState = .open - return - } else if op == .scopeMember { - if needIdentifier { return void(.unknownError(".. is not meaningful")) } - if currentState == .start { - if currentComplex.last?.isValued == true { currentState = .chain } - else { return void(.missingIdentifier) } - } - needIdentifier = true - return - } - - switch op { - case .subOpen where subScriptOpensCollection - : newTuple("#collection") - case .subOpen : if case .whiteSpace(_) = tokens[offset - 1].token { - return void(.unknownError("Subscript may not have leading whitespace")) } - complexAppend(.operator(op)) - newComplex("#subscript") - case .subClose where inCollection - : resolveCollection() - case .subClose : resolveSubscript() - case .ternaryTrue : complexAppend(.operator(.ternaryTrue)) - newComplex("#ternary") - case .ternaryFalse : resolveTernary() - complexAppend(.operator(.ternaryFalse)) - case .evaluate : return void(.unknownError("\(op) not yet implemented")) - default : complexAppend(.operator(op)) - } - } - - /// Add an atomic variable part to label, scope, member, or part, dependent on state - func variableState(_ part: String, parseBypass: Bool = false) { - /// Decay to label identifier if followed by a label indicator - if peek?.token == .labelMark { - currentLabel = part; pop() - return currentFunction == "#collection" ? void(.unknownError("Dictionary keys must be quoted")) : () - } - needIdentifier = false - switch currentState { - case .start : openMember = part - currentState = .open - case .open where openMember == nil - : openMember = part - case .open : openPath.append(part) - case .chain : resolveSubscript(with: .value(.string(part))) - } - } - - /// Add a new function to the stack - func functionState(_ function: String) { - /// If we were in the middle of a variable, close it. When the next tuple for this function's - /// parameters close, we'll rewrite the closed variable into the first tuple parameter. - if currentState == .open { guard makeVariableIfOpen() else { return } - currentState = .chain } - newTuple(function) - pop() - } - - /// Attempt to resolve the current complex expression into an atomic single param (leaving as single value complex) - func closeComplex() -> Bool { - /// pull the current complex off the stack - guard makeVariableIfOpen(), var exp = complexes.popLast() else { return false } - - var opCount: Int { countOpsWhere { _ in true } } - - func countOpsWhere(_ check: (LeafOperator) -> Bool) -> Int { - exp.reduce(0, { $0 + ($1.operator.map {check($0) ? 1 : 0} ?? 0) }) } - func firstOpWhere(_ check: (LeafOperator) -> Bool) -> Int? { - for (i, p) in exp.enumerated() { - if let op = p.operator, check(op) { return i } } - return nil } - func lastOpWhere(_ check: (LeafOperator) -> Bool) -> Int? { - for (i, p) in exp.enumerated().reversed() { - if let op = p.operator, check(op) { return i } } - return nil } - func wrapInfix(_ i: Int) -> Bool { - guard 0 < i && i < exp.count - 1, - let wrap = express([exp[i - 1], exp[i], exp[i + 1]]) else { return false } - if let e = exp[i - 1].error { return bool(.unknownError(e), currentFunctionLocation) } - if let e = exp[i + 1].error { return bool(.unknownError(e), currentFunctionLocation) } - exp[i - 1] = wrap; exp.remove(at: i); exp.remove(at: i); return true } - func wrapNot(_ i: Int) -> Bool { - if let e = exp[i + 1].error { return bool(.unknownError(e), currentFunctionLocation) } - guard exp.indices.contains(i + 1), - let wrap = express([exp[i], exp[i + 1]]) else { return false } - exp[i] = wrap; exp.remove(at: i + 1); return true } - - // Wrap single and two op operations first - if var ops = opCount as Int?, ops != 0 { - wrapCalculations: - for map in LeafOperator.evalPrecedenceMap { - while let index = lastOpWhere(map.check) { - if (map.infixed ? !wrapInfix(index) : !wrapNot(index)) { break wrapCalculations } - ops -= 1 - if opCount == 0 { break wrapCalculations } - } - } - } - - // Then wrap ternary expressions - while let tF = firstOpWhere({$0 == .ternaryFalse}) { - guard tF > 2, exp[tF - 2].operator == .ternaryTrue, exp.count >= tF, - let ternary = express([exp[tF - 3], exp[tF - 1], exp[tF + 1]]) - else { return false } - let eOne = exp[tF - 1].error - let eTwo = exp[tF + 1].error - if let eOne = eOne, let eTwo = eTwo { - return bool(.unknownError("Both sides of ternary expression produce errors:\n\(eOne)\n\(eTwo)")) } - exp[tF - 3] = ternary - exp.removeSubrange((tF - 2)...(tF + 1)) - } - - // Custom expressions can still be at most 3-part, anything more is invalid - guard exp.count <= 3 else { return false } - if exp.isEmpty { complexes.append([]); return true } - guard exp.count > 1 else { exp[0] = exp[0].resolve(&literals); complexes.append(exp); return true } - - // Handle assignment - if exp[1].operator?.assigning == true { - if exp.count == 2 { return bool(.unknownError("No value to assign")) } - if !exp[2].isValued { return bool(.unknownError("Non-valued type can't be assigned")) } - exp[2] = exp[2].resolve(&literals) - complexes.append([.expression(LKExpression.express(exp)!)]) - return true - } - - // Only blocks may have non-atomic parameters, so any complex - // expression above the first tuple must be atomic - but if we're retrying, bypass - if tuples.count > 1 || currentFunction == nil, !(retrying ?? false) { return false } - // Blocks may parse custom expressions so wrap into any expression - exp = [.expression(LKExpression.expressAny(exp)!)] - complexes.append(exp) - return true - } - - /// Hit a `)` - close as appropriate - func arbitrateClose() { - guard !(currentFunction?.hasPrefix("#") ?? false) else { - return void(.unknownError("Can't close parameters while in \(currentFunction!)")) } - /// Try to close the current complex expression, append to the current tuple, and close the current tuple - let chained = states.count > 1 ? states[states.indices.last! - 1] == .chain : false - guard closeComplex() else { return error == nil ? void(.malformedExpression) : () } - guard tupleAppend() else { return } - guard tuples.count > 1 || chained else { return } - let function = functions.removeLast() - var tuple = tuples.removeLast() - labels.removeLast() - complexes.removeLast() - states.removeLast() - switch function.0 { - /// expression - case .none where retrying == nil: - currentComplex.append(tuple.isEmpty ? .value(.trueNil) : tuple.values[0]) - /// tuple where we're in block parsing & top-level - case .none: - currentComplex.append((tuple.isEmpty && tuples.count != 1) ? .value(.trueNil) : .tuple(tuple)) - /// Method function - case .some(let m) where chained: - guard !currentComplex.isEmpty, let operand = currentComplex.popLast(), - operand.isValued else { return void(.unvaluedOperand, function.1) } - tuple.labels = tuple.labels.mapValues { $0 + 1 } - tuple.values.insert(operand, at: 0) - var mutable = false - if case .variable(let v) = operand.container, !v.isConstant { mutable = true } - let result = entities.validateMethod(m, tuple, mutable) - switch result { - case .failure(let r) : return void(r) - case .success(let M) : - let mutating = (try? result.get().first(where: {($0.0 as! LeafMethod).mutating}) != nil) ?? false - var original: LKVariable? = nil - if mutating, case .variable(let v) = operand.container { - if let match = createdVars.match(v) { - if !match.1 { return void(.unset(v.terse), function.1) } - if match.0.isConstant { return void(.constant(v.terse, mutate: true), function.1) } - } - original = v - } - - complexAppend(M.count == 1 ? .function(m, M[0].0, M[0].1, .some(original), function.1) - : .function(m, nil, tuple, .some(original), function.1)) - } - currentState = .start - /// Atomic function - case .some(let f): - guard entities.blockFactories[f] as? Evaluate.Type == nil else { - if let valid = handleEvaluateFunction(f, tuple, nested: true) { - complexAppend(.function(f, valid, nil, nil, function.1)) } - break - } - let result = entities.validateFunction(f, tuple) - switch result { - case .success(let F) - where F.count == 1 : complexAppend(.function(f, F[0].0, F[0].1, nil, function.1)) - /// Can't find an exact match yet - don't grab an actual object, just store the tuple - case .success : complexAppend(.function(f, nil, tuple, nil, function.1)) - case .failure(let r) : - /// Non-empty params, absolute failure - if !tuple.values.isEmpty { return void(r, function.1) } - /// ... otherwise, assume is implicit `Evaluate` - if let evaluate = handleEvaluateFunction("evaluate", .init([(nil, .variable(.atomic(f)))]), nested: true) { - complexAppend(.function("evaluate", evaluate, nil, nil, function.1)) - } - } - } - } - - /// open the first complex expression, param label, and tuple, etc. - newTuple(function) - - // MARK: - Parameter parsing cycle - parseCycle: - while error == nil, let next = pop() { - switch next.token { - case .param(let p) : - switch p { - case .operator(let o) : operatorState(o) - case .variable(let v) : variableState(v) - case .function(let f) : functionState(f) - case .literal(let l) : complexAppend(.value(l.leafData)) - case .keyword(let k) - where k == .`self` : openScope = LKVariable.selfScope - currentState = .open - case .keyword(let k) : complexAppend(.keyword(k)) - } - case .paramDelimit where inTuple || retrying == true - : guard closeComplex() else { - if error == nil { void(.malformedExpression) } - break } - tupleAppend() - case .paramDelimit : void(.unknownError("Expressions can't be tuples")) - case .paramsStart where currentState == .start - : newTuple() - case .paramsStart : void(.unknownError("Can't use expressions in variable identifier")) - case .paramsEnd : arbitrateClose() - if currentComplex.isEmpty { complexes.removeLast() } - if tuples.count == 1 && complexes.isEmpty { break parseCycle } - case .labelMark : guard keyValueEntry() else { break } - case .whiteSpace : break - default : __MajorBug("Lexer produced unexpected \(next) inside parameters") - } - // If this is for a block and we errored, retry once - if error != nil, retry, retrying == false { error = nil; offset -= 1; retrying = true } - } - - /// Error state from grammatically correct but unclosed parameters at EOF - if error == nil && tuples.count > 1 { return `nil`(.unknownError("Template ended with open parameters")) } - if error == nil && variableCreation.0 { - let style = variableCreation.constant ? "let" : "var" - if tuples[0].count != 1 { return `nil`(.unknownError("Declare variables with #\(style)(x) or #\(style)(x = value)")) } - var theVar: LKVariable? = nil - var value = tuples[0].values.first! - switch value.container { - case .variable(let x) where x.isAtomic: theVar = x; value = .value(.trueNil); break - case .expression(let e) where e.op == .assignment: - guard case .variable(let x) = e.lhs?.container, x.isAtomic else { fallthrough } - theVar = x; value = e.rhs!; break - case .variable: return `nil`(.unknownError("Variable declarations may not be pathed")) - case .expression(let e) where e.form.exp == .assignment: - return `nil`(.unknownError("Variable assignment at declarations may not be compound expression")) - default : return `nil`(.unknownError("Declare variables with #(\(style) x) or #(\(style) x = value)")) - } - if variableCreation.constant { theVar!.state.formUnion(.constant) } - /// Return a custom expression - return .init([(nil, .expression(.expressAny([.keyword(variableCreation.constant ? .let : .var), .variable(theVar!), value])!))]) - } - return error == nil ? tuples.popLast() : nil - } - - var defaultLocation: SourceLocation { offset > 0 ? currentLocation : lastTag } - - mutating func bool(_ cause: ParseErrorCause, _ location: SourceLocation? = nil) -> Bool { - error = .init(parseErr(cause, location ?? defaultLocation)); return false } - mutating func void(_ cause: ParseErrorCause, _ location: SourceLocation? = nil) { - error = .init(parseErr(cause, location ?? defaultLocation)) } - mutating func `nil`(_ cause: ParseErrorCause, _ location: SourceLocation? = nil) -> T? { - error = .init(parseErr(cause, location ?? defaultLocation)); return nil } - - mutating func warn(_ cause: ParseErrorCause, _ location: SourceLocation? = nil) { - warnings.append(.warning(cause, location ?? defaultLocation)) - } -} - -private extension String { - static let malformedToken = "Lexer produced malformed tokens" -} - -private extension Array where Element == Dictionary { - var flat: Set? { - var x: Set = [] - reversed().forEach { $0.forEach { if $0.value, !x.contains($0.key) { x.insert($0.key) } } } - return !x.isEmpty ? x : nil - } - - func match(_ v: LKVariable) -> (LKVariable, Bool)? { - for level in reversed() { if let index = level.index(forKey: v) { return level[index] } } - return nil - } -} diff --git a/Sources/LeafKit/LeafParser/LKTuple.swift b/Sources/LeafKit/LeafParser/LKTuple.swift deleted file mode 100644 index 7abe94d8..00000000 --- a/Sources/LeafKit/LeafParser/LKTuple.swift +++ /dev/null @@ -1,101 +0,0 @@ -internal struct LKTuple: LKSymbol { - // MARK: - Stored Properties - - var values: LKParams { didSet { setStates() } } - var labels: [String: Int] { didSet { setStates() } } - var collection: Bool = false - - // MARK: LKSymbol - private(set) var resolved: Bool - private(set) var invariant: Bool - private(set) var symbols: Set - - - // MARK: - Initializer - init(_ tuple: [(label: String?, param: LKParameter)] = []) { - self.values = [] - self.labels = [:] - self.symbols = [] - self.resolved = true - self.invariant = true - self.isEvaluable = true - for index in 0.. String { collection ? "[\(s)]" : "(\(s))" } - - // MARK: LKSymbol - func resolve(_ symbols: inout LKVarStack) -> Self { - if resolved { return self } - var updated = self - for index in values.indices where !values[index].resolved { - updated.values[index] = values[index].resolve(&symbols) - } - return updated - } - - func evaluate(_ symbols: inout LKVarStack) -> LeafData { - if labels.isEmpty { - return .array(values.map { $0.evaluate(&symbols) }) - } else { - let inverted = Dictionary(labels.map { ($0.value, $0.key) }, uniquingKeysWith: {a, _ in a}) - let dict = values.indices.map { (inverted[$0]!, values[$0].evaluate(&symbols)) } - return .dictionary(.init(uniqueKeysWithValues: dict)) - } - } - - private(set) var isEvaluable: Bool - - var baseType: LKDType? { labels.count == values.count ? .dictionary : labels.count == 0 ? .array : nil } - - // MARK: Fake Collection Adherence - var isEmpty: Bool { values.isEmpty } - var count: Int { values.count } - - var enumerated: [(label: String?, value: LKParameter)] { - let inverted = Dictionary(uniqueKeysWithValues: labels.map { ($0.value, $0.key) }) - return values.enumerated().map { (inverted[$0.offset], $0.element) } - } - - subscript(index: String) -> LKParameter? { - get { if let i = labels[index] { return i < count ? self[i] : nil }; return nil } - set { if let i = labels[index], i < count { self[i] = newValue } } - } - - subscript(index: Int) -> LKParameter? { - get { (0.. Set { + switch self { + case .parameter(_): return .init() + case .expression(let e): return e.imports() + case .tag(let t): + guard t.name == "import" else { return t.imports() } + guard let parameter = t.params.first, + case .parameter(let p) = parameter, + case .stringLiteral(let key) = p, + !key.isEmpty else { return .init() } + return .init(arrayLiteral: key) + } + } + + internal func inlineImports(_ imports: [String : Syntax.Export]) -> ParameterDeclaration { + switch self { + case .parameter(_): return self + case .tag(let t): + guard t.name == "import" else { + return .tag(.init(name: t.name, params: t.params.inlineImports(imports))) + } + guard let parameter = t.params.first, + case .parameter(let p) = parameter, + case .stringLiteral(let key) = p, + let export = imports[key]?.body.first, + case .expression(let exp) = export, + exp.count == 1, + let e = exp.first else { return self } + return e + case .expression(let e): + guard !e.isEmpty else { return self } + return .expression(e.inlineImports(imports)) + } + } +} + +// MARK: - Internal Helper Extensions + +internal extension Array where Element == ParameterDeclaration { + // evaluate a flat array of Parameters ("Expression") + // returns true if the expression was reduced, false if + // not or if unreducable (eg, non-flat or no operands). + // Does not promise that the resulting Expression is valid. + // This is brute force and not very efficient. + @discardableResult mutating func evaluate() -> Bool { + // Expression with no operands can't be evaluated + var ops = operandCount() + guard ops > 0 else { return false } + // check that the last param isn't an op, this is not resolvable + // since there are no unary postfix options currently + guard last?.operator() == nil else { return false } + + groupOps: for map in LeafOperator.precedenceMap { + while let i = findLastOpWhere(map.check) { + if map.infixed { wrapBinaryOp(i) } + else { wrapUnaryNot(i) } + // Some expression could not be wrapped - probably malformed syntax + if ops == operandCount() { return false } else { ops -= 1 } + if operandCount() == 0 { break groupOps } + } + } + + flatten() + return ops > 1 ? true : false + } + + mutating func flatten() { + while count == 1 { + if case .expression(let e) = self.first! { + self.removeAll() + self.append(contentsOf: e) + } else { return } + } + return + } + + fileprivate mutating func wrapUnaryNot(_ i: Int) { + let rhs = remove(at: i + 1) + if case .parameter(let p) = rhs, case .keyword(let key) = p, key.isBooleanValued { + self[i] = .parameter(.keyword(LeafKeyword(rawValue: String(!key.bool!))!)) + } else { + self[i] = .expression([self[i],rhs]) + } + } + + // could be smarter and check param types beyond verifying non-op but we're lazy here + fileprivate mutating func wrapBinaryOp(_ i: Int) { + // can't wrap unless there's a lhs and rhs + guard self.indices.contains(i-1),self.indices.contains(i+1) else { return } + let lhs = self[i-1] + let rhs = self[i+1] + // can't wrap if lhs or rhs is an operator + if case .parameter(.operator) = lhs { return } + if case .parameter(.operator) = rhs { return } + self[i] = .expression([lhs, self[i], rhs]) + self.remove(at:i+1) + self.remove(at:i-1) + } + + // Helper functions + func operandCount() -> Int { return reduceOpWhere { _ in true } } + func unaryOps() -> Int { return reduceOpWhere { $0.unaryPrefix } } + func binaryOps() -> Int { return reduceOpWhere { $0.infix } } + func reduceOpWhere(_ check: (LeafOperator) -> Bool) -> Int { + return self.reduce(0, { count, pD in + return count + (pD.operator().map { check($0) ? 1 : 0 } ?? 0) + }) + } + + func findLastOpWhere(_ check: (LeafOperator) -> Bool) -> Int? { + for (index, pD) in self.enumerated().reversed() { + if let op = pD.operator(), check(op) { return index } + } + return nil + } + + func describe(_ joinBy: String = " ") -> String { + return self.map {$0.short }.joined(separator: joinBy) + } + + func imports() -> Set { + var result = Set() + self.forEach { result.formUnion($0.imports()) } + return result + } + + func inlineImports(_ imports: [String : Syntax.Export]) -> [ParameterDeclaration] { + guard !self.isEmpty else { return self } + guard !imports.isEmpty else { return self } + return self.map { $0.inlineImports(imports) } + } + + func atomicRaw() -> Syntax? { + // only atomic expressions can be converted + guard self.count < 2 else { return nil } + var buffer = ByteBufferAllocator().buffer(capacity: 0) + // empty expressions = empty raw + guard self.count == 1 else { return .raw(buffer) } + // only single value parameters can be converted + guard case .parameter(let p) = self[0] else { return nil } + switch p { + case .constant(let c): buffer.writeString(c.description) + case .keyword(let k): buffer.writeString(k.rawValue) + case .operator(let o): buffer.writeString(o.rawValue) + case .stringLiteral(let s): buffer.writeString(s) + // .tag, .variable not atomic + default: return nil + } + return .raw(buffer) + } +} diff --git a/Sources/LeafKit/LeafParser/LeafParser.swift b/Sources/LeafKit/LeafParser/LeafParser.swift new file mode 100644 index 00000000..f1dde8db --- /dev/null +++ b/Sources/LeafKit/LeafParser/LeafParser.swift @@ -0,0 +1,371 @@ +// MARK: Subject to change prior to 1.0.0 release +// MARK: - + +extension String: Error {} + +internal struct LeafParser { + // MARK: - Internal Only + + let name: String + + init(name: String, tokens: [LeafToken]) { + self.name = name + self.tokens = tokens + self.offset = 0 + } + + mutating func parse() throws -> [Syntax] { + while let next = peek() { + try handle(next: next) + } + return finished + } + + // MARK: - Private Only + + private var tokens: [LeafToken] + private var offset: Int + + private var finished: [Syntax] = [] + private var awaitingBody: [OpenContext] = [] + + private mutating func handle(next: LeafToken) throws { + switch next { + case .tagIndicator: + let declaration = try readTagDeclaration() + // check terminator first + // always takes priority, especially for dual body/terminator functors + if declaration.isTerminator { try close(with: declaration) } + + // this needs to be a secondary if-statement, and + // not joined above + // + // this allows for dual functors, a la elseif + if declaration.expectsBody { + awaitingBody.append(.init(declaration)) + } else if declaration.isTerminator { + // dump terminators that don't also have a body, + // already closed above + // MUST close FIRST (as above) + return + } else { + let syntax = try declaration.makeSyntax(body: []) + if var last = awaitingBody.last { + last.body.append(syntax) + awaitingBody.removeLast() + awaitingBody.append(last) + } else { + finished.append(syntax) + } + } + case .raw: + let r = try collectRaw() + if var last = awaitingBody.last { + last.body.append(.raw(r)) + awaitingBody.removeLast() + awaitingBody.append(last) + } else { + finished.append(.raw(r)) + } + default: + throw "unexpected token \(next)" + } + } + + private mutating func close(with terminator: TagDeclaration) throws { + guard !awaitingBody.isEmpty else { + throw "\(name): found terminator \(terminator), with no corresponding tag" + } + let willClose = awaitingBody.removeLast() + guard willClose.parent.matches(terminator: terminator) else { throw "\(name): unable to match \(willClose.parent) with \(terminator)" } + + // closed body + let newSyntax = try willClose.parent.makeSyntax(body: willClose.body) + + func append(_ syntax: Syntax) { + if var newTail = awaitingBody.last { + newTail.body.append(syntax) + awaitingBody.removeLast() + awaitingBody.append(newTail) + // if the new syntax is a conditional, it may need to be attached + // to the last parsed conditional + } else { + finished.append(syntax) + } + } + + if case .conditional(let new) = newSyntax { + guard let conditional = new.chain.first else { throw "Malformed syntax block" } + switch conditional.0.naturalType { + // a new if, never attaches to a previous + case .if: + append(newSyntax) + case .elseif, .else: + let aW = awaitingBody.last?.body + let previousBlock: Syntax? + switch aW { + case .none: previousBlock = finished.last + case .some(let b): previousBlock = b.last + } + guard let existingConditional = previousBlock, + case .conditional(var tail) = existingConditional else { + throw "Can't attach \(conditional.0) to \(previousBlock?.description ?? "empty AST")" + } + try tail.attach(new) + switch aW { + case .none: + finished[finished.index(before: finished.endIndex)] = .conditional(tail) + case .some(_): + awaitingBody[awaitingBody.index(before: awaitingBody.endIndex)].body.removeLast() + awaitingBody[awaitingBody.index(before: awaitingBody.endIndex)].body.append(.conditional(tail)) + } + } + } else { + append(newSyntax) + } + } + + // once a tag has started, it is terminated by `.raw`, `.parameters`, or `.tagBodyIndicator` + // FIXME: This is a blind parsing of `.tagBodyIndicator` + // ------ + // A tag MAY NOT expect any body given a certain number of parameters, and this will blindly + // consume colons in that event when it's not inteded; eg `#(variable):` CANNOT expect a body + // and thus the colon should be assumed to be raw. TagDeclaration should first validate expected + // parameter pattern against the actual named tag before assuming expectsBody to be true OR false + private mutating func readTagDeclaration() throws -> TagDeclaration { + // consume tag indicator + guard let first = read(), first == .tagIndicator else { throw "expected .tagIndicator(\(Character.tagIndicator))" } + // a tag should ALWAYS follow a tag indicator + guard let tag = read(), case .tag(let name) = tag else { throw "expected tag name following a tag indicator" } + + // if no further, then we've ended w/ a tag + guard let next = peek() else { return TagDeclaration(name: name, parameters: nil, expectsBody: false) } + + // following a tag can be, + // .raw - tag is complete + // .tagBodyIndicator - ready to read body + // .parametersStart - start parameters + // .tagIndicator - a new tag started + switch next { + // MARK: no param, no body case should be re-evaluated? + // we require that tags have parameter notation INSIDE parameters even when they're + // empty - eg `#tag(anotherTag())` - so `#anotherTag()` should be required, not + // `#anotherTag`. If that's enforced, the only acceptable non-decaying noparam/nobody + // use would be `#endTag` to close a body + case .raw, + .tagIndicator: + // a basic tag, something like `#date` w/ no params, and no body + return TagDeclaration(name: name, parameters: nil, expectsBody: false) + // MARK: anonymous tBI (`#:`) probably should decay tagIndicator to raw? + case .tagBodyIndicator: + if !name.isEmpty { pop() } else { replace(with: .raw(":")) } + return TagDeclaration(name: name, parameters: nil, expectsBody: true) + case .parametersStart: + // An anonymous function `#(variable):` is incapable of having a body, so change tBI to raw + // Can be more intelligent - there should be observer methods on tag declarations to + // allow checking if a certain parameter set requires a body or not + let params = try readParameters() + var expectsBody = false + if peek() == .tagBodyIndicator { + if name.isEmpty { replace(with: .raw(":")) } + else { + pop() + expectsBody = true + } + } + return TagDeclaration(name: name, parameters: params, expectsBody: expectsBody) + default: + throw "found unexpected token " + next.description + } + } + + private mutating func readParameters() throws -> [ParameterDeclaration] { + // ensure open parameters + guard read() == .parametersStart else { throw "expected parameters start" } + + var group = [ParameterDeclaration]() + var paramsList = [ParameterDeclaration]() + + func dump() { + defer { group = [] } + if group.isEmpty { return } + group.evaluate() + if group.count > 1 { paramsList.append(.expression(group)) } + else { paramsList.append(group.first!) } + } + + outer: while let next = peek() { + switch next { + case .parametersStart: + // found a nested () that we will group together into + // an expression, ie: #if(foo == (bar + car)) + let params = try readParameters() + // parameter tags not permitted to have bodies + if params.count > 1 { group.append(.expression(params)) } + else { group.append(params.first!) } + case .parameter(let p): + pop() + switch p { + case .tag(let name): + guard peek() == .parametersStart else { throw "tags in parameter list MUST declare parameter list" } + // TODO: remove recursion, in parameters only not so bad + let params = try readParameters() + // parameter tags not permitted to have bodies + group.append(.tag(.init(name: name, params: params, body: nil))) + default: + group.append(.parameter(p)) + } + case .parametersEnd: + pop() + dump() + break outer + case .parameterDelimiter: + pop() + dump() + case .whitespace: + pop() + continue + default: + break outer + } + } + + paramsList.evaluate() + return paramsList + } + + private mutating func collectRaw() throws -> ByteBuffer { + var raw = ByteBufferAllocator().buffer(capacity: 0) + while let peek = peek(), case .raw(let val) = peek { + pop() + raw.writeString(val) + } + return raw + } + + private func peek() -> LeafToken? { + guard self.offset < self.tokens.count else { + return nil + } + return self.tokens[self.offset] + } + + private mutating func pop() { + self.offset += 1 + } + + private mutating func replace(at offset: Int = 0, with new: LeafToken) { + self.tokens[self.offset + offset] = new + } + + private mutating func read() -> LeafToken? { + guard self.offset < self.tokens.count else { return nil } + guard let val = self.peek() else { return nil } + pop() + return val + } + + private mutating func readWhile(_ check: (LeafToken) -> Bool) -> [LeafToken]? { + guard self.offset < self.tokens.count else { return nil } + var matched = [LeafToken]() + while let next = peek(), check(next) { + matched.append(next) + } + return matched.isEmpty ? nil : matched + } + + private struct OpenContext { + let parent: TagDeclaration + var body: [Syntax] = [] + init(_ parent: TagDeclaration) { + self.parent = parent + } + } + + private struct TagDeclaration { + let name: String + let parameters: [ParameterDeclaration]? + let expectsBody: Bool + + func makeSyntax(body: [Syntax]) throws -> Syntax { + let params = parameters ?? [] + + switch name { + case let n where n.starts(with: "end"): + throw "unable to convert terminator to syntax" + case "": + guard params.count == 1 else { + throw "only single parameter support, should be broken earlier" + } + switch params[0] { + case .parameter(let p): + switch p { + case .variable(_): + return .expression([params[0]]) + case .constant(let c): + var buffer = ByteBufferAllocator().buffer(capacity: 0) + buffer.writeString(c.description) + return .raw(buffer) + case .stringLiteral(let st): + var buffer = ByteBufferAllocator().buffer(capacity: 0) + buffer.writeString(st) + return .raw(buffer) + case .keyword(let kw) : + guard kw.isBooleanValued else { fallthrough } + var buffer = ByteBufferAllocator().buffer(capacity: 0) + buffer.writeString(kw.rawValue) + return .raw(buffer) + default: + throw "unsupported parameter \(p)" + } + // todo: can prevent some duplication here + case .expression(let e): + return .expression(e) + case .tag(let t): + return .custom(t) + } + case "if": + return .conditional(.init(.if(params), body: body)) + case "elseif": + return .conditional(.init(.elseif(params), body: body)) + case "else": + guard params.count == 0 else { throw "else does not accept params" } + return .conditional(.init(.else, body: body)) + case "for": + return try .loop(.init(params, body: body)) + case "export": + return try .export(.init(params, body: body)) + case "extend": + return try .extend(.init(params, body: body)) + case "import": + guard body.isEmpty else { throw "import does not accept a body" } + return try .import(.init(params)) + default: + return .custom(.init(name: name, params: params, body: body)) + } + } + + var isTerminator: Bool { + switch name { + case let x where x.starts(with: "end"): return true + // dual function + case "elseif", "else": return true + default: return false + } + } + + func matches(terminator: TagDeclaration) -> Bool { + guard terminator.isTerminator else { return false } + switch terminator.name { + // if can NOT be a terminator + case "else", "elseif": + // else and elseif can only match to if or elseif + return name == "if" || name == "elseif" + case "endif": + return name == "if" || name == "elseif" || name == "else" + default: + return terminator.name == "end" + name + } + } + } +} diff --git a/Sources/LeafKit/LeafRenderer.swift b/Sources/LeafKit/LeafRenderer.swift new file mode 100644 index 00000000..0d446d6b --- /dev/null +++ b/Sources/LeafKit/LeafRenderer.swift @@ -0,0 +1,225 @@ +// MARK: Subject to change prior to 1.0.0 release + +// MARK: - `LeafRenderer` Summary + +/// `LeafRenderer` implements the full Leaf language pipeline. +/// +/// It must be configured before use with the appropriate `LeafConfiguration` and consituent +/// threadsafe protocol-implementating modules (an NIO `EventLoop`, `LeafCache`, `LeafSource`, +/// and potentially any number of custom `LeafTag` additions to the language). +/// +/// 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 { + // MARK: - Public Only + + /// An initialized `LeafConfiguration` specificying default directory and tagIndicator + public let configuration: LeafConfiguration + /// A keyed dictionary of custom `LeafTags` to extend Leaf's basic functionality, registered + /// with the names which will call them when rendering - eg `tags["tagName"]` can be used + /// in a template as `#tagName(parameters)` + public let tags: [String: LeafTag] + /// A thread-safe implementation of `LeafCache` protocol + public let cache: LeafCache + /// A thread-safe implementation of `LeafSource` protocol + public let sources: LeafSources + /// The NIO `EventLoop` on which this instance of `LeafRenderer` will operate + public let eventLoop: EventLoop + /// Any custom instance data to use (eg, in Vapor, the `Application` and/or `Request` data) + public let userInfo: [AnyHashable: Any] + + /// Initial configuration of LeafRenderer. + public init( + configuration: LeafConfiguration, + tags: [String: LeafTag] = defaultTags, + cache: LeafCache = DefaultLeafCache(), + sources: LeafSources, + eventLoop: EventLoop, + userInfo: [AnyHashable: Any] = [:] + ) { + self.configuration = configuration + self.tags = tags + self.cache = cache + self.sources = sources + self.eventLoop = eventLoop + self.userInfo = userInfo + } + + /// The public interface to `LeafRenderer` + /// - Parameter path: Name of the template to be used + /// - Parameter context: Any unique context data for the template to use + /// - Returns: Serialized result of using the template, or a failed future + /// + /// Interpretation of `path` is dependent on the implementation of `LeafSource` but is assumed to + /// be relative to `LeafConfiguration.rootDirectory`. + /// + /// Where `LeafSource` is a file sytem based source, some assumptions should be made; `.leaf` + /// extension should be inferred if none is provided- `"path/to/template"` corresponds to + /// `"/.../ViewDirectory/path/to/template.leaf"`, while an explicit extension - + /// `"file.svg"` would correspond to `"/.../ViewDirectory/file.svg"` + public func render(path: String, context: [String: LeafData]) -> EventLoopFuture { + guard path.count > 0 else { return self.eventLoop.makeFailedFuture(LeafError(.noTemplateExists("(no key provided)"))) } + + // If a flat AST is cached and available, serialize and return + if let flatAST = getFlatCachedHit(path), + let buffer = try? serialize(flatAST, context: context) { + return eventLoop.makeSucceededFuture(buffer) + } + + // Otherwise operate using normal future-based full resolving behavior + return self.cache.retrieve(documentName: path, on: self.eventLoop).flatMapThrowing { cached in + guard let cached = cached else { throw LeafError(.noValueForKey(path)) } + guard cached.flat else { throw LeafError(.unresolvedAST(path, Array(cached.unresolvedRefs))) } + return try self.serialize(cached, context: context) + }.flatMapError { e in + return self.fetch(template: path).flatMapThrowing { ast in + guard let ast = ast else { throw LeafError(.noTemplateExists(path)) } + guard ast.flat else { throw LeafError(.unresolvedAST(path, Array(ast.unresolvedRefs))) } + return try self.serialize(ast, context: context) + } + } + } + + + // MARK: - Internal Only + /// Temporary testing interface + internal func render(source: String, path: String, context: [String: LeafData]) -> EventLoopFuture { + guard path.count > 0 else { return self.eventLoop.makeFailedFuture(LeafError(.noTemplateExists("(no key provided)"))) } + let sourcePath = source + ":" + path + // If a flat AST is cached and available, serialize and return + if let flatAST = getFlatCachedHit(sourcePath), + let buffer = try? serialize(flatAST, context: context) { + return eventLoop.makeSucceededFuture(buffer) + } + + return self.cache.retrieve(documentName: sourcePath, on: self.eventLoop).flatMapThrowing { cached in + guard let cached = cached else { throw LeafError(.noValueForKey(path)) } + guard cached.flat else { throw LeafError(.unresolvedAST(path, Array(cached.unresolvedRefs))) } + return try self.serialize(cached, context: context) + }.flatMapError { e in + return self.fetch(source: source, template: path).flatMapThrowing { ast in + guard let ast = ast else { throw LeafError(.noTemplateExists(path)) } + guard ast.flat else { throw LeafError(.unresolvedAST(path, Array(ast.unresolvedRefs))) } + return try self.serialize(ast, context: context) + } + } + } + + // MARK: - Private Only + + /// Given a `LeafAST` and context data, serialize the AST with provided data into a final render + private func serialize(_ doc: LeafAST, context: [String: LeafData]) throws -> ByteBuffer { + guard doc.flat == true else { throw LeafError(.unresolvedAST(doc.name, Array(doc.unresolvedRefs))) } + + var serializer = LeafSerializer( + ast: doc.ast, + context: context, + tags: self.tags, + userInfo: self.userInfo + ) + return try serializer.serialize() + } + + // MARK: `expand()` obviated + + /// Get a `LeafAST` from the configured `LeafCache` or read the raw template if none is cached + /// + /// - If the AST can't be found (either from cache or reading) return nil + /// - If found or read and flat, return complete AST. + /// - If found or read and non-flat, attempt to resolve recursively via `resolve()` + /// + /// Recursive calls to `fetch()` from `resolve()` must provide the chain of extended + /// templates to prevent cyclical errors + private func fetch(source: String? = nil, template: String, chain: [String] = []) -> EventLoopFuture { + return cache.retrieve(documentName: template, on: eventLoop).flatMap { cached in + guard let cached = cached else { + return self.read(source: source, name: template, escape: true).flatMap { ast in + guard let ast = ast else { return self.eventLoop.makeSucceededFuture(nil) } + return self.resolve(ast: ast, chain: chain).map {$0} + } + } + guard cached.flat == false else { return self.eventLoop.makeSucceededFuture(cached) } + return self.resolve(ast: cached, chain: chain).map {$0} + } + } + + /// Attempt to resolve a `LeafAST` + /// + /// - If flat, cache and return + /// - If there are extensions, ensure that (if we've been called from a chain of extensions) no cyclical + /// references to a previously extended template would occur as a result + /// - Recursively `fetch()` any extended template references and build a new `LeafAST` + private func resolve(ast: LeafAST, chain: [String]) -> EventLoopFuture { + // if the ast is already flat, cache it immediately and return + if ast.flat == true { return self.cache.insert(ast, on: self.eventLoop, replace: true) } + + var chain = chain + chain.append(ast.name) + let intersect = ast.unresolvedRefs.intersection(Set(chain)) + guard intersect.count == 0 else { + let badRef = intersect.first ?? "" + chain.append(badRef) + return self.eventLoop.makeFailedFuture(LeafError(.cyclicalReference(badRef, chain))) + } + + let fetchRequests = ast.unresolvedRefs.map { self.fetch(template: $0, chain: chain) } + + let results = EventLoopFuture.whenAllComplete(fetchRequests, on: self.eventLoop) + return results.flatMap { results in + let results = results + var externals: [String: LeafAST] = [:] + for result in results { + // skip any unresolvable references + switch result { + case .success(let external): + guard let external = external else { continue } + externals[external.name] = external + case .failure(let e): return self.eventLoop.makeFailedFuture(e) + } + } + // create new AST with loaded references + let new = LeafAST(from: ast, referencing: externals) + // 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) + } else { + // Cache extended AST & return - AST is either flat or unresolvable + return self.cache.insert(new, on: self.eventLoop, replace: true) + } + } + } + + /// Read in an individual `LeafAST` + /// + /// If the configured `LeafSource` can't read a file, future will fail - otherwise, a complete (but not + /// necessarily flat) `LeafAST` will be returned. + private func read(source: String? = nil, name: String, escape: Bool = false) -> EventLoopFuture { + let raw: EventLoopFuture<(String, ByteBuffer)> + do { + raw = try self.sources.find(template: name, in: source , on: self.eventLoop) + } catch { return eventLoop.makeFailedFuture(error) } + + return raw.flatMapThrowing { raw -> LeafAST? in + var raw = raw + guard let template = raw.1.readString(length: raw.1.readableBytes) else { + throw LeafError.init(.unknownError("File read failed")) + } + let name = source == nil ? name : raw.0 + name + + var lexer = LeafLexer(name: name, template: LeafRawTemplate(name: name, src: template)) + let tokens = try lexer.lex() + var parser = LeafParser(name: name, tokens: tokens) + let ast = try parser.parse() + return LeafAST(name: name, ast: ast) + } + } + + private func getFlatCachedHit(_ path: String) -> LeafAST? { + // If cache provides blocking load, try to get a flat AST immediately + guard let blockingCache = cache as? SynchronousLeafCache, + let cached = try? blockingCache.retrieve(documentName: path), + cached.flat else { return nil } + return cached + } +} diff --git a/Sources/LeafKit/LeafRenderer/LeafRenderer+Context.swift b/Sources/LeafKit/LeafRenderer/LeafRenderer+Context.swift deleted file mode 100644 index a7b39780..00000000 --- a/Sources/LeafKit/LeafRenderer/LeafRenderer+Context.swift +++ /dev/null @@ -1,364 +0,0 @@ -public extension LeafRenderer.Context { - static func emptyContext(isRoot: Bool = false) -> Self { .init(isRootContext: isRoot) } - - /// Initialize a context with the given dictionary assigned to `self` - init(_ context: [String: LeafDataRepresentable], isRoot: Bool = false) { - self.isRootContext = isRoot - try! setValues(to: context) - } - - /// Initialize a context with the given dictionary literal assigned to `self` - init(dictionaryLiteral elements: (String, LeafDataRepresentable)...) { - self = .init(.init(uniqueKeysWithValues: elements)) } - - /// Initialize a context with the given dictionary literal assigned to `self` - init(dictionaryLiteral elements: (String, LeafData)...) { - self = .init(.init(uniqueKeysWithValues: elements)) } - - /// Initialize a context from `Encodable`objects - init(encodable: [String: Encodable], isRoot: Bool = false) { - self.init(encodable.mapValues { $0.encodeToLeafData() }, isRoot: isRoot) - } - - /// Failable intialize `self` scope from `Encodable` object that returns a keyed container - init?(encodable asSelf: Encodable, isRoot: Bool = false) { - guard case .dictionary(let d) = asSelf.encodeToLeafData().container else { - return nil } - self.init(d, isRoot: isRoot) - } - - static var defaultContextScope: String { LKVariable.selfScope } - - subscript(scope scope: String = defaultContextScope, key: String) -> LeafDataRepresentable? { - get { self[.scope(scope), key] } - /// Public subscriptor for context values will silently fail if the scope is invalid or an existing value is not updatable - set { - guard !scope.isEmpty && !key.isEmpty, - !blocked(in: scope).contains(key), - let scope = try? getScopeKey(scope), - isUpdateable(scope, key) else { return } - let literal = !(self[scope, key]?.isVariable ?? true) - self[scope, key] = newValue != nil ? .init(newValue!, literal) : nil - } - } - - /// Set a specific value (eg `$app[key]`) to new value; follows same rules as `setVariable` - /// - /// Keys that are valid variable names will be published to Leaf (eg, `$app.id`); invalid identifiers will - /// only be accessible by subscripting. - /// - /// If the value is already set, will overwrite the existing value (unless the value or its scope is - /// globally constant *and* LeafKit is already running; literal values may be updated freely prior - /// to LeafKit starting with the caveat that, once declared or locked as literal, it may no longer be - /// reverted to a variable) - mutating func setValue(in scope: String = defaultContextScope, - at key: String, - to value: LeafDataRepresentable, - isLiteral: Bool = false) throws { - guard !key.isEmpty else { throw err("Value key must not be empty string") } - try setValues(in: scope, to: [key: value], allLiteral: isLiteral) - } - - /// Set the contextual values for a specific valid scope, overwriting if any exist. - /// - /// If scope already exists as a literal but LeafKit is running, update will fail. Will initialize context - /// scope if it does not already exist. - mutating func setValues(in scope: String = defaultContextScope, - to values: [String: LeafDataRepresentable], - allLiteral: Bool = false) throws { - try literalGuard(allLiteral) - if scope == LKVariable.selfScope && allLiteral { throw err("`self` cannot be literal") } - try canCreateVariables(scope) - try checkBlockedVariables(in: scope, .init(values.keys)) - - let scopeVar = try getScopeKey(scope) - if contexts[scopeVar]?.literal ?? false { - if LKConf.isRunning { throw err("\(scope) is a literal scoped context and cannot be updated") } - assert(allLiteral, - "\(scope) already exists as a literal scope - setting values is implicitly literal, not variable") - } - if contexts[scopeVar] == nil { contexts[scopeVar] = .init(scopeVar, allLiteral) } - contexts[scopeVar]!.setValues(values, allLiteral: allLiteral) - } - - /// Update an already existing value and maintain its variable/literal state - mutating func updateValue(in scope: String = defaultContextScope, - at key: String, - to value: LeafDataRepresentable) throws { - let scopeVar = try validateScope(scope) - guard let isVariable = contexts[scopeVar]![key]?.isVariable else { - throw err("Value must already be set to update") } - guard isVariable || !LKConf.isRunning else { - throw err("Literal context values cannot be updated after LeafKit starts") } - contexts[scopeVar]![key] = isVariable ? .variable(value) : .literal(value) - } - - /// Lock an entire existing scope and all its contained values as globally literal - mutating func lockAsLiteral(scope: String) throws { - try literalGuard() - let scopeVar = try validateScope(scope) - contexts[scopeVar]!.setLiteral() - } - - /// Lock an existing value as globally literal - mutating func lockAsLiteral(in scope: String = defaultContextScope, - key: String) throws { - try literalGuard() - let scopeVar = try validateScope(scope) - if self[scopeVar, key] == nil { throw nonExistant(scope, key) } - contexts[scopeVar]!.setLiteral(key) - } - - /// Cache the current value of `leafData` in context for an existing key - /// - /// Only applicable for variable values; locking a scope or value as literal, or declaring as such, - /// will inherently have cached the value - mutating func cacheValue(in scope: String = defaultContextScope, - at key: String) throws { - let scopeVar = try validateScope(scope) - if self[scopeVar, key] == nil { throw nonExistant(scope, key) } - self[scopeVar, key]!.refresh() - } - - /// All context variable scopes that exist in the object - var registeredContextScopes: [String] { contexts.keys.compactMap {$0.scope} } - - /// All registered contextual objects in the object - var registeredContextObjects: [(scope: String, object: Any)] { - var all: [(scope: String, object: Any)] = [] - objects.forEach { (k, v) in v.forEach { if $0.0.contains(.contextual) { all.append((k, $0.1)) } } } - return all - } - - /// All registered unsafeObject keys - var registeredUnsafeObjects: [String] { .init(unsafeObjects.keys) } - - - /// Register a Swift object to the context. - /// - /// `type: ObjectMode` specifies what ways the object is registered to the context; one or both of: - /// * As a context publishing object (must adhere to either `LeafContextPublisher` [preferred] or - /// `LeafDataRepresentable` resolving to `LeafData.dictionary`). - /// * As a raw object that `LeafUnsafeEntity` objects will have access to during serializing. - /// - /// In both cases, `key` represents the access method - for contextual objects, the values it registers - /// will be published as variables under `$key` scope in Leaf, and for unsafe objects, tags with access - /// will have `externalObjects[key]` access to the exact object. - mutating func register(object: Any, - toScope key: String, - type: ObjectMode = .default) throws { - precondition(!(type.intersection([.unsafe, .contextual])).isEmpty, - "Objects to register must be at least one of `unsafe` or `contextual`") - - var new = (type, object, Set()) - - if type.contains(.unsafe) { - try canOverlay(key, .unsafe) - unsafeObjects[key] = object - } - - if type.contains(.contextual) { - guard key.isValidLeafIdentifier else { - throw err("\(key) is not a valid identifier for a variable scope") } - try canCreateVariables(key) - - let values: [String: LeafDataRepresentable] - - if let c = object as? LeafContextPublisher { - values = c.leafVariables.mapValues { $0.container } } - else if let data = (object as? LeafDataRepresentable)?.leafData.dictionary { - values = data } - else { - values = [:] - assertionFailure("A registered external object must be either `LeafContextPublisher` or `LeafDataRepresentable` vending a dictionary when `type` contains `.contextual`") } - - if !values.isEmpty, type.contains(.preventOverlay) { new.2.formUnion(values.keys) } - try setValues(in: key, to: values) - } - - objects[key, default: []].append(new) - } - - mutating func register(generators: [String: LeafDataGenerator], - toScope key: String) throws { - try setValues(in: key, to: generators.mapValues { $0.container }) - } - - /// Clear an entire context scope. - /// - /// Note that if values were previously registered from an object, they will be removed unconditionally, - /// and if such reigstered objects prevent overlay or extension, this will not reset that state, so new - /// values will not be addable. - mutating func clearContext(scope key: String) throws { - guard let scope = try? validateScope(key) else { throw err("\(key) is not a valid scope") } - - defer { contexts[scope] = nil; if isRootContext { anyLiteral = !literalsOnly.contexts.isEmpty } } - - guard LKConf.isRunning else { return } - guard let ctx = contexts[scope], !ctx.literal else { - throw err("\(key) is a literal scope - cannot be unset while running") } - guard ctx.values.allSatisfy({$0.value.isVariable}) else { - throw err("\(key) has literal values - cannot be unset while running") } - } - - /// Remove a registered unsafe object, if it exists and is not locked. - mutating func clearUnsafeObject(key: String) throws { - try objects[key]?.indices.forEach { - if objects[key]![$0].0.contains(.unsafe) { - if objects[key]![$0].0.contains(.preventOverlay) { - throw err("\(key) is locked to an object - can't clear") } - objects[key]![$0].0.remove(.unsafe) - unsafeObjects.removeValue(forKey: key) - } - } - } - - /// Overlay & merge the values of a second context onto a base one. - /// - /// When stacking multiple contexts, only a root context may contain literals, so overlaying any - /// additional context values must be entirely variable (and if conflicts occur in a value where the - /// underlaying context holds a literal value, will error). - /// - /// If a context has options, those set in the second context will always override the lower context's options. - mutating func overlay(_ secondary: Self) throws { - guard !secondary.isRootContext else { throw err("Overlaid contexts cannot be root contexts") } - try secondary.unsafeObjects.forEach { - try canOverlay($0.key, .unsafe) - unsafeObjects[$0.key] = $0.value - } - try secondary.contexts.forEach { k, v in - let scope = k.scope! - try canCreateVariables(scope) - if contexts[k] == nil { contexts[k] = v } - else { - let blockList = blocked(in: scope) - for key in v.values.keys { - if blockList.contains(key) { throw err("\(key) is locked to an object and cannot be overlaid") } - if !(contexts[k]![key]?.isVariable ?? true) { - throw err("\(k.extend(with: key).terse) is literal in underlaying context; can't override") } - contexts[k]![key] = v[key] - } - } - } - if secondary.options != nil { - if options == nil { options = secondary.options } - else { secondary.options!._storage.forEach { options!._storage.update(with: $0) } } - } - } -} - -internal extension LeafRenderer.Context { - subscript(scope: LKVariable, variable: String) -> LKDataValue? { - get { contexts[scope]?[variable] } - set { - if contexts[scope] == nil { - if newValue == nil { return } - contexts[scope] = .init(scope) - } - contexts[scope]![variable] = newValue - } - } - - /// All scope & scoped atomic variables defined by the context - var allVariables: Set { - contexts.values.reduce(into: []) {$0.formUnion($1.allVariables)} } - - /// Return a filtered version of the context that holds only literal values for parse stage - var literalsOnly: Self { - guard isRootContext else { return .init(isRootContext: false) } - var contexts = self.contexts - for (scope, context) in contexts { - if context.literal { continue } - context.values.forEach { k, v in if v.isVariable { contexts[scope]![k] = nil } } - if contexts[scope]!.values.isEmpty { contexts[scope] = nil } - } - return .init(isRootContext: true, contexts: contexts) - } - - var timeout: Double { - if case .timeout(let b) = options?[.timeout] { return b } - else { return LKROption.timeout } } - var parseWarningThrows: Bool { - if case .parseWarningThrows(let b) = options?[.parseWarningThrows] { return b } - else { return LKROption.parseWarningThrows } } - var missingVariableThrows: Bool { - if case .missingVariableThrows(let b) = options?[.missingVariableThrows] { return b } - else { return LKROption.missingVariableThrows } } - var grantUnsafeEntityAccess: Bool { - if case .grantUnsafeEntityAccess(let b) = options?[.grantUnsafeEntityAccess] { return b } - else { return LKROption.grantUnsafeEntityAccess } } - var encoding: String.Encoding { - if case .encoding(let e) = options?[.encoding] { return e } - else { return LKROption.encoding } } - var caching: LeafCacheBehavior { - if case .caching(let c) = options?[.caching] { return c } - else { return LKROption.caching } } - var embeddedASTRawLimit: UInt32 { - if !caching.contains(.limitRawInlines) { return caching.contains(.embedRawInlines) ? .max : 0 } - if case .embeddedASTRawLimit(let l) = options?[.embeddedASTRawLimit] { return l } - else { return LKROption.embeddedASTRawLimit } - } - var pollingFrequency: Double { - if !caching.contains(.autoUpdate) { return .infinity } - if case .pollingFrequency(let d) = options?[.pollingFrequency] { return d } - else { return LKROption.pollingFrequency } - } -} - -private extension LeafRenderer.Context { - /// Guard that provided value keys aren't blocked in the scope - func checkBlockedVariables(in scope: String, _ keys: Set) throws { - let blockList = keys.intersection(blocked(in: scope)) - if !blockList.isEmpty { - throw err("\(blockList.description) \(blockList.count == 1 ? "is" : "are") locked to object(s) in context and cannot be overlaid") } - } - - /// Guard that an object can be registered to a scope - func canOverlay(_ scope: String, _ type: ObjectMode = .bothModes) throws { - if type.contains(.contextual) { try canCreateVariables(scope) } - if let x = objects[scope]?.last(where: { !$0.0.intersection(type).isEmpty && $0.0.contains(.preventOverlay) }) { - throw err("Can't overlay; \(String(describing: x.1)) already registered for `\(scope)`") } - } - - /// Guard that scope isn't locked to an object - func canCreateVariables(_ scope: String) throws { - if let x = objects[scope]?.last(where: { $0.0.contains(.lockContextVariables) }) { - throw err("Can't create variables; \(String(describing: x.1)) locks variables in `\(scope)`") } - } - - /// Validate context is root - mutating func literalGuard(_ forLiteral: Bool = true) throws { - guard forLiteral else { return } - guard isRootContext else { throw err("Cannot set literal values on non-root context") } - anyLiteral = true - } - - /// Helper error generator - func nonExistant(_ scope: String, _ key: String? = nil) -> LeafError { - err("\(scope)\(key != nil ? "[\(key!)]" : "") does not exist in context") } - - /// Require that the identifier is valid and scope exists - func validateScope(_ scope: String) throws -> LKVariable { - let scopeVar = try getScopeKey(scope) - guard contexts[scopeVar] != nil else { throw err("\(scopeVar) does not exist in context") } - return scopeVar - } - - /// Require that the identifier is valid - func getScopeKey(_ scope: String) throws -> LKVariable { - guard scope.isValidLeafIdentifier else { throw err(.invalidIdentifier(scope)) } - return .scope(scope) - } - - /// If a given scope/key in the context is updatable, purely on existential state (variable or literal prior to running) - func isUpdateable(_ scope: LKVariable, _ key: String) -> Bool { - if contexts[scope]?.frozen ?? false { return false } - if contexts[scope]?.literal ?? false && LKConf.isRunning { return false } - return self[scope, key]?.isVariable ?? true || !LKConf.isRunning - } - - /// List of variable keys that are blocked from being overlaid in the named scope - func blocked(in scope: String) -> Set { - objects[scope]?.filter { $0.0.isSuperset(of: [.contextual, .preventOverlay]) } - .reduce(into: Set()) { $0.formUnion($1.2) } ?? [] } -} diff --git a/Sources/LeafKit/LeafRenderer/LeafRenderer+Options.swift b/Sources/LeafKit/LeafRenderer/LeafRenderer+Options.swift deleted file mode 100644 index 062e6b08..00000000 --- a/Sources/LeafKit/LeafRenderer/LeafRenderer+Options.swift +++ /dev/null @@ -1,80 +0,0 @@ -// MARK: - Public Implementation - -public extension LeafRenderer.Option { - /// The current global configuration for rendering options - static var allCases: [Self] {[ - .timeout(Self.$timeout._unsafeValue), - .parseWarningThrows(Self.$parseWarningThrows._unsafeValue), - .missingVariableThrows(Self.$missingVariableThrows._unsafeValue), - .grantUnsafeEntityAccess(Self.$grantUnsafeEntityAccess._unsafeValue), - .encoding(Self.$encoding._unsafeValue), - .caching(Self.$caching._unsafeValue), - .pollingFrequency(Self.$pollingFrequency._unsafeValue), - .embeddedASTRawLimit(Self.$embeddedASTRawLimit._unsafeValue) - ]} - - func hash(into hasher: inout Hasher) { hasher.combine(celf) } - static func ==(lhs: Self, rhs: Self) -> Bool { lhs.celf == rhs.celf } -} - -public extension LeafRenderer.Options { - /// All global settings for options on `LeafRenderer` - static var globalSettings: Self { .init(LeafRenderer.Option.allCases) } - - init(_ elements: [LeafRenderer.Option]) { - self._storage = elements.reduce(into: []) { - if !$0.contains($1) && $1.valid == true { $0.update(with: $1) } } - } - - init(arrayLiteral elements: LeafRenderer.Option...) { self.init(elements) } - - - /// Unconditionally update the `Options` with the provided `option` - @discardableResult - mutating func update(_ option: LeafRenderer.Option) -> Bool { - let result = option.valid - if result == false { return false } - if result == nil { _storage.remove(option) } else { _storage.update(with: option) } - return true - } - - /// Unconditionally remove the `Options` with the provided `option` - mutating func unset(_ option: LeafRenderer.Option.Case) { - if let x = _storage.first(where: {$0.celf == option}) { _storage.remove(x) } } -} - -// MARK: - Internal Implementation - -internal extension LeafRenderer.Option { - var celf: Case { - switch self { - case .timeout : return .timeout - case .parseWarningThrows : return .parseWarningThrows - case .missingVariableThrows : return .missingVariableThrows - case .grantUnsafeEntityAccess : return .grantUnsafeEntityAccess - case .encoding : return .encoding - case .caching : return .caching - case .embeddedASTRawLimit : return .embeddedASTRawLimit - case .pollingFrequency : return .pollingFrequency - } - } - - /// Validate that the local setting for an option is acceptable or ignorable (matches global setting) - var valid: Bool? { - switch self { - case .parseWarningThrows(let b) : return Self.$parseWarningThrows.validate(b) - case .missingVariableThrows(let b) : return Self.$missingVariableThrows.validate(b) - case .grantUnsafeEntityAccess(let b) : return Self.$grantUnsafeEntityAccess.validate(b) - case .timeout(let t) : return Self.$timeout.validate(t) - case .encoding(let e) : return Self.$encoding.validate(e) - case .caching(let c) : return Self.$caching.validate(c) - case .embeddedASTRawLimit(let l) : return Self.$embeddedASTRawLimit.validate(l) - case .pollingFrequency(let d) : return Self.$pollingFrequency.validate(d) - } - } -} - -internal extension LeafRenderer.Options { - subscript(key: LeafRenderer.Option.Case) -> LeafRenderer.Option? { - _storage.first(where: {$0.celf == key}) } -} diff --git a/Sources/LeafKit/LeafRenderer/LeafRenderer.swift b/Sources/LeafKit/LeafRenderer/LeafRenderer.swift deleted file mode 100644 index fc240c36..00000000 --- a/Sources/LeafKit/LeafRenderer/LeafRenderer.swift +++ /dev/null @@ -1,387 +0,0 @@ -import Foundation - -// MARK: Subject to change prior to 1.0.0 release - -// MARK: - `LeafRenderer` Summary - -/// `LeafRenderer` implements the full Leaf language pipeline. -/// -/// It must be configured before use with the appropriate `LeafConfiguration` and consituent -/// threadsafe protocol-implementating modules (an NIO `EventLoop`, `LeafCache`, `LeafSource`, -/// and potentially any number of custom `LeafTag` additions to the language). -/// -/// 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 { - // MARK: Instance Properties - /// A thread-safe implementation of `LeafCache` protocol - public let cache: LeafCache - /// A thread-safe implementation of `LeafSource` protocol - public let sources: LeafSources - - /// Initial configuration of LeafRenderer. - public init(cache: LeafCache, - sources: LeafSources, - eventLoop: EventLoop) { - if !LKConf.started { LKConf.started = true } - - self.cache = cache - self.sources = sources - self.eL = eventLoop - self.blockingCache = cache as? LKSynchronousCache - self.cacheIsSync = blockingCache != nil - } - - // MARK: Private Only - private let eL: EventLoop - private let cacheIsSync: Bool - private let blockingCache: LKSynchronousCache? - - // MARK: - Scoped Objects - - // MARK: - LeafRenderer.Option - /// Locally overrideable options for how LeafRenderer handles rendering - public enum Option: Hashable, CaseIterable { - /// Rendering timeout duration limit in seconds; must be at least 1ms - @LeafRuntimeGuard(condition: {$0 >= 0.001}) - public static var timeout: Double = 0.050 - - /// If true, warnings during parse will throw errors. - @LeafRuntimeGuard public static var parseWarningThrows: Bool = true - - /// Controls behavior of serialize when a variable has no value in context: - /// When true, throws an error and aborts serializing; when false, returns Void? and decays chain. - @LeafRuntimeGuard public static var missingVariableThrows: Bool = true - - /// When true, `LeafUnsafeEntity` tags will have access to contextual objects - @LeafRuntimeGuard public static var grantUnsafeEntityAccess: Bool = false - - /// Output buffer encoding - @LeafRuntimeGuard public static var encoding: String.Encoding = .utf8 - - /// Behaviors for how render calls will use the configured `LeafCache` for compiled templates - @LeafRuntimeGuard public static var caching: LeafCacheBehavior = .default - - /// The limit in bytes for an `inline(..., as: raw)` statement to embed the referenced - /// raw inline in the *cached* AST. - @LeafRuntimeGuard public static var embeddedASTRawLimit: UInt32 = 4096 - - /// If caching behavior allows auto-updating, the polling frequency dictates how many seconds - /// can elapse before `LeafRenderer` checks the original source for changes - @LeafRuntimeGuard(condition: {$0.sign == .plus}) - public static var pollingFrequency: Double = 10.0 - - case timeout(Double) - case parseWarningThrows(Bool) - case missingVariableThrows(Bool) - case grantUnsafeEntityAccess(Bool) - case encoding(String.Encoding) - case caching(LeafCacheBehavior) - case embeddedASTRawLimit(UInt32) - case pollingFrequency(Double) - - public enum Case: UInt16, RawRepresentable, CaseIterable { - case timeout - case parseWarningThrows - case missingVariableThrows - case grantUnsafeEntityAccess - case encoding - case caching - case embeddedASTRawLimit - case pollingFrequency - } - } - - // MARK: - LeafRenderer.Options - /// A set of configured options for overriding global settings - /// - /// Values are only set if they *actually* override global settings. - /// - public struct Options: ExpressibleByArrayLiteral { - var _storage: Set