opal 0.3.26 → 0.3.27

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.
Files changed (74) hide show
  1. data/.gitignore +1 -6
  2. data/.travis.yml +1 -4
  3. data/Gemfile +1 -1
  4. data/README.md +3 -8
  5. data/Rakefile +26 -3
  6. data/core/array.rb +33 -91
  7. data/core/boolean.rb +1 -4
  8. data/core/class.rb +25 -71
  9. data/core/error.rb +5 -3
  10. data/core/hash.rb +3 -2
  11. data/core/kernel.rb +40 -1
  12. data/core/load_order +1 -1
  13. data/core/nil_class.rb +1 -3
  14. data/core/runtime.js +4 -44
  15. data/core/template.rb +20 -0
  16. data/core/{opal-spec → test_runner}/runner.js +0 -6
  17. data/lib/opal.rb +1 -12
  18. data/lib/opal/builder.rb +3 -10
  19. data/lib/opal/lexer.rb +1095 -1110
  20. data/lib/opal/parser.rb +229 -26
  21. data/lib/opal/rake_task.rb +3 -3
  22. data/lib/opal/version.rb +1 -1
  23. data/opal.gemspec +2 -2
  24. data/spec/core/array/assoc_spec.rb +2 -3
  25. data/spec/core/array/comparison_spec.rb +16 -0
  26. data/spec/core/array/constructor_spec.rb +0 -9
  27. data/spec/core/array/drop_spec.rb +21 -0
  28. data/spec/core/array/dup_spec.rb +15 -0
  29. data/spec/core/array/{eql_spec.rb → equal_value_spec.rb} +0 -0
  30. data/spec/core/array/index_spec.rb +26 -0
  31. data/spec/core/array/inspect_spec.rb +13 -0
  32. data/spec/core/array/intersection_spec.rb +22 -0
  33. data/spec/core/array/join_spec.rb +9 -0
  34. data/spec/core/array/keep_if_spec.rb +7 -0
  35. data/spec/core/array/minus_spec.rb +19 -9
  36. data/spec/core/array/multiply_spec.rb +13 -0
  37. data/spec/core/array/new_spec.rb +40 -0
  38. data/spec/core/array/rindex_spec.rb +21 -0
  39. data/spec/core/array/select_spec.rb +13 -0
  40. data/spec/core/array/shift_spec.rb +51 -0
  41. data/spec/core/array/slice_spec.rb +37 -0
  42. data/spec/core/array/take_spec.rb +21 -0
  43. data/spec/core/array/take_while_spec.rb +13 -0
  44. data/spec/core/array/to_a_spec.rb +7 -0
  45. data/spec/core/array/unshift_spec.rb +29 -0
  46. data/spec/core/hash/constructor_spec.rb +13 -0
  47. data/spec/core/hash/default_proc_spec.rb +20 -0
  48. data/spec/core/hash/default_spec.rb +8 -0
  49. data/spec/core/hash/delete_spec.rb +11 -0
  50. data/spec/core/hash/dup_spec.rb +10 -0
  51. data/spec/core/hash/reject_spec.rb +18 -0
  52. data/spec/core/hash/to_a_spec.rb +13 -0
  53. data/spec/core/kernel/Array_spec.rb +10 -0
  54. data/spec/core/kernel/class_spec.rb +6 -0
  55. data/spec/core/kernel/equal_spec.rb +12 -0
  56. data/spec/core/kernel/extend_spec.rb +21 -0
  57. data/spec/core/kernel/instance_eval_spec.rb +28 -0
  58. data/spec/core/kernel/instance_variable_get_spec.rb +14 -0
  59. data/spec/core/kernel/instance_variable_set_spec.rb +10 -0
  60. data/spec/core/kernel/match_spec.rb +5 -0
  61. data/spec/core/module/alias_method_spec.rb +10 -0
  62. data/spec/core/module/ancestors_spec.rb +11 -0
  63. data/spec/core/module/append_features_spec.rb +14 -0
  64. data/spec/core/proc/call_spec.rb +21 -0
  65. data/spec/core/proc/proc_tricks_spec.rb +1 -1
  66. data/spec/language/alias_spec.rb +1 -1
  67. data/spec/opal/class/instance_methods_spec.rb +13 -0
  68. data/spec/opal/kernel/attribute_spec.rb +57 -0
  69. data/spec/spec_helper.rb +1 -1
  70. data/spec/test_case.html +13 -0
  71. metadata +88 -12
  72. data/core/erb.rb +0 -32
  73. data/lib/opal/erb_parser.rb +0 -20
  74. data/spec/opal/erb/erb_spec.rb +0 -23
