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.
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.
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.
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.
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 parametersSince 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.
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.