FixScript Classes Documentation

This token processor allows you to use classes and other syntax additions.

Features:

Table of contents

Table of contents:

Types

The built-in types are:

You can also use array, hash and class types. Array types use brackets after the type, for example Float[] describes an array of floats. Hash types contains the type of the key in the brackets, for example Integer[String] describes a hash map with a String key and an Integer value.

The parameters in functions, methods and constructors can use a multiple type. This is a list of allowed types for given parameter. The parameter is treated as having Dynamic type in the body of the function, you have to decide which type was passed at runtime (for example by using the intrinsic functions such as is_int, is_string, etc.). The types are delimited by or keyword, an example: String or Integer or Float. No implicit integer to float conversions are done.

Return type

The classes implementation allows to use code written in base FixScript syntax without any changes. When adding usage of types to such code there is an ambiguity in specifying the return type: omitting the return type can mean that it either returns a dynamic type or it has no return type. While you can specify it using the Void type explicitly it is ugly syntax-wise.

There is a simple logic to aid this:

  1. When the function is inside a class it assumes no return type if not specified explicitly.
  2. For standalone functions without parameters with specified types or explicit return type it scans the first usage of return statement that returns either a single or no value. When it finds that it returns a single value it assumes a dynamic return type otherwise no return type.
  3. When specifying required native functions (having no body) it assumes no return type if not specified explicitly as the most common usage is for specifying the types and is typically not migrated from base FixScript code.

Built-in functions

The built-in types for arrays, strings and hash tables can use the built-in functions (as well as the length property) in OOP way:

var shared = Array::create_shared(100, 4);
shared.fill(-1);
log(shared.length);

var hash = {};
log("value="+hash.get("key", "(not found)"));

Extension methods for built-in types

You can also define additional methods for the built-in types (arrays, strings and hash tables). An example:

static function Array::create_filled(len: Integer, value: Value): Value[]
{
	var arr: Value[] = Array::create(len);
	arr.fill(value);
	return arr;
}

function Array::get(idx: Integer, default_value: Value): Value
{
	var value = this[idx];
	if (value == null) {
		return default_value;
	}
	return value;
}

The names for the built-in types are Array, String and Hash.

The methods can contain special generic types Key (for hash tables) and Value (for arrays and hash tables). These allow to precisely define the types. In the static methods the types for the generic types are obtained from the actual used types at the first parameter where it is used. It is then enforced to be the same in the following parameters and in the return type.

In the instance methods the this variable must be referenced explicitly to be able to call other instance methods. Only methods defined in the current script and in the imported scripts are possible to use.

List of methods for built-in types

static function Array::create(len: Integer): Dynamic[];
static function Array::create(len: Integer, element_size: Integer): Dynamic[];
static function Array::create_shared(len: Integer, element_size: Integer): Dynamic[];
static function Array::copy(dest: Value[], dest_off: Integer, src: Value[], src_off: Integer, len: Integer);

function Array::get_shared_count(): Integer;
function Array::get_element_size(): Integer;
function Array::set_length(len: Integer);
function Array::fill(value: Value);
function Array::fill(off: Integer, len: Integer, value: Value);
function Array::extract(off: Integer, len: Integer): Value[];
function Array::insert(idx: Integer, value: Value);
function Array::insert_array(idx: Integer, src: Value[]);
function Array::insert_array(idx: Integer, src: Value[], off: Integer, len: Integer);
function Array::append(src: Value[]);
function Array::append(src: Value[], off: Integer, len: Integer);
function Array::replace_range(start: Integer, end: Integer, src: Value[]);
function Array::replace_range(start: Integer, end: Integer, src: Value[], off: Integer, len: Integer);
function Array::remove(idx: Integer);
function Array::remove(off: Integer, len: Integer);
function Array::clear();

static function String::parse_int(s: String): Integer;
static function String::parse_int(s: String, default_value: Integer): Integer;
static function String::parse_int(s: String, off: Integer, len: Integer): Integer;
static function String::parse_int(s: String, off: Integer, len: Integer, default_value: Integer): Integer;
static function String::parse_float(s: String): Float;
static function String::parse_float(s: String, default_value: Float): Float;
static function String::parse_float(s: String, off: Integer, len: Integer): Float;
static function String::parse_float(s: String, off: Integer, len: Integer, default_value: Float): Float;
static function String::from_utf8(arr: Byte[]): String;
static function String::from_utf8(arr: Byte[], off: Integer, len: Integer): String;
static function String::from_utf8(dest: String, arr: Byte[]): String;
static function String::from_utf8(dest: String, arr: Byte[], off: Integer, len: Integer): String;
static function String::to_utf8(dest: Byte[], s: String): Byte[];
static function String::to_utf8(dest: Byte[], s: String, off: Integer, len: Integer): Byte[];

