Skip to content

Latest commit

 

History

History
836 lines (568 loc) · 17 KB

QUICKSTART.md

File metadata and controls

836 lines (568 loc) · 17 KB

Quickstart

A quick tour of the wax language to get started.

Variables & types

There are only 7 types in wax.

  • int: integer
  • float: floating-point number
  • string: string
  • vec: fixed size array
  • arr: dynamically resizable array
  • map: hashtables
  • struct : user defined data structures

See Appendix for how wax types map to types in the target languages.

Variable Declaration

(let x int)

Shorthand for initializing to a value.

(let x int 42)

Declaring a compound type, array of floats:

(let x (arr float))

An array of 3D vectors:

(let x (arr (vec 3 float)))

Variables default to the zero value of their type when an initial value is not specified. int and float default to 0. Other types default to null. Null is not a type itself in wax, but nullable objects can be nullified with expression (null x). To check if a variable is NOT null, use (?? x) (equivalent to x!=null in other languages).

You can also use local in place of let, to declare variables that get automatically freed when it goes out of scope. See next section for details.

See Appendix for reserved identifier names.

Variable Assignment

(set x 42)

Math

Math in wax uses prefix notation like the rest of the language. e.g.:

(+ 1 1)

When nested, (1 + 2) *3 is:

(* (+ 1 2) 3)

+ * && || can take multiple parameters, which is a shorthand that gets expanded by the compiler.

e.g., 1 + 2 + 3 + 4 is:

(+ 1 2 3 4)

a && b && c is:

(&& a b c)

which the compiler will read as:

(&& (&& a b) c)

Ternary operator

? is the ternary operator in wax. e.g., y = (x==0) ? 1 : 2 is:

(set y (? (= x 0) 1 2))

List of operators

These operators work just like their namesakes in other languages.

<< >> = && || >= <= <>
+ - * / ^ % & | ! ~ < >

Note: <> is !=. = is ==. ^ is xor.

Note: Wax ensures that && and || are shortcircuited on all targets.

Arrays and Vectors

Initializing

To allocate a vec or an arr, use (alloc <type>)

(let x (vec 3 float) (alloc (vec 3 float)))

To free it, use

(free x)

Important: Freeing the container does not free the individual elements in it if the elements of the array is not a primitive type (int/float). Simple rule: if you alloc'ed something, you need to free it yourself. The container is more like a management service that helps you arrange data; it does not take ownership.

To allocate something that is automatically freed when it goes out of scope, use the local keyword to declare it.

(local x (vec 3 float) (alloc (vec 3 float)))

The memory will be freed at the end of the block the variable belongs to, or immediately before any return statement. local variables cannot be returned or accessed out of its scope.

You can also use a literal to initialize the vec or arr, by listing the elements in alloc expression.

(let x (arr float) (alloc (arr float) 1.0 2.0 3.0))

Think of it as

float[] x = new float[] {1.0, 2.0, 3.0};

Accessing

To get the ith element of arr x:

(get x i)

To set the ith element of arr x to v;

(set x i v)

get supports a "chaining" shorthand when you're accessing nested containers. For example, if x is a (arr (arr int)) (2D array),

(get x i j)

is equivalent to

(get (get x i) j)

To set the same element to v, you'll need

(set (get x i) j v)

If the array is 3D, then get will be:

(get x i j k)

or (if you enjoy typing):

(get (get (get x i) j) k)

and set will be:

(set (get x i j) k v)

Operations

To find out the length of an array x, use # operator:

