Native types are implemented by operating system. We name native types using a single lowercase letter followed by the number of bytes used: {8, 16, 32, 64}. In next table we show the convetion of notation for these types.
Type Name | Name | Min | Maxim |
---|---|---|---|
i | Signed | 8 | 64 |
u | Unsigned | 8 | 64 | f | Float | 32 | 64 |
This will give us the native types used in Eve:
Native types are used by low level EVE core libraries and not usually necessary in Eve scripts. The system library is using these types to create Primitive Types and communicate with other languages. A native type can be boxed using a primitive type. We will define next the primitive types.
Primitive types are predefined in Eve core library. They have an equivalent native type. Primitive types are slower than equivalent native types, ocupy more memory but they have more features. Primitive types are created using OOP so tey are actually classes.
Class Name | Description | Minim | Maxim |
---|---|---|---|
Byte | Is a Numeric type having range: (0x00..0xFF) | 0 | 2^8-1 |
Short | Is a Numeric type having range: (0x0000..0xFFFF) | -2^15 | +2^15-1 |
Integer | Signed on 64 bit | -2^63 | 2^63-1 |
Natural | Unsigned on 64 bit | 0 | 2^64-1 | Real | Float precision number on 8 bytes | -n | 1.8 × 10^308 |
Symbol | Single symbol UTF-8 encoded, 4 bytes | 0 | 2^32-1 |
Ordinal | Enumeration of named Short integers | 0 | 2^16-1 |
Time | Time of the day (24h) | 0 | 8.64e+7ms |
Duration | Period of time | 0 | 2^16 |
Logic | Is a Ordinal subtype having values: False = 0, True = 1 | 0 | 1 |
Primitive types can be converted in native types using .value() method. This method will return the native type equivalent and will perform "Unboxing" operation. Value can't return Null.
In Eve we can have two categories of numbers:
Category | Eve Types |
---|---|
Discrete | Byte, Short, Integer, Natural, Ordinal, Range, Symbol, Duration |
Continuous | Float, Real, Rational, Complex |
Discrete numbers have a range from negative to positive number that can be calculate easly by the number of bytes. We have equivalent native numbers for every number of bytes from 1 to 64.
type | Chars | Equivalent | min | max | maximum number |
---|---|---|---|---|---|
Integer | 20 | i64 | -2^63 | 2^63-1 | ≤ 9,223,372,036,854,775,807 |
Natural | 20 | u64 | 0 | 2⁶⁴-1 | ≤ 18,446,744,073,709,551,615 |
For conversion into characters:
The type Real is represented using floating precision numbers.
Floating decimal numbers are most simply described by 3 Integers:
The numerical value of a finite number is −1ˢ × c × 2ⁿ Using this formula Eve define two floating point types.
Single: is single-precision 32-bit IEEE 754:
Real is Real-precision 64-bit IEEE 754:
type | Digits | Equivalent | maximum number |
---|---|---|---|
Float | 7 | f32 | ≤ 3.4 × 10^38 |
Real | 16 | f64 | ≤ 1.8 × 10^308 |
Rational | 20 | q64 | ≤ 2^64(*) |
Note Precision and range for Rational numbers is variable depending on the resolution. Ratiobal numbers are are using fixed precision arithmetics. When precision is 1, Rational numbers are equivalent to int64 that is a very large number using 20 digits.
Example | Description |
---|---|
0 | Integer zero |
123 | Integer number using symbols: {0,1,2,3,4,5,6,7,8,9} |
1/2 | Single number use symbols: {.,0,1,2,3,4,5,6,7,8,9} |
0.5 | Real number use symbols: {.,0,1,2,3,4,5,6,7,8,9} |
#numeric literals demo
driver numeric_literals:
** define global states
set g1 = 0.0 :Real;
set g2 = 0.0 :Float;
** define main process
process:
** check equality
expect g1 == g2; -- value is equivalent
expect g1 is not g2; -- not the same
expect g1 not eq g2; -- different types
** define local variables
new i :Integer; -- Initial value = 0
new n :Natural; -- Initial value = 0
new r :Real; -- Initial value = 0.00
** use modifier := to change value
let i := 9223372036854775807; -- maximum
let n := 18446744073709551615; -- maximum
let r := 1/2; -- 0.5
return;
See also: scientific notation
We name Symbol type a single UNICODE UTF32 encoded sumbol. You can use single quoted string to encapsulare a single symbol. Single quoted strings can store a single UTF8 code point but it has a fixed size of 4 bytes unlike C that has 1 byte for char. This type is called "rune" in Go language but we call it Symbol. Though we know that some people reffer to many characters together to create a single symbol.
process:
** several examples of local Symbol type variables.
new letter := 'a';
new capital := 'B';
new number := '5';
new unicode := '&beta';
new operator := '¬';
...
return;
As you can se in the example Symbol literal is enclosed in single quotes '' follow by ";" to end the declaration statement. In Eve a Symbol can hold 4 bytes but only one ASCII symbol or UNICODE symbol.
Unicode literals start with prefix U+ or U-. Such literals is compatible with single quoted Symbol code point. Unicode literals are case insensitive hexadecimals and do not require single quotes.
process:
** Several examples of local Symbol type variables.
new letter := U+0061; -- Unicode for lowercase 'a'
new capital := U+0042; -- Unicode for uppercase 'B'
new number := U+0035; -- Unicode for digit '5'
new unicode := U+03B2; -- Unicode for beta (β) character
new operator := U+00AC; -- Unicode for NOT operator (¬)
...
return;
In Eve, "boxing" and "unboxing" are crucial concepts that describe the conversion between native types and their corresponding primitive types.
Boxing is the process of wrapping a native type value within a primitive type object. This allows native types to be treated as objects, giving them access to methods and properties defined in the primitive type classes.
** Boxing (often implicit)
new x :Integer = 5; -- 5 (native i64) is boxed into an Integer object
Unboxing is the reverse process of extracting the native type value from a primitive type object. This is typically done when you need to perform low-level operations or interface with system libraries that expect native types.
** Unboxing (explicit)
let native_value :i64 = x.value(); -- Extracts the i64 value from the Integer object
.value()
to retrieve the native value.
process boxing_demo:
** Boxing
new boxed_integer :Integer = 42; -- 42 is boxed into an Integer object
** Using a method available on the boxed value
print boxed_integer.to_binary(); -- Prints binary representation
** Unboxing
let raw_value :i64 = boxed_integer.value();
** raw_value is now a native i64, without object methods
print raw_value; -- Prints 42
** This would cause an error:
print raw_value.to_binary(); -- Error: i64 has no method 'to_binary'
return;
Understanding boxing and unboxing is essential for efficient memory management and performance optimization in Eve programs, especially when working with large datasets or in performance-critical sections of code.
Note: We define implicit data types for specific data literals. For most cases, impicit data type works correctly. Sometimes, implicit data type can create undesired data type. This is because data literals may overlap between different data types. To avoid this case you should use a type hint.
Class Name | Description |
---|---|
Object | Root class for all composite data types |
Record | Flat collection of native data types. (No objects) |
Variant | A union of possible data types. |
Range | Discrete range of numbers equaly distanced (x..y:ratio) |
Pair | A pair is like a list but it has only two elements (k:v) |
String | Real quote delimited symbol: "..." |
Date | Calendar date. |
List | Dynamic ordered enumeration of values of same type |
HashMap | Enumeration of (key:value) pairs unique sorted by key |
DataSet | Enumeration of unique elements of the same type |
Table | A collection of records indexed by unique key. |
Function | Function object type/ reference to function. |
Vector | Single-dimensional, fixed size array |
Matrix | Multi-dimensiona, fixed size array |
Error | Composite type derived from Object base class that signify an exception. |
Null | Null is a constant object (not a class) that represents the absence of a valid data structure for composite types. |
A range is a notation that describe a set of discrete elements. Range is not expanded in memory. Ranges enable creation of efficient and comprehensive statements in a declarative way.
Ranges can be used in collection builders, make logic evaluations, or generate values for loops. Range logic operations should be optimized by the compiler. In for loops, elements will be generated one by one as needed until the loop end.
In next example we creste a range class. The new range is actually a subclsdd of Range. We will learn later how to use ranges in different cases. We use .. and : to define the range.
class range = (min..max:ratio) <: Range;
Numeric ranges:
#numeric range demo
driver numeric_range:
** define a validation class
class R5 = (0..5) <: Range;
process:
** test range limits
expect 0 in R5; -- assert logic expression
expect 5 in R5; -- assert logic expression
** test limits with range literals
expect 4 in (1..8:2);
expect 5 not in (-10..10:2);
return;
Symbol range, specify Unicode symbols as limits.
# using symbolic ranges to print out a collection that do not exist
driver symbol_range:
process:
** elements of these ranges are written one by one
print ('0'..'5') -- ('0','1','2','3','4','5')
print ('a'..'f') -- ('a','b','c','d','e','f')
print ('A'..'F') -- ('A','B','C','D','E','F')
** use range as validation domains
expect '0' in ('0'..'9'); -- range of digits
expect 'c' in ('a'..'z'); -- range of symbols
expect 'X' in ('A'..'Z'); -- range of symbols
return;
It is possible to create a decimal range. For this, the limits must be decimal or at least the ratio must be decimaL. If ratio is not specified, it is automaticly created: 0.1 or 0.01 depending on precision of first limit. 0.0 or 0.00. In Eve the number of zeros can establish the precision for numeric ranges.
# using fractional ranges
driver fraction_range:
process:
** elements of these ranges are written one by one
print (0..0.5:0.1) -- (0.0,0.1,0.2,0.3,0.4,0.5)
** use range as validation domain
expect 0.01 in (0.0..1.0);
expect 0.001 in (0.00..1.00);
expect 0.001 not in (0.0..1.0);
return;
The range and domain are iterable and have a number of methods that are useful for data processing but these are not collections but generators. One method is very useful and is going to be used in many examples: random(). It is implemented for all collections.
# domain demo
driver random_demo:
class Small = [0..1:0.1] <: Range;
class Large = (0..2^64 ) <: Range;
process:
** you can call random() like this
print Small.random(); -- 0.3
print Large.random(); -- 65535
** you can call random() like this
print random(1..2^32:2); -- 143453566436
print random(1..2^32:0.25); -- 43.75
return;
In computer science coercion is used to implicitly or explicitly change an entity of one data type into another of different type. This is ready to take advantage of type hierarchies and type representations. If not designed properly the coercion can be a fatal mistake. Eve is a safe language so we do only safe coercion.
Implicit coercion In Eve the arithmetic operators are polymorphic. Numeric operators can do implicit data conversion to accommodate the data types and return an accurate result. Automatic conversion is possible only when there is no risk of loosing data precision. If there is a loss of precision we can end-up with a run-time error. To prevent this Eve will implement a safe compile-time check.
#example of implicit conversion
driver implicit_coercion:
process:
** local variables
new a := 2;
new b := 1.5;
** alter a, b
let b := a; -- this implicit cast is possible b = 2.0
let b := a + 3.5; -- add 3.5 then assign result to b = 5.5
let a := b; -- error: can not assign Real to Integer
let a := 1.5; -- error: can not assign Real to Integer
return;
Explicit coercion Explicit coercion is a forced conversion. Can be used to convert backwards from higher data range to lower data range or from continuous numbers to discrete numbers. This however can cause a data or precision loss. Explicit coercion is using a function.
Examples of explicit coercion:
# explicit coercion in EVE
driver explicit_coercion:
set a = 0 :Integer;
set b = 1.5 :Real;
process:
**explicit coercion lose (0.5)
let a := floor(b);
write a; -- will print: 1
**explicit coercion add (0.5)
let a := ceiling(b);
print a; -- will print: 2
**explicit coercion rounding:
let a := round(b);
print a; -- will print: 2
return;
Number to a string
#convert number to string
driver number_to_string:
** local states
set s :String;
set v :Integer;
process:
let v := 1000;
let s := format(v); -- explicit coercion s = "1000"
expect (s == "1000");
return;
String to a number
This can be ready using the casting function parse(), only if the string contains a number. Otherwise the conversion fail and will rise and exception.
#string to number conversion
driver string_to_number:
** global states
set v :Integer ;
set b :Real ;
set s = "1000" :String;
set r = "200.02":String;
process:
let v := parse(s); -- make v = 1000
let v := parse(r); -- make v = 200 and decimal .02 is lost
let b := parse(r); -- make b = 200.02 and decimal .02 is preserved
return;
Note: Build-in functions that are located in EVE default library: { parse(), format(), ceiling(), floor() round()}. This module is one of the standard modules that are automatically included in any Eve program.
Literals are representations of specific particular data type in source code. We connect data literals to default types for every literal to be able to use type inference in declarations and expressions. In next table we show this association.
Note Next notation use "9" to show any digit in range (0..9) but it can be any digit. We also use A..F to show hexadecimal numbers examples. It can be of course any digit or letter in hexadecimal range.
Literal | Type |
---|---|
9 | Integer |
-9 | Integer |
0x9ABCDEF | Natural |
0b1010101 | Binary |
9.9 | Real |
U+0001 | Word |
U-FFFFFFFF | Binary |
Zero literals
Literal | Type |
---|---|
[] | Vector |
{} | Object/DataSet/Table |
() | List/Range/Pairs |
"" | Text |
'' | String |
0 | Integer |
0.0 | Real |
Collection literals
Literal | Type |
---|---|
(1, 2, 3) | List[Integer] |
("1","2","3") | List[String] |
{a:0, b, c} | Ordinal |
{a:"x",y:2} | Object |
{3, 4, 6} | DataSet[Integer] |
[1, 2, 3] | Vector[Byte](3) |
['a','b','c'] | Vector[Symbol](3) |
["a","b","c"] | Vector[String](3) |
[[1,2],[2,4]] | Matrix[Integer](2,2) |
Type inference is a logical association of data type from a constant literal or expression. Type inference can be used in declarations of new variables with the specific assign operators capable to infer data types.
Type inference is enabled by operators {:=, ::}. You can use type inference with keyword "new" but not with heyword "set". Type inference can improve a type declaration if the declaration is partial using operator "let".
In next example we use a List data literal to create a shared collection fo type List[Integer]. However the type used is partial. Do not have element type, that is going to be Integer.
#test partial inference
driver type_inference:
** Define a list without element type
set ls :List; -- partial type declaration
process main
** establish element data type
let ls := (0,1,2,3,4,5,6,7,8,9); -- gradial typing
print ls.type();
expect type(ls[1]) is Integer;
return;
You can declare empty collections using partial inference and determine the type of elements later, using "let". After type is established, it can't be changed later and start functioning as a contract. That is, you must respect the element types for next expressions.
# gradual type declaration
process:
new a := (); -- empty List
new v := []; -- empty Vector
new s := {}; -- empty DataSet
** add elements using gradual typing
let a := (10,11,12) -- establish element type integer
let a += "4" -- expect error, you can't add a string now
return;
We can verify the type using "is" operator:
# using operator "is" to check type
driver type_check:
** define object and initialize
set r = {name:"test", age:24} :Object;
** define hash table
set t = {'key1':"value1",'ley2':"value2"} :DataSet;
process:
** check variable types using introspection
expect type(r.name) is String;
expect type(r.age) is Integer
expect type(t.key) is Symbol;
expect type(t.value) is String;
return;
For type introspection we can use type() built-in function:
# introspection demo
process print_type:
new i := 1.5;
expect type(i) is Real;
print "type of i is #s" ? i.type();
return;
In mathematics there are very few operators: {+, -, ÷ , * } that can operate with any kind of numbers: negative, positive, rational or real. Operators are not bound to specific data types. Therefore these operators are called "polymorphic".
In EVE, operators are functions. To design polymorphic operators we overload the function signature using type dispatch. The dispatch is using left side operand first, this is the leading operand. For unary operators there is only right side operand so this becomes the leading operand.
In Latin the "falsus" and "verum" are translated in English to "false" and "true". In many languages False and True are two constants. In Fortran we use to have .T. and .F. while in C we use to have {0, 1}. Two values.
name | value | binary |
---|---|---|
False | Logic.False | 00000000 00000000 |
True | Logic.True | 00000000 00000001 |
** explicit initialization
global
set False = 0b0 :Byte;
set True = Ob1 :Byte;
Internal design
In Eve we Logic type as a subtipe of Ordinal:
class Logic = {False:0 , True} <: Ordinal;
So in Eve we have False.value == 0 and True.value == 1. These are Logic values not Boolean as in other languages. You can use False and True with == or != to establish conditionals.
Logical expressions
A logical expression is a demonstration or logical deduction having result True or False. Operator precedence is: {not, and, or, xor}. The order of operations can be controlled using operator precedence and round parentheses. Relation operators {>, <, >=, <=, ==, !=} have higer precedence than logic operators.
Result of logical expressions can be used into a conditional statement to make a decision. Also results of logical expressions can be stored in logical variables to be used later in other conditions.
Gradual typing is a type system in which some variables may be given a type. Some variables or members may be left un-typed. Therefore some type errors are reported at compile-time some other at run-time.
Gradual typing is different than type inference. On type inference the type specification is missing but default type is determined by logic deduction from literals and expressions.
A Variant is a polymorphic variable that can have one of a list of types but only one at a time. When the value is established, one type to hold that value is created.
** define variant subtype
class VNamme = {Type1 | Type2 | ... } <: Variant;
** declare variable (with initial value)
global
set v = value :VNamme;
For this we use a special type Null
** define nullable variant
driver test_nulable:
class Number = { Integer | Real | Null } <: Variant;
** use nullable variant
global
set x: Number;
process:
let x = 10; -- x is Integer
let x = 45.5; -- x is Real
let x = Null; -- x is Null
expect x is Null;
return;
A variant can establish its data type at runtime:
In next example we use a variant that can be Real or Integer.
#variant type demo
driver variant_type:
** tefine variants
set v, x ,t: { Real | Integer };
process:
** using a variant first time
let t := 12; -- assign an Integer (possible)
print type(t) -- Integer
** using a variant second time
let t := 1 / 2; -- assign a Real (possible)
print type(t) -- Real
** unsafe conversion
let x := 1.5; -- x is Real
let v := 1; -- v is Integer
let v := x; -- v becomes Real
print type(v) -- Real
return;
A variant is a way to circumvent the type system. It can be used in a generic routine. For this we use variant parameters. Using variabt parameters enable a routine to be called with different argument types making the program more dynamic:
# variant parameter in routines
driver variant_params:
** define a subroutine that can swap two numbers
** requires input/output parameters marked with @
routine swap(@x, @y: {Integer | Real} ):
** check type to be the same
expect type(x) = type(y);
** swap x, y values
new i := x; -- intermediate
let x := y; -- first swap
let y := i; -- second swap
return;
process:
** invert two Integer numbers
new x := 10;
new y := 20;
call swap(x, y);
expect (x == 20) and (y == 10);
** invert two Real numbers
new a := 1.5;
new b := 2.5;
call swap(a, b);
expect (a == 2.5) and (b == 1.5);
return;
In Eve, we represent calendar dates using a Date object that contains four fields: year, month, day.
The Date type is defined as an object with the following fields:
year
: Integer representing the yearmonth
: Integer representing the month (1-12)day
: Integer representing the day of the month (1-31)Note: Years before the Common Era/Anno Domini are represented with negative numbers.
We can create a date literal using the following format functions:
ymd()
format: "YYYY/MM/DD"dmy()
format: "DD/MM/YYYY"mdy()
format: "MM/DD/YYYY"Note: The era_label (CE or AD) is optional for positive years. If omitted, the system default (configurable) is used.
#date format demo
driver date_demo:
process:
new date1 := "2023/06/15" as YMD; -- June 15, 2023 CE
new date2 := "30/11/-753" as DMY; -- November 30, 753 BCE
new date3 := "07/04/1776" as MDY; -- July 4, 1776 AD
new date4 := "01/01/2000" as DMY; -- January 1, 2000
// Date comparisons (era labels don't affect comparison)
expect date3 > date2; // 1776 AD is after 753 BCE
expect date1 > date3; // 2023 CE is after 1776 AD
expect date4 > date3; // 2000 is after 1776
// Access individual fields
print "Year of date1: #" ? date1.year;
print "Month of date2: #" ? date2.month;
print "Day of date3: #" ? date3.day;
// Create a new date
new custom_date := {year: 1066, month: 10, day: 14};
print custom_date; // {year: 1066, month: 10, day: 14}
return;
The Date object provides various methods for manipulation and querying:
is_bce()
: Returns true if the year is negative (before Common Era), false otherwiseto_julian()
: Converts the date to Julian Day Numberfrom_julian(jdn)
: Creates a date from a Julian Day Numberadd_days(n)
: Returns a new Date object n days after this datesubtract_days(n)
: Returns a new Date object n days before this datedays_between(other_date)
: Calculates days between two datesWe use Time or Duration data types. These are primitive types not nullable.
Time format is created from string literals using two reversible functions: t12() and t24(). These functions are using constants T12 and T24 that represent standard time format.
ss: can be 0..60 seconds
xx: can be: (am/pm)
# time demo
driver time_demo:
process:
** define 3 variable of same type
new time1, time2, time3: Time;
** alter variables using "as" operator
let time1 := "00:23:63" as T24;
let time2 := "23:63:59,999" as T24;
let time3 := "11:63:59pm,10" as T12;
** check time
expect time1.h == 23;
expect time1.m == 63;
expect time1.s == 59;
expect time3.ms == 10;
expect time3.t == pm;
return;
Duration is represented as a number on 32 bits. Internally is stored as milissecond. It can hold almost 20 days. For longer duration, you must use Time object.
# duration demo
driver duration_demo:
process:
** define 3 variable of same type
new d1 := 10ms; -- 10 millisec
new d2 := 1s; -- 1000 millisec
new d3 := 1m; -- 60000 millisec
** create duration from string
new d4 := Duration("01:01:01");
** check time
print d3 -- 60000ms
print d4 -- 3661000ms
return;
For printing output using a format we use operator as that is the "quick format" operator. We can use system constant templates that are available for making different formats for different data types. This operator is a bit smarter than the "?" operator that we can use for "string templates".
Eve can constants to define date format. These constants are important
Constant | Value |
---|---|
YDM | YYYY/DD/MM |
DMY | DD/MM/YYYY |
MDY | MM/DD/YYYY |
Eve can constants to define time format. These constants are important
Constant | Value |
---|---|
T24 | HH:MM:SS,MS |
T12 | HH:MM:SSpm,MS | HH:MM:SSam,MS |
Eve can print comma "," for decimal numbers but also can print dot.
Constant | Value |
---|---|
EUR | ,. |
USA | ., |
Read next: Structure