This is the third part of YAUL series. For your convenience you can find other parts in the table of contents in Part 1 — Introduction

Today we will implement class for holding values in YAUL language

Requirements

We do not have static typing, so every variable will need to be able to store three different types of values: integers, strings, and arrays. This means that all operations will need to check what type of variable they are working with — adding numbers will be different than adding strings. So let’s begin.

using System;
using System.Linq;

namespace Compiler
{
    public class SimpleObject
    {
        public object Value { get; set; }
        public Type ValueType { get { return Value.GetType(); } }

        public SimpleObject(object value)
        {
            var expression = value as ConstantExpression;
            Value = expression != null ? expression.Value : value;
        }

        public SimpleObject(SimpleObject other)
        {
            if(IsOfType< SimpleObject[]>(other))
            {
                Value = ValueOfType< SimpleObject[]>(other).Select(x => x).ToArray();
            }else if (IsOfType< string>(other))
            {
                Value = ValueOfType< string>(other).Substring(0);
            }
            else
            {
                Value = other.Value;
            }
        }

        public static bool AreOfType< T>(SimpleObject first, SimpleObject second)
        {
            return IsOfType< T>(first) && IsOfType< T>(second);
        }

        public static bool IsOfType< T>(SimpleObject @object)
        {
            return @object.ValueType == typeof (T);
        }

        public static T ValueOfType< T>(SimpleObject @object)
        {
            return ((T) @object.Value);
        }
	}
}

We have one field representing the value. We also have copy constructor — its implementation is in fact overly complicated (because we don’t need to explicitly copy integers or strings), but it shows how we would like to handle different types of value. We also have helper functions for checking types and values. We could simplify them, but let it be for now.

Accessing array

This is the code for accessing array elements:

public SimpleObject this[int key]
{
	get
	{
		if (IsOfType< SimpleObject[]>(this))
		{
			return ValueOfType< SimpleObject[]>(this)[key];
		}
		if (IsOfType< string>(this))
		{
			return new SimpleObject(ValueOfType< string>(this)[key].ToString());
		}

		throw new InvalidOperationException("Object is not an array");
	}
	set
	{
		if (IsOfType< SimpleObject[]>(this))
		{
			ValueOfType< SimpleObject[]>(this)[key] = value;
		}
		else if (IsOfType< string>(this))
		{
			var array = ValueOfType< string>(this).ToCharArray();
			array[key] = ((string) value.Value)[0];
			Value = new string(array);
		}else
		{
			throw new InvalidOperationException("Object is not an array");
		}
	}
}

We explicitly check types when accessing values. For arrays we extract value by element index, for strings we extract specified character. We do not handle more sophisticated cases here, like surruogate pairs.

We also define methods to access elements using SimpleObject as an index variable:

public static SimpleObject GetElement(SimpleObject target, SimpleObject index)
{
	return target[(int)index.Value];
}

public static void SetElement(SimpleObject target, SimpleObject index, SimpleObject value)
{
	target[(int)index.Value] = value;
}

Basic operators

Let’s now implement basic functions:

public static SimpleObject operator +(SimpleObject a, SimpleObject b)
{
	if (AreOfType< string>(a, b))
	{
		return new SimpleObject(ValueOfType< string>(a) + ValueOfType< string>(b));
	}
	
	if (AreOfType< int>(a, b))
	{
		return new SimpleObject(ValueOfType< int>(a) + ValueOfType< int>(b));
	}
	
	if (AreOfType< SimpleObject[]>(a,b))
	{
		return new SimpleObject(ValueOfType< SimpleObject[]>(a).Concat(ValueOfType< SimpleObject[]>(b)).ToArray());
	}

	throw new IncorrectTypesException(a, b);
}

public static SimpleObject operator -(SimpleObject a, SimpleObject b)
{
	if (AreOfType< int>(a, b))
	{
		return new SimpleObject(ValueOfType< int>(a) - ValueOfType< int>(b));
	}

	throw new IncorrectTypesException(a, b);
}

public static SimpleObject operator *(SimpleObject a, SimpleObject b)
{
	if (AreOfType< int>(a, b))
	{
		return new SimpleObject(ValueOfType< int>(a) * ValueOfType< int>(b));
	}
	throw new IncorrectTypesException(a, b);
}

