Behaviour/Semantics

Operators

Precedence

Operators listed from highest to lowest precedence.
$Global access
$$Script global access
.Dot
~Bitwise complement
-Unary minus
!, notLogical not
*Multiplication
/Division
%Modular division
+Addition
-Subtraction
<<Bitwise shift left
>>Bitwise shift right
inif x in y, for x in y
<, ltLess than
>, gtGreater than
<=, leLess than or equal to
>=, geGreater than or equal to
==, eqEquality
!=, neInequality
&Bitwise and
^Bitwise xor
|Bitwise or
&&, andLogical and
xorLogical xor
||, orLogical or
• ? • : • Ternary conditional
=Assignment (right associative)
op=op-assignment, e.g. +=, *=, etc. (Right associative)

Arithmetical operators

OScript defines a standard set of arithmetical operators: +, -, *, /, and %. All five operations are defined on Integers and Longs, while modular division is not defined on Reals.

In addition to the numerical types, some non-numerical types also support addition and/or subtraction.

Addition and subtraction are also defined on Strings: addition concatenates two strings, while a - b removes the first instance of the value of b from a, e.g. "One fish, two fish" - "fish" == "One , two fish".

Lists can be added together using +.

Adding an Integer to a Date returns a new Date that many seconds forwards, while subtracting an Integer returns a new Date that many seconds back. In both cases, the Integer must be the right operand and the Date the left. One Date can also be subtracted from another to give the number of intervening seconds.

Second operand
First operand +IntegerLongRealStringDate
IntegerIntegerLongReal -- --
Long Long LongReal -- --
Real Real RealReal -- --
String -- -- -- String --
Date Date -- -- -- --
Second operand
First operand -IntegerLongRealStringDate
IntegerIntegerLongReal -- --
Long Long LongReal -- --
Real Real RealReal -- --
String -- -- -- String --
Date Date -- -- -- Integer
Second operand
First operand *, /IntegerLongRealStringDate
IntegerIntegerLongReal -- --
Long Long LongReal -- --
Real Real RealReal -- --
String -- -- -- -- --
Date -- -- -- -- --
Second operand
First operand %, <<, >>, &, |IntegerLongRealStringDate
IntegerIntegerLong -- -- --
Long Long Long -- -- --
Real -- -- -- -- --
String -- -- -- -- --
Date -- -- -- -- --

Adding and Subtracting strings

In addition to numerical values, strings can also be added and subtracted from each other. Adding two strings merely concatenates them:
String myString = 'This is a test.'
Echo( myString + ' Really.' ) //displays 'This is a test. Really.'
Subtraction, on the other hand, removes the first instance of the right operand from the left. If the right string is not a substring of the left, the left string is returned unchanged.
String myString = "This is a test."
Echo( myString - "is" )//displays "Th is a test."
Echo( myString - "is" - "is" )//displays "Th a test."

Binary operators

OScript has a standard set of operators for manipulating binary values:

<<Bitwise shift left
>>Bitwise shift right
&Bitwise and
^Bitwise xor
|Bitwise or
~Bitwise complement (unary)

All of these operators are defined only on Integers and Longs.

Left and right bitwise shift will truncate digits that are pushed beyond the scope of the data type. However, if the shift amount exceeds the number of bits that the type can store, the shift amount will be considered modulo that number of bits. I.e. if i is an integer, i << 66 == i << 2.

Boolean operators

OScript's Boolean and and or operators are short circuiting. Rather than returning Boolean values, most of them return the last value evaluated. Since almost all types can be coerced into Boolean values, this can lead to situations where pie = "steak" and "kidney" leaves pie with a value of "kidney", and "cake" or "death" evaluates to "cake" (fortunately for all those involved.) Alternatively, Boolean operators that must evaluate all arguments, such as xor or !, return Boolean values.

Indexes and slices

OScript 1-indexes sequential storage types, such as lists and strings. a[1] refers to the first element of a, a[2] the second, and so on. Negative indices refer to locations starting from the end of the sequence: a[-1] is the final element, a[-2] the second last, and so on. a[0] is always invalid.

