summaryrefslogtreecommitdiff
path: root/trees/blog/mk-lisp-1.typ
blob: 1a71e7e6bda4bb8f72ad3cdba5a07fec429bee8a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
#import "html_elements.typ": post
#show: post


This is the second part of a series of articles that I'm writing to document by
progress on building a Interpreter for myLisp, a lisp-like language. For the previous post [click
here](/mk-lisp-0).



== What is an interpreter?

An Interpreter is a program that takes souce code as input and executes it. It
is composed of several parts, today we are going to write a lexer and a parser.

The lexer will read a string of characters and output a vector of tokens, with
some information attached to them like its type. The lexer is important to the
parser because it returns the soure code in a better representation, holding
only information that matters to it, _(this is a very important pattern in
programming, thowing away information that is not important for what we want, by
keeping the data in a more convenient representation)_.

The parser will take the vector of tokens created by the lexer and create a data
structure _(in our case S-Expressions)_ which can then be executed by another part of the interpreter.

== The interpreter's implementation

I will be writing this interpreter in C. The most important reason for it is that
this is a learning project more than anything else, and in C provides us with
the bare minimum to create what we want.
It is important to note that there are better ways to implement it, but I don't
like to read other people's code before I try my own way. Maybe in the future when I'm done,
I'll change it.

== Lexer

First, lets define a little macro to help us debug.

```c
#ifdef DEBUG_F

#define DEBUG(fmt, args...)						\
    printf("(%s:%d) " fmt, __FILE__,__LINE__, ##args)

#else

#define DEBUG(f, fmt, args...) /* Do nothing */

#endif

```

Next, we need to create a few data types to store the tokens.
_As you can see our Lisp won't support strings and floats for now_.


```c
enum Token_Type {
    TOKEN_OPEN=0,
    TOKEN_CLOSE=1,
    TOKEN_NUMBER=2,
    TOKEN_SYMBOL=3,
};

/*
** Used to debug the lexer
*/
char* Token_Type_Str[4] = {"(", ")", "NUM", "SYM"};

typedef struct {
	enum Token_Type type;

	union {
		int number;
		char* name;
	}data;

}Token;
```

And then the tokenize fuction, there are a few details about it that need to be discussed.
Usually in Lisp, symbol names can be in some written in some formats, like `+1, +$,
[email protected]`. But for simplicity's sake we have a different set of rules: They must start
with an alphabetical character and not contain any whitespace or parenthesis.



```c
Token* tokenize(char* in, int* ret_sz) {
	int cursor = 0;
	int sz = 0, alloc=1;
	Token* vec = (Token*) malloc(sizeof(Token));

	while(in[cursor] != '\0') {

		Token cur;

		if(in[cursor]=='(') {
			cur.type = TOKEN_OPEN;
			cursor++;

		} else if(in[cursor]==')') {
			cur.type = TOKEN_CLOSE;
			cursor++;

		} else if(isdigit(in[cursor])) {
			int number;
			sscanf(in+cursor,"%d",&number);


			while(in[cursor] != '\0'
				  && isdigit(in[cursor])) {
				cursor++;
			}

			cur.type = TOKEN_NUMBER;
			cur.data.number = number;
			DEBUG("TOKEN_NUMBER: %d\n", number);

		} else if(isalpha(in[cursor])) {

			char* name = (char*) malloc(MAX_SYM_SZ);
			assert(name!=NULL);

			int ptr = 0;

			while(in[cursor] != '('
				  && in[cursor] != ')'
		 		  && in[cursor] != ' '
				  && in[cursor] != '\0')
			  {

				if(isalpha(in[cursor])) in[cursor] = toupper(in[cursor]);
				name[ptr] = in[cursor];
				ptr++, cursor++;
			}

			name[ptr] = '\0';

			cur.type = TOKEN_SYMBOL;
			cur.data.name = name;
			DEBUG("TOKEN_NAME: %s\n", name);

		} else {
			cursor++;
			continue;
		}

		if(alloc <= sz+1) {

			alloc *= 2;
			vec = (Token*) realloc(vec, sizeof(Token)*alloc);
		}

		DEBUG("TOKEN: %s\n", Token_Type_Str[cur.type]);

		vec[sz++] = cur;
	}

	*ret_sz = sz;

	return vec;
}
```

And that's about it for the lexer.

== Representing S-Expressions

Before we start to implement the parser we need to write the data types for it.
But this time it is a little bit different because the parser will generate a
data structure which can be executed. We are going to represent those structures
as lisp S-Expressions.

First, let's define the type `Lisp_Object`, _(it has nothing to do with OOP's
objects)_, which will represent everything that a cons cell can hold as its car
or cdr.

== Tagged Pointers

There are a few ways that we could choose to represent `Lisp_Object`, we are
going to use tagged pointers, which are common in many Lisp implementations.

Every variable in C has at least 8 bits, so every valid pointer points to an
address which is a multiple of 8. This means that the least significant bits of
a valid address will aways end with three zeros. So we can use these bits to
store additional information about what the pointer is pointing to. When we need
to deference it we just mask out those three bits.

They are useful because a pointer usually occupies eight bytes and an `int` or
`float` just 4, so we can embed ints and floats into them, all we have to do is to
shift them up three bits. That's a huge improvement over deferencing a pointer
to an integer which is allocated on the heap. Using tagged pointers we can
utilize ints and floats on the stack.


Now, we will define a variable that will represent everything that a cons cell
can hold as its car and cdr: `Lisp_Object`.

```c
typedef uintptr_t Lisp_Object;
#define NIL (Lisp_Object)0;
```

