Skip to content

Extending the API

Adding custom builtin functions

AddyScript's inner functions are all instances of the AddyScript.Runtime.InnerFunction class. This class has a static property called Globals that contains all of its predefined instances. To extend the list of the scripting engine's built-in functions, simply create new instances of InnerFunction and add them to the InnerFunction.Globals collection. The InnerFunction constructor takes as arguments a string representing the name of the function, an array of AddyScript.Runtime.Parameter objects representing the list of parameters that the function expects, and an instance of AddyScript.Runtime.InnerFunctionLogic representing the body of the function. InnerFunctionLogic is a delegate type that takes an array of AddyScript.Runtime.DataItems.DataItem objects as a parameter and returns an object of the same DataItem type as a result. Any method in a .NET class that has this prototype can be used as the body of an inner function. Here is a C# code example that demonstrates how to add a "clrscr" function to AddyScript to clear the screen:

C#
using AddyScript.Runtime;
using AddyScript.Runtime.DataItems;

public static class MyExtensions
{
    public static void RegisterFunctions()
    {
        // Here we create the clrscr InnerFunction.
        // It takes no parameter and uses ClearScreenLogic as its body
        var ClearScreen = new InnerFunction("clrscr", [], ClearScreenLogic);

        // And here we add it to the InnerFunction.Globals collection
        InnerFunction.Globals.Add(ClearScreen);
    }

    // Here we define the logic of clrscr
    private static DataItem ClearScreenLogic(DataItem[] arguments)
    {
        System.Console.Clear();
        return Void.Value; // Here we are returning 'null'
    }
}

Note: Just make sure to call MyExtensions.RegisterFunctions(); somewhere in your code before launching the interpreter.

Adding custom builtin classes

Defining a new built-in class in AddyScript can have two meanings: it can mean adding a primitive type to the scripting engine. It can also mean adding a new object type to the scripting engine. Defining a new primitive type requires much more effort than creating a new object class. In all cases, you will need to create an instance of the AddyScript.Runtime.OOP.Class metaclass and add it to the AddyScript.Runtime.Class.OOP.Class.Predefined collection.

Object classes

For a new object class, you will make it reference AddyScript.Runtime.OOP.Class.Object directly or indirectly as its base class. The metaclass has a constructor that allows you to specify the parent class. Afterward, you will only need to provide member definitions to the new class. All members can be defined manually. For methods, this means creating their AST from scratch. But there is a shortcut that consists in creating an InnerFunction which will not be added to the InnerFunction.Globals collection but will instead be converted to AddyScript.Runtime.OOP.ClassMethod using one of the ToInstanceMethod or ToStaticMethod methods of the InnerFunction class.

Example:

This is how we could define the Exception class if it didn't exist in AddyScript

C#
using System.Collections.Generic;

using AddyScript.Ast.Expressions;
using AddyScript.Ast.Statements;
using AddyScript.Runtime.DataItems;
using AddyScript.Runtime.OOP;

public static class MyExtensions
{
    public static void RegisterClasses()
    {
        // Exception is defined as a subclass of Class.Object with the name "Exception" and the given members
        var Exception = new Class(Class.Object, "Exception", Modifier.Default, GetExceptionConstructor(),
                                  GetExceptionIndexer(), GetExceptionFields(), GetExceptionProperties(),
                                  GetExceptionMethods(), GetExceptionEvents());

        // Here we add Exception to the collection of predefined classes
        Class.Predefined.Add(Exception);
    }

    // Below, the definition of all the members

    /**
    * The constructor is defined as:
    * public constructor (name, message = null)
    * {
    *     if (message === null)
    *         this.message = name;
    *     else
    *     {
    *         this.name = name!;
    *         this.message = message;
    *     }
    * }
    */
    private static ClassMethod GetExceptionConstructor()
    {
        var ctorFunc = new Function([new Parameter("name"), new Parameter("message", DataItems.Void.Value)],
                                    new Block(new IfElse(new BinaryExpression(BinaryOperator.Identical, new VariableRef("message"), new Literal()),
                                                         new Assignment(PropertyRef.OfSelf("message"), new VariableRef("name")),
                                                         new Block(new Assignment(PropertyRef.OfSelf("name"),
                                                                                  new UnaryExpression(UnaryOperator.NotEmpty, new VariableRef("name"))),
                                                                   new Assignment(PropertyRef.OfSelf("message"), new VariableRef("message")))),
                                              new Return()));

        return new ClassMethod("Exception", Scope.Public, Modifier.Default, ctorFunc);
    }

    // No indexer
    private static ClassProperty GetExceptionIndexer()
    {
        return null;
    }

    /**
    * No explicitly declared fields
    */
    private static IEnumerable<ClassField> GetExceptionFields()
    {
        return null;
    }

    /**
    * Two auto properties:
    * public property name { read; private write; }
    * public property message { read; private write; }
    */
    private static IEnumerable<ClassProperty> GetExceptionProperties()
    {
        return [
            new ClassProperty("name", Scope.Public, Modifier.Default, PropertyAccess.ReadWrite, Scope.Public, null, Scope.Private, null),
            new ClassProperty("message", Scope.Public, Modifier.Default, PropertyAccess.ReadWrite, Scope.Public, null, Scope.Private, null)
        ];
    }

    /**
    * A single "toString" method that overrides the inherited method from object:
    * public function toString(format = "") => this.name;
    */
    private static IEnumerable<ClassMethod> GetExceptionMethods()
    {
        var toStringFunc = new Function([new Parameter("format", new String(""))],
                                        Block.WithReturn(PropertyRef.OfSelf("name")));

        return [new ClassMethod("toString", Scope.Public, Modifier.Default, toStringFunc)];
    }

    // No event
    private static IEnumerable<ClassMethod> GetExceptionEvents()
    {
        return null;
    }
}

Primitive types

Creating a new primitive type goes through these same steps. But before that, you must add a new member to the AddyScript.Runtime.OOP.ClassID enumeration to represent the new type. Afterward, you will have to create a new instance of the metaclass as described above. This new class will not have a reference to a parent class, but it will have the newly defined ClassID (there is a suitable constructor in Class). After that you will need to create a new child class of AddyScript.Runtime.DataItems.DataItem to represent data of the type being defined. The Class property of this DataItem type should return the reference to the previously created Class instance. DataItem provides a whole range of virtual methods that define the behavior of an object in arithmetic operations, conversions, and property accesses. Overriding one of these methods allows you to customize the behavior of the new data type. You will probably also want to take a look at the AddyScript.Runtime.DataItems.DataItemFactory and AddyScript.Runtime.DataItems.DataItemBinder classes to add support for your data type in Marshaling operations. AddyScript.Runtime.DataItems.DataItemFactory has a CreateDataItem method that converts a .NET System.Object into a DataItem, you will certainly want to add support for your data type. AddyScript.Runtime.DataItems.DataItemBinder on its side has a Mismatch method that evaluates the degree of compatibility between .NET data types and AddyScript data types. You will also need to add support for your data type.

The last step in the process of creating a primitive type is to decide how you want to create data of this type. You may want it to have literal values or initializers or simply a static factory method. If the choice of the factory method is made, then the work is done: the factory method is probably already part of the class definition. On the other hand, for a literal value or an initializer, it will be necessary to update the analyzers so that they recognize a new category of symbols. It will also be necessary to modify the translators so that they know how to translate this new type of symbol.

Have a look at how existing primitive types are defined to better understand the whole process.