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.
{ 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:
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.
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.
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.
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:
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;
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.
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:
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.
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:
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.