├── .gitignore
├── Gemfile
├── README.rdoc
├── Rakefile
├── bin
└── lisp
├── features
├── arithmetic.feature
├── booleans.feature
├── conditionals.feature
├── functions.feature
├── numbers.feature
├── step_definitions
│ └── lisp_steps.rb
├── tail_calls.feature
└── variables.feature
├── lib
├── lisp.citrus
├── ruby_lisp.rb
└── ruby_lisp
│ ├── boolean.rb
│ ├── function.rb
│ ├── identifier.rb
│ ├── list.rb
│ ├── number.rb
│ └── scope.rb
├── showoff.json
└── slides
└── slides.md
/.gitignore:
--------------------------------------------------------------------------------
1 | Gemfile.lock
2 | .redcar
3 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'http://rubygems.org'
2 |
3 | gem 'rake'
4 | gem 'citrus'
5 | gem 'cucumber'
6 | gem 'rspec'
7 | gem 'showoff'
8 |
9 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = Lisp dojo
2 |
3 | This is a coding exercise designed to introduce you to writing a programming
4 | language. It contains Cucumber tests that guide you through building a simple
5 | Lisp-like interpreter with the following features:
6 |
7 | * Boolean and numeric literals
8 | * User-defined variables
9 | * Arithmetic functions
10 | * Conditional logic
11 | * User-defined first-class functions
12 | * Lexical closures
13 |
14 | To get started, check out a copy and run
15 |
16 | bundle install
17 |
18 | Then run the first set of tests:
19 |
20 | rake step[1]
21 |
22 | This will give you a bunch of failing tests. It is your job to fix the tests,
23 | commit your code, and move on to the next step. There are 10 steps, and the rake
24 | task runs all the tests up to the current step to make sure you don't break
25 | anything as you progress.
26 |
27 | The exercise is focused on interpretation, not parsing. A parser (based on
28 | Citrus) is provided, and it is up to you to add meaning to the parse tree by
29 | interpreting it.
30 |
31 | You can interact with the language as you build it up by running bin/lisp,
32 | which drops you into a REPL where you can execute expressions in the language.
33 |
34 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | task :step, :n do |t, args|
2 | max_step = args[:n].to_i
3 | tags = (1..max_step).map { |i| "@step#{i}" }.join ','
4 | system "cucumber --tags #{tags}"
5 | end
6 |
7 |
--------------------------------------------------------------------------------
/bin/lisp:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env ruby
2 | require 'rubygems'
3 | require 'bundler/setup'
4 | require 'readline'
5 | require File.dirname(__FILE__) + '/../lib/ruby_lisp'
6 |
7 | trap('INT') { exit }
8 |
9 | scope = RubyLisp::TopLevelScope.new
10 |
11 | loop do
12 | source = Readline.readline('>> ')
13 | Readline::HISTORY << source
14 |
15 | begin
16 | result = RubyLisp.parse(source).eval(scope)
17 | raise 'Got NIL -- need to write more code!' if result.nil?
18 | puts "=> #{result}"
19 | rescue Object => e
20 | puts "[ERROR] #{e}"
21 | end
22 | end
23 |
24 |
--------------------------------------------------------------------------------
/features/arithmetic.feature:
--------------------------------------------------------------------------------
1 | @step4
2 | Feature: Arithmetic functions
3 |
4 | Scenario Outline: Arithmetic operators
5 | When I run the program
6 | """
7 |
8 | """
9 | Then I should get ""
10 | Examples:
11 | | program | result |
12 | | (+ 3 4) | 7 |
13 | | (- 8 2) | 6 |
14 | | (* 9 7 ) | 63 |
15 | | (/ 12 4) | 3 |
16 |
17 | Scenario Outline: Comparison operators
18 | When I run the program
19 | """
20 |
21 | """
22 | Then I should get ""
23 | Examples:
24 | | program | result |
25 | | (> 4 3) | true |
26 | | (> 7 8) | false |
27 | | (>= 3 3) | true |
28 | | (>= 3 5) | false |
29 | | (< 4 3) | false |
30 | | (< 7 8) | true |
31 | | (<= 3 3) | true |
32 | | (<= 5 3) | false |
33 | | (= 7 7) | true |
34 | | (= 9 2) | false |
35 |
36 | Scenario: Compound expression
37 | When I run the program
38 | """
39 | (* (+ (- 9 3)
40 | 2)
41 | (/ 36 9))
42 | """
43 | Then I should get "32"
44 |
45 |
--------------------------------------------------------------------------------
/features/booleans.feature:
--------------------------------------------------------------------------------
1 | @step1
2 | Feature: Boolean literals
3 |
4 | Scenario: The true literal
5 | When I run the program
6 | """
7 | #t
8 | """
9 | Then I should get "true"
10 |
11 | Scenario: The false literal
12 | When I run the program
13 | """
14 | #f
15 | """
16 | Then I should get "false"
17 |
18 |
--------------------------------------------------------------------------------
/features/conditionals.feature:
--------------------------------------------------------------------------------
1 | @step6
2 | Feature: Conditional logic
3 |
4 | Scenario: Simple if statement, true branch
5 | When I run the program
6 | """
7 | (if #t 33 45)
8 | """
9 | Then I should get "33"
10 |
11 | Scenario: Simple if statement, false branch
12 | When I run the program
13 | """
14 | (if #f 33 45)
15 | """
16 | Then I should get "45"
17 |
18 | Scenario: if statement with expressions
19 | When I run the program
20 | """
21 | (if (>= 9 4)
22 | (+ 3 2)
23 | (* 8 9))
24 | """
25 | Then I should get "5"
26 |
27 | Scenario: Do not eval both branches
28 | When I run the program
29 | """
30 | (define x 21)
31 |
32 | (if #t
33 | (define x 99)
34 | (define x 55))
35 |
36 | x
37 | """
38 | Then I should get "99"
39 |
40 |
--------------------------------------------------------------------------------
/features/functions.feature:
--------------------------------------------------------------------------------
1 | Feature: User-defined functions
2 |
3 | @step7
4 | Scenario: lambda creates a function
5 | When I run the program
6 | """
7 | (lambda () (+ 5 6))
8 | """
9 | Then I should get a function
10 |
11 | @step8
12 | Scenario: Calling a 0-ary one-line function
13 | When I run the program
14 | """
15 | (define nine (lambda () (+ 4 5)))
16 | (nine)
17 | """
18 | Then I should get "9"
19 |
20 | @step8
21 | Scenario: Calling a 0-ary multiline function
22 | When I run the program
23 | """
24 | (define ten (lambda ()
25 | (define x 7)
26 | (define y 3)
27 | (+ x y)))
28 | (ten)
29 | """
30 | Then I should get "10"
31 |
32 | @step9
33 | Scenario: Calling a 2-ary function
34 | When I run the program
35 | """
36 | (define max (lambda (x y)
37 | (if (> x y)
38 | x
39 | y)))
40 |
41 | (max 3 6)
42 | """
43 | Then I should get "6"
44 |
45 | @step9
46 | Scenario: Variables are scoped to the function
47 | When I run the program
48 | """
49 | (define square (lambda (x) (* x x)))
50 |
51 | (define x 8)
52 | (square 3)
53 | x
54 | """
55 | Then I should get "8"
56 |
57 | @step9
58 | Scenario: Functions can recurse
59 | When I run the program
60 | """
61 | (define factorial (lambda (x)
62 | (if (= 0 x)
63 | 1
64 | (* (factorial (- x 1))
65 | x))))
66 | (factorial 6)
67 | """
68 | Then I should get "720"
69 |
70 | @step10
71 | Scenario: Functions are lexical closures
72 | When I run the program
73 | """
74 | (define add (lambda (x)
75 | (lambda (y)
76 | (+ x y))))
77 |
78 | (define add2 (add 2))
79 | (add2 4)
80 | """
81 | Then I should get "6"
82 |
83 |
--------------------------------------------------------------------------------
/features/numbers.feature:
--------------------------------------------------------------------------------
1 | @step2
2 | Feature: Number literals
3 |
4 | Scenario: Integer literal
5 | When I run the program
6 | """
7 | 234
8 | """
9 | Then I should get "234"
10 |
11 | Scenario: Float literal
12 | When I run the program
13 | """
14 | 3.14
15 | """
16 | Then I should get "3.14"
17 |
18 | Scenario: Negative literal
19 | When I run the program
20 | """
21 | -0.17
22 | """
23 | Then I should get "-0.17"
24 |
25 |
--------------------------------------------------------------------------------
/features/step_definitions/lisp_steps.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'bundler/setup'
3 | require 'rspec'
4 |
5 | require File.dirname(__FILE__) + '/../../lib/ruby_lisp'
6 |
7 | Before do
8 | @scope = RubyLisp::TopLevelScope.new
9 | end
10 |
11 | Given /^the variable "([^"]*)" has value "([^"]*)"$/ do |varname, value|
12 | @scope[varname] = eval(value)
13 | end
14 |
15 | When /^I run the program$/ do |source|
16 | begin
17 | @result = RubyLisp.parse(source).eval(@scope)
18 | rescue => e
19 | @error = e
20 | end
21 | end
22 |
23 | Then /^I should get "([^"]*)"$/ do |result|
24 | if @error
25 | p @error
26 | puts @error.backtrace
27 | end
28 | @error.should be_nil
29 | @result.should == eval(result)
30 | end
31 |
32 | Then /^I should get a function$/ do
33 | if @error
34 | p @error
35 | puts @error.backtrace
36 | end
37 | @error.should be_nil
38 | @result.should be_kind_of(RubyLisp::Function)
39 | end
40 |
41 | Then /^I should get an error$/ do
42 | @error.should be
43 | end
44 |
45 |
--------------------------------------------------------------------------------
/features/tail_calls.feature:
--------------------------------------------------------------------------------
1 | @step11
2 | Feature: Tail call optimization
3 |
4 | Scenario: Tail-recursive function
5 | When I run the program
6 | """
7 | (define add (lambda (n x)
8 | (if (= 0 n)
9 | x
10 | (add (- n 1)
11 | (+ x 1)))))
12 |
13 | (add 10000 1)
14 | """
15 | Then I should get "10001"
16 |
--------------------------------------------------------------------------------
/features/variables.feature:
--------------------------------------------------------------------------------
1 | Feature: Variables
2 |
3 | @step3
4 | Scenario: Evaluate a known variable
5 | Given the variable "the-var" has value "10"
6 | When I run the program
7 | """
8 | the-var
9 | """
10 | Then I should get "10"
11 |
12 | @step3
13 | Scenario: Evaluate an undefined variable
14 | When I run the program
15 | """
16 | unknown-var
17 | """
18 | Then I should get an error
19 |
20 | @step3
21 | Scenario: Evaluate a function reference
22 | When I run the program
23 | """
24 | +
25 | """
26 | Then I should get a function
27 |
28 | @step5
29 | Scenario: Defining a variable
30 | When I run the program
31 | """
32 | (define my-var 15)
33 | my-var
34 | """
35 | Then I should get "15"
36 |
37 | @step5
38 | Scenario: Storing a result in a variable
39 | When I run the program
40 | """
41 | (define k (+ (/ 21 3) 2))
42 | k
43 | """
44 | Then I should get "9"
45 |
46 |
--------------------------------------------------------------------------------
/lib/lisp.citrus:
--------------------------------------------------------------------------------
1 | grammar Lisp
2 | rule program
3 | cell*
4 | end
5 |
6 | rule cell
7 | (space* expression space*)
8 | end
9 |
10 | rule expression
11 | list | atom
12 | end
13 |
14 | rule atom
15 | (expression:(boolean | number | identifier) !(!delimiter .))
16 | end
17 |
18 | rule list
19 | ('(' cell* ')')
20 | end
21 |
22 | rule boolean
23 | ('#t' | '#f')
24 | end
25 |
26 | rule number
27 | ('-'? ('0' | [1-9] [0-9]*) ('.' [0-9]+)?)
28 | end
29 |
30 | rule identifier
31 | (~delimiter)+
32 | end
33 |
34 | rule delimiter
35 | '#' | paren | space
36 | end
37 |
38 | rule paren
39 | '(' | ')'
40 | end
41 |
42 | rule space
43 | [\s\n\r\t]
44 | end
45 | end
46 |
47 |
--------------------------------------------------------------------------------
/lib/ruby_lisp.rb:
--------------------------------------------------------------------------------
1 | module RubyLisp
2 | def self.parse(source)
3 | Lisp.parse(source)
4 | end
5 |
6 | ROOT = File.expand_path(File.dirname(__FILE__))
7 |
8 | autoload :Scope, ROOT + '/ruby_lisp/scope'
9 | autoload :TopLevelScope, ROOT + '/ruby_lisp/scope'
10 | autoload :Function, ROOT + '/ruby_lisp/function'
11 |
12 | autoload :Boolean, ROOT + '/ruby_lisp/boolean'
13 | autoload :List, ROOT + '/ruby_lisp/list'
14 | autoload :Number, ROOT + '/ruby_lisp/number'
15 | autoload :Identifier, ROOT + '/ruby_lisp/identifier'
16 |
17 | module Program
18 | def eval(scope)
19 | captures[:cell].map { |cell| cell.eval(scope) }.last
20 | end
21 | end
22 |
23 | module Cell
24 | def value
25 | expression.value
26 | end
27 |
28 | def eval(scope)
29 | expression.eval(scope)
30 | end
31 | end
32 | end
33 |
34 | require 'citrus'
35 | Citrus.require(RubyLisp::ROOT + '/lisp.citrus')
36 |
37 | def parse(source)
38 | RubyLisp.parse(source)
39 | end
40 |
41 |
--------------------------------------------------------------------------------
/lib/ruby_lisp/boolean.rb:
--------------------------------------------------------------------------------
1 | module RubyLisp::Boolean
2 | def eval(scope)
3 |
4 | end
5 | end
6 |
7 |
--------------------------------------------------------------------------------
/lib/ruby_lisp/function.rb:
--------------------------------------------------------------------------------
1 | module RubyLisp
2 | class Function
3 | end
4 | end
5 |
6 |
--------------------------------------------------------------------------------
/lib/ruby_lisp/identifier.rb:
--------------------------------------------------------------------------------
1 | module RubyLisp::Identifier
2 | def eval(scope)
3 |
4 | end
5 | end
6 |
7 |
--------------------------------------------------------------------------------
/lib/ruby_lisp/list.rb:
--------------------------------------------------------------------------------
1 | module RubyLisp::List
2 | def eval(scope)
3 |
4 | end
5 | end
6 |
7 |
--------------------------------------------------------------------------------
/lib/ruby_lisp/number.rb:
--------------------------------------------------------------------------------
1 | module RubyLisp::Number
2 | def eval(scope)
3 |
4 | end
5 | end
6 |
7 |
--------------------------------------------------------------------------------
/lib/ruby_lisp/scope.rb:
--------------------------------------------------------------------------------
1 | module RubyLisp
2 | class Scope
3 | end
4 |
5 | class TopLevelScope < Scope
6 | end
7 | end
8 |
9 |
--------------------------------------------------------------------------------
/showoff.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Build you a Lisp",
3 | "sections": [
4 | {"section":"slides"}
5 | ]
6 | }
7 |
8 |
--------------------------------------------------------------------------------
/slides/slides.md:
--------------------------------------------------------------------------------
1 | !SLIDE center
2 | # Build you a Lisp
3 |
4 | !SLIDE bullets
5 | # Our Lisp will have
6 |
7 | * Booleans
8 | * Numbers and arithmetic
9 | * Variables
10 | * Conditional logic
11 | * User-defined functions, closures
12 | * Tail call optimization?
13 |
14 | !SLIDE
15 | # Booleans
16 |
17 | @@@ ruby
18 | '#t' == true
19 | '#f' == false
20 |
21 | !SLIDE
22 | # Numbers
23 |
24 | @@@ ruby
25 | 7, 3.14, -0.236 # etc.
26 |
27 | !SLIDE
28 | # Arithmetic
29 |
30 | @@@ ruby
31 | (+ 3 4) ; -> 7
32 | (/ 12 6) ; -> 2
33 |
34 | (> 7 2) ; -> #t
35 |
36 | (= 5 5) ; -> #t
37 | (= 0 4) ; -> #f
38 |
39 | !SLIDE
40 | # Variables
41 |
42 | @@@ ruby
43 | (define x 6)
44 | x ; -> 6
45 |
46 | (+ x 1) ; -> 7
47 |
48 | (- (* x 2) 3) ; -> 9
49 |
50 | !SLIDE
51 | # Conditional logic
52 |
53 | @@@ ruby
54 | (define x 0)
55 | (if (= x 0) 2 3) ; -> 2
56 |
57 | (define x 1)
58 | (if (= x 0) 2 3) ; -> 3
59 |
60 | !SLIDE
61 | # Functions
62 |
63 | @@@
64 | (lambda () (+ 1 1)) ; -> #
65 |
66 | (define max (lambda (x y)
67 | (if (> x y)
68 | x
69 | y)))
70 |
71 | (max 3 4) ; -> 4
72 |
73 | !SLIDE
74 | # Closures
75 |
76 | @@@ ruby
77 | (define add (lambda (x)
78 | (lambda (y)
79 | (+ x y))))
80 |
81 | (define add-2 (add 2)) ; -> #
82 |
83 | (add-2 5) ; -> 7
84 |
85 | !SLIDE
86 | # (Extra credit) Tail calls
87 |
88 | @@@ ruby
89 | (define add (lambda (n x)
90 | (if (= n 0)
91 | x
92 | (add (- n 1)
93 | (+ x 1)))))
94 |
95 | (add 3000 1)
96 | == (add 2999 2)
97 | == (add 2998 3)
98 |
--------------------------------------------------------------------------------