data/lib/opal/parser.rb CHANGED
@@ -3,12 +3,52 @@ require 'opal/grammar'
3
3
  require 'opal/scope'
4
4
 
5
5
  module Opal
6
-
6
+ # This class is used to generate the javascript code from the given
7
+ # ruby code. First, this class will use an instance of `Opal::Grammar`
8
+ # to lex and then build up a tree of sexp statements. Once done, the
9
+ # top level sexp is passed into `#top` which recursively generates
10
+ # javascript for each sexp inside, and the result is returned as a
11
+ # string.
12
+ #
13
+ # p = Opal::Parser.new
14
+ # p.parse "puts 42"
15
+ # # => "(function() { ... })()"
16
+ #
17
+ # ## Sexps
18
+ #
19
+ # A sexp, in opal, is an array where the first value is a symbol
20
+ # identifying the type of sexp. A simple ruby string "hello" would
21
+ # be represented by the sexp:
22
+ #
23
+ # s(:str, "hello")
24
+ #
25
+ # Once that sexp is encounterd by the parser, it is handled by
26
+ # `#process` which removes the sexp type from the array, and checks
27
+ # for a method "process_str", which is used to handle specific sexps.
28
+ # Once found, that method is called with the sexp and level as
29
+ # arguments, and the returned string is the javascript to be used in
30
+ # the resulting file.
31
+ #
32
+ # ## Levels
33
+ #
34
+ # A level inside the parser is just a symbol representing what type
35
+ # of destination the code to be generated is for. For example, the
36
+ # main two levels are `:stmt` and `:expr`. Most sexps generate the
37
+ # same code for every level, but an `if` statement for example
38
+ # will change when written as an expression. Javascript cannot have
39
+ # if statements as expressions, so that sexp would wrap its result
40
+ # inside an anonymous function so the if statement can compile as
41
+ # expected.
7
42
  class Parser
43
+ # Generated code gets indented with two spaces on each scope
8
44
  INDENT = ' '
9
45
 
46
+ # Expressions are handled at diffferent levels. Some sexps
47
+ # need to know the js expression they are generating into.
10
48
  LEVEL = [:stmt, :stmt_closure, :list, :expr, :recv]
11
49
 
50
+ # All compare method nodes - used to optimize performance of
51
+ # math comparisons
12
52
  COMPARE = %w[< > <= >=]
13
53
 
14
54
  # Reserved javascript keywords - we cannot create variables with the
@@ -20,16 +60,32 @@ module Opal
20
60
  const
21
61
  )
22
62
 
63
+ # Statements which should not have ';' added to them.
23
64
  STATEMENTS = [:xstr, :dxstr]
24
65
 
66
+ # The grammar (tree of sexps) representing this compiled code
67
+ # @return [Opal::Grammar]
25
68
  attr_reader :grammar
26
69
 
70
+ # Holds an array of paths which this file "requires".
71
+ # @return [Array<String>]
27
72
  attr_reader :requires
28
73
 
74
+ # This does the actual parsing. The ruby code given is first
75
+ # run through a `Grammar` instance which returns a sexp to
76
+ # process. This is then handled recursively, resulting in a
77
+ # string of javascript being returned.
78
+ #
79
+ # p = Opal::Parser.new
80
+ # p.parse "puts 'hey'"
81
+ # # => "(function() { .... })()"
82
+ #
83
+ # @param [String] source the ruby code to parse
84
+ # @param [String] file the filename representing this code
85
+ # @return [String] string of javascript code
29
86
  def parse(source, file = '(file)')
