Sage-Code Laboratory
index<--

Data Types

The purpose of any programming language is to manipulate data. On the lowest level, data is basically 0 and 1 signals. Eve is a high level language. It has complex data types. Eve has a gradual-typing system that will be explained below.

Page bookmarks



Native Types

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

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.

Note: Native and primitive types are automaticly inititialized to zero value unless specified otherwise using a data literal. Symbols have initial value NIL equivalent to ''.

Numeric Types

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:

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:

Continuous numbers

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.

Numeric literals

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}

Example:


#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

Type "Symbol"

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.

Examples


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 Literal

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.

Examples


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;

Boxing and Unboxing

In Eve, "boxing" and "unboxing" are crucial concepts that describe the conversion between native types and their corresponding primitive types.

Boxing

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

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

Key Points

Note: While boxing provides additional features, it comes with a small performance overhead. Use native types for performance-critical operations where the additional features of primitive types are not needed.

Example: Boxing and Unboxing in Action


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.

Composite Types

Composite data types, also called structured types or user-defined types, group multiple primitive data types (like integers, strings) into a single unit. They define a structure to hold this data and often include ways to access and manipulate it.

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.

Range

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.

Usecases:

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.

Syntax:

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;

Notes:

Example:

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;

Example:

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;

Decimal range

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;

Random method

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.

Examples:

# 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;

Data Coercion

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.

Notes:

#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.

Default types

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

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".

Partial Inference:

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;

Collection Inference

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;

Type verification

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;

Printing type()

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;

Polymorphic operators

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.

Logical type

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

Syntax:


** 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

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.

Variant Types

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.

Syntax:

** define variant subtype
class VNamme = {Type1 | Type2 | ... } <: Variant;

**  declare variable (with initial value)
global
  set v = value :VNamme;

Properties

Making a null-able type

For this we use a special type Null

Example:


** 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;

Usability

A variant can establish its data type at runtime:

Example 1:

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;

Example 2:

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;

Calendar date

In Eve, we represent calendar dates using a Date object that contains four fields: year, month, day.

Date Structure

The Date type is defined as an object with the following fields:

Note: Years before the Common Era/Anno Domini are represented with negative numbers.

Date Literals

We can create a date literal using the following format functions:

Note: The era_label (CE or AD) is optional for positive years. If omitted, the system default (configurable) is used.

Example Usage:


#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;

Additional Methods

The Date object provides various methods for manipulation and querying:

Note: All dates are internally represented using the same numerical system, with negative years for BCE/BC.

Time & Duration

We use Time or Duration data types. These are primitive types not nullable.

Time

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)

Example:

# 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

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.

Example

# 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;

Quick Format

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".

Date Format

Eve can constants to define date format. These constants are important

ConstantValue
YDMYYYY/DD/MM
DMYDD/MM/YYYY
MDYMM/DD/YYYY

Time Format

Eve can constants to define time format. These constants are important

ConstantValue
T24HH:MM:SS,MS
T12HH:MM:SSpm,MS | HH:MM:SSam,MS

Numeric Format

Eve can print comma "," for decimal numbers but also can print dot.

ConstantValue
EUR,.
USA.,

Read next: Structure