Quantcast
Channel: David Eastman, Author at The New Stack
Viewing all articles
Browse latest Browse all 80

Introduction to Virgil, a New Language by Wasm’s Co-Creator

$
0
0
abstract

I came across a quote from the author of the language Virgil, Ben Titzer, who also happens to be a co-creator of WebAssembly: “I’m not looking to compete with Zig,” Titzer said. Having recently played with the C competitor Zig, I was interested to see what language was not competing with it.

So what is the goal of Virgil? In an email interview with The New Stack’s David Cassel, Titzer said, “I hope to make Virgil a great systems programming language, that strips away legacy cruft and yet has great features for writing robust systems code… things like virtual machines, compilers, kernels, network stacks, etc.”

Along with Rust, there is a trend towards new languages targeted at “lightweight high-performance systems” with strong cross-compilers. (Incidentally, Titzer says that Rust “can’t do the kinds of things Virgil does”).

The design reasonings that go behind building high-performance systems are quite a heady mix of compiler optimizations, code trees, unwinding branches, guard conditions, security, etc — many of which change over time. For example, today memory is cheap but security has tantamount importance, but the reverse was true 30 years ago. Titzer goes into these issues in depth in this recent Microarch podcast.

In this post, I’ll focus on using Virgil as a language, and we’ll see how it might compare with Zig and the lightweight trend. Virgil has classes and functions, and even typed parameters, so it should feel modern. But let’s install it and play with it.

Getting Started With Virgil

Out of context, the requirement options are a little unusual:

So for once, my old pre-silicon Mac is welcome.

Cranking up Warp, a Rust-based terminal, we just need to clone the repository. If you haven’t used GitHub for a bit, the clone line is stated for you on the repo’s front page under the code tab:

So now we just clone in our shell:

Now I can call Virgil from anywhere. Given where the bin directory sits, in my case I add:

export PATH=$PATH:~/Projects/TheNewStack/virgil/virgil/bin


That’s it.

So let us write the inevitable first Virgil program:

def main() { 
 System.puts("Hello World!\n"); 
}


We should just be able to run this with the in-built interpreter, like so:

Excellent. But if we want to compile it, we need to target the host architecture. Fortunately, if we run on the host, Virgil will simply detect it:

Note that we now have an executable sitting in the directory. (While Warp shows us the timings, take into account the age and state of my 2015 Macbook Pro before taking too much from the speed.)

Running the compiled code gives us:

While running this on subsequent occasions is much quicker, we obviously are not doing enough work to think much about timings.

Remember, I’m running on a native platform, otherwise, Virgil will give me a JAR. To test the cross compiler, let us quickly force it into making a JAR:

And…

So we can compare this favorably with the cross-compiling capabilities we saw in Zig.

Let’s do a bit more. We get mutable types with var:

// 'var' introduces a new, mutable variable 
var x: int = 0;  // with type and initializer 
var y: int;      // with type, initialized to default 
var z = 0;       // with initializer and inferred type


Immutable variables are introduced with def in exactly the same way.

Here is a method, which takes in an integer and returns an integer:

// recursive computation of fibonacci sequence 
def fib(i: int) -> int { 
 if (i <= 1) return 1; // base case 
 return fib(i - 1) + fib(i - 2); // recursive calls 
} 

def main() { 
 System.puti(fib(6));
}


The above prints the integer 13, as it should.

We could put this in a class, and save it as fib.v3:

class Fib { 
  def min: int = 1; 
  def calc(i: int) -> int { 
   if (i <= min) return min; // base case 
   return calc(i - 1) + calc(i - 2); // recursive calls 
  } 
} 

def main() { 
 var obj = Fib.new(); 
 var z = obj.calc(6); // method call 
 System.puti(z); 
 System.puts("\n"); 
}


Algebraic Data Types

Specific to Virgil is the Algebraic Data Type. These seem to be an intriguing mix of polymorphism, switch and enums which are used to make complex structures, without the expense of objects.

As an example, here is a Travel type:

type Travel { 
  case Walk { 
    def distance(hoursTravelled: int) -> int { 
      return hoursTravelled * 3; 
    } 
  } 
  case Cycle { 
    def distance(hoursTravelled: int) -> int { 
      return hoursTravelled * 16; 
    } 
  } 
  case Drive (speed: int) { 
    def distance(hoursTravelled: int) -> int { 
      return speed * hoursTravelled; 
    } 
  } 

  def distance(hoursTravelled: int) -> int; // top-level method declaration 
} 

def main() { 
  var walking = Travel.Walk; 
  var busTrip = Travel.Drive(50); 
  var cycling = Travel.Cycle;  
  var totalDistance = walking.distance(1) + busTrip.distance(2) + cycling.distance(1); 
  System.puts("Today, We covered a distance of "); 
  System.puti(totalDistance); 
  System.puts(" miles in 4 hours.n"); 
} 

// Today, We covered a distance of 119 miles in 4 hours.


The type is an immutable structure, with a top-level method attached. They can be deep compared for equality; the case and the parameter would have to match. In other ways, the case values act like enumerated types as well (enums).

There Be Pointers

On native targets, Virgil does have pointers — but these are inherently unsafe. This is intentional, as the language has to have full control at the byte level. After all, we are reminded that “Not only the entire compiler but also the entire runtime system and garbage collector are written in Virgil!”

Pointers are implemented as untyped, raw byte addresses. They seem to work like they do in C. Their size is the same as the target type, you can add and subtract them, compare them, etc.

The instruction load and store are used to access memory in dangerous ways:

var p: Pointer; 
var x: int = p.load<int>(); // load an int (i32) from {p} 
p.store<int>(33); // store an int into {p} 

var y: string = p.load<string>(); // unchecked, raw reference load, dangerous!


This obviously implies a different relationship between developer and host system than you get with high-level languages. Just remember, if you gaze long into an abyss, the abyss also gazes into you (and causes untraceable bugs).

Conclusion

While the language is still young, it does have a practical feel. I have by no means covered all the features, as this is just an introduction. It is clearly an active project. You can write in an object-orientated manner or a functional one. This sometimes is referred to as multi-paradigm, but in the end, this is yet another strong lightweight project that keeps low-level development healthy.

The post Introduction to Virgil, a New Language by Wasm’s Co-Creator appeared first on The New Stack.

Virgil exemplifies a trend towards languages targeted at "lightweight high-performance systems" with strong cross-compilers. We test it out.

Viewing all articles
Browse latest Browse all 80

Trending Articles