Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Lua

Programming in ObjectScript 1.5

5.00/5 (3 votes)
25 Mar 2013MIT22 min read 19.3K   125  
The ObjectScript is a new embedded programing language that mixes benefits of JavaScript, Lua, Ruby, Python and PHP. The ObjectScript has syntax from JavaScript, multiple results from Lua, sugar syntax from Ruby, magic methods from Python and much more.
Welcome to the ObjectScript 1.5 !

The ObjectScript is a new embedded programing language that mixes benefits of JavaScript, Lua, Ruby, Python and PHP. The ObjectScript has syntax from JavaScript, multiple results from Lua, sugar syntax from Ruby, magic methods from Python and much more.

Getting Started 

ObjectScript is a dynamically typed language. That means you do not have to specify the data type of a variable when you declare it, and data types are converted automatically as needed during script execution. So, for example, you could define a variable as follows:

C++
var a = 12;

And later, you could assign the same variable a string value, for example:

C++
a = "Hello World!";

Because ObjectScript is dynamically typed, this assignment does not cause an error message.

Case sensitivity

ObjectScript is case sensitive, null is not the same as Null, NULL, or any other variant. It is common to start the name of a class with a capitalised letter, and the name of a function or variable with a lower-case letter.

Whitespace and semicolons

Spaces, tabs and newlines used outside of string constants are called whitespace. Whitespace in ObjectScript source doesn't impact semantics. ObjectScript automatically detects statements, well formed statements will be considered complete (as if a semicolon were inserted between statements). Programmers can supply statement-terminating semicolons explicitly.

Use semicolons explicitly after return statement and before nested block to avoid unintended effects.

C++
return;
return a, b;
;{ // nested block
    var c = a; // c is block scoped variable
}

Comments

Comment syntax is the same as in C++ and many other languages.

C++
// a short, one-line comment

/* this is a long, multi-line comment
   about my script. May it one day
   be great. */

/* Comments /* may not be nested */ Syntax error */

Variables

Variables have no type attached, and any value can be stored in any variable. Variables are declared with a var statement, multiple variables can be declared at once. An identifier must start with a letter, underscore (_), or dollar sign ($); subsequent characters can also be digits (0-9) and the at sign (@). Variables are block scoped. Declared variable can be accessed by child blocks and functions. A variable value is null until it is initialized. Variables without declaration are environment variables (see Environments and the Global Environment).

C++
x = 0 // environment variable, because it is not declared using var statement
var function test(){ return 1 } // local function
function f() // environment function
{
  var z, r = 'foxes', 'birds' // initialized 2 local variables
  m = 'fish' // environment because it wasn't declared anywhere before
  var function child()  // local function
  {
     var r = 'monkeys'  // this variable is local and does not affect the "birds" r of the parent function
     z = 'penguins'     // the child function is able to access the variables of the parent function, this is called closure
  }
  ;{ // nested block
      twenty = 20       // environment variable, because it is not declared using var statement
      var twenty = twenty   // local variable initialized by environment variable
      var z = z         // initialize local z variable using z of parent
      z = 'bears'       // assign local z variable
      child()           // call child function of parent block
  }
  return x;             // we can use x here because it is environment
}
f()

When ObjectScript tries to resolve an identifier, it looks in the local block scope (the block scope could be a function). If this identifier is not found, it looks in the outer block that declared the local one, and so on along the scope chain until it reaches the environment scope where environment variables reside. If it is still not found, ObjectScript will use environment magic getter or setter methods (see Properties, Getters And Setters).

When assigning an identifier, ObjectScript does exactly the same process to retrieve this identifier, except that if it is not found in the environment scope, it will create the "variable" as a property of the environment object (see Environments and the Global Environment). As a consequence, a variable never declared will be environment if assigned. Declaring a variable (with the keyword var) in the environment code (i.e. outside of any function body) will declare a new local variable because of ObjectScript uses wrap function to execute code.

Note that you can forbid read an undeclared environment variable implementing magic __get method

C++
function __get(name){
    throw "read undeclared '${name}' variable"
}

You can implement autoload of classes (modules) using the same technique

C++
var checked = {}
function __get(name){
    if(!(name in checked)){
        checked[name] = true
        require(name)
        if(name in this){
            return this[name]
        }
    }
    throw "unknown class or global variable ${name}"
}

Function call syntax

When you call a function, you can pass along some values to it, these values are called arguments or parameters. These arguments can be used inside the function. You can send as many arguments as you like, separated by commas (,)

C++
printf("My name is %s, I'm %d age old", "Ivan", 19) // calls printf with 3 arguments

Declare the argument, as variables, when you declare the function

C++
function myFunction(var1, var2)
{
    return var1 + var2
}
print(myFunction(2, 3)) // outputs: 5

If the function call has no arguments, you must write an empty list () to indicate the call

C++
function test(){ print("Hello world!") }
test() // outputs: Hello world!

There are special cases to this rule. If the function has one single object argument, then the parentheses are optional

C++
print { firstname = "Ivan", lastname = "Petrov" }
// it's equivalent to
print({ firstname = "Ivan", lastname = "Petrov" })

If the function has one single argument (not object) and the function call is located just at top level of block statement (not in expression), then the parentheses are optional

C++
function test(a){
    print a // it's OK
    ;{
        print a // it's OK
    }
    // it's incorrect to call math.round with 'a' argument here, parentheses required
    // print(2 + math.round a)
    print(2 + math.round(a)) // it's OK
}
test 10 // it's OK

See Function

Magic constants

There are several magic constants in ObjectScript

__FILE__ - filename of the current file, __LINE__ - the current line number

C++
print("filename: ", __FILE__)
print("line: ", __LINE__)

Types and Values

