Course Notes

Table of Contents

This is very rough draft set of notes (more of a collection of thoughts) for the course CAS CS 392 at Boston University. It's primarily a supplement to The Rust Programming Language (RPL). It's full of typos and dumb jokes.

1. What is this course?

We usually learn PLs the "wrong" way: throw spaghetti code at the wall and see what sticks.1 This course is an experiment in learning a PL the "right" way: give an airtight specification of a PL using the mathematical framework of PL theory.

Rust is an excellent testbed for this kind of experiment. It's a PL that's gaining in popularity but has some bizzaro features in its type systems2 that turn out to be incredibly useful for addressing some of the more egregious bugs in low-level programming. Add to that the fact the theoretical underpinnings of Rust are simultaneously very old (it's based on linear logic, an innovation of Jean-Yves Girard from the 80s, based on ideas that came long before it) and very young (the formalization of Rust-proper postdates its own conception and popularity, as evidenced by the publication year (2021) of the paper we'll use in the second half of the course). This gives us a wide range of sources to better understand what the heck is going on with this language.

The points is not to learn Rust, at least not entirely.3 The point is to understand the core of Rust. Per the previous paragraph, this means learning a bit of mathematics. It also means dipping into research from this decade, which means learning to read academic papers (more of a skill than you might expect). And since we're learning a PL, we might as well build something with it. The proverbial "they" say that you don't know a PL until you've done a project in it. The less-proverbial "they" might say you don't know a PL until you understand it's formal specification. We'll kill two birds with one stone4 and build an interpreter in Rust for a subset of Rust. So our rough plan:

  • learn enough Rust to be dangerous
  • learn to read/practice reading research papers in PL, so that we can read a paper which gives a formal specification of a subset of Rust (we'll do this together as a group)
  • Test our knowledge by implementing that subset of Rust in Rust

Now, admittedly, I'd like to believe that's all the motivation we'll need. If you're interested in this course, I'd hope you also have some iota of preexisting interest in Rust. But it is my duty to (begrudgingly) motivate why you should learn something interesting.

  • For those interested in PL: Linear types, affine types, types for concurrency, these are current research areas. Rust is something of a success story, one of those rare instances of a PL becoming popular for being both interesting and usable. If you took CS320, it's one thing to give a formal specification of a clean functional PL, applying these ideas to Rust and friends will be a formative challenge.
  • For those interested in programming: I am of the opinion that it's always valuable to learn a new language paradigm, but maybe that's not enough motivation──Rust is ranked 14th according to the December 2024 TIOBE index with one of the most generally uptrending graphs in the top 20. It's being adopted by large companies like Amazon and Microsoft, but also smaller companies, anyone who needs safe low-level code. Rust is an ahead-of-time compiled language; it's good to have one of those in your back pocket, and I think Rust is one of the more interesting choices (over, say, C).
  • For those interested in systems: I can't pretend to really know what's going on in the world of systems, but Rust touts itself as having a lot of potential in this area. It may be worthwhile to understand it, even if just to be able to adequately respond to the hype.
  • Everyone else: I dunno, good luck. If you're not one of the above, I honestly don't see any reason why you should learn Rust.

Enough preamble, let's Rust.

2. Set Up

Set up and installation of Rust is comparatively easy. We'll primarily be using Cargo, a build system for Rust akin to OCaml's Dune (but quite a bit better). You should follow the rustup installation instructions in RPL for your OS.

Note: If you're running Windows, then I'll assume that you're using WSL. You're welcome to try to install directly, but I will not help you troubleshoot. Not so much because I don't want (also, I don't) but because I don't really know how.