One can obtain a subsequence of values by using slice notation: a[n:m] will return the subsequence a[n], a[n+1], a[n+2], …, a[m]. One can use negative indices in a slice, e.g. "Hello world"[2:-2] will evaluate to "ello worl". Omitting one end of the slice will extend the slice to either the beginning or end of the sequence: a[:n] returns the first $n$ elements of a, while a[n:] returns the nth element onward.

In some circumstances, slices can be assigned to. This will remove the destination slice from the sequence, and replace it with the value(s) being assigned.

The in operator

The in operator searches a list of a given value, and returns its index, if present, and 0 otherwise. Because 0 coerces to false, this can be used in a Boolean context to search for list membership.

The in operator performs no automatic coercions in its search—the expression 1.0 in { 1 } will evaluate to 0.

Comparison and Equality

Equality and inequality comparisons between most types are straight-forward: numeric types compare numerically, strings and other container types compare recursively by value. Even Undefined values behave unexpectedly. The exception is object pointer types, like DAPINode or CAPIConnect, which compare by pointer value.

Comparison operators (e.g. less than, greater than) behave as expected on numerical types. For strings, they perform a case-sensitive lexical ordering, while lists will compare each element in turn until they find one that compares greater or less than the other.

While comparing values of two different types, the result is undefined.

If you compare values of two different types, and a coercion is not possible it just compares the class numbers of the two types (as returned by the Type() function) instead.

Dot

The dot operator is used to look up fields on Assocs, Records, Frames and miscellaneous objects. If used without an initial operand, an implicit this is added in front.

The dot operator merely evaluates the value on its right, and looks up the corresponding field on the value on its left. If the value on the right is a valid identifier, it's treated as a string, i.e. a.key is equivalent to a."key". If you want to look up a key stored in a variable, one must wrap the expression in parentheses (a.(key)). Due to its high precedence, the same applies to using complex expressions as keys: a.("hello" + "world").

If one is using an integer or real value as a key, one must place a space after the dot, or wrap the number in parentheses. This is because a dot followed by one or more digits is tokenized as a real, rather than the dot operator followed by a number. Inserting the space, or wrapping the number in parentheses, removes this ambiguity.

Because of the implicit this, one can write unexpected commands. For instance, the code

. . . .a

is equivalent to

this.(this.(this.(this.a)))

—the value of this.a is evaluated, and used as a key to look up another value on this. This is repeated three times.

For information on how the value of this is defined, see the section below on this.

Globals

The $ and $$ operators are used to set or access global variables. $ is used to access globals that are visible to the entire application thread. Thread globals are not shared amongst threads. $$ globals are visible only to the current request execution context. Importantly, request globals do not persist through oscript callbacks invoked from the C layer, even within the same request.

Similar to the dot operator, the global access operators can be followed by either an identifier, which is interpreted as a string, or an arbitrary expression. The expression must, however, evaluate to a string. Due to its high precedence, wrapping the expression with parentheses is recommended.

Again like the dot operator, $ and $$ can be chained—$ $a will look up the global “a”, and if its value is a string, proceed to look up the global with that name.

Assignment

Assignment binds a value of its right-hand side to the identifier or expression on its left-hand side. The expression itself evaluates to the value assigned.

The operator-assignment operators (i.e. +=, -=, *=, |=, &= and \^\,=—note that /= is invalid) work in the following manner. Where ◊ is one of +, -, *, |, & or ^:

a ◊= b

is equivalent to

a = a ◊ b

Again, the value of the expression is the value of the assignment.

Ternary conditional

The ternary condition operator acts as it does in other languages, such as C: if the first operand evaluates to a true value, the second operand is evaluated and returned; otherwise, the third operand is evaluated and returned.

this and super

In addition to the keywords mentioned in the section on keywords, there are also two special identifiers: this and super. this refers to the current context that the script is running under. It can be any object—in addition to the obvious Objects, it could also be an Assoc, Frame or Record. If the script is invoked as a.b(), then while running b(), this will equal a. If a script value is evaluated as an expression before being invoked (either in the context of b(), or (a.b)()), then this will retain its current value.

