Walk through

Introduction

We'll go through some basics of creating models in Vesta with some examples.  We'll start out slowly, but pick up the pace midway through.  By the end, we will have built a program under Vesta and executed it under Vesta control (just like the way compilers and other tools get executed).

For starters, you'll need a package somewhere to perform these examples in. Any old package will do, but you'll probably want to create a new one (since we'll be creating files with names like .main.ves). Plus, of course, you'll need to check it out to follow through these examples, since we'll be doing things like creating files in the package and using vmake. (We assume that you are already familiar enough with Vesta to handle this. If not, you should go through the Vesta tutorial first.)

You might find that you get cache hits on some of these examples, depending on how precisely you enter the text.  (I evaluated these models while developing this tutorial, and others probably will when they go through it.)  You can try altering the models slightly (changing variable names, or stings like "Hello World" to "Greetings Planet") or just using vmake -cache none instead of vmake alone.

Hello World, Vesta Style

This is downright simple. Create a file in your package named hello.ves, and fill it with this text:
 
{
  return "Hello World!";
}

Now type vmake -result hello.ves at your shell from within the working directory for your package, and look at what you get back from the evaluator. It should look something like this:
 
Vesta evaluator, version 2.21, Sept 23, 1998

Return value of `hello.ves':
  "Hello World!"

No errors were reported.

The evaluation of `hello.ves' was successful.

See the "Hello World!" in the middle? (We made it bold so it would be easier to spot.)

You've just written a very simple program in the Vesta system modeling language. It's a function which returns a string constant.

Before we move on, let's make a small change to hello.ves:
 
{
  return _print("Hello World!");
}

If you do vmake hello.ves again, you'll get this:
 
Vesta evaluator, version 2.21, Sept 23, 1998
"Hello World!"

Return value of `hello.ves':
  "Hello World!"

No errors were reported.

The evaluation of `hello.ves' was successful.

See the first "Hello World!" which showed up that time? That's from the call to the _print function. There are three important points to note about this before moving on:

  1. Even though we printed out "Hello World!" (which was the point), we didn't remove the return statement. In fact, we can't. The Vesta system modeling language language is a functional programming language. Every system model is a function and must return a value. The last statement in any syntactically correct Vesta model must be a return statement.
  2. Although it seems silly in this case, the _print function returns a value. All functions return some value, _print just happens to return whatever it printed.
  3. We returned the value that _print returned. We didn't have to do this, but we did have to do something with the value it returned. Since it's a functional language, calling a function is not a kind of statement, only a kind of expression.
In a functional language, calling a function can't have any side effects, so if you didn't do anything with its return value, why call it at all? This particular case is a bit weird, because _print obviously does have a side effect: it produces output on the terminal. However, it's important to remember this principle.

Importing Other Models

Create another file in your package named import.ves, and put this inside it:
 
import
  hi = hello.ves;
{
  return hi();
}

Go back to your shell and vmake -trace import.ves. You'll see something like this:
 
Vesta evaluator, version 2.21, Sept 23, 1998

Return value of `import.ves':
  "Hello World!"

No errors were reported.

The evaluation of `import.ves' was successful.

Function call graph:
  0. import.ves: miss
  1. /vesta/example.com/play/jsmith_experiment/checkout/2/6/hello.ves: miss
  1. /vesta/example.com/play/jsmith_experiment/checkout/2/6/hello.ves: hit (ci=35792)
  1. /vesta/example.com/play/jsmith_experiment/checkout/2/6/hello.ves: add (ci=18548)
  0. import.ves: add (ci=18549)

As you can see, the return value is the same as it was when you evaluated hello.ves. import.ves imports the model hello.ves. It assigns a value which represents hello.ves to a variable named hi. It turns out that this value is of type function. import.ves then calls that function, and returns the value it returns.

There is something important to note here: the output from the _print function (called by hello.ves) is missing. Where did it go? Into the function cache of course! Since you recently evaluated hello.ves, the call to hello.ves resulted in a cache hit. This means that it didn't actually evaluate hello.ves, it just returned the cached result value.  The function call graph at the end of the output (which was produced by use of the -trace command-line flag) shows the details of what happened.  Evaluating import.ves was a cache miss, but that evaluating hello.ves was a cache hit.

Just for fun, do vimports import.ves at the command line.  It'll show you that import.ves imports hello.ves.

Fun with Bindings

If you only understand one of the Vesta data types, it should be bindings.  They are central to the way files and directories are manipulated in Vesta system models.  They are used to provide source files to tools (such as compilers).  They are also how generated files are returned from tools, and the way files are returned to the user (to be shipped after the successful evaluation of a model).

Create another file in your package named binding.ves, and put this inside it:
 
{
  b1 = [x=1];
  b2 = [y=2];
  junk = _print(b1);
  junk = _print(b1/x);
  junk = _print(b2);
  return b1 + b2;
}

The first two assignment statements create bindings.  Whenever you see something enclosed in square brackets, that's a binding.  Both of the bindings created contain a single name/value pair.  You've probably already figured out that the first one binds the value 1 to the name "x", and the second one binds the value 2 to the name "y".

The second print statement uses a binding lookup expression.  The expression b1/x means "look up the value associated with the name 'x' in the binding b1".  Since b1 binds the name "x" to the value 1, this evaluates to 1.  In more complex models, you may see examples of this where the binding is generated by some other expression (such as a function call) or even another binding lookup expression (which makes it possible to do nested lookups into sub-bindings, sub-sub-bindings, etc.).

 The return statement uses the binding overlay operation (which looks just like adding two bindings together) to combine the two bindings.

Now vmake binding.ves. You'll see something like this:
 
Vesta evaluator, version 2.21, Sept 23, 1998
[ x=1 ]
1
[ y=2 ]

Return value of `binding.ves':
  [ x=1, y=2 ]

No errors were reported.

The evaluation of `binding.ves' was successful.

You probably could've guessed what the result would be: a binding with two name/value pairs, combining the two in the bindings b1 and b2.

Let's make some modifications to binding.ves:
 
{
  b1 = [x=1];
  b2 = [y=2];
  junk = _print(b1);
  junk = _print(b2);
  junk = _print(b1 + b2);
  junk = _print(b2 + b1);
  return (b1 + b2) == (b2 + b1);
}

If you vmake binding.ves again, you'll see something like this:
 
Vesta evaluator, version 2.21, Sept 23, 1998
[ x=1 ]
[ y=2 ]
[ x=1, y=2 ]
[ y=2, x=1 ]

Return value of `binding.ves':
  FALSE

No errors were reported.

The evaluation of `binding.ves' was successful.

This illustrates a subtle point about bindings: the name/value pairs they contain have an order.  That's why b1 + b2 is not equivalent to b2 + b1.

Before we're done, let's do a couple more tricks with bindings.  Edit binding.ves one more time:
 
{
  b1 = [x=1];
  b2 = [y=2];
  junk = _print(b1);
  junk = _print(b2);
  b3 = [ coord/z = 3 ];
  junk = _print(b3);
  b4 = [ coord = b1 + b2 ];
  junk = _print(b4);
  junk = _print(b3 + b4);
  junk = _print(b3 ++ b4);
  return (b3 + b4) == (b3 ++ b4);
}

The assignment to the variable b3 uses a shortcut syntax for creating nested bindings.  It actually assigns b3 to a binding with the name coord bound to another binding which has z bound to 3.  The following statement is semantically equivalent:
 
  b3 = [ coord = [ z = 3 ] ];

The value assigned to b4 is similar, but its binding has the name coord bound to the result of b1 + b2.

Now vmake binding.ves again:
 
Vesta evaluator, version 2.21, Sept 23, 1998
[ x=1 ]
[ y=2 ]
[ coord=[ z=3 ] ]
[ coord=[ x=1, y=2 ] ]
[ coord=[ x=1, y=2 ] ]
[ coord=[ z=3, x=1, y=2 ] ]

Return value of `binding.ves':
  FALSE

No errors were reported.

The evaluation of `binding.ves' was successful.

This shows us another important point about manipulating bindings: the difference between the overlay operator (+) and the recursive overlay operator (++).  You'll notice that in b3 + b4, the value bound to coord in b4 (b1 + b2) completely replaces the value bound to coord in b3 ([ z = 3 ]).  However, in b3 ++ b4, the two sub-bindings bound to coord are merged.  The overlay operator only "sees" one level of binding: it simply replaces any names bound in both bindings with the value bound in the second operand (thus, coord = b1 + b2 replaces coord = [ z = 3]).  However, the recursive overlay operator will recurse into sub-bindings (and sub-sub-bindings, etc.), merging the bindings at all levels.

The Deal With Dot (.)

There's a special variable in the Vesta system modeling language language: "." (normally called "dot").  (The Vesta identifier syntax does allow variable names like this.)  Every function has an implicit final parameter which defines the value of dot.  When a function is called, if no value is specified for this implicit parameter, it takes the value of dot in the calling scope.  Even models, which have no explicit function arguments, take one argument which defines the value of dot.  In most cases, dot is implicitly passed into a function from the calling function.

Let's illustrate how this works with two of the models we used earlier.  Edit import.ves to look like this:
 
import
  hi = hello.ves;
{
  . = [ msg = "Hello World!" ];
  return hi();
}

Before calling hello.ves, this assigns dot a binding value with a single name bound to a text literal.  When we call the function hi in the return statement, this will get implicitly passed into hello.ves and become the value of dot when its body is evaluated.

Now edit hello.ves to look like this:
 
{
  return _print(./msg);
}

We've replaced the text literal for the message with a reference to ./msg.  In other words, the message is not hard-wired into hello.ves, but is instead taken from its environment.

If you vmake import.ves., you'll see the same message it used to deliver (since that's what we assigned to ./msg).  After doing that, change the message test in import.ves and evaluate it again to see that it changes the result of the evaluation.  This concept is important in the next section, so play around with it a bit to convince yourself that it works.