function String::get_element_size(): Integer;
function String::set_length(len: Integer);
function String::fill(char: Integer);
function String::fill(off: Integer, len: Integer, char: Integer);
function String::extract(off: Integer, len: Integer): String;
function String::insert(idx: Integer, src: String);
function String::insert(idx: Integer, src: String, off: Integer, len: Integer);
function String::insert_char(idx: Integer, char: Integer);
function String::append(src: String);
function String::append(src: String, off: Integer, len: Integer);
function String::replace_range(start: Integer, end: Integer, src: String);
function String::replace_range(start: Integer, end: Integer, src: String, off: Integer, len: Integer);
function String::remove(idx: Integer);
function String::remove(off: Integer, len: Integer);
function String::clear();
function String::as_const(): String;
function String::as_const(off: Integer, len: Integer): String;
function String::to_utf8(): Byte[];
function String::to_utf8(off: Integer, len: Integer): Byte[];

function Hash::get(key: Key, default_value: Value): Value;
function Hash::entry(idx: Integer): Key, Value;
function Hash::contains(key: Key): Boolean;
function Hash::remove(key: Key): Value;
function Hash::get_keys(): Key[];
function Hash::get_values(): Value[];
function Hash::get_pairs(): Dynamic[];
function Hash::clear();

Constructors

The syntax for constructors is just a syntax sugar for static methods that create the object and return it. In case you need more flexibility when creating an object (eg. using a different underlying type) you can use static methods directly.

You can also create a new instance using the "new ClassName" expression. This is used rarely because it doesn't call any constructors, just creates an empty object. Typically it is used in functions that behave like constructors (eg. when cloning) or for performance reasons. To limit misuse it can be used in the same script file only.

There is also a variant that extends the existing instance of a parent class. The form is "new ClassName: <expression>". The normal form is just calling the built-in object_create function whereas the extending form is calling object_extend under the hood.

Base object type

Classes can either extend an existing class type or optionally extend a special common class type named Object. This class allows to provide automatic implementation of to_string methods. You can also provide your own implementations of this method for custom string representations.

Some class types can't extend from Object, for example when they need serialization or are backed by other type than an array. In such cases you can also register a to_string method, albeit in a less efficient way. This is achieved by using the Object::set_to_string(obj, func) static method. It simply registers the provided function to a global (self-clearing) hash map with a weak reference key.

Another alternative is to simply put a function reference as a first entry in an array, the function must take one argument and must end with _to_string suffix.

You can then either call the to_string methods directly, or use dump or to_string (with newlines), these are automatically replaced with versions that know about class types. You can of course use the original functions by using @dump and @to_string functions.

This functionality is completely optional. You have to put object.fix file in the root of the scripts and extend the class from the Object type.

Interfaces

Interfaces provide common type for otherwise different classes. This is achieved by creating a wrapper class that contains reference to the original class and a set of function references specific to that class so it can be called in a common way.

The original class contains some method, called as_interface (when the interface class is called Interface) that returns a new instance of this wrapper class. It can also cache it when desirable (using weak references if the same existing interface instance should be always used).

Here is an example:

class Class1
{
    var @field1;
    var @field2;
    var @field3;

    function as_some_interface(): SomeInterface
    {
        return SomeInterface::create(this, Class1::common_method#1);
    }

    function common_method()
    {
        log("Class1");
    }
}

class Class2
{
    function as_some_interface(): SomeInterface
    {
        return SomeInterface::create(this, Class2::common_method#1);
    }

    function common_method()
    {
        log("Class2");
    }
}

class SomeInterface
{
    var @data;
    var @common_method_func;

    constructor create(data, common_method_func)
    {
        this.data = data;
        this.common_method_func = common_method_func;
    }

    function common_method()
    {
        common_method_func(data);
    }
}

Operator overloading

Classes can provide implementations of operators. This makes the code much more readable for classes that represent numbers or something close enough. For example strings can be concatenated by adding. However a care must be taken to not misuse this feature.

The operators are declared as special static methods with two parameters. At least one of the parameters must be the class itself. The implied return type is the class or a Boolean in case of comparison operators.

For the in-place modification operators (+=, -=, etc.) the methods have just one parameter and are instance methods. The implied return type is the class itself. The implementation must return this.

The special universal comparison operator <=> is used when a concrete comparison operator is not defined. The return value is an Integer (the result must be zero when the values are equal, less than zero if the left value is lesser than the right value or greater than zero if the left value is greater than the right value).

Here is an example:

class IntRef
{
	var value: Integer;