The behaviour of super is somewhat more complicated. When a.b() is invoked, super is set to the parent of the Object on which b is actually defined (rather than just inherited). If Object has no parents then super will be undefined. Similarly, if a script value is returned as an expression and then invoked, super will be undefined.

If a call is to another function inside of the same script, the value of this and super are unchanged.

Example: Assume we have objects a, b and c, with a being the parent of b, and b the parent of c. A method, invoke is defined on b. The following table describes the values of this and super under different calling methods. Assume that all methods are being called from an independent object, d.

Callthissuper
b.invoke()ba
c.invoke()ca
(b.invoke)()dUndefined
This doesn't apply when the script is being called as (super.b)() - it ignores the parentheses. If you must do this, assign to an intermediate value first before calling.
super's behaviour is a great source of confusion. It's only used a few places in the CS code base, and its documentation in the Builder help is limited to a mention of it as a keyword. Currently super behaves like any other object reference (indeed, the super keyword just returns an object reference), and so sets the value of this correspondingly in the invoked method. This has the following consequences: By doing something like assigning super.method to an intermediate variable before invoking it, we get the following behaviour: This behaviour is counterintuitive to people used to class-based languages, but is consistent with the general behaviour of the dot operator in OScript.

Function calls

A function call is identified by a open parenthesis ('(') at the end of an expression, followed by an optional parameter list, and a close parenthesis (')'). A callable object can be a script, built-in, or another function defined within the same script.

OScript does not support tail recursion, and has a documented stack frame limit of 127.

Identifiers

An identifier can refer to a built-in, variable, function or constant.

The following flowchart shows how an identifier (either Basename or Basename.Feature) is resolved to one of the above.

Name resolution chart

Compiler Errors and Warnings

Both the old and new compilers issue various warnings. One goal for the new compiler is to issue a broader set of errors and warnings, guarding against risky behaviour, and code that is guaranteed to cause a run-time error if executed.

Syntax errors

Syntax errors are generated by the parser, and describe syntactic errors with the source code, such as missing close parenthesis, or an invalidly formed expression.

Unknown variable errors

The old compiler would automatically create an implicit Dynamic definitions for variables used as l-values without having been previously defined. The new compiler grants no such leniency, and requires that all variables be declared.

Type errors

The old compiler attempted to deduce the type of expressions, and detect potential run-time type exceptions at compile time. Unfortunately, it wasn't as efficient as it could be.

First, it only examined types on a handful of operators, allowing one to use an integer value as though it were an object (i.e. using it with the dot operator).

Second, for what operators it did examine, it neglected to consider various cases—if the left-hand operand was Dynamic (or indeterminate), it wouldn't check the right-hand operand's type to see if it was one that was ever compatible with the operator.

Third, it lacked the advantage that the new compiler had of being able to dictate that child objects may not change the type of features, or the interface of functions. With this change, it became possible to assign types to expressions where it was previously impossible.

Most type errors fall into two categories:

  1. Using a value in a situation where it would have to be coerced to an incompatible type. Examples of this situation include
  2. Using a value in an invalid context. Examples of this include

Function return errors

The new compiler will issue an error if a \texttt{return} statement without a value is present in a function with a non-Void return type, or if a value is present in a function with a Void return type.

It does not (yet) check to make sure that all code paths terminate with an appropriate return statement if the function is non-Void.

Variable use errors

The old compilers will detect unused variables, and issue warnings about them.

The new compiler does not yet support that functionality, but when implemented, should also warn about variables that are only written to, or read before having been written to.

Unreachable code

Neither compiler currently supports this, but warnings or errors should be issued for code that cannot be reached due to continue, break or return statements.

List comprehensions

List comprehensions are quick and easy ways to build a new list from an existing list, transforming and filtering it in the process. The list comprehension

userRecords = { UAPI.GetById( uapictx, userId ) for userId in userIds if userId > 0 }

is equivalent to

List tempList
for userId in userIds
	if userId > 0
		tempList = { @tempList, UAPI.GetById( uapictx, userId ) }
	end
end
userRecords = tempList

with the advantage of being an expression, rather than a statement, and being more efficient.

The condition of the list comprehension is optional; the rest of the parts are mandatory.