`Lisp_Object` is a tagged pointer, that's why we used `uintptr_t` _(it is
defined be a variable that can store a pointer)_. And now a few functions to
operate on it.


```c
enum Tag {
	TAG_SYMBOL = 0,
	TAG_NUMBER = 1,
	TAG_CONS = 2
	/* ... */
};

Lisp_Object ptr_tag(Lisp_Object obj, enum Tag tag) {
	return obj | (int)tag;
}

Lisp_Object ptr_untag(Lisp_Object obj) {
	return obj & ~((Lisp_Object) 7);
}

int ptr_getTag(Lisp_Object obj) {
	return (int)(obj&7);
}

```
Those are the basic operations that we are going to be doing with them. But now we
need a few helper functions and macros to help us create new objects.

```c
#define OBJ(val, type)							\
	Obj_New_ ## type (val)

Lisp_Object Obj_New_symbol(char* str) {
	char* nstr = (char*) malloc(sizeof(str));
	strcpy(nstr, str);
	Lisp_Object ret = ptr_tag((Lisp_Object)nstr, TAG_SYMBOL);

	return ret;
}

Lisp_Object Obj_New_number(int val) {
	Lisp_Object nval = val;
	nval = val<<3;

	return ptr_tag(nval, TAG_NUMBER);
}

typedef struct {

	Lisp_Object car;
	Lisp_Object cdr;

}Lisp_Cons_Cell;

Lisp_Object fcons(Lisp_Object a, Lisp_Object b) {
	Lisp_Cons_Cell* cell = (Lisp_Cons_Cell*) malloc(sizeof(Lisp_Cons_Cell));
	cell->car = a, cell->cdr = b;

	return ptr_tag((Lisp_Object)&cell, TAG_CONS);
}

```

`OBJ` is a little helper macro do help us create new objects. We use it like
this:

Lisp_Object a = OBJ("hello-there", symbol);
Lisp_Object b = OBJ(42, number);

I didn't write a `Obj_New_cons` because we will be using the fcons function to
create new conses.

== Printing S-Expressions
Before we start the parser itself we need to create a function to print
S-Expressions, in order to debug it. There's an algorithm for it but I won't get
into detail. Here it is:

```c
void _Lisp_Print(Lisp_Object obj, int head);

void Lisp_Print_cons(Lisp_Object obj, int head) {
	if(head) putchar('(');

	_Lisp_Print(fcar(obj), 1);

	if(ptr_untag(fcdr(obj))==NIL) {
		putchar(')');
	} else if(ptr_getTag(fcdr(obj))!=TAG_CONS) {
		printf(" . ");
		_Lisp_Print(fcdr(obj), 0);
		putchar(')');
	} else {
		putchar(' ');
		_Lisp_Print(fcdr(obj), 0);
	}
}


void _Lisp_Print(Lisp_Object obj, int head) {
	enum Tag tag = ptr_getTag(obj);

	switch(tag) {
		case TAG_NUMBER:
			printf("%d", GET_VAL(obj, number));
			break;
		case TAG_SYMBOL:
			printf("%s", GET_VAL(obj, symbol));
			break;
		case TAG_CONS:
			Lisp_Print_cons(obj, head);
			break;
		default:
			break;
	}
}


void Lisp_Print(Lisp_Object obj) {
	_Lisp_Print(obj, 1);
	putchar('\n');
}

```

== The parser

I really liked implementing a Lisp parser a for another interpreter a few days
ago, the algorithm is so simple!

If a S-Expression is a list, whe can think of it as a list of atoms and for each
atom we parse it recursively. So, how do we parse a simple list? This is how it
would like in ~~py~~pseudocode:

def parse (n):
obj = get_obj(n)

if obj == nil:
return obj

else:
return cons(obj, parse(n+1))


It doesn't work for every S-Expression *yet*, but the main takeaway for it is that
you cons the current object with the rest of the list recursively. The only
difference between this and a complete parser is that the latter handles
parenthesis by parsing what is inside the current block and consing it with the
rest of the list.

```c
Lisp_Object parse(Token* tokens, int pos, int sz) {
	if(pos==sz) return NIL; /* the stop condition */

	if(tokens[pos].type == TOKEN_OPEN) {

		Lisp_Object car = parse(tokens,pos+1,sz);

		int aux = 1, balance = -1;

		while(balance != 0) { /*  Looking for the matching ) */

			assert(aux+pos < sz);

			if(tokens[pos+aux].type == TOKEN_OPEN)
				balance--;
			if(tokens[pos+aux].type == TOKEN_CLOSE)
				balance ++;

			aux++;
		}

		Lisp_Object cdr = parse(tokens, pos+aux, sz);

		return fcons(car, cdr);

	} else if(tokens[pos].type == TOKEN_CLOSE) {

		return NIL;

	} else if(tokens[pos].type == TOKEN_NUMBER) {

		int n = tokens[pos].data.number;
		Lisp_Object car = OBJ(n, number);

		return fcons(car, parse(tokens, pos+1, sz));

	} else if(tokens[pos].type == TOKEN_SYMBOL) {

		char* str = tokens[pos].data.name;

		Lisp_Object car = OBJ(str, symbol);
		return fcons(car, parse(tokens, pos+1, sz));

	}

	return NIL;
}

```

This code isn't that much different from the pseudocode that I've written
before, the main difference is the parenthesis handling. The code for this
article can be seen
[here](https://github.com/gbrls/lisp-interpreter/blob/a15eb40743e64e9cc60c9e01474050ebf25b59ec/main.c).

Every cons and symbol in which we allocate are not fred, so our interpreter is
leaking memory. The solution for this is called garbage collection, we are going
to implement it in a latter article.