Skip to content

Commit

Permalink
[lint] s4: Spacing around blocks/headings
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobjwalters authored Oct 18, 2024
1 parent 1021afe commit d0b6dc7
Showing 1 changed file with 84 additions and 48 deletions.
132 changes: 84 additions & 48 deletions pages/resources/lisp-workshop/step4.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ Complexity: Short

So, by now you've implemented an evaluator for simple arithmetic expressions (and maybe more, if you did the extra challenges).
But even as a calculator, our program is pretty limited! Consider the following program:

```scheme
42
(mod (pow 42 17) (* 61 53))
(mod (pow (mod (pow 42 17) (* 61 53)) (modmul-inv 17 (lcm (- 61 1) (- 53 1)))) (* 61 53))
```

Assuming correct definitions for `mod`, `pow`, `modmul-inv`, and `lcm`, the above is a demostration of encrypting and then decrypting the number 42 via [RSA](https://en.wikipedia.org/wiki/RSA_(cryptosystem)#Example). But you wouldn't know it!

Compare the above program to the one below. Both perform exactly the same computation. Which do you think is more readable?

```scheme
(define p 61)
(define q 53)
Expand All @@ -35,6 +38,7 @@ m'
In this step, we're going to work towards extending our interpreter to allow the user to introduce their own definitions, much like in the above program.

## Top-Level Declarations

Based on the code example from the previous section, you probably have an intuitive understanding of how `define` should behave.
But we need to answer a few questions before our understanding is concrete enough to fully implement it.

Expand All @@ -53,12 +57,15 @@ For now, the only declaration we'll have is `define`, but the extra tasks in thi

We'll also say that a declaration can only appear at the *top level* of a program, rather than as a sub-expression.
This means you can't do the following (whatever the definition of `foo` is):

```scheme
(foo (define two 2) two)
```

For clarity, we'll sometimes refer to declarations as *top-level declarations*.

## Environments

Our next question is: if each declaration introduces a new name, how do we keep track of them?

Simultaneously, we'll answer: how do we use these names when evaluating other parts of the program?
Expand All @@ -77,11 +84,13 @@ If it's in the environment, we can substitute the symbol with the corresponding

Let's walk through an example.
Say the user wants to run the following program:

```scheme
(define one 1)
(define two (+ one 1))
(+ one two)
```

We'll start off with an empty context, which we'll represent as the empty list: `[]`.

Next, we evaluate the top-level declaration `(define one 1)`.
Expand All @@ -94,28 +103,35 @@ Our whole expression evaluates to `2`, so we add `two -> 2` to the environment,
Finally, to evaluate `(+ one two)`, we look up the values of `one` and `two` in our context, resulting in `(+ 1 2)`, which evaluates to `3`.

## Shadowing

What should happen when we run the following program?

```scheme
(define one 1)
(define one 2)
one
```

Here, the second definition *overlaps* with the first.

There are three sensible options for output:

- Option 1: The interpreter throws an error when the user tries to redefine `one` on the second line;
- Option 2: The interpreter prints `1`, silently ignoring the second definition;
- Option 3: The interpreter prints `2`, updating the entry in the environment to `one -> 2`. Here, we say the newer definition *shadows* the older one.

What about this program?

```scheme
(define one 1)
(define number (+ one 1))
(define one 2)
one
number
```

Here, there are four possibilities:

- Option 1: The interpreter throws an error when the user tries to redefine `one` on the third line;
- Option 2: The interpreter prints `1` followed by `2`, silently ignoring the second definition;
- Option 3: The interpreter prints `2` followed by `2`. The new definition of `one` *shadows* the old one, and `number` still refers to the original definition of `one`.
Expand All @@ -124,11 +140,14 @@ Here, there are four possibilities:
All four make sense depending on the context, but option 2 might be quite confusing for the user if they expect the language to behave similarly to most common languages. We recommend you pick either option 1, option 3, or option 4. For the steps after this one, we'll assume that you're using option 3.

Also consider what behaviour your interpreter should exhibit on the following programs:

```scheme
(define plus +)
(define * plus)
```

(where `+` and `*` are primitives)

```scheme
(define rec 1)
(define rec rec)
Expand All @@ -138,6 +157,7 @@ rec
Your choice of behaviour for overlapping definitions may dictate which data structures you can use to represent your environment.

## Task

Define a new data type to represent the environment. This should look something like a map from symbols to values.

Update your `eval` function so that it takes an environment as an argument. When starting a new program, this environment should be empty.
Expand All @@ -148,45 +168,56 @@ If it doesn't, then it should throw an error.
You should also implement the function `define`.
`define` takes two arguments; a symbol `n`, acting as a name, and an expression, which is to be evaluated into a value `v`. Once evaluated, we should update our environment so that `n` maps to `v`.
`define` is a top-level declaration, meaning that it can only appear in the outer-most level of an expression tree, and doesn't return a value. The following lines are allowed:

```scheme
(define one 1)
(define secret (+ 8 (* 17 2)))
(define foo secret)
```

But these lines are not allowed:

```scheme
(id (define one 1))
(define program (define foo 123))
```

You may have to restructure your evaluator to support top-level declarations.

Finally, you should update your REPL to keep track of its environment, to allow the user to run top-level declarations in the REPL.

## Extra Challenges

These are some extra challenges you can attempt to build your understanding further, and make your interpreter more feature-complete. None of them are required for a fully-functional interpreter. They are listed in order of subjective difficulty; if you struggle on the later ones, you should move on to the next step and come back later. Depending on your language choice, they might be easier or harder than anticipated!

- Add a basic import system: define another top-level declaration called `import`, which takes a filename; loads that file; evaluates all of the top level declarations stored in it; and extends the current environment with these declarations. For example, if `foo.lisp` contains the following:
```scheme
(define my-favourite-number 12)
```

```scheme
(define my-favourite-number 12)
```

and `main.lisp` contains this program:
```scheme
(import "foo.lisp")
my-favourite-number
```
then evaluating `main.lisp` should print `12`, assuming that `foo.lisp` is in the same directory as `main.lisp`.

```scheme
(import "foo.lisp")
my-favourite-number
```

then evaluating `main.lisp` should print `12`, assuming that `foo.lisp` is in the same directory as `main.lisp`.

- Allow top-level definitions to reference each other in any order. As an example, when running the following file:
```scheme
(define foo bar)
(define bar 1)
foo
```

```scheme
(define foo bar)
(define bar 1)
foo
```

Your interpreter should output:
```scheme
1
```

```scheme
1
```

- Add a namespace system. Namespaces allow you to keep different collections of definitions separate, ensuring that they don't interfere with each other if they happen to define the same name. You could implement namespaces by defining a new top-level declaration `namespace`, which takes a symbol `n` to use as a name for the namespace, and then an arbitrary number of other top-level declarations. Crucially, a namespace can contain other namespaces! When `namespace` is evaluated, it creates a new environment with all of the declarations in it, and stores this environment in our orginal environment, under the name `n`. You'll have to update your representation of environments (or values) to support namespaces.

Expand All @@ -195,43 +226,48 @@ foo
For convenience's sake, you will also want to add `open`, which takes a symbol `n` and an expression `e`. To evaluate `(open n e)`, you should (temporarily) add all of the declarations in `n` to the current environment, and then evaluate `e` in this updated environment.

For example, the following code:
```scheme
(namespace foo
(define secret (+ 8 (* 17 2))))
(namespace int-monoid
(namespace add
(define unit 0)
(defined op +))
(namespace mul
(define unit 1)
(define op *)))
(using foo secret)
(using int-monoid (using add unit))
(open int-monoid ((using add op) 1 (using add unit)))
(open int-monoid (open mul (op unit 2)))
```

```scheme
(namespace foo
(define secret (+ 8 (* 17 2))))
(namespace int-monoid
(namespace add
(define unit 0)
(defined op +))
(namespace mul
(define unit 1)
(define op *)))
(using foo secret)
(using int-monoid (using add unit))
(open int-monoid ((using add op) 1 (using add unit)))
(open int-monoid (open mul (op unit 2)))
```

should output:
```scheme
42
1
1
2
```

```scheme
42
1
1
2
```

- Integrate your import system with your namespace system. Each file should be its own namespace. To improve user convenience, you could add a new keyword, `module`, which takes as its only argument a symbol `n`, representing the name of the namespace the rest of the file is under. The following two files should be exactly equivalent:
```scheme
(namespace foo

```scheme
(namespace foo
(define secret (+ 8 (* 17 2)))
(define x 1))
```

```scheme
(module foo)
(define secret (+ 8 (* 17 2)))
(define x 1))
```
```scheme
(module foo)
(define secret (+ 8 (* 17 2)))
(define x 1)
```
(define x 1)
```

Now when a user `import`s the above file, it should add `foo` to the current environment. To use the definition, the user has to `open` the namespace provided. As another convenience aid, provide another keyword `load`, that combines `import` and `open`.

You should also make the user specify exactly *which* definitions are to be exported. You could ask them to provide a list of names at the top of the namespace/module declaration, or make `define` take a "visibility" parameter (a symbol which can only take the value of `public` or `private`, for example).
Expand Down

0 comments on commit d0b6dc7

Please sign in to comment.