The S# language

Syntax, structure, style and shape of the S# language.

This is the S# v1.3 manual. You can also download the PDF version here.

 


Comments


Two forms of comments are supported: delimited comments and single-line comments. A delimited comment
is wrapped inside the /* */ characters. Delimited comments can occupy a portion of a line, a single line, or multiple lines.

The following example includes a delimited comment:

/* This is some sample multi-line comment.
   Syntax and code will not be parsed inside this.
*/

A single-line comment begins with characters // and extends to the end of the line.

The following example includes a single-line comment:

a = 0; // initializing variable with 0
b = 1; // initializing variable with 1

Comments don't nest. The character sequences /* and */ have no special meaning within a single-line comment, and the character sequences // and /* have no special meaning within a delimited comment.

Comments are not processed within character and string literals.

 


Data types

You can import your own data types and objects in S# but fFor the sake of simplicity the
most common build-in types are included already:

Type Name .NET Analogue Initializer
double double x = 1.23; x = 12d;
long long x = 3;
string string s=’Hello World!’
bool bool b = true
array object[] A = [1, 1+2, 'Hello', s]

 

There is also an implicit type – object – which is basically the C# System.Object.
The list of base types may be changed through xml configuration. However, it
does not mean that Script.NET can't access other .NET types. In contrast, base
types are cached by alias and may be accessed quicker that other types. Consider
this if you want to improve performance of script execution. The S# runtime infrastructure allows to filter types and whole assemblies available to the client scripts. It is also possible to mark members of a class definition with [Promote(false)] attribute to make them invisible to the default script binder.

Any type visible to the runtime can be accessed either by short name:

b = new StringBuilder();

or by its full name:

b = new System.Text.StringBuilder();

Besides the XML configuration, it is also possible to add any .NET Framework type to the S# programs and use it within a script via the RuntimeHost.AddType() instruction.

Note: If a type was not explicitly added, the S# runtime will search it in libraries loaded in the current application domain. Although it is possible to filter assemblies and types available to the script as well as completely override the assembly/type management through a custom implementation of the IAssemblyManager interface.

 


Type Conversion

Even though S# is dynamically typed language there are cases when a value should be casted to a type explicitly.

The type conversion operator is of the following form:

( Expression1 )Expression2;

  • Expression1 – any valid expression which avaluates a .NET Type
  • Expression2 – any valid expression whos value will be casted

For example:

 (string)23;
 s = string;
 b = (s)23; // b = "23";

Any type conversion during script execution is performed via the runtime Binder. The above shown examples are simply syntactic shortcuts for the following calls:

 RuntimeHost.Binder.ConvertTo(23, string);

 


Constants

 Boolean = True | False
 Object = null

String constants must be in ‘ ‘ or ” ” quotes, example:

s = 'Hello';
s = "Hello World!";

The

S# parser supports following number formats:

//Hex
h = 0xAAFFAA;
 
//Unsigned int
u = 3u;
 
//long
l = 31231231278l;
//unsigned long
ul = 23423234548ul;
 
//Double
d = 3.2312d;
//Single, Float
f = 3424.123f;
//Decimal
m = 23123.25434543m;
 
//With exponent
n1 = 4e+3;
n2 = 6.32e-3;

 


Functions

Function can be created at run-time and added into execution scope by compiling a string containing its definition.

Aside this, there are few other possibilities to introduce functions to script. Function could be native – compiled from source, or external – is .NET object implementing
IInvokable interface.

  • Simple Definition:
function NAME (id1, id2, ... , idn)
{
  Statement
}
  • With references to global variables:
function NAME (id1, id2, ... , idn) global(id1,...,idk)
{
  Statement
}

For example:

y = 100;
 
function f() global(y)
{
y = y - 1;
}
f();

After executing this script the variable y will have its value 99.

  • With contracts:

function (id1, id2, ... , idn)
[
 pre(boolExpr);
 post(boolExpr);
 invariant(boolExpr);
]
{
  Statement
}
  • By function expression (this is usually called anonymous function):
NAME = function (id1, id2, ... , idn) { Statement };

For example:

  helloFunction = function (name) { return string.Format("Hello {0}!", name);};
  helloFunction('John');

