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.- Boolean: the constants
true
andfalse
. - Number: for now, floating point. Later, integers of various sizes will be distinguished from floating point.
- String: Unicode strings. Later, byte strings will also be supported.
- List: lists of arbitrary-typed values.
- Map: maps with arbitrary-types keys and values.
- Object: maps of symbolic properties to arbitrarily-typed values.
- 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.
Numbers
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
Strings
For now, the same as JavaScript’s, but must be delimited with double quotes. JavaScript-style escapes are permitted.
"abc"
"☺"
"\u2028"
Lists
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 an iterator method iter
(see Iterators) that returns the list elements in order:
for e in [1, 2, 3].iter() { print(e) }
Maps
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"
Maps have an iterator method iter
(see Iterators) that returns each key–value pair as a list of two elements, in insertion order.
Objects
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 may be indexed. Properties may be reassigned, but new properties may not be added.
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
Variables are declared with let
, and must always include an initial value:
let n = 3
let s = "hello"
let l = [1, 2, 3]
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 may be assigned to with the assignment operator :=
:
let 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 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.
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 a “promise”.
let p = launch f()
g() // g may be called before f returns
Promises can be “awaited”:
let x = await p // x now has the value computed by f()
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")
.