WendyScript
Documentation 2.0
Getting Started
The source is entirely in C and you are welcome to clone and build it yourself. Wendy can be run either with a file or in REPL mode.
By typing wendy
into the console, wendy will be run in REPL mode. Otherwise:
wendy filename.w
Introduction
Welcome to WendyScript, a programming language! WendyScript supports first class functions, dynamic typing, optional semicolons, and short form statements. Let's start with the most basic: outputting "Hello World!".
"Hello World!"
This will output Hello World!
to the console.
You may have noticed that we did not need to use a print function. This is because expressions evaluated at the top-level will be printed out in the console by WendyScript.
Each top-level expression will be printed on seperate lines (the newline character follows the expression). For example:
"Hello World!";
"This is on a new line!";
Hello World!
This is on a new line!
This behaviour can be negated by adding a @
character in the front of
any top-level expression:
@"Hello World!";
"This is on a new line!";
Hello World!This is on a newline!
Variables
WendyScript is dynamically typed and supports three (+ null) types of data: numbers, strings, and boolean values.
let name = "Wendy"; // declares and sets a string variable
let age = 18; // declares and set a number variable
let x; // declares x and sets it to <none>, a NULL type
let valid_license = true; // declares and sets a boolean variable
age = 19; // mutates the value of age to be 19
name = 20; // dynamic types allows variable types to be changed
WendyScript also supports two special commands for numeric typed variables: inc
and dec
.
These functions can be called to add 1 or subtract 1 to a variable respectively.
let number = 9;
inc number; // number is now 10
dec number; // number is now 9
inc
and dec
can only be called
on variables that are numbers. Calling on a variable with string value will throw an error.Operators
Operators in WendyScript are shown below.
10 + 10; // Addition
9 - 2; // Subtraction
3 * 8; // Multiplication
10 / 4; // Floating Point Division
10 \ 4; // Integer Division
((1 + 2) * 10) / (5 * 3); // Any Combination
"Hello " + "World!"; // String Concatenation
// Boolean Operators
true and true;
true and false;
true or false;
false or false;
20
7
24
2.5
2
2
Hello World!
<true>
<false>
<true>
<false>
WendyScript also supports generic boolean operators like >= <= == < >
.
Functions
WendyScript supports first class functions. Functions can be passed and returned through other functions. Local functions can be declared within other functions. To bind a function to an identifier:
let f => (x) {
function_body;
};
WendyScript also supports recursive functions.
let factorial => (x) {
if (x <= 1) {
ret 1;
}
else {
ret x * factorial(x - 1);
};
};
@"The factorial of 5 is ";
factorial(5);
The factorial of 5 is 120
Here we introduce you to the ret
keyword. ret
allows functions
to return a value to the original place it was called. In this case, the returned
value 120
was left as a top level expression and thus was printed.
WendyScript functions can be written without curly braces if the function body only includes one
statement. WendyScript also supports implicit returns if the function body is an expression. The
factorial()
function written above can be simplified to:
let factorial => (x) if (x <= 1) ret 1 else ret x * factorial(x - 1);
More info about this can be found in the Short Forms section of this documentation.
Lambda
WendyScript also support lambda functions. Lambda functions can be defined using #:
.
For example:
#:(x) { ret x * 2; }
Lambda functions can be bound to identifiers via regular assignment, which
is really just an alternative to using =>
.
let f = #:(x) { ret x * 2; };
// is essentially the same as
let f => (x) { ret x * 2; };
Since functions are first class in WendyScript,
we can pass a function identifier, or a lambda function into other functions, as well as bind
local functions within other functions and even return functions. Let's create a function
named apply()
that takes in a function that accepts two values, and two additional
values, then applies the given function to the given two values.
let apply => (fn, a, b) {
ret fn(a, b);
};
This function can now be invoked by either passing a function identifier, or a lambda function.
let multiply => (x, y) { ret x * y; };
apply(multiply, 10, 20); // invoke with another function
apply(#:(a, b) { ret a * b; }, 10, 20); // invoke with a lambda function
200
200
Lists
WendyScript supports a dynamically typed collection: a list. To create a list:
let mylist = [];
This declares an empty list.
let mylist = [1, 2, "hello", false];
This declares an list of size 4 with the given elements. An list's elements can be accessed with the index:
mylist[0]; // accesses the first element
mylist[3]; // accesses the last element
When a list is pass into a function, it is always passed by reference. This is the same as assigning a list to another. A copy is not made.
let my_fn => (lst) {
lst[1] = 10; // Change the second element
ret;
};
let a = [1, 2, 3];
my_fn(a);
a;
[1, 10, 3]
You can make a shallow copy of a list using the ~
operator. For example, if you passed ~a
into my_fn
, it would
not modify the original list.
WendyScript also has a range of operators for dealing with lists.
// We have list equality and inequality
[10, 20, 30] != [20, 30, 10]; // true
[10, 20, 30] == [10, 20, 30]; // true
// List Concatenation
[10, 20, 30] + [20, 30, 40]; // [10, 20, 30, 20, 30, 40]
// Item Append
true + [10, 20, 30]; // [true, 10, 20, 30]
[10, 20, 30] + 40; // [10, 20, 30, 40]
// in operator
// Does 10 exist in the list?
10 ~ [10, 20, 30]; // true
// List Repeat
[1, 2, 3] * 3 // [1, 2, 3, 1, 2, 3, 1, 2, 3]
// This is useful when trying to instantiate a list with a fixed size
let a = [0] * 20
Objects
WendyScript also supports Object-Oriented Programming through the use
of Structures and Structure Instances. Structures are also first class citizens
and as such can be assigned and passed around. To create a structure, we
use the struct
keyword. For example:
struct posn => (x, y) [print];
In the backend, this accomplishes several tasks. A posn structure is defined
with two instance members: x and y, as well as a static member: print. A default
constructor function is also created based on the instance members. This constructor
function can be overwritten by mutating the posn.init
function,
but this constructor must end with ret this;
to pass the instance
back to the caller.
Let's define what print does:
posn.print => () {
"This is a posn object: " + this.x + ", " + this.y + ".";
};
Now we can create a posn instance with: let a = posn(10, 20);
and access
the instance members with a.x
and a.y
. We can also call print with
a.print()
.
Overloads
When dealing with custom structures, we may want to define custom behaviours
for operators, such as +
.
This can be accomplished by defining a custom function in the form:
let <struct_name> + <struct_name> => (lhs, rhs) ...
For example, we want to create an +
overload for the previously created posn
struct which will add the x and y components.
struct posn => (x, y) [print];
let <posn> + <posn> => (lhs, rhs) posn(lhs.x + rhs.x, lhs.y + rhs.y);
Now we can write:
(posn(10, 20) + posn(30, 40)).print()
This is a posn object: 40, 60.
We can also overload the implicit print function in WendyScript. This is the function that is called when an expression appears on the top level. For example:
posn(10, 20)
will print:
<struct:posn>
This isn't very helpful, so we can use the print overload:
let @ <posn> => (p) p.print();
This means we can now write:
posn(10, 20)
and get:
This is a posn object: 10, 20.
Before an expression is printed, the Wendy VM will check if there is
a print overload defined, and will call that instead. The resulting print-out
can either be returned as a string or printed out in the function.
Conditions
Conditions work the same way as in most languages with familiar syntax.
let x = 10;
if (x <= 10) {
"X is less than or equal to 10!";
}
else {
"X is greater than 10!";
};
X is less than or equal to 10!
Conditions can also include multiple boolean expressions that are
joined with either and
or or
.
let name = "Wendy";
let age = 18;
let valid_license = true;
if (age >= 19 and valid_license) {
"You are old enough to drink!";
}
else if (valid_license) {
"You're not old enough!";
}
else {
"You don't have a valid license!";
};
You're not old enough!
Loops
WendyScript supports an advanced loop syntax using the keyword for
. There are
two ways to use this for loop. The first is to provide it with one parameter, in which the
parameter must be a condition and the loop will act like a while
loop.
let x = 5;
for (x > 0) {
@(x + " ");
dec x;
};
5 4 3 2 1
The other way is to supply two parameters, in which the loop acts like a foreach
loop.
The first parameter must be an identifier (which will automatically be declared), and the second
parameter should be an iterable collection (List, Range, String). The identifier supplied will be
inflated with the value of the current object in the iterable collection. For example, to print out
the contents of a list:
for i in [5, 4, 3, 2, 1] {
@(i + " ");
}
5 4 3 2 1
You could also iterate through a range (right bound of range is always exclusive):
for i in 5->0 {
@(i + " ");
}
5 4 3 2 1
And finally, through a string:
for i in "Hello" {
@(i + "!");
}
H!e!l!l!o!
User Input
User input is easy in WendyScript! Input is achieved by the input
keyword.
let name;
@"What is your name? ";
input name;
@"Your name is: " + name;
What is your name? Wendy
Your name is: Wendy
If the user enters a number, it will be stored as such, anything else will be stored as a string.
Be aware that you must declare the variable the user input is to be stored in before prompting input
.
Time
WendyScript provides the time
keyword, which returns a UTC value of the current time.
This is often used with import random
to instantiate the seed for the random number generator.
Imports
WendyScript allows the management of modules by using the keyword import
. Import
simply pulls the external code into your code file. The VM will do basic checking
so multiple imports of the same library do not conflict. You can import
3 types of external modules:
WendyScript Library Files
These are predefined libraries that are included with the WendyScript installation. These
can be found on Github.
These are imported by bare keywords, which will pull the pre-compiled math.w
library:
import math
Other Source Files
Another source file in the same directory can be imported, if we have two files:
foo.w
and bar.w
,
we can import the contents of bar.w
into foo.w
by writing the following:
import "bar.w"
Precompiled Source Files
When working with larger projects, it might not be wise to import the contents by source file
since this means the entire project will be re-parsed and re-compiled. Instead, the Wendy compiler
offers a -c
flag, which compiles a .w
file and produces a .wc
bytecode binary (which can also be run with wendy
).
In the previous example, we can first run wendy -c bar.w
which generates a
bar.wc
file, then write in foo.w:
import bar
Typically this would be used in conjunction with make
to manage which files to
compile.
math.wc
file in the local directory,
import math
would import that and not the math library.Short Form / Shortcuts
Syntax is WendyScript is relatively relaxed. Conditions in loops and conditionals do not require parentheses and single statements do not require curly braces either. This is completely valid:
let a = 10
if a == 10 "A is 10!" else "A is not 10!"
Functions in WendyScript also have implicit returns, so if the body of the function is just an expression, it will be automatically returned.
let square => (x) {
ret x * x;
}
// can also be written as
let square => (x) x * x
// which is a lot less verbose
Most keywords in WendyScript also have a symbolic short form.
: in
& and
| or
? if
: else
# for
++ inc
-- dec
<< let
>> input
/> ret
So a factorial function could be written like this:
<<fact=>(x)?x==0/>1:/>x*fact(x-1)