ObjectScript recognizes the following types of values
  1. null, a special keyword denoting a null value
  2. boolean, either true or false
  3. number, such as 10, 0x123, 2.3, 5.7e23
  4. string, such as "Hello World!"
  5. object, such as {"one", "two", 12:"at index 12"}
  6. array, such as ["one", 31]
  7. function, such as function(){ print "Hello World!" }
  8. userdata, allows arbitrary C data to be stored in ObjectScript variables

null, boolean, number and string are primitive types.

object, array, function and userdata are not primitive types and could be used as named containers for values.

Null

Null is a type with a single value null. All variables have a null value by default, before a first assignment, and you can assign null to a variable to delete it. ObjectScript uses null as a kind of non-value.

If you want to check is a value is null, you should use === or !== operator, for example

C++
val = null
print(typeOf(val))  // outputs: null
print(val === null) // outputs: true
print(val === 0)    // outputs: false
print(val == 0)     // outputs: true

Boolean

The boolean type has two values, false and true, which represent the traditional boolean values. However, they do not hold a monopoly of condition values: In ObjectScript, any value may represent a condition. Conditionals (such as the ones in control structures) consider false, null and NaN (not a number) as false and anything else as true.

Beware that ObjectScript considers both zero and the empty string as true in conditional tests.

C++
print(typeOf(true))         // outputs: boolean
print(null ? true : false)  // outputs: false
print(1 > 2 ? true : false) // outputs: false
print(1 ? true : false)     // outputs: true
print(0 ? true : false)     // outputs: true
print("0" ? true : false)   // outputs: true
print("" ? true : false)    // outputs: true
print([1,2] ? true : false) // outputs: true

Number

You can write numeric constants with an optional decimal part, plus an optional decimal exponent. Examples of valid numeric constants are
C++
12      // decimal, base 10
3.14    // 3.14, floating-point number
3.14f   // 3.14, floating-point, C++ compatibility
2.5e3   // 2500, exponent format number
0xfe    // 254, hexadecimal, "hex" or base 16
0123    // 83, octal, base 8
0b110   // 6, binary, base 2

Numbers are represented in binary as IEEE-754 doubles (by default), which provides an accuracy nearly 16 significant digits. Because they are floating point numbers, they do not always exactly represent real numbers, including fractions

C++
printf("%.20f\n", 0.94 - 0.01)  // outputs: 0.93000000000000005000
printf("%f\n", 0.94 - 0.01)     // outputs: 0.930000

// use human friendly number format
printf("%n\n", 0.94 - 0.01)     // outputs: 0.93

// numbers are converted to string using human friendly format
print(0.94 - 0.01)              // outputs: 0.93

print(typeOf(10.5))             // outputs: number

Note: see printf, sprintf, print, echo functions.

A number has prototype of Number class

C++
print(10.prototype === Number) // outputs: true

Convert to number

Convert to number of primitive types

  • null is converted to 0
  • true is converted to 1
  • false is converted to 0
  • "12" is converted to 12
  • "0x12" is converted to 18
  • "0123" is converted to 83
  • "0b10" is converted to 2 
  • "10hello" is converted to 0
  • 12 value is a number already

all other types are converted to number calling valueOf method that must return value of a primitive type.

You can check number using numberOf function and convert to number using toNumber

C++
printf("        %9s %9s\n", 'numberOf', 'toNumber')
printf("---------------------------\n")
printf("  null: %9s %9s\n", numberOf(null), toNumber(null))
printf("  true: %9s %9s\n", numberOf(true), toNumber(true))
printf(" false: %9s %9s\n", numberOf(false), toNumber(false))
printf("  \"12\": %9s %9s\n", numberOf("12"), toNumber("12"))
printf("\"0x12\": %9s %9s\n", numberOf("0x12"), toNumber("0x12"))
printf("\"0123\": %9s %9s\n", numberOf("0123"), toNumber("0123"))
printf("\"0b10\": %9s %9s\n", numberOf("0b10"), toNumber("0b10"))
printf("\"12lo\": %9s %9s\n", numberOf("12lo"), toNumber("12lo"))
printf("    12: %9s %9s\n", numberOf(12), toNumber(12))

var obj = {
    valueOf = function(){
        return 10
    }
}
printf("   obj: %9s %9s\n", numberOf(obj), toNumber(obj))

var arr = [1,2,3]
printf(" array: %9s %9s\n", numberOf(arr), toNumber(arr))

outputs

C++
         numberOf  toNumber
---------------------------
  null:      null         0
  true:         1         1
 false:         0         0
  "12":        12        12
"0x12":        18        18
"0123":        83        83
"0b10":         2         2
"12lo":      null         0
    12:        12        12
   obj:      null        10
 array:      null         0

String

A string in ObjectScript is a sequence of characters. String is immutable, it means that you cannot change the object itself. All identical strings reference the same place in memory (including calculated at runtime). So when ObjectScript compares strings to equality, it compares only reference pointers.

C++
print(typeOf("0123456789"))     // outputs: string
print("0123456789".sub(2, 4))   // outputs: 2345

A string has prototype of String class

C++
print("".prototype === String) // outputs: true

A string literal can be specified in four different ways:

  • single quoted
  • double quoted
  • raw heredoc
  • heredoc

Single quoted

