Changelog

From Monster Wiki

Jump to: navigation, search

Contents

Why there will be no 0.15

As I wrote on the old homepage, 0.14 was the last release version of the current prototype branch of Monster. All future work will go into a future branch, implemented in C++, and starting with a complete rewrite of the engine and the language itself.

Changelog for 0.14

Abstract classes

Abstract classes are classes which cannot have any instances (objects). In other words, you cannot use 'new' or createObject to create objects from the class. To define an abstract class, do:

abstract class AbsClass
// or:
class abstract AbsClass

If you try to create an object of this class, it will fail:

var o = new AbsClass;       // Error: AbsClass is abstract

You can still inherit from an abstract class, and create objects from the child classes. Singletons and modules cannot be abstract (since they require at least one instance to be created.)

'Object' class

All classes that do not have an explicit parent will now implicitly inherit from 'Object'. So:

class MyClass
// is the same as:
class MyClass : Object

Object is an empty, abstract class that is defined internally in the VM. It can be used as a common type to store any object without knowing what class it belongs to:

Object o = new MyClass;
Object o2 = new OtherClass;
 
// To get the right type back, you have to cast:
 
MyClass m = o               // Implicit casting
var oc = OtherClass(o2)     // Explicit casting
 
// Casting will of course not work if the object is not the right type:
 
MyClass m = o2              // Runtime error
var oc = OtherClass(o)      // Runtime error

Function pointers

A function pointer (or reference) is a variable that refers to a given function in a given object. You declare one like this:

function return_type (paramtype_1, paramtype_2, ...) variable_name

Examples:

function()  var1            // No return type or parameters
function int()  var2        // Returns int, no parameters
function (float, char[]) v3 // Takes float and char[] parameter, no return type

You create a reference to a function using the @ operator, and you call it using normal () syntax. Here is an example of usage:

class FuncPtr
 
func1() { io.writeln("func1") }
func2() { io.writeln("func2") }
 
main()
{
    // Declare a function pointer
    function() ptr;
 
    // Point it to func1
    ptr = @func1;
 
    // Call it
    ptr();
 
    // Call func2
    ptr = @func2;
    ptr();
}

A pointer may point to any type of function you want, including native functions, idle functions or abstract functions. You may also reset the pointer to point to nothing, by doing:

ptr = null                  // The pointer now points to nothing, and attempting to call it will result in a runtime error
ptr = ptr.init              // Equivalent

The function() syntax is just a type specifier like any other. You can use it as a parameter type for example, or deduce it automatically using var:

var fn = @someFunction;     // Deduce the function type automatically
 
...
 
// A function that takes a function
takeFunc(function(int) dg)
{
    // Call it ten times
    for(int i;i<10;i++)
      dg(i);
}

Function pointers with objects and inheritance

A function pointer always knows the owner object of the function it refers to. So for example, if you have the following class:

class Cls
int i
func() { writeln("i=", i) }

Then references to 'func' will be called in the object that you created the reference from:

var a = new Cls(i=10)
var b = new Cls(i=20)
 
var fn = @a.func;
fn()                        // Prints 'i=10'
fn = @b.func;
fn()                        // Prints 'i=20'

In addition, function pointers will always call functions virtually:

Parent p = new Child;
var fn = @p.func;
fn();                       // Calls Child.func() (if it exists), not Parent.func()

Function pointer properties

Besides the built-in properties shared by all types (.init, .sizeof and so on), function pointers have the following two properties:

Name Type Description
.obj Object Returns the owner object of this function
.func char[] Returns the name of the function as a string

Internal changes and bug fixes

Several internal cleanups and bug fixes made it into 0.14. For example, the stack frame system (marking the beginning and end of a function on the stack) has been completely removed from the VM and is now handled entirely on the compiler side. This means less runtime bookkeeping and less complexity, especially in relation to the thread system (which throws the stack around a lot.)

This also removed several bugs / asserts, and allowed us to fix an issue where expressions like

array[$-5..$]

would evaluate the 'array' expression multiple times (once for each occurrence of $).

Changelog for 0.13

" and ' are interchangable

You may now use single quotes to define string literals:

"string"
'string'          // These are exactly equivalent

This is helpful when you want to write strings containing the " character:

'I may contain the " character'
"I may contain the ' character"

