LanguagesArchitecture

Version 0.2 is now out of beta, and officially released!

As we all know, Vale is a programming language that aims to be fast, safe, and easy to use. It's a challenging goal, and version 0.2 lays a solid foundation for the next steps in the endeavor.

This version has been prioritizing ease of use, enabling much larger Vale projects. We can now comfortably create large programs such as this roguelike game. With this version, Vale has graduated from a prototype to early alpha status.

You can find the version 0.2 binaries here.

Try it out, and if you like what you see, consider sponsoring us on GitHub! This is a free project for the good of all, and with your help, we can continue working on it for longer. 0

Below are the new features in 0.2, enjoy!

Higher RAII

This release enables Higher RAII, a form of linear typing that helps the compiler enforce that we "never forget to call that function".

For a real-world use case, check out the recent article Higher RAII, the pattern that saved me a vital 5 hours in the 7DRL Challenge.

To use Higher RAII, just add a #!DeriveStructDrop above your struct or #!DeriveInterfaceDrop above your interface, to prevent it from automatically defining a drop function (the ! means "don't").

Instead of having an implicitly-called drop function, this example has a destroyShip function that takes a boolean parameter and returns an integer.

Since there is no drop function for the struct, Vale will never automatically destroy the object. The user must explicitly call the destroyShip function.

This pattern can be used to enforce that we don't forget to call a certain function (like destroyShip) at some point in the future.

vale
#!DeriveStructDrop
struct Spaceship {
name str;
fuel int;
}


exported func main() {
ship = Spaceship("Serenity", 2);
// ship is an owning reference

println(ship.name);

fuel = (ship).destroyShip(true);
println("Fuel was {fuel}.")

}


func destroyShip(
s Spaceship,
print bool)

int {
[name, fuel] = s; // Deallocates ship
if print {
println("Destroyed {name}!");
}

return fuel;
}
stdout
Destroyed Serenity! Fuel was 2.

Higher RAII can be also be used for more powerful type-state programming 1 than any other imperative language. Try it out!

Side Notes
(interesting tangential thoughts)
0

We're particularly excited to prototype the region borrow checker, which could help Vale approach C++'s speed by getting rid of the vast majority of our memory safety overhead, and enable concurrency features like described in Seamless, Fearless, Structured Concurrency.

We've been looking forward to this version for a long time!

1

Type-state programming is a way to use the compiler to ensure that specific functions are only callable when the object is in a certain state. This is all tracked via the type system, with zero run-time overhead.

Concept Functions

Vale now supports concept functions, a way to specify that certain functions must exist for given generic parameters, without making them implement any traits.

For example, List's clone function can require that there exists a clone function for its elements:

vale
func clone<T>(self &List<T>) List<T>
where func clone(&T)T
{
result = List<T>();
foreach x in self {
result.add(clone(x));
}

return result;
}

This is often much easier than what we normally see in mainstream languages, which is to require an explicit implementation for the type.

Read more at Concept Functions!

Const Generics

We're happy to announce that Vale now has "const generics", where generic parameters can contain not only types (like the T in List<T>) but also integers, booleans, and other types.

Now, instead of repeating classes...