All examples of syntactic function definitions are equal to the function expression. In fact when function a() {…} is written it actually executes as a = function () {};. By default after compilation all function expressions will be executed, so corresponding variables will appear in script scope. This is done by FunctionDeclarationVisitor postprocessing available in the library.

Note: The functional expression:

 function (params) { body };

evaluates to an IInvokable object.

During the execution of the function a local scope (Contract Scope) is created and all variables created within this scope will be removed at function return;

Example showing various ways of invoking function:

function fac(n){
  if (n==1) return 1;
  else return n*fac(n-1);
}
rez = fac(5);
 
//pointer to a function
Func_pointer = fac;
Func_pointer(4); //Call function using pointer
 
//Anonymous function
aFunction = function(n){
  if (n==1) return 1;
  else return n*aFunction(n-1);
};
 
aFunction(5);

 


The IInvokable interface

Objects implementing the IInvokable interface will be treated as functions. Suppose there is a variable f in the function scope that implements IInvokable, then following S# code:

 f(1,2,3);

will be interpreted as following in C# code:

  IInvokable fi = f as IInvokable;
   if (fi == null || !fi.CanInvoke()) throw exception;
   return fi.Invoke(Context, new object[] {1,2,3});

Reserved function names

The following names are reserved words in the context of function definitions:

  • eval – evaluates value of an expression;
  • clear – clears all variables in context; (Obsolete)
  • array – creates typed array of objects; (see, Arrays topic)

 


Event Handling

S# has limited capabilities of handling .NET events. However it is possible to subscribe a function defined in Script on .NET event.

Note: It is possible to subscribe on events having the following signature:

public void Event(object sender, T eventArgs) or which is the same EventHandler<t>;.
</t>

Here’s an example for running S# in a simple Windows Forms application:

function OnClick(s,e)
{
  MessageBox.Show('Hello');
}
 
button.Click += OnClick;

You can also
download a sample application demonstrating this from here.

 

It is also possible to wire Routed Events with S# functions when running scripts within WPF or Silverlight applications.

Please note that by default S# runtime will unsubscribe all event handlers after script execution. However there are numerous scenarios when event handlers should be kept alive even the script execution is finished. This behavior can be achieved by configuring
RuntimeHost to preserve event handlers:

RuntimeHost.Initialize();
// Disable automatic event clearing when script execution ended
RuntimeHost.UnsubscribeAllEvents = false;

 


Context Bound Events

All event subscriptions in S# are controlled by a specific component called event manager. It stores in-memory cache of all S# function subscriptions to .NET events. Each time script execute += operator new record will be added to event manager cache.

There are two options for controlling event manager behavior in S#. Those are:

bool RuntimeHost.UnsubscribeAllEvents;
      bool RuntimeHost.ContextEnabledEvents;

If UnsubscribeAllEvents is set to true then after execution of the script, event manager will unsubscribe all S# function subscriptions.

As any S# function, event subscription will be executed in some script context. By default (ContextEnabledEvents = false) event will be executed in the new empty context, which mean that inside event only sender and eventargs parameters will be visible. However there is a possibility to enable execution of event in the same context as a script containing this event function. In this case typical RuntimeHost configuration will look like following:

 RuntimeHost.Initialize();
 RuntimeHost.UnsubscribeAllEvents = false;
 RuntimeHost.ContextEnabledEvents = true;

