-
Notifications
You must be signed in to change notification settings - Fork 26
Manual
The mylittlepony
language and compiler are the UU-team's shot at getting a base language up and running. We want a compiler that is small and clear, allowing us to implement more interesting type systems as the Upscale work progresses.
The obligatory hello-world program, hello.enc
, looks like this:
#! /usr/bin/env encorec -run
class Main
def main() : void {
print "Hello, World!"
}
You can compile this program by running encorec -clang hello.enc
. Or, if the file is executable, you can run it by saying:
$ ./hello.enc
Hello, World
It is important to know that every legal mylittlepony
program needs to have a class called Main
, with a method called main
. The runtime will allocate one object of class Main
and begin execution in its main
method.
Mylittlepony
implements active and passive classes. Allocating an object from those classes gives active or passive objects, resp. To make a class active, no special keyword is necessary, as classes are active by default: class A
. To make a class passive, use passive class P
.
Calling a method on an active object will have that method execute concurrently (also in parallel, if enough cores are available). The return type of an active method is a future, not a value. Active classes are the default in mylittlepony
.
Calling a method on a passive object will execute that method synchronously, in the calling thread. Passive objects are similar to what you'd expect in a Java-like language.
We write an active class Foo
. A foo has an ID string, calling the printID
method will print that string 5 times to stdout
. In the main method, we create two Foo
instances, and have them print their IDs
#! /usr/bin/env encorec -run
-- ex_active.enc
class Main
def main() : void
let obj1 = new Foo
obj2 = new Foo
in {
obj1.setID("obj1");
obj2.setID("obj2");
obj1.printID();
obj2.printID();
()
}
class Foo
id : string
def setID(new_id : string) : void {
this.id = new_id;
()
}
def printID() : void {
let i = 0 in {
while i < 5 {
i = i + 1;
print this.id
}
}
}
Executing this program gives nondeterministic output:
$ ./ex_active.enc
obj2
obj1
obj2
obj1
obj2
obj1
obj2
obj1
obj2
obj1
A passive class is declared using the passive
keyword:
#! /usr/bin/env encorec -run
--ex_passive.enc
passive class Location
x : int
y : int
label : string
def init(new_x:int, new_y:int, new_label:string) : void {
this.x = new_x;
this.y = new_y;
this.label = new_label;
()
}
class Main
loc : Location
def main() : void {
this.loc = new Location;
this.loc.init(1,2,"a place");
print this.loc.x;
print this.loc.y;
print this.loc.label
}
Running:
$ ./ex_passive.enc
1
2
a place
As long as we only have unary print statements, and we have several actors that are printing multiple values at the same time (eg. an ID and a value), we have no way of discerning which output is printed by what actor. To be able to print more than one value at a time we use the printf
statement:
def report(id : int) : void
printf "ID = {}\n Value = {}\n", id, this.val
Encore has anonymous functions:
let f = \ (i : int) -> 10*i in {
print f(10) -- prints '100'
}
The backslash \
syntax is borrowed from Haskell and is supposed to resemble a lambda. It is followed by a comma separated list of parameter declarations, an arrow ->
and an expression that is the function body.
The type of a function is declared similarly. The function f
above has type int -> int
. Multi-argument functions have types such as (int, string) -> bool
.
For a complete example, see ex_lambdas.enc.
There is limited support for polymorphic methods and functions. Type variables are written with lower case letters and do not need to be declared. If there is a type variable in the return type it must appear somewhere in the types of the arguments. Here is an example program that uses a polymorphic function:
class Main
def main() : void
let
apply = \(f : a -> b, x : a) -> f(x)
in
let bump = \(x : int) -> x + 1 in
print apply(bump, 3)
In some cases, you'll see code using the embed..end
keywords. There are two ways to embed c code:
as an expression, or as a toplevel embed block.
When embedding an expression, the programmer needs to assign a type to the expression; encorec
will assume that this type is correct. The value an embedded expression evaluates to is the return value of the last C-statement in the embedded code.
A toplevel embed block is a block of C-code that will be inserted into the generated C code, before compiling the program using clang
. These blocks are useful if you want to insert #include
directives, or if you want to implement C functions.
However, we do not advocate to rely on the embed
functionality too much. Code that uses embed
is highly likely to break with future updates to the language (even more likely than code that doesn't use embed
;) If you want to access local variables in an embed
expression, you'll need to wrap them: #{x}
for accessing the local variable x
. If you want to access fields, you don't wrap them, but use C's arrow operator: this->foo
for this.foo
.
#! /usr/bin/env encorec -run
embed
// We're defining the sq(int) function in a toplevel embed block.
// Each file can have no more than one toplevel embed blocks.
int sq(int i) {
return i*i;
}
end
class Main
def main() : void {
let x = 2 in {
-- Contrary to the toplevel embed block, an embed expression
-- also needs to specify a certain type.
-- In this example, the expression promises to return an int:
print (embed int sq(#{x}); end)
}
}
The syntax is documented in source code in Parser.hs.
-
Download VirtualBox image from this url
-
Start the virtual machine, using
encore
as username and password. -
~/mylittlepony
contains the source code for encore compiler.make test
would verify that everything is set up. -
The code in this VM image becomes obsolete as the development continues in the repo, so one github account, which could be used to access the latest state of encore, is provided:
encore-image
with passwordencore-image123