$ | Global access |
$$ | Script global access |
. | Dot |
~ | Bitwise complement |
- | Unary minus |
!, not | Logical not |
* | Multiplication |
/ | Division |
% | Modular division |
+ | Addition |
- | Subtraction |
<< | Bitwise shift left |
>> | Bitwise shift right |
in | if x in y , for x in y |
<, lt | Less than |
>, gt | Greater than |
<=, le | Less than or equal to |
>=, ge | Greater than or equal to |
==, eq | Equality |
!=, ne | Inequality |
& | Bitwise and |
^ | Bitwise xor |
| | Bitwise or |
&&, and | Logical and |
xor | Logical xor |
||, or | Logical or |
• ? • : • | Ternary conditional |
= | Assignment (right associative) |
op= | op-assignment, e.g. += , *= , etc. (Right associative) |
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.
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
String myString = 'This is a test.'
Echo( myString + ' Really.' ) //displays 'This is a test. Really.'
String myString = "This is a test."
Echo( myString - "is" )//displays "Th is a test."
Echo( myString - "is" - "is" )//displays "Th a test."
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
.
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.
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.
in
operatorThe 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.
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.
Type()
function) instead.
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.
The $
and $$
operators are used to set or access global variables. $
is used to access globals that are visible to the entire application, while $$
globals are visible only to the current thread. 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 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.
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
.
Call | this | super |
---|---|---|
b.invoke() | b | a |
c.invoke() | c | a |
(b.invoke)() | d | Undefined |
(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:
super.method
will be set on super
.super.method
invokes any methods, they will be super
's versions of the methods. If you expect methods to be non-virtual, this is the correct behaviour.super
will be set correctly in case it needs to call a method on its super
.super.method
to an intermediate variable before invoking it, we get the following behaviour:
this
will still be the calling object, so any data features modified by super.method
will be modified on the calling object.super.method
invokes any methods, they will be the calling object's versions of the methods. If you expect all methods to be virtual, this is the correct behaviour.super
will be undefined.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.
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.
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 are generated by the parser, and describe syntactic errors with the source code, such as missing close parenthesis, or an invalidly formed expression.
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.
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:
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.
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.
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 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.