matthewtodd-doily 0.1.0

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,157 @@
1
+ class Doily::Parser
2
+
3
+ token FUNCTION IF ELSE FOR VAR BOOLEAN_LITERAL IDENTIFIER STRING_LITERAL INTEGER_LITERAL BINARY_OPERATOR
4
+
5
+ rule
6
+ target
7
+ : function_definition
8
+ ;
9
+
10
+ function_definition
11
+ : FUNCTION '(' argument_name_list ')' block { result = Function.new(val[2], val[4]) }
12
+ ;
13
+
14
+ argument_name_list
15
+ : { result = [] }
16
+ | IDENTIFIER { result = [val[0]] }
17
+ | IDENTIFIER ',' argument_name_list { result = [val[0]] + val[2] }
18
+ ;
19
+
20
+ block
21
+ : '{' '}' { result = Block.new([]) }
22
+ | '{' statement_list '}' { result = Block.new(val[1]) }
23
+ ;
24
+
25
+ statement_list
26
+ : statement { result = [val[0]] }
27
+ | statement statement_list { result = [val[0]] + val[1] }
28
+ ;
29
+
30
+ statement
31
+ : if_statement
32
+ | for_loop
33
+ | expression ';'
34
+ ;
35
+
36
+ if_statement
37
+ : IF '(' expression ')' block { result = Conditional.new(val[2], val[4]) }
38
+ | IF '(' expression ')' block ELSE block { result = Conditional.new(val[2], val[4], val[6]) }
39
+ ;
40
+
41
+ for_loop
42
+ : FOR '(' expression ';' expression ';' expression ')' block { result = Loop.new(val[2], val[4], val[6], val[8]) }
43
+ ;
44
+
45
+ expression
46
+ : reference
47
+ | declaration
48
+ | assignment
49
+ | binary_expression
50
+ | increment
51
+ ;
52
+
53
+ reference
54
+ : variable
55
+ | INTEGER_LITERAL { result = Literal.new(val[0].to_i) }
56
+ | BOOLEAN_LITERAL { result = Literal.new(eval(val[0])) }
57
+ | string_literal
58
+ | '{' key_value_list '}' { result = Object.new(val[1]) }
59
+ | reference '.' IDENTIFIER { result = Access.new(val[0], Literal.new(val[2])) }
60
+ | reference '[' reference ']' { result = Access.new(val[0], val[2]) }
61
+ | reference '(' argument_list ')' { result = Call.new(val[0], val[2]) }
62
+ ;
63
+
64
+ variable
65
+ : IDENTIFIER { result = Reference.new(val[0]) }
66
+ ;
67
+
68
+ string_literal
69
+ : STRING_LITERAL { result = Literal.new(eval(val[0])) }
70
+ ;
71
+
72
+ key_value_list
73
+ : { result = {} }
74
+ | key_value { result = val[0] }
75
+ | key_value ',' key_value_list { result = val[0].merge(val[2]) }
76
+ ;
77
+
78
+ key_value
79
+ : string_literal ':' reference { result = { val[0] => val[2] }}
80
+ ;
81
+
82
+ argument_list
83
+ : { result = [] }
84
+ | expression { result = [val[0]] }
85
+ | expression ',' argument_list { result = [val[0]] + val[2] }
86
+ ;
87
+
88
+ assignment
89
+ : expression '=' reference { result = Assignment.new(val[0], val[2]) }
90
+ ;
91
+
92
+ declaration
93
+ : VAR IDENTIFIER { result = Declaration.new(val[1]) }
94
+ ;
95
+
96
+ binary_expression
97
+ : reference BINARY_OPERATOR reference { result = Call.new(Access.new(val[0], Literal.new(val[1])), [val[2]]) }
98
+ ;
99
+
100
+ increment
101
+ : variable '++' { result = Assignment.new(val[0], Call.new(Access.new(val[0], Literal.new('+')), [Literal.new(1)])) }
102
+ ;
103
+
104
+ ---- header ----
105
+ require 'strscan'
106
+ ---- inner ----
107
+
108
+ def self.function(string)
109
+ new.parse(string)
110
+ end
111
+
112
+ def parse(string)
113
+ @tokens = []
114
+ scanner = StringScanner.new(string)
115
+
116
+ until scanner.empty?
117
+ case
118
+ when scanner.scan(/\s+/)
119
+ # ignore space
120
+ when m = scanner.scan(/function/)
121
+ @tokens.push [:FUNCTION, m]
122
+ when m = scanner.scan(/if/)
123
+ @tokens.push [:IF, m]
124
+ when m = scanner.scan(/else/)
125
+ @tokens.push [:ELSE, m]
126
+ when m = scanner.scan(/for/)
127
+ @tokens.push [:FOR, m]
128
+ when m = scanner.scan(/var/)
129
+ @tokens.push [:VAR, m]
130
+ when m = scanner.scan(/true|false/)
131
+ @tokens.push [:BOOLEAN_LITERAL, m]
132
+ when m = scanner.scan(/==|</)
133
+ @tokens.push [:BINARY_OPERATOR, m]
134
+ when m = scanner.scan(/\+\+/)
135
+ @tokens.push [m, m]
136
+ when m = scanner.scan(/[(){}\[\],\.:;=]/)
137
+ @tokens.push [m, m]
138
+ when m = scanner.scan(/[a-zA-Z_]+/)
139
+ @tokens.push [:IDENTIFIER, m]
140
+ when m = scanner.scan(/"([^"])*"/)
141
+ @tokens.push [:STRING_LITERAL, m]
142
+ when m = scanner.scan(/'([^'])*'/)
143
+ @tokens.push [:STRING_LITERAL, m]
144
+ when m = scanner.scan(/\d+/)
145
+ @tokens.push [:INTEGER_LITERAL, m]
146
+ else
147
+ raise ParseError.new(scanner)
148
+ end
149
+ end
150
+
151
+ @tokens.push [false, false]
152
+ do_parse
153
+ end
154
+
155
+ def next_token
156
+ @tokens.shift
157
+ end
@@ -0,0 +1,5 @@
1
+ Dir.chdir(File.join(File.dirname(__FILE__), '..')) do
2
+ Dir['doily/types/*.rb'].each do |filename|
3
+ require filename
4
+ end
5
+ end
@@ -0,0 +1,35 @@
1
+ module Doily
2
+ class Access
3
+ def initialize(target, name)
4
+ @target = target
5
+ @name = name
6
+ end
7
+
8
+ def assign(reference, binding)
9
+ ruby_target = @target.to_ruby(binding)
10
+ ruby_name = @name.to_ruby(binding)
11
+ ruby_reference = reference.to_ruby(binding)
12
+
13
+ if ruby_target.respond_to?(:has_key?)
14
+ ruby_target.store(ruby_name, ruby_reference)
15
+ else
16
+ ruby_target.send("#{ruby_name}=", ruby_reference)
17
+ end
18
+ end
19
+
20
+ def to_ruby(binding)
21
+ ruby_target = @target.to_ruby(binding)
22
+ ruby_name = @name.to_ruby(binding)
23
+
24
+ if ruby_target.respond_to?(ruby_name.to_s)
25
+ if ruby_name == 'length'
26
+ ruby_target.length
27
+ else
28
+ ruby_target.method(ruby_name)
29
+ end
30
+ else
31
+ ruby_target[ruby_name]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ module Doily
2
+ class Assignment
3
+ def initialize(target, reference)
4
+ @target = target
5
+ @reference = reference
6
+ end
7
+
8
+ def to_ruby(binding)
9
+ @target.assign(@reference, binding)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,41 @@
1
+ module Doily
2
+ class DelegateBinding
3
+ def initialize(delegate)
4
+ @delegate = delegate
5
+ end
6
+
7
+ def fetch(name)
8
+ @delegate.method(name)
9
+ end
10
+ end
11
+
12
+ class Binding
13
+ def initialize(parent, contents={})
14
+ @parent = parent
15
+ @contents = contents
16
+ end
17
+
18
+ def fetch(name)
19
+ if @contents.has_key?(name)
20
+ @contents[name]
21
+ else
22
+ @parent.fetch(name)
23
+ end
24
+ end
25
+ end
26
+
27
+ class ArgumentBinding < Binding
28
+ def initialize(parent, names, values)
29
+ raise ArgumentError.new(names, values) unless names.length == values.length
30
+ contents = {}
31
+ names.zip(values).each { |key, value| contents[key] = value }
32
+ super parent, contents
33
+ end
34
+ end
35
+
36
+ class LocalBinding < Binding
37
+ def store(name, value)
38
+ @contents[name] = value
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,17 @@
1
+ module Doily
2
+ class Block
3
+ def initialize(statements = [])
4
+ @statements = statements
5
+ end
6
+
7
+ def to_ruby(binding)
8
+ binding = LocalBinding.new(binding)
9
+
10
+ result = nil
11
+ @statements.each do |statement|
12
+ result = statement.to_ruby(binding)
13
+ end
14
+ result
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ module Doily
2
+ class Call
3
+ def initialize(target, args)
4
+ @target = target
5
+ @args = args
6
+ end
7
+
8
+ def to_ruby(binding)
9
+ @target.to_ruby(binding).call(*@args.map { |arg| arg.to_ruby(binding) })
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ module Doily
2
+ class Conditional
3
+ def initialize(conditional, true_block, false_block = Block.new)
4
+ @conditional = conditional
5
+ @true_block = true_block
6
+ @false_block = false_block
7
+ end
8
+
9
+ def to_ruby(binding)
10
+ if @conditional.to_ruby(binding)
11
+ @true_block.to_ruby(binding)
12
+ else
13
+ @false_block.to_ruby(binding)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module Doily
2
+ class Declaration
3
+ def initialize(name)
4
+ @name = name
5
+ end
6
+
7
+ def assign(reference, binding)
8
+ binding.store(@name, reference.to_ruby(binding))
9
+ end
10
+
11
+ def to_ruby(binding)
12
+ binding.store(@name, nil)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ module Doily
2
+ class Function
3
+ def initialize(parameters, block)
4
+ @parameters = parameters
5
+ @block = block
6
+ end
7
+
8
+ def bind(binding)
9
+ BoundFunction.new(@parameters, @block, binding)
10
+ end
11
+
12
+ def call(*args)
13
+ bind(nil).call(*args)
14
+ end
15
+
16
+ def delegate(delegate)
17
+ bind(DelegateBinding.new(delegate))
18
+ end
19
+
20
+ class BoundFunction
21
+ def initialize(parameters, block, binding)
22
+ @parameters = parameters
23
+ @block = block
24
+ @binding = binding
25
+ end
26
+
27
+ def call(*args)
28
+ @block.to_ruby(ArgumentBinding.new(@binding, @parameters, args))
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ module Doily
2
+ class Literal
3
+ def initialize(value)
4
+ @value = value
5
+ end
6
+
7
+ def to_ruby(binding)
8
+ @value
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ module Doily
2
+ class Loop
3
+ def initialize(before_all, before_each, after_each, block)
4
+ @before_all = before_all
5
+ @before_each = before_each
6
+ @after_each = after_each
7
+ @block = block
8
+ end
9
+
10
+ def to_ruby(binding)
11
+ binding = LocalBinding.new(binding)
12
+ @before_all.to_ruby(binding)
13
+ loop do
14
+ break unless @before_each.to_ruby(binding)
15
+ @block.to_ruby(binding)
16
+ @after_each.to_ruby(binding)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ module Doily
2
+ class Object
3
+ def initialize(properties)
4
+ @properties = properties
5
+ end
6
+
7
+ def to_ruby(binding)
8
+ result = {}
9
+ @properties.each do |key, value|
10
+ result[key.to_ruby(binding)] = value.to_ruby(binding)
11
+ end
12
+ result
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Doily
2
+ class Reference
3
+ def initialize(name)
4
+ @name = name
5
+ end
6
+
7
+ def assign(reference, binding)
8
+ binding.store(@name, reference.to_ruby(binding))
9
+ end
10
+
11
+ def to_ruby(binding)
12
+ binding.fetch(@name)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,140 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class DoilyTest < Test::Unit::TestCase
4
+ should 'handle nothing' do
5
+ Doily('function() {}').call.should == nil
6
+ end
7
+
8
+ context 'delegation to a ruby object' do
9
+ setup do
10
+ @delegate = Class.new do
11
+ def echo(*args); args; end
12
+ end.new
13
+ end
14
+
15
+ should 'handle calling a function' do
16
+ Doily('function() { echo(); }').delegate(@delegate).call.should == []
17
+ end
18
+
19
+ should 'handle calling a function with one literal argument' do
20
+ Doily('function() { echo("foo"); }').delegate(@delegate).call.should == ['foo']
21
+ end
22
+
23
+ should 'handle calling a function with many literal arguments' do
24
+ Doily('function() { echo("foo", 42); }').delegate(@delegate).call.should == ['foo', 42]
25
+ end
26
+
27
+ should 'handle calling a function with a value from the binding' do
28
+ Doily('function(document) { echo(document); }').delegate(@delegate).call('key' => 'value').should == [{'key' => 'value'}]
29
+ end
30
+
31
+ should 'handle calling a function with a property access on a variable in the binding' do
32
+ Doily('function(document) { echo(document.key); }').delegate(@delegate).call('key' => 'value').should == ['value']
33
+ end
34
+
35
+ should 'handle calling a function with an invocation on a variable in the binding' do
36
+ Doily('function(document) { echo(document.keys()); }').delegate(@delegate).call('key' => 'value').should == [['key']]
37
+ end
38
+ end
39
+
40
+ should 'handle a boolean comparison of integers' do
41
+ Doily('function() { 1 == 1; }').call.should == true
42
+ end
43
+
44
+ should 'handle a boolean comparison of strings' do
45
+ Doily('function() { "foo" == "foo"; }').call.should == true
46
+ end
47
+
48
+ should 'handle a boolean comparison with calls' do
49
+ Doily('function() { "foo".reverse() == "oof"; }').call.should == true
50
+ end
51
+
52
+ should 'handle a false boolean comparison' do
53
+ Doily('function() { "foo" == "bar"; }').call.should == false
54
+ end
55
+
56
+ should 'handle a negative boolean comparison' do
57
+ Doily('function() { 1 < 2; }').call.should == true
58
+ end
59
+
60
+ should 'handle an if statement' do
61
+ Doily('function() { if (1 == 1) { 42; } }').call.should == 42
62
+ end
63
+
64
+ should 'handle a negative if statement' do
65
+ Doily('function() { if (1 == 2) { 42; } }').call.should == nil
66
+ end
67
+
68
+ should 'handle an else statement' do
69
+ Doily('function() { if (1 == 2) { 42; } else { "foo"; } }').call.should == 'foo'
70
+ end
71
+
72
+ should 'handle variable declaration' do
73
+ Doily('function() { var foo = "42"; }').call.should == '42'
74
+ end
75
+
76
+ should 'handle variable declaration with continuing statements' do
77
+ Doily('function() { var foo = "42"; foo; }').call.should == '42'
78
+ end
79
+
80
+ should 'handle hash literals' do
81
+ Doily('function() { {"name":"Bob", "age":42 }; }').call.should == { 'name' => 'Bob', 'age' => 42 }
82
+ end
83
+
84
+ should 'handle square-bracket hash access with literal' do
85
+ Doily('function(document) { document["key"]; }').call('key' => 'value').should == 'value'
86
+ end
87
+
88
+ should 'handle square-bracket hash access with expression' do
89
+ Doily('function(document) { var key = "key"; document[key]; }').call('key' => 'value').should == 'value'
90
+ end
91
+
92
+ should 'handle hash assignment' do
93
+ Doily('function() { var doc = {}; doc["key"] = "value"; doc; }').call.should == { 'key' => 'value' }
94
+ end
95
+
96
+ should 'handle literal true' do
97
+ Doily('function() { true; }').call.should == true
98
+ end
99
+
100
+ should 'handle literal false' do
101
+ Doily('function() { false; }').call.should == false
102
+ end
103
+
104
+ should 'handle single-quoted strings' do
105
+ Doily("function() { 'foo'; }").call.should == 'foo'
106
+ end
107
+
108
+ should 'handle increment operator' do
109
+ Doily('function() { var i = 0; i++; i; }').call.should == 1
110
+ end
111
+
112
+ context 'for loop' do
113
+ setup do
114
+ @counter = Class.new do
115
+ attr_reader :count
116
+
117
+ def initialize
118
+ @count = 0
119
+ end
120
+
121
+ def tick
122
+ @count = @count + 1
123
+ end
124
+ end.new
125
+ end
126
+
127
+ should 'work' do
128
+ Doily('function() { for (var i=0; i < 3; i++) { tick(); } }').delegate(@counter).call
129
+ @counter.count.should == 3
130
+ end
131
+ end
132
+
133
+ should 'special-case property access for length (no parens necessary to call)' do
134
+ Doily('function() { "foo".length; }').call.should == 3
135
+ end
136
+
137
+ should 'index arrays' do
138
+ Doily('function(array) { array[0]; }').call(['a', 'b']).should == 'a'
139
+ end
140
+ end