As a consequence of this, character literals (the previous use of ') no longer exist. Instead all strings of length 1 are implicitly convertible to 'char'. Example:

char ch = 'a'     // This works like you would expect
char c2 = 'abc'   // Error

This means that most existing code will still work. In cases where it's important that 'x' is a char and not a string, you can use explicit casting:

var a = 'x'       // This creates a string
var b = char('x') // This creates a char

String escape sequences

String literals can now contain escape sequences. An escape sequence is a special 'code' that is replaced with a predefined character at compile time. A complete list of codes is given below. Most of them should be familiar to C*, Python and D users, but a couple are unique to Monster.

Code Decimal value Description
\a 7 bell character
\b 8 backspace
\t 9 horizontal tab
\n 10 newline
\v 11 vertical tab
\f 12 feed form
\r 13 carriage return
\e 27 ANSI escape character (\033)
\" 34 double quote character
\' 39 single quote character
\\ 92 backslash
\NNN NNN Decimal character value (N are digits 0-9)
\0NNN or \oNNN Octal character value (N are digits 0-7)
\xXX Hexadecimal character value (X are digits 0-9, a-z or A-Z)
\uXXXX Unicode character up to 16 bits (X are hexadecimal digits)
\UXXXXXXXX Unicode character up to 32 bits (X are hexadecimal digits)
writeln("\nSome string")                   // Write a newline at the beginning of the line
writeln("This is a \" terminated string")  // Writes 'this is a " terminated string'
writeln("\66ad")                           // Writes 'Bad' (because 'B' has ASCII value 66)
writeln("\e[32mgreen\e[0m")                // Writes in color if your terminal supports ANSI codes

All the numerical escape sequences take a variable number of digits. The number of digits specified in the table above only defines the maximum number of digits that are read for the given sequence. So for example, \U takes 1-8 digits, and you may write \ua3b, \Ua3b, \U000a3b or \U00000a3b - all of them are equivalent.

writeln("A")       // All of these write the character 'A' to screen
writeln("\65")
writeln("\o101")
writeln("\x41")
writeln("\u41")
writeln("\u0041")
writeln("\U41")
writeln("\U041")
writeln("\U000041")
writeln("\U00000041")

Double quotes

Inside any double-quoted string you may repeat the double-quote character twice ("") to represent a single " within the string. Similarly, in single-quoted strings (see below) you may repeat the quote to represent a single quote. Example:

writeln("I can write "" and '")
writeln('I can write " and ''')

Output:

I can write " and '
I can write " and '

WYSIWYG strings (aka. backslash strings)

Wysiwyg (what you see is what you get) strings are string literals where backslashes are not interpreted as escape sequences. A wysiwyg string is on the form r"..." or \"...", and the equivalent r'...' and \'...' for single quotes. Examples:

"c:\\dir\\myfile"   // Normal string - you have to use the escape sequence \\ to get backslashes
r"c:\dir\myfile"    // Wysiwyg string - backslashes are allowed
\"c:\dir\myfile"    // Equivalent
r'c:\dir\myfile'    // Equivalent
 
r"This "" is a quote" // "" and '' still work, wysiwyg only affects backslashes

Console function calls

A new, simplified function call syntax is now available in 'console mode':

// These are equivalent in console mode:
print(1,2,3)
print 1,2,3;
print 1 2 3
 
exit()
exit

This does NOT affect normal script files. Although it could be nice to call functions this way, the changes would affect too much of the parser code and the language syntax itself to be worth it.

Moved debugging / tracing to vm.dbg

Logging and debugging functions have been moved to the D module monster.vm.dbg. There is also a new dbg.log() function, and a stream dbg.dbgOut which controls where the log goes, and which can be set to null to disable logging.

Logging of various part of the engine (like the function stack or the thread changing mechanism) can be enabled and disabled at compile time in options.d.

Implicit compile time conversion of arrays

Arrays can be converted from one type to another implicitly, if the base type is convertible:

float a[]   = [1,2,3,4]       // Ok, even if the array literal has the type int[]
double[] d  = [1.0, 2.0]      // Ok, float -> double conversion

However this only works on compile-time conversions at the moment, because runtime casting could have unintended consequences that are difficult to debug. Casting creates a copy and this might not be what you would expect:

// This example does NOT currently work because it would be inconsistent
int[] orig = [1,2,3];
...
int[] i = orig;               // Refers to the same array
float[] f = orig;             // Creates a copy because of casting

Changelog for 0.12

Packages

Monster 0.12 has a new package system, which allows an easier way to group several classes together under one name. The package names are directly tied to the directories in the file system (or in your virtual file system, see further down.) You never need to declare packages anywhere, just put the script files in a subdirectory. For example, say you have the following files:

a.mn            - class A
dir/b.mn        - class B
dir/dir2/c.mn   - class C

You may access the class names in the following manner:

// Specify package (directory) names explicitly:
A a;
dir.B b;
dir.dir2.C c;
 
// Import a package to access its classes without using the package name
import dir;
B b2;
dir2.C c2;

You do not need to specify the package name to access classes that are in the same package as yourself. In case there is a conflict between a local class name and a global one, you can always use the special package name 'global' to explicitly select the package you want:

var a = new global.A
var b = new global.dir.B
var c = new global.dir.dir2.C

You can also specify package names to vm.load() in D/C++:

vm.load("dir.B");
// Or equivalently
vm.load("dir/b.mn");

See below for more information about vm.load().

The package system required some restructuring of the scope system, and hasn't been heavily tested yet. It should be considered experimental.

Semicolon becomes optional

Semicolons at the end of statements and declarations are now optional. For example, the following piece of code:

int i = 3;
float f;
f = i*5.5;
writeln("f is now ", f);
func1(); func2();

can now be rewritten

int i = 3
float f
f = i*5.5
writeln("f is now ", f)
func1(); func2()

This makes the language easier to use for non-C gurus. Semicolons are still supported though and always will be. Notice the semicolon in the last line is still needed because the statements are on the same line.

Removed MonsterClass constructors, replaced with vm.load()

To load a class you should now use vm.load(), which takes the class name, the file name, or both as a parameter. You can no longer load classes by creating MonsterClass instances yourself. The reason for this is to avoid errors if the class was already loaded. When used on an existing class, vm.load() will simply return the already loaded instance. There are also overloaded versions which load from streams, and loadString that loads a class directly from string. Examples:

mc = vm.load("MyClass");                    // Will load myclass.mn from file
mc = vm.load("MyClass");                    // Will return the existing MonsterClass instance
mc = vm.load("class2.mn");                  // Specify filename
mc = vm.load("Class2", "class2.mn");        // Specify both
mc = vm.load(someStream);                   // Load from stream
mc = vm.loadString("class MyClass; int i"); // Load from string

Constructors

Monster class scripts can now define constructors. These take no parameters (as of yet) and have the following syntax:

class MyClass
 
new
{
  io.writeln("Now inside the constructor")
}

When inheriting from another class, all the base class constructors are called before the current one. You can always assume that all class variables have been initialized properly before the constructor is called, except if this initialization is to be done in a child class constructor.

Removed = as an expression

Assignment can no longer be used as an expression, only as a statement. In plain English, this means that expressions such as the following are not allowed:

var v = (a=b); // Not allowed anymore, a=b does not return a value
a=b;           // Still allowed, since no value is needed

Using assignment as expressions is relatively rare, so this does not affect most existing code at all. It does however free up the = symbol for other uses, such as for the next two points.

Set object variables with 'new'

When creating a new object with 'new', you can now optionally set object member variables for the new objects. Consider the following class:

class MyClass : MVM
int i = 5
 
main()
{
  var a = new MyClass;
  writeln(a.i);              // = 5, default value for i
  var b = new MyClass(i=8);
  writeln(b.i);              // = 8
}

Any number and combination of member variables can be specified, including variables in parent classes. The values are set BEFORE the constructor (if any) is called. This means that you can effectively pass parameters to the constructor this way. Example:

class MyClass : MVM
 
int i, j, k;
 
new
{
  writelns("In constructor:",i,j,k);
}
 
main()
{
  new MyClass                // constructor prints 0 0 0
  new MyClass(i=10,k=20)     // constructor prints 10 0 20
}

Named parameters when calling functions

When calling a function, you can now specify function parameters explicitly by name instead of by order:

func(int x, int y) {}
...
// These are all equivalent
func(10,20)
func(x=10, y=20)
func(y=20, x=10) // The order doesn't matter when names are given

You can mix the two modes, but all the by-order parameters must come before the by-name ones:

someFunc(1,2,3,4, x=5, y=6, z=7);    // Ok
someFunc(1,2,3,x=5, 4, y=6, z=7);    // Error

You cannot use named parameters on vararg (variable argument) functions.

Named function parameters are especially useful when combined with the next point:

Optional function parameters

Like in most C-like languages, you can now give default values to function parameters:

int someFunc(int i, int j, int k=4, int m=5) {...}
 
someFunc(1,2,3,4); // Ok
someFunc(1,2,3);   // Ok, m is set to 5
someFunc(1,2);     // Ok, k,m is set to 4,5
someFunc(1);       // Error, j not set

Unlike most languages, however, you can specify an optional parameter before (to the left of) a non-optional one. This is mostly useful when combined with named parameters (see the previous point.) In any case, the rule is that all non-optional parameters must be given one (and only one) value to make the function call valid. Examples:

myFunc(int a=1, int b=2, int c=3, int d) {...}
 
myFunc(1,2,3);         // Error, d is missing
myFunc(1,2,3,4);       // Ok
myFunc(d=1);           // Ok
myFunc(a=4,d=3,b=90);  // Ok
myFunc(6,7,d=8);       // Ok, sets a=6, b=7, c=3 (default value), d=8
myFunc(1,2,3,4, a=5);  // Error, 'a' was given multiple values

Overwrite variables in parent classes

Child classes can now overwrite the default initial values of variables defined in parent classes. Example:

class ParentClass
int i, j=5
new { writelns(i,j); }

The constructor will print "0 5". You can override the values in a child class however:

class ChildClass : ParentClass
i = 10
j = 20

These will be set before ANY constructors are called, so the constructor in ParentClass will now print "10 20".

To summarize the order of initialization:

  • First, initial values are set, including initial values overridden in child classes (if any).
  • Second, values given to new(var=value) are set (if any), overriding the initial value.
  • Third and finally, constructors are called, which may also overwrite any variable values.

Set initial state in classes

You can now set the initial state in a class:

class Test
state myState
{
  //...
}
 
state = myState     // Set initial state to myState

All newly created objects of the class (except cloned objects) start out in the given state. You can also specify state labels. The initial state can be overridden i child classes, but can only be specified once per class. You can also set this manually in native code using

mc = vm.load("Test");
mc.setDefaultState("myState");

Stack trace

The function vm.getTrace() returns a string containing all the functions on the Monster function stack. Output example:

Trace:
native MyClass.printTrace (<---- current function)
script MyClass.test2
script MyClass.test1 (<---- first Monster function)

If you want to include your own functions in the stack trace, you can use vm.trace() and vm.untrace() to add/remove them from the stack. vm.trace() takes parameters the same way as the D function writefln. Example:

void myfunc(int a)
{
    vm.trace("myfunc %s in %s", a, this);   // Push ourselves on the function stack
    scope(exit) vm.untrace();               // We must remember to remove ourselves again when we exit
    // ... do whatever
    if(error)
        writefln(vm.getTrace);              // Print the current function stack
}

(This is the D interface only. The C++ interface will use streams like std::cout and family.)

Enums

Monster 0.12 adds enums. Enums allow you to assign names to predefined values, and makes a new type that can hold these values. Basic example:

enum MyEnum
{
  First,               // First entry gets the value 0 if not specified
  Second = 10,         // How to set the value explicitly
  Third                // value=11, use the previous value + 1 if not specified
  Fourth = 20          // Terminating commas are optional (if a line break is present)
  Fifth,               // value=21, commas are also allowed on the last line
}

Usage:

MyEnum e                    // Create a variable of the type MyEnum
writeln(e)                  // Prints "no value", default init value is to select none of the given values
 
e = MyEnum.Second           // Select the Second value
writeln(e)                  // Prints "MyEnum.Second"
writeln(e.value)            // Prints 10
 
writeln(e == MyEnum.Third)  // false
writeln(e == MyEnum.Second) // true
 
e = MyEnum.init             // Reset to the initial value
e = null                    // ditto

You can look up enum values based on their value:

var e = MyEnum[10]          // sets e to MyEnum.Second
writeln(MyEnum[21])         // prints MyEnum.Fifth

or based on the enum name:

writeln(MyEnum["Second"])   // prints MyEnum.Second
char[] s = "Fourth"
writeln(MyEnum[s].value)    // prints 20

You can also specify extra data fields to go with the enum values:

enum MyEnum : int field1 : float field2
{
  First : 12 : 34.5         // sets value=0, field1=12, field2=34.5
  Second = 10 : 20          // sets value=10, field1=20, field2=nan (default for floats)
  Thierd = 20 : 1 : 2       // value=20, field1=1, field2=2
}
...
e = MyEnum.First
writeln(e.field2)           // prints 34.5

Here's a practical example:

enum Error : char[] msg
{
  None     = 0 : "No error"
  ToBig    = 1 : "Value was to big"
  NoFile   = 10: "File not found"
  Unknown  = 99: "Unknown error"
}
...
Error e = getError()
if(e != Error.None)
  writeln("Error: ", e.msg)

Enum casting

Enum values can be implicitly cast to any of its field types. This includes the built-in fields of the enum name ("Enum.Name" of type char[]) and the .value property (of type long). Example:

enum Color : float[] rgb
{
  Red = 1  : [1.0, 0.0, 0.0]
  Green    : [0.0, 1.0, 0.0]
  Blue     : [0.0, 0.0, 1.0]
  Gray = 9 : [0.6, 0.6, 0.6]
}
...
Color c = Color.Green;
char[] str = c;             // becomes str="Color.Green"
int i = c;                  // becomes i=c.value, which is 2
float[] cval = c;           // becomes cval=[0,1,0]

The types are matched against the fields from left to right, with the name and the value selected first if they match.

Console / Interactive script execution

There's a new module for interactive execution of Monster code. The module executes one line of script at a time, and can also handle multi-line commands (such as code blocks). It's designed to be used for in-game consoles. To try it out, just execute the 'mvm' program without any arguments. Here's a sample session:

>>> 1+1
2
>>> int i = 50
>>> i*3
150
>>> {
   ... int k
   ... k = 12*i
   ... writeln(k)
   ... }
600
>>> k
Undefined identifier k
>>> exit

Here's how to set up a simple console in D (the C++ interface is under construction.)

import monster.modules.console;
...
// Setup code (run only once)
Console cn = new Console();
cn.addImports("module1", "module2"); // Add whatever Monster modules you want to import by default
cn.setPrompt("> ");                  // Set a custom propmt (optional)
cn.prompt();                         // Display the prompt before the user types something (also optional)
printText(cn.output());              // Display output

Replace the fictional 'printText' function with the function that prints text to your in-game console. Here's the update code:

// Update step (run each time the user enters a new line into the console)
char[] text = getUserText();         // Get the text from the console (replace with your own function)
cn.input(text);                      // Runs the code
cn.prompt();                         // Print the prompt on the next line
printText(cn.output());              // Display the results

The console will only execute the code once it has gotten a complete statement. If you eg. start a code block with { and don't terminate it, the prompt will change to '...' (by default), and the console will require more input lines before it runs the code. If you want to handle the prompt manually you can skip the call to prompt() and use the return value from input() to get the status.

Options file

There's a new file called monster/options.d, which can be used to set various compile time options for the library. The purpose of this file is to let each user customize the language and library to fit their needs.

Monster is intended as an embeddable library, and not as a general programming language. Because of this, it's possible to let the library - or even the language itself - be altered and adjusted for each individual project, according to the tastes and needs of the developer. This is a unique features that most other languages do not have (and in fact cannot have!), because they aim to provide one strictly defined standard that all implementations are compatible with. This is not the case for a purely embedded language, since there is seldom (if ever) any need to share code between individual projects.

Only a few options are available at the moment, but more are planned. The current options control settings such as:

  • which modules to load at startup (if any)
  • whether to use the system clock for sleep()
  • whether to include case insensitive string comparison operators (=i= and !=i=)

and a few more. Future options could include:

  • making the entire language case insensitive
  • strict vs loose typing (the language will support both by default)

More suggestions are welcome! You need to recompile the library for changes to take effect.

Virtual File System (VFS) abstraction

The VM now uses a (very thin) abstraction layer between itself and the file system. This allows you to easily read script files from archives or other data sources, or to hook Monster up with an existing VFS.

Many developers have their own self-developed file systems or archive readers, or they use existing ones such as PhysicsFS. The Monster VFS doesn't try to reinvent the wheel, instead it has a dead-simple interface to interface with existing code. To insert your own VFS, create a subclass of the class monster.modules.vfs.VFS and override the following two functions:

bool has(char[] name);            // Returns true if the file 'name' exists
bool hasDir(char[] dir);          // Returns true if the directory 'dir' exists
Stream open(char[] name);         // Open the given file and return it as a stream

Create as many VFS instances as you like and insert them with vm.addVFS(). (They're searched in the order of insertion.) To add file system paths, use vm.addPath() as before.

The C++ API is equivalent, with the class VFS having the two functions:

bool has(const char* file);
bool hasDir(const char* dir);
MSource *open(const char* file);

The MSource class is a simple wrapper around the stream or data source you want to use. The MStreamSource and MPtrSource classes are included for convenience:

MSource *src = new MStreamSource(cppStream);        // Read from a standard C++ std::istream
MSource *src = new MPtrSource(ptr, size);           // Read from a memory buffer
MSource *src = new MPtrSource("class M;int i");     // Read from a string

To read from any other source of data, simply create your own subclass of MSource (found in monster.h) and return it in VFS::open(). For example, the OpenMW project interfaces Monster with the OGRE graphics engine, meaning that Monster scripts are read automatically from ZIP files and other archives available through OGRE.

Even easier startup

You no longer need to call initAllModules(), the modules are loaded automatically at startup (according to your settings in options.d). The C++ function InitMonster() has also been made obsolete - the D runtime is now initialized automatically.

A complete D program to run a script now looks like:

import monster.monster;
void main()
{
    vm.run("myscript.mn");
}

and similarly in C++:

#include <monster.h>
using namespace Monster;
int main()
{
    vm.run("myscript.mn");
}

bindNew

Native class constructors were introduced in v0.10. A native constructor is a native (C/C++/D or other back-end language) function that is executed on new Monster objects upon creation. You bind native constructors with the 'bindConst' function in MonsterClass.

Monster 0.12 introduces a new kind of native constructor, which are bound with the 'bindNew' function, like this:

mc = vm.load("MyClass");
mc.bindNew(&myConstructor);

The difference between the two kinds of constructors, is that the 'bindConst' constructor is called on all newly created (or cloned) objects, while the 'bindNew' constructor is only called on objects created from script code - ie. using the 'new' or 'clone' keywords. This can be used to bind a native counterpart to the object in those cases. If both types of native constructors are present, the 'bindNew' constructor is executed first. (And both are executed before the script-defined constructor, if any.)

Explicit casting

You can now cast variables and values explicitly from one type to another. Explicit casting is useful for type conversions that are not (by default) done implicitly, such as converting floating point numbers to integers. The syntax is as follows:

float f = 3.12;
int i = int(f);         // Cast 'f' to int
writeln(i);             // prints '3'

Currently this is only useful for casting to integer types and for casting between classes (see the next point), but more will come later.

Implicit downcasting of classes

Upcasting class references is a natural part of polymorphic object oriented programming - upcasting means to convert implicitly from a derived class to a base class:

Parent p;               // Parent is the base class
Child c = new Child;    // Child is a derived class of Parent
p = c;                  // Normal upcasting

Downcasting means to convert the other way, from parent to child. This is useful in many cases, but unlike upcasting it is only possible if the object pointed to is of the right type. Monster allows implicit downcasting as well, but will fail if the object type is not correct:

Parent p = new Child1;
Child1 c1;
Child2 c2;
c1 = p;                 // Ok, p points to a Child1 object
c2 = p;                 // Error, the object pointed to by p is not derived from Child2

The last line in the above example will fail immediately and produce an error message when it is executed. You can turn off implicit downcasting completely (and only allow explicit downcasts, see previous point) in options.d.

Various bug fixes and improvements

Several bugfixes and cleanups have been implemented so far. Most important was an initialization bug which prevented the library from working on Windows with the DMD compiler. This gave the following cryptic error message:

Error: io:1: File must begin with a class or module statement, found module
Personal tools