---
title: 'Languages: High and Low'
description: How high is up?
date: 2022-08-19
draft: true
---

import Sidenote from '@components/Sidenote.astro';

Here's a fun way to start an argument: Go ask on a programming forum whether C should be considered a "high-level" or "low-level" language. The majority of responses will be along the lines of "low-level, obviously, you have to call `malloc()` all the time", but every once in a while you'll find some crusty old wizard who earned his sripes on MULTICS grousing about how kids these days don't understand what a _real_ low-level language looks like, and back in my day we had to flip individual bits with our teeth, and so on.

The truth is, asking whether a language is high- or low-level unqualifiedly is sort of like asking whether an object is heavy. Heavy relative to what? The question is open-ended without some frame of reference.<Sidenote>Of course, most people have an implicit frame of reference for this question that resolves to "heavy relative to the objects I commonly encounter in my day-to-day life". As it turns out (in my opinion), most programmers have a similar implicit frame of reference when it comes to languages, which is where you get the disagreement I mentioned earlier.</Sidenote> A boulder is heavy relative to a peanut, but quite light compared to an oil tanker. 

A better question, in my opinion, is whether one language is _higher-_ or _lower-level_ than another, and this is where it gets interesting. Most people will probably agree that higher-level languages are _more abstract_ than lower-level ones, i.e. they take more of the fiddly details of what's actually going on inside the computer and hide them away so that you, the programmer, don't have to worry about them. 

You can probably throw little language-specific details back and forth until the cows come home, but I think there are a few common "break-points" that you can use to group languages into buckets of similarly high- or low-level-ness. To me, they look something like this:

* **Level 1** langauges give you control over individual registers (just assembly, basically)
* **Level 2** languages give you control over memory allocation and raw pointers. E.g. C and C++, and Zig I guess? Also a lot of older languages like Pascal and Ada.
* **Level 3** languages handle memory allocation and deallocation for you, but still distinguish between the stack and the heap. Java, C#, and Go fall here, as do Nim, Crystal, probably Pony, V, and others.<Sidenote>You can even make the argument that Rust belongs at this level. Even though it's often referred to as a "systems" language, which most people associate with "low level", it _kind of_ abstracts away the work of allocating and deallocating memory. It just doesn't abstract it quite as _much_ as other languages, since you do still have to think about it.</Sidenote> You can further split this level into those that require a runtime (Java, C#) and those that don't (Go, Nim, Crystal, etc.) Although more properly I guess the latter category just embed their runtime, rather than requiring it to exist separately.
* **Level 4** languages give you control over things like intermediate variables and so on - there are probably a lot of sub-levels here, like having to instantiate loop variables vs. having them handled for you,<Sidenote>Or both! Hi Javascript!</Sidenote> but I think this is the core of it. Most dynamic/interpreted languages probably fall here: Python, JS, Ruby, PHP, Perl, etc.
* **Level 5** languages don't give you control over any of those. The only one I can think of off the top of my head is SQL, but there are probably others. I have a hunch that at this point you tend to see a lot more domain-specific stuff, since it's easier to abstract away details when you know what the use-case will look like.

I can also imagine a Level 0 which gives you control over things that aren't currently exposed by the underlying hardware. For instance, you could have a language construct that allows you to "pin" memory regions to a certain cache level, guaranteeing that you can always access it quickly. Or you could attach a "hint" to a branching codepath that allows you to override the CPU's branch predictor, if you know that one case will be overwhelmingly more common than the other. I wonder whether we'll start to see this sort of thing in the future, as Moore's Law continues to slow down and people start looking for more and more ways to squeeze maximum performance out of their hardware.
