Ursa language reference
See also:
- The grammar (made for Ohm)
- The standard library
- The test samples
- The Rosetta Code examples
Introduction
Ursa is a language that is designed to look familiar to users of mainstream languages circa 2020. It is an imperative functional language that will offer static type checking and a class system that will have a circa 2000 feel. It will come with a standard library that offers file system and network access, keyboard and mouse interaction, and basic sound and graphics support.
Once it reaches maturity, it is intended to remain stable for a long time; colloquially, there will be no version 2.0. However, it has not been thoroughly designed in advance, and until it reaches version 1.0, its design is subject to change.
The rest of this document will become the language reference.
Language basics
Ursa is a functional language: the basic syntactic unit is the expression. Every expression evaluates to some value.
Comments
Comments come in two flavours: single-line comments start with //
, and continue until the end of the line. Comments delimited with /*…*/
may span multiple lines, and may be nested.
Identifiers
Identifiers, or symbols, consist of a letter or underscore followed by zero or more letters, digits and underscores. The definition of “letter” and “digit” have not yet been made precise.
abc
_
_12
sh33p
Built-in data types
The following data types are built in:
null
A distinguished value, used as the value of expressions that have no other natural value. It has the following methods:
equals
notEquals
These are also available as operators.
Boolean
This consists of the constants true
and false
. It has the following methods:
equals
notEquals
not
These are also available as operators.
Number
For now, floating point. Later, integers of various sizes will be distinguished from floating point.
For now, only decimal numbers are permitted, with an optional decimal point and fractional part (no exponent notation). Later, hexadecimal and binary will also be supported, as well as exponent notation.
3
42
-1
It has the following methods:
equals
notEquals
toString
pos
neg
bitwiseNot
lt
leq
gt
geq
add
sub
mul
div
mod
exp
bitwiseAnd
bitwiseOr
bitwiseXor
shiftLeft
shiftRight
shiftRightArith
These are also available as operators.
String
Unicode strings. Later, byte strings will also be supported. For now, the same as JavaScript’s, but must be delimited with double quotes. JavaScript-style escapes are permitted.
"abc"
"☺"
"\u2028"
Strings have the following methods available as operators:
equals
notEquals
The following methods are also available:
get(n)
returns then
th character of the string (0-indexed).iter()
returns an iterator for the characters of the string.split(sep)
returns a list of sub-strings separated by the stringsep
.
List
Lists of arbitrary-typed values.
List literals are written between square brackets, with list items separated by commas:
[1, 2, 3]
[4, "hello", false]
Lists may be indexed (indices start at 0) and their length taken:
[1, 2, 3].get(1) // 2
[1, 2, 3].set(1
Lists have the following methods:
len()
returns the number of elements in the list.get(n)
returns then
th element of the list, ornull
ifn
is not a valid index.set(n
sets then
th element of the list to valuev
. The list must already contain at leastn
values.push(v)
adds the valuev
to the end of the list.pop()
removes the last value from the list and returns it, ornull
if the list is empty.slice([from[
returns a new list consisting of the items fromfrom
inclusive toto
exclusive. If ${to} is omitted, items to the end of the original list are included in the new one. Iffrom
is also omitted, the entire list is copied.iter()
returns an iterator (see Iterators) that returns the list elements in order.sorted()
returns a new list with the elements sorted in ascending order.join(sep)
returns a string consisting of the elements of the list separated by the stringsep
.
Here is an example that uses iter
:
for e in [1, 2, 3].iter() { print(e) }
Map
Maps with arbitrary-types keys and values.
Map literals are written between braces, with the key and value separated by a colon, and key–value pairs separated by commas:
{"a": 1, "b": 2, "c": 3}
{1: "a", 2: "hello", [1, 2, 3]: false}
Maps may be indexed. Note the final example: two lists with identical contents are not the same value!
{"a": 1, "b": 2, "c": 3}.get("a") // 1
{1: "a", "hello": 2, [1, 2, 3]: false}.set("hello"
The following methods are available:
get(k)
returns the value corresponding to keyk
, ornull
if no such key exists.set(k
sets keyk
to valuev
, and returns the map.delete(k)
removes the entry for key ${k}, if any, and returns the map.has(k)
returnstrue
if the map contains key ${k}, andfalse
otherwise.iter()
returns an iterator (see Iterators) that successively returns lists of each key–value pair, in insertion order.keys()
returns an iterator over the keys, in insertion order.values()
returns an iterator over the values, in insertion order.
Object
Maps of symbolic properties to arbitrarily-typed values.
Object literals are written between braces, with a property name and value separated by an equals sign:
{a = 1, b = [1, 2, 3], c = false}
Objects have two methods:
equals
notEquals
Function
Functions are first-class. See the next section for more details.
Type annotations and definitions are built into the grammar, but are currently ignored, so this guide does not yet describe them.
Code
Ursa expressions combine the built-in data types above with operators, control-flow primitives, and functions.
Sequences
Expressions may be combined into a sequence by separating them with semi-colons. The value of a sequence is that of its last expression:
1; 2; 3 // value: 3
4; "hello" // value: "hello"
Semi-colons may usually be omitted at the end of a line.
Blocks
A block is a sequence written in curly braces. Blocks are used to clarify the syntax.
Declarations
Constants are declared with let
and variables with var
. Declarations must include an initial value:
let n = 3
let s = "hello"
let l = [1, 2, 3]
var total = 0
The value of a declaration expression is the value of its right-hand side. The scope of the identifier includes the initializer; this makes it easy to define recursive functions. On the other hand, the following is not allowed:
let x = 1 // x is 1
let x = x + 1
The second let
redeclares x
, and since x
has not yet been given a value when the x
on the right-hand side is evaluated, an error will result.
Simultaneous declarations may be made, separated by and
, which allows mutually-recursive functions to be written easily.
let a = 3 and let b = 4
Operators
The following operators are available. They are given in decreasing order of precedence.
Unary operators
not
logical not~
bitwise not+
the identity operation on numbers-
negation
Binary operators
**
exponentiation*
product/
quotient%
remainder+
addition-
subtraction==
equals (value equality for atomic values, reference equality for containers)!=
not equals<
less than<=
less than or equal>
greater than>=
greater than or equal&
bitwise and^
bitwise exclusive-or|
bitwise or<<
left shift>>
arithmetic right shift>>>
logical right shift
Assignments
A variable or object property may be assigned to with the assignment operator :=
:
var x = 1
x := x + 1 // x is now 2
The value of an assignment expression is the value of its right-hand side.
Conditionals
The if
…else
form is used to evaluate expressions conditionally. if
is followed by the condition; if it evaluates to true, the block following the condition will be evaluated; otherwise the block following else
will be:
if true {1} else {2} // value: 1
if false {3} else {4} // value: 4
Conditional expressions can be chained:
if false {1} else if false {2} else {3} // value: 3
The short-circuit boolean operators and
and or
are also provided. and
evaluates its right-hand operand only if its left-hand operand is true. or
evaluates its right-hand operand only if its left-hand operand is false:
true and 2 // value: 2
false and 2 // value: false
false or 2 // value: 2
true or 2 // value: true
For now, values are considered to be true or false as they would be in JavaScript; in future, only boolean values will be allowed.
Loops
A general loop is written with the loop
operator followed by a block:
loop {} // loop forever
To exit a loop, use a break
expression. The value of a loop, if it terminates, is the value supplied to break
, if any; otherwise null
.
loop { break 4 } // value: 4
The continue
expression jumps back to the top of a loop.
There is a special form of loop for use with an iterator function, for
:
for i of range(5) { print (i) }
See Iterators.
Functions
Functions are first-class in Ursa. A function is written using the fn
keyword, followed by its formal parameters in parentheses, followed by its body as a block. The function parameters are constant (as if declared with let
). The value of a function is the value of the last expression evaluated before it returns, or the value given with return
, if any, or null
. Functions are called by adding parentheses containing zero or more arguments to a function value:
let f = fn(x) { x + 1 }
f(1) // value: 2
Iterators
An iterator is just a function, but to be useful it should return a different value each time it is called, and then null
when there are no more values. Lists and maps provide an iter
method that iterates over the elements of the list or map. Iterators are often written using Generators.
Generators
Generators are functions where a single invocation can be called and return more than once. When a generator function is called, it returns a function that successively returns the values of the generator, until it returns null
. After that, the generator will only return null
.
The yield
keyword is used to return a value from a generator. If return
is used in a generator, then it will stop at that point.
Generators are introduced with the keyword gen
(instead of fn
), and yield
may only be used in generators.
Here is an example of a generator that takes a list as input, and returns its values, doubled:
let g = gen(l) {
for n of l.iter() { yield n * 2 }
}
When a generator is called, it takes a single argument which becomes the value of the yield
expression. Here is an example of a generator that keeps a running total and returns the current total on each call:
let totalizer = gen() {
var i = 0
loop {
i := i + (yield i)
}
}
let t = totalizer()
print(t(0)) // prints: 0
print(t(3)) // prints: 3
print(t(2)) // prints: 5
Asynchronous execution
This area of Ursa is under development and will change in some details.
It is possible to make an expression run asynchronously by prefixing it with launch
. This gives an “operation”.
let p = launch f()
g() // g may be called before f returns
Operations can be “awaited”:
let x = await p // x now has the value computed by f()
When the main program stops running, any currently-running operations are also stopped.
use
The use
expression imports definitions from outside the current module. The expression use x.y.z
is equivalent to let z = x.use("y.z")
.