Changelog
From Monster Wiki
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