	constructor create(value: Integer)
	{
		this.value = value;
	}

	operator + (op1: IntRef, op2: IntRef)
	{
		return create(op1.value + op2.value);
	}

	operator + (op1: IntRef, op2: Integer)
	{
		return create(op1.value + op2);
	}

	operator + (op1: Integer, op2: IntRef)
	{
		return create(op1 + op2.value);
	}

	// same for other operators and combinations
}

Structures

Structure is a special kind of a class that allow to store multiple packed instances in an array. This has an advantage for bulk data processing and multithreading (when used with shared arrays). Usage of shared arrays also avoids the GC overhead.

Internally the reference to a structure is just an offset to the backing array. This offset is pointing just after the last field (end of the structure) to allow usage of zero offset as a null value. This also results into an error when the null is accessed because it will be out of the bounds of the backing array.

The structure reference can be cast to Integer to get the offset at the beginning of the struct (internally the size will be subtracted). Doing the opposite (casting from Integer) will internally add the size. Casting between different sized struct references adjusts the offset so it points to the end of the new struct type. Comparing the structs uses the subtracted offset (the beginning of the structure). To get or set the raw value cast to Dynamic instead of Integer.

You can also create a structure reference from an index with code like SomeStruct(5) to get the 6th element in a packed structure array. To get the index back use some_struct.index.

The structures are declared in the same way as classes with the difference that the struct keyword is used instead of a class.

Accessing of fields requires a reference to the backing array and uses this syntax: array[some_struct].field.

You can have methods in structs, in the case of instance methods the this variable refers to the offset only so in most cases the instance methods are required to have the first parameter be an array or other object that contain a reference to such array. There is a direct syntax for calling the methods: array[some_struct].method() (the array is passed as the first parameter internally).

The syntax for backing array type is [Struct] (or [Dynamic] in case it contains different kinds of structs). You can also use Dynamic[] or Dynamic to also allow arbitrary direct array access.

The struct arrays have three properties: length to obtain the number of contained structs, first to get the first struct reference (may be out of bounds if the array is empty), last to get the last struct reference (may be also out of bounds).

You can do pointer arithmetic on structs. Any addition or subtraction is multiplied by the size of the structure to allow processing of the adjacent entries.

You can use foreach on struct arrays. In such case the value is the structure pointer.

When using structs backed by a shared array there is a behavior (of shared arrays) where floats are stored with their raw value. This is not a problem on assign, but normally when retrieving you will get just the raw value as an integer instead of a float. The implementation of structs automatically handles it, the float is restored to the normal float type when obtained from the struct (by multiplying it with 1.0).

You can also set or append the whole struct at once. This has the advantage of setting the unspecified fields with a zero value.

Here is an example:

struct Base
{
	var base_field: Integer;
}

struct Test: Base
{
	var @parent: Test;
	var @field1: Integer;
	var @field2: Integer;

	function update(owner: TestOwner, field1: Integer, field2: Integer)
	{
		var array = owner.array;
		array[this].field1 = field1;
		array[this].field2 = field2;
	}

	function replace(owner: TestOwner, field1: Integer, field2: Integer)
	{
		var array = owner.array;
		array[this] = {
			.field1: field1,
			.field2: field2
		};

		// you can also use explicit struct type like this:
		var dynarr = owner.array as Dynamic;
		dynarr[this] = Test {
			.field1: field1,
			.field2: field2
		};
	}

	function to_string(owner: TestOwner)
	{
		var array = owner.array;
		return "Test(parent=#"+array[this].parent+")";
	}
}

class TestOwner
{
	var @common: Integer;
	var @array: [Test];

	function create_test(parent: Test): Test
	{
		var test = length(array as Dynamic[]) as Test;
		array[] = {
			.base_field: 123,
			.parent: parent
		};
		return test;
	}