public static SimpleObject operator /(SimpleObject a, SimpleObject b)
{
	if (AreOfType< int>(a, b))
	{
		return new SimpleObject(ValueOfType< int>(a) / ValueOfType< int>(b));
	}

	throw new IncorrectTypesException(a, b);
}

public static SimpleObject operator %(SimpleObject a, SimpleObject b)
{
	if (AreOfType< int>(a, b))
	{
		return new SimpleObject(ValueOfType< int>(a) % ValueOfType< int>(b));
	}

	throw new IncorrectTypesException(a, b);
}

All of them are rather trivial. Addition for numbers works as usual, for strings it joins them, for arrays it catenates them. Other operators works only for numbers.

Comparisons

Comparison operators are pretty trivial:

public static bool operator <(SimpleObject a, SimpleObject b)
{
	if (AreOfType< string>(a, b))
	{
		return ValueOfType< string>(a).CompareTo(ValueOfType< string>(b)) <  0;
	}

	if (AreOfType< int>(a, b))
	{
		return ValueOfType< int>(a) < ValueOfType< int>(b);
	}

	if (AreOfType< SimpleObject[]>(a, b))
	{
		var first = ValueOfType< SimpleObject[]>(a);
		var second = ValueOfType< SimpleObject[]>(b);
		if (first.Length != second.Length)
		{
			return first.Length < second.Length;
		}

		for (int i = 0; i < first.Length; ++i)
		{
			if (first[i] != second[i])
			{
				return first[i] < second[i];
			}
		}

		return false;
	}

	throw new IncorrectTypesException(a, b);
}

public static bool operator >(SimpleObject a, SimpleObject b)
{
	return false == (a < b) && (a != b);
}

public static bool operator <=(SimpleObject a, SimpleObject b)
{
	return a < b || a == b;
}

public static bool operator >=(SimpleObject a, SimpleObject b)
{
	return false == (a < b);
}

public static bool operator ==(SimpleObject a, SimpleObject b)
{
	if (AreOfType< string>(a, b))
	{
		return ValueOfType< string>(a) == ValueOfType< string>(b);
	}

	if (AreOfType< int>(a, b))
	{
		return (int)a.Value == (int)b.Value;
	}

	if (AreOfType< SimpleObject[]>(a, b))
	{
		return ValueOfType< SimpleObject[]>(a).SequenceEqual(ValueOfType< SimpleObject[]>(b));
	}

	return false;
}

public static bool operator !=(SimpleObject a, SimpleObject b)
{
	return false == (a == b);
}

Numbers and strings are compared using .NET functions. For arrays we first compare their length, and then we compare their contents.

True or false

We can also convert objects to boolean values:

public static bool operator true(SimpleObject @object)
{
	return IsTrue(@object);
}

private static bool IsTrue(SimpleObject @object)
{
	if (IsOfType< int>(@object))
	{
		return ValueOfType< int>(@object) != 0;
	}

	if (IsOfType< string>(@object))
	{
		return false == string.IsNullOrEmpty(ValueOfType< string>(@object));
	}

	if (IsOfType< SimpleObject[]>(@object))
	{
		return (ValueOfType< SimpleObject[]>(@object)).Length != 0;
	}

	return true;
}

public static bool operator false(SimpleObject @object)
{
	return false == IsTrue(@object);
}

public static implicit operator bool(SimpleObject @object)
{
	return IsTrue(@object);
}

public static bool operator !(SimpleObject @object)
{
	return false == IsTrue(@object);
}

Number is true only when it is not zero. String is true when it is not null or empty. Array is true when it is not empty.

Equality

For completeness we implement equality operators:

protected bool Equals(SimpleObject other)
{
	return AreOfType< SimpleObject[]>(this, other)
			   ? (ValueOfType< SimpleObject[]>(this).SequenceEqual(ValueOfType< SimpleObject[]>(other)))
			   : Value.Equals(other.Value);
}

public override bool Equals(object obj)
{
	if (ReferenceEquals(null, obj)) return false;
	if (ReferenceEquals(this, obj)) return true;
	if (obj.GetType() != GetType()) return false;
	return Equals((SimpleObject)obj);
}

public override int GetHashCode()
{
	return (Value != null ? Value.GetHashCode() : 0);
}

And we are done. To be sure that we implemented everything correctly we should implement tests.

Summary

Now we are able to perform basic operations on our values. Since every variable will be represented using just this one type, we don’t need to handle different representation cases and implement casting.