Eve Macros
One file, two purposes
A project spreads its work over many files: the driver at the root, the aspects in asp/, the modules in lib/ (see Scripts). This is the right shape for a large application, but it is heavy for a small one. A script that only needs to read a file and print a report, or a job that runs two workers in parallel, should fit in one file that you can copy, mail or run with one command. Eve has two kinds of such files:
| Free script | Eve macro | |
|---|---|---|
| Purpose | a small tool, a test, an experiment: statements run from top to bottom | a small application: a driver with its own aspects and shared code |
| Line 1 | #!, required | #! optional, or a # title |
| Top level | statements (new, let, if, for, print, …) | declarations only: shared declarations, aspects, one driver |
| Subprograms, classes | no | yes, shared by the whole file |
| Aspects | applies the aspects of a project | declares its own aspects; may also apply those of a project |
Parallel groups, jobs, recover | no | yes, in the process of the driver |
| Run | eve tool.eve | eve pipeline.eve: runs process main of its driver |
| Imported by others | no | no: a macro is self-contained |
Both are self-contained: nothing in them can be imported by another script, so they never become a hidden dependency. Code that several programs need belongs in a module.
Free script
A free script starts with #! and runs its statements in order, to the end of the file. It has no driver, no process and no subprograms (the full rules are on the page Scripts).
#!/usr/bin/env eve
** a free script: statements in sequence, it ends at the end of the file
new total = 0 :Integer;
for i in (1..10) do
let total += i;
done;
print "total: {total}";
Use a free script when the program is one straight sequence. When it needs a function, a class, a job with recover, or an aspect of its own, it has outgrown the free script: make it a macro.
Eve macro
An Eve macro is a hybrid script: one file that holds shared declarations, any number of aspects and exactly one driver. No keyword marks it: a file with a top-level driver and other top-level declarations is a macro. The declarations are written at column 1, in any order; by custom the shared declarations come first, then the aspects, and the driver last, so the file reads from the tools to the work.
#!/usr/bin/env eve
# score: a driver and its aspect in one file
** shared declarations: seen by the aspects and by the driver
set bonus = 5 :Integer;
new total := 0;
function twice(x: Integer) => (@result: Integer) is
let result := x * 2;
return;
** an exclusive aspect runs alone, so it may change a shared variable
exclusive aspect collect is
process main(points: ()Integer) is
for p in points do
let total += p + bonus;
done;
return;
end collect;
** the driver: process main is the entry point of the macro
driver score is
process main is
apply collect((10, 20));
apply collect((30, 40));
print "total: {total}"; ** total: 120
print "double: {twice(total)}"; ** double: 240
return;
end score;
Shared declarations
Every top-level declaration that is not an aspect or the driver is shared by the whole file: classes, functions, procedures, methods, constants (set), variables (new) and imports (from … use). This is the difference from a project, where an aspect sees no global of the driver: in a macro the driver and its aspects share what the file declares at the top. Each aspect still keeps its own scope and state, and it sees no variable of the driver or of another aspect; only the shared declarations are common.
- The driver and the exclusive aspects use every shared declaration, the variables too: an exclusive aspect runs alone, so it changes a shared variable safely.
- A concurrent aspect uses only what is thread safe: constants, classes, atomic variables (
Atomic(:T)), channels, and the functions and procedures that the compiler proves thread safe. Anything else is a compile error that names it (below).
A parallel job in one file
A macro is the natural home of a small parallel job. The driver starts its concurrent aspects in a parallel group (see Concurrency); a shared class carries the results back. An output is declared before the group, so it is still there after done.
#!/usr/bin/env eve
# pipeline: two workers in parallel, one file
class Result = {name :String, count :Integer} <: Object is
constructor(name = "" :String, count = 0 :Integer) => (@self) is
let self := Object();
let self.name := name;
let self.count := count;
return;
end Result;
concurrent aspect job_worker_1 is
process main(@res: Result) is
** internal sequential job logic
let res := Result("worker 1", 10);
return;
end job_worker_1;
concurrent aspect job_worker_2 is
process main(@res: Result) is
** internal sequential job logic
let res := Result("worker 2", 20);
return;
end job_worker_2;
# driver executing parallel tasks
driver pipeline_driver is
process main is
new r1 := Result();
new r2 := Result();
p_exec: parallel
do
start job_worker_1(@r1);
start job_worker_2(@r2);
done p_exec;
print "{r1.name}: {r1.count}, {r2.name}: {r2.count}";
return;
end pipeline_driver;
This example is the demo pipeline_macro; every demo of the tutorial that needs aspects is written as a macro (Demos & Examples).
Concurrent aspects that must add to the same total use a shared atomic variable. Reading it gives a value that depends on the timing of the tasks, so the function that reads it is stochastic and its name ends with !:
# counter: eight tasks add to one shared atomic total
new count = 0 :Atomic(:Integer);
procedure add(n: Integer) is
let count += n;
return;
function total!() => (@result: Integer) is
let result := count;
return;
concurrent aspect adder is
process main(times: Integer) is
for i in (1..times) do
add(1);
done;
return;
end adder;
driver counter is
process main is
parallel
do
for i in (1..8) do
start adder(1000);
done;
done;
print "total: {total!()}"; ** total: 8000, at every run
return;
end counter;
Rules
- A macro has exactly one driver; it runs like a driver and its
process maintakes the command-line arguments. - Line 1 may be
#!, so the file runs as a command on Unix (./pipeline.eve), or a#title. A#!file without a driver is a free script. - Only declarations at the top level: a statement outside a declaration is an error.
- The aspects of a macro are private: no other driver can apply or start them. A macro exports nothing and can not be imported.
- An aspect named in
applyorstartis searched first in the macro, then inasp/and at the project root. An aspect of the macro with the name of an aspect file of the project is an error, so the reader never wonders which one runs. - Apart from the shared declarations, the aspects follow the rules of Aspects and of the parallel groups.
Compile errors
| Mistake | Message |
|---|---|
| two drivers in one file | a macro has only one driver |
| a statement at the top level | a statement outside a declaration in a macro |
aspects or shared declarations, no driver (no #!) | a macro needs a driver |
| a concurrent aspect uses a plain shared variable | the concurrent aspect 'adder' uses 'count', which is not thread safe |
| a concurrent aspect calls a subprogram that changes one | … calls 'bump', which is not thread safe |
Which one to choose
A program usually grows in three steps, and each step keeps the code of the one before:
- Free script: a few statements to try an idea or to do a small job.
- Eve macro: when the script needs functions, classes, error recovery or aspects, move the statements into
process mainof a driver and write the rest as shared declarations and aspects, in the same file. - Project: when the file grows too long, or other programs need its aspects or its code, move the aspects to
asp/and the shared code to a module inlib/; the driver stays as it is.
Read next: Modules