Namespace definitions
Global namespace containing types and functions accessible without any namespace prefix.
Global namespace containing types and functions accessible without any namespace prefix.
Types
An immutable signed integer type, supporting extremely large values (upwards of 100,000 decimal digits).
Literals of big_integer
type can be written like integers, but with the suffix L
, e.g. 123L
or 0x123L
. big_integer
s support the operators +
, -
, *
, /
and %
with typical behavior.
A data type with two possible values: true
and false
.
Boolean logic can be performed with the and
, or
and not
operators, for example:
>>> true and false
false
>>> true or false
true
>>> not true
false
>>> not false
true
In addition, booleans are used in ternary expressions:
>>> if (true) 1 else 2
1
>>> if (false) 1 else 2
2
The in
operator returns a boolean:
>>> 1 in list<integer>()
false
>>> 1 in [1, 2]
true
Booleans are used in if
- and if/else
-statements:
>>> if (false) { print("hello"); } else { print("goodbye"); }
goodbye
>>> if (false) { print("hello"); }
>>> if (true) { print("hello"); }
hello
>>>
Boolean expressions are the basis of zero-argument when
-statements (each expression to the left of a '->
' symbol has boolean type (and in this context else
is equivalent to true
)):
when {
x == 1 -> return 'One';
x >= 2 and x <= 7 -> return 'Several';
x == 11, x == 111 -> return 'Magic number';
some_value 1000 -> return 'Special case';
else -> return 'Unknown';
}
Boolean conditions are used in while loops:
while (x < 10) {
print(x);
x = x + 1;
}
Functions can have boolean
return type (as can queries and operations), and indeed many functions and properties in the Rell standard library have boolean
type.
function foo(x: integer): boolean {
return x >= 10;
}
An array of bytes. This type is immutable.
A generic type for mutable ordered collections of elements. Subtype of iterable<T>
. Supports many standard operations such add insertion, removal, lookup and sorting.
A real number data type with high precision.
Not a complete equivalent of floating-point types, as there are a fixed maximum number of digits before the decimal point (131072
, or 2^17
digits) and after the decimal point (20
digits).
Examples:
123.456
.789
7e+33
decimal('123456789.98765')
An enum is a set of member constants who share a type.
The declaration enum example { A, B, C }
defines a type example
, with three member constants, example.A
, example.B
and example.C
.
Enum names and member constant names follow the same rules as all identifiers in Rell. However, by convention, enum names use snake_case
, and member constants use UPPER_SNAKE_CASE
.
Examples:
enum primary_color { RED, BLUE, GREEN }
enum error { TIMEOUT, MALFORMED_RESPONSE, NOT_FOUND, UNAUTHORIZED, UNKNOWN }
enum cardinal_direction { NORTH, EAST, SOUTH, WEST }
Generic Transfer Value (GTV) is a data type for the serialization and transfer of structured data, much like JSON.
GTV is used in Rell to encode operation and query arguments and results that are exchanged with clients. Unlike JSON, GTV has a stable byte serialization format and well-defined cryptographic hash, making it well-suited to this purpose. In addition, GTV supports byte arrays.
GTV supports the following types:
GTV Type | Closest Rell Equivalent |
---|---|
NULL | null |
BYTEARRAY | byte_array |
STRING | text |
INTEGER | integer |
DICT | map<text, gtv> |
ARRAY | list<gtv> |
BIGINTEGER | big_integer |
GTV does not support all Rell types, so not every value in Rell can be converted to GTV. For example, GTV has no support for non-integer numbers, and therefore the decimal
type is encoded in GTV as text.
Rell types can be encoded as GTV in two modes: compact and pretty, and the distinction between the two is a real semantic difference, and is not merely a difference in whitespace when converted to text. The two modes differ in the following ways:
Compact GTV encode struct values as a lists of attributes, while pretty GTV encode them as a dictionaries (thus struct member names are preserved).
Compact GTV encode named-field tuples as a lists of attributes, while pretty GTV encode them as a dictionaries (thus tuple field names are preserved). There is no difference between the two in encoding of unnamed-field tuples.
Examples of GTV:
>>> (x = 1, y = 'a', z = true).to_gtv()
[1,"a",1]
>>> (x = 1, y = 'a', z = false).to_gtv_pretty()
{"x":1,"y":"a","z":0}
>>> [1: 'a', 2: 'b', 3: 'c'].to_gtv()
[[1,"a"],[2,"b"],[3,"c"]]
>>> [1: 'a', 2: 'b', 3: 'c'].to_gtv_pretty()
[[1,"a"],[2,"b"],[3,"c"]]
>>> set([1, 2, 3, 4]).to_gtv()
[1,2,3,4]
>>> set([1, 2, 3, 4]).to_gtv_pretty()
[1,2,3,4]
>>> struct a { x: integer; y: decimal; };
>>> a(10, 10.1).to_gtv()
[10,"10.1"]
>>> a(10, 10.1).to_gtv_pretty()
{"x":10,"y":"10.1"}
Rell operations expect their arguments as compact-encoded GTV, whereas queries expect pretty-encoded GTV arguments, hence client applications are required to use those respective formats when making operation and query calls to Rell applications.
Generic Transfer Value (GTV) is a data type for the serialization and transfer of structured data, much like JSON.
GTV is used in Rell to encode operation and query arguments and results that are exchanged with clients. Unlike JSON, GTV has a stable byte serialization format and well-defined cryptographic hash, making it well-suited to this purpose. In addition, GTV supports byte arrays.
GTV supports the following types:
GTV Type | Closest Rell Equivalent |
---|---|
NULL | null |
BYTEARRAY | byte_array |
STRING | text |
INTEGER | integer |
DICT | map<text, gtv> |
ARRAY | list<gtv> |
BIGINTEGER | big_integer |
GTV does not support all Rell types, so not every value in Rell can be converted to GTV. For example, GTV has no support for non-integer numbers, and therefore the decimal
type is encoded in GTV as text.
Rell types can be encoded as GTV in two modes: compact and pretty, and the distinction between the two is a real semantic difference, and is not merely a difference in whitespace when converted to text. The two modes differ in the following ways:
Compact GTV encode struct values as a lists of attributes, while pretty GTV encode them as a dictionaries (thus struct member names are preserved).
Compact GTV encode named-field tuples as a lists of attributes, while pretty GTV encode them as a dictionaries (thus tuple field names are preserved). There is no difference between the two in encoding of unnamed-field tuples.
Examples of GTV:
>>> (x = 1, y = 'a', z = true).to_gtv()
[1,"a",1]
>>> (x = 1, y = 'a', z = false).to_gtv_pretty()
{"x":1,"y":"a","z":0}
>>> [1: 'a', 2: 'b', 3: 'c'].to_gtv()
[[1,"a"],[2,"b"],[3,"c"]]
>>> [1: 'a', 2: 'b', 3: 'c'].to_gtv_pretty()
[[1,"a"],[2,"b"],[3,"c"]]
>>> set([1, 2, 3, 4]).to_gtv()
[1,2,3,4]
>>> set([1, 2, 3, 4]).to_gtv_pretty()
[1,2,3,4]
>>> struct a { x: integer; y: decimal; };
>>> a(10, 10.1).to_gtv()
[10,"10.1"]
>>> a(10, 10.1).to_gtv_pretty()
{"x":10,"y":"10.1"}
Rell operations expect their arguments as compact-encoded GTV, whereas queries expect pretty-encoded GTV arguments, hence client applications are required to use those respective formats when making operation and query calls to Rell applications.
A JSON (JavaScript Object Notation) datatype.
JSON values are created from text and support conversion to GTV via gtv.from_json()
. They can be stored in the database as an entity attribute.
JSON values in Rell are represented internally with text, which has been validated as legal JSON.
Represents a mutable array list. Subtype of collection<T>
.
A mutable map from keys of type K
to values of type V
, where K
is immutable, and V
may be either mutable or immutable. map<K,V> is a subtype of
iterable<(K,V)>`. It is implemented as a hash-map, with iteration order determined by the order in which the entries were added.
Parent of all object types. An object is a singleton data structure that resides in the SQL database.
Objects are much like entities, with the following restrictions:
only a single instance is allowed for each definition
all object attributes must have default values
object values cannot be created or deleted from code (they are automatically created during blockchain initialization)
Example definition:
object state {
mutable x: integer = 0;
mutable s: text = 'n/a';
}
Object attributes are accessed directly:
print(state.x);
print(state.s);
Object values can be modified directly, or with an update
statement:
// Direct modification
state.x += 10;
state.s = 'Updated';
// Modification via update statement
update state ( x += 5, s = 'Updated' );
The primary key of a database record.
Implemented as a 64-bit integer, but requires explicit conversion to and from integer with the constructor rowid(integer)
and the method rowid.to_integer()
. ROWID values cannot be negative.
ROWID supports the standard complement of comparison operators (==
, !=
, <
, >
, <=
and >=
), and conversion to and from GTV.
Examples:
function get_rowid(username: text) {
val u = user @ { .name == username };
return u.rowid;
}
val freds_rowid: rowid = user @ { .name == "Fred" } ( .rowid );
val valid_rowids: list<rowid> = user @* { .rowid >= min_rowid };
Note that the recommended way to manipulate entity values is via typed references (e.g. u: user
in the above example), as this is type-safe. Reliance on rowid
is only recommended in rare cases where the standard pattern is not possible, as the compiler does not know what type of entity a given rowid
value is intended to reference. Consider the example below:
entity user {}
entity company {}
val u: user = user @ {};
val c: company = company @ {};
val u2: user = c; // Bad, and the compiler tells us so.
val u_rowid: rowid = c.rowid; // Likely to lead to errors, but the compiler can't help us.
A mutable set of elements of type T
, where T
is immutable. Subtype of collection<T>
. Implemented as a hash-set, with iteration order determined by the order in which the elements were added.
A type with no member values, much like void
in other languages.
Typically used as a return type for functions where no return value is required. Indeed, when a function is declared without a specified return type, the return type is implicitly unit
. In other words, the function definition:
function f(...) { ... }
is equivalent to:
function f(...): unit { ... }
Functions
Returns the absolute value of a big_integer value; i.e. the value itself if it's positive or its negation if it's negative.
Returns the absolute value of a decimal value; i.e. the value itself if it's positive or its negation if it's negative.
Returns the absolute value of a integer value; i.e. the value itself if it's positive or its negation if it's negative.
Assert two values are equal.
Assert that the given events were emitted during the construction of the last block, in the specified order.
Events are emitted in application code using op_context.emit_event()
.
Example - single event:
Application code
operation main() {
op_context.emit_event("my_message_type", "my_data".to_gtv());
}
Test code
function test_main() {
main().run();
// Assertion passes
rell.test.assert_events(("my_message_type", "my_data".to_gtv()));
}
function test_main_bad_1() {
main().run();
// Assertion fails - wrong message type
rell.test.assert_events(("other_message_type", "my_data".to_gtv()));
}
function test_main_bad_2() {
main().run();
// Assertion fails - wrong message content
rell.test.assert_events(("my_message_type", "other_data".to_gtv()));
}
Example - multiple events:
Application code
operation main() {
op_context.emit_event("my_message_type", "my_data".to_gtv());
op_context.emit_event("other_message_type", "other_data".to_gtv());
}
Test code
function test_main() {
main().run();
// Assertion passes
rell.test.assert_events(
("my_message_type", "my_data".to_gtv()),
("other_message_type", "other_data".to_gtv())
);
}
function test_main_bad() {
main().run();
// Assertion fails - event order incorrect
rell.test.assert_events(
("other_message_type", "other_data".to_gtv()),
("my_message_type", "my_data".to_gtv())
);
}
To assert a subset of events, or to assert independently of event order, use rell.test.get_events()
and make assertions over elements of the returned list.
Asserts a function fails; i.e. throws an exception.
Example
Application code
// This will throw a exception.
function bad(): integer {
return [0][1];
}
Test code
// This test will pass.
function test_bad() {
rell.test.assert_fails(bad(*));
}
Asserts a function fails; i.e. throws an exception; with a given exception message.
Verifies that the given exception message is a substring of the exception thrown by the given function (if thrown).
Example
Application code
// This will throw a exception.
function bad(): integer {
return [0][1];
}
Test code
// This test will pass.
function test_bad() {
rell.test.assert_fails("out of bounds", bad(*));
}
Assert a value is false
.
Prefer other assertion functions where possible, as they provide better error messages.
Assert that a value is greater than or equal to a given lower bound.
Assert that a value falls within given bounds.
Specifically, assert that the value is greater than or equal to a lower bound, and less than or equal to an upper bound.
Assert that a value falls within given bounds.
Specifically, assert that the value is greater than or equal to a lower bound, and less than an upper bound.
Assert that a value is greater than a given lower bound.
Assert that a value falls within given bounds.
Specifically, assert that the value is greater than a lower bound, and less than or equal to an upper bound.
Assert that a value falls within given bounds.
Specifically, assert that the value is greater than a lower bound, and less than an upper bound.
Assert that a value is less than or equal to a given upper bound.
Assert that a value is less than a given upper bound.
Assert two values are unequal.
Assert a value is not null
.
Assert a value is true
.
Prefer other assertion functions where possible, as they provide better error messages.
Compute an Ethererum public key from a signature and a hash.
Similar to Solidity's ecrecover()
, though differs in that:
This function takes
rec_id
, rather thanv
, whererec_id == v - 27
.The parameter order is different.
This function returns a 64-byte public key, not a 20-byte address. An address can be obtained by taking the last 20 bytes of the
keccak256()
digest of the returned public key, e.g.:
val address: byte_array = keccak256(eth_ecrecover(...)).sub(44);
The signature (consisting of the r
, s
and rec_id
components) will typically be obtained with a procedure equivalent to eth_sign(data_hash, privkey)
, where privkey
and pubkey
form a keypair (pubkey
being returned form this method).
The signature component rec_id
is an adjusted recovery identifier, equivalent to Ethereum's recovery identifier (usually denoted as v
) minus 27, i.e. rec_id == v - 27
.
The given 32-byte array data_hash
is typically a cryptographic hash obtained from a larger data structure using a hashing function such as hash()
, sha256()
or keccak256()
.
Example
The following is a Node.js script which uses ecrecover()
(the equivalent to crypto.eth_ecrecover()
) from the Ethereum Web3 library:
const Web3 = require('web3');
const web3 = new Web3();
var r = '0xcf722a47bcf1da61967ccc6405e31db4d37bce153255a6937e5cceb222caead0';
var s = '0xcf722a47bcf1da61967ccc6405e31db4d37bce153255a6937e5cceb222caead0';
var h = '0x53d7b11e61a8059aa4bc3248d24b2936436c9796dfe7f18e414c181004f79427';
var v = '0x1c';
var address = web3.eth.accounts.recover({'r':r,'s':s,'messageHash':h,'v':v});
console.log(address); // prints 0x5b0c087542D5C1E66Df0041e179c4201675B1614
An equivalent script in Rell is as follows:
val r = x'cf722a47bcf1da61967ccc6405e31db4d37bce153255a6937e5cceb222caead0';
val s = x'cf722a47bcf1da61967ccc6405e31db4d37bce153255a6937e5cceb222caead0';
val h = x'53d7b11e61a8059aa4bc3248d24b2936436c9796dfe7f18e414c181004f79427';
val v = 0x1c;
val pubkey = eth_ecrecover(r, s, v - 27, h);
val address = keccak256(pubkey).sub(12);
print(address); // prints 0x5b0c087542d5c1e66df0041e179c4201675b1614
Note that in the Rell script, v
is an integer, while in the Node.js script it is an 0x
-prefixed hexadecimal string.
Checks if a value is present, i.e. not null
and not empty.
The negation of empty()
.
Accepts arguments of nullable type (T?
), checking that they are not null, and of collection type (collection<T>
), checking that they contain at least one element. Where an argument is a nullable collection (collection<T>?
), it is checked both for non-nullity and non-emptiness.
Examples:
val x: integer? = null; exists(x)
returnsfalse
val y: integer? = 1; exists(y)
returnstrue
val l1: list<integer>? = null; exists(l1)
returnsfalse
val l2: list<integer>? = []; exists(l2)
returnsfalse
val l3: list<integer>? = [1]; exists(l3)
returnstrue
Note that when exists()
is used within a database at-expression, and its argument is also a database at-expression, the inner at-expression can refer to entities of the outer one, as long as the inner uses @*
(as opposed to @
, @+
or @?
). Such expressions are efficient as they can be translated into a single nested SQL query.
Inner at-expressions can use @
, @+
or @?
, but in those cases, the inner expression cannot refer to entities of the outer one, and such cases are typically slow as they cannot be translated into a single nested query. They must instead be translated into multiple SQL queries, with the existence check occurring in the Rell runtime rather than in the database, where it could be done more efficiently. The translation into multiple queries also prevents the inner expression from referencing the outer expression, as this is only possible through a translation into SQL that leverages the scoping rules of nested SQL queries.
Examples:
user @* { exists( company @* { .city == user.city } ) }
returns alluser
s who share acity
with acompany
.user @* { exists( company @* { user.city } ) }
is equivalent to the above, but uses the more concise attribute matching syntax.
Check if a given public key is a signer of the current transaction; i.e. if it's in the list of signers returned by op_context.get_signers()
.
Compute the Keccak256 digest (hash) of the given byte array.
Returns the greater of two big_integer values; i.e. a
if a > b
, or b
otherwise.
Returns the greater of two decimal values; i.e. a
if a > b
, or b
otherwise.
Returns the greater of two integer values; i.e. a
if a > b
, or b
otherwise.
Returns the lesser of two big_integer values; i.e. a
if a < b
, or b
otherwise.
Returns the lesser of two decimal values; i.e. a
if a < b
, or b
otherwise.
Returns the lesser of two integer values; i.e. a
if a < b
, or b
otherwise.
Asserts that a value is non-null.
Asserts that a list is non-null and non-empty.
Asserts that a map is non-null and non-empty.
Asserts that a set is non-null and non-empty.
Asserts that a value is non-null.
Asserts that a list is non-null and non-empty.
Asserts that a map is non-null and non-empty.
Asserts that a set is non-null and non-empty.
Compute the SHA-256 digest (hash) of the given byte array.
Safely call a function that may fail (i.e. that may throw an exception).
Accepts nullary function references, i.e. references to functions of type () -> T
, and returns T?
, i.e. the return value of the reference function, or null
.
Exceptions thrown during the call are caught and logged with a stack trace.
Changes to the database that occur during the call are rolled back when an exception is thrown.
Examples:
function fails(): integer {
return list<integer>()[1]; // out of bounds
}
function succeeds(): unit { return 0; }
try_call(fails(*)) // logs an out of bounds exception message and returns null
try_call(succeeds(*)) // logs nothing, returns 0
Safely call a function that may fail (i.e. that may throw an exception).
Accepts nullary unit-typed function references, i.e. references to functions of type () -> unit
.
Exceptions thrown during the call are caught and logged with a stack trace.
Changes to the database that occur during the call are rolled back when an exception is thrown.
Examples:
function fails(): unit {
list<integer>()[1]; // out of bounds
}
function succeeds(): unit {}
try_call(fails(*)) // logs an out of bounds exception message and returns false
try_call(succeeds(*)) // logs nothing, returns true
Safely call a function that may fail (i.e. that may throw an exception).
Accepts nullary function references, i.e. references to functions of type () -> T
, and a default value to return if the call fails.
Exceptions thrown during the call are caught and logged with a stack trace.
Changes to the database that occur during the call are rolled back when an exception is thrown.
Examples:
function fails(): integer {
return list<integer>()[1]; // out of bounds
}
function succeeds(): unit { return 0; }
try_call(fails(*), 17) // logs an out of bounds exception message and returns 17
try_call(succeeds(*), 17) // logs nothing, returns 0
Verify a signature against a message and public key.
More precisely, verify that signature
was obtained with a procedure equivalent to get_signature(data_hash, privkey)
, where privkey
and pubkey
form a keypair.
Accepts valid public keys of size 33
or 65
bytes. Note that not all byte arrays of acceptable length constitute valid public keys.