30
87
  @grammar = Grammar.new
31
88
  @requires = []
32
- @comments = false
33
89
  @file = file
34
90
  @line = 1
35
91
  @indent = ''
@@ -43,24 +99,54 @@ module Opal
43
99
  top @grammar.parse(source, file)
44
100
  end
45
101
 
46
- def warn(msg)
47
- puts "#{msg} :#{@file}:#{@line}"
48
- end
49
-
102
+ # This is called when a parsing/processing error occurs. This
103
+ # method simply appends the filename and curent line number onto
104
+ # the message and raises it.
105
+ #
106
+ # parser.error "bad variable name"
107
+ # # => raise "bad variable name :foo.rb:26"
108
+ #
109
+ # @param [String] msg error message to raise
50
110
  def error(msg)
51
111
  raise "#{msg} :#{@file}:#{@line}"
52
112
  end
53
113
 
114
+ # Instances of `Scope` can use this to determine the current
115
+ # scope indent. The indent is used to keep generated code easily
116
+ # readable.
117
+ #
118
+ # @return [String]
54
119
  def parser_indent
55
120
  @indent
56
121
  end
57
122
 
123
+ # Create a new sexp using the given parts. Even though this just
124
+ # returns an array, it must be used incase the internal structure
125
+ # of sexps does change.
126
+ #
127
+ # s(:str, "hello there")
128
+ # # => [:str, "hello there"]
129
+ #
130
+ # @result [Array]
58
131
  def s(*parts)
59
132
  sexp = Sexp.new(parts)
60
133
  sexp.line = @line
61
134
  sexp
62
135
  end
63
136
 
137
+ # Converts a ruby method name into its javascript equivalent for
138
+ # a method/function call. All ruby method names get prefixed with
139
+ # a '$', and if the name is a valid javascript identifier, it will
140
+ # have a '.' prefix (for dot-calling), otherwise it will be
141
+ # wrapped in brackets to use reference notation calling.
142
+ #
143
+ # mid_to_jsid('foo') # => ".$foo"
144
+ # mid_to_jsid('class') # => ".$class"
145
+ # mid_to_jsid('==') # => "['$==']"
146
+ # mid_to_jsid('name=') # => "['$name=']"
147
+ #
148
+ # @param [String] mid ruby method id
149
+ # @return [String]
64
150
  def mid_to_jsid(mid)