The simplest way to specify a string is to enclose it in single quotes (the character ').

To specify a literal single quote, escape it with a backslash (\). To specify a literal backslash, double it (\\). All other instances of backslash will be treated as a literal backslash: this means that the other escape sequences you might be used to, such as \r or \n, will be output literally as specified rather than having any special meaning.

C++
print 'this is a simple string'

// outputs: Arnold once said: "I'll be back"
print 'Arnold once said: "I\'ll be back"'

// outputs: You deleted C:\*.*?
print 'You deleted C:\\*.*?'

// outputs: You deleted C:\*.*?
print 'You deleted C:\*.*?'

// outputs: This will not expand: \n a newline
print 'This will not expand: \n a newline'

// outputs: Expressions do not ${expand} ${either}
print 'Expressions do not ${expand} ${either}'

Double quoted

If the string is enclosed in double-quotes ("), ObjectScript will interpret more escape sequences for special characters:

  • \n - linefeed (LF or 0x0A (10) in ASCII)
  • \r - carriage return (CR or 0x0D (13) in ASCII)
  • \t - horizontal tab (HT or 0x09 (9) in ASCII)
  • \" - double-quote
  • \\ - backslash
  • \$ - dollar sign
  • \[1-9]\d{0,2} - the sequence of characters matching the regular expression is a character in decimal notation
  • \0[0-7]{1,3} - the sequence of characters matching the regular expression is a character in octal notation
  • \x[0-9A-Fa-f]{1,2} - the sequence of characters matching the regular expression is a character in hexadecimal notation

As in single quoted strings, escaping any other character will result in the backslash being printed too.

The most important feature of double-quoted strings is usage of string expression executed within it.

Syntax of string expression

A string expression begins with ${ and ends with }. To specify a literal ${, escape it with a backslash \${.

C++
print "this is a string"

// outputs: three plus three is 6
print "three plus three is ${3+3}"

foobar = "blah"
// outputs: the value of foobar is blah
print "the value of foobar is ${foobar}"

// outputs: the value of foobar is ${foobar}
print "the value of foobar is \${foobar}"

function factorial(a){
    return a <= 1 ? 1 : a*factorial(a-1)
}
var p = 10
// outputs: factorial of 10 is 3628800
print "factorial of ${p} is ${factorial(p)}"

Raw heredoc

Raw heredoc begins with <<<MARKER' (<<< + MARKER + ' - single quote) and ends with MARKER. The string itself follows, and then the same marker again to close the string. The raw heredoc text behaves just like a single-quoted string. There is no parsing is done inside a raw heredoc. The construct is ideal for embedding code or other large blocks of text without the need for escaping. It shares some features in common with the SGML <![CDATA[ ]]> construct, in that it declares a block of text which is not for parsing.

The raw heredoc removes the first line with spaces and new line character. Also the one removes and last new line character if end marker is located at start line position.

C++
// outputs: this is a string
print <<<~~~'this is a string~~~

// outputs: true
print 'this is a string' == <<<~~~'
this is a string
~~~

print <<<==='

Harry Potter is due to start
his fifth year at Hogwarts School
of Witchcraft and Wizardry.

===

print 'end'

Heredoc

Heredoc begins with <<<MARKER" (<<< + MARKER + " - double quote) and ends with MARKER. The string itself follows, and then the same marker again to close the string. A heredoc is specified similarly to a raw heredoc, but parsing is done inside a heredoc. See syntax of string expression.

ObjectScript will interpret escape sequence \$ as $ (dollar sign).

Convert to string

Convert to string of primitive types

  • null is converted to "null" string
  • true is converted to "true" string
  • false is converted to "false" string
  • 0.3 is converted to "0.3" string, numbers are converted to string using human friendly number format %n (see printf function)
  • string value is a string already

all other types are converted to string calling valueOf method that must return value of a primitive type.

You can check string using stringOf function and convert to string using toString

C++
printf("        %9s %9s\n", 'stringOf', 'toString')
printf("---------------------------\n")
printf("  null: %9s %9s\n", stringOf(null), toString(null))
printf("  true: %9s %9s\n", stringOf(true), toString(true))
printf(" false: %9s %9s\n", stringOf(false), toString(false))
printf("  \"12\": %9s %9s\n", stringOf("12"), toString("12"))
printf("\"0x12\": %9s %9s\n", stringOf("0x12"), toString("0x12"))
printf("\"0123\": %9s %9s\n", stringOf("0123"), toString("0123"))
printf("\"0b10\": %9s %9s\n", stringOf("0b10"), toString("0b10"))
printf("    12: %9s %9s\n", stringOf(12), toString(12))

var obj = {
    valueOf = function(){
        return 10
    }
}
printf("   obj: %9s %9s\n", stringOf(obj), toString(obj))

var arr = [1,2,3]
printf(" array: %9s %9s\n", stringOf(arr), toString(arr))

outputs

C++
         stringOf  toString
---------------------------
  null:      null      null
  true:      true      true
 false:     false     false
  "12":        12        12
"0x12":      0x12      0x12
"0123":      0123      0123
"0b10":      0b10      0b10
    12:        12        12
   obj:      null        10
 array:      null   [1,2,3]

Note: string doesn't implement character access but you could implement it like this (see Properties, Getters And Setters)

C++
function String.__get(i){
    return this.sub(i, 1)
}

function String.__getdim(i, count){
    return this.sub(i, count)
}

var str = "0123456789"
print(str.sub(2, 4))    // outputs: 2345
print(str[2])           // outputs: 2
print(str[2, 3])        // outputs: 234

Object

An object is a list of zero or more pairs of property names and associated values

C++
{"one", "two", 12:"at index 12", ["user" .. "name"]: "at index username", some = "extended syntax"}

The simplest constructor of object is the empty constructor

C++
a = {}; // create an empty object and store its reference in a

You should not use an object literal { at the beginning of a statement. This will lead to not behave as you expect, because the { will be interpreted as the beginning of a block

C++
{
    var a = 12;
}

Lets view some examples

C++
days = {"Sunday", "Monday", "Tuesday", "Wednesday",
        "Thursday", "Friday", "Saturday"}

will initialize days[0] with the string "Sunday" (the first element has always index 0), days[1] with "Monday", and so on

C++
print(days[4])  // outputs: Thursday

Constructors do not need to use only constant expressions. You can use any kind of expression for the value

C++
tab = {sin(1), sin(2), sin(3), sin(4)}

To initialize an object to be used as a record, ObjectScript offers the following syntax

C++
a = {x=0, y=0}

it's equivalent to

C++
a = {x=0, y=0,}
a = {["x"]=0, ["y"]=0}
a = {x:0, y:0}
a = {x:0; y:0;}

so you can always put a comma after the last entry, you can use ":" or "=" to make pair of a property, you can always use a semicolon (;) instead of a comma (,)

C++
a = { one = "great", two = "awesome", 1 = "value at index 1" };
print(typeOf(a))        // outputs: object

b = "one";
a = { [b] = "great" };  // it's the same as: a = {}; a[b] = "great"
print(a[b])             // outputs: great
print(a["one"])         // outputs: great
print(a.one)            // outputs: great

a = { one = "great" };  // it's the same as: a = { "one": "great" }
print(a[b])             // outputs: great
print(a["one"])         // outputs: great
print(a.one)            // outputs: great

a = { one = "great", two = "awesome" };
a = { one: "great", two: "awesome" }; // JavaScript object notation is fully supported
a = { one: "great", two: "awesome", }; // a comma after the last entry is valid

a = {"zero", "one", "two"}
print(a) // outputs: {0:"zero", 1:"one", 2:"two"}
delete a[1]
print(a) // outputs: {0:"zero", 2:"two"}

See Object-oriented programming (OOP)

An object has prototype of Object class

C++
print({}.prototype === Object) // outputs: true

Array

An array is a list of indexed values

C++
a = ["zero", "one", "two"]
print(typeOf(a))    // outputs: array
print(a.length)     // outputs: 3
print(a[1])         // outputs: one
delete a[1]
print(a.length)     // outputs: 2
print(a[1])         // outputs: two

The array is very compact structure so if you don't need a container of properties it's a good reason to use array instead of object.

An array has prototype of Array class

C++
print([].prototype === Array) // outputs: true

Function

You can think of functions as procedures that your application can perform

C++
var max = function(a, b){ return a > b ? a : b }
print(max(10, 3)) // outputs: 10

Functions are first-class values in ObjectScript. That means that functions can be stored in variables, passed as arguments to other functions, and returned as results.

An unconventional, but quite convenient feature of ObjectScript is that functions may return multiple results

C++
var func = function(){ return 1, 2 }
var x, y = func()
print x // outputs: 1
print y // outputs: 2

The functions are objects themselves. As such, they have properties and methods. Any reference to a function allows it to be invoked using the () operator

C++
var func = function(){ print arguments }
var func2 = function(f){ f.apply(null, ...) }
func2(func,1,2,3)   // outputs: [1,2,3]

Three dots (...) is rest arguments, it returns array of undeclared arguments passed to function, arguments returns array of all arguments passed to function.

Moreover, ObjectScript supports nested functions and closures. The nested functions are functions defined within another function. They are created each time the outer function is invoked. In addition to that, each created function forms a lexical closure: the lexical scope of the outer function, including any local variables and arguments, become part of the internal state of each nested function object, even after execution of the outer function concludes

C++
var a = function(){
    var x = 2
    return function(){ retutn x++ }
}
var b = a()
print b() // outputs: 2
print b() // outputs: 3
print b() // outputs: 4

Function returns the last expression evaluated

C++
print({|a, b| a + b}(2, 3)) // outputs: 5

A function has prototype of Function class

C++
print((function(){}).prototype === Function) // outputs: true

Chain function calls

C++
var obj = {
    do = function(a){
        print a;
        return this;
    }
}
obj.do{
    1: "one"
}.do{
    2: "two"
}.do{
    3: "three"
}

outputs

C++
{1:"one"}
{2:"two"}
{3:"three"}

Standard functions variables

Any function contains following local variables

  • this - method can access the instance variables through the this variable
  • _F - alias of the current function
  • _E - function environment (see Global Variables)
  • _G - global variables (see Global Variables)

Alternative function syntax

You can declare function using {|params|body} syntax, for example

C++
3.times{|i| print i}

outputs

C++
0
1
2

How could it work in ObjectScript?

First of all let's declare times method for numbers. The numbers have Number prototype so we should add new method for the Number prototype

C++
function Number.times(func){ // it's equal to Number.times = function(func) ...
    for(var i = 0; i < this; i++){
        func(i)
    }
}

Everything is done, let's use it

C++
5.times(function(i){ print i })

or just use sugar syntax

C++
3.times{|i| print i}

One more example

C++
print "factorial(20) = " .. {|a| a <= 1 ? 1 : a*_F(a-1)}(20)

outputs

C++
factorial(20) = 2432902008176640000

Function in C++

ObjectScript can call functions written in ObjectScript and functions written in C++ or C. All the standard library in ObjectScript is written in C++. Application programs may define other functions in C

C++
#include "objectscript.h"

int test(OS * os, int, int, int, void*)
{
    os->pushNumber(123);
    return 1; // number of values that function returns
}

int main()
{
    OS * os = OS::create();     // create OS instance
    os->pushCFunction(test);
    os->setGlobal("test");
    os->eval("print(test())");  // outputs: 123
    os->release();              // reelease OS instance
    return 0;
}

There is also binder included in ObjectScript so you can declare C++ functions like this

C++
#include "objectscript.h"
#include "os-binder.h"

float test(float a, float b){ return a + b; }

int main()
{
    OS * os = OS::create();
    os->setGlobal(def("test", test));   // use binder
    os->eval("print(test(2, 3))");      // outputs: 5
    os->release();
    return 0;
}

See osbind MSVC example project and more examples included in repository.

Userdata

An userdata allows arbitrary C++ or C data to be stored in ObjectScript variables. It's used to represent new types created by an application program or a library written in C++ or C.

Operators

ObjectScript has the following types of operators

  1. Assignment operator
  2. Comparison operators
  3. Arithmetic operators
  4. Bitwise operators
  5. Logical operators
  6. String operator
  7. Special operators

Assignment and Multiple assignment operator

Assignment is the basic means of changing the value of a variable or a object property:

C++
a = "Hello" .. " world!"
t.n = t.n + 1
a = b = 2

ObjectScript allows multiple assignment, where a list of values is assigned to a list of variables in one step. Both lists have their elements separated by commas

C++
a, b = 1, 2

the variable a gets the value 1 and b gets 2. In a multiple assignment, ObjectScript first evaluates all values and only then executes the assignments. Therefore, you can use a multiple assignment to swap two values, as in

C++
x, y = y, x                // swap x for y
a[i], a[j] = a[j], a[i]    // swap a[i] for a[j]

ObjectScript always adjusts the number of values to the number of variables. When the list of values is shorter than the list of variables, the extra variables receive null as their values, when the list of values is longer, the extra values are silently discarded

C++
a, b, c = 0, 1
print(a, b, c)         // 0   1   null
a, b = a+1, b+1, b+2   // value of b+2 is ignored
print(a,b)             // 1   2
a, b, c = 0
print(a,b,c)           // 0   null   null

The last assignment in the above example shows a common mistake. To initialize a set of variables, you must provide a value for each one 

C++
a, b, c = 0, 0, 0
print(a, b, c)           // 0   0   0

You can use multiple assignment simply to write several assignments in one line. But often you really need multiple assignment, for example, to swap two values. A more frequent use is to collect multiple returns from function calls

C++
a, b = f()

f() returns two results: a gets the first and b gets the second.

To initialize a set of variables by one single value, use following syntax

C++
a = b = c = 0

Comparison operators

=== is exactly equal to (value and type)

C++
print(2 === "5") // outputs: false
print(2 === "2") // outputs: false
print(2 === 2) // outputs: true

!== is not exactly equal to (value and type)

C++
print(2 !== "5") // outputs: true
print(2 !== "2") // outputs: true
print(2 !== 2) // outputs: false

All other comparison operators use fast implementation for following types: null, boolean, number. In this case arguments are converted to numbers (null converts to 0, true to 1 and false to 0) and the ones are used to make real comparison (__cmp method is not used).

If one of arguments is not one of the types: null, boolean, number then the comparison operator process result of __cmp method of specification

C++
function __cmp(b){
    // returns negative number if this < b
    // returns positive number if this > b
    // returns 0 if this == b
}

<=> compare arguments using the specification of the __cmp method

C++
print(2 <=> 3) // outputs: -1
print(2 <=> 2) // outputs: 0

== is equal to

C++
print(2 == 3) // outputs: false
print(2 == 2) // outputs: true

!= is not equal to

C++
print(2 != 3) // outputs: true
print(2 != 2) // outputs: false

> is greater than

C++
print(2 > 3) // outputs: false
print(2 > 2) // outputs: false

< is less than

C++
print(2 < 3) // outputs: true
print(2 < 2) // outputs: false

>= is greater than or equal to

C++
print(2 >= 3) // outputs: false
print(2 >= 2) // outputs: true

<= is less than or equal to

C++
print(2 <= 3) // outputs: true
print(2 <= 2) // outputs: true

Arithmetic operators

All arithmetic operators use fast implementation for following types: null, boolean, number. In this case arguments are converted to numbers (null converts to 0, true to 1 and false to 0) and the ones are used to make arithmetic operation (magic method are not used).

If one of arguments is not type of: null, boolean, number then the arithmetic operation returns result of magic method called.

+ addition or call magic __add method

C++
print(2 + 3) // outputs: 5

- subtraction or call magic __sub method

C++
print(2 - 3) // outputs: -1

* multiplication or call magic __mul method

C++
print(2 * 3) // outputs: 6

/ division or call magic __div method

C++
print(2 / 4) // outputs: 0.5

% modulus (division remainder) or call magic __mod method

C++
print(5 % 2) // outputs: 1

** raises the first argument to the power of the second argument or call magic __pow method

C++
print(25 ** 0.5) // outputs: 5

unary + doesn't change the sign of number parameter or call magic __plus method

C++
print(+(5))     // outputs: 5
print(+(-5))    // outputs: -5

unary - changes the sign of number parameter or call magic __neg method

C++
print(-(+5))    // outputs: -5
print(-(-5))    // outputs: 5

++ increments (this operator is allowed only for local variables)

C++
var a = 5
print(++a)      // expanded to (a = a + 1), outputs: 6
print(a)        // outputs: 6
print(a++)      // expanded to (temp = a; a = a + 1; temp), outputs: 6
print(a)        // outputs: 7

-- decrements (this operator is allowed only for local variables)

C++
var a = 5
print(--a)      // outputs: 4
print(a)        // outputs: 4
print(a--)      // outputs: 4
print(a)        // outputs: 3

Note: there is no + operator for string but you could implement + as concatenation for example

C++
function String.__add(b){
    return this .. b
}

print "foo" + 5 // outputs: foo5

Bitwise operators

All bitwise operators use fast implementation for following types: null, boolean, number. In this case arguments are converted to numbers (null converts to 0, true to 1 and false to 0) and the ones are used to make bitwise operation (magic method are not used).

If one of arguments is not type of: null, boolean, number then the bitwise operation returns result of magic method called.

& is bitwise AND, returns a one in each bit position for which the corresponding bits of both operands are ones (or call magic __bitand method)

C++
a = 6
b = 3
print(a & b) // outputs: 2

| is bitwise OR, returns a one in each bit position for which the corresponding bits of either or both operands are ones (or call magic __bitor method)

C++
a = 6
b = 3
print(a | b) // outputs: 7

^ is bitwise XOR, returns a one in each bit position for which the corresponding bits of either but not both operands are ones (or call magic __bitxor method)

C++
a = 6
b = 3
print(a ^ b) // outputs: 5

unary ~ is bitwise NOT, inverts the bits of its operand (or call magic __bitnot method)

C++
a = 6
print(~a) // outputs: -7

<< is left SHIFT, shifts a in binary representation b bits to the left, shifting in zeros from the right (or call magic __lshift method)

C++
a = 6
b = 1
print(a << b) // outputs: 12

>> is right SHIFT, shifts a in binary representation b bits to the right, discarding bits shifted off (or call magic __rshift method)

C++
a = 6
b = 1
print(a >> b) // outputs: 3

Logical operators

&& is logical AND, returns a if it can be converted to false, otherwise, returns b. Thus, when used with boolean values, && returns true if both operands are true, otherwise, returns false.

C++
a = 6
b = null
print(a && b) // outputs: null

|| is logical OR, returns a if it can be converted to true, otherwise, returns b. Thus, when used with boolean values, || returns true if either operand is true, if both are false, returns false.

C++
a = 6
b = null
print(a || b) // outputs: 6

?: is conditional operator, returns a if conditional can be converted to true, otherwise, returns b.

C++
a = 6
b = 3
print(a < b ? a : b) // outputs: 3

unary ! is logical NOT, returns false if its single operand can be converted to true, otherwise, returns true.

C++
a = 6
print(!a)     // outputs: false

Note: use !! to convert value to boolean type.

String operator

.. is string concatenation (see Convert to string)

C++
a = 5
b = true
print(a .. b) // outputs: 5true

Special operators

in retuns a boolean value that depends on second argument type.

If the second argument is object then the in returns true if the specified property is in the specified object.

If the second argument is array then the in returns true if the specified value is contained in the specified array.

C++
var a = {1: "one", second: "two"}
print(1 in a)           // outputs: true
print("second" in a)    // outputs: true
print("one" in a)       // outputs: false

var b = [1, "one", "two"]
print(1 in b)           // outputs: true
print("second" in b)    // outputs: false
print("one" in b)       // outputs: true

Actually this operator returns result of a global __in function.

extends operator extends a one object by another. Usually it's used for Object Oriented Programming.

C++
var IvanPerson = extends Person {
    __construct: function(){
        super("Ivan", "Petrov")
    }
}

Actually this operator returns result of a global __extends function

C++
function __extends(obj, obj2){
    obj2.prototype = obj
    return obj2
}

See Object-oriented programming (OOP).

delete deletes property or array index (see Properties, Getters And Setters)

C++
a = [1, 2, 3, 4, 5]
delete a[1]
print(a)    // outputs: [1,3,4,5]

a = {one=1, two=2]
delete a.one
print(a)    // outputs: {"two":2}

Actually this operator calls a global __delete function.

is returns a boolean value that indicates if an object is an instance of a particular class.

C++
classA = {}
classB = extends classA {}
a = classA()        // create new inctance of classA
b = classB()        // create new inctance of classB
print(a is classA)  // outputs: true
print(b is classA)  // outputs: true
print(a is classB)  // outputs: false
print(b is classB)  // outputs: true
print(classA is classA)     // outputs: false
print(classB is classA)     // outputs: true
print(classA is classB)     // outputs: false
print(classB is classB)     // outputs: false

Actually this operator returns result of a global __is function.

isprototypeof returns a boolean value that indicates if an object is a particular class or an instance of a particular class.

C++
classA = {}
classB = extends classA {}
a = classA()        // create new inctance of classA
b = classB()        // create new inctance of classB
print(a isprototypeof classA)   // outputs: true
print(b isprototypeof classA)   // outputs: true
print(a isprototypeof classB)   // outputs: false
print(b isprototypeof classB)   // outputs: true
print(classA isprototypeof classA)      // outputs: true
print(classB isprototypeof classA)      // outputs: true
print(classA isprototypeof classB)      // outputs: false
print(classB isprototypeof classB)      // outputs: true

Actually this operator returns result of a global __isprototypeof function.

unary # returns result of magic __len method. The __len method is already declared for following types

  • array - returns number of elements
  • string - returns length of string in bytes
  • object - returns number of properties

for example

C++
a = "abc"
print(#a)   // outputs: 3
a = [1, 2, 3, 4, 5]
print(#a)   // outputs: 5

There is also length property in ObjectScript

C++
function Object.__get@length(){ return #this }

So you can use length property as alias to # operator

C++
a = "abc"
print(a.length)     // outputs: 3
a = [1, 2, 3, 4, 5]
print(a.length)     // outputs: 5

... is rest arguments, returns array of undeclared arguments passed to function

C++
function test(a, b){
    print(...)
    print(arguments)
}

test(1, 2, 3, 4, 5)

outputs

C++
[3,4,5]
[1,2,3,4,5]

Global Variables

Global variables do not need declarations. You simply assign a value to a global variable to create it. It is not an error to access a non-initialized variable, you just get the special value null as the result

C++
print(a)  // outputs: null
a = 10
print(a)  // outputs: 10

Usually you do not need to delete global variables, if your variable is going to have a short life, you should use a local variable. But, if you need to delete a global variable, just assign null to it

C++
a = 10
a = null
print(a)  // outputs: null

Environments and the Global Environment

Any reference to a global name v is syntactically translated to _E.v. Moreover, every function is compiled in the scope of an external local variable called _E, so _E itself is never a global name in a function.

Any object used as the value of _E is called an environment.

ObjectScript keeps a distinguished environment called the global environment. This value is kept at a special object in the C registry. In ObjectScript, the variable _G is initialized with this same value.

When ObjectScript compiles a function, it initializes the value of its _E upvalue with the global environment. Therefore, by default, global variables in ObjectScript code refer to entries in the global environment. Moreover, all standard libraries are loaded in the global environment and several functions there operate on that environment.

You can execute any function with a different environment using Function.applyEnv method. For example let's view code of evalEnv functions inside of std.os file:

C++
function evalEnv(str, env){
    return compileText(str).applyEnv(env || _G, null, ...)
}

If ... Else Statements

Conditional statements are used to perform different actions based on different conditions.

Very often when you write code, you want to perform different actions for different decisions. You can use conditional statements in your code to do this.

There are the following conditional statements in ObjectScript:

if statement - use this statement to execute some code only if a specified condition is true

C++
a = 2
b = 5
if(a < b){ print 'true' }   // outputs: true

if...else statement - use this statement to execute some code if the condition is true and another code if the condition is false

C++
a = 7
b = 5
if(a < b){
    print 'true'
}else{
    print 'false'
}
// outputs: false

if...elseif....else statement - use this statement to select one of many blocks of code to be executed

C++
a = 5
b = 5
if(a < b){
    print 'true'
}elseif(a == b){
    print 'equal'
}else{
    print 'false'
}
// outputs: equal

Note: you can use else if instead of elseif if you like.

For loop

There are two kind of for loop in ObjectScript. the first one is the same to C++ and some other languages

C++
for(var i = 0; i < 3; i++){
    print i
}

outputs

C++
0
1
2

The second one is used for iteration process

C++
for(var i, v in ["zero", "one", "two"]){
    print("${i}: ${v}")
}

outputs

C++
0: zero
1: one
2: two

See Iterators.

Break and Continue

The break statement "jumps out" of a loop.

C++
for(var i = 0; i < 10; i++){
    print i
    if(i == 3) break
}

outputs

C++
0
1
2
3

The continue statement "jumps over" one iteration in the loop.

C++
for(var i = 0; i < 5; i++){
    if(i % 2 == 0) continue
    print i
}

outputs

C++
1
3

Try, Catch, Throw

The try statement lets you to test a block of code for errors.

The catch statement lets you handle the error.

The throw statement lets you create custom errors.

C++
function printException(e){
    print e.message
    for(var i, t in e.trace){
        printf("#${i} ${t.file}%s: %s, args: ${t.arguments}\n",
            t.line > 0 ? "(${t.line},${t.pos})" : "",
            t.object && t.object !== _G ? "<${typeOf(t.object)}#${t.object.id}>.${t.name}" : t.name)
    }
}

function testFunction(){
    try{
        var a = 1
        var b = 0
        var c = a / b
    }catch(e){
        print "\ncatch exception #1"
        printException(e)
        throw "error message"
    }
}

try{
    testFunction()
}catch(e){
    print "\ncatch exception #2"
    printException(e)
}

outputs

C++
catch exception #1
division by zero
#0 ../../examples-os/try.os(14,13): testFunction, args: {}
#1 ../../examples-os/try.os(23,14): {{main}}, args: {}
#2 {{CORE}}: require, args: {"filename":"../../examples-os/try.os","required":true,"source_code_type":0,"check_utf8_bom":true}

catch exception #2
error message
#0 ../../examples-os/try.os(18,3): testFunction, args: {}
#1 ../../examples-os/try.os(23,14): {{main}}, args: {}
#2 {{CORE}}: require, args: {"filename":"../../examples-os/try.os","required":true,"source_code_type":0,"check_utf8_bom":true}

Iterators

An iterator allows you to iterate over the elements of a collection. For example

C++
a = { null, true, 12, "0" }
for(k, v in a){
    print("${k} --> ${v}")
}

outputs

C++
0 --> null
1 --> true
2 --> 12
3 --> 0

ObjectScript compiles the above program to

C++
a = { null, true, 12, "0" }
{
    var iter_func = a.__iter();
    for(var iter_valid;;){ // infinite loop
        iter_valid, k, v = iter_func();
        if(!iter_valid) break;
        print("${k} --> ${v}")
    }
}

The first result value of iter_func indicates either valid or not valid current step, second and other return values are user used values.

Any iterator needs to keep some state between successive calls, so that it knows where it is and how to proceed from there. Closures provide an excellent mechanism for that task. Remember that a closure is a function that accesses local variables from its enclosing function.

For example, array iterator is declated as

C++
Array.__iter = function(){
    var i, self = 0, this
    return function(){
        if(i < #self){
            return true, i, self[i++]
        }
    }
}

Note that iterator of function is itself. It's declared as

C++
Function.__iter = function(){ return this }

So we can write iterator like that

C++
var range = function(a, b){
    return function(){
        if(a <= b){
            return true, a++
        }
    }
}

for(var i in range(10, 13)){
    print( "i = ", i )
}

outputs

C++
i = 10
i = 11
i = 12
i = 13

another example

C++
Range = {
    __construct = function(a, b){
        if(b){
            this.a, this.b = a, b
        }else{
            this.a, this.b = 0, a - 1
        }
    },
    __iter = function(){
        var a, b = this.a, this.b
        return a <= b
            ? {|| a <= b && return true, a++ }
            : {|| a >= b && return true, a-- }
    },
}

for(var i in Range(-2, -4))
    print i

outputs

C++
-2
-3
-4

and one more example

C++
function Number.to(b){
    return Range(this, b)
}

for(var i in 5.to(7))
    print i

outputs

C++
5
6
7

Properties, Getters And Setters

A getter is a method that gets the value of a specific property. A setter is a method that sets the value of a specific property.

C++
a = {
    __color: "red",
    __get@color = function(){ return this.__color },
    __set@color = function(v){ this.__color = v },
}

print a.color       // outputs: red
a.color = "blue"
print a.color       // outputs: blue

Note that @ is not a special symbol, any functions and variables can contain @.

Another example

C++
a = {
    __color: "red",
    __get = function(name){
        if(name == "color")
            return this.__color
    },
    __set = function(name, v){
        if(name == "color")
            this.__color = v
    },
    __del = function(name){
        if(name == "color")
            delete this.__color
    },
}

print a.color       // outputs: red
a.color = "blue"
print a.color       // outputs: blue
delete a.color
print a.color       // outputs: null

Multi dimensional properties

ObjectScript supports a.color (or a["color"]) syntax. What about?

C++
a[x, y] = 12

Yes, ObjectScript supports the above syntax, it's called multi dimensional properties

C++
a = {
    __matrix = {},
    __getdim = function(x, y){
        return this.__matrix[y*4 + x]
    },
    __setdim = function(value, x, y){
        this.__matrix[y*4 + x] = value
    },
    __deldim = function(x, y){
        delete this.__matrix[y*4 + x]
    },
}
// it's compiled to a.__setdim(5, 1, 2)
a[1, 2] = 5
// it's compiled to print(a.__getdim(1, 2)
print a[1, 2]                               // outputs: 5
// it's compiled to a.__deldim(1, 2)
delete a[1, 2]
print a[1, 2]                               // outputs: null

Note that __setdim method receives the first argument as new property value and other arguments as dimensional attributes of property. For example

C++
a = {
    __matrix = {},
    __getdim = function(x, y, z){
        return this.__matrix[z*16 + y*4 + x]
    },
    __setdim = function(value, x, y, z){
        this.__matrix[z*16 + y*4 + x] = value
    },
}
a[1, 2, 3] = 5
print a[1, 2, 3] // outputs: 5

Empty dimensional properties

What about?

C++
b = a[]
a[] = 2
delete a[]

ObjectScript provides following special methods __getempty, __setempty and __delempty that you can use if necessary.

Object-oriented programming (OOP)

Object-oriented programming is a programming paradigm that uses abstraction to create models based on the real world. It uses several techniques and paradigms, including modularity, polymorphism, and encapsulation.

ObjectScript is OOP language, also you can override arithmetic, bitwise, concatenation, comparison and unary operators.

Core Objects

ObjectScript has several objects included in its core, there are global variables named Object, Array, String, Number, Boolean, Function and Userdata.

Every object in ObjectScript is an instance of the Object and therefore inherits all its properties and methods.

Custom Objects

C++
var a = {
    num: 1,
    __get@number: function(){
        return this.num
    },
    __add = function(b){
        return this.number + b.number
    },
}

var b = extends a {
    num: 2,
}

print a + b     // outputs: 3

The Class

ObjectScript is a prototype-based language which contains no class statement. Any object could be used as class.

C++
Person = {
    __construct = function(firstname, lastname){
        this.firstname = firstname
        this.lastname = lastname
    },
    __get@fullname = function(){
        return this.firstname .. " " .. this.lastname
    },
    walk = function(){
        print this.fullname .. " is walking!"
    },
}

The Object (Class Instance)

To create a new instance of an object we use () operator for class variable

C++
var p = Person("James", "Bond")

is equivalent to

C++
var p = {}
p.prototype = Person
p.__construct("James", "Bond")

run

C++
p.walk()    // outputs: James Bond is walking!
print p     // outputs: {"firstname":"James","lastname":"Bond"}

Inheritance

Inheritance is a way to create a class as a specialized version of class. You need to use extends operator

C++
var IvanPerson = extends Person {
    __construct: function(){
        super("Ivan", "Petrov")
    },
}
var p = IvanPerson()
p.walk()    // outputs: Ivan Petrov is walking!
print p     // outputs: {"firstname":"Ivan","lastname":"Petrov"}

the extends operator has syntax extends exp1 exp2 where exp1 and exp2 are any valid expressions, it's equivalent to

C++
(function(exp1, exp2){
    exp2.prototype = exp1
    return exp2
})()

Encapsulation

In the previous example, IvanPerson does not need to know how the Person class's walk() method is implemented, but still can use that method. The IvanPerson class doesn't need to explicitly define that method unless we want to change it. This is called encapsulation, by which every class inherits the methods of its parent and only needs to define things it wishes to change.

Default instance properties

There is special property named __object in ObjectScript to describe default instance properties

C++
Test = {
    __object = {
        a = 0,
        b = [1, 2],
    }
}

Test2 = extends Test {
    __object = {
        a = 12345
    }
}

var v1 = Test()         // create new instance of Test class
v1.b[0] = 10
var v2 = Test2()        // create new instance of Test2 class
print "v1: "..v1        // outputs: v1: {"a":0,"b":[10,2]}
print "v2: "..v2        // outputs: v2: {"a":12345,"b":[1,2]}
print "class: "..Test   // outputs: class: {"__object":{"a":0,"b":[1,2]}}

Alternative 'this' syntax

There is alternative syntax in ObjectScript to read and write instance properties

C++
__construct = function(firstname, lastname){
    @firstname = firstname  // it's the same this.firstname = firstname
    @lastname = lastname    // it's the same this.lastname = lastname
}

Simple OOP example

C++
var vec3 = {
    __construct = function(x, y, z){
        @z, @y, @x = z, y, x
    },
    __add = function(b){
        vec3(@x + b.x, @y + b.y, @z + b.z)
    },
    __mul = function(b){
        vec3(@x * b.x, @y * b.y, @z * b.z)
    },
}

var v1 = vec3(10, 20, 30)
var v2 = vec3(1, 2, 3)
var v3 = v1 + v2 * v2
print v3

outputs

C++
{"x":11,"y":24,"z":39}

Compiling ObjectScript from source code

Clone repo

C++
git clone git://github.com/unitpoint/objectscript.git
cd objectscript

Compile ObjectScript for Linux

C++
mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=$(pwd)/../ ..
make
make install

The result files (os, oscript) will be located inside of bin folder. Run ObjectScript program

C++
oscript script.os

Note: ObjectScript uses PCRE (perl-compatible regular expression library) and cURL (client-side URL transfer library) libraries installed externally.

Compile ObjectScript for Windows

Open proj.win32\examples.sln, there are helpful projects in MSVC solution.

Resources

ObjectScript is universal scripting language, there are no compromises any more.

Thank you for your reading. Looking forward to your reply. 

Evgeniy Golovin   

License

This article, along with any associated source code and files, is licensed under The MIT License