@davidberneda v0.0.18-alpha June-2021
https://github.com/davidberneda/Vidi
Important: DRAFT. EVERYTHING MIGHT CHANGE.
Vidi is a strict typing, object oriented language that borrows most of its features from existing languages like Java, C#, Delphi / Pascal and others.
It is case-insensitive by default. That means for example Game
is considered equal to gAmE
, but it can also be set to be case-sensitive.
The following examples contain comments as a single-line of text beginning with: //
Numbers can be expressed in several ways:
123 // Integer
-4567 // Negative
12.345 // Float
4e2 // Exponent
-5e-3 // Exponent negative
// Other bases:
0xFF // Hexadecimal base 16
0b11011 // Binary base 2
0c217 // Octal base 8
42_000_000 // Optional digit separator
Double or single quotes can be used to delimit text.
"Hello" // Double-quotes
'World' // Single-quotes
"How're you?" // Single quote inside double quotes
'Say "abc" !' // Double quotes inside single quotes
True
False
[ 1, 2, 3 ] // Simple array
[ ["a","b"], ["c", "d", 'e'] ] // Array inside array
Ranges express minimum and maximum values:
1..10 // from 1 to 10
-12..-2 // from -12 to -2
Ranges can be used in several places, like for example when declaring an array:
MyArray : Integer[1..10] // An array of 10 Integer values
Or to specify custom Integer
types to benefit from overflow checking:
// A custom Integer class from 1 to 3
Podium is 1..3 {}
P : Podium := 4 // <-- Error. Overflow
// The 'Podium' class can also be used as an array dimension:
Winners : Integer[Podium] // same as: Integer[1..3]
Or to use a range in a for
loop:
for Num in 0..1000 { }
Or in function parameters and result types:
MyFunction( MyParam : 20..1000): 4..10 { }
Ranges can also be returned from functions:
Months : Range { 1..12 }
A range can also be used as a type of an array:
Podiums is 1..3[10] {} // An array of 10 integer values, each value from 1 to 3
To obtain a portion (an slice) of an array:
Big ::= ['a','b','c','d','e','f','g']
Small ::= Big[2..4] // An slice with elements from index 2 to index 4: c d e
Boolean operators:
and or not xor
Conditional operator:
2 > 1 ? True : False // Ternary
Membership operator:
'A' in 'ABC' // True
5 in [1,2,3] // False
2 + 3 - 5 * (6 / -7) // Basic math
255 or 0xFF
128 and 255
64 xor 32
not 123
"Hello" + "World" // Text addition
// Other mathematical expressions are done using functions instead of symbols:
Math.Power(5,2) // 5 elevated to 2 is: 25
Math.Modulo(10,3) // 10 modulo 3 is: 1
BinaryShift.Left(2,4) // 2 << 4 is: 32
BinaryShift.Right(32768,4) // 32768 >> 4 is: 2048
Equality operators:
= <> > < >= <=
Parenthesis are used to group expressions and indicate precedence:
(4+2) * 6 - ((5/9) * (Abc - Xyz))
Identifiers must begin with an alpha character (a
to z
) or _
(underline), and then any digit (0
to 9
), alpha or underline.
Examples:
Abc
X123
My_Name
_Test4
_4Z
The :
symbol (colon) is used to separate the variable identifier (variable name) and its type:
Simple variables:
A : Integer
B : Text
A variable can optionally define a default value (initial value) using the :=
symbol:
F : Float := 123.45 // Value initialization
Variable type can be optionally omitted to infer it from its initial value:
Data ::= True // Type inference. (Data is Boolean)
Planet ::= Earth // Planet variable is of the same type as Earth value
Arrays are declared using the []
bracket symbols, and can also be optionally initialized:
Colors : Text[] := [ "Red", "Blue" ]
Matrix : Float[ 3,3 ] // Alternative way: Float[3][3]
Ranges and expressions can also be used to declare array dimensions:
Numbers : Integer[ 1..(2*10) ] // 20 elements, from 1 to 20
Multiple variables of the same type can be declared in a single line:
Name, Surname, Address : Text // Three Text variables
// Also supported optional same value for multiple variables:
X,Y,Z : Float := 1.23
// Multiple variables type can be inferred:
This, That ::= True // Both This and That are variables of Boolean type
The :=
symbol assigns (sets) the right-side value or expression, to the left-side variable:
X:Text
X := 'Hello' // <-- assignment
Arithmetic assignments ( += -= *= /=
) are supported:
Z : Integer
Z := 1
Z += 3 // Z := Z+3
Z -= 2 // Z := Z-2
Z *= 5 // Z := Z*5
Z /= 4 // Z := Z/4
Text and arrays support additions:
Hi : Text
Hi := 'Hello'
Hi += ' World!' // string concatenation
Nums : Integer[]
Nums += [1,2,3] // Equals to Array Nums.Append method
Nums += 4
These data types (numeric, text and booleans) are "value types". They are always copied when assigning variables:
A : Integer := 123
B : Integer := A
// A and B are independent. Modifying A does not change B.
A := 456 // B value is still 123
The rest of types (objects, arrays and functions) are always assigned "by reference".
Person { Name: Text } // simple class
A : Person
B : Person := A
// A and B point to the same Person variable.
// Modifying one, changes the other:
A.Name := 'John' // B.Name is also John now
The final
keyword is used to define variables that cannot be modified (readonly):
final Pi : := 3.1415
final Hello : Text := 'Hello'
// Pi := 123 <-- Error, final constant cannot be modified
Expressions are allowed to initialize final variables, including calling type-level functions:
final A ::= 1
final B ::= A + 1
final C ::= Math.Square(5) // 5*5 = 25
Structures, records, classes and interfaces are the same thing in Vidi.
Person {
Name : Text
}
A class can be extended from another class using the is
keyword:
Customer is Person {
Code : Integer
}
In the above example, the Customer
class derives from the Person
class.
Person
is the ancestor class of Customer
.
The sys
module contains most basic classes. The SomeThing
class is the root of any other class.
Literal numbers, texts, arrays, etc are also classes. Types and routines are classes too. Everything is SomeThing
.
SomeThing {}
The Self
keyword (equivalent to this or it or base in other languages) represents the class instance itself.
Foo is Integer {
Bar() {
SomeClass.Test(Self) // Passing ourselves as a parameter to Test function
}
}
Class types and procedures / routines / methods / functions can be nested, unlimited, at any scope.
Life { // class
Tree { // subclass
Plant( Quantity : Integer) { // method
Forest is Text[] { // subclass inside method
}
MyForest : Forest // variable of Plant class
SubMethod() { }
}
}
}
Sub elements are accessed using the .
symbol, for example to declare a variable of a sub-class type:
Pine : Life.Tree
Exactly like methods, class parameters can be used when variables are declared, to initialize (construct) them.
// Parameter: SomeName
Customer(SomeName: Text) is Person {
Name:= SomeName
}
Cust1 : Customer("John")
Cust2 : Customer("Anne")
A variable can also be defined to be of type Type
.
Food {} // a simple class
Fruit is Food {}
Rice is Food {}
MyFoodType : Type // future: Type(Food)
MyFoodType := Rice
MyFood : MyFoodType // <-- equivalent to MyFood : Rice
There is no special syntax for generic types.
Class parameters of type Type
can be used to specialize generic classes.
List(T:Type) is T[] {} // Parameter of type: Type
Numbers is List(Float) {} // List of Float
Names is List(Text) {} // List of Text
As there are no pointers, casting is only allowed within types of the same class hierarchy.
Class1 {}
Class2 is Class1 {}
C2 : Class2
C1 : Class1 := C2 // Correct, same hierarchy
// C2_bis : Class2 := C1 // <-- Error, casting must be explicit
C2_bis : Class2 := Class2(C1) // <-- Casting is correct
Note: Experimental, not yet finished
MyBaseClass {}
MyDerivedClass is MyBaseClass { Foo : Integer }
MyDerivedData : MyDerivedClass
MyData : MyBaseClass := MyDerivedData
// 1) Access to Foo is forbidden, compiler error. Casting is necessary
MyData.Foo := 456
// 2) Correct, but might generate an exception at runtime if MyData is not MyDerivedClass
MyDerivedClass(MyData).Foo := 789
// 3) Correct access because the "if" does the casting automatically
if MyData is MyDerivedClass
MyData.Foo := 123 // No runtime exception will happen
Also called routines, procedures or functions.
Area : Float { return 123 }
Method parameters are passed by default as read-only constants and cannot be modified.
Make( Wheels : Integer ) {
// Wheels parameter cannot be changed inside
}
The out
keyword in front of a parameter means the parameter must be assigned a value:
Parts( Style:Text, out Price:Float ):Boolean {
Price:=123 // <-- Price must be assigned
}
Returning more than one value ("tuples") is done using structs (records):
Format { Size:Integer Name:Text } // <-- The record
// Routine returning the record:
MyFunction : Format {
Result : Format
Result.Size := 123
Result.Name := 'abc'
return Result
// Future releases might allow: return 123, 'abc'
}
// Calling the method and obtaining the tuple X:
X ::= MyFunction
Console.Put(X.Name)
Variables can also be declared using a class declaration just after the :
colon symbol:
Planet : { Name:Text, Radius:Float }
Planet.Name := 'Saturn'
// An array can be used to initialize all class fields, in order:
AnotherPlanet: := [ 'Saturn', 58232 ]
// Also array of arrays:
Planets : { Name:Text, Radius:Float } [] :=
[
[ 'Mars', 3389.5 ],
[ 'Earth', 6371.0 ]
]
The last parameter of a method can be declared with the special ...
prefix, to allow passing an undetermined number of parameters.
This is just syntactic sugar of passing an array without the need of typing the [ ]
symbols around values.
Print( Values : Data...) {
for Value in Values Console.PutLine(Value)
}
// Call examples:
Print
Print('abc')
Print(123,'abc',True)
PrintNumbers( Values : Integer... ) {
for Value in Values Console.PutLine(Value)
}
PrintNumbers(7,8,9,10,11) // similar to: [7,8,9,10,11]
Routines can have the same name if they have different parameters and/or return values:
Write( Number : Integer) {}
Write( Number : Float):Integer[] {}
Write( Word : Text, Other : Boolean) {}
A child class can declare methods with exactly the same name, parameters and return values as its ancestor parent class.
The Ancestor
keyword refers to its parent method.
Class1 {
Proc() {}
}
Class2 is Class1 {
Proc() {
Ancestor // calls Class1.Proc
}
}
Methods can be declared with the final
keyword to forbid overriding them in derived classes.
final Proc() {}
When a method body is empty, it is considered abstract. There is no special syntax to declare it.
Test {
Foo(A:Integer):Text {} // <-- abstract method
}
That means the method cannot be called (it is an error at compile time), and that derived classes must implement (override) it and fill it with content.
There is no special syntax to declare interfaces. Simple classes that have no fields (no variables), and all their methods are abstract, are considered interfaces.
MyInterface {
MyMethod( Data : Boolean ):Text {} // abstract function
}
Classes can be derived from interfaces:
MyClass is MyInterface { // Deriving from an interface
MyMethod( Data : Boolean ):Text { return "abc" } // must implement abstract method
}
Classes that have methods with exactly the same name, parameters and return values of methods of an interface, can be used like instances of that interface.
SomeClass {
MyMethod( Data : Boolean ):Text { return "abc" }
}
// This method requires a MyInterface parameter
Example( Value : MyInterface) {
Value.MyMethod(True)
}
Some1 : SomeClass
Example(Some1) // Some1 variable is considered of MyInterface type
In the above code, SomeClass
class is not derived from MyInterface
but can be used as if it was.
The with
keyword imports (loads) modules located in separate files.
It can be used anywhere on a file, not only at the top.
Imported symbols are only available at the scope after with
.
with Module1, Module2, Module3.MyClass
MyClass {
with SomeModule // inner scope with
Test : SomeClass // SomeClass is declared inside SomeModule
}
// SomeModule symbols cannot be accessed here, outside MyClass scope
Module file names might contain spaces or characters not allowed in module identifiers. In this case, the with
syntax allows enclosing the module name in quotes like a text string literal:
with "My module with spaces"
Aliasing allows replacing module names with custom ones. For example to shorten its length or to avoid clash duplicates.
with Foo:= My_Long_Module.My_Class // use Foo as alias
Foo1 : Foo // variable of type My_Class
A module can aggregate several other modules in one single place:
// Module3
// Use modules with aliases
with M1:=Module1, M2:=Module2 // etc
// Declare modules as new classes
Module1 is M1 {}
Module2 is M2 {}
When using the above module Module3
, the Module1
and Module2
contents will be ready available without needing to use them in with
keywords.
The hidden
keyword prefixing a class, field or method makes it unavailable outside its scope.
hidden MyClass {
hidden MyField : Integer
hidden MyFunction : Boolean {}
hidden MySubClass {}
}
Unused hidden items will produce an error at compile-time.
The shared
keyword means an element (variable or method) belongs to type-level, not instance-level.
This is the equivalent of class variables in other languages.
Colors {
shared Default : Text := "Red"
}
Colors.Default := "Blue" // Can be used at type-level, without any instance
Type-level methods are auto-discovered. There is no special syntax to declare them.
When a method do not access any non-shared field or non-type level methods, it is considered shared
.
Colors {
shared Default : Text := "Red"
// Type-level procedure, no shared keyword necessary
SetDefault( Value : Text) { Default:=Value }
}
Colors.SetDefault( "Green" )
There is no special syntax to declare namespaces. Classes with no fields and no methods are considered namespaces.
// Module1
MyNamespace {
MyClass {}
}
Modules with duplicate namespace names can be merged, to aggregate (contribute) new classes to the same namespace:
// Module2
MyNamespace {
OtherClass {}
}
The with
keyword can also be used to reference just only a sub element instead of to everything in the module:
// Module3
with Module1.MyNameSpace,
Module2.MyNameSpace
Test is OtherClass {
Some : MyClass
}
Deriving one type from another just for the convenience of strict type checking:
// Type alias
Year is Integer {}
Y : Year
Month is Integer {}
M : Month
Y := M // <-- Error. Different types.
Class1 {}
Class2 is Class1 {}
C1 : Class1
C2 : Class2
// ERROR: C2 := C1 <-- equivalent but forbidden (strict check)
The Type
class provides methods to inspect (reflect) existing types:
// Type checking:
if Type.is( C1, Class1 ) ...
// Obtaining the list of methods of a given type or instance:
Methods:Method[] := Type.Methods( C1 )
At any scope, including in other modules, types can be extended with new methods and subclasses. So for example we can declare this class in one module:
// Module 1
MyClass {
}
And then, inside the same module or in other external modules, we can declare new methods and subclasses of MyClass
:
// Module 2
with Module1
MyClass.MyProcedure() {} // New extended Procedure
MyClass.MySubClass { X:Float } // New extended Sub-class
These new extended elements can then be used as normal, also in different modules.
// Module 3
with Module1, Module2
Foo : MyClass
Foo.MyProcedure() // Calling an extension as if it was a normal method
Bar : MyClass.MySubClass
Bar.X := 123
These extensions, like any normal type, are only available inside the scope where they are declared.
Extended types can also be extended:
MyClass.MySubClass.MyNewMethod() {}
Bar.MyNewMethod()
A type can be used as a function declaration:
MyProcType is (A:Text, B:Integer) {} // two parameters, A and B
This type can then be used anywhere like normal types:
Foo(Function: MyProcType) {
Function('Hello',123)
}
To be compatible with MyProcType
, functions should be signature-compatible:
MyFunction(A:Text, B:Integer) {
Console.PutLine(A, ' ', B.AsText)
}
The MyFunction
function can now be called or passed to other methods.
Foo(MyFunction) // shows 'Hello 123'
Also called lambdas or callbacks in other languages, functions can be passed as parameters "inline":
// Same as the above example "MyFunction", but unnamed
Foo(
(A:Text, B:Integer) {
Console.PutLine(A,' ',B)
}
)
Or assigned to variables of the custom function type:
// Same as above, but using a variable
MyFunction_Variable : MyProcType := { Console.PutLine(A,' ',B) }
Foo(MyFunction_Variable)
The is {}
syntax is used to declare enumerations.
Colors is { Red, Green, Blue, Yellow }
Variables and constants can then use the enumeration items:
MyColor : := Colors.Blue
These enumerations can also be used as dimensions for arrays:
Names : Text[Colors] // Array of four text items
Names[Colors.Green] := "I Like Green"
And the for in
statement can loop all the enumeration items:
for Color in Colors {
Console.PutLine(Color)
}
a := b
b := c + d
if a=b
foo
else
bar
while a>b {
if a=0
break // "break" exits the "while" loop
else
a:= a - 1
}
repeat {
b += 1
if b=5
continue // "continue" jumps to start of "repeat"
} until a<>b
// Single statements do not require { }
repeat
b +=1
until b>5
A simple loop without any counter variable:
for 1 to 10 {} // ten times
for 5..7 {} // three times
An integer range:
for t in 1..10 {} // ten times
Traditional loop using the to
keyword:
a ::= 5 b ::= 7
for x: := a to b {} // three times
for y: := 1+a to 9 {} // four times, from 6 to 9
The optional counter variable:
- Cannot be reused or accessed outside the
for
block - Cannot be modified inside the
for
loop - It cannot be an already declared variable
- Its type is always automatically inferred
The in
keyword can loop over an enumerated type:
Colors is { Red, Blue }
for c in Colors {} // iterates all Colors
The in
keyword can also be used to loop an array:
Nums: := [ 6,2,9 ]
for i in Nums { Console.Put(i) } // iterate an array
The array can also be declared inline, without using a variable:
for i in [ 6,2,9 ] // the type of "i" is automatically inferred
A Text
expression is an array of characters so it can also be iterated:
for x in "abc" {} // for each character in text
Descending order loops:
for 10..1 {} // ten times from 10 down to 1 (descending)
Also called switch, select or case in other languages.
Name::= "Jane"
when Name {
"Jane" { DoThis }
"Peter" { DoThat }
else
DoElse
}
Comparison expressions can also be used:
num ::= 5
abc ::= 3
when abc+num {
< 3 { Console.PutLine('Lower than 3') }
4 { Console.PutLine('Equals 4') }
<> 6 { Console.PutLine('Different than 6') }
else { } // otherwise
}
After the first condition that matches the expression is found, execution flow exits.
The return
statement exits a method, with an optional value if the method is a function
Test {
Foo() { return }
Bar:Text { return "abc" }
}
// The return keyword is optional at the last expression of a function:
Square(X:Float):Float { X*X }
Error handling (exceptions) follows the standard of other languages using try
, catch
, finally
keywords.
Code inside the try
block is protected, so in case an error happens, the finally
block of code is always executed:
try {
X::=1/0 // <-- error divide by zero !
}
finally {
Console.PutLine('Always executed')
}
The catch
code is executed when a runtime error happens inside the try
block:
try { X::=1/0 }
catch {
// Optional code, might be empty {}
Console.PutLine('An error happened')
}
The catch
keyword can optionally specify a type (MyError
class in this example), so only these errors will be processed:
MyError { Code:Integer }
Foo() { Exception.Raise(MyError) } // <-- just an example of generating an error
try { Foo() }
catch MyError {
Console.PutLine('MyError happened')
}
If the catch
type is prefixed with a variable name (X
in the following example), then the fields of the error type can be accessed:
try { Foo() }
catch X:MyError {
Console.PutLine('MyError happened: ', X.Code)
}
The catch
and finally
sections can coexist (finally
must go after catch
for clarity):
try { Foo() }
catch { Console.PutLine('Error happened') }
finally { Console.PutLine('Always executed') }
Multiple catch
blocks can be used to respond to different errors. Duplicates are not allowed.
try { Foo() }
catch X:MyError { Console.PutLine('MyError happened: ', X.Code) }
catch DivideByZero { Console.PutLine('Division by Zero') }
// catch DivideByZero {} <-- duplicate compile-time error
Methods can call themselves in a recursive way:
Factorial(x:Integer):Float {
x=0 ? 1 : x * Factorial(x-1)
}
Factorial(5) // Returns 120
There are situations where methods should be called but are not yet declared. These are handled automatically, no special syntax is necessary. (Note: Not yet developed)
TestInner(Work:Boolean) {} // <-- empty placeholder
TestForward() {
TestInner(False) // <-- not yet declared
}
// This replaces the placeholder above:
TestInner(Work:Boolean) {
if Work
TestForward
}
{
TestInner(True)
}
No special syntax for properties. A property "getter" can be a field:
Foo : Integer := 123
Or a function:
Foo : Integer { return MyFoo }
And optionally, a property "setter" which is just a function with the same name and one parameter:
Foo(Value:Integer) { MyFoo:=Value } // Setter
The compiler will handle property access transparently:
Foo:=123 // will call the setter method: Foo(123)
Classes can define a single, unnamed, parameter-less final
method that will be called when variables get out of scope.
Shop {
Console.PutLine( 'Open!' )
final {
Console.PutLine( 'Closed!' )
}
}
{
MyShop : Shop
// do something with MyShop ...
} // <-- at the end of the scope, MyShop finalizer is called
Note: Experimental, not yet finished
New expression operators can be implemented to provide cosmetic "syntax sugar" using symbols or keywords.
// Declare a new "is" operator, using the existing Type.is function
Operator.is := Type.is
// Use example
Foo : Boolean
// Equivalent expressions
if Type.is(Foo, Boolean) Console.PutLine('Ok')
// Using the new "is" operator
if Foo is Boolean Console.PutLine('Ok')
// Existing basic operators like +, -, *, >, < etc could theoretically be re-implemented as extensions.
// The compiler finds the best overload for left and right types, if there is more than one.
A + B // calls Integer.Add(A,B) if both A and B are Integer-compatible
ancestor
and
break
catch
continue
else
False
final
finally
for
hidden
if
in
indexed
is
not
or
out
repeat
return
self
shared
to
True
try
until
when
while
xor
with
{ } // code block
. // membership Foo.Bar
[ ] // arrays [1,2,3]
:= // assignment Foo:=123
: // type declaration Foo:Integer
, // parameters 1,2,3
( ) // expression groups, parameters
.. // ranges 1..100
... // many values parameter
? // condition expression A=B ? 1 : 2
> // greater than
>= // greater or equal than
< // lower than
<= // lower or equal than
= // equal
<> // different than
+ // addition
- // subtraction
* // multiplication
/ // division
// Single line comments
/*
Multiple line
comments
*/
Inline : Text := "allow" + /* comments */ "around code"