The Story Behind std_env

Now let's do something a little more typical of what you'll see in other packages. Create another file in your package named .main.ves, and put this inside it:
 
from /vesta/vestasys.org/platforms/linux/redhat/i386 import
    std_env/8;
{
  . = std_env()/env_build();
  return ./target_platform
}

This introduces several new things, so let's go over them one at a time:

  1. We're importing a model from another package.  In order to do so, we have to tell Vesta where to find it.  In fact, we have to tell Vesta the complete path to it, including the version of the package that we want to import.
  2. The from ... import construct is a short-hand for importing models with long paths. What we see in this model:
  3. from /vesta/vestasys.org/platforms/linux/redhat/i386 import std_env/8;
    Is equivalent to:
    import std_env = /vesta/vestasys.org/platforms/linux/redhat/i386/std_env/8/build.ves;
    It's most useful when you need to import several models with with a common path prefix, like this:
    from /vesta/example.com/shared/path import
        foo/7;
        bar/12;
  4. Unlike the previous example, we don't specify the name of the Vesta model to import.  If you import an entire directory, you get the build.ves file inside that directory (or an error if there isn't one).  So in this case, we're actually importing /vesta/vestasys.org/platforms/linux/redhat/i386/std_env/8/build.ves, even though the text "build.ves" doesn't appear anywhere in the model.
  5. We don't give a variable name to assign the model to.  If you omit the variable name and equals sign, the first component of the path after the word "import" is used.  This means that the variable std_env holds the function corresponding to the model std_env/8/build.ves.
  6. The assignment statement (the first statement in the body of the model) sets the value of dot.  This is how the "environment" (including standard libraries, the bridges which invoke compilers, and other common functions and data) gets passed around between models.
  7. The return statement uses an expression which indexes into the value assigned to dot.  (For now, ignore the details of the assignment statement.)  The value assigned to dot is a binding.  The expression in the return statement looks up the value assigned to the name "target_platform" in the binding assigned to dot.
