Willet compiles to JavaScript and borrows many of its conventions and types.
Unlike JavaScript, Willet does not require commas or semicolons to define statements and separate expressions. Willet is not whitespace sensitive like Python. The Willet syntax is well defined so its clear to the parser where statements start and end.
console.log('This is Willet!')
const myFunction = #() => {
const myList = [1 2 3]
console.log('I\'m in a function')
myList
}
myFunction()
Blocks in Willet is zero to many statements within a brace enclosed block. The last value in the block is returned.
const v1 = {}
const v2 = { 1 }
const v3 = {
console.log('hello')
2
}
const v4 = {
const s = 7
s + v3
}
v1 == null
v2 == 1
v3 == 2
v4 == 9
Blocks in Willet can be used in the following places to Willet:
- On their own to scope a set of statements.
- As the body of a function
- As a parameter to Macros
Math and infix operations work the same way as in JavaScript. Parentheses to group expressions are written #()
1 + 1 / 2
// This is a parentheses wrapped expressions.
#(1 + 1) * 2
The equality operator ==
in Willet performs a strict equality check like the ===
operator in Javascript.
1 == 1 // true
1 == '1' // false
// one line comment
/*
mutiline
comment
*/
Willet uses let
and const
which have the same semantics as JavaScript.
let modifiableVar = 5
const readOnlyVar = 'Never changes'
These are all the same as JavaScript.
- Boolean:
true
andfalse
null
undefined
- Number
- BigInt
- Regular Expressions - Future feature
'Single quotes'
"And double quotes both work"
`Back ticks allow string interpolation ${1 + 1}
and can have multiple lines`
Symbol style strings like :red
allow easy creation of simple strings that reference fields.
:red == 'red'
// true
Lists are written using []
square brackets and create an Immutable.js List
[1 2 3]
Maps are similar to JavaScript Objects and create an Immutable.js Map. A #{
starts a map and a }
closes it.
#{
alpha: 'a value'
foo: 5
}
Sets are written using #[]
and create an Immutable.js Set.
#[1 2 3]
Before discussing control flow we need to discuss what is considered truthy and falsey.
The following values are considered falsey
false
null
undefined
All other values are truthy. Unlike JavaScript these values are truthy.
0
NaN
""
Empty strings
The if/elseif/else execute blocks if conditions are met
if (condition) {
doSomething()
}
elseif (otherCondition) {
doSomethingElse()
}
else {
otherThing()
}
Note: elseif
is a single word
The blocks within the commands all return the last statement of the block. The if/elseif/else command as a whole returns the result of the block that is executed.
The result of an if/else can be assigned to a variable or returned from a function. Note that parentheses wrapping the if or a block are required. This is a downside and potential flaw in the language. The if else structure parses initially as two separate statements. Later during semantic parsing it is combined into a separate expression.
const maximum = #(
if (x > y) {
x
}
else {
y
}
)
Cond is an alternative to if else expressions that can result in terser code.
cond {
condition
doSomething()
otherCondition
doSomethingElse()
else
otherThing()
}
Here's the last if else example as a cond
const maximum = cond {
x > y
x
else
y
}
Error handling is accomplished with a try catch finally
expression. This works exactly the same as in JavaScript with the exception that the last expression of a try block is returned if no error is thrown and the last expression of the catch if an error is thrown.
Example of a function that returns true if an error is thrown and false otherwise.
const throwsError = #(fn) => {
try {
fn()
false
}
catch (e) {
true
}
}
Errors can be thrown with the throw
operator just like JavaScript.
throw new Error('Failure message')
raise
is a shortcut for throwing a new error with a specified message.
raise('Failure message')
for
is used for list comprehensions. It takes one or more sequences and converts it into a single lazy sequence of results.
for (x [1 2 3]
y [:a :b]) {
[x y]
}
// Returns
[
[1 :a]
[1 :b]
[2 :a]
[2 :b]
[3 :a]
[3 :b]
]
for
can also take a :when
plus a condition clause to limit results
const evenNums = for (x (range 0 1000)
:when x % 2 == 0) {
x
}
// evenNums is [0 2 4 6 8 10 ...]
TODO link to the standard library docs of use map.
Functions in Willet create JavaScript arrow functions. The syntax is very similar.
const increment = #(v) => v + 1
The last evaluated value in the function is returned. Willet has no return
keyword.
const factorial = #(n) => {
if (n == 0 || n == 1) {
1
}
else {
n * factorial(n - 1)
}
}
Functions are invoked just as in JavaScript with parentheses.
const increment = #(v) => v + 1
increment(1) // => 2
The rest parameter syntax can collection any number of arguments.
const myFunc = #(p1 p2 ...others) => {
// others is an array of the other arguments
}
myFunc(1 2 3 4 5)
// others will be a javascript array of [3, 4, 5]
The splat operator ...
allows expanding a sequence to pass into a function.
const array = [2 3 4]
myFunc(1 ...array 5)
// my func is passed 1, 2, 3, 4, 5 as arguments
Macros are code that run at compile time to create new code. It's like extending the Willet compiler to support new kinds of code forms. Several of the existing parts of Willet are implemented as macros like chain
and for
.
This defines a macro that will run some code conditionally if a condition is not true. It's basically the opposite of if
defmacro unless = #(context block condition) => {
quote {
if (!unquote(condition)) {
unquote(block)
}
}
}
Using the macro:
unless (false) {
console.log('running')
}
Macros are passed 2 or more arguments:
- A compilation context - This is mostly not needed except in cases where the macro is calling back into Willet compilation code.
- block - A Block if the macro invocation was passed one.
- arguments - Any additional arguments are the arguments passed to the macro call
Macros are run during the compilation process so they don't receive arguments in the form of Abstract Syntax Tree (AST) Willet nodes. The AST Nodes are datastructures that represent Willet code.
Consider the following macro and call:
defmacro doesNothing = #(context block theArg) => {
console.log(`theArg: ${JSON.stringify(theArg null 2)}`)
theArg
}
doesNothing(true)
What is the output when we compile this code and run it:
Output During Compilation:
theArg: {
"_type": "BooleanLiteral",
"value": true
}
Note: AST are Immutable JS datastructures but are printed in the docs as JSON
Compiled JavaScript:
let doesNothing = (() => {
// Compiled doesNothing code here
})();
true; // <--- the output of the doesNothing macro is here
During compilation the doesNothing
macro was passed the AST node representation of the literal value true
. It printed that node and then returned it. The Willet compiler replaced the call doesNothing(true)
with the resulting code true
. The doesNothing
macro code is also present in the output code but is not invoked at run time.
The AST nodes passed into a macro can be directly manipulated or created but there are two helper functions for creating AST nodes with desired values.
Quote is a macro itself that will produce the AST that it is passed. It's like a templating system for creating Willet code.
Running quote at runtime
quote(1 + 1)
Produces output like this:
{
"_type": "InfixExpression",
"operator": "+",
"left": {
"_type": "NumberLiteral",
"value": 1
},
"right": {
"_type": "NumberLiteral",
"value": 1
}
}
Unquote is used to punch a hole in the quoted code to allow dynamic replacement of variables and values defined outside the quoted code.
Unquote is used here to reference the value argument in this macro.
defmacro multipleByPi = #(context block value) => {
quote(unquote(value) * Math.PI)
}
- Macros are defined with
defmacro
and must be defined at the top level of a module. - Macros can be exported from a module and used in another file.
There are two macros built into Willet that help in debugging macros. They expand calls to a macro and return the new AST.
parseWillet
- Returns the raw AST of an expanded macro call.macroexpand
- Returns the expanded Willet code of a macro call.
parseWillet
is good when you want to see the literal AST nodes for detailed debugging. macroexpand
produced easier to read Willet code.
parseWillet(
unless (false) {
console.log('running')
}
)
{
"_type": "Block",
"statements": [
{
"_type": "IfList",
"items": [
{
"_type": "If",
"cond": {
"_type": "UnaryExpression",
"operator": "!",
"target": {
"_type": "BooleanLiteral",
"value": false
}
},
"block": {
"_type": "Block",
"...": "..."
}
}
]
}
]
}
macroexpand(
unless (false) {
console.log('running')
}
)
// Produces
{
if (! false) {
{
console.log("running")
}
}
}
TODO Willet Standard library