├── .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 | --------------------------------------------------------------------------------