	function process_all()
	{
		for (var i=0 as Test; i<length(array as Dynamic[]); i++) {
			array[i].base_field = 100;
			this[i].update(1, 2);
		}

		// better:
		for (var i=array.first; i<=array.last; i++) {
			array[i].base_field = 100;
			this[i].update(1, 2);
		}

		// or just:
		foreach (var i in array) {
			array[i].base_field = 100;
			this[i].update(1, 2);
		}

		// custom range:
		for (var i=Test(2); i<Test(5); i++) {
			array[i].base_field = 100;
			this[i].update(1, 2);
		}
	}
}

The this variable

The this variable used in instance functions and constructors is a normal variable. It can be assigned, or it can even contain a null value (in fact any kind of value). This is because the concept of classes is separated from the underlying implementation type.

This can be used for various things, for example in callbacks you can pass an array of multiple values instead of just the object and unpack it in the code by assigning the object instance into the this variable. Another example is the usage of weak references (again typically in callbacks).

Some methods can check explicitly for nulls in this. For example comparison functions or to_string implementations.

A slight downside of this approach is that when a null is passed to an instance method, there is no check at the time of method call, it will error only when the object is accessed in the method, for some methods it can even succeed if it's not accessed at all.

Field offsets

You can get the field offset with SomeClass::field_name and the size with SomeClass::SIZE. This is useful when working with the underlying array representation directly.

You can also get references to methods with SomeClass::method#1. The number of arguments must count with the this parameter for instance methods.

Class definitions using private constants

Sometimes you may want to define classes without actually using the classes token processor. For example if you're not using classes (or want it optional) but still provide the comfort of using them. Another example is the ability to make multiple versions of classes token processor (or other token processors) to work together.

The format is simple, to declare a class use a @class_SomeClass private constant with a string value (can be empty). The string can contain these attributes:

The value is delimited by '=', multiple attributes are separated by ',' and when the attribute can contain multiple values they're separated by a ':'. No whitespace is allowed.

You can define methods by declaring a @method_SomeClass_method_name_1 private constant, where the last part is a number of parameters (including the implied 'this' parameter). You can either specify static methods using the static attribute for the class, or just use static instead of method in the private constant name. Undefined methods are still recognized based on the prefix (all parameters and the return type are Dynamic in that case).

You can define global functions by using a @global_some_func_1 private constant to define a function named some_func with a single parameter.

Every method uses the '(SomeType, Integer, Float): Boolean' format to specify the parameters. This means specifying the types of the parameters (the parameter for 'this' is omitted for non-static methods), optionally followed by a return type. Whitespace is allowed.

You can define operators by using a @operator_SomeClass_add_1 private constant. The value is in the format 'method_name (Type1, Type2)' (the inplace variants omit the first type as it is always the class type). The trailing number can have any value, it is used to allow multiple constants when there are multiple variants of the same operator. The names for the operators are as follows:

SymbolName
+add
-sub
*mul
/div
%rem
&and
SymbolName
|or
^xor
<<shl
>>shr
>>>ushr
<lt
SymbolName
>gt
<=le
>=ge
==eq
!=ne
<=>cmp
SymbolName
+=add_inplace
-=sub_inplace
*=mul_inplace
/=div_inplace
%=rem_inplace
&=and_inplace
SymbolName
|=or_inplace
^=xor_inplace
<<=shl_inplace
>>=shr_inplace
>>>=ushr_inplace

This is an example how it can look:

const @class_SomeClass = "extend=BaseClass,prefix=somecls,struct=SCLS,static=create#2:static#0";
const @field_SomeClass_field1 = "Integer";
const @field_SomeClass_field2 = "Boolean";
const @static_SomeClass_create_2 = "(Integer, Boolean): SomeClass";
const @method_SomeClass_instance_function_3 = "(Integer, Boolean)";
const @operator_SomeClass_add_1 = "add (SomeClass, SomeClass)";
const @operator_SomeClass_add_2 = "add_int (SomeClass, Integer)";
const @operator_SomeClass_add_inplace_1 = "add_int_inplace (Integer)";
const @global_some_func_2 = "(SomeClass, Integer): Boolean";

You can specify scripts that are automatically also imported when importing the script with the classes definitions. This is useful when classes are referenced from other scripts. An example:

const @import_scripts = "some/script,other";

API

The API allows other token processors to integrate with classes processing. To get access to the API, just import classes script and obtain the context. Then you can register various hooks. The hooks are added based on actual needs.

You can also use the API without classes, just use the prefixes class_context_ and class_type_ for the methods and pass the objects as a first parameter.

const {
	TYPE_DYNAMIC,
	TYPE_VOID,
	TYPE_INTEGER,
	TYPE_FLOAT,
	TYPE_BOOLEAN,
	TYPE_STRING
};

const {
	EXT_TYPE_CLASS,
	EXT_TYPE_ARRAY,
	EXT_TYPE_HASH,
	EXT_TYPE_STRUCT,
	EXT_TYPE_STRUCT_ARRAY,
	EXT_TYPE_MULTIPLE
};

class ClassContext
{
	static function get(fname: String): ClassContext;
	static function get(fname: String, required: Boolean): ClassContext;
	function register_function_call(name: String, get_types_func, adjust_call_func, data);
	function register_postprocess(func, data);
	function get_class(name: String): ClassType;
	function get_const_type(name: String): ClassType;
	function get_local_type(name: String): ClassType;
	function get_variable_type(name: String): ClassType;
}

class ClassType
{
	static function create_array(base: ClassType): ClassType;
	static function create_hash(base: ClassType, index: ClassType): ClassType;
	function is_class(): Boolean;
	function is_array(): Boolean;
	function is_hash(): Boolean;
	function is_struct(): Boolean;
	function is_struct_array(): Boolean;
	function is_multiple(): Boolean;
	function is_assignable_from(other: ClassType): Boolean;
	function get_class_name(): String;
	function get_parent_class(): ClassType;
	function get_method_real_name(name: String, types: ClassType[], is_static: Boolean): String
	function get_base(): ClassType;
	function get_index(): ClassType;
	function get_multiple_count(): Integer;
	function get_multiple_type(idx: Integer): ClassType;
	function to_string(): String;
	static function dump_list(list: ClassType[]);
}

The types are divided into two enumerations. The simple types are just integers and you can directly use them (cast from/to ClassType). The extended types are contained in an array and the first entry is the type.

ClassContext class

static function get(fname: String): ClassContext
static function get(fname: String, required: Boolean): ClassContext
Obtains context for given script file name. The function register a check if given file is using classes unless the required optional parameter is set to false. The file name must correspond to the currently processed script.
function register_function_call(name: String, get_types_func, adjust_call_func, data)
Registers adjustment of function call to otherwise not defined function (the resolution is done as last). Multiple registrations to the same name can be done. Provide a name of the function (without parameter count, you can pass null to match all names) and the callbacks with these signatures:
function get_types(data, name: String, num_params: Integer, line: Integer): ClassType[]
Called when the function name is matched, provides number of the parameters. You need to return an array of types (the first index is for the return type, the rest is for the expected types of the parameters) or null if there is no function for given number of parameters. The returned array is modified and passed to the adjust_call callback.
function adjust_call(data, name: String, types: ClassType[], tokens, src: String, start: Integer, end: Integer): Integer
Called after the function call is generated with all type conversions. The types (after any conversion) are provided in an array (the previously given array is modified). You have also access to the generated tokens and the starting and the ending index (exclusive). You need to return the new ending index after custom adjustments.
function register_postprocess(func, data)
Registers function to be called after processing of classes. The registered functions are called in a reversed order to allow wrapping behavior of different token processors. The signature of the function is:
function postprocess(data, fname: String, tokens, src: String)
Called after processing of classes.
function get_class(name: String): ClassType
Returns class type for given name. Must be called within processing.
function get_const_type(name: String): ClassType
Returns the type for a constant. Returns -1 if the constant with given name is not defined. Must be called within processing.
function get_local_type(name: String): ClassType
Returns the type for a local variable. Returns -1 if the local variable with given name is not defined. Must be called within processing.
function get_variable_type(name: String): ClassType
Returns the type for a variable. Returns -1 if the variable with given name is not defined. Must be called within processing.
function get_source(): String
Returns the source code referenced by the tokens. Must be called within processing.
function append_tokens(tokens)
Appends new tokens at the end of the script, these will be processed with the classes features. The tokens array must not be reused (it will be processed in a deferred manner). Must be called within processing.

ClassType class

static function create_array(base_type: ClassType): ClassType
Creates an array type with given base type.
static function create_hash(base_type: ClassType, index_type: ClassType): ClassType
Creates a hash type with given base and index type.
function is_class(): Boolean
Returns true when the type is a class.
function is_array(): Boolean
Returns true when the type is an array.
function is_hash(): Boolean
Returns true when the type is a hash.
function is_struct(): Boolean
Returns true when the type is a struct.
function is_struct_array(): Boolean
Returns true when the type is a struct array.
function is_multiple(): Boolean
Returns true when the type is a multiple type.
function is_assignable_from(other: ClassType): Boolean
Returns true when the given type is assignable to the current type.
function get_class_name(): String
Returns name of class.
function get_parent_class(): ClassType
Returns parent class.
function get_method_real_name(name: String, types: ClassType[], is_static: Boolean): String
Returns the real name of method (or null) with given types (the first entry is a return value).
function get_base(): ClassType
Returns the base type of array or hash.
function get_index(): ClassType
Returns the index type of hash.
function get_multiple_count(): Integer
Returns the count of types in a multiple type.
function get_multiple_type(idx: Integer): ClassType
Returns the type at given index in a multiple type.
function to_string(): String
Returns the string representation of the type.
static function dump_list(list: ClassType[])
Dumps list of types in string representation.