FixScript

Blog

Introduction to FixScript (2022/12/27)

FixScript is a programming language which is both embeddable and standalone with strong support for backward and forward compatibility. It has familiar C-like syntax with ability to create new syntaxes using token processors.

It started as a very simple scripting language for a specific need. But when I caught myself using it also for the main program it became obvious that the language is much more than just for the scripting.

The key theme of the language (and the implementation) is simplicity. But not that kind of simplicity that limits you but empowers you. I've made sure to include everything essential while not making it bloated or too complex.

FixScript provides a minimal standard library. This is useful in embedded/scripting usage where you don't want any outside access. It doesn't even provide an ability to get the current time despite having performance logging that can print the time spent.

There are also the core libraries that provide functionality for various common things (I/O, 2D graphics, cross-platform GUI, multithreading, native access, sorting, JSON, etc.). These are fairly independent of each other but can integrate together. That way you can use just the needed parts.

A major feature of the language is an ability to extend the language itself. FixScript has a concept of token processors. These are scripts that enable arbitrary processing of the tokens before passing it into the compiler. It was designed with both simple and complex additions in mind. In fact the classes implementation (including the type system) is implemented as a library.

How the language looks like?

FixScript uses a familiar C-like syntax. A simple example:

function main()
{
    log("Hello, world!");
}

Very straightforward. What about some arrays?

function main()
{
    var arr = [10, 20, 30];

    // append to end:
    arr[] = 40;
    arr[] = 50;

    for (var i=0; i<length(arr); i++) {
        log(arr[i]);
    }
}

Pretty standard. Hash tables?

function main()
{
    var hash = {
        "test": 123,
        "blah": 456
    };

    hash{"other"} = 789;

    for (var i=0; i<length(hash); i++) {
        var (key, value) = hash_entry(hash, i);
        log({key, " = ", value});
    }
}

This looks more interesting. Initialization is standard, access uses curly brackets. This allows to distinguish between array and hash access so it doesn't have to do it at runtime. And the entries in hash tables can be directly accessed by their index (in the insertion order).

Ok but that string concatenation when logging is quite hairy. I can certainly agree, however it has clear advantages (no ambiguity with addition, more clear where the boundaries are and a quite good syntax for multi-line strings).

Ok this sounds nice and all but not sure about the syntax... sure, what about this?

use "classes";

function main()
{
    var hash: Integer[String] = {
        "test": 123,
        "blah": 456
    };

    hash["other"] = 789;

    foreach (var key, value in hash) {
        log(key+" = "+value);
    }
}

What happened? Did we switch into another language? No, by using the classes we introduce the type system that allows to infer what kind of values we are working with thus being able to use a nicer, unified syntax. This is the syntax that most would use in FixScript, but it is important to know the base syntax as well.

But what if I don't want the types and still have a nice syntax?

use "simple";

function main()
{
    var hash = {
        "test": 123,
        "blah": 456
    };

    hash.other = 789;

    foreach (var key, value in hash) {
        log(key+" = "+value);
    }
}

This defers various things into a runtime, making it slower but more simple. This is what most scripting languages do.

Ok, C-like syntax is nice and all but what I really like is Python syntax, very clean. While FixScript doesn't currently have that option it is something that I would want to provide as well. It would look something like this:

use "snake"

hash = { "test": 123, "blah": 456 }
hash.other = 789

for key, value in hash:
    log("%s = %s" % (key, value))

In the end any syntax is possible by writing a corresponding token processor that handles it. All these syntaxes are just FixScript libraries. However it is probably a better idea to spend the effort to do various specific additions than to have tons of totally different syntaxes.

What about classes?

In the base language classes are defined using a convention:

const {
    OBJECT_field1,
    OBJECT_field2,
    @OBJECT_private_field,
    OBJECT_SIZE
};

function test()
{
    var obj = object_create(OBJECT_SIZE);
    obj->OBJECT_field1 = 123;
    obj->OBJECT_field2 = 456;
    obj->OBJECT_private_field = "test";
}

The object_create is just a more descriptive name for the array creation of given size. The -> operator is just for better readibility and is the same as accessing the array at given constant offset.

Similarly you can also extend classes like this:

const {
    EXTENDED_field = OBJECT_SIZE,
    EXTENDED_SIZE
};

function test(obj)
{
    var ext = object_extend(obj, EXTENDED_SIZE);
    ext->EXTENDED_field = 123.456;
}

You can see that the simple combination of auto-counted constants (enums) and arrays provides ability to use objects. Unlike other scripting languages this approach makes it more performant and more mistakes are caught at the compilation time.

You can also search for all the usages of a certain method or a field without any language specific tools.

However, as you've already learned, it is better to use an explicit syntax for the classes, like so:

use "classes";

class Object
{
    var field1: Integer;
    var field2: Integer;
    var @private_field: String;

    constructor create()
    {
    }
}

class Extended: Object
{
    var field: Float;

    constructor create()
    {
    }
}

function test()
{
    var obj = Object::create();
    obj.field1 = 123;
    obj.field2 = 456;
    obj.private_field = "test";

    var ext = Extended::create();
    ext.field = 123.456;
}

The constructors automatically create or extend the objects. The usage is also type-safe. But it is still the same as the above under the hood. You can read more about the classes in the documentation.

On extensibility

There are various languages that are extendable. Usually it is by exposing certain mechanisms that you can use to hook your extensions into. This provides ability to generate code but in a limited way. Some languages do this at runtime which is easier to write but comes with a runtime speed penalty.

The usual problem with these approaches is that you must fit your use case to the provided syntax and it rarely fits perfectly. In comparison, FixScript allows any syntax changes and it's done as a preprocessing step. This allow to implement the extension properly with just the right syntax for the job and no extra boilerplate. There is also no runtime speed penalty.

Having such flexibility can be both a good thing and a bad thing. For example it can be hard to reason about the code. However from my experience when the additions can be designed with a perfect syntax for the task there is no doubt what it does. It requires to follow some rules though (no surprises being the most important one).

Integration with the tools such as IDEs is also not a problem. The token processors can be run by the tools and interact with theirs APIs (for example to handle auto-complete based on the types).

Performance

FixScript is designed with speed in mind. The language is somewhere between the compiled languages and the dynamic languages. It is dynamically typed but most of the operations work based on a particular type. This allow for an optimizing compiler to directly deduct the types.

By default FixScript is using a JIT compiler on x86 and x86_64 architectures. It also contains a quite fast interpreter for other platforms / environments. In the more long-term a fully optimizing AOT compiler is also planned.

Conclusion

FixScript is a fully usable standalone general-purpose language that can build native executables for any supported platform from every platform.

Or you can use it together with the C language. The implementation is a single C file that can be easily embedded into C projects. This allow for a very powerful combination where you can use the advantages of both languages while minimizing the disadvantages.

What comes next?

A lot of things. Currently I'm developing various libraries for audio, video, cryptography, vector/parallel processing and 3D graphics. There is a work on FixIDE to make it easier to use. A full WebAssembly support (both browser and WASI, including an emulation of threads) is coming soon.

Comments

1. jezek2 (2023/01/12 18:30)
More discussions:
https://news.ycombinator.com/item?id=34315137
https://old.reddit.com/r/ProgrammingLanguages/comments/zwp51v/introduction_to_fixscript/

Add comment

Name:
Content:
Confirmation code:
 
  (type the letters shown in the picture)