If you'd like, you can go through the Hello, World! section of RPL, but we won't be using the rust compiler directly in this course. Instead, we'll follow RPL's recommendations on project hygiene and always assume a Cargo project. You should go through the Hello, Cargo! section of RPL (we'll also go over it in lecture).

2.1. Cargo

Cargo is a build system for Rust. As usual, you can use

cargo --help

To see a list of commands for Cargo. The main one's we'll need:

  • cargo new project_name: create a new Cargo project (and directory) called cargo_name
  • cargo build: Compile the project
  • cargo run: Run the executable built by cargo build (rebuilds first if necessary)
  • cargo clean: Cleans up build files (usually not necessary, but a good first step when troubleshooting an error)

You should go through the Hello, Cargo! section of RPL to practice this process.

3. The Basics

This is a collection of concepts that are common to all PLs. I expect that you're already familiar with all of these concepts. The goal here is to provide a more careful analysis of these concepts, primarily with respect to syntax. Each section begins with a BNF specification of the Rust syntax. These specifications are approximations, but for simple programs are sufficient.

3.1. Variables and Mutability

Grammar:

<var-decl>  ::= let <var-ident> = <expr>
              | let mut <var-ident> = <expr>
<assign>    ::= <var-ident> = <expr>
<var-ident> ::= ; snake_case ;

Variables are immutable by default. This is generally safer (mutability is the root of much evil). If you attempt to mutate an immutable variable, you'll get a compiler error. You can reuse (i.e., shadow) variable names with declarations.

Example:

let x = 1;         // assign x to the value 1
assert_eq!(x, 1);  // assert that x is equal to 1
                   // (also, note the comment syntax)

We use the mut keyword to declare mutable variables, which are allowed to be reassigned/mutated.

Example:

let mut x = 1;
x = 2;
assert_eq!(x, 2);

3.2. Constants

Grammar:

<const-decl>  ::= const <const-ident> : <ty> = <expr>
<const-ident> ::= ; SCREAMING_SNAKE_CASE ;

Constants are like immutable variables except that:

  • they are declared with the const keyword
  • by convention, they are named using SCREAMING_SNAKE_CASE
  • they must be type annotated
  • they must be assigned to values computed at compile-time (e.g., no stdin reads)

Constants are primarily useful for code clarity.

Example:

const SPEED_OF_LIGHT : u32 = 299792458;
assert_eq!(SPEED_OF_LIGHT, 299792458);

3.3. Data Types

Grammar:

<ty>          ::= <scalar-ty> | <compound-ty>
<scalar-ty>   ::= <int-ty> | <float-ty> | bool | char
<int-ty>      ::= <sint-ty> | <uint-ty>
<sint-ty>     ::= i16 | i32 | i64 | i128 | isize
<uint-ty>     ::= u16 | u32 | u64 | u128 | usize
<float-ty>    ::= f32 | f64
<compound-ty> ::= <tuple-ty> | <array-ty>
<tys>         ::= ϵ | <ty> | <ty> , <tys>
<tuple-ty>    ::= ( <tys> )
<array-ty>    ::= [<ty>;<expr>]

<int-lit>     ::= ; see docs ;
<float-lit>   ::= ; see docs ;
<char-lit>    ::= ; see docs ;
<bool-lit>    ::= true | false

<exprs>       ::= ϵ | <expr> | <expr> , <exprs>
<tuple-lit>   ::= ( <exprs> )
<tuple-field> ::= ; see docs ;
<expr>        ::= <expr>.<tuple-field>

<list-lit>    ::= [ <exprs> ]
<expr>        ::= <expr>[<expr>]

<lit>         ::= <int-lit> | <float-lit>
               | <char-lit> | <bool-lit>
               | <tuple-lit> | <string-lit>

Rust is statically typed, i.e., the type of every variable is known at compile-time. Rust does some amount of type inference (except for constant, it seems). When in doubt, type annotate variables. It doesn't hurt, and can act as a simple form of documentation.

There are two kinds of data types: scalar and compound. Here's a short outline, most of this should be familiar.

  • Scalar:
    • Integers:
      • i8, i16, i32, i64, i128, and u8, u16, u32, u64, u128
      • The number indicates the number of bits used to represent the integer. The default for type inference is i32.
      • There is also isize and usize used for indexing
      • There are standard literals for decimal, hex, octal, binary, and bytes. Look them up if you need them.
      • Rust's compiler can check for integer overflow in debug mode, but wraps by default for release mode. We're gonna ignore this for now (as if we'll be writing any production code…)
    • Floating-point numbers:
      • f32 and f64. The default for type inference is f64.
      • Represented using IEEE-754 (of course)
    • Characters:
      • type: char
      • example: 'q', '✗'
      • four bytes, represent unicode scalars (characters are weird but we won't dwell on this)
    • Booleans:
      • type: bool
      • two literals: true and false
  • Compound:
    • Tuples:
      • type: (t1, t2,..., tk)
      • literal: (e1, e2,..., ek)
      • accessor: p.i

        Example:

        let tup: (i32, bool, u32) = (2, true, 2);
        assert_eq!(tup.1, true);
        
    • Arrays:
      • type: [t; n] where t is a type and n is a usize for the number of elements. IMPORTANT: Arrays are fixed length.
      • Arrays are allocated on the stack. Rust panics if an index is out of bounds (in particular, it actually checks at runtime).
      • indexing: a[i], where i is a usize.

        Example:

        const J : usize = 5;
        let a : [u32; J] = [1, 2, 3, 4, 5];
        let i : usize = 2;
        assert_eq!(a[i], 3);
        

Check the appendix in RPL on Operators for details on operators, arithmetic, Boolean or otherwise. We will, of course, see more types as the course goes on (and define our own types).

3.4. Functions

Grammar:

<fun-decl>  ::= fn <fun-ident>(<params>) <block>
              | fn <fun-ident>(<params>) -> <ty> <block>
<params>    ::= ϵ | <param> | <param> , <params>
<param>     ::= <var-ident> : <ty>
<block>     ::= { <stmts> }
<fun-ident> ::= ; snake_case ;

Functions in Rust behave much like functions in other PLs. A couple key notes:

  • parameters must be type-annotated
  • the return type must be given if the function returns a value

There is a return keyword, but we don't often use it; the return value is the last expression in the function block. If no last expression is given then the return value is () of type () (i.e., the unit type); see the <block> case in the given grammar for more details about how the bodies of functions might look (there's a bit of trickiness with regards to semicolons but I'm not going to dwell on it since the compiler is pretty good at catching these things).

Example:

fn sum_of_squares(x : u32, y : u32) -> u32 {
    let x_squared = x * x;
    let y_squared = y * y;
    x_squared + y_squared // NO SEMICOLON
}

3.5. Statements and Expressions

Grammar:

<stmts>      ::= ϵ
               | <expr>
               | <fun-decl> <stmts>
               | <stmt> ;<stmts>
               | <expr> ;<stmts>
               | <expr-no-sc> <stmts>
<stmt>       ::= <var-decl> | <const-decl> | <assign>
<expr-no-sc> ::= <if-expr> | <while-expr> | <for-expr>
<expr>       ::= <block>
               | <lit>
               | <uop-expr>
               | <bop-expr>
               | <call-expr>
               | ( <expr> )
<uop-expr>   ::= <uop> <expr>
<bop-expr>   ::= <expr> <bop> <expr>
<uop>        ::= ; see docs ;
<bop>        ::= ; see docs ;
<call-expr>  ::= <fn-ident>(<exprs>);

Statements perform actions and do not have a value. Expressions have values (and are evaluated). The only statements we've seen so far are declarations. Any expression can be assigned to a variable (you should check this, it works even for things like while expressions).

3.6. Control Flow

Grammar:

<if-expr>      ::= if <expr> <block> <else-if-expr>
<else-if-expr> ::= ϵ | else <block> | else if <block> <else-if-expr>
<while-expr>   ::= while <expr> <block>
<for-expr>     ::= for <var-ident> in <expr> <block>
<ret-expr>     ::= return <expr>
<expr>         ::= <if-expr> | <while-expr> | <for-expr> | <ret-expr>

Control flow is also pretty standard. A couple things to keep in mind:

  • control flow is defined by expressions. This means they can be used as the values of variables; this is particularly nice for if-expressions
  • the then-case and the else-case blocks must be the same type

Example.

fn is_prime(n: i32) -> bool {
    for i in 2..n {
        if n % i == 0 {
            return false
        }
    }
    true
}

4. Ownership

Ownership accounts for the first plateau in the learning curve of Rust. In rough terms, values have owners, e.g., values aren't necessarily copied on reassignment.5 Ownership is motivated by the way that Rust (and many systems PLs) manages memory, so we have to spend some a bit of time learning about this.

4.1. Stack and Heap

Rust is a systems PL, which means that we need to care at least a little bit about how memory is managed. We won't be going all the way to the hardware; we'll take a sufficiently detailed view to be able to appreciate the interesting aspects of Rust's type/borrow system, and a sufficiently abstract view as to not get caught up in implementation details, e.g., of the memory allocator.

There are, in rough terms, three kinds of memory we need to care about (though we'll primarily focus on the latter two):

  1. static memory
  2. the stack
  3. the heap

Static memory consists of constants used throughout the execution of a program; it's located in a fixed place which is accessible during execution and the values therein never change. For example, when we define a variable whose value is a string slice:

let x : &str = "hello world"

The "hello world" part is stored in static memory and x is given a reference to this location. This is about all we'll say about static memory.6

Next up, the stack. The stack consists of the values of parameters and local variables relevant to a function call.7 There are a lot of positive things to say about the stack:

  • Values put on the stack are very quick to access─they're "right there", so to speak, when a function is called.
  • The stack is compactly organized; it's first-in first-out, there's no wasted space, everything relevant to the function is there when we need it, and thrown away when we're done with it.

This seems ideal.8 The problem: there are three major restrictions on the kind of data that can be put on the stack:

  1. The data must be fixed-size, known at compile time.
  2. The data cannot change size throughout the execution of the program.
  3. The data on the stack relevant to a function does not persist after control is returned to the caller.

It's worth noting before moving on that, although these are common restrictions among systems PLs, they are ultimately design decisions; this optimization allows the Rust compiler to know exactly how much memory to allocate when a function is called. There is nothing inherent to a stack which makes it so we can't store data whose size is unknown at compile time (variable-size data is a different question, that would be much harder to deal with in a stack).

So, if we want to do "reasonable" programming, i.e., programming with potentially persistent data of potentially variable size, we need to put it somewhere else in memory.

The heap is where we store this variable-size persistent data. Because the size is unknown at runtime, the program itself has to allocate that memory, i.e., it has to ask a memory allocator for the amount of memory that it wants. And because the data may be persistent, we have to tell the memory allocator when we're done with it. This also means that, when we work with data on the heap, we need a level of indirection; if a function call needs access to data on the heap we put a reference on the stack, which we can follow to get to that data.

There are a lot of negative things to say about the heap:

  • Access is slower; the processor has to potentially jump around; there may (read: are likely to) be references to references to references, and following these references takes time.
  • The heap is organized less compactly; if we tell the memory allocator we no longer need some data on the heap, the memory allocator can free it so that it can be used later, but this may fragment the heap, leaving "holes" that add up to a large amount of memory, but cannot be used to hold a contiguous piece of data.
  • The system of allocation/deallocation is exactly the source of memory bugs:
    • A dangling point is what you get when you keep around a reference to a location on the heap that no longer contains valid data.
    • A memory leak is what you get when you allocate memory, but lose the reference to it so that it can't be freed.
    • A data race is what you get when you have multiple processes vying for the same data on the heap.

It's a kind of Pandora's box; the reality is we need the heap, we need to be able to work with variable-sized persistent data. And maybe part of our goal will be to minimize heap allocation where possible in the case that memory allocation/deallocation becomes a bottleneck, but we can't do much without it.9

The Picture. You'll often see diagrams of this form to describe the layout of memory. The stack and heap grow in opposite directions. And there may be "holes" on the heap due to freed memory. It's a bit of a caricature but is sufficient for our purposes.

  (HIGH ADDRS)
┌───────────────┐
│ FN1 PARAM     │ STACK
├───────────────┤ ↓↓↓↓↓
│ FN1 LOCAL VAR │
├───────────────┤
│ FN1 LOCAL VAR │
├───────────────┤
│ FN2 PARAM     │
├───────────────┤
│ FN2 PARAM     │
├───────────────┤
│ FN2 LOCAL VAR │
├───────────────┤
│ FN2 LOCAL VAR │
├───────────────┤
        ⋮
        ⋮
        ⋮
├───────────────┤
│ HEAP DATA     │
├───────────────┤
│ FREE          │
├───────────────┤
│ FREE          │
├───────────────┤
│ HEAP DATA     │
├───────────────┤ ↑↑↑↑
│ HEAP DATA     │ HEAP
├───────────────┤╮
│ CONSTANTS     ││
├───────────────┤├STATIC
│ CONSTANTS     ││
└───────────────┘╯
  (LOW ADDRS)

Okay, so we have the stack and the heap, and all systems PLs have to deal with the tradeoffs presented by these two kinds of memory. There are, in broad strokes, four ways that have become common approaches (in order roughly of decreasing user control):

  1. Explicit allocation/deallocation (C)
  2. Ownership (Rust)
  3. Automatic Reference Counting (Swift, C++ via smart pointers)
  4. Garbage Collection (Python, Java, OCaml, pretty much all high-level PLs)

4.1.1. Explicit Allocation

In "traditional" systems PLs like C, the programmer is in charge of when to allocate and deallocate heap memory.10 This is done using the methods malloc (for allocating) and free (for deallocating). Pretty simple, for every malloc, we need exactly one free when were done with the memory we allocated. Except that, as we learn from Uncle Ben, with great power comes great responsibility. Having control over allocation is what also gives us the ability to introduce memory bugs, and experience has shown us that these kinds of bugs are prevalent and difficult to track down.

Example. A dangling pointer in C.

int main(void) {
  int *x = (int*)malloc(sizeof(int));
  *x = 2;
  free(x);
  printf("%d\n", *x);
  return 0;
}

Example. A memory leak in C.

void leak(void) {
  int *x = (int*)malloc(sizeof(int));
  *x = 2;
  printf("%d\n", *x);
}

int main(void) {
  leak();
  return 0;
}

4.1.2. Garbage Collection

On the other side of the user-control spectrum is garbage collection, which is an automatic way of allocating and deallocating memory. It's another idea which is simple in concept and complicated in detail: periodically during the execution of the program we:

  • mark all the data on the heap that still has a reference in the stack;
  • deallocate everything that on the heap which isn't marked.

The process that does this is called the garbage collector (GC). No more memory bugs, no more thinking about memory at all, data is data. The drawback: with less responsibility comes less power. We give up compute to periodically clean up the heap. And in most cases this is fine, if we don't need performance then we don't need performance. But if we do need performance, there isn't much we can do with a GC except maybe choose a better GC (some algorithms are better than others, and performance can depend on the application domain).

4.1.3. Automatic Reference Counting

Before going into borrowing, I thought I'd point out one more alternative to memory management, automatic reference counting, notably used by Swift. The idea:

  • keep track of the number of references to a piece of data on the heap;
  • deallocate when that number is 0.

Example. The Swift documentation has a nice example of this. This program only prints allocating and deallocating once, even though a reference goes out of scope several times.

class Stuff {
    init() {
        print("allocating")
    }
    deinit {
        print("deallocating")
    }
}

var r1 : Stuff? = Stuff()
var r2 : Stuff? = r1
var r3 : Stuff? = r2

r1 = nil
r2 = nil
r3 = nil
allocating
deallocating

This is a nice idea, it has its benefits and drawbacks, some of which we'll discuss more later. Rust also supports this style of memory management, so the community must see something in it.

4.1.4. Ownership

Finally, the protagonist of our story, Rust's ownership. This approach lies somewhere between explicit allocation and reference counting; you never have to allocate memory explicitly in Rust, but there's no unspoken runtime computations (like reference count updating) happening in the background.

According to the RPL, the ownership rules of Rust are:

  • Every value has a single owner.
  • When the owner goes out of scope, any memory associated with the value is freed.

I think the best way to understand these rules is to focus on the second one: when a variable goes out of scope, we free its associated memory. This is a stupid-simple policy, and it certainly solves the problem of memory leaks; every variable must go out of scope at some point, so there's at least one deallocation for every allocation.

But this is an awful policy when it comes to dangling pointers. How do we know that there isn't some other reference to the same data when we go to free it? Not to mention it's problematic with respect to freeing memory that's not allocated (i.e., more than one deallocation for one allocation).

This is where the first rule comes in: when a variable goes out of scope, they must own the data. No one else can refer to it. In other words, ownership (the idea that every value has an owner) allows us to adopt a stupid-simple policy on freeing data (free when your owner goes out of scope). Another way to think about it: it's like automatic reference counting, except you're not allowed to have a reference count greater than 1.

Example. This policy means there are things we just cannot do, e.g., we can't have two references to the same data on the heap. If one goes out of scope, it will free its associated data, leaving the second one referring to data that is no longer valid.

fn main() {
    let x = String::from("hello world");
    let y = x;
    println!("{x}");
    println!("{y}");
}
error[E0382]: borrow of moved value: `x`
  --> src/main.rs:49:15
   |
47 |     let x = String::from("hello world");
   |         - move occurs because `x` has type `String`, which does not implement the `Copy` trait
48 |     let y = x;
   |             - value moved here
49 |     println!("{x}");
   |               ^^^ value borrowed here after move
   |

When we try this, we get a type error. And this is important to emphasize, it's not a runtime error. It's also worth noting there nothing inherently wrong with this code from a computational perspective. Consider the equivalent C code:

int main(void) {
  char* x = "hello world";
  char* y = x;
  printf("%s\n", x);
  printf("%s\n", y);
  return 0;
}
hello world
hello world

But it is the philosophy of Rust that we should program conservatively, disallowing some things, and thank ourselves in the long run, instead of coding fast-and-loose, doing whatever we want, and potentially paying for it later.11

4.2. References and Borrowing

I'm gonna put to RPL 4.2 here.

4.3. Slice Types

5. Structures

Structure definition syntax:

<stmts>      ::= <stmt-no-sc> <stmts>
<stmt>       ::= struct <struct-ident>
<stmt-no-sc> ::= struct <struct-ident> { <ft-pairs> }
<ft-pairs>   ::= ϵ | <ft-pair> | <ft-pair> , <ft-pairs>
<ft-pair>    ::= <var-ident> : <ty>

Structure identifiers must be capitalized.

Example:

struct Player {
    name: String,
    score: i32,
}

Stucture instantiation syntax:

<expr>     ::= <struct-ident>
             | <struct-ident> { <fv-pairs> }
             | <struct-ident> ( <tys> )
<fv-pairs> ::= ϵ | <fv-pair> | <fv-pair> , <fv-pairs>
<fv-pairs> ::= <var-ident> : <expr> | <var-ident>

Note the use of tuple structures and field init shorthand syntax.

Example:

let p = Player {
    name: String::from("Ash"),
    score: 0,
}

Field access/update syntax (dot-notation):

<assign> ::= <var-ident>.<var-ident> = <expr>
<expr>   ::= <expr>.<var-ident>

Structure update syntax:

<expr> ::= <struct-ident> { <fv-pairs>..<expr> }

5.1. Structures and Ownership

It is common for structures to own their data. It's possible to store references, but this requires that we discuss lifetimes (to come, I promise).

Values are moved per field so it is possible to move only a subset of the values of a structure. This should probably only be used for self-updates.

Question. Why would we want to use &str over String in a structure?

Note. References are short term. They should not usually be used to structure data.

Question. Can we build cyclic structures with references?

5.2. Derived Traits

Derived trait

5.3. Methods

Method syntax:

<stmt-no-sc> ::= impl <struct-ident> <block>

We can have multiple implementation blocks

**Automatic reference/dereference

5.4. Associated Functions

We use the Self keyword:

<ty>   ::= Self
<expr> ::= Self | Self { <tv-pairs> }

6. Enumerations

Enumeration definition Syntax:

7. Option Types

8. Pattern Matching

9. if let

Footnotes:

1

My nature does not allow me to introduce anything without a wannabe-hybrid-Salinger-Wallace-esqe preamble. I wish it didn't have to be this way but at some point, we can't break from our aesthetic inclinations.

2

An uncommon pairing, most popular languages are beige wallpaper boring.

3

I don't even like Rust that much, to be honest. It's probably most fascinating to me from a social theoretical perspective than from a PL design perspective.

4

I had a conversation with some folks about possible alternative idioms. PETA recommends "feed two birds with one scone." Not punchy enough in my opinion. My dad came up with two pretty good ones: "strike out to batters with one pitch" (nonsensical in a pleasant way) and "destroy two planets with one deathstar" (to better align with nerd culture).

5

In Rust we're able to specify whether something is copied on reassignments (using a mechanism called traits, similar to interfaces or type classes).

6

We include this brief note about it basically to answer the question: If we have a parameter to a function which is a string slice, where is that slice actually stored? It can't be be on the stack because the size of data on the stack has to be known at compile time.

7

The stack is also occasionally called the call stack, but prefer to be a bit cagey on this, as there are certainly examples of programming languages which draw a distinction between the call stack and the stack used during the execution of a program, e.g., some implementations of Forth.

8

And this is, in essence, the motivation behind stack-oriented PLs like Forth: fine-grained control of the stack means more efficient program.

9

And we should generally be wary of pre-mature optimization

10

Hard-core C programmers like to point out that there is no notion of stack and heap in the C specification. I can't claim to have a strong argument against this─I'm not a systems programmer─except that everyone already thinks of allocation as heap allocation in C, and this underlying some C compilers.

11

As we will see, this philosophy also pays off when we want to parallelize our code.