Tuesday, February 14, 2012

2. Some Wally samples


Parts: 1 2 3 4 5 6 7

In this part I'll be introducing you to Wally by providing some example code and indicating how it would (or should) evaluate.

white spaces

Wally will look a bit like Python w.r.t. indentation and line-breaks acting like end of statements. For example, take the function twice:

twice(n)
    temp = n + n
    return temp
x = twice(6)
print(x)         # => 12

(note that the # => 12 comment indicates that the value 12 is either returned, or displayed)

The two statements temp = n + n and return temp both belong to the code block of the function twice. Notice that both statements don't have a semi colon indicating their end: a line break indicates the end of a statement.

comments

Wally only knows single line comments which start with a # sign:

# a single line comment
# and another comment

data types

Wally will support the following data types: boolean, number, string, list and user/programmer defined objects. There will also be none which is the null equivalent in Java.

boolean

A boolean is one of:

true
false

number

A number can be an integer, or floating point number:

1234
100.987

string

A string is a double quoted value. If you want to use a literal double quote, it needs to be escaped with another double quote:

"a string"
"A ""B"" C"    # => A "B" C 
"1\t2"         # => 1    2

list

A list is zero or more expressions/values separated by a comma, surrounded by { and }. Note that although Wally will be white-space sensitive, lists will be able to span more than a single line. The following two lists are therefor equivalent:

{1, 2, 3, 4, {5, 55, 555}}
 
{
  1, 2, 3, 4, 
  {
    5, 55, 555
  }
}

objects

A user object starts with the keyword obj followed by an identifier, which will be the name of the object. After that, one or more indented identifiers (instance variables) and/or functions are placed under the object indicating these belong to an instance of such an object. For example, a Rational object might look like this:

obj Rational
 
    num # the numerator
    den # the denominator
 
    # a function with the same name of the 'obj' acts like a constructor
    Rational(n, d)
        num = n
        den = d
 
    denominator()
        return den
 
    # multiply 'this' with 'that'
    mult(that)
        return new Rational(num*that.num, den*that.den)
 
    numerator()
        return num
 
    # returns a string representation of 'this'
    str()
        return num + "/" + den
 
# some statements outside the object are executed as a script
s = new Rational(3, 4)
t = new Rational(5, 6)
 
println("s   = " + s)
println("t   = " + t)
println("s*t = " + (s*t))

Executing the script above would result in the following output:

s   = 3/4
t   = 5/6
s*t = 15/24

operator overloading

As you may have noticed in the Rational object, the expression (s*t) resulted in the mult function to be invoked. Some function names have a special meaning in Wally, like mult and str in the Rational object. Below is a list of all special function names that map to an operator, or a special action:

add(that)           # this + that
and(that)           # this && that
div(that)           # this / that
eq(that)            # this == that
get(index)          # this[index]
gt(that)            # this > that
gteq(that)          # this >= that
in(needle)          # check if needle is present in this
lt(that)            # this < that
lteq(that)          # this <= that
mod(that)           # this % that
mult(that)          # this * that
not()               # !this
or(that)            # this || that
pow(n)              # this ^ n
set(index, expr)    # this[index] = expr
size()              # this' size
str()               # this' string representation
sub(that)           # this - that
More on this later.

expressions

The usual mathematical operators can be used in Wally. Below are some examples in order of their precedence (the operators with the lowest precedence are listed first).

logical or

true || false      # => true
false || false     # => false

logical and

true && false      # => false
false && false     # => false
true && true       # => true

relational

3 >= 4             # => false
99 > 4             # => true
3 <= 3             # => true
3 < 4              # => true

equality

"1" == 1           # => false
"1" != 1           # => true
{1, 2} == {2, 1}   # => false
true != false      # => true

addition

2 + 3              # => 5
"ab" + 2           # => "ab2"
{1, 2} + 3         # => {1, 2, 3}
{1, 2} + {3, 4}    # => {1, 2, {3, 4}}

subtraction

2 - 3              # => -1
{1, 2, 2} - 2      # => {1, 2}
{1, 2, 3} - {3}    # => {1, 2, 3}

multiplication

2 * 3              # => 6
"abc" * 3          # => "abcabcabc"

division

6 / 3              # => 2
1 / 3              # => 0.33333333333

modulus

5 % 3              # => 2
5 % 1.5            # => 0.5

unary minus, not

-5                 # => -5
!true              # => false
!!false            # => false

power

2^10               # => 1024
2^3^4              # => 2417851639229258300000000
(2^3)^4            # => 4096

built-in functions

Wally will have 3 built-in functions: print, println and assert.

print("x")                  # => "x"
println("x")                # => "x\n"
assert(2 == 1+1)            # => passes
assert(2 == 1-1)            # => throws an error
assert(2 == 1-1, "Oops!")   # => throws an error with the message: "Oops!"

assignments

Assignments can be simple assignments like:

a = 5
print(a)    # => 5

But it could also be assignments in (nested) lists:

list = {1, 2, 3, {44, 55}}
list[2] = "c"
println(list)               # => {1, 2, c, {44, 55}}
list[3][0] = "d"
println(list)               # => {1, 2, c, {d, 55}}

or object instances:

obj Box
    value
 
b = new Box()
println(b.value)    # => none
b.value = 42
println(b.value)    # => 42

For all binary operations, +, -, * etc., there will also be the shorthand: +=, -=, *= etc. Where a += 1 is equivalent to a = a + 1.

user defined functions

A user defined function is just an identifier with parenthesis placed after it, with optional parameters inside these parenthesis followed by an indented block of statements.

factorial(n)
    if n < 2
        return n
    else
        return n * factorial(n - 1)
 
print(factorial(5))    # => 120

Note that a call to factorial could also be placed before the function is defined:

print(factorial(5))    # => 120

factorial(n)
    if n < 2
        return n
    else
        return n * factorial(n - 1)

And functions have their own scope: a variable defined in the global scope of a script is not visible inside a function, nor are variables defined inside the function available in the global scope. Below is an example that illustrates this:

total = "global total"
 
f()
    total = "local total"
    return total
 
println(total)    # => global total
println(f())      # => local total
println(total)    # => global total

parameter passing

Like Java, Wally passes its parameters by value. Note that this means its references to objects are copied and passed by value, not the entire object itself! A little demo:

obj Box
 
    value
 
    Box(v)
        value = v
 
# function f
f(b)
    b.value = 1
 
# function g    
g(b)
    b = new Box(100)
 
box = new Box(42)
println(box.value)    # => 42
f(box)
println(box.value)    # => 1
g(box)
println(box.value)    # => 1

In other words, changing the object's internal attributes is possible, but assigning a new instance to b, as done in the function g(...), has no effect on the original object instance box. Again, this works the same as in Java.

control statements

There will be the following control statements: if, for and while. In both the for and while statements, a break, or an early return, can terminate the loop.

if statement

An if statement will look like this:

if a > b
    println("a is more than b")
elif a < b
    println("a is less than b")
else
    println("a and b are equal")

ternary

There will also be the ternary statement:

n = a > b ? "a" : "b"

which would be equivalent to the following if statement:

n = none
if a > b
    n = "a"
else
    n = "b"

for statement

A for statement comes in two flavors:

for n=? to ? ...
for n in ? ...

where the question marks are expressions. Some examples:

for i = 1 to 3
    println(i)
 
for n in {10, 20, 30}
    println(n)
 
for c in "abc"
    if c == "c"
        break
    println(c)

When the script above is executed, the following will be printed to the console:

1
2
3
10
20
30
a
b

The nice thing about the for ... in ... statement is that you can iterate over custom objects, as long as the functions get(...) and size() are implemented. An example:

obj Bag
 
    itemA, itemB, itemC
 
    Bag(a, b, c)
        itemA = a
        itemB = b
        itemC = c
 
    get(index)
        if index == 0
            return itemA
        elif index == 1
            return itemB
        else
            return itemC
 
    size()
        return 3
 
    str()
        return "Bag=[" + itemA + ", " + itemB + ", " + itemC + "]"
 
# the script
bag = new Bag("x", "y", "z")
println(bag)
for item in bag
    println(item)

which would print:

Bag=[x, y, z]
x
y
z

while statement

The while statement will probably not pose any problems:

x = 5
 
while x > 0
    println("x=" + x)
    x -= 1

would print:

x=5
x=4
x=3
x=2
x=1

in operator

Finally, there's the in operator that tests if a certain value is present in a collection, or object.

list = {3, 4, 5}    
string = "abc"
 
println(2 in list)        # => false
println(5 in list)        # => true
println("A" in string)    # => false
println("b" in string)    # => true

And if the in function is implemented in a custom object, the in operator would use that overridden function:

obj Bag
 
    value_a, value_b
 
    Bag(a, b)
        value_a = a
        value_b = b
 
    in(needle)
        return value_a == needle || value_b == needle
 
# test the bag's in(...) function below
bag = new Bag("beer", "coffee")
 
println("Beer" in bag)    # => false
println("beer" in bag)    # => true

wrap up

That pretty much sums up the functionality we'll be putting into Wally. I realize I didn't provide many details about objects and how the operator overloading mechanism works exactly, but I'll get to that in later parts of this blog-series. The first problem we'll be tackling is the lexical analysis of Wally, of which the trickiest part will be how to let ANTLR properly recognize indent and dedent white spaces (and ignore them when inside a list!). Continue reading here.

If you like, you can also play around with the language a bit: I deployed a small interpreter in Appspot here.

3 comments:

Anonymous said...

Looking forward to more! I am currently working on a DSL to C translation, and the more examples, the better!

Anonymous said...

Is it alright to reference some of this on my page if I include a backlink to this site?

Bart Kiers said...

Sure, no problem.