purple-lang 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []