WendyScript

Documentation 2.0

Getting Started

Try it online! Github Source

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

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.

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)
© Copyright Felix Guo 2019