Heck: The Basics

In my previous article, I explained the purpose of a language like Heck and the basic idea of what it can do, but what does that actually mean?

Well, there are two main things I stated about this programming language.

First of all, Heck will be a compiled language. This means you will be able to produce extremely fast binaries, but it also means that the language will be statically-typed. I prefer static typing over dynamic-typing for one major reason; if you really think about the purpose of a programming language, you'll realize that it's a just a way to bridge the gap between computers and human understanding.

Fortunately, static typing happens to be helpful for both computers and humans. Obviously, it helps computers because they can know ahead of time how much memory to allocate, how to use data more effectively, and they won't waste time doing type checks, which can slow down your code by a significant degree.

Static typing is also easier on humans. Sure, you might not want to explicitly state data types everywhere (which isn't even a requirement for static typing), and for small programs, it doesn't really matter because you already know the types of your variables, but once things scale up and people have to be able to understand your code, not knowing the type of something can be a serious issue. I've seen countless people complaining when their IDE can't tell them the type of a variable, and they usually end up trying to determine it at run time.

There are still some arguments for dynamic typing, such as the smaller code size, the ability to reuse more code, and that overall, things just seem more simple, but statically-typed languages have shown that you can have all of these things without dynamic typing. You can actually take most, if not all of the good parts of dynamic typing and make them statically-typed, because, after all, when you can understand the code, computers usually can too.

This brings me to the second thing I stated about Heck, which is that its only as complicated as it has to be. When you need it to be, it's a very powerful and low-level language that should try and help you with your memory management in a way similar to Rust, but you can also use it like a scripting language because the compiler is smart enough to figure that stuff out for you. This is helpful because programmers want to be able to use things that are fast and complicated in a simple manner, which is probably the main reason Python is so popular; it lets you use C when performance really matters and then the rest is easy, but as I stated in my previous article, it's a lot easier when you don't have to bridge the gap between two languages.

So, what do I actually mean when I say you can take the good parts of dynamic typing and make them statically-typed? Well, first of all, there are many tricks that modern languages have already demonstrated, such as function templates and the auto keyword, for example. Using these two features alone, the following C++ code demonstrates several aspects of dynamic typing:

#include <string>

template <typename T1, typename T2>
auto add(T1 a, T2 b) {
    return a + b;
}

int main() {
    
    // adds a float and an integer
    auto num = add(1.0, 2);
    
    // adds two std::strings, which have an overloaded + operator
    auto str = add(std::string("hello "), std::string("world"));
    
}

And the above code is functionally equivalent to this valid Heck code:

function add(a, b) {
    return a + b
}

let num = add(1.0, 2)

let str = add("hello ", "world")

What's nice about these features is that you never actually have to specify any data types, but the compiler can still deduce them without actually running the code.

The problem with C++ is that you have to type extra code to specify that it's a function template and that it takes two type arguments, but a smart enough compiler can assume that's what you mean when you omit the return and argument types.

There's only one limitation to this, however. You can't really store collections that contain mixed types, at least not directly, but I think something is wrong with your code if you need this functionality. Encapsulating mixed types in a polymorphic class is much more clean and efficient anyway because it isn't actually using data types as part of your code's logic.

This is a clear demonstration of the philosophy behind Heck. If you don't specify types, the compiler will figure it out for you, but you can also be as explicit as you want.

Another example of this philosophy is how the language deals with object references. Most high-level languages use reference counting to deal with garbage collection, and this has even become a common practice in C++. Reference counted pointers are very powerful and they cover most use cases, which has partially led to the success of langauges like C# and Java, so when creating objects in Heck, it is assumed that you want to use reference counting.

Reference counting is slow, however, and sometimes when performance really matters, you will want to use either a unique reference that can transfer ownership or to just store objects directly on the stack.

Heck will have three different types of references upon its first release, although this is subject to change.

The first type of reference is called a shared reference. Shared references are the default type of reference, and they're reference counted. The average programmer could use Heck perfectly fine using only this type of reference because it works exactly like an object reference in most high-level languages, such as Java, C#, Python, and JavaScript. Shared references can be created simply by using the new keyword:

class Foo {
    
}

let a = new Foo() // a is a reference to a Foo object

The second type of reference is called a unique reference, and the object that it references can only have one owner at a time. You can create it using the unique keyword:

class Bar {
    function f() {
    }
}

let b = new unique Bar()

let c = b // transfer ownership to c

b.f() // compiler error: ownership was transferred to c

The final type of reference is the temporary reference. These references have no performance overhead; they work just like raw pointers in C or C++, but the compiler will restrict their use beyond the lifetime of the object they are referencing.

Here is an example demonstrating the use of temporary references in relation to the lifetime of the objects they reference:

class Baz {
    // The compiler knows that a Baz object cannot have a longer lifetime than the value it's referencing
    int& value;

    // since the "value" argument gets assigned to the instance variable "value," their lifetimes must be the same
    function Baz(int& value) {
        this.value = value;
    }
}

let num = 5

// initialize a with a reference to num (OK)
let a = Baz(num)

{
    let num2 = 6
    
    // reassign a to a new Baz object that references num2 (COMPILER ERROR)
    a = Baz(num2)
}

When the Baz object "a" is declared, it's initialized with a reference to the variable "num." The compiler knows that the Baz object could keep a reference to "num" for it's entire lifetime, but since both variables are declared in the same scope, they have the same lifetime, and the code will compile.

The curly brackets create a new scope, which means "num2" will go out of scope before "num," giving it a shorter lifetime. If "a" is reassigned to reference "num2," it will be referencing invalid memory once "num2" goes out of scope and gets destroyed, therefore, attempting to do so will result in a compiler error.

The above example only shows temporary references being used with objects on the stack because objects that are allocated on the heap can have much more complicated lifetimes. There are ways to perform safety checks for objects on the heap, but these might not be present in the initial release of Heck. It is undecided if a temporary reference to a shared or unique reference should reference the object itself or the actual reference. If the former ends up being the case, the compiler will need to determine the minimum lifetime of the object that is being referenced. For example, if a shared reference is created somewhere on the stack, the lifetime of the object it references may be extended once the shared reference gets passed to a function.

These references will likely slow down compile time due to the extra safety checks, but they will give you the speed of raw pointers without any risk.

Fortunately, as previously stated, if you feel like you aren't experienced enough to use temporary references, you can always fall back on shared references. Also, the syntax and behavior of the different types of references are subject to change, so if you have any suggestions for how references should be dealt with - or any other aspect of the language for that matter - please message me or leave a comment somewhere.

Obviously, this article doesn't fully demonstrate how to use Heck, but hopefully you'll understand the purpose of the language as well as some of its capabilities. I plan on announcing more updates in this blog as I progress on my implementation of Heck, so please stay tuned!

Comments

Popular Posts