Unlocking the True Power of Const with ValueScript

voltrevo

Andrew Morris

Posted on April 10, 2023

Unlocking the True Power of Const with ValueScript

On the surface, const allows you to clarify that some variables in your JavaScript program don't change:

const x = 3;
x++; // TypeError 💥
Enter fullscreen mode Exit fullscreen mode

However, this breaks down as soon as you use a non-primitive value:

const x = [1, 2];
x.push(3); // Sure, why not?
Enter fullscreen mode Exit fullscreen mode

There's two ways of thinking about why JavaScript allows this:

  1. const is about which object is bound to x. When you push 3 into the array, x still points to the same object. The array changed, but x did not.
  2. JavaScript has no choice but to allow this, because the current const is already the best consistent behavior it can provide.

To see (2), consider:

const x = getAThing();

const valueBefore = x.value;
someOtherFunction();
const valueAfter = x.value;

valueBefore === valueAfter; // true?
Enter fullscreen mode Exit fullscreen mode

(Complete example with false result.)

It is simply too difficult in JavaScript to determine whether x.value is changed by someOtherFunction(), and therefore it doesn't know whether (deep-style) const has been violated.

(Actually, it's probably fine for simple demonstrations like this one, but this isn't true of the much more complicated examples that quickly emerge in real programs.)

Alternatively, you could also track the const status of the memory itself at runtime, but this would add runtime overhead every time you used const. Large objects could also be composed of many smaller objects with varying const requirements, potentially causing a lot of confusion of its own.

If you want to say const like you mean it, then you might want to avoid writing functions like getAThing() and someOtherFunction() that make changes to shared objects. However, as programs scale, it's very difficult to prevent this. Additionally, there's the mental cost of pursuing this approach.


Enter ValueScript. It's a dialect of TypeScript (and JavaScript) with value semantics. Everything is a value like the 3 from the first example. A 3 is a 3 is a 3. You can increment a variable to make it 4, but that changes the variable. Turning the actual number 3 into 4 would be nonsense.

In ValueScript, the same is true of objects. A [1, 2] is a [1, 2] is a [1, 2]. You can push 3 into it to get [1, 2, 3], but that changes the variable. Turning the actual [1, 2] into [1, 2, 3] would be nonsense.

This is why ValueScript gives you a type error for the second example:

const x = [1, 2];
x.push(3); // TypeError 💥
Enter fullscreen mode Exit fullscreen mode

Knowing whether a method changes the class instance is still a bit tricky, but it's manageable in ValueScript because changes are always local. ValueScript uses a const_subcall instruction for .push, which ensures this cannot be changed inside the call.

ValueScript is able to keep things local by using reference counting and copy-on-write instead of shared references. Otherwise, you could also violate const like this:

export default function main() {
  const leftBowl = ["apple", "mango"];

  let rightBowl = leftBowl;
  rightBowl.push("peach");

  return leftBowl.includes("peach");
  // JavaScript:  true  (even though leftBowl is const)
  // ValueScript: false
}
Enter fullscreen mode Exit fullscreen mode

In JavaScript, leftBowl and rightBowl are the same object, so putting a peach into rightBowl also puts a peach into leftBowl, even though leftBowl is const.

ValueScript doesn't link them together this way. rightBowl can be modified freely, but this has no effect on leftBowl. This is good, because leftBowl is const.

This ability to control variables with const also has benefits beyond programming clarity. It's extremely beneficial that the compiler is able to know when variables change and when they don't. In particular, there's two other places where variables are effectively const even when the const keyword hasn't been used:

  1. Variables that are captured into functions
  2. Imported variables

In these situations, the compiler also uses const_subcall for method calls to ensure they stay constant.

These extensions of const requirements and the enhanced ability to know when variables are constant will also be super beneficial for the future of static analysis in ValueScript. It means the analyzer will know how your program works more often, which will help it optimize, find issues, and reduce binary sizes.

ValueScript is in early development, but it has an extensive playground demonstrating a large subset of JavaScript. It's also open source.

💖 💪 🙅 🚩
voltrevo
Andrew Morris

Posted on April 10, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related