StaDyn

An efficient hybrid static and dynamic typing programming language

The syntax of StaDyn is based on C# 3.0. It can be specified the type of references explicitly or use the var keyword as implicitly typed references. However, varreferences of StaDyn are more powerful than implicitly typed local variables in C# 3.0, since var is a new type in StaDyn. It can be used for parameter types, return values, class fields and initialized local variables.

StaDyn also supports dynamic references, later added to C# 4.0. Its behavior is similar to var, but with a more lenient approach to follow the flavor of dynamic languages. Later, we detail the exact difference between var and dynamic.

Variables can have multiple types in the same scope

Static typing commonly forces a variable of type T to have the same type within the scope in which it is defined. However, this is not the case in dynamic languages, where variables can hold different types in the same scope. StaDyn allows variables to hold different types in the same scope, but static type checking is still performed, holding the concrete types of each var and dynamic reference.

        using System;
        class Test {
            public static void Main() {
                Console.Write("Your age, please: ");
                var age = Console.In.ReadLine();
                Console.WriteLine("You are " + age + " years old.");

                age = Convert.ToInt32(age);
                Console.WriteLine(age.GetType());
                age++;
                Console.WriteLine("Happy birthday, you are " + age + " years old now.");
                int length = age.Length; // * Compiler error
		}
	}
                

The age variable is first inferred as string. Then it changes to int. In each usage of age, the particular concrete type is used to type-check the program. In this way, the compiler shows an error for the last line of code, since integers do not provide the Length property.

The generated code does not use a single Object variable to represent age, but two different variables whose types are string and int. This makes the generated code to be more efficient, since runtime type conversions are not required.

Statically Typed Duck Typing

The static type system of StaDyn is flow-sensitive. This means that it takes into account the flow context of each var and dynamic reference. It gathers concrete type information (opposite to classic abstract type systems): StaDyn knows all the possible types of a var / dynamic reference may have. Following this scheme, the language offers statically typed duck typing. If a var reference points to a set of objects and all of them implement the m message (i.e., method, property or field), it can be safely invoked. The objects do not need to implement a common interface or class declaring the implementation of m. The following code is an example of this feature:

        var exception;
        if (new Random().NextDouble()<0.5)
            exception = new ApplicationException("An application exception.");
        else
            exception = new SystemException("A system exception");
        Console.WriteLine(exception.Message);
                

In the code above, exception may point to anApplicationException or to a SystemException object. Both objects have the Message property and, therefore, it is safe to use this property. It is not necessary to define a common interface or class to pass this message. Since type inference system is flow-sensitive and uses concrete types, the programmer obtains a type-safe duck-typing system.

If dynamic is used to declare exception, type checking is a little bit different. In such a case, an m message can be passed if either ApplicationException or SystemException provides an implementation for m (a compiler error is shown if neither provides m). This behavior is more lenient and closer to dynamic language, but still provides static type checking.

Separation of the Dynamism Concern

Let's add another alternative in the assignment of the exception reference:

       var exception;
        Random random = new Random();
       switch (random.Next(1,4)) {
       case 1: 
            exception = new ApplicationException("An application exception.");
            break;
       case 2:
            exception = new SystemException("A system exception");
            break;
       case 3:
            exception = "This is not an exception";
            break;
        }
        Console.WriteLine(exception.ToString());
        Console.WriteLine(exception.Message); // * Compiler error?
                

The ToString message is correct because it is offered by the three possible objects created. However, the Message property is not provided by string. By default, the compiler uses the everythingStatic option, and the following error message is shown:

        C:\...> sample3.cs(21.37): Error ErrorManagement.UnknownMemberError (Semantic error). 
                'Message': no suitable member found.
                

However, we can be more optimistic and set all the var references in the program as dynamic. In this case, the compiler accepts a message if there is one possibility that the program can behave without runtime type errors. The compiler accepts the previous program if we compile it with the everythingDynamic option:

        C:\...> stadyn /ed sample3.cs 
                

We do not need to set all the var references in the program as dynamic. First, we can declare the exception reference as dynamic. It is also possible to specify the dynamism of a var reference with an XML file. The following is the sample3.dyn file that sets the exception reference as dynamic:

        <?xml version="1.0" encoding="utf-8"?>
        <application name="sample3">
          <namespace name="GettingStarted">
            <class name="Test">
                  <method name="Main">
                    <dynvar name="exception" />
                  </method>
                </class>
            </namespace>
        </application>
                

The existing StaDyn plugin for Visual Studio, among other functionalities, handles the XML files used to modify the dynamism of var references.

It is worth noticing that, even though the everythingDynamic option makes the compiler to be more lenient, static typing is still performed. This is shown in the following example:

            var reference;
            if (new Random().NextDouble() < 0.5)
                reference = "String";
            else
                reference = 3;
            Console.WriteLine(reference.Message);
                

