Skip to content

Commit 1938aee

Browse files
committed
Version 1.0
0 parents  commit 1938aee

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2481
-0
lines changed

.circleci/config.yml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
version: 2
2+
jobs:
3+
build:
4+
docker:
5+
- image: runar0/docker-gcc-nasm-cmake
6+
steps:
7+
- checkout
8+
- run:
9+
name: Download BATS
10+
command: curl -LkSs https://api.github.com/repos/bats-core/bats-core/tarball -o master.tar.gz
11+
- run:
12+
name: Install BATS
13+
command: tar -xzf master.tar.gz && cd bats-core-bats-core-* && ./install.sh /usr/local && cd ..
14+
- run:
15+
name: CMake
16+
command: mkdir build && cd build && cmake .. && make
17+
- run:
18+
name: Test
19+
command: bats test/test.bats

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.o

CMakeLists.txt

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# The compiler
2+
project(compile)
3+
file(GLOB_RECURSE sources src/*.c src/*.h)
4+
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin)
5+
add_executable(compile ${sources})
6+
7+
# Static libraries
8+
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib)
9+
10+
# Static library that provides closure functionality to Assembly files
11+
project(closure)
12+
add_library(closure STATIC src/closure.c)
13+
14+
# Static library of basic functions, written in NASM
15+
project(standard C)
16+
enable_language(ASM_NASM)
17+
if(CMAKE_ASM_NASM_COMPILER_LOADED)
18+
add_library(standard STATIC src/standard.asm)
19+
endif(CMAKE_ASM_NASM_COMPILER_LOADED)

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Andrew Craig
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# Little Functional Language Compiler
2+
3+
[![CircleCI](https://circleci.com/gh/andycraig/functional-compiler/tree/master.svg?style=svg)](https://circleci.com/gh/andycraig/functional-compiler/tree/master)
4+
5+
A compiler for LFL ('Little Functional Language'), an original functional programming language. LFL has first-class functions and closures and supports recursion, and has Lisp-style syntax. The compiler is hand-written in C and Assembly.
6+
7+
I created LFL as a learning exercise, after wondering how anonymous functions might be implemented.
8+
9+
[Overview](#overview) | [Building](#building) | [Usage example](#usage-example) | [Feature showcase](#feature-showcase) | [Keywords](#keywords) | [Built-in functions](#built-in-functions) | [Limitations](#limitations) | [References](#references) | [Testing](#testing)
10+
11+
## Overview
12+
13+
The compiler takes in a file of LFL code, processes it and outputs the corresponding Assembly code.
14+
15+
Here's an example of an LFL program, which creates an anonymous function that adds 1 to its argument, creates an alias `inc` for that function, and applies it to 1 to yield 2:
16+
17+
```
18+
(let inc (λ x (plus x 1))
19+
(inc 1))
20+
```
21+
22+
The compiler converts this to:
23+
24+
```
25+
; Assembly code generated by compiler
26+
global main
27+
extern printf, malloc ; C functions
28+
extern make_closure, call_closure ; built-in functions
29+
extern plus, minus, equals ; standard library functions
30+
31+
section .text
32+
_f0:
33+
push rbp
34+
mov rbp, rsp
35+
sub rsp, 40 ; memory for local variables
36+
mov rax, rdi ; pointer to vector of arguments
37+
38+
... ET CETERA
39+
40+
call printf
41+
mov rax, 0 ; exit code 0
42+
leave
43+
ret
44+
45+
section .data
46+
message: db "%d", 10, 0 ; 10 is newline, 0 is end-of-string
47+
```
48+
49+
The steps the compiler goes through are:
50+
51+
1. **Tokenising:** The text file of LFL code is read in and converted into a list of symbols.
52+
2. **Parsing:** An Abstract Syntax Tree (AST) of function calls, constants and variable names is generated from the list of symbols.
53+
3. **Processing special forms:** Nodes in the AST with keywords (e.g., `λ`/`lambda`, `if`) are converted into special AST nodes.
54+
4. **Processing lambdas:** The bodies of lambda expressions in the AST are pulled up to the global level and named, and the lambda expressions themselves are replaced with calls to a special function `make_closure`.
55+
5. **Emitting Assembly code:** The AST is traversed, and at each node the corresponding Assembly code is written to the output file.
56+
57+
## Building
58+
59+
Build with CMake (requires NASM to build `libstandard.a`):
60+
61+
```
62+
mkdir build
63+
cd build
64+
cmake ..
65+
make # Creates binary 'bin/compile' and static libraries 'lib/libclosure.a', 'lib/libstandard.a'
66+
```
67+
68+
## Usage example
69+
70+
Let's use [`examples/example.code`](examples/example.code). Compile it to Assembly with:
71+
72+
```
73+
bin/compile examples/example.code example.asm
74+
```
75+
76+
It produces a file `example.asm` of Assembly. To turn this Assembly code into something that can be run, it needs to be further processed with the NASM Assembly compiler and linked:
77+
78+
```
79+
nasm -f elf64 example.asm
80+
gcc -no-pie -o example example.o lib/libclosure.a lib/libstandard.a
81+
```
82+
83+
Now we can run it:
84+
85+
```
86+
./example # Should print '2'
87+
```
88+
89+
## Feature showcase
90+
91+
Here's [an example program](examples/example_first_class.code) that shows closures and first-class functions in action:
92+
93+
```
94+
(let make-adder
95+
(λ x ; A function that returns a function
96+
(λ y (plus x y))) ; Creates a closure over x
97+
(let add5 (make-adder 5)
98+
(add5 3))) ; Prints '8'
99+
```
100+
101+
[This program](examples/example_mult.code) uses recursion to do integer multiplication:
102+
103+
```
104+
(defrec mult_aux ; Use 'defrec' to define a recursive function
105+
(lambda x y acc
106+
(if
107+
(equals x 1)
108+
acc
109+
(mult_aux (minus x 1) y (plus acc y))))) ; Recursion happens here
110+
111+
(def mult ; Wrapper for recursive function
112+
(lambda x y (mult_aux x y y)))
113+
114+
(mult 3 4) ; Prints '12'
115+
```
116+
117+
## Keywords
118+
119+
### `λ` (aka `lambda`)
120+
121+
Syntax is `(λ ARG_1 ... ARG_N RESULT)`. Example:
122+
123+
```
124+
(λ x y (plus x y)) ; Just adds x and y
125+
```
126+
127+
### `if`
128+
129+
Syntax is `(if PREDICATE TRUE-CASE FALSE-CASE)`. Example:
130+
131+
```
132+
(if (equals x 0) 0 (plus x 1)) ; Adds 1 to x unless it was 0
133+
```
134+
135+
### `let`/`letrec`
136+
137+
Syntax is `(let ARG DEFINITION BODY)`. Example:
138+
139+
```
140+
(let x 7
141+
(plus x 1)) ; Prints '8'
142+
```
143+
144+
`letrec` must be used when creating a recursive function.
145+
146+
### `def`/`defrec`
147+
148+
Syntactic sugar for a `let/letrec` wrapped around the last expression in the file. Example:
149+
150+
```
151+
(def double (λ x (plus x x )))
152+
153+
(double 2) ; Prints '4'
154+
```
155+
156+
## Built-in functions
157+
158+
[`standard.asm`](src/standard.asm) defines the following functions:
159+
160+
- `plus` (e.g.: `(plus 3 2)`, which returns 5)
161+
- `minus` (e.g.: `(minus 3 2)`, which returns 1)
162+
- `equals` (e.g.: `(equals 3 3)`, which returns 1)
163+
164+
... and that's it.
165+
166+
## Limitations
167+
168+
- Functions can have up to four arguments, and up to three of these can be free variables captured by closures. (These free variables include other functions produced by `let`/`letrec`/`def`/`defrec`.) This restriction is because the compiler uses the fastcall calling convention, which requires using named registers for the first few arguments and the stack after that, and I didn't implement passing arguments using the stack.
169+
- Integers (and functions) are the only data types. No floats, no strings, no lists ... You name it, it's not implemented.
170+
- Register use is about as inefficient as it could be: registers other than `rax` are almost unused, except when passing arguments.
171+
- No way of getting input from the user.
172+
- Only form of output beyond the automatic printing of the last expression.
173+
- Rampant memory leaks (both the compiler and the Assembly code it outputs).
174+
- No run-time checking to verify that calls are made only on functions.
175+
176+
## References
177+
178+
I found these resources useful when learning how to implement closures:
179+
180+
- [Closure conversion: How to compile lambda](http://matt.might.net/articles/closure-conversion/)
181+
- [Lecture 11: First-class Functions](https://course.ccs.neu.edu/cs4410/lec_lambdas_notes.html) of Northeastern University's course CS 4410/6410: Compiler Design
182+
183+
## Testing
184+
185+
That the [code examples](examples) produce the expected results can be verified by installing the [Bash Automated Testing System (BATS)](https://github.com/bats-core/bats-core) and running
186+
187+
```
188+
bats test/test.bats
189+
```

examples/example.code

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
; Comments start with semicolons
2+
; The last expression in the code file is printed
3+
(let inc (λ x (plus x 1))
4+
(inc 1))

examples/example_closure.code

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
((let y 3
2+
(lambda x y)) 1)

examples/example_compose.code

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
(def compose
2+
(λ f g
3+
(λ x (f (g x)))))
4+
5+
(def inc
6+
(λ x (plus x 1)))
7+
8+
(def double
9+
(λ x (plus x x)))
10+
11+
(let double-inc (compose double inc)
12+
(double-inc 1)) ; Outputs '4'

examples/example_defs.code

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
(def inc
2+
(lambda x (plus x 1)))
3+
4+
(def double
5+
(lambda x (plus x x)))
6+
7+
(double (inc 3))

examples/example_defs2.code

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
(def one
2+
(lambda 1))
3+
4+
(def constantly-one
5+
(lambda (one)))
6+
7+
(constantly-one 3)

examples/example_factorial.code

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
(defrec mult_aux ; Use 'defrec' to define a recursive function
2+
(lambda x y acc
3+
(if
4+
(equals x 1)
5+
acc
6+
(mult_aux (minus x 1) y (plus acc y))))) ; Recursion happens here
7+
8+
(def mult ; Wrapper for recursive function
9+
(lambda x y (mult_aux x y y)))
10+
11+
(defrec fac
12+
(lambda x
13+
(if
14+
(equals x 1)
15+
1
16+
(mult x (fac (minus x 1))))))
17+
18+
(fac 5) ; Prints 120

examples/example_first_class.code

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(let make-adder
2+
(λ x ; A function that returns a function
3+
(λ y (plus x y))) ; Creates a closure over x
4+
(let add5 (make-adder 5)
5+
(add5 3))) ; Prints '8'

examples/example_lambda.code

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
((lambda x (plus x 1)) 1)

examples/example_letrec.code

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(letrec f (lambda x
2+
(if (equals x 0)
3+
x
4+
(f (minus x 1))))
5+
(f 3))

examples/example_lets.code

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(let inc (lambda x (plus x 1))
2+
(let inc2 (lambda x (inc x))
3+
(inc2 3)))

examples/example_mult.code

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
(defrec mult_aux ; Use 'defrec' to define a recursive function
2+
(lambda x y acc
3+
(if
4+
(equals x 1)
5+
acc
6+
(mult_aux (minus x 1) y (plus acc y))))) ; Recursion happens here
7+
8+
(def mult ; Wrapper for recursive function
9+
(lambda x y (mult_aux x y y)))
10+
11+
(mult 3 4) ; Prints '12'

examples/example_negative.code

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
(plus -5 1)

examples/example_rec.code

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
(def plus2 (lambda x y (plus x y)))
2+
3+
(defrec f
4+
(lambda x
5+
(if
6+
(equals x 1)
7+
1
8+
(plus2 x (f (minus x 1))))))
9+
10+
(f 3) ; Prints 6

examples/example_unicode.code

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
((λ x (plus x 1)) 1)

0 commit comments

Comments
 (0)