Now go to the command line and do vmake (note the lack of a parameter naming a specific model file).  You should see this:
 
Vesta evaluator, version 2.21, Sept 23, 1998

Return value of `.main.ves':
  "Linux2.4-i386"

No errors were reported.

The evaluation of `.main.ves' was successful.

As you can see, the value assigned to ./target_platform is the string "Linux2.4-i386".  We didn't need to tell vmake that it was .main.ves that we wanted to evaluate: that's the default model to evaluate.

The value generated by the std_env model (and stored in the variable dot) is big and complex.  (If you're feeling adventurous, you can print it out and look at it.)  It contains all kinds of things like standard libraries and their associated header files, the bridge functions for invoking different compilers, command line options for different tools, and even compiler executables.  (Try looking at ./Cxx/expert/root: it's the filesystem used when compiling C++.)  Normally, you call std_env to generate this for you in .main.ves, then you implicitly pass dot to another model (often build.ves) to actually do some compilation.

Of course, std_env is not strictly necessary, it's just a convention.  Every model could import all the different pieces it needs (like compilers and libraries), but centralizing all this common stuff in std_env makes most models much simpler.

Inline Source Code and Other Hacks Your Mother Wouldn't Approve Of

I think that one of the best ways to understand a language is to do something out of the ordinary with it.  It's important to understand not just the syntax and semantics of a language, but where its boundaries lie: what can and can't you do with it?  We'll push on the Vesta system modeling language language in this exercise, and see some things you might not normally encounter.

