A quick tour of the wax language to get started.
There are only 7 types in wax.
int
: integerfloat
: floating-point numberstring
: stringvec
: fixed size arrayarr
: dynamically resizable arraymap
: hashtablesstruct
: user defined data structures
See Appendix for how wax types map to types in the target languages.
(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.
(set x 42)
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)
?
is the ternary operator in wax. e.g., y = (x==0) ? 1 : 2
is:
(set y (? (= x 0) 1 2))
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.
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};
To get the i
th element of arr
x
:
(get x i)
To set the i
th 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)
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.
(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.
(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.
(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.
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 j
th point of the i
th polyline in the arr
of polylines
:
(get polylines i j x)
(set (get polylines i j) x 42.0)
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"
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 cast
ed. You can define and call custom functions to do the job.
(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 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
))
(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 (<> x 0) (do
))
which is equivalent to
while (x != 0){
}
Break works for both
(while (<> x 0) (do
(if (= y 0) (then
(break)
))
))
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))
))
)
To call a function, use call
keyword, followed by function name, and the list of arguments.
(call foo 1 2)
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)
)
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)
)
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)
)
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)
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)
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")
)
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 extern
s)
You can also have extern variables in addition to functions:
(extern PI float)
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\");")
)
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 |
- 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 toheU7c__U7c__U40__U2d__worU7c__d
in target languages.