Files
divkit/client/ios/DivKit/Expressions/CalcExpression/CalcExpression.swift
T
booster a80af2c369 Fixed ternary operator parsing
commit_hash:66d3bd78fb4ebd1148a7f6988f72fc6ff90d8268
2025-08-29 11:23:52 +03:00

689 lines
18 KiB
Swift

//
// CalcExpression.swift
// Expression
//
// Version 0.13.5
//
// 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 Foundation
import VGSL
struct CalcExpression {
private let root: Subexpression
var variableNames: [String] {
root.symbols.compactMap {
if case let .variable(name) = $0 {
return name
}
return nil
}
}
static func parse(_ expression: String) throws -> CalcExpression {
var unicodeScalarView = UnicodeScalarView(expression.unicodeScalars)
return try CalcExpression(root: unicodeScalarView.parseSubexpression(upTo: []))
}
func extractDynamicVariableNames(_ context: ExpressionContext) throws -> [String] {
try root.extractDynamicVariableNames(context)
}
func evaluate(_ context: ExpressionContext) throws -> Any {
try root.evaluate(context)
}
}
enum Subexpression {
case literal(Any)
case symbol(CalcExpression.Symbol, [Subexpression])
var isOperand: Bool {
switch self {
case let .symbol(symbol, args) where args.isEmpty:
switch symbol {
case .infix, .prefix:
false
default:
true
}
case .symbol, .literal:
true
}
}
var symbols: Set<CalcExpression.Symbol> {
switch self {
case .literal:
return []
case let .symbol(symbol, subexpressions):
var symbols = Set([symbol])
for subexpression in subexpressions {
symbols.formUnion(subexpression.symbols)
}
return symbols
}
}
func evaluate(_ context: ExpressionContext) throws -> Any {
switch self {
case let .literal(value):
return value
case let .symbol(symbol, args):
if let evaluator = context.evaluators(symbol) {
return try evaluator.invoke(args: args, context: context)
}
throw ExpressionError("Undefined symbol: \(symbol.name).")
}
}
func extractDynamicVariableNames(_ context: ExpressionContext) throws -> [String] {
switch self {
case .literal:
return []
case let .symbol(symbol, subexpressions):
var dynamicVariables = [String]()
if let variableName = try? context.dynamicVariablesEvaluator(symbol)?.invoke(
args: subexpressions,
context: context
) as? String {
dynamicVariables.append(variableName)
}
for subexpression in subexpressions {
try dynamicVariables.append(contentsOf: subexpression.extractDynamicVariableNames(context))
}
return dynamicVariables
}
}
}
// MARK: Expression parsing
// Workaround for horribly slow Substring.UnicodeScalarView perf
fileprivate struct UnicodeScalarView {
typealias Index = String.UnicodeScalarView.Index
private(set) var startIndex: Index
private(set) var endIndex: Index
private let characters: String.UnicodeScalarView
/// Returns the remaining characters
var unicodeScalars: Substring.UnicodeScalarView {
characters[startIndex..<endIndex]
}
private var first: UnicodeScalar? {
isEmpty ? nil : characters[startIndex]
}
private var isEmpty: Bool {
startIndex >= endIndex
}
init(_ unicodeScalars: String.UnicodeScalarView) {
characters = unicodeScalars
startIndex = characters.startIndex
endIndex = characters.endIndex
}
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
}
private subscript(_ index: Index) -> UnicodeScalar {
characters[index]
}
private func index(after index: Index) -> Index {
characters.index(after: index)
}
private mutating func popFirst() -> UnicodeScalar? {
if isEmpty {
return nil
}
let char = characters[startIndex]
startIndex = characters.index(after: startIndex)
return char
}
}
extension String {
fileprivate init(_ unicodeScalarView: DivKit.UnicodeScalarView) {
self.init(unicodeScalarView.unicodeScalars)
}
}
extension UnicodeScalarView {
private enum Number {
case number(String)
case integer(String)
var value: String {
switch self {
case let .number(value):
value
case let .integer(value):
value
}
}
}
private 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
}
private 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
}
private mutating func scanCharacter(_ character: UnicodeScalar) -> Bool {
scanCharacter { $0 == character } != nil
}
private mutating func scanToEndOfToken() -> String? {
scanCharacters {
switch $0 {
case " ", "\t", "\n", "\r", "(", ")":
false
default:
true
}
}
}
private mutating func skipWhitespace() -> Bool {
if let _ = scanCharacters({
switch $0 {
case " ", "\t", "\n", "\r":
true
default:
false
}
}) {
return true
}
return false
}
private mutating func scanMethodName() -> String? {
_ = skipWhitespace()
var name = ""
if let head = scanCharacter(isIdentifierHead) {
name = head
} else {
return nil
}
if let tail = scanCharacters(isIdentifier) {
name += tail
}
_ = skipWhitespace()
return name
}
private mutating func parseDelimiter(_ delimiters: [String]) -> Bool {
_ = skipWhitespace()
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
}
private mutating func parseToken(isOperandExpected: Bool) throws -> Subexpression? {
if !isOperandExpected, let op = parseOperator() {
return op
}
if let identifier = try parseNumericLiteral() ?? parseIdentifier() ?? parseStringLiteral() {
return identifier
}
if isOperandExpected {
return parseOperator()
}
return nil
}
private mutating func parseNumericLiteral() throws -> Subexpression? {
func scanInteger() -> String? {
scanCharacters {
if case "0"..."9" = $0 {
return true
}
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() throws -> Number? {
var number: Number
var endOfInt = self
let sign = scanCharacter { $0 == "-" } ?? ""
if let integer = scanInteger() {
endOfInt = self
if scanCharacter(".") {
guard let fraction = scanInteger() else {
self = endOfInt
return .integer(sign + integer)
}
number = .number("\(sign)\(integer).\(fraction)")
} else {
number = .integer(sign + integer)
}
} else if scanCharacter(".") {
guard let fraction = scanInteger() else {
self = endOfInt
return nil
}
number = .number("\(sign).\(fraction)")
} else {
self = endOfInt
return nil
}
if let exponent = scanExponent() {
number = .number(number.value + exponent)
}
return number
}
guard let number = try scanNumber() else {
return nil
}
switch number {
case let .integer(value):
guard let intValue = Int(value) else {
throw ExpressionError("Value \(value) can't be converted to Integer type.")
}
return .literal(intValue)
case let .number(value):
guard let doubleValue = Double(value) else {
throw ExpressionError.unexpectedToken(value)
}
return .literal(doubleValue)
}
}
private mutating func parseOperator() -> Subexpression? {
guard let op1 = scanCharacter({ "+-*/%=<>!&|?:(.".unicodeScalars.contains($0) }) else {
return nil
}
let op2: String? = switch op1 {
case "=", "<", ">":
scanCharacter { $0 == "=" }
case "|":
scanCharacter { $0 == "|" }
case "&":
scanCharacter { $0 == "&" }
case "!":
scanCharacter { "=:".unicodeScalars.contains($0) }
case "+", "-":
scanCharacter { "+-".unicodeScalars.contains($0) }
default:
nil
}
if let op2 {
return .symbol(.infix(op1 + op2), [])
}
return .symbol(.infix(op1), [])
}
private mutating func parseIdentifier() throws -> Subexpression? {
var identifier = ""
if let head = scanCharacter(isIdentifierHead) {
identifier = head
} else {
return nil
}
var prevChar: UInt32 = 0
if let tail = scanCharacters({
let char = $0.value
if char == 0x2E, prevChar == 0x2E {
return false
}
prevChar = $0.value
switch char {
case 0x2E: // .
return true
default:
return isIdentifier($0)
}
}) {
identifier += tail
}
if prevChar == 0x2E {
throw ExpressionError.unexpectedToken(".")
}
return makeVariable(identifier)
}
private mutating func parseStringLiteral() throws -> Subexpression? {
if !scanCharacter("'") {
return nil
}
var string = ""
var part: String?
repeat {
part = scanCharacters { $0 != "'" && $0 != "\\" }
string += part ?? ""
if scanCharacter("\\"), let c = popFirst() {
switch c {
case "'", "\\":
string.append(Character(c))
case "@" where scanCharacter("{"):
string += "@{"
default:
throw ExpressionError("Incorrect string escape.")
}
part = ""
}
} while part != nil
if scanCharacter("'") {
return .literal(string)
}
throw ExpressionError("Closing ' expected.")
}
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 {
throw ExpressionError.unexpectedToken("\(rhs)")
}
if case let .symbol(symbol, _) = rhs {
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), _):
guard stack.count > i + 4,
takesPrecedence(symbol.name, over: op2)
else {
fallthrough
}
default:
try collapseStack(from: i + 2)
return
}
}
stack[i...i + 2] = [.symbol(.infix(symbol.name), [lhs, rhs])]
try collapseStack(from: 0)
} else if case let .symbol(symbol2, _) = rhs {
if case .prefix = symbol2 {
try collapseStack(from: i + 2)
} else {
stack[i + 2] = .symbol(.prefix(symbol2.name), [])
try collapseStack(from: i + 2)
}
}
}
} else if case let .symbol(symbol, _) = lhs {
// Treat as prefix operator
if rhs.isOperand {
stack[i...i + 1] = [.symbol(.prefix(symbol.name), [rhs])]
try collapseStack(from: 0)
} else if case .symbol = rhs {
// Nested prefix operator?
try collapseStack(from: i + 1)
}
}
}
func collapseStackToSingleExpression() throws -> Subexpression {
try collapseStack(from: 0)
if let first = stack.first {
if first.isOperand {
return first
}
throw ExpressionError("Operand expected")
}
throw ExpressionError("Empty expression")
}
func scanArguments() throws -> [Subexpression] {
let delimiter: Unicode.Scalar = ")"
var args = [Subexpression]()
if first != delimiter {
let delimiters = [",", String(delimiter)]
repeat {
try args.append(parseSubexpression(upTo: delimiters))
} while scanCharacter(",")
}
guard scanCharacter(delimiter) else {
throw ExpressionError("Closing ) expected.")
}
return args
}
_ = skipWhitespace()
var operandPosition = true
var precededByWhitespace = true
while !parseDelimiter(delimiters),
let expression = try parseToken(isOperandExpected: operandPosition) {
// Prepare for next iteration
var followedByWhitespace = skipWhitespace() || isEmpty
switch expression {
case let .symbol(.infix(name), _):
switch name {
// ternary ?: operator
case "?":
let arg0 = try collapseStackToSingleExpression()
let arg1 = try parseSubexpression(upTo: [":"])
guard scanCharacter(":") else {
throw ExpressionError(": expected.")
}
let arg2 = try parseSubexpression(upTo: delimiters)
stack[stack.count - 1] = .symbol(.ternary, [arg0, arg1, arg2])
// method call
case ".":
guard let lastSymbol = stack.last else {
throw ExpressionError.unexpectedToken(".")
}
guard let methodName = scanMethodName(), scanCharacter("(") else {
throw ExpressionError("Method expected after .")
}
var args = try scanArguments()
args.insert(lastSymbol, at: 0)
stack[stack.count - 1] = makeMethod(methodName, args)
// function or method args
case "(":
switch stack.last {
case let .symbol(.variable(name), _)?:
var args = try scanArguments()
let parts = name.split(separator: ".")
if parts.count == 1 {
// function()
stack[stack.count - 1] = makeFunction(name, args)
} else {
// variable.method()
let variableName = parts.dropLast().joined(separator: ".")
args.insert(makeVariable(variableName), at: 0)
stack[stack.count - 1] = makeMethod(String(parts.last!), args)
}
default:
let subexpression = try parseSubexpression(upTo: [")"])
stack.append(subexpression)
guard scanCharacter(")") else {
throw ExpressionError("Closing ) expected.")
}
}
operandPosition = false
followedByWhitespace = skipWhitespace()
default:
switch (precededByWhitespace, followedByWhitespace) {
case (true, false):
stack.append(.symbol(.prefix(name), []))
default:
stack.append(expression)
}
operandPosition = true
}
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 ExpressionError.unexpectedToken(junk)
}
return try collapseStackToSingleExpression()
}
}
private let operatorPrecedence: [String: (
precedence: Int,
isRightAssociative: Bool
)] = {
var precedences = [
"[]": 100,
"*": 1, "/": 1, "%": 1, // multiplication
// +, -, |, ^, etc: 0 (also the default)
"!:": -3,
// comparison: -4
"&&": -5, // and
"||": -6, // or
":": -8, // ternary
].mapValues { ($0, false) }
precedences["?"] = (-7, true) // ternary
for op in ["<", "<=", ">=", ">", "==", "!="] { // comparison
precedences[op] = (-4, true)
}
return precedences
}()
private func takesPrecedence(_ lhs: String, over rhs: String) -> Bool {
let (p1, rightAssociative) = operatorPrecedence[lhs] ?? (0, false)
let (p2, _) = operatorPrecedence[rhs] ?? (0, false)
if p1 == p2 {
return !rightAssociative
}
return p1 > p2
}
private func isIdentifierHead(_ c: UnicodeScalar) -> Bool {
switch c.value {
case 0x5F, // _
0x41...0x5A, // A-Z
0x61...0x7A: // a-z
true
default:
false
}
}
private func isIdentifier(_ c: UnicodeScalar) -> Bool {
switch c.value {
case 0x2E, // .
0x30...0x39: // 0-9
true
default:
isIdentifierHead(c)
}
}
private func makeVariable(_ name: String) -> Subexpression {
.symbol(.variable(name), [])
}
private func makeFunction(_ name: String, _ args: [Subexpression]) -> Subexpression {
.symbol(.function(name), args)
}
private func makeMethod(_ name: String, _ args: [Subexpression]) -> Subexpression {
.symbol(.method(name), args)
}