Single line comments in Cliver start with #
and multiline comments are enclosed within #= =#
pairs.
In Cliver identifiers, start with an underscore (_) or a utf-8 letter and thereafter can contain letters, numbers, underscores, and any utf-8 alphanumeric characters. They may end with an exclamation mark (!), followed optionally by single quotes (')
# valid identifiers
val Abc, _D0ef, Ghi!, Jkl', Mno!''
Cliver import syntax is inspired by JavaScript and TypeScript.
Any identifier prefixed with underscore (_) is considered private and cannot be imported.
These can only appear at the top of a scope.
All other identifiers are exported implicitely.
# import module for it's side effect
import Package\Module
import "./filename.cli"
# import all into current scope
import ... from Package\Module
import ... from "./filename.cli"
# import all as a namespace
import ...Abc from Package\Module
import ...Abc from "./filename.cli"
# import specific things
import A, B from Package\Module
import A, B from "./filename.cli"
# import as identifier syntax
import A as B from Package\Module
import A as B from "./filename.cli"
variables are declared with the var keyword and constants by the val keyword. Semicolons are optional in Cliver.
# without initial value
var x;
# with initial value
var x = value
val y = value
# with type annotation
var x :: Type = value
val y :: Type = value
# with type signature
var :: Type
x = value
val :: Type
y = value
There are a whole bunch of standard types and the type system is flexible enought to let you define your own types. The type declarations are polymorphic so they do support overloading like with functions. They also support destructuring.
# type alias
type NewType = ExistingType
type NewType(a, b) = ExistingType
Type constructors can take parameters; the parameter can be a generic type, an abstract type or a concrete type.
If the parameter is not annotated with a type, then it is considered generic.
If the parameter is abstract, the parameter value should be a subtype of the specified type.
If it's a concrete type, the parameter value should be a literal value of that type.
# type constructor with parameters
type TypeCtor(a, b :: AbstractType, c :: ConcreteType) =
| DataCtorA
| DataCtorB(c, b)
Abstract types have no values associated with them they are merely there for building the type hierarchy.
But however they can have a structural type definition.
An abstract type can inherit from another abstarct type.
Subtyping is only possible with Abstract types. The root abstract type is DataType.
Concrete types have one or more data constructors associated with them. All data constructors are publically accessable values.
# abstract type decalration
type AbstractCtor
# concrete type declaration
type ConcreteCtor =
| DataCtorA
| DataCtorB
type ConcreteCtor(a, b) =
| DataCtorA
| DataCtorB(a, b)
type constraints follow the same rules as type constructor parameters.
type ConcreteCtor =
| DataCtorA
| DataCtorB(a, b)
where a :: Type
# multiple constraints
type ConcreteCtor =
| DataCtorA
| DataCtorB(a, b)
where (a :: Type, b :: Type)
Structural typing defines the object structure of a type. They can have value assertion to check whether the value associated with the type meets certain conditions.
type AbstractCtor = {
propertyA :: Type,
methodB :: Type
}
# with value assertions
type AbstractCtor = {
value -> boolean_expression,
propertyA :: Type,
methodB :: Type
}
# with lone value assertion
type AbstractCtor where value -> boolean_expression
# in concrete types
type InterfaceType = {
propertyA :: Type,
methodB :: Type
}
type ConcreteCtor :: InterfaceType =
| DataCtorA
| DataCtorB(a, b)
where (a :: Type, b :: Type)
type Maybe(a) =
| Just(a)
| None
type Iterable(m) = {
map :: (a -> b) -> m(b)
}
instance self :: Maybe(a)
fun unwrap(): match self
case Just(x): x
case None: throw Error("Failed to unwrap Maybe value as it is 'None'")
end
instance self :: Maybe(a) impl Iterable(self)
fun map(f): match self
case Just(x): Just(f(x))
case None: None
end
Functions are the backbone of Cliver. There are 3 main types of functions. Their base type is AbstractFunction.
These are simple one-line function expressions.
# untyped
(...parameters) -> expression
# with type anotation
(paramA :: Type, paramB :: Type) :: Type -> expression
A function body has two varient
# inline varient
fun funName(...parameters): expression
# block varient
fun funName(paramA :: Type, paramB :: Type) :: Type
# ...
end
The functions syntax is flexible enough to create constructs such as Constructors, Generators, Macros, etc.
# Constructor
fun FunName<self S>() :: S({ aProp :: Type, aMethod :: Type })
# ...
end
# Generator
fun FunName<yield Y, payload P, return R>() :: Y(Type) + P(Type) + R(Type)
# ...
end
# Macro
fun FunName<macro M>() :: M(Type)
# ...
end
# ...etc,.
They are similar to NamedFunctions execpt they do not have a name.
# inline varient
fun(...parameters): expression
# block varient
fun(paramA :: Type, paramB :: Type) :: Type
# ...
end
If the type annotations of a UnitFunction gets out of hand, consider switching to an AnonFunction.
All operators in Cliver are just functions.
They can be referenced like functions and can be passed around like any other value.
# operator referance as callback
funName(argA, (+))
funName(argA, (*))
# operator invoked like a function
(+)(1, 2, 3, 4, 5) # 15
(*)(1, 2, 3, 4, 5) # 120
a do expression can contain any number of statements and/or expressions and returns the last value inside of it.
val item = do
# ...
value
end
In this operation, a value is passed through various functions and each function transforms it and passes the transformed value to the next function. There are two type of pipeline operators in Cliver, transformation pipelines and error pipelines. The pipeline syntax has two varients: point free pipelines and expressive pipelines.
# point free pipelines
value
`` functionA # transformation pipelines
`` functionB # transformation pipelines
?? e -> print(e) # error pipeline
# expressive pipelines
value as altVal
`` functionA(altVal)
`` functionB(altVal)
?? (e -> print(e))(altVal)
If a function accepts atleast 2 or optionally many arguments, then it can be called using infix function call notation.
fun add(...n): # ...
print(1 `add` 2) # 3
print(10 `add` 10.5) # 20.5
It is an alternative to the below approach.
# without external callback and using regular callback
funName(argA, fun(...args :: Array(Int)) :: String
# ...
end)
# with external callback notation
funName(argA, fun(...args :: Array(Int)) :: String) do
# ...
end
There are no classes in Cliver instead there are Constructor functions.
fun CtorFunction<self>(argA, argB)
# constructor logic...
@@where
val propA = value
fun methodB()
# ...
end
end
Accessors, i.e getters and setters are special functions.
fun CtorFunction<self>()
fun setVal<getter>()
# getter logic
end
fun getVal<setter>(value)
# setter logic
end
end
In Constructor functions, composition is done through import statements.
Cliver doesn't support inheritance in it's OO design.
type AType(S) = () -> S({ aProp :: Type })
type BType(S) = () -> ReturnType(AType(S)) + S({ bProp :: Type })
fun :: AType(S)
A<self S>()
# ...
val aProp = value
end
fun :: BType(S)
B<self S>()
# ...
@@where
val { ..._ } = A()
val bProp = value
end
The methods and properties of a static constructor is bound to the constructor rather than to the constructed objects.
fun CtorFunction<static S>() :: S({ propA :: Infer, methodB :: Infer })
# static constructor logic...
@@where
val propA = value
fun methodB()
# ...
end
end
Objects are similar to most other programming languages. They are created by Constructor functions.
CtorFunctionA()
CtorFunctionB()
The cascade notation is a syntatic form of the Builder design pattern.
objB()
..propA = value
..methodB()
..methodA()
There are two types of conditionals in Cliver; if conditional and match expression.
The pass
keyword skips the current conditional block.
There exists 3 syntatic variants of this construct.
# If statements - variant1
if condition
# ...
end
if condition
# ...
else
# ...
end
if condition
# ...
elseif condition
# ...
end
if condition
# ...
elseif condition
# ...
else
# ...
end
# If statements - variant2
if condition:
expression
if condition
# ...
else:
expression
if condition
# ...
elseif condition:
expression
if condition
# ...
elseif condition
# ...
else:
expression
There's another variant which makes use of pattern matching.
if expression as pattern
# ...
end
The third variant is the if...else expression
print(if condition: expression else: expression)
Usage of pass
keyword in an if statement
if condition
# ...
elseif condition
# ...
pass
# skip the current elseif block and execute the next conditional block, i.e the next elseif block
# ...
elseif condition
# ...
else
# ...
end
Match expression is the pattern matching construct in Cliver. It has 2 syntatic variants.
# match expression - variant1
val value = match expression do
case pattern:
statement / expression
statement / expression
expression
case pattern:
statement / expression
statement / expression
expression
case _:
statement / expression
statement / expression
expression
end
# match expression - variant2
val value = match expression
case pattern:
expression
case pattern:
expression
case _:
expression
Usage of pass
keyword in a match expression
val value = match expression
case pattern:
pass
case pattern:
expression
case _:
expression
There exists only one looping construct in Cliver. It has atleast 6 variants.
The statement form of the for loop can have 3 block macros inside of it.
The block macros can be one of:
@@broken
- the loop was terminated with a break clause@@completed
- the looping was completed successfully without breaking and it ran atleast once@@never
- the loop never ran.
# for statements
for item in iterable
# ...
end
for item in iterable
# ...
@@broken
# ...
@@completed
# ...
@@never
# ...
end
# traditional C-style syntax
for(i = 1; i < x; i += 1)
# ...
end
# syntatic equivalent of while loop
for condition
# ...
end
There exists a for expression which returns an iterator and can be used in arrays and other data structures.
It is lazy evaluated and needs to be spread using ...
operator in order to be evaluated.
val arr = [...for item in iterable: item]
break and continue are used to alter the execution of the loop and are only available within the for statement.
There are two main error handling constructs in Cliver and it is the do...catch and error pipeline operator.
It is used for block level error handling.
do...catch construct comes with an optional done block.
It will execute after the execution of all do and catch blocks, regardless of the error and can have 3 block macros inside of it.
The block macros can be one of:
@@caught
- there was an error and it was caught by a catch block,@@uncaught
- the error was not caught or an uncaught error was thrown,@@success
- the code ran without producing an error.
do
# ...
catch e :: Type
# ...
end
do
# ...
catch e: # This form doesn't support type annotation
expression
do
# ...
catch e :: Type
# ...
done
@@caught
# ...
@@uncaught
# ...
@@success
# ...
end
There's no expression variant of this construct.
The error pipeline operator functions identically to the do-catch expression except it is used for inline error handling and can also be used in function pipelines.
val someVal = expression ?? callback
If an expression throws an error and the expression is enclosed withn a function then it can be used to return the error as an object from the function.
fun funName()
val someVal = expression ?? x -> return x # exits the function named 'funName' with value x
end
These statements are used to enable and disable certain language features. These can only appear at the top of a scope.
use "linting: force semicolons", "!feature: variable shadowing"
use "!typing: type inference"
Labels are used in conjunction with break and continue clauses in loops and as expressions create associations between a value and an identifier. They both are similar in syntax and in a way functions similar to an assignment operation.
# Labels
outer as for x in arr:
inner as for y in x:
if condition:
break outer
# As expression
if expression as identifier
print(identifier)
end
# As expressions are useful in anonymous functions since they could be used to enable recursion
funName(identifier as fun()
# ...
identifier()
end)
Cliver has a wide variety of standard types.
This type is inspired by haskell. It is handy when dealing with potential empty values.
Maybe is a type constructor containing two data constructors.
type Maybe(a) =
| Just(a)
| None
# handling a Maybe value
val :: Maybe(Char)
item = ['A', 'B', 'C'].find(x -> x == 'D')
print(item || 'N')
# returning a maybe value
fun :: Array(Char) -> Char? # same as Maybe(Char)
findItem(arr)
# ...
return if found: Just(item) else: None
end
It is simply a compiler constant and is a type alias rather than being a distinct type.
The possible values of this type are primitive literals and expressions yielding primitive values that can be infered at compile time.
References types or runtime types are not permitted. Explicit type assertion is required for asserting Mustbe values.
name = "Abc" :: Mustbe(String)
name = f"Abc" # error
val newName = "Xyz"
name = newName
# returning Mustbe value
fun :: Int -> Infer
isEven(num)
return num % 2 == 0 :: Mustbe(Boolean)
end
print(isEven(10)) # True
This type constructor only contains 2 values, True and False
type Boolean() =
| True
| False
Number is an abstract type containg many core number types.
Int
Eg: -1, -2, 0, 1, 2, 3, ...
There's also a Uint counterpart.
type Int
type.sub(_ :: Int)
# Union(Int8, Int16, Int32, Int128)
Float
Eg: -2.0, -0.5, 1.0, 1.5, 10.99, ...
There's also a Ufloat counterpart.
type Float
type.sub(_ :: Float)
# Union(Float16, Float32, Float128)
BigNumber
This type represents arbitary precision Numbers.
Eg: BigInt - 1!n, 2!n, -10000!n ...
Eg: BigFloat - 1.2!n, -0.2!n, 11.5000!n ...
type BigNumber
type.sub(_ :: BigNumber)
# Union(BigInt, BigFloat)
BigFloat types are really only useful when used in conjunction with GenericIrrational or Fractional types
val Pi = 22!p / 7!p # :: GenericIrrational
val Tau = 2!p * Pi # :: GenericIrrational
val x = BigFloat(32, Pi), y = BigFloat(32, Tau)
print(x + y) # ...
Fractional
This type represents a ratio or a fraction. This is the only concrete type in the subtypes of Rational.
Eg: 1//2, 1//4, 3//4 ...
val fr :: Fractional(Int, Int) = 1//6
print(fr.numer, fr.denom) # Numerator(1) Denominator(6)
Int, Float, BigNumber and Fractional are subtypes of Rational which itself is a subtype of Real.
Irrationals
There are 3 values for this type NaN, Infinites and Infinity.
type Irrational() :: Real =
| NaN
| Infinites
| Infinity
Complex Numbers
Eg: 1 + 2!im, 1!im, 2 - 3im, ...
Unlike in mathematics, Complex is not a super type of Real rather they are sibling types in the type hierarchy.
Base-2 - decimal
Eg: 0b101, 0b1100, -0b1011, ...
Base-8 - octal
Eg: 0o347, 0o6534, -0o5260, ...
Base-16 - hexadecimal
Eg: 0xff460, 0xbc461, -0x20cae, ...
Scientific Notation
Eg: 6.022!e + 23, 1.6!e - 35, -5.3!e + 4, ...
Numbers can be tagged by an identifier.
fun :: Uint -> Int
fact(n): n * fact(n - 1)
print(5!fact) # 120
When multiplying a number with a identifier, you can omit the (*) sign and deal with multiplications in a mathematically accurate notation.
Eg: 2x + 1, -3y(5 + 2), 2.25z, ...
Implicit multiplication involving 0 as the numeric operand is invalid however 0.0 is valid.
Eg:0x, 0y + 4, ...
This data type represents either ASCII charactors or utf-8 unicode charactors.
Eg: ASCIIChar - 'A', '7', '!', ...
Eg: UnicodeChar - '🎉', 'Â', 'α', ...
type Char
type.sub(_ :: Char)
# Union(ASCIIChar, UnicodeChar)
String is an Array of Char values.
Eg: ASCIIString - "Abc", "$7ffG", "Ab*8", ...
Eg: UnicodeString - '🎉zzʑ', 'Âlp', 'α🕶ɜ', ...
Eg: IdString - \abC, \Bcd, \🎉ʑ01, ...
type String :: Array
type.sub(_ :: String)
# Union(ASCIIString, UnicodeString, IdString)
Strings are immutable but there exists a mutable version suffixed with !
# mutable String
"Abc"!
Notice however that IdStrings such as
\Abc!
is not mutable even though it is suffixed with!
When strings are placed next to each other, they can merge into a single string.
print("abc" "def" "ghi") # abcdefghi
# with IdString
print(\abc\def\ghi) # abcdefghi
This works with chars too; i.e they merge into a String in a similar fashion.
If a string is formed with atleast 3 double quotes, it can span multiple lines and can include n-1 consecutive double quotes where n is the number of double quotes it began with.
"""
multiline
string
"""
""""
also
multiline
string
""""
# mutable multiline string
"""
multiline
string
"""!
Strings can be tagged to enable interpolation using the 'f' tag. They can be turned into raw Strings using 'r' tag. The 're' tag turns a String into a Regular Expression.
val world = "earth", punch = '!'
val greet = f"hello {world}$punch"
print(greet) # hello earth!
val regex = re"w+@w+\.com"
val email = "[email protected]"
print(email.match(regex)) # Just(RegexMatch)
val rawText = r"newline (\n)"
print(rawText) # newline (\\n)
tagging can also be done with multiline strings
They can be finite or infinite.
type Range :: DataType
type.sub(_ :: Range)
# Union(NumericRange, UnicodeRange, DateTimeRange)
# syntax
(start, step) to last
Eg: NumericRange
print(1 to 5) # 1 2 3 4 5
# same as
print((1, 1) to 5) # 1 2 3 4 5
Eg: UnicodeRange
print(\a to \d) # a b c d
# same as
print((\a, 1) to \d) # a b c d
if the last element is the irrational value Infinity, the Range tends to positive infinity or if its Infinites it tends to negative infinity.
The
to
operator is also used for performing type convertions.'10' to _ :: Int # 10
Most collections in Cliver are immutable and some even have mutable counterparts prefixed with an exclamation mark.
Arrays are the most basic collection type in Cliver.
The super type is AbstractArray. Array indexing starts at 1 rather than at 0
val items :: Array(Type) = [A, B, C, D]
Items of an Array are accessed used the square bracket notation.
items[1] # A
# mutable version
val items :: Array!(Type) = [A, B, C, D]!
# add a value to the end of the array
items.add(value)
# add a value at a specific index
items.add(value; index: i)
# update an existing index
items[i] = value
# remove the first occurance of a value
items.drop(value)
# remove a value at an index
items.drop(index: i)
The in
operator can check for the presence of a value in an array.
Arrays support destructuring with the following syntax.
val [itemA, itemB] = items
fun(items.[itemA, itemB])
# ...
end
Array comprehension is done using for expressions
[...for item in items: if isValid(item): item]
Tuples are immutable, fixed sized collections. They can contain multiple types and can have named arguments. Tuples are not iterable.
val :: Tuple(TypeA, TypeB; TypeC, TypeD)
items = (itemA, itemB; itemC, itemD: ValueD)
Values in a tuple are accessed using indexing just like Array. Also the indexing starts at 1.
items[1] # ValueA
items[\itemD] # ValueD
Values can also be accessed using destructuring
val (itemA, itemB; itemC) = items
fun(items.(itemA, itemB; itemC))
# ...
end
Maps contain key-value pairs. By default they are immutable.
The super type is AbstractMap.
val pairs :: Map(KeyType, ValueType) = {
keyA: valueA,
keyB: valueB
}
val pairs = {:} # empty Map
val pairs = {_:_} # empty Map
Mutable Maps can be formed using !
suffix.
val pairs :: Map!(KeyType, ValueType) = {
keyA: valueA,
keyB: valueB
}!
# add a new entry
pairs.add(key: value)
# update an existing entry
pairs[key] = value
# remove an entry
pairs.drop(key)
Values within a map can be accessed using square bracket notation.
pairs[keyA] # valueA
Any map that has an empty pair or that which contains atleast a single pair can have implicit keys; i.e the name of the identifier is the key of type IdString and the value being the value of the identifier
val pairs = {_:_, x, y, z}
The behaviour of the in
operator varies with the lhs value when used on a Map.
print(keyA in pairs) # True
print((keyA: valueA) in pairs) # True
print((_: valueA) in pairs) # True
print((keyA: _) in pairs) # True
Maps support destructuring with the following syntax.
val {keyA, keyB as keyC} = pairs
fun(pairs.{keyA, keyB})
# ...
end
Map comprehension can be done using for expressions
{...for (key: value) in (pairs.keys, pairs.values): if isValid(key): (key: value)}
The collection type Set is a mathematical construct that can only contain unique values.
val items = {} # empty Set
val items = {1, 2, 2, 3, 4, 1, 5} # {1, 2, 3, 4, 5}
Sets support union (|)
, intersection (&)
and other mathematical math operations.
They are immutable by default. The mutable version suffixed with !
.
val items = {1, 2, 3, 4}!
items.add(5) # true - added
items.add(3) # false - not added
Like Arrays, Sets also support comprehension notation.
{...for item in items: if isValid(item): item}
It is another mathematical construct Cliver supports. They are similar to Arrays but contains data in rows and columns.
val mat = [
a, b, c;
d, e, f
]
mat.shape() # 2//3 - two rows and 3 columns
Macros are the backbone of Cliver's metaprogramming system.
A macro is a construct which can access and modify the AST structure of a supplied statement or expression.
# Example
fun runTime<meta>()
@@where do
import elapsed from Std\DateTime
val start = elapsed()
${meta.raw}
val stop = elapsed()
print(stop - start + "ms")
end
end
@runTime
for i in 1 to 100000
print(i)
end
# prints: ---ms