Let's keep working on .main.ves:
 
from /vesta/vestasys.org/platforms/linux/redhat/i386 import
    std_env/8;
{
  . = std_env()/env_build();
  code = "#include <iostream.h>\n" +
         "main(){cout<<\"Hi there\\n\";}\n";
  return ./Cxx/program("foo", [ foo.cxx = code ], [],
                       <./libs/c/clib_umb>);
}

The variable code is a text string.  I've broken up the string into two lines to make it a little more readable, and concatenated the pieces together with the + operator.  As you can see, it is a complete C++ program, which one might ordinarily format like this:
 
#include <iostream.h>

main()
{
  cout << "Hi there\n";
}

I've just compressed it down a bit by removing most of the whitespace one would ordinarily see.

The return statement invokes a function which is part of the standard environment.  Specifically, it looks up the function bound to the name program inside the binding bound to Cxx inside the binding assigned to the variable dot.  This function compiles and links a C++ program.  It requires four arguments:

  1. The name of the executable to produce (in other words, the filename the linker should be told to generate)
  2. A binding of source files to be compiled
  3. A binding of header files included by the source code to be compiled
  4. A list of libraries to link with
We'll call our program "foo", so that's the first argument.  For the source files, we'll have just one, named foo.cxx, and filled with the text in the variable code.  We don't have any header files of our own, so we pass the empty binding for the third argument.  We'll use some standard libraries such as the C++ and C runtime libraries.  (Don't worry too much about the fourth argument.  It just grabs libraries from the standard environment and puts them in a list.)

Let's look a little more closely at the second parameter.  The first thing to note is that the source code we're compiling is not stored in a file, it's inlined in the system model.  This is not the way one usually deals with source code under Vesta.  Usually, the source code would be in a separate file and imported with a files clause at the beginning of the model.  However, once you bring in a file with a files clause, it's just a text value, indistinguishable from one defined with a literal in a system model.  In fact, all files manipulated by the Vesta evaluator are text values, including compiler executables, object files, and other tool result files. This means that they're interchangeable, so there's nothing preventing us from putting source code in a system model as we've done here.

The second thing to note about the binding we pass to ./Cxx/program is that it binds a filename (foo.cxx) to the file's contents (the C++ program stored in the variable code).  If you think about it, this is very similar to a directory in the filesystem, which also associates names (filenames) with values (file contents).  In fact, as far as Vesta is concerned, a binding is a directory.  Just as text values and files are interchangeable, so are bindings and directories.  (You might want to take a minute to think about these two points, as they have a lot of implications.  If you don't get it right now, don't worry about it.)

Let's go do a vmake.  Assuming you don't get a cache hit (which might happen if someone else did this exercise recently), you'll see this:
 
