LanguagesArchitecture

We're going to commit a cardinal sin today and talk about syntax design! 0

There are three main alternatives for variable declaration:

  • Specifying whether the variable can change, such as:
    • Javascript's let x = 4 vs. const x = 4
    • Rust's let x = 4 vs. let mut x = 4
    • Java's int x = 4 vs. final int x = 4
    • Swift's let x = 4 vs. var x = 4
  • Not specifying whether the variable can change, such as:
    • Old Javascript's var x = 4
    • Go's x := 4 (though it's not really a keyword per se)
  • Leaving off keywords completely, such as:
    • Python's x = 4

The previous version of Vale used the first option, let and let mut. 1

However, it recently changed to a fourth option, which was so nice that it became the default behavior in version 0.2.

Side Notes
(interesting tangential thoughts)
0

It's such a travesty to be talking about something as mundane as syntax during the week we're prototyping deterministic replayability!

1

Vale is a language that aims to be fast and memory-safe, while still being easy to learn. This syntax change helps with that!

The Fourth Option

Most languages can't just use x = 4, because that already meant something. That's the assignment statement, which modifies an existing variable that we already declared.

And alas, Python tried combining those two statements. It didn't go well. 2

However, there's another option here: let's change the assignment statement!

Instead of this:

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

We can have this:

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

In other words, x = 3 declares a variable, and set x = 42 assigns it.

It was odd at first, but after using it for a few weeks, we think this is a huge improvement.

Syntax design can be a tricky endeavor. When exploring new syntax, we had to suppress the knee-jerk unfamiliarity avoidance, and actually experiment. Without experimenting, it's easy to get stuck with what's familiar, even if there are better options.

2

This leads to problems; if you rename your x = 1 declaration to y = 1, but forget to modify x = 42 assignment below, you now accidentally have two variables!

Why We Like It

To our great surprise, we've found that our codebases have a lot more declarations than assignments, so it makes sense to require the extra keyword on assignments because they're rarer.

We sampled three Vale projects. One had 111 declarations, and only 35 assignments. That's only 21% assignments! The other two were even lower, at 20% and 6%.

This isn't just Vale either. A randomly chosen Rust library, Rocket, had about 8%. 3

When did this happen? We used to assign variables all the time!

3

This is approximate; I used let(\s+mut)? (4437 results) and ^\s*[\w\[\]\.]+\s*[\+\-\*\/]?=\s+ (351 results) and which may have missed some corners, such as lambdas and mutating call returned values.

Parsing with regular expressions is fun!

We suspect moving towards more declarative patterns has contributed to this shift. Let's see some examples!

First, we no longer need assignment to return a value from an if-statement. Compare these two snippets in Scala:

scala
var weight = 0 if (i_am_a_potato) { weight = 42 } else { weight = 73 }
scala
val weight = if (i_am_a_potato) { 42 } else { 73 }

Second, our for-loops became foreach loops, removing that pesky i++. Compare these two snippets in Java:

java
for (int i = 0; i < ships.size(); i++) { ships[i].launch(); }
java
for (Spaceship ship : ships) { ship.launch(); }

Third, we loop over collections less, and now use specialized methods like find a lot more. Compare these two snippets in Javascript:

js
let foundIndex = -1; for (let i in ships) { if (ships[i].name == "Firefly") { foundIndex = i; } }
js
let foundIndex = ships.findIndex( x => (x.name == "Firefly"));

Nevertheless, we seem to have a lot less assignments nowadays, so it makes sense to have the extra keyword on the rarer statement, not the more common one.

Another Factor

Note how the declaration doesn't specify whether we can change the variable, like let vs. let mut.

One of the benefits of that distinction was that we could easily know whether the variable could change in the future.

We actually kept that distinction for a while; we used the ! symbol, such as d! = 3.

However, we decided to not require it for local variables, because the set keyword makes assignment more noticeable than it was before.

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

For example, if we want to know whether the variable d = 3 can change, we just need to look for a set d = 7 keyword somewhere in the function, which is much more noticeable now than the previous assignment syntax was.

However, that reasoning doesn't apply to structs. A struct's members might be modified from various far-flung files in our codebase.

vale
struct MyStruct {
x! int;
}

For that reason, we kept the ! on struct members. Other languages do this as well, such as OCaml, and it seems to be a pretty good balance.

Conclusion

Of course, we can't generalize too much. Every language is different, so we can't say that every new language should use this new scheme. Still, newer languages should give it some thought!

Thanks for visiting, and we hope you enjoyed this article!

In the coming weeks, we'll be writing more about our "region borrow checker" which helps eliminate Vale's memory safety overhead, so subscribe to our RSS feed, twitter, or the r/Vale subreddit, and come hang out in the Vale discord!

If you found this interesting, please consider sponsoring us:

With your help, we can write nonsense like this more often!

- Evan Ovadia

Vale's Vision

Vale aims to bring a new way of programming into the world that offers speed, safety, and ease of use.

The world needs something like this! Currently, most programming language work is in:

  • High-overhead languages involving reference counting and tracing garbage collection.
  • Complex languages (Ada/Spark, Coq, Rust, Haskell, etc.) which impose higher complexity burden and mental overhead on the 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 and implemented a lot of new techniques:

  • Generational Memory, for a language to ensure an object still exists at the time of dereferencing.
  • Higher RAII, a form of linear typing that enables destructors with parameters and returns.
  • Fearless FFI, which allows us to call into C without risk of accidentally corrupting Vale objects.
  • Perfect Replayability, to record all inputs and replay execution, and completely solve heisenbugs and race bugs.

These techniques have also opened up some new emergent possibilities, which we hope to implement:

  • Region Borrow Checking, which adds mutable aliasing support to a Rust-like borrow checker.
  • Hybrid-Generational Memory, which ensures that nobody destroys an object too early, for better optimizations.
  • 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.

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 Fearless 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 a novel combination of ideas from the research world and original innovations. Our goal is to publish our techniques, even the ones that couldn't fit in Vale, so that the world as a whole can benefit from our work here, not just those who use Vale.

Our medium-term goals:

  • Finish the Region Borrow Checker, 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.
  • 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.

We aim to publish articles biweekly on all of these topics, and create 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!