Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Test

Language constructs

There are a variety of constructed abstractions in a language that not only give it logical/expressive power, but help guide and define the user-friendlyness of the language. A good example of this is Rust’s lifetime and borrowing rules. While these rules make it possible to write and express programs that would be difficult to keep track of in a language like C, it also steepens the language’s learning curve.

In designing Wright, I want to make a wide variety of language constructs available to the user to help make the language more elegant, without making it too much more difficult to use. In designing these language constructs, a few principles should be kept in mind.

  1. Wright aims to be a relatively simple, easy to use language.
  2. Wright aims to protect the user, to the greatest extent possible, from writing buggy code.
  3. Wright aims to show an appropriate amount of the language’s internals. Users should be able to reason about how their code runs and what allocates, or doesn’t.
  4. Wright is a multipurpose programming language. No one paradigm or style should be expressly promoted over others.

With those principles in mind, we can start to establish a set of features to guide the language’s design.

  1. Wright is strongly typed, and will infer types wherever possible.
  2. Wright is garbage collected. – I changed my mind on this – Wright will have lifetimes and borrowing similar to Rust.
  3. Wright has traits.
  4. Wright has enumerations.
  5. Wright has tagged unions.
  6. Wright has classes record types.
  7. Wright has type aliases.
  8. Wright has constraints (to be discussed further).
  9. Wright has inheritance for traits, enumerations, tagged unions, and constraints.
  10. Functions are first class language constructs in Wright.
  11. Wright does not have macros – most macro-style meta programming should be achievable with generics.
  12. Wright has abstract types – representation & implementation can be dependent on the generic used.

On Constraints:

Wright will be one of the few multipurpose languages that I know of to use constraints. Constraints can be a very powerful tool for logical induction. They allow a programmer to define and check parts of their program at compile time. Wright constraints will be invokable both at compile time and at runtime. There may be some exceptions if we ever decide to allow definition of compile-time only (const constraint) constraints. Constraints will be strongly bound to a type, but that type may be generic (so constraints on lists and arrays will be possible). Constraints will act very similarly to functions, carrying zero sense of state or instantiation like a class might.

Note

This document is a work in progress, and may be changed or updated further at a later date.

User defined optimizations

One of the hardest things for me to reconcile as I build this language is how to make it high-level, while still providing the ability to do relatively low-level things. I would make it completely low-level, however Rust already exists as a well-liked, mature, production-ready, memory-safe language with many of the same features I hope to build into Wright. Building Wright as another low-level language with a borrow checker and functional programming elements would not only make it completely derivative of Rust, but also introduce many of the same drawbacks that Rust has in terms of expressing Futures & other complex memory-related types and in terms of learning-curve (especially around the borrow checker).

In order to do both, the vast majority of programming in wright will be covered under a garbage collector. Programmers will write classes, enums, and unions, without ever thinking too hard about memory allocation or management.

… TBD

Exposing Optimizations to Users

This one is several months later than all the other ones, after the project once again stalled out with me getting busy with work and life. I’ve been dwelling on where I want this project to go lately though, and figured I’d jot down some thoughts, both so that I don’t forget but also so that others can see the evolution of this looking back on it perhaps (though I kinda doubt that anyone will ever read most of this stuff lol). For a while now, part of what’s killed my motivation to work on this has been that it’s felt too close to Rust. Sure they can share similarities, and it’s not uncommon for languages to be inspired by one another, but if this ends up being effectively the same language as Rust (from a user perspective) then that means I haven’t done anything interesting or new.

With that in mind, I’ve been considering what I like and dislike about Rust (and other languages) and have come up with the following. Many languages hide their optimizations behind interfaces that are easier to abstractly understand. If you’re lucky, some may mention them somewhere in some documentation, but it’s very rare that you’re ever able to directly interact with the optimizer. The surface of the language is abstract concepts like structs, classes, interfaces, functions, etc. and then all of the stuff that takes you down to the hardware is like magic glue. But what if it didn’t have to be that way. What if you wrote (and/or could read) the glue yourself when you wanted to, without getting too messy.

An example of what I’m talking about comes with rust’s optimizations around Options containing references. Options are special-cased such that for all other types, they hold a byte of data storing the discriminant, and then store the actual contents of the option adjacently. The exceptions to this are when using Options containing references or NonZero<Integer> types. In those cases, it stores the None variant just as a 0 or a null pointer (also a 0) and the type size remains unchanged. This is not something that you could do for a type you wrote – If you had a type you wrote that could compress types containing it into a smaller size, it would be cool if there was a way to express that to the compiler without mucking with bytes manually, or using gross and unreliable mem::transmutes (in rust at least).

We also must remember the relationship languages have with each other – it is very rare that a large project will truly all be in one language. Many rust projects use wrapper crates and generated bindings around C libraries, and the same is true for other languages. Generating and maintaining those bindings can be tricky, and is non-trival – the difference in ABI sometimes causes extra instructions to be generated in some places, which can add up if a function is being called tens of millions of times a second.

What I come to at the end of this is an idea for a language that is explicitly aware of everything. This means the hardware it’s running on (to the extent possible), the ABI of external libraries it’s using, and the specific memory layout and calling conventions for each of the structures and functions defined by the user.

If this is sounding lower-level than the language described by previous design notes, that’s because it is! Rust has already for years been moving towards being more of a C++ replacement than a C replacement, which it does well. This language would live down closer to C, llvm IR, and maybe zig (which I’ve still never used at time of writing). The difference with those languages I’m hoping to achieve here is both a better user experience (C especially has years of history baked into its spec), and the ability to have the language get out of your way more when you don’t need the lower level control.

Threads

For many languages, threading can be a point of tension. When to use it (especially now that single-threaded async is more common), how to use it, and how to optimize it are all common issues.

In building wright, I decided it would be best to separate async and syncronous code/threads to avoid unnecessarily compiling/linking/running an async runtime to manage futures.

The Backend(s) of the Wright Compiler and Interpreter.

I have had a many thoughts, opinions, and different stances on what I wanted to build for Wright’s backend. I have changed my mind more times than I can count, and I’m certain I will continue to change my mind on this several more times.

So far it appears there are a few main target options:

LLVMCraneliftJVM / Java BytecodeBespoke bytecode compiler & interpreterBespoke bytecode compiler & transpiler
OutputMachine codeMachine code.class fileCustom bytecodeCustom bytecode & transpiler targets
Targetsvery manyx86_64, aarch64 (ARM64), s390x (IBM Z), riscv64JVMAnything that the rust based interpreter can run onvery many (assuming transpile to LLVM)

Right now I’m largely tempted to target both a bespoke bytecode interpreter (perhaps in addition to a transpiler) and LLVM. I like the idea of compiling to Cranelift as well, but the additional work for it may be more than it’s worth. Compiling to the JVM would be cool for interoperability with Java/Scala/Kotlin/etc programs, but my language is so different from them that there would be a significant amount lost in translation from Wright to the JVM. I will start with the bespoke interpreter/transpiler.