Richard 1.0 - Rant's New Scripting Engine

Rant 2.0 is almost here, and it launches with a new scripting language I wrote called Richard! We're still ironing out the bugs and writing the documentation and the standard library, but here's a post describing most of what Richard is about.

The History of Richard

Let's look back to Rant 1.0, all those months ago. Rant 1.0 had a syntax for simple arithmetic operations. This supported basic math operations as well as variables. A simple arithmetic expression would look like this:

`a = 10; a += 5; a` # 15

Though it served its purpose well, it lacked many features. The only variable type was a number type (double), and any kind of control flow wasn't possible. The arithmetic parser itself wasn't well integrated with the rest of Rant, using a different lexer and "VM" than the rest of Rant.

The new scripting system was originally divided into separate issues: we needed to add a string type to the arithmetic system, we needed to integrate the variable system with all the other types of variables (carriers and subroutines), and we needed to integrate the parser with the rest of Rant. We eventually realized that we would be better off just redesigning the language itself, leading to the idea of a new language inside of Rant called Rave.

Originally, Rave was going to compile into bytecode similar to IL, which would run on a VM within Rant. However, Berkin figured out a way to restructure the Rant parser and VM to remove recursion, and we decided that using this system for Rave would be best. At some point Berkin decided to use Rave as the name of the Rant command line utility for dictionaries, and so the scripting engine was renamed Richard, for completely mature reasons.

After a lot of hard work and spaghetti code, Richard was committed in 901be7f. We're still working out the bugs and adding some new features.

The Basics

Richard is pretty similar to Javascript, if Javascript was smaller, less featureful, and written by someone who had never written a programming language before. It runs within Rant, and is contained within a [@ ... ] tag:

[@ 2 + 2 ]

Richard has variables - obviously - of which there are two syntaxes. The regular name = value; syntax works, and will create the variable in the current scope. There is also a var name = value; or just var name; syntax, which will create the variable in the current scope whether or not a value is given.

There are nine basic types in Richard:

  • number

  • string

  • bool

  • list

  • object

  • function

  • pattern string

  • no

  • undefined

And what are all of these sweet types?

  • number types are internally represented as a double (i.e. a 64-bit double-precision number). Basic infix operations are supported - +, -, *, /, % - as well as groups - (2 + 2) / 2. Order of operations applies, of course. There are assignment versions of those as well - +=, -=, *=, /=, and %=.

  • string types are .NET strings internally, which means I think they support Unicode. I'm not entirely sure. They are delimited by double quotes - single quotes not supported. There is a length property, accessible through the same way as any other object property - "string".length or "string"["length"]. You can also use an array indexer if you want to get a certain character of a string i.e. "string"[2] # "r". String concatenation is done using a special operator à la D. That is, the operator is ~. It will cast whatever is on the right side of it to a string if it isn't already a string, but the left side must be either a string or a pattern string. There is also an assignment version - ~=.

  • bool types are just simple booleans. true or false, you know the deal. There are a few boolean operators which return bool values. You probably know these from other languages: ==, !=, >, >=, <, <=, and !.

  • list types are lists of objects. There are three ways of defining them: a bare list syntax, using just commas - x = 1, 2, 3, 4, 5;, a bracket syntax like in Javascript - x = [1, 2, 3, 4, 5], and a list initializer for creating empty lists of a certain length - x = list 12;. When using the bare list syntax, any variables specified within that are lists will be expanded - that is, you can use the bare list syntax to concat two lists together: x = 1, 2; y = 3, 4, 5; z = x, y;. If you don't want this, you can use the bracket syntax. You should also use the bracket syntax for creating a list of lists, or for passing lists as an argument to function calls, or for creating lists inside of object literals. You can access a list's length using the length property: x = 1, 2, 3, 4, 5; x.length # 5. If a list is returned from an expression (it's the last value evaluated, or it's explicitly returned using a return statement), it will be executed as a block in Rant.

  • object types are key/value objects, like in Javascript. And similarly to Javascript, the syntax is JSON. Not Javascript object syntax, but JSON. That means all key names must be either strings or just bare text (i.e. {test: 2}). You can access an object's property via dot syntax ({"test":2}.test) or via bracket accessor syntax ({"test":2}["test"]). You can also set object properties the same way i.e. x = {"test":2}; x.test = 3; x["test"] = 4.

  • function types are, well, functions. There are two ways to define them: using a C-like syntax - x = function(a, b) { a + b }; - or using a lambda syntax - x = (a, b) => a + b; Function calls are the same as every other language: x(2, 3). You can return things from them using a return statement, just like every other language, though it'll return the last value evaluated in the function if there's no return statement. You can also return from any other statement in Richard, which will end the expression and return the value to the Rant pattern.

  • pattern string types are like strings, but they can be executed as Rant patterns. It is declared using a dollar sign in front of a string: $"<noun>". They can be executed two ways: using a function call - $"<noun>"() - or by being returned from an expression. You can use a concatenation operator like strings.

  • no is a null value. This means that it is a value that has been explicitly set to null. It's a constant: x = no. Why no instead of null? It's funny.

  • undefined is a value that hasn't been set yet. In Richard, this type is identified by the constant ??? - that is, if you set something to undefined (why?), it would look like this: x = ???

