Embedded microcontrollers are amazing. For a few dollars you can have a postage stamp sized device with 500K of RAM, multiple megabytes of Flash ROM, Wifi, Bluetooth and lots of I/O. You can connect up displays for clocks and connected information terminals, add sensors to make your own weather station, ot hook up addressable LEDs to make your own holiday light show. The uses are endless.
A while back I decided to write some software to turn an ESP8266 into a general purpose computer. I added a command shell, accessible from the built-in serial port or WiFi and part of the Flash ROM is used as a solid-state disk. It has TCP and UDP networking and plenty of other library code to let you build lots of cool stuff.
I decided I wanted it to have a scripting language. This would make it simple to compile the script into bytecodes that are executed in an interpreter. This avoids having to upload native code and allows the scripts to be executed on a Mac for debugging. It’s a custom language that borrows from JavaScript, C and C++, and I call it M8RScript.
So why write a custom language rather than just port over an existing one? There are JavaScript, Python and Lua interpreters that already run on the ESP, for instance. There are a few reasons:
You can tune the language constructs to the platform. I originally wroter m8rscript for the ESP8266, but it can run and many other, similarly powerful chips.
Existing languages bring along baggage from the larger systems they were originally designed for. So many features are missing or hobbled.
- It’s really fun to write a new language. You can experiment with different constructs and ways of wrtiting the byte codes to maximize the efficiency of the system.
The ability to run m8rscript code on a Mac really makes debugging fast. A big computer has lots of resources to let you dig into the code to find issues and verify that new code works.
A Tour of M8RScript
M8rscript is an untyped language, supporting integer, float, boolean, string and object data types. Functions are first class objects and arrays are a type of object. Automatic casting is done to get types to match, using rules similar to those in JavaScript. Here’s a simple script that prints “Hello World” several times:
var i = 0;
while (i < 10) {
println("Hello World");
delay(1);
i++;
}
While similar to JavaScript in the fact that it’s untyped and supports similar data type and semantics, it is a vastly simplified version. First of all semicolons are not optional, every statement must end with one. Additionally there is no implicit variable or property creation. Every variable and property must be defined with a var statement before being used. All variables are block scoped. This and other simplifications were chosen to not only make the system more compact but also to take away some of the confusing semantics that plagues JavaScript.
Switch statements are also simplified. The break statement is not used in a switch. Each case of the switch executes exactly one statement (that statement can of course be a compound statement surrounded by braces). This is one of the experiments I’m doing. Switches that fall through have caused countless problems, so I’ve gotten rid of them.
var v = 3.5;
switch (v) {
case 4:
case 3.5: print("1\n");
default: print("2, ");
case "abc": print("3, ");
}
Because of its untyped nature switches can have case statements with any type.
The class data type is borrowed from C++. A class is a first class object, in fact it’s an Object with properties that are member variables or functions. The syntax used to create a class is simply syntactic sugar over the normal Object construction syntax.
class Circle
{
var _radius;
var PI = 3.14159;
constructor(radius) { _radius = radius; }
function area() { return PI * _radius * _radius; }
}
var myCircle = new Circle(6);
println("myCircle(6) area (s/b 113.09724) = " + myCircle.area());
Functions are also first class objects. They can be created with a name, in which case they are member functions of the parent Object. Or they can be anonomous in which case they can be assigned to a variable or passed as parameters. And lambdas or closures can be created.
function test(b) {
var c = 5;
return function(e) {
return b + c + e;
};
}
var f = test(3);
println("nested function with closure (s/b 15): " + f(7));
When test is called it returns a closure, which is a function that encapsulates the passed variables b, c and e.
For a more complete description of the language head over to the m8rscript Wiki and you can browse or clone the repository.
Leave a Reply