65
151
  if /\=|\+|\-|\*|\/|\!|\?|\<|\>|\&|\||\^|\%|\~|\[/ =~ mid.to_s
66
152
  "['$#{mid}']"
@@ -69,11 +155,21 @@ module Opal
69
155
  end
70
156
  end
71
157
 
72
- # guaranteed unique id per file..
158
+ # Used to generate a unique id name per file. These are used
159
+ # mainly to name method bodies for methods that use blocks.
160
+ #
161
+ # @return [String]
73
162
  def unique_temp
74
163
  "TMP_#{@unique += 1}"
75
164
  end
76
165
 
166
+ # Generate the code for the top level sexp, i.e. the root sexp
167
+ # for a file. This is used directly by `#parse`. It pushes a
168
+ # ":top" scope onto the stack and handles the passed in sexp.
169
+ # The result is a string of javascript representing the sexp.
170
+ #
171
+ # @param [Array] sexp the sexp to process
172
+ # @return [String]
77
173
  def top(sexp, options = {})
78
174
  code = nil
79
175
  vars = []
@@ -93,9 +189,29 @@ module Opal
93
189
  code = "#{INDENT}var #{vars.join ', '};\n" + INDENT + @scope.to_vars + "\n" + code
94
190
  end
95
191
 
96
- "function() {\n#{ code }\n}"
192
+ "(function() {\n#{ code }\n})();"
97
193
  end
98
194
 
195
+ # Every time the parser enters a new scope, this is called with
196
+ # the scope type as an argument. Valid types are `:top` for the
197
+ # top level/file scope; `:class`, `:module` and `:sclass` for the
198
+ # obvious ruby classes/modules; `:def` and `:iter` for methods
199
+ # and blocks respectively.
200
+ #
201
+ # This method just pushes a new instance of `Opal::Scope` onto the
202
+ # stack, sets the new scope as the `@scope` variable, and yields
203
+ # the given block. Once the block returns, the old scope is put
204
+ # back on top of the stack.
205
+ #
206
+ # in_scope(:class) do
207
+ # # generate class body in here
208
+ # body = "..."
209
+ # end
210
+ #
211
+ # # use body result..
212
+ #
213
+ # @param [Symbol] type the type of scope
214
+ # @return [nil]
99
215
  def in_scope(type)
100
216
  return unless block_given?
101
217
 
@@ -106,6 +222,15 @@ module Opal
106
222
  @scope = parent
107
223
  end
108
224
 
225
+ # To keep code blocks nicely indented, this will yield a block after
226
+ # adding an extra layer of indent, and then returning the resulting
227
+ # code after reverting the indent.
228
+ #
229
+ # indented_code = indent do
230
+ # "foo"
231
+ # end
232
+ #
233
+ # @result [String]
109
234
  def indent(&block)
110
235
  indent = @indent
111
236
  @indent += INDENT
@@ -116,6 +241,17 @@ module Opal
116
241
  res
117
242
  end
118
243
 
244
+ # Temporary varibales will be needed from time to time in the
245
+ # generated code, and this method will assign (or reuse) on
246
+ # while the block is yielding, and queue it back up once it is
247
+ # finished. Variables are queued once finished with to save the
248
+ # numbers of variables needed at runtime.
249
+ #
250
+ # with_temp do |tmp|
251
+ # "tmp = 'value';"
252
+ # end
253
+ #
254
+ # @return [String] generated code withing block
119
255
  def with_temp(&block)
120
256
  tmp = @scope.new_temp
121
257
  res = yield tmp
@@ -123,12 +259,6 @@ module Opal
123
259
  res
124
260
  end
125
261
 
126
- # Should be overriden in custom parsers to handle a require
127
- # statement.
128
- def handle_require(arglist)
129
- "/* require statement removed */"
130
- end
131
-
132
262
  # Used when we enter a while statement. This pushes onto the current
133
263
  # scope's while stack so we know how to handle break, next etc.
134
264
  #
@@ -146,10 +276,28 @@ module Opal
146
276
  result
147
277
  end
148
278
 
279
+ # Returns true if the parser is curently handling a while sexp,
280
+ # false otherwise.
281
+ #
282
+ # @return [Boolean]
149
283
  def in_while?
150
284
  @scope.in_while?
151
285
  end
152
286
 
287
+ # Processes a given sexp. This will send a method to the receiver
288
+ # of the format "process_<sexp_name>". Any sexp handler should
289
+ # return a string of content.
290
+ #
291
+ # For example, calling `process` with `s(:lit, 42)` will call the
292
+ # method `#process_lit`. If a method with that name cannot be
293
+ # found, then an error is raised.
294
+ #
295
+ # process(s(:lit, 42), :stmt)
296
+ # # => "42"
297
+ #
298
+ # @param [Array] sexp the sexp to process
299
+ # @param [Symbol] level the level to process (see `LEVEL`)
300
+ # @return [String]
153
301
  def process(sexp, level)
154
302
  type = sexp.shift
155
303
  meth = "process_#{type}"
@@ -160,6 +308,27 @@ module Opal
160
308
  __send__ meth, sexp, level
161
309
  end
162
310
 
311
+ # The last sexps in method bodies, for example, need to be returned
312
+ # in the compiled javascript. Due to syntax differences between
313
+ # javascript any ruby, some sexps need to be handled specially. For
314
+ # example, `if` statemented cannot be returned in javascript, so
315
+ # instead the "truthy" and "falsy" parts of the if statement both
316
+ # need to be returned instead.
317
+ #
318
+ # Sexps that need to be returned are passed to this method, and the
319
+ # alterned/new sexps are returned and should be used instead. Most
320
+ # sexps can just be added into a s(:return) sexp, so that is the
321
+ # default action if no special case is required.
322
+ #
323
+ # sexp = s(:str, "hey")
324
+ # parser.returns(sexp)
325
+ # # => s(:js_return, s(:str, "hey"))
326
+ #
327
+ # `s(:js_return)` is just a special sexp used to return the result
328
+ # of processing its arguments.
329
+ #
330
+ # @param [Array] sexp the sexp to alter
331
+ # @return [Array] altered sexp
163
332
  def returns(sexp)
164
333
  return returns s(:nil) unless sexp
165
334
 
@@ -206,10 +375,28 @@ module Opal
206
375
  end
207
376
  end
208
377
 
378
+ # Returns true if the given sexp is an expression. All expressions
379
+ # will get ';' appended to their result, except for the statement
380
+ # sexps. See `STATEMENTS` for a list of sexp names that are
381
+ # statements.
382
+ #
383
+ # @param [Array] sexp the sexp to check
384
+ # @return [Boolean]
209
385
  def expression?(sexp)
210
386
  !STATEMENTS.include?(sexp.first)
211
387
  end
212
388
 
389
+ # More than one expression in a row will be grouped by the grammar
390
+ # into a block sexp. A block sexp just holds any number of other
391
+ # sexps.
392
+ #
393
+ # s(:block, s(:str, "hey"), s(:lit, 42))
394
+ #
395
+ # A block can actually be empty. As opal requires real values to
396
+ # be returned (to appease javascript values), a nil sexp
397
+ # s(:nil) will be generated if the block is empty.
398
+ #
399
+ # @return [String]
213
400
  def process_block(sexp, level)
214
401
  result = []
215
402
  sexp << s(:nil) if sexp.empty?
@@ -231,6 +418,27 @@ module Opal
231
418
  result.join(@scope.class_scope? ? "\n\n#@indent" : "\n#@indent")
232
419
  end
233
420
 
421
+ # When a block sexp gets generated, any inline yields (i.e. yield
422
+ # statements that are not direct members of the block) need to be
423
+ # generated as a top level member. This is because if a yield
424
+ # is returned by a break statement, then the method must return.
425
+ #
426
+ # As inline expressions in javascript cannot return, the block
427
+ # must be rewritten.
428
+ #
429
+ # For example, a yield inside an array:
430
+ #
431
+ # [1, 2, 3, yield(4)]
432
+ #
433
+ # Must be rewitten into:
434
+ #
435
+ # tmp = yield 4
436
+ # [1, 2, 3, tmp]
437
+ #
438
+ # This rewriting happens on sexps directly.
439
+ #
440
+ # @param [Sexp] stmt sexps to (maybe) rewrite
441
+ # @return [Sexp]
234
442
  def find_inline_yield(stmt)
235
443
  found = nil
236
444
  case stmt.first
@@ -415,12 +623,12 @@ module Opal
415
623
  args ||= s(:masgn, s(:array))
416
624
  args = args.first == :lasgn ? s(:array, args) : args[1]
417
625
 
418
- if args.last[0] == :block_pass
626
+ if args.last.is_a?(Array) and args.last[0] == :block_pass
419
627
  block_arg = args.pop
420
628
  block_arg = block_arg[1][1].to_sym
421
629
  end
422
630
 
423
- if args.last[0] == :splat
631
+ if args.last.is_a?(Array) and args.last[0] == :splat
424
632
  splat = args.last[1][1]
425
633
  args.pop
426
634
  len = args.length
@@ -509,7 +717,7 @@ module Opal
509
717
  # @param [Symbol] meth :attr_{reader,writer,accessor}
510
718
  # @param [Array<Sexp>] attrs array of s(:lit) or s(:str)
511
719
  # @return [String] precompiled attr methods
512
- def attr_optimize(meth, attrs)
720
+ def handle_attr_optimize(meth, attrs)
513
721
  out = []
514
722
 
515
723
  attrs.each do |attr|
@@ -555,16 +763,12 @@ module Opal
555
763
 
556
764
  case meth
557
765
  when :attr_reader, :attr_writer, :attr_accessor
558
- attrs = arglist[1..-1]
559
- if @scope.class_scope? && attrs.all? { |a| [:lit, :str].include? a.first }
560
- return attr_optimize meth, attrs
561
- end
766
+ return handle_attr_optimize(meth, arglist[1..-1])
562
767
  when :block_given?
563
768
  return js_block_given(sexp, level)
564
769
  when :alias_native
565
770
  return handle_alias_native(sexp) if @scope.class_scope?
566
771
  when :require
567
- # return handle_require(arglist)
568
772
  path = arglist[1]
569
773
 
570
774
  if path and path[0] == :str
@@ -699,7 +903,7 @@ module Opal
699
903
  spacer = "\n#{@indent}#{INDENT}"
700
904
  cls = "function #{name}() {};"
701
905
  boot = "#{name} = __klass(__base, __super, #{name.inspect}, #{name});"
702
- comment = "#{spacer}// line #{ sexp.line }, #{ @file }, class #{ name }" if @comment
906
+ comment = "#{spacer}// line #{ sexp.line }, #{ @file }, class #{ name }"
703
907
 
704
908
  "(function(__base, __super){#{comment}#{spacer}#{cls}#{spacer}#{boot}\n#{code}\n#{@indent}})(#{base}, #{sup})"
705
909
  end
@@ -753,7 +957,7 @@ module Opal
753
957
  spacer = "\n#{@indent}#{INDENT}"
754
958
  cls = "function #{name}() {};"
755
959
  boot = "#{name} = __module(__base, #{name.inspect}, #{name});"
756
- comment = "#{spacer}// line #{ sexp.line }, #{ @file }, module #{ name }" if @comment
960
+ comment = "#{spacer}// line #{ sexp.line }, #{ @file }, module #{ name }"
757
961
 
758
962
  "(function(__base){#{comment}#{spacer}#{cls}#{spacer}#{boot}\n#{code}\n#{@indent}})(#{base})"
759
963
  end
@@ -808,7 +1012,7 @@ module Opal
808
1012
 
809
1013
  # block name &block
810
1014
  if args.last.to_s.start_with? '&'
811
- block_name = args.pop[1..-1].to_sym
1015
+ block_name = args.pop.to_s[1..-1].to_sym
812
1016
  end
813
1017
 
814
1018
  # splat args *splat
@@ -874,7 +1078,6 @@ module Opal
874
1078
  end
875
1079
 
876
1080
  comment += "\n#{@indent}"
877
- comment = nil unless @comments
878
1081
 
879
1082
  if recvr
880
1083
  if smethod
@@ -15,7 +15,7 @@ module Opal
15
15
  @dir = @project_dir
16
16
  @build_dir = 'build'
17
17
  @specs_dir = 'spec'
18
- @files = Dir['lib/**/*.{rb,js,erb}']
18
+ @files = Dir['lib/**/*.rb']
19
19
  @dependencies = []
20
20
 
21
21
  yield self if block_given?
@@ -67,9 +67,9 @@ module Opal
67
67
  @dependencies.each { |dep| build_gem dep }
68
68
  end
69
69
 
70
- desc "Run tests through phantomjs"
70
+ desc "Run specs in spec/index.html"
71
71
  task 'opal:test' do
72
- runner = File.join(Opal.core_dir, 'opal-spec', 'runner.js')
72
+ runner = File.join Opal.core_dir, 'test_runner', 'runner.js'
73
73
  sh "phantomjs #{runner} spec/index.html"
74
74
  end
75
75
 
data/lib/opal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Opal
2
- VERSION = '0.3.26'
2
+ VERSION = '0.3.27'
3
3
  end