Click here to Skip to main content
15,879,535 members
Articles / Programming Languages / Visual Basic

Flee - Fast Lightweight Expression Evaluator

Rate me:
Please Sign up or sign in to vote.
4.91/5 (47 votes)
11 Oct 2007LGPL310 min read 195K   3.7K   108  
A .NET expression evaluator that compiles to IL and is designed for speed.
Imports NUnit.Framework
Imports ciloci.Flee

<TestFixture()> _
Public Class ExpressionTestFixture

	Private Const COMMENT_CHAR As Char = "'"c
	Private Const SEPARATOR_CHAR As Char = ";"c
	Private MyTypeTesterMap As IDictionary
	Private MyExpressionOwner As ExpressionOwner
	Private MyTestImports As ImportsCollection

	Public Sub New()
		MyTypeTesterMap = New Hashtable()
		MyTypeTesterMap.Add(GetType(Byte), GetType(ByteExpressionTester))
		MyTypeTesterMap.Add(GetType(SByte), GetType(SByteExpressionTester))
		MyTypeTesterMap.Add(GetType(Short), GetType(ShortExpressionTester))
		MyTypeTesterMap.Add(GetType(UShort), GetType(UShortExpressionTester))
		MyTypeTesterMap.Add(GetType(Integer), GetType(IntegerExpressionTester))
		MyTypeTesterMap.Add(GetType(UInteger), GetType(UnsignedIntegerExpressionTester))
		MyTypeTesterMap.Add(GetType(Int64), GetType(Int64ExpressionTester))
		MyTypeTesterMap.Add(GetType(UInt64), GetType(UInt64ExpressionTester))
		MyTypeTesterMap.Add(GetType(Double), GetType(DoubleExpressionTester))
		MyTypeTesterMap.Add(GetType(Single), GetType(SingleExpressionTester))
		MyTypeTesterMap.Add(GetType(String), GetType(StringExpressionTester))
		MyTypeTesterMap.Add(GetType(Boolean), GetType(BooleanExpressionTester))
		MyTypeTesterMap.Add(GetType(Char), GetType(CharExpressionTester))
		MyTypeTesterMap.Add(GetType(Decimal), GetType(DecimalExpressionTester))
		MyTypeTesterMap.Add(GetType(Object), GetType(ObjectExpressionTester))
		MyExpressionOwner = New ExpressionOwner

		MyTestImports = New ImportsCollection()
		MyTestImports.AddNamespace("System")
		MyTestImports.AllowGlobalImport = True
	End Sub

	<Test()> _
	Public Sub TestValidExpressions()
		Me.ProcessScriptTests("ValidExpressions.txt", AddressOf DoTestValidExpressions)
	End Sub

	<Test()> _
	Public Sub TestInvalidExpressions()
		Me.ProcessScriptTests("InvalidExpressions.txt", AddressOf DoTestInvalidExpressions)
	End Sub

	<Test()> _
	Public Sub TestValidCasts()
		Me.ProcessScriptTests("ValidCasts.txt", AddressOf DoTestValidExpressions)
	End Sub

	<Test()> _
	Public Sub TestCheckedExpressions()
		Me.ProcessScriptTests("CheckedTests.txt", AddressOf DoTestCheckedExpressions)
	End Sub

	<Test()> _
	Public Sub TestStringNewlineEscape()
		Dim e As New Expression("""a\r\nb""", MyExpressionOwner)
		Dim s As String = DirectCast(e.Evaluator, ExpressionEvaluator(Of String))()
		Dim expected As String = String.Format("a{0}b", ControlChars.CrLf)
		Assert.AreEqual(expected, s)
	End Sub

	<Test()> _
	Public Sub TestSerialize()
		Dim ms As New System.IO.MemoryStream()
		Dim bf As New System.Runtime.Serialization.Formatters.Binary.BinaryFormatter()

		Dim owner As New SerializableExpressionOwner()
		Dim options As New ExpressionOptions()
		options.Imports.AddType(GetType(Math))
		options.Imports.ImportBuiltinTypes = True
		Dim e As New Expression("sqrt(byte.maxvalue + 1)", owner, options)
		bf.Serialize(ms, e)

		ms.Seek(0, IO.SeekOrigin.Begin)

		Dim e2 As Expression = bf.Deserialize(ms)
		Dim d As ExpressionEvaluator(Of Double) = e2.Evaluator
		Dim result As Double = d()
		Assert.AreEqual(16.0, result)
	End Sub

	<Test()> _
	Public Sub TestMultiTreaded()
		' Test that we can parse and evaluate from multiple threads
		Dim t1 As New System.Threading.Thread(AddressOf ThreadRun)
		t1.Name = "Thread1"

		Dim t2 As New System.Threading.Thread(AddressOf ThreadRun)
		t2.Name = "Thread2"

		Dim e As New Expression("1+1*200", MyExpressionOwner)

		t1.Start(e)
		t2.Start(e)

		t1.Join()
		t2.Join()
	End Sub

	Private Sub ThreadRun(ByVal o As Object)
		' Test parse
		For i As Integer = 0 To 10 - 1
			Dim e As New Expression("1+1*200", MyExpressionOwner)
		Next

		' Test evaluate
		Dim evaluator As ExpressionEvaluator(Of Integer) = DirectCast(o, Expression).Evaluator

		For i As Integer = 0 To 10 - 1
			evaluator()
		Next
	End Sub

	<Test()> _
	Public Sub TestBuiltinsImport()
		' Test our builtin type importing
		Dim options As New ExpressionOptions()
		options.Imports.ImportBuiltinTypes = True
		Dim e As New Expression("long.maxvalue", MyExpressionOwner, options)
		Dim evaluator As ExpressionEvaluator(Of Long) = e.Evaluator
		Assert.AreEqual(Long.MaxValue, evaluator())

		options.Imports.ImportBuiltinTypes = False
		Me.AssertCompileException("long.maxvalue", GetType(Long), options.Imports)
	End Sub

	<Test()> _
	Public Sub TestBoxing()
		' Test that we box properly
		Dim options As New ExpressionOptions()
		options.ResultType = GetType(Object)

		Dim e1 As New Expression("1000", MyExpressionOwner, options)
		Dim d1 As ExpressionEvaluator(Of Object) = e1.Evaluator
		Dim o As Object = d1()
		Assert.IsInstanceOfType(GetType(Int32), o)

		options.ResultType = GetType(ValueType)

		Dim e2 As New Expression("12.34", MyExpressionOwner, options)
		Dim d2 As ExpressionEvaluator(Of ValueType) = e2.Evaluator
		Dim vt As ValueType = d2()
		Assert.IsInstanceOfType(GetType(Double), vt)
	End Sub

	<Test()> _
	Public Sub TestImports()
		Dim e As Expression
		Dim imps As ImportsCollection
		Dim del As ExpressionEvaluator(Of Double)

		imps = New ImportsCollection()
		' Import math type directly
		imps.AddType(GetType(Math))

		Dim options As New ExpressionOptions()
		options.Imports = imps

		' Should be able to see PI without qualification
		e = New Expression("pi", MyExpressionOwner, options)
		del = e.Evaluator
		Assert.AreEqual(Math.PI, del())

		imps = New ImportsCollection()
		' Import system namespace
		imps.AddNamespace("System")
		options.Imports = imps

		' Should be able to see pi by qualifying with Math
		e = New Expression("Math.pi", MyExpressionOwner, options)
		del = e.Evaluator
		Assert.AreEqual(Math.PI, del())

		imps = New ImportsCollection()
		' Import root namespace
		imps.AllowGlobalImport = True
		options.Imports = imps

		' Should see pi by fully qualified name
		e = New Expression("System.Math.pi", MyExpressionOwner, options)
		del = e.Evaluator
		Assert.AreEqual(Math.PI, del())

		' Import nothing
		imps = New ImportsCollection()
		options.Imports = imps
		' Should not be able to see PI
		Me.AssertCompileException("pi", GetType(Double), imps)
		Me.AssertCompileException("Math.pi", GetType(Double), imps)
		Me.AssertCompileException("system.math.pi", GetType(Double), imps)
	End Sub

	<Test()> _
	Public Sub TestStringEquality()
		' Test our string equality
		Dim options As New ExpressionOptions()

		' Should be equal
		Dim e As New Expression("""abc"" = ""abc""", MyExpressionOwner, options)
		Dim evaluator As ExpressionEvaluator(Of Boolean) = e.Evaluator
		Assert.IsTrue(evaluator())

		' Should not be equal
		e = New Expression("""ABC"" = ""abc""", MyExpressionOwner, options)
		evaluator = e.Evaluator
		Assert.IsFalse(evaluator())

		' Should be not equal
		e = New Expression("""ABC"" <> ""abc""", MyExpressionOwner, options)
		evaluator = e.Evaluator
		Assert.IsTrue(evaluator())

		' Change string compare type
		options.StringComparison = StringComparison.OrdinalIgnoreCase

		' Should be equal
		e = New Expression("""ABC"" = ""abc""", MyExpressionOwner, options)
		evaluator = e.Evaluator
		Assert.IsTrue(evaluator())

		' Should also be equal
		e = New Expression("""ABC"" <> ""abc""", MyExpressionOwner, options)
		evaluator = e.Evaluator
		Assert.IsFalse(evaluator())

		' Should also be not equal
		e = New Expression("""A"" <> ""z""", MyExpressionOwner, options)
		evaluator = e.Evaluator
		Assert.IsTrue(evaluator())
	End Sub

	<Test()> _
	Public Sub TestDynamicExpressionOwner()
		Me.TestStronglyTypedDynamicExpressionOwner()
		Me.TestObjectDynamicExpressionOwner()
	End Sub

	Private Sub TestStronglyTypedDynamicExpressionOwner()
		Dim owner As New DynamicExpressionOwner(Of Integer)
		owner.Variables("a") = 100
		owner.Variables("b") = -100

		Dim e As New Expression("a+b", owner)
		Dim evaluator As ExpressionEvaluator(Of Integer) = e.Evaluator
		Dim result As Integer = evaluator()
		Assert.AreEqual(result, 100 + -100)

		owner.Variables("B") = 1000
		result = evaluator()
		Assert.AreEqual(result, 100 + 1000)
	End Sub

	Private Sub TestObjectDynamicExpressionOwner()
		Dim owner As New DynamicExpressionOwner(Of Object)
		owner.Variables("a") = 100
		owner.Variables("b") = 2.25

		Dim e As New Expression("a+b", owner)
		Dim evaluator As ExpressionEvaluator(Of Double) = e.Evaluator
		Dim result As Double = evaluator()
		Assert.AreEqual(result, 100 + 2.25)

		owner.Variables("B") = 1000.25
		result = evaluator()
		Assert.AreEqual(result, 100 + 1000.25)

		owner.Variables("string") = "String!"
		e = New Expression("string", owner)
		Dim evaluator2 As ExpressionEvaluator(Of String) = e.Evaluator
		Assert.AreEqual(evaluator2(), "String!")
	End Sub

	Private Sub DoTestValidExpressions(ByVal line As String)
		Dim arr As String() = line.Split(SEPARATOR_CHAR)
		Dim typeName As String = String.Concat("System.", arr(0))
		Dim expressionType As Type = Type.GetType(typeName, True, True)

		Dim options As New ExpressionOptions()
		options.ResultType = expressionType
		options.Imports = MyTestImports

		Dim tester As ExpressionTester = Activator.CreateInstance(MyTypeTesterMap.Item(expressionType))
		Dim e As New Expression(arr(1), MyExpressionOwner, options)
		tester.DoTest(e, arr(2))
	End Sub

	Private Sub DoTestInvalidExpressions(ByVal line As String)
		Dim arr As String() = line.Split(SEPARATOR_CHAR)
		Dim expressionType As Type = Type.GetType(arr(0), True, True)
		Me.AssertCompileException(arr(1), expressionType, MyTestImports)
	End Sub

	Private Sub AssertCompileException(ByVal expression As String, ByVal returnType As Type, ByVal imps As ImportsCollection)
		Try
			Dim e As Expression = Me.CreateExpression(expression, returnType)
			Assert.Fail()
		Catch ex As ExpressionCompileException

		End Try
	End Sub

	Private Sub DoTestCheckedExpressions(ByVal line As String)
		Dim arr As String() = line.Split(SEPARATOR_CHAR)
		Dim expression As String = arr(0)
		Dim checked As Boolean = Boolean.Parse(arr(1))
		Dim shouldOverflow As Boolean = Boolean.Parse(arr(2))

		Dim options As New ExpressionOptions()
		options.Imports = MyTestImports
		options.ResultType = GetType(Object)
		options.Checked = checked

		Try
			Dim e As New Expression(expression, MyExpressionOwner, options)
			Dim d As ExpressionEvaluator(Of Object) = e.Evaluator
			d()
			Assert.IsFalse(shouldOverflow)
		Catch ex As OverflowException
			Assert.IsTrue(shouldOverflow)
		End Try
	End Sub

	Private Function CreateExpression(ByVal expression As String, ByVal resultType As Type) As Expression
		Dim options As New ExpressionOptions()
		options.ResultType = resultType
		Return New Expression(expression, MyExpressionOwner, options)
	End Function

	Protected Sub ProcessScriptTests(ByVal scriptFileName As String, ByVal processor As LineProcessor)
		Console.WriteLine("Testing: {0}", scriptFileName)
		Dim scriptPath As String = System.IO.Path.Combine("../../TestScripts", scriptFileName)
		Dim instream As New System.IO.FileStream(scriptPath, IO.FileMode.Open, IO.FileAccess.Read)

		Dim sr As New System.IO.StreamReader(instream)

		Try
			Me.ProcessLines(sr, processor)
		Finally
			instream.Close()
		End Try
	End Sub

	Private Sub ProcessLines(ByVal sr As System.IO.StreamReader, ByVal processor As LineProcessor)
		While sr.Peek() <> -1
			Dim line As String = sr.ReadLine()
			If line.StartsWith(COMMENT_CHAR) = False Then
				Try
					processor(line)
				Catch ex As Exception
					Console.WriteLine("Failed line: " & line)
					Throw
				End Try
			End If
		End While
	End Sub
End Class

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Web Developer
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions