Skip to content

jithujoshyjy/Cliver

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

99 Commits
 
 

Repository files navigation

Comments

Single line comments in Cliver start with # and multiline comments are enclosed within #= =# pairs.

Naming Conventions

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!''

Import/Export

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 and Constants

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

Type Declaration

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 and Concrete types

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

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

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

Functions are the backbone of Cliver. There are 3 main types of functions. Their base type is AbstractFunction.

UnitFunction

These are simple one-line function expressions.

# untyped
(...parameters) -> expression

# with type anotation
(paramA :: Type, paramB :: Type) :: Type -> expression
NamedFunctions

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,.
AnonFunction

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.

Operators as Functions

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
Do Expressions

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
Function Pipeline Operation

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)
Infix Function Call

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
External Callback Notation

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

Object Oriented Programming

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

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

Composition in Constructors

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

Static Constructors

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

Objects are similar to most other programming languages. They are created by Constructor functions.

CtorFunctionA()
CtorFunctionB()
Object Cascade Notation

The cascade notation is a syntatic form of the Builder design pattern.

objB()
    ..propA = value
    ..methodB()
    ..methodA()

Control Structures

Conditionals

There are two types of conditionals in Cliver; if conditional and match expression. The pass keyword skips the current conditional block.

If Conditional

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

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

Loops - the for loop

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:

  1. @@broken - the loop was terminated with a break clause
  2. @@completed - the looping was completed successfully without breaking and it ran atleast once
  3. @@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.

Error Handling Constructs

There are two main error handling constructs in Cliver and it is the do...catch and error pipeline operator.

Do-Catch construct

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:

  1. @@caught - there was an error and it was caught by a catch block,
  2. @@uncaught - the error was not caught or an uncaught error was thrown,
  3. @@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.

Error Pipeline Operator

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
Use Statements

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 and As Expressions

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)

Primitive Types

Cliver has a wide variety of standard types.

Maybe

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
Mustbe

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
Boolean

This type constructor only contains 2 values, True and False

type Boolean() =
    | True
    | False
Number

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.

Numeric Notations

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

Tagged Numbers

Numbers can be tagged by an identifier.

fun :: Uint -> Int
fact(n): n * fact(n - 1)

print(5!fact) # 120
Implicit Multiplication

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

Char

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

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 !

String Fragments

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.

Mutiline Strings

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
"""!
Tagged Strings

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 = "hello@word.com"
print(email.match(regex)) # Just(RegexMatch)

val rawText = r"newline (\n)"
print(rawText) # newline (\\n)

tagging can also be done with multiline strings

Range

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

Collections

Most collections in Cliver are immutable and some even have mutable counterparts prefixed with an exclamation mark.

Array

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]
Tuple

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
Map

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)}
Set

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}
Matrix

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

Metaprograming

Macros are the backbone of Cliver's metaprogramming system.

Macro

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

About

a new language definition

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published