Data types
JARU is a dynamically typed language: the type belongs to the value, not to the variable. The same variable can hold an integer and, later on, a string, and you don't have to say beforehand which type it will have.
All values are first-class: they can be stored in variables, passed to functions and returned from them.
| Type | Description | Example |
|---|---|---|
| bool | True or false. | true, false |
| nil | The absence of a value. | nil |
| integer | 32-bit whole numbers. | 123, 0x1F |
| float | Double-precision decimal numbers. | 0.456, 1e3 |
| string | Text strings, in single or double quotes. | 'Goku', "Krilin" |
| list | An ordered collection of elements that grows and shrinks. | [1, 2, 3, 4] |
| map | A collection of key-value pairs. | {"key1": "value1", "key2": "value2"} |
| array | A fixed-size collection, with one or more dimensions. | {1, 2, 3, 4} |
Functions, classes, their instances and structs are values too: see Functions, Classes and objects and Structs.
Bool
The boolean type represents a logical value: true or false. You can also write on and off: they are the same values (on == true is true).
var isAdult = true
var hasPermission = false
Booleans come out of comparisons and are used in control structures: if, while, for...
var age = 18
if (age >= 18) then
print("Is an adult")
else
print("Is a minor")
end
In the example, age is compared with 18: if it's greater than or equal, "Is an adult" is printed; otherwise, "Is a minor". Which other values count as true or false in an if is in What is false.
Nil
nil is the absence of a value. A function that ends without return returns it, and you can assign it to say that a variable doesn't hold anything yet:
var target = nil
if (target == nil) then
println("No target")
end
A variable declared without a value isn't nil but 0: var x is the same as var x = 0.
nil has a single method:
| Method | Returns |
|---|---|
toString() | The string "nil". |
Any other method gives the error ERR_ATTRIBUTE_NOT_FOUND (Nil has no method 'size'.). It often happens when a function returns nil and you call a method on what it returned.
Integer
Integers are whole numbers, 32-bit and signed: they go from -2147483648 to 2147483647.
var x = 10
var mask = 0xFF
A number written without a decimal point or exponent is an integer. If it doesn't fit in 32 bits, the compiler stores it as a float: 3000000000 is a float. Hexadecimal numbers, on the other hand, stay in 32 bits: 0xFFFFFFFF is the integer -1.
Overflow
An integer addition, subtraction or multiplication that goes out of range doesn't raise an error: it wraps around and keeps the lower 32 bits.
var big = 2147483647
println(big + 1) // -2147483648
println(big * 2) // -2
var thousand = 100000
println(thousand * thousand) // 1410065408
println(thousand * 1.0 * thousand) // 10000000000
If a calculation can go beyond two billion, do it with floats: it's enough for one of the numbers to be a float, as in the last line.
Integer methods
Methods are called on a variable, not on a written number: n.toHex() works and (255).toHex() doesn't compile (see Basic syntax).
| Method | Returns | With n = 255 |
|---|---|---|
isEven() | true if it's even. | false |
isOdd() | true if it's odd, negative numbers too. | true |
toBinary([digits]) | A string with the number in binary. digits, from 1 to 32, pads with zeros on the left. | "11111111"; with 12, "000011111111" |
toHex([digits]) | A string with the number in hexadecimal, uppercase and without 0x. digits, from 1 to 8, pads with zeros. | "FF"; with 4, "00FF" |
toBool() | false if it's 0, and true otherwise. | true |
toByte() | The lower 8 bits: an integer from 0 to 255. | 255 (300 gives 44 and -1 gives 255) |
toFloat() | The same number, as a float. | 255 (float) |
toInt() | The same integer. | 255 |
toString() | A string with the number. | "255" |
toUnsigned() | Its unsigned value: a negative number becomes a float, with its 32 bits read as unsigned. | 255 (-1 gives 4294967295, a float) |
- If the number needs more digits than you ask for, it gets them all:
n.toBinary(4)gives"11111111". Asking for more than 32 binary digits or 8 hexadecimal ones is the same as asking for the maximum. - With a negative number,
toBinary()andtoHex()show its 32 bits:-1gives 32 ones and"FFFFFFFF". - If
digitsisn't an integer, the error isERR_TYPE_MISMATCH; with more arguments than a method takes,ERR_INVALID_ARG_NUMBER.
var n = 255
println(n.isOdd()) // true
println(n.toHex()) // FF
println(n.toHex(4)) // 00FF
println(n.toBinary(12)) // 000011111111
var negative = -1
println(negative.toHex()) // FFFFFFFF
println(negative.toByte()) // 255
var big = 300
println(big.toByte()) // 44
toUnsigned() is for a 32-bit value with the top bit set, like 0xDEADBEEF read from a sensor: as an integer it comes out negative, and toUnsigned() gives its unsigned value. It doesn't fit in a JARU integer, so it comes out as a float, like a written number that doesn't fit in 32 bits:
var reading = 0xDEADBEEF
println(reading) // -559038737
println(reading.toUnsigned()) // 3735928559
Float
Floats are double-precision floating-point numbers (64 bits, IEEE 754): they keep about 15 significant digits. A number written with a decimal point or an exponent is a float: 3.14, 1.0, 2.5e-3.
var x = 3.14
How they are printed
println writes a float with just the digits needed so that, read back, it's the same number: 0.1 comes out as 0.1, and 0.1 + 0.2, which isn't exactly 0.3, comes out as 0.30000000000000004. toString() rounds it to 15 digits. A float without a fractional part is written without a point: 10.0 is written 10, although it's still a float.
The floats you write in the code don't lose digits, even when they're very small or very large: 1.23456789e-10 and 1e300 reach the program as they are. Only one smaller than 2.2e-308, not counting 0, becomes 0.
var total = 0.1 + 0.2
println(total) // 0.30000000000000004
println(total.toString()) // 0.3
var ten = 10.0
println(ten, " ", type(ten) == tFloat) // 10 true
Float methods
| Method | Returns | Example |
|---|---|---|
toInt() | The integer, dropping the decimals: it truncates toward zero. | 3.75 gives 3, and -3.75 gives -3 |
toBool() | false if it's 0.0, and true otherwise. | 0.1 gives true |
toFloat() | The same float. | 3.75 gives 3.75 |
toString() | A string with up to 15 significant digits. | 0.1 + 0.2 gives "0.3" |
- Outside the integer range,
toInt(), likeint(), keeps the lower 32 bits:3000000000.0gives-1294967296. - Floats don't have
isEven(),toHex()or the other integer methods: calling them givesERR_ATTRIBUTE_NOT_FOUND(Float has no method 'isEven'.). Convert them to an integer first withtoInt().
NaN and infinity
An operation without a numeric result, like sqrt(-1), gives NaN (not a number). And a result too big for a float gives infinity.
nan == nanisfalse, andnan != nanistrue. To find out whether a value is NaN, compare it with itself:v != vis only true with NaN.- NaN is true in an
if. - Lists and maps do find a NaN: a list that holds one gives
trueincontains(). - Dividing by zero doesn't give infinity or NaN: it throws
ERR_DIVISION_BY_ZERO, with floats too. - When printed, infinity comes out as
inf. How NaN comes out depends on the platform: on Windows,-nan(ind).
var root = sqrt(-1)
println(root == root) // false
println(root != root) // true: it's NaN
var big = 1.0e100
var infinity = big * big * big * big
println(infinity) // inf
Integers, floats and division
If there's a float in an operation, the result is a float: 1.5 + 2 gives 3.5. Between two integers, +, -, * and div give an integer, and / always gives a float, even when the division is exact: 6 / 3 is the float 2.
For an integer division, use div: it truncates toward zero, so 7 div 2 is 3 and -7 div 2 is -3. Use it especially when the result will be an index: a list doesn't accept a float index (ERR_TYPE_MISMATCH, List index must be an integer.), and a float doesn't have the integer methods.
println(7 / 2) // 3.5
println(6 / 3) // 2 (it's a float)
println(7 div 2) // 3
println(-7 div 2) // -3
var list = [10, 20, 30, 40]
var middle = list.size() div 2
println(list[middle]) // 30
An integer and a float with the same value are equal: 1 == 1.0 is true. To convert from one type to another there are int(), float() and round(), in the Standard library, and the toInt() and toFloat() methods. All the operators are in Operators.
/between two integers is calculated in double precision. It used to be done in single precision:1 / 3gave0.3333333432674408andint(80 / (40 / 30))gave59. Now they give0.3333333333333333and60.divbetween integers is exact over the whole 32-bit range; it used to fail from 16,777,216 on.- NaN is no longer equal to itself, and
0.0 == -0.0istrue. - On the ESP32-P4,
int()andtoInt()with a negative float gave0. Now they truncate the same way on every platform.
String
The string type holds text: a word, a sentence or a paragraph. It's written in double quotes or single quotes:
var name = "John"
var phrase = 'Hello world!'
var paragraph = "This is an example paragraph with multiple lines of text."
Strings have methods. For example, size() gives the length of a string and find() looks for some text inside it:
// Get the length of a character string
var name = "John"
println(name.size()) // Prints 4
// Search for a substring within a string
var phrase = "Hello world!"
println(phrase.find("world")) // Prints 6
All their methods, the quotes, the escape characters and the pitfalls are in Strings.
List
A list holds a collection of values, of the same type or of different types. It's written with the values between square brackets [ ]:
var names = ["John", "Peter", "Michael"]
var ages = [25, 30, 35]
var mixed = ["John", 25, true]
Lists can be changed after they're created. You reach each element with its index between square brackets, starting at 0:
print(names[0]) // prints John
names[1] = "Andrew" // changes the value at position 1 to "Andrew"
Lists have methods for the usual operations. For example, append(value) adds an element at the end:
var names = ["John", "Peter"]
names.append("Michael") // the names list now contains ["John", "Peter", "Michael"]
All the list methods, foreach and how to copy them are in Lists, maps and arrays.
Map
A map holds key-value pairs, like a dictionary. It's written with the pairs between braces {}:
var ages = {
"John": 25,
"Peter": 30,
"Michael": 35
}
You can also create an empty map and add the pairs later:
var ages = {}
ages["John"] = 25
ages["Peter"] = 30
ages["Michael"] = 35
You reach a value with its key between square brackets []:
var johnsAge = ages["John"] // johnsAge contains 25
Its methods (size(), keys(), values(), contains(key), remove(key)...) are in Lists, maps and arrays.
Array
An array holds an ordered sequence of elements, like a list, but with a fixed size: once created, it doesn't grow or shrink.
It's written with the elements, separated by commas, between braces {}:
var numbers = {1, 2, 3, 4, 5}
var names = {"John", "Peter", "Michael"}
You can also create it empty, with its size, and assign the values later. Its elements start at 0:
var names[10]
names[8] = "This is a demo"
You reach an element with its index between square brackets [], starting at 0:
var numbers = {1, 2, 3, 4, 5}
var secondNumber = numbers[1] // secondNumber contains 2
Its methods (size(), numdim(), dim(), fill(value)...) are in Lists, maps and arrays.
Multidimensional arrays
An array can have several dimensions: var table[10][20] creates an array of 10 × 20 elements. You reach them with one index per dimension, table[i][j]. It's a single array with two indexes, not an array of arrays: size() gives the total number of elements (200), and dim(), the size of each dimension ([10, 20]).
var array = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
// Access a specific element in the matrix
println(array[1][1]) // Output: 5
// Iterate through matrix elements
for (var i = 0; i < 3; i++)
for (var j = 0; j < 3; j++)
println(array[i][j])
end
end
Examples of array declarations with different numbers of dimensions:
// One-dimensional array
var array1D = {1, 2, 3, 4, 5}
// Two-dimensional array
var array2D = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
// Three-dimensional array
var array3D = {{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}, {{10, 11, 12}, {13, 14, 15}, {16, 17, 18}}}
What is false
In an if, in a while and with !, and and or, these values count as false:
falseandnil;- the integer
0and the float0.0(also-0.0); - the empty string
""; - an empty list
[], an empty map{}and an array of size 0.
Everything else is true: "0", "false", " ", [0], NaN, instances, structs, functions...
func describe(v)
if (v) then
return "true"
end
return "false"
end
println(describe(0)) // false
println(describe("")) // false
println(describe([])) // false
println(describe("0")) // true
println(describe([0])) // true
Finding the type: type()
type(value) returns a number that tells you a value's type. Compare it with the type constants (tInteger, tFloat, tString, tList, tMap...), which are all in the Standard library.
func whatIs(v)
if (type(v) == tInteger) then
return "integer"
elsif (type(v) == tFloat) then
return "float"
elsif (type(v) == tString) then
return "string"
end
return "something else"
end
println(whatIs(7)) // integer
println(whatIs(7 / 2)) // float
println(whatIs("7")) // string
typeis a reserved word, and it always takes parentheses.- An integer and a float have different types even when they have the same value:
1 == 1.0istrue, buttype(1) == type(1.0)isfalse. - Functions and lambdas give
tFunction. A method taken from an instance without calling it (var m = player.greet) givestMethod. A class givestClass, and its instances,tInstance. - Sprites,
Bytesand other module objects givetAbstract. - The values of an
enumare integers: they givetInteger. - There's no constant for structs:
type()gives23for a struct and24for its declaration. - An error value, the one some functions return when they fail (see Errors and exceptions), gives
tError, like the error of acatch.
All the known issues of JARU, together, are in Known issues.