Print and Run: Making a shitty language from scratch. Part 1.5
Tags: javascript, programming, learning, tutorial
Published: 2023-11-30 10:52:05

## Intro

Welcome back to making a shitty language from scratch.

This is a smaller installment, hence the `1.5`.

We left off with our implementation allowing for addition and subtraction to be done through our custom syntax.

This post will focus on cleaning up our code and introducing a couple of utilities that will come in handy for implementing variables in the upcoming Part 2.

> PS: You can read the original [here](https://andi.dev/blog/shitty-language-part-1.5/)

## Introducing the standard library (also known as STD)

You can think of the standard library as a set of functionality that a language comes with out of the box. There's some pedantic differences between functionality that's built directly into the language, and a standard library, but let's not worry about that.

For our purposes, the STD and built-in functionality are one and the same. And right now, we only have 2 functions in our standard library: "+" and "-".

As a first step in our cleanup, let's refactor things a bit so that the standard library is more evident in the code. If you don't remember, the final code for Part 1 looked like this:

```js
const sum = (numbers) =>
	numbers.reduce((sum, n) => sum + n, 0);

const EVALUATE = (input) => {
	if(!Array.isArray(input)) {
		return input;
	}

	const [fn, ...rawArguments] = input;
	const arguments = rawArguments.map(arg => EVALUATE(arg));


	if(fn === "+") {
		return sum(arguments);
	}
	if(fn === "-") {
		const [first, ...rest] = arguments;
		return first - sum(rest);
	}

	throw Error("Function not supported:", fn);
}
```

We will extract the "+", and "-" functions into a `std` object. This object will hold all future std functionality as well:

```js
const sum = (numbers) =>
	numbers.reduce((sum, n) => sum + n, 0);

// A more readable standard library
const std = {
	"+": (args) => sum(args),
	"-": ([first, ...rest]) => first - sum(rest)
}

const EVALUATE = (input) => {
	if(!Array.isArray(input)) {
		return input;
	}

	const [fn, ...rawArguments] = input;
	const arguments = rawArguments.map(arg => EVALUATE(arg));

	if(std[fn]) {
		return std[fn](arguments);
	}

	throw Error("Function not supported:", fn);
}
```

You'll notice that in the body of the `EVALUATE` function, we've replaced the if statements for each individual operation with a generic `if(std[fn])` statement. This will check if the function exists in the std, and if it does it will call it with the `arguments` array and return the result.

Now the body of the evaluator should stay slim, even if we add another 50 functions in the standard library.

## Printing

I should've started this series with implementing a print function and running a "hello world" program.

It's not too late to add one now. It will be useful for debugging future programs once the language gets more complex.

First we will add a new function to the std:

```js
const std = {
	"+": (args) => sum(args),
	"-": ([first, ...rest]) => first - sum(rest),
	"print": (arguments) => console.log(...arguments)
}
```
Luckily, `console.log` already handles printing multiple arguments so our print function is a very thin wrapper around it.

Since we already have the generic if statement in the evaluator, we don't need to add any extra code.

Running

```js
EVALUATE(["print", "hello", "world"]);
```
will print `"hello world"` to the console.

We can also combine printing with the math operations we implemented last time:

```js
EVALUATE(["print", ["+", 4, ["-", 3, 10]]]) // should log -3
```

<aside>

	<sub>Note<sub>

	We're going to make our language have a functional programming flavor.

	One of the most important features of a functional language is that **everything** is an expression. That means that anything you write in that language will result in a value.

	A good explanation for statements vs expressions is the difference between if statements and the ternary operator.

	You can't do `const myVar = if(x) 10 else 20;`. The if/else statement doesn't result in a value, it just runs the code inside the branch.

	But you can do `const myVar = x ? 10 : 20;` because the ternary operator is an expression, and expressions always result in values.

</aside>

Let's make the print function an expression and not just a statement.

It's very simple, all it has to do to be an expression is to result in a value. And how do functions result in values? By returning something.

```js
const std = {
	...
	"print": (arguments) => {
		console.log(...arguments);
		return arguments;
	}
}
```

It might seem inconsequential to do this, but if we stick with it for all of the language's functions, you'll notice the pay-off at the end for the kind of programs you can write when everything is an expression.

Ok, printing is done, let's switch gears to other improvements we can make.

## Running multiple statements

Have you noticed that our evaluator only accepts one top-level function at a time? We can only evaluate either a `print`, `+`, or `-` function. These functions accept arguments that are other function calls, but we can't string multiple function calls at the top-level.

What if you want to run multiple unrelated instructions, like in a real language?

There are a few ways to do this, like allowing the evaluator to accept an array of statements as well as a single function call. Which would look like:
```js
EVALUATE([
	["print", "hi"],
	["print", "this is another statement"],
]) // the 2 print statements are unrelated to each other
```

But that can introduce weird edge-cases to handle all throughout our interpreter.

Remember that we call evaluate on all of the arguments recursively. So we'll then have to implement some code to distinguish arrays made of instructions, or function calls.

Maybe it's possible to distinguish:
```js
["print", ["+", 1, 2], ["-", 6, 8]]
```
From:
```js
[["print", ["+", 1, 2]], ["print", ["-", 6, 8]]]
```

But do we want to have to deal with that?

The easier choice is to do what we've been doing until now: add another function to our std.

We'll call this function `run`, since it'll be used to run multiple instructions. Adjusting the example above, the program would look like this instead:

```js
EVALUATE(["run",
	["print", "hi"],
	["print", "this is another statement"],
])
```
You can see that the function run is called with two arguments, the two print function calls.

But what should run return?

This is where we will take inspiration from functional languages again.

In an imperative language like JS, we have an explicit return statement. And a return statement is the last operation that happens before a function call results in a value.

In functional languages, every instruction is also an expression that results in a value. The usual convention is that rather than using an explicit return syntax, they implicitly return the last expression's value.

Think of this imaginary language's function:
```js
function test() {
	"a string";
	someFunctionCall();
	10;
}
```

The last evaluated expression is the hardcoded number `10`, and that's what the `test` function will return implicitly.

Now let's implement returning the last argument for the `run` function:

```js
const std = {
	...
	run: (args) => args[args.length - 1]
}
```

We could change our print function to also return only the last argument, for uniformity. We'll extract the logic for getting the last argument in a separate function as well:

```js
const std = {
	"+": (args) => sum(args),
	"-": ([first, ...rest]) => first - sum(rest),
	last: (args) => args[args.length - 1],
	print: (args) => {
		console.log(...args);
		return std.last(args);
	},
	run: (args) => std.last(args)
}
```

> 🎉 Notice how there's still no need to change the `EVALUATE` function's body, even with all these std changes!


## Final code

Now we're in a good place to implement variables in Part 2. Stay tuned for it.

Here is the full code up until now

```js
const sum = (numbers) =>
	numbers.reduce((sum, n) => sum + n, 0);

const std = {
	"+": (args) => sum(args),
	"-": ([first, ...rest]) => first - sum(rest),
	last: (args) => args[args.length - 1],
	print: (args) => {
		console.log(...args);
		return std.last(args);
	},
	run: (args) => std.last(args)
}

const EVALUATE = (input) => {
	if(!Array.isArray(input)) {
		return input;
	}

	const [fn, ...rawArguments] = input;
	const arguments = rawArguments.map(arg => EVALUATE(arg));

	if(std[fn]) {
		return std[fn](arguments);
	}

	throw Error("Function not supported:", fn);
}
```