purple-lang 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ *.gem
2
+
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
@@ -0,0 +1,41 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ purple (0.0.1)
5
+ treetop
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ awesome_print (1.1.0)
11
+ coderay (1.0.8)
12
+ diff-lcs (1.1.3)
13
+ method_source (0.8.1)
14
+ polyglot (0.3.3)
15
+ pry (0.9.10)
16
+ coderay (~> 1.0.5)
17
+ method_source (~> 0.8)
18
+ slop (~> 3.3.1)
19
+ rake (0.9.2.2)
20
+ rspec (2.11.0)
21
+ rspec-core (~> 2.11.0)
22
+ rspec-expectations (~> 2.11.0)
23
+ rspec-mocks (~> 2.11.0)
24
+ rspec-core (2.11.1)
25
+ rspec-expectations (2.11.3)
26
+ diff-lcs (~> 1.1.3)
27
+ rspec-mocks (2.11.3)
28
+ slop (3.3.3)
29
+ treetop (1.4.12)
30
+ polyglot
31
+ polyglot (>= 0.3.1)
32
+
33
+ PLATFORMS
34
+ ruby
35
+
36
+ DEPENDENCIES
37
+ awesome_print
38
+ pry
39
+ purple!
40
+ rake
41
+ rspec
@@ -0,0 +1,25 @@
1
+ purple
2
+ ======
3
+
4
+ a programming language being built for fun.
5
+
6
+ ## current features
7
+ - basic arithmetic
8
+ - variable assignment
9
+
10
+ ## run example
11
+ ```
12
+ bin/purple examples/basic.ppl
13
+ ```
14
+
15
+ ## install from gem
16
+ ```
17
+ sudo gem install purple
18
+ purple <source-file>
19
+ ```
20
+
21
+ ## run the tests
22
+ ```bash
23
+ bundle install && bundle exec rake spec
24
+ ```
25
+
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |t|
5
+ t.pattern = Dir.glob('spec/**/*_spec.rb')
6
+ t.rspec_opts = '--color'
7
+ end
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ $:.push File.join(File.dirname(__FILE__), '../lib')
3
+ require 'purple'
4
+
5
+ # TODO: proper commandline-fu (options, help, etc.)
6
+ raise "must provide a source file name as argument" unless ARGV.length > 0
7
+ source = File.read(ARGV.first)
8
+ puts Purple::SexpVM.new.evaluate(Parser.parse(source))
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ require 'ripper'
3
+ require 'ap'
4
+ require 'pry'
5
+
6
+ def sexp(code)
7
+ ap Ripper.sexp(code)
8
+ end
9
+
10
+ sexp "x = 5"
11
+
12
+ binding.pry
@@ -0,0 +1,5 @@
1
+
2
+ # an example program to test-drive purple
3
+
4
+ x = 2 * 2 + 15 / 5
5
+
@@ -0,0 +1,3 @@
1
+ require 'purple/parser'
2
+ require 'purple/sexp_vm'
3
+
@@ -0,0 +1,81 @@
1
+
2
+ module Purple
3
+ module Infix
4
+ # apply associativity rules and order of precedence to an sexp.
5
+ #
6
+ # e.g. this input:
7
+ # [ [:int, 5], :-, [:int, 2], :-, [:int, 1] ]
8
+ # becomes:
9
+ # [ :-, [:-, [:int, 5], [:int, 2]], [:int, 1] ]
10
+ def self.process_infix(unordered)
11
+ rpn(shunting_yard unordered)
12
+ end
13
+
14
+ module PrecedenceTable
15
+ Operator = Struct.new(:precedence, :associativity)
16
+
17
+ def self.infix_operator?(operator)
18
+ !lookup(operator).nil?
19
+ end
20
+
21
+ def self.lookup(operator)
22
+ @operators[operator]
23
+ end
24
+
25
+ def self.op(associativity, *operators)
26
+ @precedence ||= 0
27
+ @operators ||= {}
28
+ operators.each do |operator|
29
+ @operators[operator] = Operator.new(@precedence, associativity)
30
+ end
31
+ @precedence += 1
32
+ end
33
+
34
+ # operator precedence, low to high
35
+ #op :left, '||'
36
+ #op :left, '&&'
37
+ #op :none, '==', '!='
38
+ #op :left, '<', '<=', '>', '>='
39
+ op :left, :+, :-
40
+ op :left, :*, :/
41
+ #op :right, '^'
42
+ end
43
+
44
+ def self.rpn(input)
45
+ results = []
46
+ input.each do |object|
47
+ if PrecedenceTable.infix_operator? object
48
+ r, l = results.pop, results.pop
49
+ results << [object, l, r]
50
+ else
51
+ results << object
52
+ end
53
+ end
54
+ results.first
55
+ end
56
+
57
+ # given an infix expression, apply precedence and associativity
58
+ # result is in RPN
59
+ # http://en.wikipedia.org/wiki/Shunting-yard_algorithm
60
+ def self.shunting_yard(input)
61
+ [].tap do |rpn|
62
+ operator_stack = []
63
+ input.each do |object|
64
+ if PrecedenceTable.infix_operator? object
65
+ op1 = object
66
+ op1_left = PrecedenceTable.lookup(op1).associativity == :left
67
+ op1_prec = PrecedenceTable.lookup(op1).precedence
68
+ rpn << operator_stack.pop while (op2 = operator_stack.last) &&
69
+ (op2_prec = PrecedenceTable.lookup(op2).precedence) &&
70
+ (op1_left ? op1_prec <= op2_prec : op1_prec < op2_prec)
71
+ operator_stack << op1
72
+ else
73
+ rpn << object
74
+ end
75
+ end
76
+ rpn << operator_stack.pop until operator_stack.empty?
77
+ end
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,34 @@
1
+ require 'treetop'
2
+ require 'purple/syntax_nodes'
3
+
4
+ class Parser
5
+ Treetop.load File.join(File.expand_path(File.dirname __FILE__), 'purple_grammar.treetop')
6
+ @@parser = PurpleParser.new
7
+
8
+ def self.parse(code)
9
+ tree = @@parser.parse code
10
+ raise "Parse error at offset: #{@@parser.index} : #{@@parser.failure_reason}" if tree.nil?
11
+ clean! tree
12
+ tree.to_a
13
+ end
14
+
15
+ private
16
+
17
+ def self.clean!(root)
18
+ return if root.elements.nil?
19
+
20
+ # TODO: this is a hack - fix it
21
+ # treat infix operation chains specially
22
+ if root.class == Purple::InfixOperationChain
23
+ root.elements.map! { |e| [e.primary, e.infix_operator] }
24
+ root.elements.flatten!
25
+ end
26
+
27
+ # remove stuff which is irrelevant to AST
28
+ root.elements.reject! { |n| n.class == Treetop::Runtime::SyntaxNode }
29
+
30
+ root.elements.each { |n| clean! n }
31
+ end
32
+
33
+ end
34
+
@@ -0,0 +1,103 @@
1
+
2
+ grammar Purple
3
+
4
+ # program structure
5
+ rule program
6
+ (comment / statement)+ <Program>
7
+ end
8
+
9
+ rule statement
10
+ space? ( assignment ) space? <Statement> /
11
+ expression
12
+ end
13
+
14
+
15
+ # literals
16
+ rule nil
17
+ 'nil' <NilLiteral>
18
+ end
19
+
20
+ rule true
21
+ 'true' <TrueLiteral>
22
+ end
23
+
24
+ rule false
25
+ 'false' <FalseLiteral>
26
+ end
27
+
28
+ rule integer
29
+ ('+' / '-')? [0-9]+ <IntegerLiteral>
30
+ end
31
+
32
+ # assignment
33
+ rule assignment
34
+ identifier space? assignment_operator space? expression <Assignment>
35
+ end
36
+
37
+ # expressions
38
+ rule expression
39
+ space? (infix_expression / primary) <Expression>
40
+ end
41
+
42
+ rule infix_expression
43
+ infix_operation_chain primary <InfixExpression>
44
+ end
45
+
46
+ rule infix_operation_chain
47
+ (primary space? infix_operator space?)+ <InfixOperationChain>
48
+ end
49
+
50
+ rule primary
51
+ nil / true / false / identifier / integer
52
+ /
53
+ '(' expression ')' <Expression>
54
+ end
55
+
56
+ # operator sets
57
+ rule infix_operator
58
+ addition_operator / subtraction_operator / multiplication_operator / division_operator
59
+ end
60
+
61
+ # operators
62
+ rule assignment_operator
63
+ '=' <AssignmentOperator>
64
+ end
65
+
66
+ rule addition_operator
67
+ '+' <AdditionOperator>
68
+ end
69
+
70
+ rule subtraction_operator
71
+ '-' <SubtractionOperator>
72
+ end
73
+
74
+ rule multiplication_operator
75
+ '*' <MultiplicationOperator>
76
+ end
77
+
78
+ rule division_operator
79
+ '/' <DivisionOperator>
80
+ end
81
+
82
+ rule identifier
83
+ [a-zA-Z]+ <Identifier>
84
+ end
85
+
86
+ rule body
87
+ (expression / literal / space)* <Body>
88
+ end
89
+
90
+
91
+ rule space
92
+ [\s]+
93
+ end
94
+
95
+ rule comment
96
+ space? '#' [^"\n"]* ( "\n" / eof )
97
+ end
98
+
99
+ rule eof
100
+ !.
101
+ end
102
+
103
+ end
@@ -0,0 +1,49 @@
1
+
2
+
3
+ module Purple
4
+ class SexpVM
5
+
6
+ def initialize
7
+ @vars = {}
8
+ end
9
+
10
+ def evaluate(sexp)
11
+ f, *args = *sexp
12
+ #ap "x"*80
13
+ #ap f.inspect
14
+ #ap args.inspect
15
+ ret =case f
16
+ when :program
17
+ # TODO: eliminate redundant array surrounding each expr
18
+ ret = nil
19
+ args.each { |stmt| ret = evaluate stmt.first }
20
+ ret
21
+ when :assign
22
+ raise 'huh?' unless args.length == 2
23
+ ident, expr = args.first, args.last
24
+ name = ident.last
25
+ @vars[name] = evaluate expr
26
+ when :int
27
+ raise 'huh?' unless args.length == 1
28
+ args.first
29
+ when :+
30
+ raise 'huh?' unless args.length == 2
31
+ evaluate(args.first) + evaluate(args.last)
32
+ when :-
33
+ raise 'huh?' unless args.length == 2
34
+ evaluate(args.first) - evaluate(args.last)
35
+ when :*
36
+ raise 'huh?' unless args.length == 2
37
+ evaluate(args.first) * evaluate(args.last)
38
+ when :/
39
+ raise 'huh?' unless args.length == 2
40
+ evaluate(args.first) / evaluate(args.last)
41
+ when :ident
42
+ @vars[args.first]
43
+ else
44
+ raise "unknown sexp op: #{f}"
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,68 @@
1
+ require 'purple/infix.rb'
2
+
3
+ module Purple
4
+
5
+ # given a proc which generates ast for a node,
6
+ # create a subclass of Treetop::Runtime::SyntaxNode for it.
7
+ #
8
+ # e.g., these two are equivalent:
9
+ # IntegerLiteral = node_def -> { [:int, text_value.to_i] }
10
+ #
11
+ # class FalseLiteral < Treetop::Runtime::SyntaxNode
12
+ # def to_a
13
+ # [:int, text_value.to_i]
14
+ # end
15
+ # end
16
+ def self.node_def(to_ast)
17
+ klass = Class.new(Treetop::Runtime::SyntaxNode) do
18
+ @to_a_proc = to_ast
19
+ def to_a
20
+ self.instance_exec(&self.class.instance_variable_get(:@to_a_proc))
21
+ end
22
+ end
23
+ end
24
+
25
+ Program = node_def -> { [:program] + elements.map { |e| e.to_a } }
26
+
27
+ Statement = node_def -> { elements.map { |e| e.to_a } }
28
+
29
+ NilLiteral = node_def -> { [:nil] }
30
+
31
+ TrueLiteral = node_def -> { [:true] }
32
+
33
+ FalseLiteral = node_def -> { [:false] }
34
+
35
+ IntegerLiteral = node_def -> { [:int, text_value.to_i] }
36
+
37
+ Assignment = node_def -> { [:assign, elements.first.to_a, elements.last.to_a ] }
38
+
39
+ InfixExpression = node_def -> {
40
+ unordered = elements.first.elements.map { |e| e.to_a } << elements.last.to_a
41
+ Purple::Infix.process_infix unordered
42
+ }
43
+
44
+ InfixOperationChain = node_def -> { elements.map { |e| e.to_a } }
45
+
46
+ AssignmentOperator = node_def -> { }
47
+
48
+ AdditionOperator = node_def -> { :+ }
49
+
50
+ SubtractionOperator = node_def -> { :- }
51
+
52
+ MultiplicationOperator = node_def -> { :* }
53
+
54
+ DivisionOperator = node_def -> { :/ }
55
+
56
+ Identifier = node_def -> { [:ident, text_value] }
57
+
58
+ Expression = node_def -> {
59
+ # TODO: fix this shit
60
+ if elements.first.class == InfixExpression
61
+ elements.first.to_a
62
+ else
63
+ elements.map { |e| e.to_a }
64
+ end
65
+ }
66
+
67
+ end
68
+
@@ -0,0 +1,27 @@
1
+ #
2
+ # -*- encoding: utf-8 -*-
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "purple-lang"
6
+ s.version = "0.0.1"
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = [ "Tim Miller" ]
9
+ s.email = [ "" ]
10
+ s.homepage = "https://github.com/echohead/purple"
11
+ s.summary = %q{programming language sandbox}
12
+ s.description = %q{}
13
+
14
+ s.required_ruby_version = ">= 1.9.3"
15
+ s.required_rubygems_version = ">= 1.3.7"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_runtime_dependency "treetop", ">= 0"
23
+ s.add_development_dependency "rake", ">= 0"
24
+ s.add_development_dependency "rspec", ">= 0"
25
+ s.add_development_dependency "awesome_print", ">= 0"
26
+ s.add_development_dependency "pry", ">= 0"
27
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'booleans' do
4
+
5
+ it 'should parse into sexp' do
6
+ sexp('true').should == [:true]
7
+ sexp('false').should == [:false]
8
+ end
9
+
10
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'comments' do
4
+ it 'should parse full-lines' do
5
+ parse("# a comment\nfoo").should == [:program, [[:ident, "foo"]]]
6
+ end
7
+
8
+ it 'should parse at end of line' do
9
+ parse("foo # <- the foo").should == [:program, [[:ident, "foo"]]]
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'identifiers' do
4
+ it 'should parse' do
5
+ sexp('foobar').should == [:ident, "foobar"]
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'infix' do
4
+
5
+ it 'does left-associativity' do
6
+ unordered = [ [:int, 5], :-, [:int, 2], :-, [:int, 1] ]
7
+ out = [ :-, [:-, [:int, 5], [:int, 2]], [:int, 1] ]
8
+ Purple::Infix.process_infix(unordered).should == out
9
+ end
10
+
11
+ it 'does order of operations' do
12
+ unordered = [ [:int, 3], :+, [:int, 2], :*, [:int, 5] ]
13
+ out = [ :+, [:int, 3], [:*, [:int, 2], [:int, 5]] ]
14
+ Purple::Infix.process_infix(unordered).should == out
15
+ end
16
+
17
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+ require 'purple'
3
+
4
+ describe 'integer' do
5
+
6
+ it 'should parse into sexp' do
7
+ sexp('3').should == [:int, 3]
8
+ sexp('0').should == [:int, 0]
9
+ sexp('-512').should == [:int, -512]
10
+ sexp(' 1003456').should == [:int, 1003456]
11
+ end
12
+
13
+ end
14
+
15
+
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'nil' do
4
+
5
+ it 'should parse into sexp' do
6
+ sexp('nil').should == [:nil]
7
+ sexp(' nil').should == [:nil]
8
+ end
9
+
10
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ code = <<-EOS
4
+
5
+ # an example program
6
+
7
+ x = 5 - 2 - 1
8
+ EOS
9
+
10
+
11
+ sexp = \
12
+ [:program,
13
+ [
14
+ # x = 5 - 2 - 1
15
+ [:assign,
16
+ [:ident, "x"],
17
+ [:-,
18
+ [:-,
19
+ [:int, 5],
20
+ [:int, 2]
21
+ ],
22
+ [:int, 1]
23
+ ]
24
+ ]
25
+ ]
26
+ ]
27
+
28
+ describe 'sandbox' do
29
+ it 'should work' do
30
+ parse(code).should == sexp
31
+ end
32
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ describe Purple::SexpVM do
5
+
6
+ it 'should return the value of an assignment' do
7
+ Purple::SexpVM.new.evaluate([:assign, [:ident, 'x'], [:int, 5]]).should == 5
8
+ end
9
+
10
+ it 'should do basic arithmetic' do
11
+ evaluate("x = 1 + 1").should == 2
12
+ evaluate("x = 10 / 2 - 7 + 3 * 4 + 1\nx").should == 11
13
+ evaluate("x = (3 - 1) * 2 + (5 * (7 - 2))\nx").should == 29
14
+ end
15
+
16
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'source layout' do
4
+
5
+ it 'should allow empty lines' do
6
+ sexp("\n\nx = \n1\n").should == [:assign, [:ident, "x"], [[:int, 1]]]
7
+ end
8
+
9
+ end
@@ -0,0 +1,18 @@
1
+ require 'purple'
2
+ require 'rspec'
3
+ require 'ap'
4
+
5
+ # parse a chunk of code and return its AST representation
6
+ def parse(code)
7
+ Parser.parse code
8
+ end
9
+
10
+ # return AST of a single expression, without surrounding boilerplate
11
+ def sexp(code)
12
+ parse(code).last.first
13
+ end
14
+
15
+ # evaluate a block of code and return its result
16
+ def evaluate(code)
17
+ Purple::SexpVM.new.evaluate parse(code)
18
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: purple-lang
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tim Miller
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: treetop
16
+ requirement: &6468740 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *6468740
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &6467760 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *6467760
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &6467220 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *6467220
47
+ - !ruby/object:Gem::Dependency
48
+ name: awesome_print
49
+ requirement: &6466380 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *6466380
58
+ - !ruby/object:Gem::Dependency
59
+ name: pry
60
+ requirement: &6465720 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *6465720
69
+ description: ''
70
+ email:
71
+ - ''
72
+ executables:
73
+ - purple
74
+ - ruby_sexp.rb
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - .gitignore
79
+ - Gemfile
80
+ - Gemfile.lock
81
+ - README.md
82
+ - Rakefile
83
+ - bin/purple
84
+ - bin/ruby_sexp.rb
85
+ - examples/basic.ppl
86
+ - lib/purple.rb
87
+ - lib/purple/infix.rb
88
+ - lib/purple/parser.rb
89
+ - lib/purple/purple_grammar.treetop
90
+ - lib/purple/sexp_vm.rb
91
+ - lib/purple/syntax_nodes.rb
92
+ - purple-lang.gemspec
93
+ - spec/boolean_spec.rb
94
+ - spec/comment_spec.rb
95
+ - spec/identifier_spec.rb
96
+ - spec/infix_spec.rb
97
+ - spec/integer_spec.rb
98
+ - spec/nil_spec.rb
99
+ - spec/sandbox_spec.rb
100
+ - spec/sexp_vm_spec.rb
101
+ - spec/source_layout_spec.rb
102
+ - spec/spec_helper.rb
103
+ homepage: https://github.com/echohead/purple
104
+ licenses: []
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ! '>='
113
+ - !ruby/object:Gem::Version
114
+ version: 1.9.3
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: 1.3.7
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 1.8.11
124
+ signing_key:
125
+ specification_version: 3
126
+ summary: programming language sandbox
127
+ test_files: []