Language Features

Data Types

Wrapped String References

done

Wrapped string literals sr wraps multiple source strings in an uniform structure. There is no reason to delete a sr unless you want to de-allocate, referred data of the string.

You can add two sr using + operator. However, resulting value will be of type str, as memory allocation is required. It is recommended to use a string buffer for concatenating a string.

a: sr = "A"
b: sr = "B"
c = a + b
# Type of c would be str

Type inference during creating a variable will create a value of sr type whenever "string literal" is used on RHS.

my_sr = "Hello World"
another_sr: sr = "Hello World"

String literals

done

Internally string literals such as "hello world" are neither str or sr. They utilize a hidden data type :s: for string literal.

String literals are efficient and does not require any additional memory allocation.

If you + two string literals that will be done at compile time.

String literals are automatically converted to sr or str.

(Semi)Managed Strings

done
  • String allocations and de-allocations are abstracted away, so strings are immutable and automatically deleted.
  • Assignment copy values and automatically delete previous value.
  • Strings are also copied every time you create a new variable or pass it to a function or assign it to a container (object, array, etc).
  • It is not meant to be fast. It is meant to be easily usable.
  • At C code level yk__sds data structure will be used. (yk__sds is a char* with special header placed just before).
  • πŸ’€οΈ Managed strings are not deleted when used in arrays, maps, or objects. (This is intentional and not a bug.)
    • You need to manage deletion of str object in this case.
    • Use libs.strings.array.del_str_array() to delete a string array.
  • Supports + operator to join two str values.
Yaksha Programming Language Mascot
  • Data Type - str
  • Internally this is a binary string.
  • sds library (part of runtime lib) takes care of the heavy lifting.
a: str = "hello world"
# support -> datatype βœ… | literal βœ…
println(a)

Standard integers

done
  • Default integer is a 32bit signed integer.
  • This is compiled to int32_t on all platforms.
Data Type - int or i32
a: int = 4       # datatype βœ… | literal βœ…
print("Four =")
println(a)

Integer types

in-progress
  • Signed types - i8, i16, i32, i64
  • Unsigned types - u8, u16, u32, u64
# Default integer type is i32
a: int = 1       # datatype βœ… | literal βœ…
b: i32 = 2       # datatype βœ… | literal βœ…
c: i8 = 3i8      # datatype βœ… | literal βœ…
d: i16 = 4i16    # datatype βœ… | literal βœ…
e: i32 = 5i32    # datatype βœ… | literal βœ…
f: i64 = 6i64    # datatype βœ… | literal βœ…

# Unsigned
g: u8 = 3u8      # datatype βœ… | literal βœ…
h: u16 = 4u16    # datatype βœ… | literal βœ…
i: u32 = 4u32    # datatype βœ… | literal βœ…
j: u64 = 5u64    # datatype βœ… | literal βœ…

Float types

done
  • f32 or float - single precision floats.
  • f64 - double precision floats.
Data Type - f32, float and f64
a: f32 = 1.0f    # datatype βœ… | literal βœ…
b: f64 = 2.0     # datatype βœ… | literal βœ…
c: float = 3.0f  # datatype βœ… | literal βœ…

Syntax features

Let statement

done
  • Create a new variable.
  • If you want to assign to a variable, it needs to be created.
  • If no value is provided default value for data type is used.
Default value for str is an empty string.
def main() -> int:
    a: int = 10
    print(a)
    return 0

Basic Functions

done
  • Return type must be mentioned always.
  • If return type is None it means no data is returned. (void in C world.)
def main() -> int:
    print("Hello World\n")
    return 0

Exposing C functions

done
  • πŸ’€ You can only call @nativexxx functions from normal functions.
  • πŸ’€ @nativexxx functions cannot call each other or normal functions.

@native - native functions

@native("getarg")
def get_arg(n: int) -> str:
    pass

@native
def get_global_arg(n: int) -> str:
    ccode "yk__sdsdup(global_args[yy__n])"
Yaksha Programming Language Mascot
  • If ccode is there instead of an argument, then it is used as the message body.
Click to see output C code
yk__sds yy__get_arg(int32_t nn__n) { return getarg(nn__n); }
yk__sds yy__get_global_arg(int32_t nn__n) 
{
    yk__sdsdup(global_args[yy__n]);
}

@nativemacro - macros with arguments

