|
XL (provisional name) is a dynamically typed, Java-like language for writing scripts in Slice. Slice on the Tablet PC can execute both Python and XL scripts; however, neither language can call functions defined in the other. On the (not yet released) Android and Swing versions of Slice, only XL scripts are supported.
Python and XL scripts call the same core Slice operations, with minor exceptions (see below). Translation from Python to XL is generally very simple, except when Python libraries are used (which is rare within Slice). XL does not currently have any libraries.
XL was developed for two reasons: (1) The Python implementation for Android looked like it would be awkward to use as a scripting language. (2) We have long felt that a language specifically written for Slice scripting could make application development easier. At the current stage of development, this is not yet the case, but the opportunity now exists.
As already noted, XL has Java-like syntax,
and generally preserves Java semantics with the important
difference that it is dynamically typed.
The most obvious differences are what is left out.
XL has classes, but no fields;
all methods are static (by default; the static
keyword is not used), and class names are used only for
name disambiguation.
Accordingly, there are no objects; classes are not instantiable.
XL has no exception mechanism (try/catch),
since this is of limited use without instantiable classes.
(A central principle of Slice scripts is that they
do not save values "internally"
across script invocations - such data is
always stored in the state tree.
Given this, instantiable classes would be of little value,
and might lead to violations of this principle.)
The other major difference from Java is the addition of
heterogeneous lists, as in Python (and Lisp and ...),
with Python-like notation for constants (square brackets
and commas).
(For Java compatibility, set braces - {, } - can
also be used; we always use square brackets in our scripts.)
The list operations are similar to Python's (e.g. range subscripts),
but with slight differences in syntax.
XL also has a Python-like dictionary type, but it is
rarely used (for the same reason that objects, if they existed
in XL, would not be used, as discussed in the previous paragraph).
XL code is loaded into Slice apps in a way analogous to Python:
In the root node of the initial XML file, the attribute
XLDefs gives the names of files containing XL code.
Each file contains one or more class definitions.
Correspondence between class names and filenames is not assumed,
and there are no visibility restrictions on classes (or methods).
We start with a few examples, then describe the differences between XL and Java, describe the few differences in the use of XL, as compared to Python, within Slice, and then lastly give the syntax and semantics in detail. The first two sections should suffice for most purposes.
We assume the reader is familiar with Slice scripting in general.
XL is not intended to be a standalone scripting language. These examples are drawn from a Slice application, a version of the networked lecture app. A basic understanding of that app is required to understand these examples: In this app, a lecturer writes on slides, and his or her writing is sent to a display. The lecture and display roles of this app are different in that the display is entirely passive: it receives slides and strokes from the lecture role, but does not send messages. However, the two roles use basically the same tree structure, as follows:
Slice
Lecture
VerticalToolbar
HorizontalToolbar
Slides
InkPanel (repeated; one for each slide)
Label (optional; underlying image, usually imported from PPT)
Stroke (repeated)
We illustrate XL with several examples of scripts from this app, also providing the Python versions for comparison.
tree function
This commonly-used function creates a tree from a node name,
a list of attributes, and a list of children.
The only complication is that the "Type"
attribute can appear in the list of attributes, and then
it must be passed to the core MakeTreeNode
function.
The Python version:
def tree(name, attrlist, subtrees):
# Check if Type attribute is being used
typeattr = None
for i in range(0, len(attrlist)-1, 2):
if attrlist[i] == "Type":
typeattr = str(attrlist[i+1])
break
t = TreeNode.MakeTreeNode(name, typeattr)
for i in range(0, len(attrlist)-1, 2):
attrname = attrlist[i]
attrvalue = attrlist[i+1]
if attrname != "Type":
t.SetAttribute(attrname, str(attrvalue))
for t1 in subtrees:
t.AppendChild(t1)
return t
and the XL version:
TreeNode tree(String name, Listattrlist, List subtrees) { String typeattr = null; for (int i : [0..attrlist.count()-2 by 2]) if (attrlist[i] == "Type") { typeattr = attrlist[i+1]; break; } noyb t = TreeNode.MakeTreeNode(name, typeattr); for (int i : [0..attrlist.count()-2 by 2]) { attrname = attrlist[i]; attrvalue = attrlist[i+1]; if (attrname != "Type") t[attrname] = attrvalue; } for (TreeNode t1 : subtrees) t.AppendChild(t1); return t; }
The correspondence is perhaps clear enough as to be self-explanatory. We make just a couple of points:
count, which gives
the length of a list or string.
[i..j],
analogous to Python's [i:j], can be used for
creating integer lists in general, not just as subscripts.
(A range expression can contain negative numbers,
with the same meaning as in Python, only when it is
used as a subscript.)
noyb ("none of your business") is the type
of a variable that may contain values of different types.
At present, no use is made of these types; they are strictly
for documentation.
(Methods must be given a return type, although, again, it
is not checked at present.)
The next script is the one that starts network connections, depending upon the "role" of this Slice instance - lecturer or display The Python version:
def StartRole():
if Root["Role"] == "Display":
NetworkHelper.TreeNodeMessage += LectureHandler
NetworkHelper.SetNetworkOption("TcpPublishingServer", "",
int(Root["DisplayPort"]))
NetworkHelper.StartNetwork()
else: # Role = "Lecturer"
NetworkHelper.SetNetworkOption("TcpClient", Root["DisplayIP"],
int(Root["DisplayPort"]) )
started = NetworkHelper.StartNetwork()
if not started:
tryagain = Slice.Prompt("Server not found, try again?", "")
if tryagain:
StartRole()
else:
MessageBox.Show("Running as standalone app.")
Root["NetworkMode"] = "Standalone"
return
and XL:
void StartRole() {
if (Root["Role"] == "Display") {
XLEngine.RegisterMessageHandler("LectureHandler");
NetworkHelper.SetNetworkOption("TcpPublishingServer", "",
Root["DisplayPort"].toInt());
NetworkHelper.StartNetwork();
}
else { // Role = "Lecturer"
NetworkHelper.SetNetworkOption("TcpClient", Root["DisplayIP"],
Root["DisplayPort"].toInt());
started = NetworkHelper.StartNetwork();
if (!started) {
tryagain = Slice.Prompt("Server not found, try again?", "");
if (tryagain)
StartRole();
else {
Slice.Message("Running as standalone app.");
Root["NetworkMode"] = "Standalone";
}
}
}
}
Most of XL can be explained by a few differences from Java. We will not go into all the Java features that are omitted from Java, but just the principal ones. The next section presents the full syntax of XL; the semantics mostly follows Java but we will explain some finer points of difference there.
As mentioned above, the major differences between XL and Java are: (1) XL does not have objects - all methods are static and classes have no fields (even static ones); (2) XL is dynamically typed; the major consequence of this is that (3) XL has (heterogeneous) lists built in.
The syntax of XL is drawn from Java.
The statements of XL - if, while, do-while, for (in two forms:
index variable and iterator), switch, return, labeled statements,
break, continue, and expression statements -
look and work very much as in Java;
try/catch is absent.
(The only notable exception is that the cases in a switch statement
can be arbitrary expressions, and are evaluated dynamically
each time the case in question is tested.)
Package and import statements are ignored.
Variables may be declared (legal types are
int,
char,
float,
String,
boolean,
and noyb,
and parameterized types, which can be any name followed by type
arguments enclosed in </> brackets
(as in Java);
the common parameterized types would be List
and Dictionary.
However, type declarations are ignored; they are included only
for future purposes, and, of course, for documentation.
(The noyb type is used to declare variables that will,
in fact, change type during execution.)
The rest of this section is divided into four subsections: method-calling, lists and dictionaries, type conversions, and printing; these represent the major areas of difference between XL and Java.
As already stated, all methods of XL are static.
There is no overloading of method names, in the Java sense (where the
particular method to be called is determined by the number and type
of arguments).
Methods defined in different classes can have the same name, with
the call being disambiguated from the class and method names alone.
Specifically, XL has the following forms of method call:
Although XL itself does not have overloading, calls to
core functions are
resolved using standard overloading rules (based on the dynamic
types of the arguments).
XL has heterogeneous lists.
The usual notation for list constants is
Strings can also be used as lists, with all the operations below acting
as if on a list of char's (except that, where appropriate,
the results will be strings, rather than character lists).
XL's list operations are:
We note two final points about lists:
(1) When subscripting is performed on a string, the result is a string;
that is, the string is converted to a list of chars only for the duration
of the operation, and the result is converted back.
(No operation is provided to actually convert a string to a list.)
(2) The equality operation on lists uses structural
equality (so takes linear time);
no "object identity" operation is provided, and
no other comparison operations are implemented on lists.
Dictionaries are maps from strings to other values (of arbitrary type).
The empty list [] acts also as the empty dictionary.
Dictionary constants are written
Note that the attributes of a TreeNode can be accessed by subscripting
with a string, and can be assigned similarly (just as in Python).
Nonetheless, TreeNodes are not dictionaries;
they cannot be iterated over in for loops, and the
Conversions are much as in Java (except that the decision to convert a
value is, of course, made at run-time).
We mention three operations provided in XL for manual
conversions:
Special cases:
We note that there is one automatic conversion that is done in a core
function, not as part of XL (so is also done for Python scripts):
When calling GetChild, if the argument is a string
that can be converted to an int, then that conversion is done.
Note that there is no loss of generality here: the overloading of
GetChild on a string argument searches for a child with the given tag,
but a tag cannot have the form of a number, so that version could never
succeed anyway.
We have found that this is otherwise a source of bugs when a node attribute
is used as an argument to GetChild.
The script-writer should use
Just one operation is provided in XL for printing to the console:
Generally speaking, this is only useful for debugging
(as are the core functions
The core Slice functions that can be used from either
Python or XL are documented on the
framework reference page.
There are a few functions that are different when scripting
with Python or XL.
Here we describe those methods that are specific to XL.
First, to load files of XL definitions, set the
Function names associated with script attributes are
executed as usual in Slice, and may be either XL
or Python functions.
Slice looks first for an XL definition for a given function name,
and tries Python only if no XL definition exists.
Specific differences in XL:
Lexing in XL is much like Java.
One special note on lexing: The lexing of sequences of
the character > follows the rules above, so that, for example,
the sequence >> always lexes as the right-shift operator.
This creates a problem for parameterized types, and causes
the type expression
In the following grammar listing, non-terminals begin with
lower-case letters and categories of terminal symbols
(e.g. Identifier) begin with upper-case letters.
Keywords are given in all upper-case (although
unlike identifiers, keywords are not case-sensitive in XL).
The grammar is substantially LL(1), but with some exceptions.
When a rule is followed by a list of tokens in square brackets,
it gives a condition under which the rule is applicable;
namely, the rule is to be used only if the token following the
next one is one of the listed tokens.
In other words, these tokens give us, in effect, two-symbol
lookahead.
(Note that the added lookahead symbols do not refer to the
token after this production, but rather to the
very next token in the input stream.
Thus, the rule
This grammar was originally adapted from the
ANTLR grammars.
The extended grammar syntax was removed and the grammar simplified
to remove most unused features.
This is the grammar actually used to construct the XL parser.
All control structures and operators behave, as much as possible,
as in Java, so we will not document each one.
Here are the exceptions:
Method-calling
f(args): unqualified method name.
If the method is defined in the current class (the one in which the
call appears), that method is used.
Otherwise, f must be defined in exactly one other class
(i.e. a globally unique definition), and
that definition is used.
C.f(args), where C is
an XL class. f must be defined in C,
and that definition is used.
C.f(args), where C is one of the
core class names:
Slice,
TreeNode,
NetworkHelper,
or XLEngine.
(The latter is new; see core function documentation.)
This is a method call to a static core method.
exp.f(args), where
exp is an expression.
Since XL does not have objects, this can only be a call to a core
Slice method.
At present, the only public core classes with non-static methods
are TreeNode and StrokeNode,
so exp must evaluate to an object of one of those classes.
Lists and dictionaries
[e0, e1, ..., en],
as in Python.
Subscripting is also similar to Python, but more flexible in one way:
subscripts can be lists of integers, and then the result is a sublist
(generalizing Python's slice indices).
[e0..e1 by e2],
which gives the list of all numbers starting from
e0, incrementing by
e2, and ending before the first number greater than
e1.
If e0 is omitted, it defaults to zero;
if the by clause is omitted,
the step defaults to 1 if e0<e1,
-1 otherwise.
e1 can be omitted only if the list is in the syntactic position
of a subscript; see below.
L.count(): the length of L.
exp :: L: non-destructively add
exp to the front of L.
At present, this operation copies L, and therefore does not
take constant time (as it should, and does in Python, Lisp, etc.).
L1 + L2: non-destructively concatenate
L1 and L2.
Again, unlike most languages, this operation copies both lists.
L[e], where e evaluates to
a single integer (or a string convertible to an integer):
return that element.
L[]: return the empty list.
(This is a degenerate version of the next case.)
L[e1, ..., ek], where
either k> 1, or e1 is not an integer
or string.
The expressions ei are processed in order,
each contributing to the list of subscripts as follows.
This always produces a list of results, even if
k=1 and the processing described below produces
a single index.
L[j] is added to
the result list, if j is non-negative;
if j is negative, then length(L)-j
is used as the subscript.
[0, 2, 5],
and L is a list, then L[x] is
that same thing as L[0, 2, 5].
(Again, it is not exactly as if the list were interpolated,
in that if L has just this one subscript, and this list
consists of a single integer j, the result is a one-element list
containing L[j], rather
than ijust L[j].)
e0..e1 by e2,
the range expression is evaluated as indicated above, and the
generated elements are used as subscripts.
In this case, e1 may be omitted, and it
defaults to length(L)-1.
(A syntactic restriction is that
e0 and
e1 may not both be omitted.)
L[e1, ..., ek] = exp.
Again, the case where k = 1 and ek evaluates
to an int or string is special: the right-hand side value
simply replaces the corresponding element in L.
Otherwise, the list of indices is generated just as described above, but
we need to distinguish the case where the right-hand value is
a list.
If it is not, then it is simply assigned to each location
in L given by the list of indices.
If the right-hand value is a list, then the elements of that
list are assigned to the corresponding elements of L,
with the list being repeated as often as necessary.
For example, if L = ["a", "b", "c", "d"]
then L[1, 3] = ["e", "f"] will result in L
having value ["a", "e", "c", "f"],
and
L[1, 2, 3] = ["e", "f"] will result in L
having value ["a", "e", "f", "e"].
(With this syntax, there is no direct way to replace several
elements of a list
by the same list; for example, L[0,1,3] = ['a', 'b', 'c'] will replace
the first element of L by 'a', the second by 'b', etc.
The only way to make each of those elements of L
contain the list ['a', 'b', 'c']
is to write a loop: for (i: [0, 1, 3]) L[i] =
['a', 'b', 'c'];.)
[s0 -> e0, s1 -> e1, ...],
where each si is a String-valued expression and
ei is an arbitrary expression.
The only operations permitted on dictionaries are
single-item subscripting (with a string),
single-item assignment, and union of dictionaries
using operator +;
for the union operation, collisions are resolved
in favor of the second argument.
In a for loop for (name : exp) ...,
if the exp is a dictionary, it is, in effect,
converted to a list of key/value pairs (two-element lists),
in no prescribed order,
for the iteration.
+ operator is not applicable to them.
Automatic conversions
*, -,
/, and % - strings are converted to ints
if possible, and if not, then to floats, if possible.
toInt(), toFloat(),
and toString().
These convert any reasonable thing - such as strings that are in
numeric format - to the given type.
Syntactically, these are used as non-static methods - e.g.
e.toInt().
If they fail, the computation is aborted, so they should only be
used when their argument is known to be convertible to the corresponding
type of value (in particular, if applied to a string, the string should
definitely have the correct numeric format).
toInt() in this case,
but this is easy to forget, and since GetChild is overloaded, it will
simply call the string version and fail (return null), with
no exception being raised.
Printing
e.print(). This prints e, followed
by a newline.
Slice.Log(..)
and Slice.LogLine(..)).
To show the user a message as part of the application,
use Slice.Message(..)), which produces
a dismissable popup window with the given message.
Differences from Python in the context of Slice
XLDefs attribute of the root.
Slice.SetXLGlobal(name, exp)
(analogous to SetGlobal for Python globals).
The assigned value (exp) can be referenced using
the variable name.
XLEngine.RegisterMessageHandler(function-name),
XLEngine.RegisterDisconnectHandler(function-name),
XLEngine.RegisterClientlostHandler(function-name), or
XLEngine.RegisterClientjoinedHandler(function-name).
The argument to each is the name of the function being registered,
as a string.
(In Python scripts, this is done by assigning to variables
NetworkHelper.TreeNodeMessage, etc., using +=.)
NetworkHelper.StartNetwork() cannot be
used in XL.
Instead, use the version that takes a list argument (which can
also be used in Python).
XL syntax
Lexical rules
boolean,
break,
by,
case,
catch,
char,
class,
continue,
default,
do,
else,
float,
if,
false,
for,
import,
instanceof,
int,
noyb,
null,
package,
return,
switch,
throw,
true,
try,
void,
and while.
(Note: In some cases, the syntactic construct corresponding to
these keywords in Java is not implemented in XL, but the keywords
are reserved for the future.
These cases are:
catch,
try, and
instanceof.
noyb and by are keywords in XL but not in Java.)
+ or
-), and an integer.
(XL does not have octal, hex, or binary constants.)
\n,
\t,
and
\r.
\c are recognized;
this sequence is identical to the character c (i.e. the
backslash is ignored) except when c is n, t, b, f, or r, in which
case the character is replaced by the corresponding control character.
The escape sequence \" is a double quote that does not
terminate the string.
/*,
followed by arbitrary characters (including newlines) terminating
with the first occurrence of sequence */.
Single-line (C++-style)
comments consist of the character sequence //,
followed by arbitrary non-newline characters, terminating
at the first newline.
..,
-,
->,
-=,
--,
*,
*=,
::,
<,
>,
<=,
>=,
<<,
<<=,
>>,
>>=,
=,
==,
!,
!=,
+,
++,
+=,
/,
/=,
|,
|=,
||,
||=,
&,
&&,
&&=,
^,
^=,
%,
%=,
and ~.
These sequences are self-terminating in the sense that they do not need to
be separated by spaces.
For example, the sequence +++ is interpreted
as operator ++ followed by operator
+.
The meaning of each operator is as in Java.
.,
.*,
{,
},
;,
:,
,,
(,
),
[,
],
?.
(Note that, unlike in Java, comma is not an operator.)
List<List<C>>
to be syntactically incorrect.
Thus, this type must be entered as
List<List<C> >.
Context-free syntax
statement ::= Identifier : statement [ : ]
says that if we are parsing a statement and the next token is Identifier,
then this should be parsed as a labelled statement if and only if
a colon immediately follows the identifier.)
compilationUnit ::= packageDeclarationOpt importDeclarationList typeDeclarationList
packageDeclarationOpt ::=
| packageDeclaration
packageDeclaration ::= PACKAGE qualifiedName ;
importDeclarationList ::=
| importDeclaration importDeclarationList
typeDeclarationList ::=
| classDeclaration typeDeclarationList
importDeclaration ::= IMPORT qualifiedName dotStarOpt ;
dotStarOpt ::=
| .*
qualifiedName ::= Identifier dotIdentifierList
dotIdentifierList ::=
| . Identifier dotIdentifierList
classDeclaration ::= CLASS Identifier classBody
classBody ::= { classBodyDeclarationList }
classBodyDeclarationList ::=
| memberDecl classBodyDeclarationList
memberDecl ::= methodDeclaration
methodDeclaration ::= type Identifier formalParameters block
type ::= classType
| primitiveType
classType ::= Identifier typeArgumentsOpt
primitiveType ::= BOOLEAN
| CHAR
| INT
| FLOAT
| VOID
| NOYB
typeArgumentsOpt ::=
| typeArguments
typeArguments ::= < type commaTypeList >
commaTypeList ::=
| , type commaTypeList
formalParameters ::= ( formalParameterDeclsOpt )
formalParameterDeclsOpt ::=
| formalParameterDecls
formalParameterDecls ::= typeOpt Identifier typeIdentList
typeOpt ::= [ Identifier, < ]
| type
typeIdentList ::=
| , typeOpt Identifier typeIdentList
block ::= { statementList }
statementList ::=
| statement statementList
localVariableDeclarationStatement ::= localVariableDeclaration Semicolon
localVariableDeclaration ::= type variableDeclarator variableDeclaratorList
variableDeclaratorList ::=
| , variableDeclarator variableDeclaratorList
variableDeclarator ::= Identifier variableInitializerOpt
variableInitializerOpt ::=
| = expression
colonExprOpt ::=
| : expression
elseStmtOpt ::= [ tok != tokens.Else ]
| ELSE statement
statement ::= block
| Identifier : statement [ : ]
| localVariableDeclarationStatement [ Identifier, < ]
| ASSERT expression colonExprOpt ;
| IF parExpression statement elseStmtOpt
| FOR Lparen forstatement
| WHILE parExpression statement
| DO statement WHILE parExpression ;
| SWITCH parExpression { caseStmts }
| RETURN expressionOpt ;
| THROW expression ;
| BREAK identifierOpt ;
| CONTINUE identifierOpt ;
| expression ;
| ;
forstatement ::= type typeFollower [ Identifier, < ]
| typeFollower
typeFollower ::= Identifier : expression ( statement [ : ]
| expressionList ; expressionOpt ; expressionList ) statement
caseStmts ::=
| caseList neStatementList caseStmts
| DEFAULT : neStatementList caseStmts
neStatementList ::= statement statementList
caseList ::= CASE expression : caseListOpt
caseListOpt ::=
| caseList
expressionList ::=
| expression commaExprList
commaExprList ::=
| , expression commaExprList
parExpression ::= ( expression )
identifierOpt ::=
| Identifier
expressionOpt ::=
| expression
expression ::= primary exprContOpt
exprContOpt ::=
| exprCont
exprCont ::= binop primary exprContOpt
| ? condExprTail
| incrOptr exprContOpt
condExprTail ::= expression : expression
primary ::= simplePrimary primarySuffix
| unaryOp primary
simplePrimary ::= listExpression
| parExpression
| Identifier argumentsOpt
| literal
argumentsOpt ::=
| arguments
primarySuffix ::=
| . Identifier argumentsOpt primarySuffix
| listExpression primarySuffix
listExpression ::= { listElementListOpt }
| [ listElementListOpt ]
listElementListOpt ::=
| listElement commaListElementList
| , [ ] , } ]
commaListElementList ::=
| , [ ] , } ]
| , listElement commaListElementList
listElement ::= dotDotOpt expression mapOrRangeOpt
dotDotOpt ::=
| ..
mapOrRangeOpt ::=
| .. expressionOpt byExprOpt
| -> expression
byExprOpt ::=
| BY expression
arguments ::= ( expressionList )
literal ::= Intlit
| Floatlit
| Charlit
| Stringlit
| TRUE
| FALSE
| NULL
binop ::= assignmentOp
| logicalOp
| bitwiseOp
| comparisonOp
| consop
| shiftOp
| arithOp
consop ::= ::
logicalOp ::= || | &&
bitwiseOp ::= | | & | ^
comparisonOp ::= == | != | <= | >= | < | >
shiftOp ::= << | >>
arithOp ::= + | - | * | / | %
assignmentOp ::= = | += | -= | *= | /= | &=
| |= | ^= | %= | <<= | >>=
unaryOp ::= + | - | ~ | ! | ++ | --
incrOptr ::= ++ | --
==, can be applied to
any type of value except a dictionary.
It employs structural equality on all other types (so that the
equality test between two lists or strings takes linear time),
except "object" types: XL values that are actually
Slice values, such as TreeNodes (which are obtained as the values
of special Slice variables like Root and as the results of
some core function calls); for those values, object identity
is used.
== operator.
for (name : exp) ...) can be
used to iterate over lists, strings, or dictionaries.
For the latter, the name is assigned pairs (two-element
lists) giving all the key/value bindings in the dictionary,
in no prescribed order.