Here is an example which demonstrate this case (C#):

      RuntimeHost.ContextEnabledEvents = true;
      RuntimeHost.UnsubscribeAllEvents = false;
 
      Script s = Script.Compile(
         @"
            invoked = false;
 
            function handler(s,e) global(invoked) {
             invoked = true;
            }
 
            test = new EventSource();
            test.NameChanged += handler;
 
            return test;
          "
         );
 
      EventSource resultVal =(EventSource)s.Execute();
      Assert.IsFalse((bool)s.Context.GetItem("invoked", false));
 
      //At this point event will be executed in the script's context, i.e. s.Context
      resultVal.Name = "TestName";
 
      //Now, check that the value in context was changed
      Assert.IsTrue((bool)s.Context.GetItem("invoked", false)); 
  public class EventSource
  {
    string name;
 
    public string Name
    {
      get
      {
        return name;
      }
      set
      {
        name = value;
        if (NameChanged != null)
          NameChanged.Invoke(this, EventArgs.Empty);
      }
    }
 
    public event EventHandler<eventargs> NameChanged;
  }</eventargs>

 


The Script context

Script Context is an object stores run-time information: variables and import types. With the Script Context you can add .NET objects to use in the script.

There are a number of methods defined:

  • Get the value of variable with name id within the Context: Lookup(string id)
  • Add a .NET object to the Script context with specified name: AddObject(string name, object value)
  • Add a .NET type to the script context to use it in new expressions: AddType(string name, Type value)
  • Add a build-in object, which means you can invoke its Methods without specifying the name:
    AddBuildInObject? (object object)

For example, in C# code (look the example above) you would add

  AddBuildInObject(typeof(Math))

and in the script you would be able to access the functions of the Math object straight away:

 a = Pow(2,3); //Will call Math.Pow(2,3);

You can remove the build-in object via the RemoveBuildInObject method.

The context also allows you to get the static field values of some useful elements like NaN:

x = Context.GetStaticValue( 'double.NaN' );

 


Statements

A program in S# script is a sequence of statements. There are three common statement types supported: sequencing, looping and branching.

if … then … else

if (Expression) Statement else Statement

For example:

if (x&gt;0) y = y + 1 ; else y = y – 1;
if (x&gt;0) message = 'X is positive';

for …

for (Expression1;Expression2;Expression3) Statement

For example:

sum=0;
for(i=0; i<t ;10; i++) sum = sum + a[i];

while …

while (Expression) Statement

For example:

while (i&gt;0) i = i-1;

foreach … in …

foreach (Identifier in Expression) Statement

Note: The result of Expression calculation must implement IEnumerable. Expression evaluates only once, before loop starts.

For example:

arr=![1,2,3,4,5]; sum = 0;
foreach(i in arr ) sum = sum + i;

switch

switch (Expression)
{
   case expr1: Statement
   ...
   default: Statement
}

For example:

switch (i)
{
   case 1: MessageBox.Show('Hello!');
   case 2: MessageBox.Show('?');
   default: MessageBox.Show('No way');
}

using

using ( object or type )
{
    . . .
}

Example 1:

using (Math)
{
  return Pow(2,10);
}

Example 2:

a = new List< |int|>();
using(a)
{
  Add(10);
  Add(20);
}
return a[0];

break, continue

This has usual meaning and can be used only inside a loop.

return

Used only inside function calls.

 


Expressions

The syntax of expressions is very standard, examples are:

X = (y+4)*2;
Y = a[5] + 8;
Z = Math.Sqrt(256);
P = new System.Drawing.Point(3,4);
'this is string' is string

Expressions can be concatenated using the following operators:

+, -, *, / ,%, ! , | , &  , != , < , > , is

There is also special operator new for creating instances of imported types.

 


Arrays

Build-in arrays has a .NET type of object[]. The elements of array may be accessed in a usual way: Array[index].

Example:

 a = [1,2,3,4];
 
b = a[1];

This is equal to following code in C#:

 object[] a = new object[] {1,2,3,4};
 
 object b = a[1];

There is also a custom function array which creates a typed array. There are two cases for usage array function:

  • Explicit type definition:
  •  a = array(string, 'alex', 'peter');
  • Implicit type inference:
  •  a = array('alex', 'peter'); // a is of type string[]
     b = array('alex', 1, 2); // b is of type object[]

 


Generic parameters

You can use generics in S# through the syntax

 MethodName< |TypeName| > ;(...)

For example it you have an Attach< T > method you can call it with the string type as follows:

 Attach< |string| > (...)

 


Variables

All variables in Script .NET are global and non-typed. They are stored in the Script Context.
The Script Context is a special structure which is used by Script Engine to store run-time information and interact with .NET.

However, function body creates local Contexts and store declared variables there. Functions have access to global variables.

X, x1, name_of_var.

Variables may be used in expressions, statements. The type information is computed
at run-time.

 


The scope

The purpose of the script Scope is to:

  • resolve names of variables: associate value with name, return value by given name;
  • Associate name with IInvokable object (function)

Scopes in S# form a hierarchy:

 

There are different types of scopes: global, local, function scope, using statement scope, and events scope. All of them are used to resolve names: either variable’s, type’s, function’s name or method’s and properties’ names in of certain object in case of using statement. It is possible to inherit and implement a Scope class to introduce custom behavior of name resolution.

Before script execution the user can add objects,types and functions into Script’s scope, so they will be available for script.

For example:

    List < int > vals = new List < int >;();
    vals.AddRange(new int[] { 1, 2, 3, 4 });
 
    Script script = Script.Compile(@"
            rez = 0;
            foreach (number in numbers)
            rez += number;"
           ); 
 
    //Adding variable to script's scope
    script.Context.SetItem("numbers", vals); 
 
    object rez = script.Execute();
    Console.WriteLine(rez);

 


Global and Local Variables

Global variables are variables that are accessible in every scope. Such variables are used extensively to pass information between sections of code that don’t share a caller/callee relation like functions.

Example:

// Creates a global variable "g_variable1" with value of 100
g_variable1 = 100;
 
// Creates a global variable "g_variable2" with value of 200
g_variable2 = g_variable1 + 100;
 
function MyFunction1() { ... }
 
function MyFunction2() { ... }

Local variables are variables that are given local scope. Such variables are are accessible only from the function or block in which it is declared. Local variables are contrasted with global variables.

Example:

// Creates a global variable
my_variable = 100;
 
function MyFunction()
{
  // Creates a local variable
  my_variable = 200;
}

To avoid naming conflicts when referencing local and global variables S# provides a special global: keyword that provides access to global scope

Example:

// Creates a global variable
g_variable = 100;
 
function MyFunction()
{
  // Creates a local variable
  g_variable = 200;
 
  Console.WriteLine("Local variable value: " + g_variable);
  Console.WriteLine("Global variable value: " + global:g_variable);
}
 
// Invokes "MyFunction" function
MyFunction();

When running the example above you will get the following output as a result:

Local variable value: 200
Global variable value: 100

 


Merging Global and Local Scopes

S# also provides a possibility importing a set of global variables into the local scope of a function. This is achieved by means of global() statement used within function definition.

Example:

myVariable1 = 10;
myVariable2 = "hello";
 
function MyFunction() global (myVariable1, myVariable2)
{
  myVariable1++;
  myVariable2+= " world!";
 
  Console.WriteLine(myVariable1);
  Console.WriteLine(myVariable2);
}

The example above will provide the following output when executed:

11
hello world!

Please note that after variables from global scope are merged into the local one for “MyFunction” function it is not possible to declare local variables with the same names. It is recommended to use “global()” statement for functions that are intended to manipulate static (global) variables primarily.

 


Scopes by example


Creating global variable

  a = 1;

Creating global variable in local scope

           //Global scope
 
           { //Local scope 1
 
             {//Local scope 2
 
               //Create global variable from local scope
               a = 4;
 
             }
           }
 
           // In this scope a = 4
           return a;

Temporary local variables

           //Global scope
 
           { //Local scope 1
             var a; //Create empty variable in local scope 1
 
             {//Local scope 2
 
               //This will set variable to top-most scope which contains
               //definition for variable, which is Local scope 1
               a = 4;
             }
           }
 
           //Global scope still empty

Variable resolving rules

           //Global scope
 
           { //Local scope 1
             var a; //Create empty variable in local scope 1
 
             {//Local scope 2
 
               //This will set variable to top-most scope which contains
               //definition for variable, which is Local scope 1
               a = 5;
 
               { //Local scope 3
 
                 var a; //Create empty variable in local scope 3
 
                 //This will set variable to top-most scope which contains
                 //definition for variable, which is Local scope 1
                 global:a = 4;
 
                 a = 3; // Set local variable
               }
             }
 
             //Create variable in global scope equal to current value of a in this scope
             b = a;
           }
 
           //b = 4;
           return b;

Temporary variables in for loop

 var sum = 0;
for (var x=0; x < 10; x++)
{
 var temp = x;
 sum += x;
}
//Here temp and x variables are absent
return sum;