(# x)

vec's length is already burnt into its type, so # is not needed.

To insert v into a an array x at index i, use

(insert x i v)

So say to push to the end of the array, you might use

(insert x (# x) v)

To remove n values starting from index i from array x, use

(remove x i n)

To produce a new array that contains a range of values from an array x, starting from index i and with length n, use

(set y (arr int) (slice x i n))

Note that if the result of slice operation is neither assigned to anything nor returned, it would be a memory leak since slice allocates a new array.

These four are the only operations with syntax level support (#, insert remove and slice are keywords). Other methods can be implemented as function calls derived from these fundamental operations.

Maps

(let m (map str int) (alloc (map str int)))

(set m "xyz" 123)

(insert m "abc" 456) ; exactly same as 'set'

(print (get m "xyz"))

(remove m "xyz")

(print (get m "xyz")) 
;^ if a value is not there, the "zero" value of the element type is returned
; for numbers, 0; for compound types, null.

Map key type can be int float or str. Map value type can be anything.

Structs

(struct point
    (let x float)
    (let y float)
)

Structs are declared with struct keyword. In it, fields are listed with let expressions, though initial values cannot be specified (they'll be set to zero values of respective types when the struct gets allocated).

Another example: structs used for implementing linked lists might look something like this:

(struct list
	(let len int)
	(let head (struct node))
	(let tail (struct node))
)
(struct node
	(let prev (struct node)) 
	(let next (struct node))
	(let data int)
)

Structs fields of struct type are always references. Think of them as pointers in C:

struct node {
    struct node * prev;
    struct node * next;
    int data;
};

However the notion of "pointers" is hidden in wax; From user's perspective, all non-primitives (arr,vec,map,str,struct) are manipulated as references.

Instantiating

(let p (struct point) (alloc (struct point)))

To free:

(free p)

The local keyword works for structs the same way it does for arrays and vectors.

Accessing

The get and set keywords are overloaded for structs too.

To get field x of a (struct point) instance p:

(get p x)

To set field x of struct point to 42:

(set p x 42.0)

If the struct is an element of an array, say the jth point of the ith polyline in the arr of polylines:

(get polylines i j x)
(set (get polylines i j) x 42.0)

Strings

In wax, string is an object similar to an array.

To initialize a new string:

(let s str (alloc str "hello"))

To free the string:

(free s)

To append to a string, use << operator.

(<< s " world!")

Now s is modified in place, to become "hello world!".

Note that a string does not always need to be allocated to be used, but it needs to be allocated if it needs to:

  • be modified
  • persist outside of its block

E.g. if all you want is to just print a string:

(let s str "hello world")
(print s)

is OK. (And so is (print "hello world"))

The right-hand-side of string << operator does not have to be allocated, while the left-hand-side must be.

If the function returns a string, it needs to be allocated.

To add a character to a string, << can also be used:

(<< s 'a')

Note that 'a' is just an integer (ASCII of a is 97). It's the same as:

(<< s 97)

To add the string "97" instead, cast expression can be used (see more about casting in next section):

(<< s (cast 97 str))

Strings can be compared with = and <> equality tests. They actually check if the strings contain the same content, NOT just checking if they're the exact same object.

(let s str (alloc str "hello"))
(<< s "!!")
(print (= s "hello!!"))
;; prints 1

To find out the length of a string:

(# s)

To get a character from a string:

(let s str "hello")
(let c int (get s 0)) ;; 'h'==104

To copy part of a string into a new string use (slice s i n) the same way as slice for arr:

(let s str "hello")
(slice s 1 3)  ;; "ell"

Casting

Ints and Floats can be cast to each other implicitly. You can also use (cast var to_type) to do so explicitly:

(let x float 3.14)
(let y int (cast x int))

Numbers can be cast to and from string with the same cast keyword.

(let x float (cast "3.14" float))
(let y str (cast x str))

In other words, cast is also responsible for doing parseInt parseFloat toString present in other languages.

Types other than int float str cannot be casted. You can define and call custom functions to do the job.

Control Flow

If statement

(if (= x 42) (then
    (print "the answer is 42")
))

with else:

(if (= x 42) (then
    (print "the answer is 42")
)(else
    (print "what?")
))

else if:

(if (= x 42) (then
    (print "the answer is 42")
)(else (if (< x 42) (then
    (print "too small!")
)(else (if (> x 42) (then
    (print "too large!")
)(else    
    (print "impossible!")
))))))

with && and || and !:

(if (|| (! (&& (= x 42) (= y 666))) (< z 0)) (then
    (print "complicated logic evaluates to true")
))

For loops

(for i 0 (< i 100) 1 (do
    (print "i is:")
    (print i)
))

The first argument is the looper variable name, the second is starting value, the third is stopping condition, the fourth is the increment, the fifth is a (do ...) expression containing the body of the loop. It is equivalent to:

for (int i = 0; i < 100; i+= 1){

}

Looping backwards (99, 98, 97, ..., 2, 1, 0), iterating over the same set of numbers as above:

(for i 99 (>= i 0) -1 (do

))

Looping with a step of 3 (0, 3, 6, 9, ...):

(for i 0 (< i 100) 3 (do

))

Iterate over a Map

(let m (map str int) (alloc (map str int)))

; populate ...

(for k v m (do
    (print "key is")
    (print k)
    (print "val is")
    (print v)
))

While loops

(while (<> x 0) (do

))

which is equivalent to

while (x != 0){

}

Break

Break works for both

(while (<> x 0) (do
    (if (= y 0) (then
        (break)
    ))
))

Functions

A minimal function:

(func foo 
    (return)
)

A simple function that adds to integers, returning an int:

(func add_ints (param x int) (param y int) (result int)
    (return (+ x y))
)

A function that adds 1 to each element in an array of floats, in-place:

(func add_one (param a (arr float))
    (for i 0 (< i (# a)) 1 (do
        (set a i (+ (get a i) 1.0))
    ))
)

Fibonacci:

(func fib (param i int) (result int)
	(if (<= i 1) (then
		(return i)
	))
	(return (+
		(call fib (- i 1))
		(call fib (- i 2))
	))
)

Calling

To call a function, use call keyword, followed by function name, and the list of arguments.

(call foo 1 2)

The main function

The main function is optional. If you're making a library, then you probably don't want to include a main function. If you do, the main function will map to the main function of the target language, (if the target language has the notion of main function, that is).

The main function has 2 signatures, similar to C. One that takes no argument, and returns an int that is the exit code. The other one takes one argument, which is an array of strings containing the commandline arguments, and returns the exit code.

(func main (result int)
    (return 0)
)

(func main (param args (arr str)) (result int)
    (for i 0 (< i (# args)) 1 (do
        (print (get args i))
    ))
    (return 0)
)

Function Signature

Functions need to be defined before they're called. Therefore for mutually recursive functions, (or for organizing code), it is useful to declare a signature first.

(func foo (param x int) (param y int) (result int))

It looks just like a function without body. Therefore, to instead define a void function that actually does nothing at all, a single return needs to be the body to make it not turn into a function signature.

(func do_nothing
    (return)
)

File Layout

Functions and structures can only be defined on the top level. So your source code file might looks something like this:

;; constants
(let XYZ int 1)
(let ZYX float 2.0)

;; data structures
(struct Foo
    (let x int 3)
)

;; implementation
(func foo
    (return)
)
(func bar
    (return)
)

;; optional main function
(func main (result int)
    (call foo)
    (call bar)
)

Macros

wax supports C-like preprocessor directives. In wax, macros look like other expressions, but the keywords are prefixed with @.

(@define MY_CONSTANT 5)

(@if MY_CONSTANT 5
    (print "yes, it's")
    (print @MY_CONSTANT)
)

after it goes through the preprocessor, the above becomes:

(print "yes, it's")
(print 5)

Note the @ in @MY_CONSTANT when it is used outside of a macro expression.

If a macro is not defined, and is tested in an @if macro, the value defaults to 0:

(@if IM_NOT_DEFINED 1
    (print "never gets printed")
)
(@if IM_NOT_DEFINED 0
    (print "always gets printed")
)

The second argument to @define can be omitted, which makes it default to 1:

(@define IMPLICITLY_ONE)

(print "printing '1' now:")
(print @IMPLICITLY_ONE)

Including Files

To include another source code, use:

(@include "path/to/file.wax")

The content of the included file gets dumped into exactly where this @include line is. To make sure a file doesn't get included multiple times, use:

(@pragma once)

To include a standard library, include its name without quotes:

(@include math)

Target-specific behaviors

These macros are pre-defined to be 1 when the wax compiler is asked to compile to a specific language, so the user can specify different behavior for different languages:

TARGET_C
TARGET_JAVA
TARGET_TS
...

For example:

(@if TARGET_C 1
    (print "hello from C")
)
(@if TARGET_JAVA 1
    (print "hello from Java")
)

External Functions

To call functions written in the target language, you can describe their signature with extern keyword so that the compiler doesn't yell at you for referring to undefined things.

For example:

(extern sin (param x float) (result float))

Then in your code, you can write:

(call sin 3.14)

(This is exactly how (@include math) is implemented: the functions get mapped to the math library of the target language with externs)

You can also have extern variables in addition to functions:

(extern PI float)

Inline "Assembly"

You can embed fragments of the target language into wax, similar to embedding assembly in C, using the (asm "...") expression. For example:

(@if TARGET_C 1
    (asm "printf(\"hello from C\n\");")
)
(@if TARGET_JAVA 1
    (asm "System.out.println(\"hello from Java\n\");")
)
(@if TARGET_TS 1
    (asm "console.log(\"hello from TypeScript\n\");")
)

Appendix

Datatype mapping

wax tries to give the generated code an "idiomatic" look & feel by mapping wax types directly to common types in target language whenever possible, in favor of rolling out custom ones.

int float str vec arr map
C int float char* T* w_arr_t* (custom impl.) w_map_t* (custom impl.)
Java int float String T[] ArrayList<T> HashMap<K,V>
TypeScript number number string Array Array Record<K,V>
Python int float str list list dict
C# int float string T[] List<T> Dictionary<K,V>
C++ int float std::string std::array std::vector std::map
Swift Int Float String? w_Arr<T>? (wraps [T]) w_Arr<T>? (wraps [T]) w_Map<K,V>? (wraps Dictionary)
Lua number number string table table table

Reserved identifier names

  • Identifiers beginning with w_ are reserved. (They're for wax standard library functions)
  • Identifiers ending with _ are reserved. (They're for resolving clashes with target language reserved words)
  • Identifiers containing double underscore __ are reserved. (They're for temporary variables generated by the compiler)

  • Identifiers colliding with target language reserved words are automatically resolved by waxc by appending _ to their ends; Nevertheless, it's better to avoid them altogether.
  • Identifiers must start with a letter [A-z], and can contain any symbol that is not {}[]() or whitespaces. Non-alphanumeric symbols are automatically fixed by the compiler, e.g. he||@-wor|d is a valid identifier in wax, and will be translated to heU7c__U7c__U40__U2d__worU7c__d in target languages.