Statements

There are a few control flow statements in Richard.

if statements, along with their companion else statements, are a way of conditionally executing a specified block of code. You use them like you do in any other language:

if(true)
{
    return "It's true!";
}
else
{
    return "This should never happen."
}

while statements are, again, the same as they are in every language since C:

x = 5;
while(x > 0)
{
    x--;
    Output.print(Convert.toString(x) ~ " ");
}

for statements are different! They're actually for..in statements, like in Javascript. The reasoning is that there are very few times that you ever need to use a for loop in a way that's not just iterating over something from beginning to end, and if you want to do that, you can use a while loop.

You can use a for..in loop to iterate over lists or objects:

x = "this", "is", "a", "test", "string";

for(i in x)
{
    Output.print(x[i] ~ " ");
}

y = {
    "key1": "string",
    "key2": "another string"
};

for(k in y)
{
    Output.print(y[k] ~ " ");
}

As you can tell, the key is returned, not the object. For arrays, this will be the index.

You can break out of both kinds of loops.

Standard Library

Richard currently doesn't have a very robust standard library, though we'll be adding to it constantly.

For now, there's two types of standard library functions: properties and global objects. Properties are created on a specific type of object, and can be used on any type of that object. For example, string types have a length property, and list types have a last property. Global objects are global Richard key/value objects which have properties of their own. For example, there's a Math global object, which has a property PI which returns the mathematical constant Pi. It also has functions like sin and atan2.

There isn't documentation for the standard library yet (I haven't written the extension for Constitution), but every function has tests, and you can take a look at those.

Sample Program

I'm too lazy to write my own sample program, but here's one Berkin wrote:

{
  var Select =
  {
    # filter(condition(current))
    where: (lst, filter) =>
    {
      var results = [];
      for(var i in lst)
      {
        if (filter(lst[i]))
          results.push(lst[i]);
      }
      return results;
    },
    # accumulator(current, next)
    concat: (lst, accumulator) =>
    {
      var buffer = """";
      for(var i in lst)
      {
        buffer = accumulator(buffer, lst[i]);
      }
      return buffer;
    }
  };

  return Select.concat(
    Select.where([1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
    (x) => {
      return x % 2 == 0;
    }),
    (current, next) => {
      if (current.length > 0) current ~= "", "";
      current ~= next;
      return current;
    });
}

It's an implementation of some Linq-inspired features in Richard. I'll probably add some of these (where / filter, map / select, reduce, etc) into the Richard standard library before release.

In Conclusion

Richard is currently really buggy, but it's still usable! If you find any bugs, please add a test for it! We're especially concerned about "white" exceptions, i.e. exceptions that aren't RantRuntimeExceptions or RantCompilerExceptions.