Vesta evaluator, version 2.21, Sept 23, 1998
"Building program foo"
0/localhost: /usr/bin/g++ -c -I -I/usr/include -I/usr/include/g++-3 -O0 -g2 -pipe foo.cxx
0/localhost: /usr/bin/g++ -L -L. -o foo -O1 -Wl,-u -Wl,pthread_self hello.o libstdc++.a libgcc.a libpthread.so.0 libm.so.6 libc.so.6 libc_nonshared.a

Return value of `.main.ves':
  [ foo=<file 0x95fe7ae2> ]

No errors were reported.

The evaluation of `.main.ves' was successful.

As you can see, the program was compiled and linked into an executable.  Go ahead and ship it somewhere and run it to convince yourself this worked.

Running Tools Under Vesta

Now let's do one more thing with this little program: invoke it from within Vesta, just like compilers are invoked.  Edit .main.ves one more time:
 
from /vesta/vestasys.org/platforms/linux/redhat/i386 import
    std_env/8;
{
  . = std_env()/env_build();
  code = "#include <iostream.h>\n" +
         "main(){cout<<\"Hi there\\n\";}\n";
  exe = ./Cxx/program("foo", [ foo.cxx = code ], [],
                      <./libs/c/clib_umb>);
  cmd = <"foo">;
  . ++= [ root/.WD = exe ];
  . ++= [ root = ./build_root(<"glibc", "libstdc++">) ];
  r = _run_tool(./target_platform, cmd,
                /*stdin=*/ "",
                /*stdout_treatment=*/ "value");
  return [foo.out = r/stdout];
}

Let's go over the changes here:

  1. Instead of returning the program generated by ./Cxx/program, we capture it in a variable named exe.  If you were to look at it, you'd see that it's a binding with the name foo bound to a text value, which is a string of the data in the executable we've compiled.
  2. We create a list of strings to represent the command line we want Vesta to invoke for us and store it in the variable cmd.  In this case, the command-line is very simple: it's just one string, "foo", the name of our executable.
  3. We modify the value of dot with the in-place recursive overlay operator (++=).  What we're actually accomplishing here is making our executable available to the tool invocation.  ./root/.WD will be the working directory when we invoke a tool.  Since bindings are directories, we can merge the binding in exe (which contains our executable) into this binding.  This makes it so that the working directory will contain our executable foo when we run our command-line.
  4. We also merge the C and C++ run-time libraries into ./root. This is necessary, because our executable foo may need shared libraries for the C and C++ run-time library.
  5. We invoke the primitive function _run_tool.  This is how all commands are executed by Vesta (compilers, the linker, etc.).  We pass it a few arguments: the target platform (./target_platform), the command line (which we constructed in cmd), the text for the standard input stream (empty in this case), and an indication of how it should treat the standard output.  The important one here is that we tell it to return the standard output to us as a value.
  6. As the result of the model, we return a file named foo.out with the standard output from our invocation of _run_tool (from r/stdout) as its contents.
If you evaluate this model and ship its result, you can look at the contents of foo.out, and see the string the C++ code printed.

The way ./root affects _run_tool is very significant, so we should talk about it a little more.  Vesta builds use a technique called filesystem encapsulation.  Every time a tool is invoked under Vesta (every time _run_tool is called), Vesta creates a temporary filesystem derived from the binding in ./root.  Vesta uses the UNIX system call chroot(2) to make this temporary filesystem the entire universe, as far as the running tool can see.  (The directory defined by ./root becomes equivalent to / in the UNIX filesystem.)  This makes it possible for Vesta to have complete control over which files the tool reads and to be able to know which files the tool has written.  Although we don't demonstrate it here, a call to _run_tool that generates files will return them in the form of a binding to text values.  This is also how Vesta does precise dependency checking.  Since it provides the filesystem a tool sees, it knows every file read by the tool invocation, which allows it to determine exactly which sources a given build result depended upon.


Kenneth C. Schalk <ken@xorian.net>   / Vesta SDL Programmer's Reference