Files
2026-01-29 19:53:51 -08:00

1561 lines
56 KiB
Swift
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// Expression.swift
// Expression
//
// Version 0.12.12
//
// Created by Nick Lockwood on 15/09/2016.
// Copyright © 2016 Nick Lockwood. All rights reserved.
//
// Distributed under the permissive MIT license
// Get the latest version from here:
//
// https://github.com/nicklockwood/Expression
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Dispatch
import Foundation
/// Immutable wrapper for a parsed expression
/// Reusing the same Expression instance for multiple evaluations is more efficient
/// than creating a new one each time you wish to evaluate an expression string
public final class Expression: CustomStringConvertible {
private let root: Subexpression
/// Evaluator for individual symbols
public typealias SymbolEvaluator = (_ args: [Double]) throws -> Double
/// Type representing the arity (number of arguments) accepted by a function
public enum Arity: ExpressibleByIntegerLiteral, CustomStringConvertible, Equatable {
public typealias IntegerLiteralType = Int
/// An exact number of arguments
case exactly(Int)
/// A minimum number of arguments
case atLeast(Int)
/// Any number of arguments
public static let any = atLeast(0)
/// ExpressibleByIntegerLiteral constructor
public init(integerLiteral value: Int) {
self = .exactly(value)
}
/// The human-readable description of the arity
public var description: String {
switch self {
case let .exactly(value):
return "\(value) argument\(value == 1 ? "" : "s")"
case let .atLeast(value):
return "at least \(value) argument\(value == 1 ? "" : "s")"
}
}
/// Equatable implementation
/// Note: this works more like a contains() function if
/// one side a range and the other is an exact value
/// This allows foo(x) to match foo(...) in a symbols dictionary
public static func == (lhs: Arity, rhs: Arity) -> Bool {
switch (lhs, rhs) {
case let (.exactly(lhs), .exactly(rhs)),
let (.atLeast(lhs), .atLeast(rhs)):
return lhs == rhs
case let (.atLeast(min), .exactly(value)),
let (.exactly(value), .atLeast(min)):
return value >= min
}
}
}
/// Symbols that make up an expression
public enum Symbol: CustomStringConvertible, Hashable {
/// A named variable
case variable(String)
/// An infix operator
case infix(String)
/// A prefix operator
case prefix(String)
/// A postfix operator
case postfix(String)
/// A function accepting a number of arguments specified by `arity`
case function(String, arity: Arity)
/// A array of values accessed by index
case array(String)
/// The symbol name
public var name: String {
switch self {
case let .variable(name),
let .infix(name),
let .prefix(name),
let .postfix(name),
let .function(name, _),
let .array(name):
return name
}
}
/// Printable version of the symbol name
var escapedName: String {
return UnicodeScalarView(name).escapedIdentifier()
}
/// The human-readable description of the symbol
public var description: String {
switch self {
case .variable:
return "variable \(escapedName)"
case .infix("?:"):
return "ternary operator \(escapedName)"
case .infix("[]"):
return "subscript operator \(escapedName)"
case .infix("()"):
return "function call operator \(escapedName)"
case .infix:
return "infix operator \(escapedName)"
case .prefix:
return "prefix operator \(escapedName)"
case .postfix:
return "postfix operator \(escapedName)"
case .function:
return "function \(escapedName)()"
case .array:
return "array \(escapedName)[]"
}
}
/// Required by the Hashable protocol
public var hashValue: Int {
return name.hashValue
}
/// Equatable implementation
public static func == (lhs: Symbol, rhs: Symbol) -> Bool {
switch (lhs, rhs) {
case let (.variable(lhs), .variable(rhs)),
let (.infix(lhs), .infix(rhs)),
let (.prefix(lhs), .prefix(rhs)),
let (.postfix(lhs), .postfix(rhs)),
let (.array(lhs), .array(rhs)):
return lhs == rhs
case let (.function(lhs), .function(rhs)):
return lhs == rhs
case (.variable, _),
(.infix, _),
(.prefix, _),
(.postfix, _),
(.function, _),
(.array, _):
return false
}
}
}
/// Runtime error when parsing or evaluating an expression
public enum Error: Swift.Error, CustomStringConvertible, Equatable {
/// An application-specific error
case message(String)
/// The parser encountered a sequence of characters it didn't recognize
case unexpectedToken(String)
/// The parser expected to find a delimiter (e.g. closing paren) but didn't
case missingDelimiter(String)
/// The specified constant, operator or function was not recognized
case undefinedSymbol(Symbol)
/// A function was called with the wrong number of arguments (arity)
case arityMismatch(Symbol)
/// An array was accessed with an index outside the valid range
case arrayBounds(Symbol, Double)
/// Empty expression
public static let emptyExpression = unexpectedToken("")
/// The human-readable description of the error
public var description: String {
switch self {
case let .message(message):
return message
case .emptyExpression:
return "Empty expression"
case let .unexpectedToken(string):
return "Unexpected token `\(string)`"
case let .missingDelimiter(string):
return "Missing `\(string)`"
case let .undefinedSymbol(symbol):
return "Undefined \(symbol)"
case let .arityMismatch(symbol):
let arity: Arity
switch symbol {
case let .function(_, requiredArity):
arity = requiredArity
case .infix("()"):
arity = .atLeast(1)
case .array,
.infix("[]"):
arity = 1
case .infix("?:"):
arity = 3
case .infix:
arity = 2
case .postfix,
.prefix:
arity = 1
case .variable:
arity = 0
}
let description = symbol.description
return "\(description.prefix(1).uppercased())\(description.dropFirst()) expects \(arity)"
case let .arrayBounds(symbol, index):
return "Index \(Expression.stringify(index)) out of bounds for \(symbol)"
}
}
/// Equatable implementation
public static func == (lhs: Error, rhs: Error) -> Bool {
switch (lhs, rhs) {
case let (.message(lhs), .message(rhs)),
let (.unexpectedToken(lhs), .unexpectedToken(rhs)),
let (.missingDelimiter(lhs), .missingDelimiter(rhs)):
return lhs == rhs
case let (.undefinedSymbol(lhs), .undefinedSymbol(rhs)),
let (.arityMismatch(lhs), .arityMismatch(rhs)):
return lhs == rhs
case let (.arrayBounds(lhs), .arrayBounds(rhs)):
return lhs == rhs
case (.message, _),
(.unexpectedToken, _),
(.missingDelimiter, _),
(.undefinedSymbol, _),
(.arityMismatch, _),
(.arrayBounds, _):
return false
}
}
}
/// Options for configuring an expression
public struct Options: OptionSet {
/// Disable optimizations such as constant substitution
public static let noOptimize = Options(rawValue: 1 << 1)
/// Enable standard boolean operators and constants
public static let boolSymbols = Options(rawValue: 1 << 2)
/// Assume all functions and operators in `symbols` are "pure", i.e.
/// they have no side effects, and always produce the same output
/// for a given set of arguments
public static let pureSymbols = Options(rawValue: 1 << 3)
/// Packed bitfield of options
public let rawValue: Int
/// Designated initializer
public init(rawValue: Int) {
self.rawValue = rawValue
}
}
/// Creates an Expression object from a string
/// Optionally accepts some or all of:
/// - A set of options for configuring expression behavior
/// - A dictionary of constants for simple static values
/// - A dictionary of arrays for static collections of related values
/// - A dictionary of symbols, for implementing custom functions and operators
public convenience init(
_ expression: String,
options: Options = [],
constants: [String: Double] = [:],
arrays: [String: [Double]] = [:],
symbols: [Symbol: SymbolEvaluator] = [:]
) {
self.init(
Expression.parse(expression),
options: options,
constants: constants,
arrays: arrays,
symbols: symbols
)
}
/// Alternative constructor that accepts a pre-parsed expression
public convenience init(
_ expression: ParsedExpression,
options: Options = [],
constants: [String: Double] = [:],
arrays: [String: [Double]] = [:],
symbols: [Symbol: SymbolEvaluator] = [:]
) {
// Options
let boolSymbols = options.contains(.boolSymbols) ? Expression.boolSymbols : [:]
let shouldOptimize = !options.contains(.noOptimize)
let pureSymbols = options.contains(.pureSymbols)
/// Evaluators
func symbolEvaluator(for symbol: Symbol) -> SymbolEvaluator? {
if let fn = symbols[symbol] {
return fn
} else if boolSymbols.isEmpty, case .infix("?:") = symbol,
let lhs = symbols[.infix("?")], let rhs = symbols[.infix(":")]
{
// TODO: get rid of this special case? - it's unlikely that it's used by anyone
return { args in try rhs([lhs([args[0], args[1]]), args[2]]) }
}
return nil
}
func pureEvaluator(for symbol: Symbol) -> SymbolEvaluator? {
switch symbol {
case let .variable(name):
if let constant = constants[name] {
return { _ in constant }
}
case let .array(name):
if let array = arrays[name] {
return { args in
guard let index = Int(exactly: floor(args[0])),
array.indices.contains(index)
else {
throw Error.arrayBounds(symbol, args[0])
}
return array[index]
}
}
default:
if let fn = symbolEvaluator(for: symbol) {
return fn
}
}
guard let fn = Expression.mathSymbols[symbol] ?? boolSymbols[symbol] else {
if case let .function(name, _) = symbol {
for case let .function(_name, arity) in symbols.keys where name == _name {
return { _ in throw Error.arityMismatch(.function(name, arity: arity)) }
}
}
return Expression.errorEvaluator(for: symbol)
}
return fn
}
self.init(
expression,
impureSymbols: { symbol in
switch symbol {
case let .variable(name):
if constants[name] == nil, let fn = symbols[symbol] {
return fn
}
case let .array(name):
if arrays[name] == nil, let fn = symbols[symbol] {
return fn
}
default:
if !pureSymbols, let fn = symbolEvaluator(for: symbol) {
return fn
}
}
return shouldOptimize ? nil : pureEvaluator(for: symbol)
},
pureSymbols: pureEvaluator
)
}
/// Alternative constructor for advanced usage
/// Allows for dynamic symbol lookup or generation without any performance overhead
/// Note that both math and boolean symbols are enabled by default - to disable them
/// return `{ _ in throw Expression.Error.undefinedSymbol(symbol) }` from your lookup function
public init(
_ expression: ParsedExpression,
impureSymbols: (Symbol) -> SymbolEvaluator?,
pureSymbols: (Symbol) -> SymbolEvaluator? = { _ in nil }
) {
root = expression.root.optimized(
withImpureSymbols: impureSymbols,
pureSymbols: {
if let fn = pureSymbols($0) ?? Expression.mathSymbols[$0] ?? Expression.boolSymbols[$0] {
return fn
}
if case let .function(name, _) = $0 {
for i in 0 ... 10 {
let symbol = Symbol.function(name, arity: .exactly(i))
if impureSymbols(symbol) ?? pureSymbols(symbol) != nil {
return { _ in throw Error.arityMismatch(symbol) }
}
}
}
return Expression.errorEvaluator(for: $0)
}
)
}
/// Alternative constructor with only pure symbols
public convenience init(_ expression: ParsedExpression, pureSymbols: (Symbol) -> SymbolEvaluator?) {
self.init(expression, impureSymbols: { _ in nil }, pureSymbols: pureSymbols)
}
/// Verify that the string is a valid identifier
public static func isValidIdentifier(_ string: String) -> Bool {
var characters = UnicodeScalarView(string)
switch characters.parseIdentifier() ?? characters.parseEscapedIdentifier() {
case .symbol(.variable, _, _)?:
return characters.isEmpty
default:
return false
}
}
/// Verify that the string is a valid operator
public static func isValidOperator(_ string: String) -> Bool {
var characters = UnicodeScalarView(string)
guard case let .symbol(symbol, _, _)? = characters.parseOperator(),
case let .infix(name) = symbol, name != "(", name != "["
else {
return false
}
return characters.isEmpty
}
private static var cache = [String: Subexpression]()
private static let queue = DispatchQueue(label: "com.Expression")
/// For testing
static func isCached(_ expression: String) -> Bool {
return queue.sync { cache[expression] != nil }
}
/// Parse an expression and optionally cache it for future use.
/// Returns an opaque struct that cannot be evaluated but can be queried
/// for symbols or used to construct an executable Expression instance
public static func parse(_ expression: String, usingCache: Bool = true) -> ParsedExpression {
// Check cache
if usingCache {
var cachedExpression: Subexpression?
queue.sync { cachedExpression = cache[expression] }
if let subexpression = cachedExpression {
return ParsedExpression(root: subexpression)
}
}
// Parse
var characters = Substring.UnicodeScalarView(expression.unicodeScalars)
let parsedExpression = parse(&characters)
// Store
if usingCache {
queue.sync { cache[expression] = parsedExpression.root }
}
return parsedExpression
}
/// Parse an expression directly from the provided UnicodeScalarView,
/// stopping when it reaches a token matching the `delimiter` string.
/// This is convenient if you wish to parse expressions that are nested
/// inside another string, e.g. for implementing string interpolation.
/// If no delimiter string is specified, the method will throw an error
/// if it encounters an unexpected token, but won't consume it
public static func parse(
_ input: inout Substring.UnicodeScalarView,
upTo delimiters: String...
) -> ParsedExpression {
var unicodeScalarView = UnicodeScalarView(input)
let start = unicodeScalarView
var subexpression: Subexpression
do {
subexpression = try unicodeScalarView.parseSubexpression(upTo: delimiters)
} catch {
let expression = String(start.prefix(upTo: unicodeScalarView.startIndex))
subexpression = .error(error as! Error, expression)
}
input = Substring.UnicodeScalarView(unicodeScalarView)
return ParsedExpression(root: subexpression)
}
/// Clear the expression cache (useful for testing, or in low memory situations)
public static func clearCache(for expression: String? = nil) {
queue.sync {
if let expression = expression {
cache.removeValue(forKey: expression)
} else {
cache.removeAll()
}
}
}
/// Returns the optmized, pretty-printed expression if it was valid
/// Otherwise, returns the original (invalid) expression string
public var description: String {
return root.description
}
/// All symbols used in the expression
public var symbols: Set<Symbol> {
return root.symbols
}
/// Evaluate the expression
public func evaluate() throws -> Double {
return try root.evaluate()
}
/// Standard math symbols
public static let mathSymbols: [Symbol: SymbolEvaluator] = {
var symbols: [Symbol: SymbolEvaluator] = [:]
// constants
symbols[.variable("pi")] = { _ in .pi }
// infix operators
symbols[.infix("+")] = { $0[0] + $0[1] }
symbols[.infix("-")] = { $0[0] - $0[1] }
symbols[.infix("*")] = { $0[0] * $0[1] }
symbols[.infix("/")] = { $0[0] / $0[1] }
symbols[.infix("%")] = { fmod($0[0], $0[1]) }
// prefix operators
symbols[.prefix("-")] = { -$0[0] }
// functions - arity 1
symbols[.function("sqrt", arity: 1)] = { sqrt($0[0]) }
symbols[.function("floor", arity: 1)] = { floor($0[0]) }
symbols[.function("ceil", arity: 1)] = { ceil($0[0]) }
symbols[.function("round", arity: 1)] = { round($0[0]) }
symbols[.function("cos", arity: 1)] = { cos($0[0]) }
symbols[.function("acos", arity: 1)] = { acos($0[0]) }
symbols[.function("sin", arity: 1)] = { sin($0[0]) }
symbols[.function("asin", arity: 1)] = { asin($0[0]) }
symbols[.function("tan", arity: 1)] = { tan($0[0]) }
symbols[.function("atan", arity: 1)] = { atan($0[0]) }
symbols[.function("abs", arity: 1)] = { abs($0[0]) }
// functions - arity 2
symbols[.function("pow", arity: 2)] = { pow($0[0], $0[1]) }
symbols[.function("atan2", arity: 2)] = { atan2($0[0], $0[1]) }
symbols[.function("mod", arity: 2)] = { fmod($0[0], $0[1]) }
// functions - variadic
symbols[.function("max", arity: .atLeast(2))] = { $0.reduce($0[0]) { max($0, $1) } }
symbols[.function("min", arity: .atLeast(2))] = { $0.reduce($0[0]) { min($0, $1) } }
return symbols
}()
/// Standard boolean symbols
public static let boolSymbols: [Symbol: SymbolEvaluator] = {
var symbols: [Symbol: SymbolEvaluator] = [:]
// boolean constants
symbols[.variable("true")] = { _ in 1 }
symbols[.variable("false")] = { _ in 0 }
// boolean infix operators
symbols[.infix("==")] = { (args: [Double]) -> Double in args[0] == args[1] ? 1 : 0 }
symbols[.infix("!=")] = { (args: [Double]) -> Double in args[0] != args[1] ? 1 : 0 }
symbols[.infix(">")] = { (args: [Double]) -> Double in args[0] > args[1] ? 1 : 0 }
symbols[.infix(">=")] = { (args: [Double]) -> Double in args[0] >= args[1] ? 1 : 0 }
symbols[.infix("<")] = { (args: [Double]) -> Double in args[0] < args[1] ? 1 : 0 }
symbols[.infix("<=")] = { (args: [Double]) -> Double in args[0] <= args[1] ? 1 : 0 }
symbols[.infix("&&")] = { (args: [Double]) -> Double in args[0] != 0 && args[1] != 0 ? 1 : 0 }
symbols[.infix("||")] = { (args: [Double]) -> Double in args[0] != 0 || args[1] != 0 ? 1 : 0 }
// boolean prefix operators
symbols[.prefix("!")] = { (args: [Double]) -> Double in args[0] == 0 ? 1 : 0 }
// ternary operator
symbols[.infix("?:")] = { (args: [Double]) -> Double in
if args.count == 3 {
return args[0] != 0 ? args[1] : args[2]
}
return args[0] != 0 ? args[0] : args[1]
}
return symbols
}()
}
// MARK: Internal API
extension Expression {
/// Fallback evaluator for when symbol is not found
static func errorEvaluator(for symbol: Symbol) -> SymbolEvaluator {
switch symbol {
case .infix(","),
.infix("[]"),
.function("[]", _),
.infix("()"):
return { _ in throw Error.unexpectedToken(String(symbol.name.prefix(1))) }
case let .function(called, arity):
let keys = Set(mathSymbols.keys).union(boolSymbols.keys)
for case let .function(name, expected) in keys where name == called && arity != expected {
return { _ in throw Error.arityMismatch(.function(called, arity: expected)) }
}
fallthrough
default:
return { _ in throw Error.undefinedSymbol(symbol) }
}
}
}
// MARK: Private API
private extension Expression {
static func stringify(_ number: Double) -> String {
if let int = Int64(exactly: number) {
return "\(int)"
}
return "\(number)"
}
/// https://github.com/apple/swift-evolution/blob/master/proposals/0077-operator-precedence.md
static let operatorPrecedence: [String: (precedence: Int, isRightAssociative: Bool)] = {
var precedences = [
"[]": 100,
"<<": 2, ">>": 2, ">>>": 2, // bitshift
"*": 1, "/": 1, "%": 1, "&": 1, // multiplication
// +, -, |, ^, etc: 0 (also the default)
"..": -1, "...": -1, "..<": -1, // range formation
"is": -2, "as": -2, "isa": -2, // casting
"??": -3, "?:": -3, // null-coalescing
// comparison: -4
"&&": -5, "and": -5, // and
"||": -6, "or": -6, // or
"?": -7, ":": -7, // ternary
// assignment: -8
",": -100,
].mapValues { ($0, false) }
let comparisonOperators = [
"<", "<=", ">=", ">",
"==", "!=", "<>", "===", "!==",
"lt", "le", "lte", "gt", "ge", "gte", "eq", "ne",
]
for op in comparisonOperators {
precedences[op] = (-4, true)
}
let assignmentOperators = [
"=", "*=", "/=", "%=", "+=", "-=",
"<<=", ">>=", "&=", "^=", "|=", ":=",
]
for op in assignmentOperators {
precedences[op] = (-8, true)
}
return precedences
}()
static func `operator`(_ lhs: String, takesPrecedenceOver rhs: String) -> Bool {
let (p1, rightAssociative) = operatorPrecedence[lhs] ?? (0, false)
let (p2, _) = operatorPrecedence[rhs] ?? (0, false)
if p1 == p2 {
return !rightAssociative
}
return p1 > p2
}
static func isOperator(_ char: UnicodeScalar) -> Bool {
// Strangely, this is faster than switching on value
if "/=­+!*%<>&|^~?:".unicodeScalars.contains(char) {
return true
}
switch char.value {
case 0x00A1 ... 0x00A7,
0x00A9,
0x00AB,
0x00AC,
0x00AE,
0x00B0 ... 0x00B1,
0x00B6,
0x00BB,
0x00BF,
0x00D7,
0x00F7,
0x2016 ... 0x2017,
0x2020 ... 0x2027,
0x2030 ... 0x203E,
0x2041 ... 0x2053,
0x2055 ... 0x205E,
0x2190 ... 0x23FF,
0x2500 ... 0x2775,
0x2794 ... 0x2BFF,
0x2E00 ... 0x2E7F,
0x3001 ... 0x3003,
0x3008 ... 0x3030:
return true
default:
return false
}
}
static func isIdentifierHead(_ c: UnicodeScalar) -> Bool {
switch c.value {
case 0x5F,
0x23,
0x24,
0x40, // _ # $ @
0x41 ... 0x5A, // A-Z
0x61 ... 0x7A, // a-z
0x00A8,
0x00AA,
0x00AD,
0x00AF,
0x00B2 ... 0x00B5,
0x00B7 ... 0x00BA,
0x00BC ... 0x00BE,
0x00C0 ... 0x00D6,
0x00D8 ... 0x00F6,
0x00F8 ... 0x00FF,
0x0100 ... 0x02FF,
0x0370 ... 0x167F,
0x1681 ... 0x180D,
0x180F ... 0x1DBF,
0x1E00 ... 0x1FFF,
0x200B ... 0x200D,
0x202A ... 0x202E,
0x203F ... 0x2040,
0x2054,
0x2060 ... 0x206F,
0x2070 ... 0x20CF,
0x2100 ... 0x218F,
0x2460 ... 0x24FF,
0x2776 ... 0x2793,
0x2C00 ... 0x2DFF,
0x2E80 ... 0x2FFF,
0x3004 ... 0x3007,
0x3021 ... 0x302F,
0x3031 ... 0x303F,
0x3040 ... 0xD7FF,
0xF900 ... 0xFD3D,
0xFD40 ... 0xFDCF,
0xFDF0 ... 0xFE1F,
0xFE30 ... 0xFE44,
0xFE47 ... 0xFFFD,
0x10000 ... 0x1FFFD,
0x20000 ... 0x2FFFD,
0x30000 ... 0x3FFFD,
0x40000 ... 0x4FFFD,
0x50000 ... 0x5FFFD,
0x60000 ... 0x6FFFD,
0x70000 ... 0x7FFFD,
0x80000 ... 0x8FFFD,
0x90000 ... 0x9FFFD,
0xA0000 ... 0xAFFFD,
0xB0000 ... 0xBFFFD,
0xC0000 ... 0xCFFFD,
0xD0000 ... 0xDFFFD,
0xE0000 ... 0xEFFFD:
return true
default:
return false
}
}
static func isIdentifier(_ c: UnicodeScalar) -> Bool {
switch c.value {
case 0x30 ... 0x39, // 0-9
0x0300 ... 0x036F,
0x1DC0 ... 0x1DFF,
0x20D0 ... 0x20FF,
0xFE20 ... 0xFE2F:
return true
default:
return isIdentifierHead(c)
}
}
}
/// An opaque wrapper for a parsed expression
public struct ParsedExpression: CustomStringConvertible {
fileprivate let root: Subexpression
/// Returns the pretty-printed expression if it was valid
/// Otherwise, returns the original (invalid) expression string
public var description: String {
return root.description
}
/// All symbols used in the expression
public var symbols: Set<Expression.Symbol> {
return root.symbols
}
/// Any error detected during parsing
public var error: Expression.Error? {
if case let .error(error, _) = root {
return error
}
return nil
}
}
/// The internal expression implementation
private enum Subexpression: CustomStringConvertible {
case literal(Double)
case symbol(Expression.Symbol, [Subexpression], Expression.SymbolEvaluator?)
case error(Expression.Error, String)
var isOperand: Bool {
switch self {
case let .symbol(symbol, args, _) where args.isEmpty:
switch symbol {
case .infix,
.prefix,
.postfix:
return false
default:
return true
}
case .symbol,
.literal:
return true
case .error:
return false
}
}
func evaluate() throws -> Double {
switch self {
case let .literal(value):
return value
case let .symbol(symbol, args, fn):
guard let fn = fn else { throw Expression.Error.undefinedSymbol(symbol) }
return try fn(args.map { try $0.evaluate() })
case let .error(error, _):
throw error
}
}
var description: String {
func arguments(_ args: [Subexpression]) -> String {
return args.map {
if case .symbol(.infix(","), _, _) = $0 {
return "(\($0))"
}
return $0.description
}.joined(separator: ", ")
}
switch self {
case let .literal(value):
return Expression.stringify(value)
case let .symbol(symbol, args, _):
guard isOperand else {
return symbol.escapedName
}
func needsSeparation(_ lhs: String, _ rhs: String) -> Bool {
let lhs = lhs.unicodeScalars.last!, rhs = rhs.unicodeScalars.first!
return lhs == "." || (Expression.isOperator(lhs) || lhs == "-")
== (Expression.isOperator(rhs) || rhs == "-")
}
switch symbol {
case let .prefix(name):
let arg = args[0]
let description = "\(arg)"
switch arg {
case .symbol(.infix, _, _),
.symbol(.postfix, _, _),
.error,
.symbol where needsSeparation(name, description):
return "\(symbol.escapedName)(\(description))" // Parens required
case .symbol,
.literal:
return "\(symbol.escapedName)\(description)" // No parens needed
}
case let .postfix(name):
let arg = args[0]
let description = "\(arg)"
switch arg {
case .symbol(.infix, _, _),
.symbol(.postfix, _, _),
.error,
.symbol where needsSeparation(description, name):
return "(\(description))\(symbol.escapedName)" // Parens required
case .symbol,
.literal:
return "\(description)\(symbol.escapedName)" // No parens needed
}
case .infix(","):
return "\(args[0]), \(args[1])"
case .infix("?:") where args.count == 3:
return "\(args[0]) ? \(args[1]) : \(args[2])"
case .infix("[]"):
return "\(args[0])[\(args[1])]"
case let .infix(name):
let lhs = args[0]
let lhsDescription: String
switch lhs {
case let .symbol(.infix(opName), _, _)
where !Expression.operator(opName, takesPrecedenceOver: name):
lhsDescription = "(\(lhs))"
default:
lhsDescription = "\(lhs)"
}
let rhs = args[1]
let rhsDescription: String
switch rhs {
case let .symbol(.infix(opName), _, _)
where Expression.operator(name, takesPrecedenceOver: opName):
rhsDescription = "(\(rhs))"
default:
rhsDescription = "\(rhs)"
}
return "\(lhsDescription) \(symbol.escapedName) \(rhsDescription)"
case .variable:
return symbol.escapedName
case .function("[]", _):
return "[\(arguments(args))]"
case .function:
return "\(symbol.escapedName)(\(arguments(args)))"
case .array:
return "\(symbol.escapedName)[\(arguments(args))]"
}
case let .error(_, expression):
return expression
}
}
var symbols: Set<Expression.Symbol> {
switch self {
case .literal,
.error:
return []
case let .symbol(symbol, subexpressions, _):
var symbols = Set([symbol])
for subexpression in subexpressions {
symbols.formUnion(subexpression.symbols)
}
return symbols
}
}
func optimized(
withImpureSymbols impureSymbols: (Expression.Symbol) -> Expression.SymbolEvaluator?,
pureSymbols: (Expression.Symbol) -> Expression.SymbolEvaluator
) -> Subexpression {
guard case .symbol(let symbol, var args, _) = self else {
return self
}
args = args.map {
$0.optimized(withImpureSymbols: impureSymbols, pureSymbols: pureSymbols)
}
if let fn = impureSymbols(symbol) {
return .symbol(symbol, args, fn)
}
let fn = pureSymbols(symbol)
var argValues = [Double]()
for arg in args {
guard case let .literal(value) = arg else {
return .symbol(symbol, args, fn)
}
argValues.append(value)
}
guard let result = try? fn(argValues) else {
return .symbol(symbol, args, fn)
}
return .literal(result)
}
}
// MARK: Expression parsing
/// Workaround for horribly slow Substring.UnicodeScalarView perf
private struct UnicodeScalarView {
typealias Index = String.UnicodeScalarView.Index
private let characters: String.UnicodeScalarView
private(set) var startIndex: Index
private(set) var endIndex: Index
init(_ unicodeScalars: String.UnicodeScalarView) {
characters = unicodeScalars
startIndex = characters.startIndex
endIndex = characters.endIndex
}
init(_ unicodeScalars: Substring.UnicodeScalarView) {
self.init(String.UnicodeScalarView(unicodeScalars))
}
init(_ string: String) {
self.init(string.unicodeScalars)
}
var first: UnicodeScalar? {
return isEmpty ? nil : characters[startIndex]
}
var isEmpty: Bool {
return startIndex >= endIndex
}
subscript(_ index: Index) -> UnicodeScalar {
return characters[index]
}
func index(after index: Index) -> Index {
return characters.index(after: index)
}
func prefix(upTo index: Index) -> UnicodeScalarView {
var view = UnicodeScalarView(characters)
view.startIndex = startIndex
view.endIndex = index
return view
}
func suffix(from index: Index) -> UnicodeScalarView {
var view = UnicodeScalarView(characters)
view.startIndex = index
view.endIndex = endIndex
return view
}
mutating func popFirst() -> UnicodeScalar? {
if isEmpty {
return nil
}
let char = characters[startIndex]
startIndex = characters.index(after: startIndex)
return char
}
/// Returns the remaining characters
fileprivate var unicodeScalars: Substring.UnicodeScalarView {
return characters[startIndex ..< endIndex]
}
}
private typealias _UnicodeScalarView = UnicodeScalarView
private extension String {
init(_ unicodeScalarView: _UnicodeScalarView) {
self.init(unicodeScalarView.unicodeScalars)
}
}
private extension Substring.UnicodeScalarView {
init(_ unicodeScalarView: _UnicodeScalarView) {
self.init(unicodeScalarView.unicodeScalars)
}
}
private extension UnicodeScalarView {
mutating func scanCharacters(_ matching: (UnicodeScalar) -> Bool) -> String? {
var index = startIndex
while index < endIndex {
if !matching(self[index]) {
break
}
index = self.index(after: index)
}
if index > startIndex {
let string = String(prefix(upTo: index))
self = suffix(from: index)
return string
}
return nil
}
mutating func scanCharacter(_ matching: (UnicodeScalar) -> Bool = { _ in true }) -> String? {
if let c = first, matching(c) {
self = suffix(from: index(after: startIndex))
return String(c)
}
return nil
}
mutating func scanCharacter(_ character: UnicodeScalar) -> Bool {
return scanCharacter { $0 == character } != nil
}
mutating func scanToEndOfToken() -> String? {
return scanCharacters {
switch $0 {
case " ",
"\t",
"\n",
"\r":
return false
default:
return true
}
}
}
mutating func skipWhitespace() -> Bool {
if let _ = scanCharacters({
switch $0 {
case " ",
"\t",
"\n",
"\r":
return true
default:
return false
}
}) {
return true
}
return false
}
mutating func parseDelimiter(_ delimiters: [String]) -> Bool {
outer: for delimiter in delimiters {
let start = self
for char in delimiter.unicodeScalars {
guard scanCharacter(char) else {
self = start
continue outer
}
}
self = start
return true
}
return false
}
mutating func parseNumericLiteral() -> Subexpression? {
func scanInteger() -> String? {
return scanCharacters {
if case "0" ... "9" = $0 {
return true
}
return false
}
}
func scanHex() -> String? {
return scanCharacters {
switch $0 {
case "0" ... "9",
"A" ... "F",
"a" ... "f":
return true
default:
return false
}
}
}
func scanExponent() -> String? {
let start = self
if let e = scanCharacter({ $0 == "e" || $0 == "E" }) {
let sign = scanCharacter { $0 == "-" || $0 == "+" } ?? ""
if let exponent = scanInteger() {
return e + sign + exponent
}
}
self = start
return nil
}
func scanNumber() -> String? {
var number: String
var endOfInt = self
if let integer = scanInteger() {
if integer == "0", scanCharacter("x") {
return "0x\(scanHex() ?? "")"
}
endOfInt = self
if scanCharacter(".") {
guard let fraction = scanInteger() else {
self = endOfInt
return integer
}
number = "\(integer).\(fraction)"
} else {
number = integer
}
} else if scanCharacter(".") {
guard let fraction = scanInteger() else {
self = endOfInt
return nil
}
number = ".\(fraction)"
} else {
return nil
}
if let exponent = scanExponent() {
number += exponent
}
return number
}
guard let number = scanNumber() else {
return nil
}
guard let value = Double(number) else {
return .error(.unexpectedToken(number), number)
}
return .literal(value)
}
mutating func parseOperator() -> Subexpression? {
if var op = scanCharacters({ $0 == "." }) ?? scanCharacters({ $0 == "-" }) {
if let tail = scanCharacters(Expression.isOperator) {
op += tail
}
return .symbol(.infix(op), [], nil)
}
if let op = scanCharacters(Expression.isOperator) ??
scanCharacter({ "([,".unicodeScalars.contains($0) })
{
return .symbol(.infix(op), [], nil)
}
return nil
}
mutating func parseIdentifier() -> Subexpression? {
func scanIdentifier() -> String? {
var start = self
var identifier = ""
if scanCharacter(".") {
identifier = "."
} else if let head = scanCharacter(Expression.isIdentifierHead) {
identifier = head
start = self
if scanCharacter(".") {
identifier.append(".")
}
} else {
return nil
}
while let tail = scanCharacters(Expression.isIdentifier) {
identifier += tail
start = self
if scanCharacter(".") {
identifier.append(".")
}
}
if identifier.hasSuffix(".") {
self = start
if identifier == "." {
return nil
}
identifier = String(identifier.unicodeScalars.dropLast())
} else if scanCharacter("'") {
identifier.append("'")
}
return identifier
}
guard let identifier = scanIdentifier() else {
return nil
}
return .symbol(.variable(identifier), [], nil)
}
/// Note: this is not actually part of the parser, but is colocated
/// with `parseEscapedIdentifier()` because they should be updated together
func escapedIdentifier() -> String {
guard let delimiter = first, "`'\"".unicodeScalars.contains(delimiter) else {
return String(self)
}
var result = String(delimiter)
var index = self.index(after: startIndex)
while index != endIndex {
let char = self[index]
switch char.value {
case 0:
result += "\\0"
case 9:
result += "\\t"
case 10:
result += "\\n"
case 13:
result += "\\r"
case 0x20 ..< 0x7F,
_ where Expression.isOperator(char) || Expression.isIdentifier(char):
result.append(Character(char))
default:
result += "\\u{\(String(format: "%X", char.value))}"
}
index = self.index(after: index)
}
return result
}
mutating func parseEscapedIdentifier() -> Subexpression? {
guard let delimiter = first,
var string = scanCharacter({ "`'\"".unicodeScalars.contains($0) })
else {
return nil
}
while let part = scanCharacters({ $0 != delimiter && $0 != "\\" }) {
string += part
if scanCharacter("\\"), let c = popFirst() {
switch c {
case "0":
string += "\0"
case "t":
string += "\t"
case "n":
string += "\n"
case "r":
string += "\r"
case "u" where scanCharacter("{"):
let hex = scanCharacters {
switch $0 {
case "0" ... "9",
"A" ... "F",
"a" ... "f":
return true
default:
return false
}
} ?? ""
guard scanCharacter("}") else {
guard let junk = scanToEndOfToken() else {
return .error(.missingDelimiter("}"), string)
}
return .error(.unexpectedToken(junk), string)
}
guard !hex.isEmpty else {
return .error(.unexpectedToken("}"), string)
}
guard let codepoint = Int(hex, radix: 16),
let c = UnicodeScalar(codepoint)
else {
// TODO: better error for invalid codepoint?
return .error(.unexpectedToken(hex), string)
}
string.append(Character(c))
default:
string.append(Character(c))
}
}
}
guard scanCharacter(delimiter) else {
return .error(string == String(delimiter) ?
.unexpectedToken(string) : .missingDelimiter(String(delimiter)), string)
}
string.append(Character(delimiter))
return .symbol(.variable(string), [], nil)
}
mutating func parseSubexpression(upTo delimiters: [String]) throws -> Subexpression {
var stack: [Subexpression] = []
func collapseStack(from i: Int) throws {
guard stack.count > i + 1 else {
return
}
let lhs = stack[i]
let rhs = stack[i + 1]
if lhs.isOperand {
if rhs.isOperand {
guard case let .symbol(.postfix(op), args, _) = lhs else {
// Cannot follow an operand
throw Expression.Error.unexpectedToken("\(rhs)")
}
// Assume postfix operator was actually an infix operator
stack[i] = args[0]
stack.insert(.symbol(.infix(op), [], nil), at: i + 1)
try collapseStack(from: i)
} else if case let .symbol(symbol, _, _) = rhs {
switch symbol {
case _ where stack.count <= i + 2,
.postfix:
stack[i ... i + 1] = [.symbol(.postfix(symbol.name), [lhs], nil)]
try collapseStack(from: 0)
default:
let rhs = stack[i + 2]
if rhs.isOperand {
if stack.count > i + 3 {
switch stack[i + 3] {
case let .symbol(.infix(op2), _, _),
let .symbol(.prefix(op2), _, _),
let .symbol(.postfix(op2), _, _):
if !Expression.operator(symbol.name, takesPrecedenceOver: op2) {
fallthrough
}
default:
try collapseStack(from: i + 2)
return
}
}
if symbol.name == ":", case let .symbol(.infix("?"), args, _) = lhs { // ternary
stack[i ... i + 2] = [.symbol(.infix("?:"), [args[0], args[1], rhs], nil)]
} else {
stack[i ... i + 2] = [.symbol(.infix(symbol.name), [lhs, rhs], nil)]
}
try collapseStack(from: 0)
} else if case let .symbol(symbol2, _, _) = rhs {
if case .prefix = symbol2 {
try collapseStack(from: i + 2)
} else if ["+", "/", "*"].contains(symbol.name) { // Assume infix
stack[i + 2] = .symbol(.prefix(symbol2.name), [], nil)
try collapseStack(from: i + 2)
} else { // Assume postfix
stack[i + 1] = .symbol(.postfix(symbol.name), [], nil)
try collapseStack(from: i)
}
} else if case let .error(error, _) = rhs {
throw error
}
}
} else if case let .error(error, _) = rhs {
throw error
}
} else if case let .symbol(symbol, _, _) = lhs {
// Treat as prefix operator
if rhs.isOperand {
stack[i ... i + 1] = [.symbol(.prefix(symbol.name), [rhs], nil)]
try collapseStack(from: 0)
} else if case .symbol = rhs {
// Nested prefix operator?
try collapseStack(from: i + 1)
} else if case let .error(error, _) = rhs {
throw error
}
} else if case let .error(error, _) = lhs {
throw error
}
}
func scanArguments(upTo delimiter: Unicode.Scalar) throws -> [Subexpression] {
var args = [Subexpression]()
if first != delimiter {
let delimiters = [",", String(delimiter)]
repeat {
do {
try args.append(parseSubexpression(upTo: delimiters))
} catch Expression.Error.unexpectedToken("") {
if let token = scanCharacter() {
throw Expression.Error.unexpectedToken(token)
}
}
} while scanCharacter(",")
}
guard scanCharacter(delimiter) else {
throw Expression.Error.missingDelimiter(String(delimiter))
}
return args
}
_ = skipWhitespace()
var operandPosition = true
var precededByWhitespace = true
while !parseDelimiter(delimiters), let expression =
parseNumericLiteral() ??
parseIdentifier() ??
parseOperator() ??
parseEscapedIdentifier()
{
// Prepare for next iteration
var followedByWhitespace = skipWhitespace() || isEmpty
switch expression {
case let .symbol(.infix(name), _, _):
switch name {
case "(":
switch stack.last {
case let .symbol(.variable(name), _, _)?:
let args = try scanArguments(upTo: ")")
stack[stack.count - 1] =
.symbol(.function(name, arity: .exactly(args.count)), args, nil)
case let last? where last.isOperand:
let args = try scanArguments(upTo: ")")
stack[stack.count - 1] = .symbol(.infix("()"), [last] + args, nil)
default:
// TODO: if we make `,` a multifix operator, we can use `scanArguments()` here instead
// Alternatively: add .function("()", arity: .any), as with []
try stack.append(parseSubexpression(upTo: [")"]))
guard scanCharacter(")") else {
throw Expression.Error.missingDelimiter(")")
}
}
operandPosition = false
followedByWhitespace = skipWhitespace()
case ",":
operandPosition = true
if let last = stack.last, !last.isOperand, case let .symbol(.infix(op), _, _) = last {
// If previous token was an infix operator, convert it to postfix
stack[stack.count - 1] = .symbol(.postfix(op), [], nil)
}
stack.append(expression)
operandPosition = true
followedByWhitespace = skipWhitespace()
case "[":
let args = try scanArguments(upTo: "]")
switch stack.last {
case let .symbol(.variable(name), _, _)?:
guard args.count == 1 else {
throw Expression.Error.arityMismatch(.array(name))
}
stack[stack.count - 1] = .symbol(.array(name), [args[0]], nil)
case let last? where last.isOperand:
guard args.count == 1 else {
throw Expression.Error.arityMismatch(.infix("[]"))
}
stack[stack.count - 1] = .symbol(.infix("[]"), [last, args[0]], nil)
default:
stack.append(.symbol(.function("[]", arity: .exactly(args.count)), args, nil))
}
operandPosition = false
followedByWhitespace = skipWhitespace()
default:
switch (precededByWhitespace, followedByWhitespace) {
case (true, true),
(false, false):
stack.append(expression)
case (true, false):
stack.append(.symbol(.prefix(name), [], nil))
case (false, true):
stack.append(.symbol(.postfix(name), [], nil))
}
operandPosition = true
}
case let .symbol(.variable(name), _, _) where !operandPosition:
operandPosition = true
stack.append(.symbol(.infix(name), [], nil))
default:
operandPosition = false
stack.append(expression)
}
// Next iteration
precededByWhitespace = followedByWhitespace
}
// Check for trailing junk
let start = self
if !parseDelimiter(delimiters), let junk = scanToEndOfToken() {
self = start
throw Expression.Error.unexpectedToken(junk)
}
try collapseStack(from: 0)
switch stack.first {
case let .error(error, _)?:
throw error
case let result?:
if result.isOperand {
return result
}
throw Expression.Error.unexpectedToken(result.description)
case nil:
throw Expression.Error.emptyExpression
}
}
}