@nativemacro
def min_int(a: int, b:int) -> int:
    ccode "((nn__a < nn__b) ? nn__a : nn__b)"

@nativemacro("((nn__a > nn__b) ? nn__a : nn__b)")
def max_int(a: int, b:int) -> int:
    pass
Click to see output C code
#define yy__min_int(nn__a, nn__b) ((nn__a < nn__b) ? nn__a : nn__b)
#define yy__max_int(nn__a, nn__b) ((nn__a > nn__b) ? nn__a : nn__b)

@nativedefine - simple #define

@nativedefine("banana")
def banana(a: int, b: int, c:int) -> int:
    pass
Click to see output C code
#define yy__banana banana

@varargs - variable argument functions

  • πŸ’€ Can only be used with @nativedefine.
@nativedefine("yk__newsdsarray")
@varargs
def new(count: int, s: str) -> Array[str]:
    pass
Click to see output C code
#define yy__new yk__newsdsarray
// Assume that yk__newsdsarray is something like below
// yk__sds *yk__newsdsarray(size_t count, ...)

Template functions

not-started
  • Return type can also be a template-arg if that template-arg is used in parameters.
  • String passed inside @template( should be single upper case characters separated by commas.
This means it is limited to 26 template arguments max.
@native("yk__arrput")
@template("T")
def arrput(a: Array[T], v: T) -> None:
    pass

@native("yk__hmput")
@template("K,V")
def hmput(a: HashMap[K,V], key: K, value: V) -> None:
    pass

@native("yk__hmget")
@template("K,V")
def hmget(a: HashMap[K,V], key: K) -> V:
    pass

GPU/OpenCL device functions

not-started
  • Easy access to GPU through OpenCL.
@device
def calculate(n: int) -> int:
   return 1 + 1 

Defer statement

done
  • Defer something to happen at the end of the scope.
  • Before any return from a function.
  • Before break, continue or end of while loop body.
    • This behaviour is different from what you see in go-lang.
  • Before end of if body or end of else body.
Yaksha Programming Language Mascot
  • defer works as a stack.
  • That means deferred expressions are executed in last deferred first executed order.
  • Please note that this is not compatible with how go programming language defer works.
def onexit() -> int:
    println("All done")
    return 0

def main() -> int:
    defer onexit()
    println("Hello World")
    return 0

Output:

Hello World
All done

Del statement

in-progress
  • Delete values.
  • Delete arrays, and other builtin data structures without deleting content.
def main() -> int:
    a: Array[int]
    defer del a
    arrput(a, 1)
    arrput(a, 2)
    arrput(a, 3)
    println(a[0])
    return 0
Compiles to free or other runtime functions.

Class statement

in-progress
  • Create a custom data structure.
  • πŸ’€ Templated structures are not supported yet.
  • πŸ’€ Inheritance is not supported yet.
class Student:
    student_id: int
    name: str
    address: str

class Teacher:
    teacher_id: int
    name: str
    address: str

Creating and freeing objects

def main() -> int:
    # Non primitive types are initialized to None 
    john: Student
    # Creating an instance, will be allocated in heap
    # Results in a malloc
    john = Student()
    defer del john
    # Set fields
    john.student_id = 10
    # str objects in structures are not freed automatically
    john.name = "John Smith"
    john.address = "1 Road, Negombo"
    defer del john.name
    defer del john.address
    return 0

It might be better to create a custom function to delete custom objects.

def del_student(st: Student) -> None:
    del st.name
    del st.address
    del st 

def main() -> int:
    john: Student = Student()
    defer del_student(john)
    
    john.student_id = 10
    john.name = "John Smith"
    john.address = "1 Road, Negombo"
    return 0

Exposing native structures

@nativedefine("something")
class Something:
    something_id: int

# Use @onstack for purely stack allocated structs
@nativedefine("Color")
@onstack
class Color:
    r: int
    g: int
    b: int
Click to see output C code
#define yy__Something something
#define yy__Color Color

Import statement

in-progress
  • Import a file.
import io

def main() -> int:
    file: io.File = io.open("Haha")
    defer io.close(file)
    if file == None:
        println("-- failed to read file --")
        return 1
    data: str = io.readall(file)
    println("-- read file --")
    println(data) 
    return 0
Name mangling takes place for this.

While loop

done

Loop as long as the expression evaluates to True.

def main() -> int:
    a = 10
    while a > 0:
        println(a)
        a -= 1
    return 0

Foreach loop

done

For each allow you to iterate each element of a given array.

def main() -> int:
    e1: Array[int] = array("int", 1, 2, 3)
    e2 = array("int", 4, 5, 6, 7)
    for i: int in e1:
        for j in e2:
            print(i)
            print(" - ")
            println(j)
    del e1
    del e2
    return 0

Endless for loop

done

Endless for loop will iterate until break is executed.

def main() -> int:
    c: int = 0
    for:
        if c == 2:
            break
        println(1)
        c += 1
    return 0

C-For loop

done

Standard for loop from C family of languages.

def add(a: int, b: int) -> int: a + b

def main() -> int:
    for (x = 0; x < 10; x = x + 1):
        println(x)

    a: str = ""
    for (x = 0; x < 4; x += 1):
        a += "hello "
    println(a)

    b: str = ""
    c: str = "x"
    for (b += c; b != "xxx"; b += c): pass
    println(b)

    for (x = 0; x < 10i8; x = add(x, 2i8)):
        println(x)

    return 0
Yaksha Programming Language Mascot
  • Note that Yaksha allows omitting return, assuming last expression matches the return data type.
  • This is why add function does not have a return statement.

Builtin Functions

in-progress
  • βœ… print(primitive) -> None - Print without a new line
  • βœ… println(primitive) -> None - Print + new line
  • βœ… len(Array[T]) -> int - Get length of arrays,maps
  • βœ… arrput(Array[T], T) -> None - Put item to an array
  • βœ… arrpop(Array[T]) -> T - Remove last item from an array and return it
  • βœ… arrnew("T", int) -> Array[T] - Create a new array of given size. (Uninitialized elements)
  • βœ… arrsetcap(Array[T], int) -> None - Set array capacity / grow memory. Does not affect length.
  • βœ… arrsetlen(Array[T], int) -> None - Set array length. Each element will be an uninitialized element.
  • βœ… array("T", T...) -> Array[T] - Create a new array from given elements
  • βœ… getref(T) -> Ptr[T] - Get a pointer to given object
  • βœ… unref(Ptr[T]) -> T - Dereference a pointer
  • βœ… charat(str, int) -> int - Get a character at a specific location in string
  • βœ… shnew(Array[SMEntry[T]]) -> None - Initialize Array[SMEntry[T]] object
  • βœ… shput(Array[SMEntry[T]], str, T) -> None - Put item to an Array[SMEntry[T]]
  • βœ… shget(Array[SMEntry[T]], str) -> T - Get item from an Array[SMEntry[T]]
  • βœ… shgeti(Array[SMEntry[T]], str) -> int - Get item index from an Array[SMEntry[T]] (-1 if not found)
  • βœ… hmnew(Array[MEntry[K,T]]) -> None - Initialize Array[MEntry[K,T]] object
  • βœ… hmput(Array[MEntry[K,T]], K, T) -> None - Put item to an Array[MEntry[K,T]]
  • βœ… hmget(Array[MEntry[K,T]], K) -> T - Get item from an Array[MEntry[K,T]]
  • βœ… hmgeti(Array[MEntry[K,T]], K) -> int - Get item index from an Array[MEntry[K,T]] (-1 if not found)
  • βœ… cast("T", X) -> T - Data type casting builtin
  • βœ… qsort(Array[T], COMP) -> bool - Sort an array, returns True if successful

# Comparision is a function of type:
# Function[In[Const[AnyPtrToConst],Const[AnyPtrToConst]],Out[int]])
#
# Example:
def cmp_int(a: Const[AnyPtrToConst], b: Const[AnyPtrToConst]) -> int:
    # Compare two given integers
    val_a: int = unref(cast("Ptr[int]", a))
    val_b: int = unref(cast("Ptr[int]", b))
    return val_b - val_a

def print_array(x: Array[int]) -> None:
    print("len=")
    println(len(x))
    pos = 0
    length: int = len(x)
    while pos < length:
        print(x[pos])
        print(" ")
        pos = pos + 1
    println("")

def main() -> int:
    x1 = array("int", 1, 2, 3, 3, 2, 1, 5, 4)
    println("before x1:")
    print_array(x1)
    qsort(x1, cmp_int)
    println("after x1:")
    print_array(x1)
  • βœ… iif(bool, T, T) -> T - Ternary functionality
  • βœ… foreach(Array[T],Function[In[T,V],Out[bool]],V) -> bool - For each element in array execute given function
  • βœ… countif(Array[T],Function[In[T,V],Out[bool]],V) -> int - For each element in array count if function returns true
  • βœ… filter(Array[T],Function[In[T,V],Out[bool]],V) -> Array[T] - Create a new array with filtered elements based on return value of given function
  • βœ… map(Array[T],Function[In[T,V],Out[K]],V) -> Array[K] - Create a new array with result of given function
  • βœ… binarydata("data") -> Const[Ptr[Const[u8]]] - Create constant binary data (must pass in a string literal). Returns Const[Ptr[Const[u8]]] that does not need to be deleted.
  • βœ… make("T") -> Ptr[T] - Allocate a single object.
  • βœ… inlinec("T", "code") -> T - Inline C code resulting in T data type. Example - inlinec("int", "sizeof(char)")
Yaksha Programming Language Mascot
  • Builtin functions may call different implementations based on input.

Non primitive data types

This section describes other essential types of builtin structures.

Dynamic Arrays

in-progress
def main() -> int:
    a: Array[i32]
    # Prior to calling arrput pointer is set to None
    defer del a
    arrput(a, 1)
    arrput(a, 2)
    # Array access works with `[]`
    print(a[0])
    return 0
Yaksha Programming Language Mascot
  • Must ensure array elements are freed. int need not be deleted as they are primitive types.

String Hash Map

in-progress
  • HashMap with str keys and given data type values.
  • Values need to be deleted when no longer needed.
def main() -> int:
    m: Array[SMEntry[int]]
    # shnew must be called before using the array as a String Hash Map
    shnew(m)
    defer del m
    shput(m, "banana", 10)
    r: int = shget(m, "banana")
    return r
Yaksha Programming Language Mascot
  • Array[SMEntry[?]] keys are deleted automatically when del is invoked.
  • len will give the total number of elements of the String Hash Map.

Hash Map

in-progress

Simple single key single value hashmaps.

Yaksha Programming Language Mascot
  • Data type Array[MEntry[K,T]].
  • key and value both need to be deleted.

Macros & YakshaLisp

in-progress

YakshaLisp macros are one of the most important features of Yaksha. You can think of it as an interpreted language that lives in Yaksha compiler.

Because that is what it is!

It has it’s own built in functions, can use import, read/write files and even use metamacro directive to create quoted input functions(similar to defun, except input args are not evaluated and returns quoted output that is immediately evaluated). Has multiple data types (list, map, callable, string, expression). Support’s q-expressions {1 2 3} (inspired by build-your-own-lisp’s lisp dialect), and special forms. A simple mark and sweep garbage collector is used. Provides a mediocre REPL and ability to execute standalone lisp code using a command. Not only that, it also support reflection using builtin callables such as this and parent (which returns a mutable current or parent scope as a map).

Yaksha Programming Language Mascot
  • Yo dog!, I heard you like macros, so I added meta-macro support in your macro processor.
  • So you can meta-program while you meta-program.

Philosophy?

YakshaLisp provide the ability to write token-list to token-list conversion macros. One can use my_macro!{t t t} style expansion to achieve this. So why YakshaLisp? because it needs to process a list of tokens. Additionally, Yaksha and YakshaLisp are polar opposites of each other, therefore I think they can compliment each other nicely.

Think Yin-Yang!

What are the differences?

Language differences
YakshaYakshaLispC99
manual memory managementgarbage collectedmanual memory management
compiled (to C99)interpretedcompiled by most compilers to target platform.
can be multi-threadedonly single threadedcan be multi-threaded, etc
indent based syntaxparenthesis based syntaxC family scope syntax
c-like function pointersclosures, function body not evaluated unless called. First class functions/lambdafunction pointers
multiple integer types and float / double / boolonly 64bit signed integer is supported.multiple integer types and float / double / bool, enums & unions
hygienic macros using $name syntax (automated gensym), non-hygienic macros are also supported (do not use the $ prefix for identifiers), can also generate C style macros/code with nativexxx annotations and ccode keyword in some cases.metamacro / eval / parse / this / parent / q-expressions #define / #include
has statements and expressionsexpressions onlyhas statements and expressions
no support for exceptionsexceptions are a string that you can throw and catchsetjmp
no support for reflectionsupports reflection - repr / this / parentno support for reflection

Builtin functions, values & prelude

done

All below listed functions are available at the moment.

  • nil - this has value of {} (an empty list)
  • true - value of 1
  • false - value of 0
  • newline - value of \r\n or \n as a string.
  • +, -, *, /, modulo - basic functions
  • ==, !=, <, >, <=, >= - comparison functions
  • and, or, not - boolean operator functions
  • bitwise_and, bitwise_or, bitwise_not, bitwise_xor, bitwise_left_shift, bitwise_right_shift - bitwise operator functions to be applied to number values (underlying structure of a number is a 64bit signed integer)
  • filter, map, reduce - basic functional-programming functions
  • map_get, map_has, map_set, map_remove, map_values, map_keys - functions to manipulate map-objects.
  • access_module - access an element from another root environment (uses same imports at the top of the file as in Yaksha)
  • this, parent - returns current scope or parent scope as a map-object (can be modified)
  • magic_dot - wrapper around map_get and access_module (supports both type of objects) to read a value. Special syntax sugar symbol1::symbol2 expands to (magic_dot symbol1 symbol2)
  • io_read_file, io_write_file, print, println, input - I/O functions
  • def/define - define an undefined value in current environment with a given symbol.
  • setq - update an already defined value
  • = - combination of setq and def. try to def and if it fails use setq.
  • quote - create a list with non-evaluated expressions parsed as arguments. This has a somewhat-similar (but not 100% similar) effect to {}
  • list - create a list with all arguments to list function evaluated.
  • eval, parse, repr - evaluate a list or single expression, parse to q-expression, convert values to strings.
  • raise_error, try, try_catch - functions to use exceptions during evaluation.
  • head, tail, cons, push, pop, insert, remove - list manipulation functions
  • for, while - looping functions
  • scope, do - do these expressions in a child scope scope or in current scope (do)
  • is_list, is_list, is_int, is_string, is_truthy, is_callable, is_nil, is_metamacro, is_module - check types
  • defun, lambda - create named and anonymous functions
  • cond, if - conditions
  • random - random number between given range
  • time - unix time as number

Macro environments

Inside Yaksha a root (known as builtins_root) environment is created. It will house builtin functions and results of executing prelude code.

A single .yaka file in imports (or current compiled file) will have their own root environment that is a child of builtins_root.

During garbage collection, mark phase will start marking from builtins_root and file level roots.

Environment is an unordered_map with string keys. Environments and maps use same data type internally.

How does it look like

# β•”β•β•—β”Œβ”€β”β”Œβ”¬β”β”Œβ”€β”β”¬β”¬  β”Œβ”€β”  β•”β•¦β•—β”¬β”Œβ”¬β”β”Œβ”€β”
# β•‘  β”‚ β”‚β”‚β”‚β”‚β”œβ”€β”˜β”‚β”‚  β”œβ”€    β•‘ β”‚β”‚β”‚β”‚β”œβ”€
# β•šβ•β•β””β”€β”˜β”΄ β”΄β”΄  β”΄β”΄β”€β”˜β””β”€β”˜   β•© β”΄β”΄ β”΄β””β”€β”˜
# β•”β•β•—β”¬β”Œβ”€β”β”Œβ”€β”  β•”β•— ┬ β”¬β”Œβ”€β”β”Œβ”€β”
# β• β•£ β”‚β”Œβ”€β”˜β”Œβ”€β”˜  β• β•©β•—β”‚ β”‚β”Œβ”€β”˜β”Œβ”€β”˜
# β•š  β”΄β””β”€β”˜β””β”€β”˜  β•šβ•β•β””β”€β”˜β””β”€β”˜β””β”€β”˜
macros!{
    (defun to_fb (n) (+ (if (== n 1) "" " ") (cond
        ((== 0 (modulo n 15)) "FizzBuzz")
        ((== 0 (modulo n 3)) "Fizz")
        ((== 0 (modulo n 5)) "Buzz")
        (true (to_string n))
        )))
    (defun fizzbuzz () (list (yk_create_token YK_TOKEN_STRING (reduce + (map to_fb (range 1 101))))))
    (yk_register {dsl fizzbuzz fizzbuzz})
}

def main() -> int:
    println(fizzbuzz!{})
    return 0