The error message obtained when compiled in dynamic mode is the following one:

        C:\...> stadyn /ed sample3.cs
        C:\...> sample4.cs(13.41): Error ErrorManagement.NoTypeHasMember (Semantic error). 
                The dynamic type '\/([Var(6)={6}=string] ,[Var(5)={5}=int])' has no valid type type with 'Message' member.
                

Parametric Polymorphism by means of Type Reconstruction

Concrete type reconstruction is not limited to local variables. StaDyn performs a global flow-sensitive analysis of var and dynamic references. The result is a powerful parametric polymorphism (generics) more straightforward for programmers than the one offered by Java, C# and C++. For example, we can create the following generic Wrapper class:

    class Wrapper {
        private var attribute;

        public Wrapper(var attribute) {
            this.attribute = attribute;
        }

        public var get() {
            return attribute;
        }

        public void set(var attribute) {
            this.attribute = attribute;
        }
    }

    class Test {
        public static void Main() {
            string aString;
            int aInt;
            Wrapper wrapper = new Wrapper("Hello");
            aString = wrapper.get();
            aInt = wrapper.get(); // * Compiler error

            wrapper.set(3);
            aString = wrapper.get(); // * Compiler error
            aInt = wrapper.get();
        }
    }
                

The Wrapper class can wrap any type. Each time we call the set method, the new concrete type of the parameter is saved in the type of the implicit object (i.e., the object used to call set). By using this mechanism, the two lines indicated in the code above report compilation errors. Notice that this coding style is type safe and it is easier to learn than the parametric polymorphism used in C++ and the bounded polymorphism offered by Java and C#.

The runtime performance of the code above is the same as if we had specified types explicitly. Although the source code makes use of var references, concrete types are known at compile time. The generated code uses that static type information to undertake different optimizations and provide better runtime performance than existing dynamic languages.

Var and dynamic parameters

Since the StaDyn compiler type-checks both dynamically and statically typed code, it is possible to combine both approaches in the very same application. Let's analyze the following code:

        public static var upper(var parameter) {
            return parameter.ToUpper();
        }
        public static var getString(var parameter) {
            return parameter.ToString();
        }
                

Both methods require the parameter to implement a method, returning the result of the method call. In the second case, the method will be valid for any argument, since ToString is provided by every object. In the upper method, the parameter should be any object able to respond to the ToUpper message.

To support var and dynamic parameters, StaDyn implements a constraint-based type system. The next code is correct:

           string aString = "hello";
            aString=upper("hello");
            Console.WriteLine(aString);
                

In this case, the returned type is inferred to string. What happens if the parameter has a flow-sensitive type? Here we have an example:

          var reference;
          if (new Random().NextDouble() < 0.5)
              reference = "hello";
          else
              reference = new SystemException("A system exception");
          aString=getString(reference); // * Correct!
          Console.WriteLine(aString);
          aString=upper(reference); // * Compiler error (reference is static)
                

reference may point to a string or to an exception. Therefore, it is type safe to invoke the getString method, but we might get an erroneous dynamic behavior if we call the upper method. This is the reason why, if we set reference as static we get a compiler error. Otherwise, if reference is set as dynamic (or declared as dynamic), the same source code will be compiled without errors. This more lenient approach may produce type errors at runtime.

Alias Analysis for Concrete Type Evolution

The problem of determining if a storage location may be accessed in more than one way is called alias analysis. Two references are aliased if they point to the same object. Alias analysis is mainly used for compiler optimizations. We use it to know the concrete types a reference may be pointing to. The following example uses the Wrapper class previously shown:

    class Test {
        private var testField;

        public void setField(var param) {
            this.testField = param;
        }

        public var getField() {
            return this.testField;
        }

        public static void Main() {
            var wrapper = new Wrapper("hi");
            var test = new Test();
            test.setField(wrapper);
            string s = test.getField().get(); // * Correct!

            wrapper.set(true);
            bool b = test.getField().get();   // * Correct!
            string s = test.getField().get(); // * Compiler Error
        }
    }
                

Initially, the wrapper reference points to a string object. Then we create a Test object that references the original Wrapper object. If we get the object inside the wrapper object inside the test object, we get a string object. Then we set a bool attribute to the wrapper object. Repeating the previous access to the object inside the wrapper object inside the test object, we get a bool object!

The alias analysis implemented is type-based (uses type information to compute alias), inter-procedural (makes use of inter-procedural flow information), context-sensitive (differentiates between different calls to the same method), and maybe alias (detects all the objects a reference may point to; opposite to must point to).

If you want to start playing with StaDyn, it is time to download it from GitHub and start coding.