Eval is a lightweight interpreter framework written in Swift, evaluating expressions at runtime
Eval is a lightweight interpreter framework written in Swift, for π±iOS, π₯ macOS, and π§Linux platforms.
It evaluates expressions at runtime, with operators and data types you define.
π Pros | π Cons |
---|---|
π₯ Lightweight - the whole engine is really just a few hundred lines of code | π€ Creating custom operators and data types, on the other hand, can take a few extra lines - depending on your needs |
β Easy to use API - create new language elements in just a matter of seconds | β»οΈ The evaluated result of the expressions must be strongly typed, so you can only accept what type you expect the result is going to be |
π’ Fun - Since it is really easy to play with, itβs joyful to add - even complex - language features | - |
π Fast execution - Iβm trying to optimise as much as possible. Has its limitations though | π§ Since it is a really generic concept, some optimisations cannot be made, compared to native interpreters |
The framework currently supports two different types of execution modes:
Letβs see just a few examples:
Itβs extremely easy to formulate expressions (and evaluate them at runtime), like
5 in 1...3
evaluates to false
Bool type'Eval' starts with 'E'
evaluates to true
Bool type'b' in ['a','c','d']
evaluates to false
Bool typex < 2 ? 'a' : 'b'
evaluates to "a"
or "b"
String type, based on the x
Int input variableDate(2018, 12, 13).format('yyyy-MM-dd')
evaluates to "2018-12-13"
string'hello'.length
evaluates to 5
Integernow
evaluates to Date()
And templates, such as
{% if name != nil %}Hello{% else %}Bye{% endif %} {{ name|default('user') }}!
, whose output is Hello Adam!
or Bye User!
Sequence: {% for i in 1...5 %}{{ 2 * i }} {% endfor %}
which is 2 4 6 8 10
And so on⦠The result of these expressions depends on the content, determined by the evaluation. It can be any type which is returned by the functions (String, [Double], Date, or even custom types of your own.)
You can find various ways of usage in the examples section below.
This is a really early stage of the project, Iβm still deep in the process of all the open-sourcing related tasks, such as firing up a CI, creating a beautiful documentation page, managing administrative tasks around stability.
Please stay tuned for the updates!
For the expressions to work, youβll need to create an interpreter instance, providing your data types and expressions you aim to support, and maybe some input variables - if you need any.
let interpreter = TypedInterpreter(dataTypes: [number, string, boolean, array, date],
functions: [multipication, addition, ternary],
context: Context(variables: ["x": 2.0]))
And call it with a string expression, as follows.
let result = interpreter.evaluate("2 * x + 1") as? Double
Letβs check out a fairly complex example, and build it from scratch! Letβs implement a language which can parse the following expression:
x != 0 ? 5 * x : pi + 1
Thereβs a ternary operator ?:
in there, which we will need. Also, supporting number literals (0
, 5
, and 1
) and boolean types (true/false
). Thereβs also a not equal operator !=
and a pi
constant. Letβs not forget about the addition +
and multiplication *
as well!
First, here are the data types.
let numberLiteral = Literal { value,_ in Double(value) } //Converts every number literal, if it can be represented with a Double instance
let piConstant = Literal("pi", convertsTo: Double.pi)
let number = DataType(type: Double.self, literals: [numberLiteral, piConstant]) { String(describing: $0) }
let trueLiteral = Literal("true", convertsTo: true)
let falseLiteral = Literal("false", convertsTo: false)
let boolean = DataType(type: Bool.self, literals: [trueLiteral, falseLiteral]) { $0 ? "true" : "false" }
(The last parameter, expressed as a block, tells the framework how to formulise this type of data as a String for debug messages or other purposes)
Now, letβs build the operators:
let multiplication = Function<Double>(Variable<Double>("lhs") + Keyword("*") + Variable<Double>("rhs")) { arguments in
guard let lhs = arguments["lhs"] as? Double, let rhs = arguments["rhs"] as? Double else { return nil }
return lhs * rhs
}
let addition = Function<Double>(Variable<Double>("lhs") + Keyword("+") + Variable<Double>("rhs")) { arguments in
guard let lhs = arguments["lhs"] as? Double, let rhs = arguments["rhs"] as? Double else { return nil }
return lhs + rhs
}
let notEquals = Function<Bool>(Variable<Double>("lhs") + Keyword("!=") + Variable<Double>("rhs")) { arguments in
guard let lhs = arguments["lhs"] as? Double, let rhs = arguments["rhs"] as? Double else { return nil }
return lhs != rhs
}
let ternary = Function<Any>(Variable<Bool>("condition") + Keyword("?") + Variable<Any>("true") + Keyword(":") + Variable<Any>("false")) { arguments in
guard let condition = arguments["condition"] as? Bool else { return nil }
if condition {
return arguments["true"]
} else {
return arguments["false"]
}
}
Looks like, weβre all set. Letβs evaluate our expression!
let interpreter = TypedInterpreter(dataTypes: [number, boolean],
functions: [multipication, addition, notEquals, ternary])
let result : Double = interpreter.evaluate("x != 0 ? 5 * x : pi + 1", context: Context(variables: ["x": 3.0]))
XCTAssertEqual(result, 15.0) //Pass!
Now, that we have operators and data types, we can also evaluate anything using these data types:
interpreter.evaluate("3 != 4") as Bool
interpreter.evaluate("2 + 1.5 * 6") as Double
(since multiplication is defined earlier in the array, it has a higher precedence, as expected)interpreter.evaluate("true ? 1 : 2.5") as Double
As you have seen, itβs really easy and intuitive to build custom languages, using simple building blocks. With just a few custom data types and functions, the possibilities are endless. Operators, functions, string, arrays, datesβ¦
The motto of the framework: Build your own (mini) language!
You have a few options to include the library in your app.
Just add the following line to your dependencies:
.package(url: "https://github.com/tevelee/Eval.git", from: "1.5.0"),
And reference it by name in your targets:
targets: [
.target(name: "MyAwesomeApp", dependencies: ["Eval"]),
]
And finally, run the integration command:
swift package resolve
Just add the following line to your Podfile
:
pod 'Eval', '~> 1.5.0'
And install the new dependency:
pod install
Just add the following line to your Cartfile
:
github "tevelee/Eval" >= 1.5.0
And install the new dependency:
carthage update
(Not recommended! Please use a package manager instead to keep your dependencies up to date.)
Clone the repository content and copy the files into a new target in your app.
The interpreter itself does not define anything or any way to deal with the input string on its own.
All it does is recognising patterns.
By creating data types, you provide literals to the framework, which it can interpret as an element or a result of the expression.
These types are transformed to real Swift types.
By defining functions, you provide patterns to the framework to recognise.
Functions are also typed, they return Swift types as a result of their evaluation.
Functions consist of keywords and variables, nothing more.
if
, or {
, }
).Functions also have blocks, which provide the recognised variables in a key-value dictionary parameter, and you can do whatever you want with them: print them, convert them, modify or assign them to context-variables.
The addition function above, for example, consists of two variables on each side, and the +
keyword in the middle. It also requires a block, where both sides are given in a [String:Any]
, so the closure can get the values of the placeholders and add them together.
Thereβs one interesting aspect of this solution: Unlike traditional - native - interpreters or compilers, this one recognises patterns from top to bottom.
Meaning, that it looks at the input string, your expression, and recognises patterns in priority order, and recursively go deeper and deeper until the most basic expressions are met.
A traditional interpreter, however, parses expressions character by character, feeding the results to a lexer, the tokeniser, then builds up an abstract syntax tree (which is highly optimisable), and finally converts it to a binary (compiler) or evaluates it at runtime (interpreter), in one word: bottom-up.
The two solutions can be compared in various ways. The two main differences are in ease of use, and performance.
This version of an interpreter provides an effortless way to define patterns, types, etc., but has its cost! It cannot parse as optimally as a traditional compiler could, as it doesnβt have an internal graph of expressions (AST), but still performs in a much more than acceptable way.
Definition-wise, this framework provides an easily understandable way of language-elements, but the traditional one really lacks behind, because the lexer is usually an ugly, hardly understandable state machine, or regular expression, BAKED INTO the interpreter code itself.
I have another project, in which Iβm generating Objective-C and Swift model objects with loads of utils, based on really short templates. This project was not possible currently in Swift, as there is no template language - capable enough - to create my templates. (I ended up using a third party PHP framework, called Twig). So finally, I created one for Swift!
It turned out, that making it a little more generic - here and there - makes the whole thing really capable and flexible of using in different use-cases.
The pattern matching was there, but soon I realised, that Iβm going to need expressions as well, for printing, evaluating in if/while statements and so on. First, I was looking at an excellent library, Expression, created by Nick Lockwood, which is capable of evaluating numeric expressions. Unfortunately, I wanted a bit more, defining strings, dates, array, and further types and expressions, so I used my existing pattern matching solution to bring this capability to life.
It ended up quite positively after I discovered the capabilities of a generic solution like this. The whole thing just blew my mind, language features could have been defined in a matter of seconds, and I wanted to share this discovery with the world, so here you are π
β
βI included a few use-cases, which bring significant improvements on how things are processed before - at least in my previous projects.
β
I was able to create a full-blown template language, completely, using this framework and nothing else. Itβs almost like a competitor of the one I mentioned (Twig). This is the most advanced example of them all!
I created a standard library with all the possible operators you can imagine. With helpers, each operator is a small, one-liner addition. Added the important data types, such as arrays, strings, numbers, booleans, dates, etc., and a few functions, to be more awesome. Take a look for inspiration!
Together, it makes an excellent addition to my model-object generation project, and REALLY useful for server-side Swift development as well!
I created another small example, parsing attribtuted strings from simple expressions using XML style tags, such as bold, italic, underlined, colored, etc.
With just a few operators, this solution can deliver attributed strings from basic APIs, which otherwise would be hard to manage.
My connected project is an iOS application, using the Spotify HUB framework, in which I can now provide rich strings with my view-models and parse them from the JSON string results.
A color parser is also used by the BFF (Backend For Frontend, not π) project I mentioned before. It can parse Swift Color objects from many different styles of strings, such as #ffddee
, or red
, or rgba(1,0.5,0.4,1)
. I included this basic example in the repository as well.
Anyone is more than welcome to contribute to Eval! It can even be an addition to the docs or to the code directly, by raising an issue or in the form of a pull request. Both are equally valuable to me! Happy to assist anyone!
In case you need help or want to report a bug - please file an issue. Make sure to provide as much information as you can; sample code also makes it a lot easier for me to help you. Check out the contribution guidelines for further information.
I collected some use cases, and great opportunities for beginner tasks if anybody is motivated to bring this project to a more impressive state!
Please check out https://tevelee.github.io/Eval for the more detailed documentation pages!
I am Laszlo Teveli, software engineer, iOS evangelist. In my free time I like to work on my hobby projects and open sourcing them π
Feel free to reach out to me anytime via tevelee [at] gmail [dot] com
, or @tevelee
on Twitter.
Eval is available under the Apache 2.0 licensing rules. See the LICENSE file for more information.