Lumi Language Guide¶
Welcome to Lumi programming language guide!
Lumi is a general purpose programming language that is good for all needs, specially for long-term projects, or where resource efficiency is needed.
Lumi aims to be safe, efficient, flexible, easy to write, and easy to maintain. See Lumi Language Goals and Features for more details.
Lumi files use .lm
extension.
“Lumi” name was chosen because:
“Lumi” is an abbreviation for “Illuminating”, and Lumi language aims to cast new light to the programming world. (it’s also the meaning of my first name)
“Lumi” means “snow” in Finnish, which is bright, light, handy, flexible, strong and fun - such as the Lumi language. (and also - I like snow)
“Lumi” is short, easy to pronounce, and fun to say.
The Lumi image is Designed by kjpargeter / Freepik
A Work in Progress…¶
Lumi is still under initial planning and building. Many features are already implemented and it is possible to write complex programs with Lumi, but some key elements are not, and many implemented feature may change dramatically.
The latest state of the language is named “Temporary Lumi 5”, or “TL5” in short, to emphasizing the current temporary state of the language.
Contributing to Lumi¶
Any help, suggestion, comment or questions is welcome! See Lumi repository wiki for more details on how to contribute to Lumi.
Quick Start - Hello World Example¶
This is a quick guide to install, compile and run Lumi “hello-world” example in Linux.
Quick Installation¶
Clone Lumi repository (git clone
https://github.com/meircif/lumi-lang.git
).
Enter the repository root directory: cd lumi-lang
.
Install it: make install
.
Hello World Program¶
module hello-world
func ! show()
sys.println(user "hello world")!
main!
show()!
Compile it: lumi docs/hello-world.5.lm
.
Run it:
>>> docs/hello-world
hello world
Hello World Test¶
module hello-world-test
var Bool println-raise
var String printed-text
mock ! sys.println(user Buffer text)
if println-raise
raise! "error in println"
printed-text.concat(user text)!
test show-hello-world-test()
println-raise := false
printed-text.clear()
hello-world.show()!
assert! printed-text.equal(user "hello world")
test show-raise-test()
println-raise := true
assert-error! hello-world.show(), "error in println"
Compile it: lumi -t hello-world docs/hello-world-test.5.lm
docs/hello-world.5.lm
.
Run it:
>>> docs/hello-world-test
Running tests:
testing show-hello-world-test... OK
testing show-raise-test... OK
testing code coverage... 100%
Tests passed
Installing and Building Lumi¶
Syntax Highlighting¶
For Lumi syntax highlighting it’s recommended to use one of:
Visual Studio Code editor with Lumi Language extension installed
Atom editor with language-lumi package installed
Installing Lumi¶
Note
In all shell examples below $CC
assumed to hold the C compiler, and the
current directory assumed to be on Lumi repository root directory
clone or download Lumi repository:
git clone https://github.com/meircif/lumi-lang.git
run
make install
or install manually:
build the latest Lumi compiler with a C compiler:
$CC TL5/tl5-compiler.c TL4/lumi.4.c -ITL4 -o tl5-compiler
build the “lumi” command with a C compiler:
$CC lumi-command/lumi.c TL4/lumi.4.c -ITL4 -o lumi
add the Lumi compiler and the
lumi
command to the system path (for example:sudo install lumi tl5-compiler /usr/local/bin/
)
Building the Lumi Compiler¶
A Lumi compiler must first be built using a C compiler. That Lumi compiler can then be used to generate C code from Lumi code.
It is recommended to add the compiler executable to the system path, for
example, in Linux move it to /usr/local/bin/
.
Latest Version - TL5 Compiler¶
make tl5-compiler
# or
$CC TL5/tl5-compiler.c TL4/lumi.4.c -ITL4 -o tl5-compiler
Old Versions¶
## TL4 compiler
make tl4-compiler
# or
$CC TL4/tl4-compiler.c TL3/lumi.3.c -ITL3 -ITL4 -I. -o tl4-compiler
## TL3 compiler
make tl3-compiler
# or
$CC TL3/tl3-compiler.c TL2/lumi.2.c -ITL2 -o tl3-compiler
## TL2 compiler
make tl2-compiler
# or
$CC TL2/tl2-compiler.c TL1/lumi.1.c -ITL1 -o tl2-compiler
## TL1 compiler
make tl1-compiler
# or
$CC TL1/tl1-compiler.c TL0/tl0-file.c TL0/tl0-string.c -ITL0 -o tl1-compiler
## TL0 compiler
make tl0-compiler
# or
$CC TL0/tl0-compiler.c TL0/tl0-file.c TL0/tl0-string.c -o tl0-compiler
Building the lumi
Command¶
The lumi
command must first be built using a C compiler. lumi
command
can then be used to compile an executable directly
from Lumi code by running Lumi compiler and C compiler one after another.
make lumi
# or
$CC lumi-command/lumi.c TL4/lumi.4.c -ITL4 -o lumi
It is recommended to add the compiler executable to the system path, for
example, in Linux move it to /usr/local/bin/
.
Using the lumi
Command¶
The lumi
command is a tool that helps compile, test, and run Lumi code.
For example, lumi
command can be used to build
an executable directly from Lumi code by simply running lumi hello.5.lm
.
The lumi
command:
assumes the used Lumi compilers are already built and are in the system path
uses the
CC
environment variable to determine the C compiler command, usinggcc
if not existssupports all Lumi version from TL0 to TL5
Command Help¶
Running lumi -h
or lumi --help
will print help:
>>> lumi -h
Usage: lumi [options] file...
Options:
-h/--help print this help
--version print lumi command version
-o <file> output file name
-t <module> compile test program for <module>
-l <module> compile shared library exporting <module>
-c only create C file(s)
-TL<version> only run C compiler for TL<version>
-e <argument> extra argument for C compilation
-p <lumipath> path of lumi-lang repository
-r run the compiled program
-ra <arguments> run the compiled program with given arguments
-v/--verbose print executed commands
-d/--debug only print commands without execution
Usage¶
The basic usage of lumi
command is to take one or more Lumi files and
create a single executable from them. For example:
lumi hello.5.lm
will create a hello
named executable compiled from hello.5.lm
,
generating a hello.c
C file in the process. This is done by running Lumi
compiler and C compiler one after another.
If multiple Lumi files are given, the generated name will be based on the first input file.
lumi
command detects the TL version based on the input file extension
.[TL version].lm
and runs the respective Lumi compiler.
Specifying an Explicit Output File Name¶
The output file name can be explicitly defined with -o <output file name>
.
For example:
lumi hello.5.lm -o output
will generate output
named executable, and output.c
named C file in the
process.
Compiling Tests¶
Lumi compiler allows generating testing code for a specific
Lumi module. This feature can be used in lumi
command with -t <tested
module name>
. For example:
lumi -t hello hello-tests.5.lm hello.5.lm
will generate hello-tests
executable that tests the hello
module.
Running a Lumi test executable with -xml
argument will also generate a
cobertura.xml
named file with code coverage XML report in cobertura
scheme.
Only Running Lumi Compiler¶
To only run the Lumi compiler -c
flag can be used. For example:
lumi -c hello.5.lm
will only generate hello.c
C file.
Only Running C Compiler¶
To only run the C compiler -TL<TL version>
flag can be used. The TL version
number must be given as it cannot be detected from the input C file name. For
example:
lumi -TL5 hello.c
will only generate hello
executable, assuming hello.c
was generated by
TL5.
Extra C arguments¶
To add extra arguments to the C compilation -e
can be used. For
example:
lumi hello.5.lm -e external.c
will add external.c
as an input to the C compiler, while ignoring it in the
Lumi compilation. This is mainly needed when external C code is called
from Lumi.
Running the Generated Executable¶
The generated executable can also be run using -r
. For example:
lumi -r hello.5.lm
will generate hello
executable and then run it.
It is possible to also send arguments to the executable using
-ra <arguments>
.
For example:
lumi -r hello.5.lm -ra 'first-arg "second arg"'
Will run hello first-arg "second arg"
.
Verbose and Debug¶
Adding -v
or --verbose
option will also print the executed commands.
Adding -d
or --debug
option will only print the commands without
execution.
Old Version Limitations¶
TL4 and below assumes LUMIPATH is correctly configured
multiple input Lumi files are not supported in TL0 and TL1
implicit output name is determined by the last file in TL2, and not the first
TL2 and TL3 generate multiple C files - one C file for each input Lumi file, this also meas that an explicit output name for C files is not supported
testing is only supported in TL4 and above
LUMIPATH¶
For C linking purposes in TL4 and below lumi
command needs to know the
local Lumi repository root directory path. This can be configured by one of:
running
lumi
command inside the Lumi repository root directorysetting the value of
LUMIPATH
environment variable to the pathrunning
lumi
with flag-p <path>
(this will overrideLUMIPATH
environment variable)
Using Lumi Compiler Directly¶
Using the lumi Command is recommended to compile Lumi code. Using the Lumi compiler directly is possible, but it is less convenient.
Latest Version - TL5 Compiler¶
The TL5 compiler generates a single C file based on one or more Lumi files. For example
tl5-compiler hello.c hello.5.lm other.5.lm
will generate hello.c
C file from hello.5.lm
and other.5.lm
Lumi
files.
Compiling Testing Code¶
To generate C code that tests a specific Lumi module -t <tested module name>
should be used. For example:
tl5-compiler -t hello hello-tests.c hello.5.lm hello-tests.5.lm
will generate hello-tests.c
C file from hello.5.lm
and
hello-tests.5.lm
Lumi files, with C code that tests the hello
module.
Building an Executable¶
The generated C file can be compiled on its own to an executable using a C compiler. For example:
$CC hello.c -o hello
will generate hello
executable.
Old Versions¶
## TL4
# compiling hello.4.lm to hello.c
tl4-compiler hello.c hello.4.lm other.4.lm
# compiling tests
tl4-compiler -t hello hello-tests.c hello.4.lm hello-tests.4.lm
# compiling the C code as an executable
$CC hello.c TL4/lumi.4.c -ITL4 -o hello
## TL3
# compiling hello.3.lm to hello.c
tl3-compiler hello.3.lm
# compiling the C code as an executable
$CC hello.c TL3/lumi.3.c -I. -ITL3 -o hello
## TL2
# compiling hello.2.lm to hello.c
tl2-compiler hello.2.lm
# compiling the C code as an executable
$CC hello.c TL2/lumi.2.c -ITL2 -o hello
## TL1
# compiling hello.1.lm to hello.c
tl1-compiler hello.1.lm hello.c
# compiling the C code as an executable
$CC hello.c TL1/lumi.1.c -ITL1 -o hello
## TL0
# compiling hello.0.lm to hello.c
tl0-compiler hello.0.lm hello.c
# compiling the C code as an executable
$CC hello.c TL0/tl0-file.c TL0/tl0-string.c -ITL0 -o hello
Lumi Language Goals and Features¶
Prioritized Goals¶
Enforce safe and reliable code, free from:
illegal memory access
memory corruption
memory leaks
integer overflow
unexpected crashes
etc…
Allow writing of efficient code, suitable for real-time embedded:
Modern programming languages solve goal #1 by making everything dynamically allocated and garbage collected, but this is inefficient and unpredictable. Lumi will allow as much efficiency and freedom as possible, as long as goal #1 is not harmed.
Be flexible, easy to write, and easy to maintain:
This goal binds together 3 different goals:
Flexible - whenever possible Lumi should allow choosing from a variety of options
Easy to write - Lumi code should be easy to learn and writing code should be as efficient as possible
Easy to maintain - It should be easy to find and fix bugs and add new features, even in large and complex projects
The above goals are bound together because no one is prioritized above the other and they sometimes contradict. Lumi will do its best to reach all of them with minimum harm to each one.
Features¶
Code generating: Lumi compiler will initially generate C code. Other “modern machine language” codes may also be generated, such as: Java (for Android and other devices), Objective-C/Swift (for Apple devices), JavaScript/WebAssembly (for web application), and more…
Safe Memory Management - allowing easy trade-off between flexibility and performance by the user
Thread Safety - Lumi code is ensured to be thread safe
Integer Ranges and Overflow Prevention - Lumi code is ensured to be free from integer overflow and underflow
Type System - Lumi is strongly typed, and allows variety of typing styles with different trade-offs between simplicity, generality and performance
Exception-like error handling implemented with return values
Strict coding conventions - all Lumi code should look the same
Built-in support for productivity features:
testing and mocking
documentation
profiling
Memory Management¶
Memory should be managed correctly to reach Lumi goal #1, to reach Lumi goal #2 - it should be done as efficiently as possible, and to reach Lumi goal #3 - it should be flexible.
This is achieved by combining 3 forms of managing - allowing easy trade-off between flexibility and performance by the user:
No Performance Overhead - compile time only reference managing
More Flexible Reference Managing - with a small performance cost
Note
Below is a non-final suggestion to implement this, it will probably be developed further over time. See Variables and Constants section for the currently implemented syntax.
No Performance Overhead - compile time only reference managing¶
Lumi will allow performance free reference managing that will be done only in compile time.
Every objects has a single “owner” entity - which is another object or a stack block. When an owner is destroyed it automatically destroys the referenced object, unless the ownership was moved to another entity. Stack and global variables are treated as owners - but they cannot move their ownership.
This has some similarities to the memory management in Rust.
Owners can give the reference to multiple temporary “user” entities. Users are free to read and modify the referenced object - but cannot destroy it or modify its sub-owners. Users are temporary because they cannot be used after any object of their type is destroyed, as the compiler cannot guarantee they are pointing to a legal object any more.
Owners can “borrow” the reference to a single temporary owner that automatically returns the ownership back to the original owner at the end of its code block. While borrowing the original owner cannot be used. The temporary owner has full control over the reference, except the ability to destroy it.
This is currently implemented, but not fully optimized. In the future the syntax may be slightly different and look like this:
owner String some-string(String{16}()) ; new owner reference
user-func(user some-string) ; give reference to a user
borrowing-func(temp some-string) ; borrow ownership to a temporary owner
owning-func(owner some-string) ; move ownership, cannot be used anymore
More Flexible Reference Managing - with a small performance cost¶
Lumi will allow more complex and flexible reference managing that come with a small and predictable performance cost.
Same as No Performance Overhead - compile time only reference managing with the addition of weak references. To allow this the owner should be declared as “strong”. It will work the same way as a regular owner, plus that it can now give “weak” references to any other entity without limitations. Weak references will automatically test that the reference is still valid before accessing it.
There are several ways to implement this - but all need some extra memory to manage the weak references, and some extra processing time to check if the weak reference is valid. In all implementations the extra overhead is small and predictable.
This is currently implemented, but in the future the syntax may be slightly different and look like this:
strong String some-string(String{16}()) ; new strong owner reference
user-func(user some-string) ; give reference to a user
borrowing-func(temp some-string) ; borrow ownership to a temporary owner
weak-func(weak some-string) ; give weak reference
owning-func(strong some-string) ; move ownership
Note
Strong reference counting is currently not supported because it can cause memory leaks because of reference loops. It may be allowed in the future in cases where the compiler can ensure no reference loop is possible.
Maximum Flexibility - but with performance issues¶
Lumi will allow declaring a reference as garbage-collected, which will allow passing references freely without limitation. The memory will only be cleared when all “strong” references are destroyed. The garbage-collector must check and remove reference loops to avoid memory leaks.
To allow this a reference should be declared as “shared”. This reference can then be passed to other “shared”, “user” or “weak” references.
Implementing a garbage-collector has a significant and unpredictable performance cost, but some Lumi users may be willing to pay it in some sections of their project where performance is less important.
This is not implemented yet, but in the future the syntax may look like this:
shared String some-string(String{16}()) ; new shared reference
shared-func(shared some-string) ; copy shared reference
user-func(user some-string) ; give reference to a user
weak-func(weak some-string) ; give weak reference
Conditional and Empty References¶
As default, (non-weak) references always point to a legal object.
To allow empty references, the reference type must be declared as “conditional”
using the ?
sign. Empty value can be set using _
sign.
This is currently implemented, but not fully optimized. In the future the syntax may be slightly different and look like this:
user String? cond-str ; initialized as empty
cond-str := some-string ; now not empty
cond-str := _ ; now is empty again
if cond-str? ; check if has value
; can be used safely here...
else
; here we know it's empty...
cond-str!.clear() ; raise error if empty
func-with-cond(user _) ; send empty to function
Thread Safety¶
Running threads should be safe to reach Lumi goal #1, therefore the compiler will enforce it.
This page explains a first suggestion to implement this.
The default approach - complete data isolation¶
The compiler takes care that each data is only accessible from a single thread - creating a complete isolation of data between threads. Global data will be duplicated for each thread (probably using “thread local” mechanism). This mean no data sharing is possible - ensuring thread safety.
This is the default approach on all data unless one of the thread data sharing mechanism described below is used.
Sharing data between threads¶
Constant and immutable data¶
Every compile-time constant or run-time immutable data can be safely shared between threads. The compiler will ensure no thread can modify these data.
Atomic operations¶
Based on the platform, atomic types will be provided, and all operations on an atomic item will be atomic.
Messaging¶
It will be possible to send messages between threads, the data on the message will be copied from one thread to another in a safe manner taken care by the compiler.
Built-in thread-safe data structures¶
Various built-in thread-safe data structures will be provided that can be used to share data between threads.
Automatic locks¶
It will be possible to mark data as thread-shared, and the compiler will automatically protect access to this data using various lock types.
Integer Ranges and Overflow Prevention¶
As part of Lumi goal #1 the compiler will enforce code that is without any integer overflow (or underflow).
Integer Ranges¶
In Lumi all integers are declared with explicit range
Int{minimum:maximum}
and the compiler enforces that it will always contain
values inside this legal range.
For example, the compiler will not allow these assignments:
func compute(Int{0:20} x)->(Int{-10:10} y)
y := 20 ; error - clearly out of range
y := x ; error - may be out of range
Assigning a constant or an integer with overlapped range are naturally legal, for example:
func compute(Int{-10:10} x)->(Int{-20:20} y)
y := 10 ; legal - inside legal range
y := x ; legal - all legal values of x are overlapped by y range
Integer Arithmetic¶
The result of any arithmetic operation is an integer with a new range based on the operator. For example:
func compute(Int{6:12} x, Int{0:100} y)
x + y ; return range is Int{6:112}
x - y ; return range is Int{-12:94}
x * y ; return range is Int{0:1200}
x div y ; return range is Int{0:16}
x mod y ; return range is Int{0:11}
Compile Time Overflow Prevention¶
The compiler will not allow operation that may result in an overflow (or and underflow). For example:
func compute(Uint64 a, Uint64 b)
a + b ; error - potential overflow
-a ; error - potential underflow
Run-Time Overflow Checking¶
It is possible to check for an overflow in run-time using !
or ?
together with one of + - *
operators. For example:
~~~ raises an error when overflow detected ~~~
func ! raising-compute(Uint64 x, Uint64 y)->(Uint64 z)
z := x +! y
z := x -! y
z := x *! y
func handling-compute(Uint64 a, Uint64 b)->(Uint64 z)
if-error z := x +? y
z := 0
if-error z := x -? y
z := 0
if-error z := x *? y
z := 0
Efficient Native Wraparound¶
Utilizing native overhead-free wraparound is supported by using wraparound
keyword. The result of native wraparound is naturally limited only for the
ranges that native wraparound is guaranteed to be supported: Uint8 Uint16
Uint32 Uint64
(Int{0:255} Int{0:65535} Int{0:4294967295}
Int{0:18446744073709551615}
). For other ranges wraparound must be done
manually using ((value - min) mod (max - min + 1)) + min
for example.
wraparound
can be used as a single unary operator before assignment or
together with one of += -= *=
assignment operators:
func compute(Uint32 x)->(Uint32 y)
y := wraparound x + 1
y := compute(wraparound x - 1)
y wraparound+= 1
y wraparound-= 1
y wraparound*= 1
The result range of using wraparound
as above is the same as the target
assignee range (Uint32
in the examples above). This means that the assigned
range must always be one of Uint8 Uint16 Uint32 Uint64
.
wraparound
can also be used together with + - *
operators:
func compute(Int{0:1000000} x, Uint64 y)->(Uint32 z)
z := x wraparound+ y
z := x wraparound- y
z := x wraparound* y
The result range of using wraparound
as above is the minimal from Uint8
Uint16 Uint32 Uint64
that overlaps the left operand (Uint32
from x
in
the examples above). This means that the left operand can be any unsigned
range.
Clamping¶
Clamping allows shrinking an integer range to a smaller range Int{min:max}
by converting any value larger than max
to max
and smaller than
min
to min
. This can be done automatically using clamp
keyword.
Clamping is not overhead-free because the checking and converting must be done
at run-time.
clamp
can be used as a single unary operator before assignment or
together with one of += -= *=
assignment operators:
func compute(Uint32 x)->(Uint32 y)
y := clamp x + 1
y := compute(clamp x - 1)
y clamp+= 1
y clamp-= 1
y clamp*= 1
Using clamp
as above will clamp the result to the range of the target
assignee (Uint32
in the examples above).
clamp
can also be used together with + - *
operators:
func compute(Uint32 x, Uint64 y)->(Uint32 z)
z := x clamp+ y
z := x clamp- y
z := x clamp* y
Using clamp
as above will clamp the result to the range of the left operand
(Uint32
from x
in the examples above).
On assignment it is possible to raise an error instead of clamping using !
or ?
together with clamp
. Whenever a value is too small or big for the
assignee target range - instead of setting min
or max
an error is raised.
For example:
~~~ raises an error when clamping ~~~
func ! raising-compute(Uint32 x)->(Uint32 y)
y := clamp! x + 1
y := raising-compute(clamp! x - 1)
func handling-compute(Uint32 x)->(Uint32 y)
if-error y := clamp? x + 1
y := 0
Sequences Index Integer Range¶
planned - not supported in TL5
It is planned to support a special range that is bound to a sequence and can only hold values that are legal indices to the sequence.
It may look like this:
func example(Array{Char} array)->(Char result)
var Int{array} index
result := array[index] ; no need to check index at run-time
Type System¶
Lumi is strongly typed, which means that:
Each symbol is statically bound to one specific type and can only hold data from this type.
Type correctness is enforced in compile time by the compiler.
Lumi has a variety of built-in primitive and complex types, and allows adding user defined types in a variety of typing styles. All these allow writing code with different trade-off between simplicity, generality and performance, and adapting different programming paradigms.
Typing Styles of User Defined Types¶
All user defined types in Lumi are built based on two basic typing styles:
These can be combined to build more complex typing styles:
Static Structures¶
Typing style for the static structure syntax.
This is the most basic, simple and efficient typing style. A static structures (or “structure” in short) is simply a record that groups together multiple variables of any kind under one type.
Structures may contain methods. Methods are functions with an implicit first
parameter named self
that is a reference to an instance of the type.
It is possible to declare constants and global variables inside a structure. They are not part of the structure record, but are like normal constants and global variables that are named under the type name-space.
Dynamic Interfaces¶
Typing style for the dynamic interface syntax.
Dynamic interfaces are the basics of dynamic dispatch in Lumi. A dynamic interface (or “dynamic” in short) groups together multiple dynamic members that will be implemented differently for multiple objectives. Dynamics can be implemented for a specific structure, or purely without any binding.
Dynamic members are usually methods, but variables and references of any type can also be dynamic members - where each implementation initializes them with a different constant value.
Dynamics are always used as references and cannot be allocated because they have no structure. A dynamic references can be set with any type that implements the dynamic.
“Implementing” a dynamic means implementing each dynamic method and initializing each dynamic variable with a constant value.
Dynamics standard method dispatch is dynamic. This means that when an implementing type is passed to a dynamic reference, any method called on the dynamic reference will use the implementing type implementation.
Dynamics may contain constants and global variables, they are not dynamic members and behave exactly as global members in structures - they are just global elements under the type name-space.
Dynamics cannot have static fields, but may contain static methods. They are also not dynamic members and behave exactly as methods in structures - they must be implemented directly in the dynamic, and their dispatch is static.
Extending Types¶
Types can be extended by adding functionality to some base types.
Structures can extend other structures. An extending structure contains all members from all base structures, plus its own members. An extending structure can override methods of a base structure, other members may not be overridden.
Structures method dispatch is static. This means that when an extending structure is passed to a base structure reference, any method called on the base reference will use the base structure implementation even if the extending structure overrode that method.
Dynamics can extend other dynamics. An extending dynamic contains all members from all base dynamics, plus its own members.
An extending structure may override any dynamic implementation of its base structure. Nevertheless, if an extending structure reference is passed to a base structure reference, and then the base structure reference is passed to a dynamic reference, any method call on the dynamic reference will use the base structure implementation because structure dispatch is static.
Classes - Binding Dynamic Interfaces and Static Structures¶
Typing style for the class syntax.
Sometimes binding together static structures and dynamic interfaces under a single type is useful, mainly to adapt the OOP (object oriented programming) paradigm. A type with this kind of binding is also known as a “class”.
Classes may be ad-hock binds between already declared structures and dynamics, or declared as classes up-front in a type definition. Types declared as classes may have both static and dynamic members, and the compiler creates an implicit static structure and an implicit dynamic interface - each with its respected members. The compiler then creates the class as a bind between these both implicit types.
Classes may extended any number of structures, dynamics, and other classes. The extending class implicit structure extends all base structures and the implicit structures of all base classes. Similarly, the extending class implicit dynamic extends all explicit and implicit base dynamics.
Classes may also implement dynamics. Any implementation method of the dynamic is also dynamic in the class. As opposed to structures, if an extending class reference is passed to a base class reference, and then the base class reference is passed to a dynamic reference, any method call on the dynamic reference will use the the extending class implementation because class dispatch is also dynamic.
Parameterized Types¶
Typing style for the parameterized type syntax.
It is possible to declare types with parameters to avoid code duplication of generic types. Each parameter can be either static or dynamic.
Static Parameters¶
Static parameters are like templates - for each different usage of any static parameter a new type will be automatically generated. Static parameters can be type names, or constant values of a specific type.
Dynamic Parameters¶
Dynamic parameters represent a generic type and only accept type names.
The main advantage of dynamic parameters is that - as oppose to static parameters - different usage of it will not generated a new type.
The disadvantage is that dynamic parameters can only be used as abstract references, as the same code handles references of different types with unknown structure.
Embedding a Dynamic Reference in a Static Structures¶
Typing style for the embedded dynamic reference syntax.
For some memory optimization scenarios, it is better if a dynamic reference of a class will be implemented only with one C pointer, and the dynamic structure reference will be embedded inside the type static structure (as done in C++).
Lumi will support this, but the exact implementation is still under planning.
Automatic Dynamic Interfaces¶
This is an experimental typing style idea that will allow automatic creation and implementation of dynamic interfaces based on the actual usage of a reference.
For each reference typed as Auto
the compiler will automatically create a
dynamic interface based on the methods called on this reference. Any type that
implements the same methods used by the reference can be assigned to it, and an
implementation of the dynamic interface will be automatically created by the
compiler. For example:
; a dynamic interface with "example" method will be created buy the compiler
; and used as the parameter actual type
func auto-example(user Auto automatically-typed-dynamic-reference)
automatically-typed-dynamic-reference.example()
struct SomeStruct
func some-method()
var SomeStruct some-item
; implementation to the automatically created dynamic interface will be
; created by the compiler that uses "SomeStruct.example" method as the
; implementation to the "example" dynamic method
auto-example(user some-item)
This feature is an experimental idea because it’s unclear whether it is a good idea, and there may be some edge cases that will make it hard to implement.
General Syntax Highlights¶
blocks are declared with indentation (as in Python)
strict white-spacing:
tabs are syntax errors
indentation is exactly 4 spaces
no spaces in line end
extra indentation for line breaking is exactly 8 spaces
exactly one space around operators
file must end with a single newline
in general, any whitespace in the syntax must be used exactly
strict naming conventions:
the default for everything is
lowercase-only-with-hyphens
(a.k.a “kebab-case”)types are
FirstLetterUppercase
(a.k.a “CamelCase)compile time constants are
UPPERCASE-ONLY-WITH-HYPHENS
(a.k.a “FAT-KEBAB-CASE”)
TL[number] - Temporary Lumi Language¶
Lumi language development is done in an iterative style, where in each step a compiler is written to a temporary Lumi language - “TL” in short - which is a partial (or different) syntax of the final Lumi language.
These temporary Lumi languages are marked as “TL[number]” where “number is the iteration step number. “TL0” is the initial compiler temporary Lumi language, the next iteration is TL1 and so on…
Latest Version - TL5¶
The Lumi language is still a work in progress and the final syntax is not decided yet. The latest working compiler is for Temporary Lumi 5 (TL5) syntax, and this guide will describe it, and the differences between it and the planned final Lumi syntax.
The final Lumi syntax is still under planning, so this guide refers only to the current planning state of the final syntax. Changes will happily be made based on coding experience and suggestions.
Basic Syntax¶
Comments¶
In TL5 Single line comments start with ;
, multi-line
comments start with [;
, and end with ;]
.
Comments that are not in line start are not supported yet - but will be
supported in the final syntax.
; single line comment
[; <-- multi-line comment start
multi
line
comment
multi-line comment end --> ;]
var Uint32 x ; not supported yet :(
Some suggest to change this in the final syntax to be as in C with //
,
/*
and */
.
Documentation¶
Documentation have their own dedicated syntax: they start and end with ~~~
.
Documentation must be placed at line start and may be single or multi-line.
In TL5 documentation are treated as comments. In the final syntax they must come before the element they are documenting, they could be used dynamically in the code, and would be used to automatically generate external documentation.
~~~ single line documentation ~~~
func documented-function()
; do stuff
~~~ <-- multi-line documentation start
multi
line
documentation
multi-line documentation end --> ~~~
func another-documented-function()
; do stuff
Operators¶
assignment:
:=
arithmetic:
+
,-
,*
,div
,mod
,clamp
,wraparound
assignment and arithmetic:
+=
,-=
,*=
bitwise:
bnot
,bor
,band
,xor
,shr
,shl
relational (arithmetic):
=
,<>
,<
,>
,<=
,>=
relational (referential):
is
,is-not
,?
logical:
not
,or
,and
miscellaneous:
.
,[]
,[:]
,()
,:=:
Any operator may be followed by a line brake with additional indentation of exactly 8 spaces:
x := 3 +
4
y :=
3 + 4
z :=
3 +
4
Operator Precedence¶
. [] () ?
, left-to-rightbnot
- +
,* div mod
,bor band xor shr shl
, left-to-right [1]= != > < >= <= is is-not
, left-to-right [2]not
or
,and
, left-to-right [1]clamp wraparound
, only one allowed:= += -= *= :=:
, only one allowed
[1] cannot combine operators from different sub-groups of this group, they
must be separated using ()
, for example a + b * c
is not legal and
should be changed to a + (b * c)
[2] multiple operators from this group combined will be separated with
and
operator, for example, a < b < c < d
is treated as a < b and
b < c and c < d
Modules¶
In TL5 each Lumi file is declared under a single module, multiple files may be declared under the same module.
The first line of each file must declare its module using the module
keyword:
module my-module-name
Only a single documentation block can come before it.
Using any item of another module must come after the other module prefix:
var other-module.SomeType variable
other-moudle.function(user variable)
In the final syntax modules and libraries support will be greatly extended - the exact syntax is still under planning.
Built-in Types¶
Integer¶
- class Int(minimum-value, maximum-value)¶
- Parameters
minimum-value – (optional) the minimum legal value (inclusive), default is
0
maximum-value – the maximum legal value (inclusive)
An integer that can only hold a range of values between its minimum and maximum (inclusive), enforced by the compiler. Obviously “minimum-value” should be equal or smaller than “maximum-value”.
For example:
Int{10:20}
can only hold numbers between10
and20
.If only one parameter is given it is treated as the maximum and the minimum is automatically
0
.For example:
Int{100}
can only hold numbers between0
and100
.If the minimum is negative the integer is consider “signed”, else it is “unsigned”.
The actual representation of an integer in the memory is the minimal possible from 8,16,32,64 bits based on the limits. This mean that only ranges that fit in
uint64_t
orint64_t
are supported:-9223372036854775808
to9223372036854775807
for signed integer, and0
to18446744073709551615
for unsigned.The default value of an uninitialized integer is
0
if it is within its legal range, otherwise the integer must be manually initialized.- Aliases for common ranges:
Uint8
forInt{0:255}
Sint8
forInt{-127:127}
Uint16
forInt{0:65535}
Sint16
forInt{-32767:32767}
Uint32
forInt{0:4294967295}
Sint32
forInt{-2147483647:2147483647}
Uint64
forInt{0:18446744073709551615}
Sint64
forInt{-9223372036854775807:9223372036854775807}
- Supported integer literals:
decimal:
70695
binary:
0b100101
octal:
0175
hexadecimal:
0xa30eb9f6
negative integer:
-
before any positive integer literal:-23
A complete guide to integer range management is in Integer Ranges and Overflow Prevention.
- str(user String text)¶
write into
text
the integer converted to string in decimal- Raises
if
text
is too short to store the number
Boolean¶
- class Bool¶
A boolean value that can only be a
true
or afalse
constant.
Constants:
- Bool true
- Bool false
Character¶
- class Char¶
A single Unicode code-point, which is the same as
Int{1114111}
.Character literal are surrounded with
'
characters:'a'
. Special characters can be written with\
escape character as in C:\' \" \? \a \b \f \n \r \t \v \\
.
Real Number¶
planned - not supported yet in TL5
- class Real¶
Floating point real number, same as
float
in C.Real number literal is a decimal number with
.
character in the middle, with optional exponential suffix:2.4
,-0.3
,4.0
,2.34e2
,-5.678e-12
.
Function¶
Array¶
- class Array(length, subtype)¶
Sequence of any typed item with static length.
- Parameters
length – array static length and the actual allocation size
subtype – the type of each item in the array
For example:
Array{12:Uint32}
,Array{6:String{16}}
.Array references should be declared without the
length
parameter: justArray{Uint32}
orArray{String}
for example.Accessing a single item can be done using
array[index]
.Note
If the index can be out of range it is checked at run-time and an error may raise. In such case the
!
warning sign must be used if error is to be propagated:array[index]!
.It is possible to extract a sub-array from an array by slicing:
array[start-index:sub-array-length]
. This will not copy the array but return an array reference that points to the original array.- length()->(var Uint32 length)¶
return (static) length of the array
Buffer¶
String¶
- class String¶
Holds a legal UTF-8 string with dynamic length. The compiler ensures that the last character is a null-terminator (
'\0'
).String literals are strings surrounded by
"
characters:"I am a string literal"
. Escape characters can be used.String literals may contain line breaks, with additional indentation of exactly 8 spaces. It is treated as
\n
, or ignored if\
is used before it:; the same as "line\nbrake" s := "line break" ; the same as "linebrake" s := "line\ break"
String is currently not implicitly converted to
Array{Byte}
when needed.- length()->(var Uint32 length)¶
returns current (dynamic) string length, not counting the null-terminator
- new(user Buffer value)¶
initialize this string with a copy of
value
- Raises
if not enough memory
- clear()¶
make this string empty
- equal(user Buffer other)->(var Bool is-equal)¶
return whether this string is exactly equal to
other
- get(copy Uint32 index)->(var Char value)¶
return character at place
index
- Raises
if
index
is out of range
- set(copy Uint32 index, copy Char value)¶
set character at place
index
tovalue
- Raises
if
index
is out of range
- append(copy Char character)¶
append
character
to this string end- Raises
if has no room for another character
- concat(user Buffer other)¶
concatenate
other
to this string end- Raises
if has no room for
other
- concat-int(copy Sint64 number)
covert
number
to string and concatenate it to this string end- Raises
if has no room for
number
string
- find(user Buffer pattern)->(copy Uint32 index)¶
return index of first occurrence of
pattern
in this string, return this stringlength
ifpattern
not found
- has(copy Char character)->(var Bool has)¶
return whether this string contains
character
Files¶
- class File¶
Basic type for managing files, is extended by these types:
FileReadText
FileReadBinary
FileWriteText
FileWriteBinary
FileReadWriteText
FileReadWriteBinary
- FileReadText(user String file-name)¶
open
file-name
for read only in textual mode- Raises
if file opening failed
- FileReadBinary(user String file-name)¶
open
file-name
for read only in binary mode- Raises
if file opening failed
- FileWriteText(user String file-name, copy Bool append)¶
open
file-name
for write only in textual modefile is created if it does not exist
if
append
is true all writes will be appended to the file end- Raises
if file opening failed
- FileWriteBinary(user String file-name, copy Bool append)¶
open
file-name
for write only in binary modefile is created if it does not exist
if
append
is true all writes will be appended to the file end- Raises
if file opening failed
- FileReadWriteText(user String file-name, copy Bool append, copy Bool create)¶
open
file-name
for read and write in textual modeif
append
is true:file is created if it does not exist
all writes will be appended to the file end
create
is ignoredelse, if
create
is true file is cleared of data if exists, or created if it does not exist- Raises
if file opening failed
- FileReadWriteBinary(user String file-name, copy Bool append, copy Bool exist)¶
open
file-name
for read and write in binary modeif
append
is true:file is created if it does not exist
all writes will be appended to the file end
create
is ignoredelse, if
create
is true file is cleared of data if exists, or created if it does not exist- Raises
if file opening failed
- close()¶
close this file, does nothing if this file is already closed
- Raises
if closing failed
- tell()->(var Sint64 offset)¶
return current position of the file
- Raises
if getting offset failed
- seek-set(var Sint64 offset)
set file position to
offset
relative to file start- Raises
if setting offset failed
- seek-cur(var Sint64 offset)
set file position to
offset
relative to the current position- Raises
if setting offset failed
- seek-end(var Sint64 offset)
set file position to
offset
relative to file end- Raises
if setting offset failed
- flush()¶
flush any buffered written data to the file
- Raises
if flush failed
- get()->(var Char value, var Bool is-eof)¶
only available in
FileReadText
andFileReadWriteText
read one character from this file into
value
or setis-eof
totrue
if end-of-file reached- Raises
if read failed
- get()->(var Byte value, var Bool is-eof)
only available in
FileReadBinary
andFileReadWriteBinary
read one byte from this file into
value
or setis-eof
totrue
if end-of-file reached- Raises
if read failed
- getline()->(user String line, var Bool is-eof)¶
only available in
FileReadText
andFileReadWriteText
read one line from this file into
line
or setis-eof
totrue
if end-of-file reachednew-line character is not added to
line
end- Raises
if read failed or
line
is too short to store the line
- read(user Array{Byte} data)->(var Uint32 bytes-read)¶
only available in
FileReadBinary
andFileReadWriteBinary
read bytes from file to
data
up to the its length, set inbytes-read
the number of actual read bytes- Raises
if read failed
- put(copy Char value)¶
only available in
FileWriteText
andFileReadWriteText
write
value
character to this file- Raises
if writing failed
- put(copy Byte value)
only available in
FileWriteBinary
andFileReadWriteBinary
write
value
byte to this file- Raises
if writing failed
- write(user Array{Char} data)->(var Uint32 written)¶
only available in
FileWriteText
andFileReadWriteText
try write all
data
characters to this file, set inwritten
the number of actual written characters- Raises
if writing failed
- write(user Array{Byte} data)->(var Uint32 written)
only available in
FileWriteBinary
andFileReadWriteBinary
try write all
data
bytes to this file, set inwritten
the number of actual written bytes- Raises
if writing failed
sys
Module¶
- Array{String} sys.argv
holds program arguments
- FileReadText sys.stdin
can be used to read from the standard input stream
- FileWriteText sys.stdout
can be used to write to the standard output stream
- FileWriteText sys.stderr
can be used to write to the standard error stream
- sys.print(user String text)¶
print
text
to the standard output stream, same as callingsys.stdout.write
- Raises
if writing failed
- sys.println(user String text)¶
print
text
appended with new-line character to the standard output stream- Raises
if writing failed
- sys.getchar()->(var Char character, var Bool is-eof)¶
read one character from the standard input stream into
value
or setis-eof
totrue
if end-of-file reached, same as callingsys.stdin.get
- Raises
if read failed
- sys.getline(user String line)->(var Bool is-eof)¶
read one line from the standard input stream into
line
or setis-eof
totrue
if end-of-file reached, same as callingsys.stdin.getline
new-line character is not added to
line
end- Raises
if read failed or
line
is too short to store the line
- sys.exit(copy Sint32 status)¶
terminates execution of the program immediately with
status
as the exit status valuecalls C
exit
function- Raises
if exiting failed
- sys.system(user String command)->(var Sint32 status)¶
execute
command
by the host command processor and return the return status of the commandcalls C
system
function- Raises
if command fails to execute
- sys.getenv(user String name, user String value)->(var Bool exists)¶
set into
value
the value of the environment variablename
, or setexists
tofalse
if it does not exist
Variables and Constants¶
In TL5 the Lumi memory management is mostly implemented, excluding the third management form from the 3 planned.
Compile-Time Constants¶
In TL5 only global integer compile-time constants are supported. The final Lumi syntax is planned to support constants from all types, and allow definition of constants inside the name-space of a specific type.
Integer compile-time constants are declared in TL5 by:
const Int CONSTANT-NAME 12
Here 12
is an example constant value with range Int{12:12}
. The
constant value can be any constant expression, which may include:
integer numbers
other integer constants
enumerators
integer operators, where the operands are constant expressions
Enumerators¶
Enumerators are a set of constant symbols that are treated as
integer compile-time constants. The first symbol is allocated a value of 0
,
the second is 1
and so on…
In TL5 enumerators can only be declared in the global scope. The final Lumi syntax is planned to support enumerators under a specific type, will allow definition of specific values for the enumerator symbols, and will generate automatic conversion functions between symbol names and values.
Enumerators are declared in TL5 by:
enum EnumeratorName
FIRST-SYMBOL-NAME
SECOND-SYMBOL-NAME
THIRD-SYMBOL-NAME
Using an enumerator is done by EnumeratorName.SYMBOL-NAME
.
The amount of values is defined by a special length
value, for example
EnumeratorName.length
is 3
.
Primitive Variables¶
Primitive variables are declared using var
keyword:
var Int{100} integer-variable
var Int{10:20} with-initialization(copy 12)
If no explicit initialization value given - primitive values are initialized with each type’s default initialization value:
References¶
References are declared using the wanted memory access keywords:
owner
: simple owner referenceuser
: simple user referencetemp
: simple temporary owner referencestrong
: reference counted strong referenceweak
: reference counted weak reference
owner String string-owner-reference
user Array{Uint32} user-reference-with-initialization(user some-int-array)
temp String temporary-owner-reference
strong String string-strong-reference
weak Array{Uint32} weak-reference-with-initialization(weak some-int-array)
References must be assigned with a value before used.
For primitive type references the pointed value can be accessed using a special
value
named field:
int-reference.value := 4
Conditionals¶
Conditional references are declared by appending ?
character in type end:
owner String? conditional-owner-reference
user Array?{Uint32} conditional-array-with-initialization(user some-int-array)
The _
sign can be used to represent an empty reference:
conditional-reference := _ ; setting the reference to be empty
func-with-conditional-argument(user _) ; passing empty to a function
If no explicit initialization value given - conditional references are by
default initialized as empty (_
).
When a conditional reference is used it is checked at run-time to not be empty. If it is - an error is raised.
Note
In such case the !
warning sign must be used if error is to be
propagated: conditional-reference!.field
Weak References¶
Weak references may point to outdated data that was removed in the past. Therefor, when a weak reference is used it is checked at run-time to not be outdated. If it is - an error is raised.
Note
In such case the !
warning sign must be used if error is to be
propagated: weak-reference!.field
Comparisons¶
Comparing references by-reference is done using the is
and is-not
operators.
the ?
operator can be used to check if a reference is usable: not empty and
not an outdated weak reference. To explicitly check for emptiness and not for
being outdated of a reference that is both conditional and weak is
and
is-not
operators can be used with _
as operand.
if first-reference is second-reference
; both references reference to the same object, or both are empty
if first-reference is-not second-reference
; both references do not reference to the same object
if reference?
; reference is usable - not empty and not outdated
if not reference?
; reference is not usable - it is empty or outdated
if reference is _
; reference is empty
if reference is-not _
; reference is not empty, but may or may not be outdated
Static Allocation¶
Static allocation is done using var
or s-var
keywords:
var String{256} string-static-allocation
s-var Array{34:Uint32} static-strong-int-array!
Note
s-var
initialization may fail - the !
warning sign must be used
if error is to be propagated
Doing this in the global scope will allocate the data in the process global data section. Doing this in a function scope will allocate the data in the stack.
Statically allocated variables cannot pass their ownership to owner
references.
Dynamic Allocation¶
Dynamic allocation is done by using the type as a function:
string-owner-reference := String{256}()!
array-strong-reference := Array{34:Uint32}()!
Note
dynamic allocation may fail - the !
warning sign must be used
if error is to be propagated
It’s probably a good idea to store the returned object in an owner
reference, otherwise it will be deleted in the end of the block.
Functions¶
Summary¶
Function are declared using the func
keyword
func func-name(copy Uint32 input-argument)->(var Uint32 output-argument)
; function implementation...
Functions are called using their name:
func-name(copy 4)->(var some-int)
Arguments¶
First input arguments (a.k.a “parameters”) are written inside (...)
. If the
function has no input arguments an empty ()
should be used.
If the function has output arguments (“outputs” in short) they are written
second inside a different (...)
after a ->
symbol with no whitespace.
Multiple outputs are supported.
In function deceleration each argument must be declared with access type
name
trio. In function calling only access name
duo is needed.
See Access explanation below.
A new line can be placed before any argument access, with additional indentation of exactly 4 spaces:
func split-arguments0(copy Uint32 x, copy Uint32 y)->(
var Uint32 z, var Uint32 w)
func split-arguments1(
copy Uint32 x, copy Uint32 y)->(var Uint32 z, var Uint32 w)
func split-arguments2(
copy Uint32 x, copy Uint32 y)->(
var Uint32 z, var Uint32 w)
func split-arguments3(
copy Uint32 x,
copy Uint32 y)->(
var Uint32 z,
var Uint32 w)
Access¶
An “access” defines the memory access of the argument. It can be one of:
copy
parameter:
the parameter is a new memory copy of the called argument
changes to the parameter will not change any caller variable
any expression can be given
var
output:
the parameter is a reflection of an actual variable
changes to the parameter will also change the caller variable
only a writable value can be given
user
, owner
, temp
, strong
or weak
parameter:
the argument is a reference to an object
changes to the reference itself will not change the called reference
changes to the object will change the called object
any expression can be given
user
orweak
means the parameter is a simple referenceowner
orstrong
means the caller has passed the ownership of the referenced object to the function, and the object will be deleted in the function end if the ownership is not passed in the function bodytemp
means the caller has temporally passed the ownership of the referenced object to the function, and the ownership will be returned after function end and cannot be deleted or permanently passed forward by the function
user
, owner
, strong
or weak
output:
the parameter is a reflection of an actual reference
changes to the reference itself will also change the called reference
only a writable value can be given
In TL5 copy
and var
are not yet supported for
complex types.
In the planned final syntax this will be extended, and the access may be omitted in a default usage.
Return and Output¶
In TL5 output is written by setting a value to an output argument:
func example()->(var Uint32 first-output, user String second-output)
first-output := 4
second-output := String{16}()
A return
statement can be used to stop the function in the middle:
func example(copy int x)
if x < 0
return
; do something
In the final syntax this may be possible:
func example()->(var Uint32 first-output, owner String second-output)
return 4, String{16}()
Error Handling¶
Raising an error can be done using the raise
statement. Functions that
may raise an error must be marked with !
:
func ! example()
raise!
In TL5 an optional string expression can be raised:
func ! example()
raise! "error message"
Error Propagation¶
Unless caught, raised error will propagate to the caller function, up until the main function - where uncaught errors will stop the execution of the program, print the raised error message if given, and print a call traceback.
In the function code whenever an error may be raised and propagated to the
caller - the !
warning sign must be added. A functions that may raise an
error must also add the !
warning sign to its deceleration.
Error Catching¶
A local error can be handled using if-ok
or if-error
:
if-ok x := array[3]
; no error raised
else
; index out of bound handling
if-error x := array[4]
; index out of bound handling
else-if-ok x := array[6]
; no error raised
else-if-error x := array[5]
; index out of bound handling
else
; no error raised
Note
if-ok
must be followed by else
to ensure error is not ignored
A try
statement will catch an error raised inside it and break the
execution of the rest of the block. The error will be ignored unless try
is
followed by a catch
statement. The code under the catch
statement will
only run if the above try
statement caught an error.
try
; do something that may raise errors
catch
; do some error handling
Calling a Function¶
When calling a function the access of each argument must be written:
example(copy primitive-input, user reference-input)->(
var primitive-output, owner owner-output)
If the function may raise an error and the caller propagates the error - !
warning sign must be used:
raising-example(copy input)->(var output)!
User Defined Types¶
As explained in Type System, Lumi allows variety of typing styles for creating user defined types.
In TL5 only basic structures and classes are supported.
User defined types behave like built-in complex types with references, static allocation and dynamic allocation.
Static Structures¶
Syntax for the static structure typing style.
In TL5, structures may not contain string or array variables, only their respected references. It will be supported in the final Lumi syntax.
Structures are declared using the struct
keyword:
struct ExampleStruct
var Uint32 integer-variable
user String string-reference
Members of struct can be accessed using .
operator:
var ExampleStruct struct-variable
struct-variable.integer-variable := 3
struct-variable.string-reference := "some string"
Structures are implemented in C as simple C structures. Structure references are implemented as pointers to the C structure.
Global Members¶
This is not supported yet in TL5.
Global members are declared under the type scope:
struct ExampleStruct
const Uint32 GLOBAL-CONSTANT(12)
global var Uint32 global-variable
Outside the type definitions they can only be accessed with the type name as prefix:
some-integer := ExampleStruct.GLOBAL-CONSTANT
ExampleStruct.global-variable := 5
Methods¶
Methods are declared as normal functions, except they are
declared inside the type scope, and the self
parameter should not be
declared, instead, its access is declared before the function name.
Inside the
method implementation self
keyword can be used to access the implicit self
parameter. Constants and global variables of the type can be accessed using
global
keyword.
struct ExampleStruct
var Uint32 integer-variable
const Uint32 GLOBAL-CONSTANT(12)
global var Uint32 global-variable
~~~ "self" access is "user" ~~~
func user method(copy Uint32 num)
self.integer-variable := num + global.GLOBAL-CONSTANT
global.global-variable := num
It possible to split the function deceleration from its implementation. In this
case the function deceleration should be followed by _
.
struct ExampleStruct
func user method(copy Uint32 num) _
func user ExampleStruct.method(copy Uint32 num)
; implementation...
There are two ways to call a method:
instance.method(copy 4) ; OOP style
ExampleStruct.method(var instance, copy 4) ; functional style
Constructor Method¶
If possible, structure members are automatically initialized to their default
value on construction. This can be extended by defining a “constructor” method
for the structure. This method will be called on every instance construction
after the default initialization. A constructor is declared with a dedicated
name new
.
struct ExampleStruct
new() _
func ExampleStruct.new()
; custom initialization
A constructor cannot have outputs, and if it has parameters - they must be given on every object creation:
struct ExampleStruct
var Uint32 integer-variable
owner String string-reference
new(copy Uint32 x, owner String s)
self.integer-variable := x
self.string-reference := s
func usage()
var ExampleStruct variable(copy 4, owner String{12}(user "some string"))
owner ExampleStruct reference := ExampleStruct(copy 4,
owner String{12}(user "some string"))
Structures that have members without a defined default value must implement a constructor. The constructor must also directly initialize these fields. Members without a defined default value are:
non-conditional references
integers that
0
is not a legal value of their rangevariables of types with a constructor
Destructor Method¶
A “destructor” method can also be defined for a structure. This method will be
called just before any object destruction. A destructor is declared as a normal
method with a dedicated name cleanup
.
struct ExampleStruct
cleanup() _
func ExampleStruct.cleanup()
; destruction code
A destructor cannot have any kind of arguments.
In TL5 destructors cannot raise errors - but it may be supported in the future.
Note
Lumi Automatically deletes any memory allocated in the structure and calls the cleanup function of all members and base classes - there is no need to do it manually
Extending Structures¶
In TL5 a structure may only extend one other structure.
struct ExtendingStruct(BaseStruct, OtherBaseStruct)
var Uint32 additional-field
The extending structure may be used in any place one of its base structures is expected:
owner BaseStruct base-struct := ExtendingStruct()
The extending structure may overwrite a base method, the overwriting method arguments access and type must be identical to the base overridden method.
struct BaseStruct
func method(copy Uint32 num)
; implementation...
struct ExtendingStruct(BaseStruct)
func method(copy Uint32 num)
; other implementation...
An overwriting function can call the overwritten function using base
keyword. Other overwritten methods can be called using base.other-method
.
struct ExtendingStruct(BaseStruct)
func method(copy Uint32 num)
base(copy num)
base.other-method()
Example for the static dispatch of structures:
var ExtendingStruct extending-struct
user BaseStruct base-struct(user extending-struct)
extending-struct.method(copy 4) ; will call ExtendingStruct.method
base-struct.method(copy 4) ; will call BaseStruct.method
BaseStruct.method(var extending-struct, copy 4) ; will call BaseStruct.method
Dynamic Interfaces¶
Syntax for the dynamic interface typing style.
Dynamic interfaces (or “dynamics” in short) are declared using the dynamic
keyword:
dynamic ExampleDynamic
func dynamic-method(copy Uint32 num)
func another-method()->(var Uint32 result)
var Uint32 dynamic-variable
Dynamic variables are not supported in TL5.
Dynamics are always used as references and cannot be allocated because they have no structure:
func use-dynamic(user ExampleDynamic example)
example.dynamic-method(copy 3)
Now use-dynamic
function can be called with any item that implements
ExampleDynamic
.
Dynamics are implemented in C as a C structure containing all the dynamic members, where dynamic methods are implemented as pointer to functions. Each implementation of the dynamic is a global instance of this structure. Dynamic references are implemented as 2 references: one reference to the dynamic structure and another reference to the implementing type instance.
Non-Dynamic Members¶
This is not supported in TL5.
Constants and global variables are declared and used exactly as global members in static structures.
Static methods must be declared using static
prefix:
dynamic ExampleDynamic
func dynamic-method(copy Uint32 num)
static func static-method(copy Uint32 num)
; implementation
Extending Dynamics¶
This is not supported in TL5.
Same syntax as structures:
dynamic ExtendingDynamic(BaseDynamic, OtherBaseDynamic)
func additional-method(copy Uint32 num)
Support Dynamics in Structures¶
A structure can support a dynamic by implementing all
its dynamic members and explicitly declare it using the support
keyword.
some implemented members may be added under the support
line:
struct ExampleStructure
func method(copy Uint32 num)
; implementation...
support ExampleDynamic in ExampleStructure
func another-method()->(var Uint32 result)
; another implementation...
When a structure supports a dynamic, every structure
that extends it also supports the dynamic using the base structure
implementation. The extending structure may override some members of the
dynamic, but to use these overrides as the implementation of the dynamic
another support
statement for the extended structure must be added:
struct BaseStruct
func method(copy Uint32 num)
; base implementation...
support ExampleDynamic in BaseStruct
struct ExtendingStruct(BaseStruct)
func method(copy Uint32 num)
; overriding implementation...
support ExampleDynamic in ExtendingStruct
Example for the dynamic dispatch of dynamics:
var ExampleStructure example-struct
var ExtendingStruct extending-struct
user BaseStruct base-struct(user extending-struct)
user ExampleDynamic example-dynamic
example-dynamic := example-struct
example-dynamic.method(copy 4) ; will call ExampleStructure.method
example-dynamic := extending-struct
example-dynamic.method(copy 4) ; will call ExtendingStruct.method
example-dynamic := base-struct
example-dynamic.method(copy 4) ; will call BaseStruct.method
; will not call ExtendingStruct.method becasue structure dispach is static
Default Dynamic Member Implementation¶
This is not supported in TL5.
A dynamic may give a default implementation to some or all of its members and
its base dynamics members. Method implementations can use self
and
global
keywords to access its own members.
dynamic ExampleDynamic
func implemented-method(copy Uint32 num) _
func unimplemented-method()->(var Uint32 result)
var Uint32 implemented-variable(copy 5)
var Uint32 unimplemented-variable
func ExampleDynamic.implemented-method(copy Uint32 num) _
; implementation...
Classes and Binds¶
Syntax for the class typing style.
In TL5 this only partially implemented:
Only
class
type definition is supported,Bind
is notAll restrictions on structures also apply to classes
Only methods can be dynamic
Variables don’t need to start with
static
keyword - as they cannot be dynamic or global
A straightforward way to use classes is using the built-in Bind
typed
references. References of this type only accept types that extend all bound
structures and implement all bound dynamics.
user Bind{ExampleStruct:ExampleDynamic} class-reference
Another way to use classes is to declare a type as a class in its definition
using the class
keyword. Each non-global member of the class must come
after a static
or a dynamic
keyword to declare witch implicit type this
member belongs to: the structure or the dynamic. Global members are only
defined under the name-space of the class.
class ExampleClass
static var Uint32 static-field ; part of the implicit structure
dynamic func dynamic-method(copy Uint32 num) ; part of the implicit dynamic
global var Uint32 global-variable ; defined under the class name-space
Classes can implement dynamics using the same syntax as structures.
Class references are implemented using two C pointers: one for the structure, and one for the dynamic.
Extending Classes¶
As all types:
class ExtendingClass(BaseStruct, BaseDynamic, BaseClass)
static var Uint32 addition-static-field
dynamic func addition-dynamic-method(copy Uint32 num)
In TL5 a class may only extend one other class or structure.
Example for the dynamic dispatch of classes:
var ExtendingClass extending-class
user BaseClass base-class(user extending-class)
user ExampleDynamic example-dynamic
extending-class.method() ; will call ExtendingClass.method
base-class.method() ; will call ExtendingClass.method
example-dynamic := extending-struct
example-dynamic.method() ; will call ExtendingStruct.method
example-dynamic := base-struct
example-dynamic.method() ; will call ExtendingStruct.method
Using the Implicit Structure or Dynamic of a Class¶
This is not supported in TL5.
The implicit structure of a class can be used using the built-in Struct
type, and the implicit dynamic can be used using the built-in Dynamic
type. This is not supported in TL5.
var Struct{ExampleClass} static-structure-only
user Dynamic{ExampleClass} dynamic-interface-only
Parameterized Types¶
Syntax for the parameterized type typing style.
Each type parameter must have a type and a name. For static type names Type
should be used as the parameter type, and for dynamic parameters Generic
should be used as the parameter type. The parameter name must conform the
naming standard of types if one of these is used, else it must conform naming
standard of constants.
struct ParametrizedType{Uint32 CONSTANT-PARAMETER:Type TypeParameter:Generic GenericParameter}
var String{CONSTANT-PARAMETER} parametrized-sized-string
var TypeParameter static-parametrized-typed-variable
user GenericParameter dynamic-parametrized-typed-reference
Whenever a parameterized type is used it must be set with appropriate values for each parameter
var ParametrizedType{8:Uint32:File} specific-variable
This is partially supported in TL5:
only dynamic parameters are supported (
Generic
type)no need to add
Generic
- only the parameter name is neededArrays are not supported as parameter values
Embedded Dynamic Reference¶
Syntax for the embedded dynamic reference typing style.
This is not supported in TL5.
Embedded classes can be declared using the built-in Embed
type:
; "ExampleStruct" structure with "ExampleDynamic" reference embedded
; inside it
var Embed{ExampleStruct:ExampleDynamic} explicit-embedded-variable
; "ExampleClass" static structure with a reference to its dynamic structure
; embedded inside it
var Embed{ExampleClass} implicit-embedded-variable
The syntax may change as this typing style is still under planning.
Control Flow¶
If-Else Condition¶
If-else condition is declared using if
, else
, and else-if
keywords.
The condition expression must be boolean typed.
if x > 4
; do something
else-if x < 2
; do something
else
; do something
Switch-Case Condition¶
This is not supported in TL5.
Switch-case condition is declared using the switch
keyword, contains
multiple case
blocks, and optionally one last default
block. A
fallthrough
statement must be used to fall-through the next case - the
default is not to fall-through.
switch number
case 34
; do something
case 23
; do something
fallthrough
case 45, 67, 26, 56, 67, 89, 56, 87
; do something
default
; do something
Simple Loop¶
Simple loop is declared using the loop
keyword and contains one or more
while
statements inside it. The loop continues while every while
statement inside it is true, and stops immediately when the first while
statement inside it is false.
loop
; do something
while number < 6
; do something
while not boolean-variable
; do something
Loops can be broken immediately using a break
statement:
loop
; do something
if number = 0
break
; do something
That makes while condition
the same as if not condition break
.
A continue
statement can be used to only stop the current iteration and
start over from loop beginning:
loop
; do something
if num = 3
continue
; do something
It is possible to limit the number of loop iterations, when the limit is reached an error is raised:
loop! number
; do something
while condition
Note
The !
warning sign must be used if error is to be propagated.
Loops must contain at least one while
, break
or return
statement -
otherwise the compiler will complain about an infinite loop. If an infinite
loop is intentional loop-infinite
must be used:
loop-infinite
; do something forever
Repeat Loop¶
A simple loop that just repeats itself a specific number of times:
repeat number
; do this "number" times
For Loop¶
For loop iterates over a specific set of values, and is declared using the
for
keyword.
Iterating numbers incrementally, limits can be any integer expression:
for number in 3:7
; "number" will iterate 3,4,5,6
Number iteration with explicit step amount, this is not supported in TL5:
for number in 9:1:-2
; "number" will iterate 9,7,5,3
Array iteration:
for item in array
; "item" will iterate each item of "array"
String iteration:
for character in "Example"
; "character" will iterate E,x,a,m,p,l,e
Buffer iteration:
for byte in `baffdaca`
; "character" will iterate ba,ff,da,ca
In all for loops it is possible to ignore the iteration item by replacing it
with _
:
for _ in 3:7
; will iterate 4 times
User Defined Iterators¶
A type can be made into an iterator in TL5 by implementing
a step
named method that has the following deceleration:
- step()->(user SomeType? value, var Bool has-another-item)¶
Is called once before any iteration. Iteration continue only if
has-another-item
istrue
. In such casevalue
returns the next iteration value, and the iteration should advance one step.SomeType
declared in this method is used as the iterator value type.
An instance of such iterator type can be used in for loops:
for item in iterator-instance
; "item" will iterate as implemented by "iterator-instance" type
This interface may change in the final syntax - the exact syntax is still under planning.
Testing¶
Lumi has built-in testing and mocking capabilities.
Testing code should be written under a different module than the tested code. In the testing code there should be test cases that test the tested module using assertions. Mocking should be used to simulate external interfaces.
Lumi can then run all test cases automatically, while also checking code coverage for the tested module.
Test Cases¶
Can be written using the test
keyword:
test test-case-name()
; test code
The test case is consider a success if the code runs without any assertion or runtime errors.
Assertions¶
Assertions test that a certain condition is true.
The basic assertion is the assert
statement that checks whether a given
boolean expression is true. If not, this statement will raise an assertion error
and the test will fail.
assert! number = 4
Another assertion is the assert-error
statement that checks whether a given
expression raises an error. If the execution of the statement didn’t raise an
error this statement will raise an assertion error and the test will fail.
assert-error! raising-function()
In TL5 assert-error
supports an optional additional
string literal that is the expected error message. If the raised error message
is not exactly the same as this literal, assert-error
will raise an
assertion error and the test will fail.
assert-error! raising-function(), "expected error message"
Note
The !
warning sign must be used if error is to be propagated, which is
recommended for the test to fail…
Mocking¶
Mocking can replace an external interface with simulated behavior.
In TL5, only functions and methods can be mocked.
Mocking a function or method is done using the mock
keyword. The function
name and its arguments access and type must much the mocked function exactly.
Whenever the mocked function is used the mocking function is called instead.
mock mocked-module.mocked-function(copy Uint32 input)->(var Uint32 output)
; mocking body
mock mocked-module.MockedType.mocked-method()
; mocking body
Built-in functions and methods can also be mocked:
mock Sys.print(user String text)
; mocking body
mock FileReadText.new(user String filename)
; mocking body
The mocked function can still be called using mocked
member:
mocked-function-name.mocked()
To disable mocking of a function active
member can be set to
false
. To re-enable mocking it can be set back to true
.
Mocks are active by default.
mocked-function-name.active := false
mocked-function-name()
mocked-function-name.active := true
Interacting with Other Languages¶
Lumi allows calling C code directly. This is useful for using an external C library, or using already written C code within Lumi code.
Warning
Calling C code cannot be guaranteed to be safe as the C code can mangle with the Lumi memory management.
It is also possible to call Lumi functions from other languages by compiling Lumi to a shared library that exports C style functions.
The cdef
Module¶
The builtin cdef
module contains various C declarations that help
interacting with C code.
C Primitives¶
cdef
contains many C primitive types:
cdef.Char
cdef.Uchar
cdef.Short
cdef.Ushort
cdef.Int
cdef.Uint
cdef.Long
cdef.Ulong
cdef.Size
cdef.Float
cdef.Double
cdef.LongDouble
C Pointers¶
cdef
contains cdef.Pointer
generic type to declare C pointers:
var cdef.Pointer{cdef.Int} pointer-to-int
var cdef.Pointer{cdef.Char} pointer-to-char
; "cdef.CharP" is an alias to "cdef.Pointer{cdef.Char}"
var cdef.CharP also-pointer-to-char
var cdef.Pointer{cdef.Uchar} pointer-to-uchar
var cdef.Pointer{cdef.Pointer{cdef.Int}} pointer-to-pointer-to-int
var cdef.Pointer void-pointer
Set and get pointed value:
var cdef.Int value
pointer-to-int.set-point-to(var value)
pointer-to-pointer-to-int.set-point-to(var pointer-to-int)
pointer-to-int := pointer-to-pointer-to-int.get-pointed-at(copy 0)
value := pointer-to-int.get-pointed-at(copy 0)
Get and set pointer from array, string and buffer:
user Array{cdef.Int} int-array
user String string
user Buffer buffer
pointer-to-int := int-array
pointer-to-char := string.cdef-pointer()
pointer-to-uchar := buffer
value := pointer-to-int.get-pointed-at(copy 3)
cdef.copy-to-string(copy pointer-to-char, user string)!
cdef.copy-to-buffer(copy pointer-to-uchar, copy 4, user buffer)!
Pointer to complex types:
user SomeStruct reference
var cdef.Pointer{SomeStruct} pointer-to-struct
pointer-to-struct := reference
reference := pointer-to-struct.get-ref-at(copy 0)
Calling C Functions¶
To call a C function from Lumi it must first be declared using the
native func
statement:
; allow calling a C function with header:
; "int some_c_function(int number)"
native func cdef.Int some-c-function(copy cdef.Int number)
; "_" characters in the C function name are replaced with "-" in Lumi
; allow calling a C function with header:
; "void CfunctionName(int number, char* text)"
native func name-used-in-lumi(
copy cdef.Int number,
copy cdef.Pointer{cdef.Char} text) "CfunctionName"
; Lumi name can be different than the C name
These functions can now be called from Lumi code using their Lumi name.
Native functions Have some limitation compared to normal Lumi functions:
have no output arguments, only input parameters and an optional single return type
all parameters must be primitives, with
copy
accesscannot be called with output arguments:
native-function()->(copy output-argument)
is illegal, may useoutput-argument := native-function()
instead
Accessing C Global Variables¶
In order to access a C global variable it must be declared using the
native var
statement:
; allow accessing "int some_c_variable" global variable
native var cdef.Int some-c-variable
; allow accessing "char* CvariableName" global variable
native var cdef.Pointer{cdef.Char} name-used-in-lumi "CvariableName"
These variables can now be accessed from Lumi code using their Lumi name.
Only primitive types can be declared as native variables.
Accessing C Global Constants or Defines¶
In order to access a C global constant or a #define
value it must be
declared using the native const
statement:
; allow accessing "SOME_C_CONSTANT" global constant
native const cdef.Int SOME-C-CONSTANT
; allow accessing "c_constant_name" global constant
native const cdef.Int NAME-USED-IN-LUMI "c_constant_name"
These constant can now be accessed from Lumi code using their Lumi name.
Only primitive types can be declared as native constants. Currently in TL5 only integer types are supported.
Accessing C Structures¶
It is possible to access custom C structures and their internal fields using
the native struct
statement with var
lines for each needed field:
; allow using "SomeCStruct" structure that have fields:
; int some_filed;
; char* other_field;
native struct SomeCStruct
var cdef.Int some-filed
var cdef.Pointer{cdef.Char} other-field
; allow using "struct c_struct_name" structure that have fields:
; int CfieldName;
; char* CanotherName;
native struct NameUsedInLumi "struct c_struct_name"
var cdef.Int field-name-used-in-lumi "CfieldName"
var cdef.Pointer{cdef.Char} another-lumi-field "CanotherName"
Not all of the original fields must be declared - only the ones that are needed to be used in Lumi. It is also legal to not declare any fields at all:
native struct SomeCStruct
These structures can now be accessed from Lumi code using their Lumi name.
Native structures are treated as values and not as references like Lumi structures. A pointer to the native structures can be used instead:
var cdef.Pointer{SomeCStruct} pointer-to-native-struct
Native structures fields are accessed as in Lumi structures:
native-struct.some-filed
. This also works with pointers to native
structures: pointer-to-native-struct.some-filed
.
Native structures can be used in other native functions, variables, constants, and structures:
native func SomeCStruct c-func-name(copy SomeCStruct input)
native func cdef.Pointer{SomeCStruct} other-func(
copy cdef.Pointer{SomeCStruct} input)
native var SomeCStruct c-var-name
native var cdef.Pointer{SomeCStruct} other-var
native struct CstructName
var SomeCStruct struct-field
var cdef.Pointer{SomeCStruct} pointer-field
var cdef.Pointer{OtherStruct} self-pointer
Accessing Custom C Types¶
It is possible to handle values for custom C types that may be of any kind: integers, structures, pointers, etc. These types are treated as “abstract” values in Lumi, meaning that their exact structure is unknown and cannot be accessed.
C types can be declared using the native type
statement:
; allow using "SomeCtype" type:
native type SomeCtype
; allow using "c_type_name" type:
native type NameUsedInLumi "c_type_name"
These types can now be accessed from Lumi code using their Lumi name.
Native types are treated as abstract unknown values, the only way to use their content is by other C functions.
Writing C Code Directly¶
It is possible to write C code directly using native code
in global scope,
or just native
inside a function
native code "#define SOME_NEEDED_DEFINE 1"
func is-unix()->(var Bool result)
native "#ifdef __UNIX__"
result := true
native "#else"
result := false
native "#endif"
This may be used in some special cases where the other methods above are not sufficient, or to write some special glue code between Lumi and C.
C Wrapper Code¶
It’s recommended to wrap native C declarations with pure Lumi declarations that takes care for correct usage of the C declarations, and to present a simple and safe pure Lumi interface.
Standard Library¶
Currently implemented modules in TL5 standard library:
data structures (basic)
time
math (basic)
os (basic)
zlib (basic)
Planned future modules in the standard library:
more data structures
math (full)
os (full)
zlib (full)
system
IO
multiprocessing
paths and files
more compressions
web-server (DB, HTTP, ext…)
GUI
Code Examples¶
Hello World Program¶
A simple “Hello World” program written in TL5:
module hello-world
func ! show()
sys.println(user "hello world")!
main!
show()!
Testing the Hello World Program¶
Testing code for the “Hello World” program:
module hello-world-test
var Bool println-raise
var String printed-text
mock ! sys.println(user Buffer text)
if println-raise
raise! "error in println"
printed-text.concat(user text)!
test show-hello-world-test()
println-raise := false
printed-text.clear()
hello-world.show()!
assert! printed-text.equal(user "hello world")
test show-raise-test()
println-raise := true
assert-error! hello-world.show(), "error in println"
Fibonacci Function¶
func fibonacci(copy Uint64 n)->(var Uint64 res)
var Uint64 prev(copy 1)
res := 0
repeat n
var Uint64 sum(copy res clamp+ prev)
prev := res
res := sum
Complex number Type¶
struct Complex
var Sint64 real
var Sint64 imaginary
new(copy Sint64 real, copy Sint64 imaginary)
self.real := real
self.imaginary := imaginary
func user ! str(user String out-str)
self.real.str(user out-str)!
out-str.append(copy ' ')!
if self.imaginary > 0
out-str.append(copy '+')!
else
out-str.append(copy '-')!
out-str.append(copy ' ')!
var String imaginary-str
if self.imaginary > 0
self.imaginary.str(user imaginary-str)!
else
(- self.imaginary).str(user imaginary-str)!
out-str.concat(user imaginary-str)!
out-str.append(copy 'i')!
func ! usage-example()
var Complex complex(copy 5, copy 3)
var String complex-str
complex.str(user complex-str)!
sys.println(user complex-str)!
Serialization¶
This is not supported yet in TL5.
Lumi is planned to support 3 layout types for converting structures to buffers, and vice versa:
encoding - same layout as the structure platform-specific memory layout
serializing - platform-independent and CPU efficient implicit layout
packing - explicit, platform-independent, user defined layout
type |
CPU |
memory |
cross-platform |
explicit |
---|---|---|---|---|
encoding |
best |
medium |
no |
no |
serializing |
medium |
worst |
yes |
no |
packing |
worst |
best |
yes |
yes |