vale
struct Vec2<T> {
elements [#2]T; // 2-element array of T
}

struct Vec3<T> {
elements [#3]T; // 3-element array of T
}

struct Vec4<T> {
elements [#4]T; // 4-element array of T
}

exported func main() {
v = Vec3<int>([#][3, 4, 5]);
...
}

...we can have one:

vale
struct Vec<N int, T> {
elements [#N]T;
}

exported func main() {
v = Vec<3, int>([#][3, 4, 5]);
...
}

We also now have the compile-time spread operator .. for structs, which enabled us to implement tuples in the standard library, instead of in the compiler.

We have some interesting things planned for const generics, see Const Generics and the Compile-Time Spread Operator for more!

Removing Let and Let Mut

The 0.2 release removes the let and let mut keywords, making our syntax cleaner and more readable.

Before:

vale
func main() {
let a = 3;
let b = 3;
let c = 3;
let mut d = 3;
d = 7;
println(d);
}
stdout
7

After:

vale
func main() {
a = 3;
b = 3;
c = 3;
d = 3;
set d = 7;
println(d);
}
stdout
7

Read more at On Removing Let and Let Mut!

Faster Compile Times

This version of the compiler is 3x as fast as the previous version, after we did some optimizations:

  • Memoized all of our .hashCode() calls, and used interning to speed up .equals() calls.
  • Wrote a newly optimized generics solver, which operates on primitives instead of heap-allocated classes.
  • Migrated from Scala parser combinators to a hand-written recursive descent parser.

We also updated to LLVM 13 under the hood.

Modules

Vale has a new take on modules, which is as easy to use as Java's packages while providing the flexibility of Rust's crates.

Check out the Modules Guide to see them!

We make heavy use of our module system in our new standard library, too.

Downcasting

We can now downcast interfaces to a specified struct, using the as function.

In this example, we're downcasting a Ship interface reference to a FireflyShip struct reference.

Downcasting will always return a Result<T, E> where T is the struct type, and E is the interface type. In this example, it returns a Result<FireflyShip, Ship>.

It will either be an Ok containing a FireflyShip, or if the ship wasn't actually a FireflyShip it will contain an Err with the original Ship.

Here, we're calling .expect() because we know it is indeed a FireflyShip.

vale
interface Ship { }

struct FireflyShip {
name str;
}

impl Ship for FireflyShip;

exported func main() {
// Implicitly upcast to Ship
ship Ship = FireflyShip("Serenity");

// Can downcast with as:
fireflyShip =
ship.as<FireflyShip>().expect()
;

println(fireflyShip.name);
}
stdout
Serenity

Foreach and Break

Vale now supports foreach loops, and the break statement as well.

See the Collections Guide for more!

vale
exported func main() {
l = List<int>().add(1).add(3).add(7);
foreach [i, x] in l.entries() {
println(i + ": " + x);
if i == 1 {
break;
}

}

}
stdout
0: 1 1: 3

FFI

When we use the exported keyword on a function, Vale will generate headers so that C can call that function. We can also call C functions by declaring an extern function.

See Externs and Exports in the guide for more!

Here, main is calling out into an external C function that reads an int from the keyboard.

The extern func readInt() int; is telling Vale that the C code will declare a function named readInt.

vale
exported func main() {
i = readInt();
println("User entered: " + i);
}

extern func readInt() int;
c
#include <stdint.h> #include <stdio.h> #include "mvtest/readInt.h" extern ValeInt mvtest_readInt() { int64_t x = 0; scanf("%ld", &x); return x; }
stdin
42
stdout
User entered: 42

Vale's approach to FFI is unusual. Vale regards its own objects as in a separate "region" of memory than C's objects, and doesn't directly expose Vale objects to C. 2

This separation enables some pretty interesting upcoming features. "Separated FFI" will allow us to be certain we're not accidentally corrupting our Vale objects from C. "Perfect Replayability" will then rely on that memory-safety and determinism to allow us to easily reproduce any bug from a previous run. 3

What's next for 0.3

Now that we have a usable and solid language to build on, version 0.3 will be focused on prototyping our flagship feature, the region borrow checker.

Also keep an eye out for our next series of articles, which paint a more holistic picture of how Vale will combine generational references, "life cells", and the region borrow checker to provide maximum speed, memory safety, while still remaining easy to use.

Here's to a great year, and a great version!

Thanks for reading, we hope you enjoyed this article! And if you're impressed with our track record and believe in the direction we're heading, please consider sponsoring us on github:

With your support, we can work on this full-time, and bring speed and safety to more programmers than ever before!

2

This may be relaxed in the future, using unsafe blocks.

3

Stay tuned for the next two articles, on Separated FFI and Perfect Replayability. If you want early access, feel free to swing by the discord server and ask!

About the Vale Language Project

The Vale Language Project is not just about making Vale, it's also about exploring, discovering, and publishing new programming language mechanisms that enable speed, safety, and ease of use.

The world needs more exploration here! Currently, most programming language research is in:

  • High-overhead languages involving reference counting and tracing garbage collection.
  • Complex languages (Ada/Spark, Coq, Rust, Haskell, etc.) which impose a higher complexity burden on the average programmer.

These are useful, but there is a vast field of possibilities in between, waiting to be explored!

Our aim is to explore that space, discover what it has to offer, and make speed and safety easier than ever before.

In this quest, we've discovered a lot of new techniques:

These techniques have also opened up some new emergent possibilities:

  • Seamless concurrency, the ability to launch multiple threads that can access any pre-existing data without data races, without the need for refactoring the code or the data.
  • Object pools and bump-allocators that are memory-safe and decoupled, so no refactoring needed.
  • Separated FFI, which allows us to call into C without risk of accidentally corrupting Vale objects.
  • Deterministic replayability, to record all inputs and replay execution. Goodbye races and heisenbugs!
  • Higher RAII, a form of linear typing that enables destructors with parameters and returns.

We also gain a lot of inspiration from other languages, and are finding new ways to combine their techniques:

  • We can mix an unsafe block with Separated FFI to make a much safer systems programming language!
  • We can mix Erlang's isolation benefits with functional reactive programming to make much more resilient programs!
  • We can mix region borrow checking with Pony's iso to support shared mutability.

...plus a lot more interesting ideas to explore!

The Vale programming language is only one combination of the features we've found. Our goal is to publish all the techniques we've found, even the ones that couldn't fit in Vale, so that other languages can make strides in this area.

Our medium-term goals:

  • Publish the Language Simplicity Manifesto, a collection of principles to keep programming languages' learning curves down.
  • Publish the Memory Safety Grimoire, a collection of "memory safety building blocks" that languages can potentially use to make new memory models, just like Vale combined generational references and scope tethering.
  • Prototype the Region Borrow Checker in Vale, to show the world that shared mutability can work with borrow checking!
  • Prototype Hybrid-Generational Memory in Vale, to see how fast and easy we can make single ownership.

We aim to publish articles biweekly on all of these topics, and inspire the next generation of fast, safe, and easy programming languages.

If you want to support our work, please consider sponsoring us on GitHub!

With enough sponsorship, we can:

  • Work on this full-time.
  • Turn the Vale Language Project into a 501(c)(3) non-profit organization.
  • Make Vale into a production-ready language, and push it into the mainstream!