-
-
Notifications
You must be signed in to change notification settings - Fork 329
/
parser.rb
2304 lines (1922 loc) · 62.3 KB
/
parser.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
require 'opal/lexer'
require 'opal/grammar'
require 'opal/target_scope'
require 'opal/version'
module Opal
class Parser
# A fragment holds a string of generated javascript that will be written
# to the destination. It also keeps hold of the original sexp from which
# it was generated. Using this sexp, when writing fragments in order, a
# mapping can be created of the original location => target location,
# aka, source-maps!
class Fragment
# String of javascript this fragment holds
attr_reader :code
def initialize(code, sexp = nil)
@code = code.to_s
@sexp = sexp
end
# In debug mode we may wish to include the original line as a comment
def to_code
if @sexp
"/*:#{@sexp.line}*/#{@code}"
else
@code
end
end
# inspect the contents of this fragment, f("fooo")
def inspect
"f(#{@code.inspect})"
end
def line
@sexp.line if @sexp
end
end
# Generated code gets indented with two spaces on each scope
INDENT = ' '
# Expressions are handled at diffferent levels. Some sexps
# need to know the js expression they are generating into.
LEVEL = [:stmt, :stmt_closure, :list, :expr, :recv]
# All compare method nodes - used to optimize performance of
# math comparisons
COMPARE = %w[< > <= >=]
# Reserved javascript keywords - we cannot create variables with the
# same name
RESERVED = %w(
break case catch continue debugger default delete do else finally for
function if in instanceof new return switch this throw try typeof var let
void while with class enum export extends import super true false native
const static
)
# Statements which should not have ';' added to them.
STATEMENTS = [:xstr, :dxstr]
# Final generated javascript for this parser
attr_reader :result
# generated fragments as an array
attr_reader :fragments
# Parse some ruby code to a string.
#
# Opal::Parser.new.parse("1 + 2")
# # => "(function() {....})()"
def parse(source, options = {})
@sexp = Grammar.new.parse(source, options[:file])
@line = 1
@indent = ''
@unique = 0
@helpers = {
:breaker => true,
:slice => true
}
# options
@file = options[:file] || '(file)'
@source_file = options[:source_file] || @file
@method_missing = (options[:method_missing] != false)
@arity_check = options[:arity_check]
@const_missing = (options[:const_missing] == true)
@irb_vars = (options[:irb] == true)
@method_calls = {}
@fragments = self.top(@sexp).flatten
@fragments.unshift f(version_comment)
@result = @fragments.map(&:code).join('')
end
# Always at top of generated file to show current opal version
def version_comment
"/* Generated by Opal #{Opal::VERSION} */\n"
end
def source_map
Opal::SourceMap.new(@fragments, '(file)')
end
def extract_parser_options(content)
result = {}
if /^#\ opal\:(.*)/ =~ content
$~[1].split(',').map(&:strip).each do |opt|
next if opt == ""
opt = opt.gsub('-', '_')
if opt =~ /no_/
result[opt.sub(/no_/, '').to_sym] = false
else
result[opt.to_sym] = true
end
end
end
result
end
# This is called when a parsing/processing error occurs. This
# method simply appends the filename and curent line number onto
# the message and raises it.
#
# parser.error "bad variable name"
# # => raise "bad variable name :foo.rb:26"
#
# @param [String] msg error message to raise
def error(msg)
raise SyntaxError, "#{msg} :#{@file}:#{@line}"
end
# This is called when a parsing/processing warning occurs. This
# method simply appends the filename and curent line number onto
# the message and issues a warning.
#
# @param [String] msg warning message to raise
def warning(msg)
warn "#{msg} :#{@file}:#{@line}"
end
# Instances of `Scope` can use this to determine the current
# scope indent. The indent is used to keep generated code easily
# readable.
#
# @return [String]
def parser_indent
@indent
end
# Create a new sexp using the given parts. Even though this just
# returns an array, it must be used incase the internal structure
# of sexps does change.
#
# s(:str, "hello there")
# # => [:str, "hello there"]
#
# @result [Array]
def s(*parts)
sexp = Array.new(parts)
sexp.line = @line
sexp
end
# @param [String] code the string of code
# @return [Fragment]
def f(code, sexp = nil)
Fragment.new(code, sexp)
end
alias_method :fragment, :f
# Converts a ruby method name into its javascript equivalent for
# a method/function call. All ruby method names get prefixed with
# a '$', and if the name is a valid javascript identifier, it will
# have a '.' prefix (for dot-calling), otherwise it will be
# wrapped in brackets to use reference notation calling.
#
# mid_to_jsid('foo') # => ".$foo"
# mid_to_jsid('class') # => ".$class"
# mid_to_jsid('==') # => "['$==']"
# mid_to_jsid('name=') # => "['$name=']"
#
# @param [String] mid ruby method id
# @return [String]
def mid_to_jsid(mid)
if /\=|\+|\-|\*|\/|\!|\?|\<|\>|\&|\||\^|\%|\~|\[/ =~ mid.to_s
"['$#{mid}']"
else
'.$' + mid
end
end
# Converts a ruby lvar/arg name to a js identifier. Not all ruby names
# are valid in javascript. A $ suffix is added to non-valid names.
def lvar_to_js(var)
var = "#{var}$" if RESERVED.include? var.to_s
var.to_sym
end
# Used to generate a unique id name per file. These are used
# mainly to name method bodies for methods that use blocks.
#
# @return [String]
def unique_temp
"TMP_#{@unique += 1}"
end
# Generate the code for the top level sexp, i.e. the root sexp
# for a file. This is used directly by `#parse`. It pushes a
# ":top" scope onto the stack and handles the passed in sexp.
# The result is a string of javascript representing the sexp.
#
# @param [Array] sexp the sexp to process
# @return [String]
def top(sexp, options = {})
code, vars = nil, nil
# empty file = nil as our top sexp
sexp = s(:nil) unless sexp
in_scope(:top) do
indent {
scope = s(:scope, sexp)
scope.line = sexp.line
code = process(scope, :stmt)
code = [code] unless code.is_a? Array
code.unshift f(@indent, sexp)
}
@scope.add_temp "self = $opal.top"
@scope.add_temp "$scope = $opal"
@scope.add_temp "nil = $opal.nil"
@scope.add_temp "def = $opal.Object._proto" if @scope.defines_defn
@helpers.keys.each { |h| @scope.add_temp "$#{h} = $opal.#{h}" }
vars = [f(INDENT, sexp), @scope.to_vars, f("\n", sexp)]
if @irb_vars
code.unshift f("if (!$opal.irb_vars) { $opal.irb_vars = {}; }\n", sexp)
end
end
if @method_missing
stubs = f("\n#{INDENT}$opal.add_stubs([" + @method_calls.keys.map { |k| "'$#{k}'" }.join(", ") + "]);\n", sexp)
else
stubs = []
end
[f("(function($opal) {\n", sexp), vars, stubs, code, f("\n})(Opal);\n", sexp)]
end
# Every time the parser enters a new scope, this is called with
# the scope type as an argument. Valid types are `:top` for the
# top level/file scope; `:class`, `:module` and `:sclass` for the
# obvious ruby classes/modules; `:def` and `:iter` for methods
# and blocks respectively.
#
# This method just pushes a new instance of `Opal::Scope` onto the
# stack, sets the new scope as the `@scope` variable, and yields
# the given block. Once the block returns, the old scope is put
# back on top of the stack.
#
# in_scope(:class) do
# # generate class body in here
# body = "..."
# end
#
# # use body result..
#
# @param [Symbol] type the type of scope
# @return [nil]
def in_scope(type)
return unless block_given?
parent = @scope
@scope = TargetScope.new(type, self).tap { |s| s.parent = parent }
yield @scope
@scope = parent
end
# To keep code blocks nicely indented, this will yield a block after
# adding an extra layer of indent, and then returning the resulting
# code after reverting the indent.
#
# indented_code = indent do
# "foo"
# end
#
# @result [String]
def indent(&block)
indent = @indent
@indent += INDENT
@space = "\n#@indent"
res = yield
@indent = indent
@space = "\n#@indent"
res
end
# Temporary varibales will be needed from time to time in the
# generated code, and this method will assign (or reuse) on
# while the block is yielding, and queue it back up once it is
# finished. Variables are queued once finished with to save the
# numbers of variables needed at runtime.
#
# with_temp do |tmp|
# "tmp = 'value';"
# end
#
# @return [String] generated code withing block
def with_temp(&block)
tmp = @scope.new_temp
res = yield tmp
@scope.queue_temp tmp
res
end
# Used when we enter a while statement. This pushes onto the current
# scope's while stack so we know how to handle break, next etc.
#
# Usage:
#
# in_while do
# # generate while body here.
# end
def in_while
return unless block_given?
@while_loop = @scope.push_while
result = yield
@scope.pop_while
result
end
def in_case
return unless block_given?
old = @case_stmt
@case_stmt = {}
yield
@case_stmt = old
end
# Returns true if the parser is curently handling a while sexp,
# false otherwise.
#
# @return [Boolean]
def in_while?
@scope.in_while?
end
# Processes a given sexp. This will send a method to the receiver
# of the format "process_<sexp_name>". Any sexp handler should
# return a string of content.
#
# For example, calling `process` with `s(:sym, 42)` will call the
# method `#process_lit`. If a method with that name cannot be
# found, then an error is raised.
#
# process(s(:int, 42), :stmt)
# # => "42"
#
# @param [Array] sexp the sexp to process
# @param [Symbol] level the level to process (see `LEVEL`)
# @return [String]
def process(sexp, level = :expr)
type = sexp.shift
meth = "process_#{type}"
raise "Unsupported sexp: #{type}" unless respond_to? meth
@line = sexp.line
__send__(meth, sexp, level)
end
# The last sexps in method bodies, for example, need to be returned
# in the compiled javascript. Due to syntax differences between
# javascript any ruby, some sexps need to be handled specially. For
# example, `if` statemented cannot be returned in javascript, so
# instead the "truthy" and "falsy" parts of the if statement both
# need to be returned instead.
#
# Sexps that need to be returned are passed to this method, and the
# alterned/new sexps are returned and should be used instead. Most
# sexps can just be added into a s(:return) sexp, so that is the
# default action if no special case is required.
#
# sexp = s(:str, "hey")
# parser.returns(sexp)
# # => s(:js_return, s(:str, "hey"))
#
# `s(:js_return)` is just a special sexp used to return the result
# of processing its arguments.
#
# @param [Array] sexp the sexp to alter
# @return [Array] altered sexp
def returns(sexp)
return returns s(:nil) unless sexp
case sexp.first
when :break, :next, :redo
sexp
when :yield
sexp[0] = :returnable_yield
sexp
when :scope
sexp[1] = returns sexp[1]
sexp
when :block
if sexp.length > 1
sexp[-1] = returns sexp[-1]
else
sexp << returns(s(:nil))
end
sexp
when :when
sexp[2] = returns(sexp[2])
sexp
when :rescue
sexp[1] = returns sexp[1]
if sexp[2] and sexp[2][0] == :resbody
if sexp[2][2]
sexp[2][2] = returns sexp[2][2]
else
sexp[2][2] = returns s(:nil)
end
end
sexp
when :ensure
sexp[1] = returns sexp[1]
sexp
when :begin
sexp[1] = returns sexp[1]
sexp
when :while
# sexp[2] = returns(sexp[2])
sexp
when :return
sexp
when :xstr
sexp[1] = "return #{sexp[1]};" unless /return|;/ =~ sexp[1]
sexp
when :dxstr
sexp[1] = "return #{sexp[1]}" unless /return|;|\n/ =~ sexp[1]
sexp
when :if
sexp[2] = returns(sexp[2] || s(:nil))
sexp[3] = returns(sexp[3] || s(:nil))
sexp
else
s(:js_return, sexp).tap { |s|
s.line = sexp.line
}
end
end
# Returns true if the given sexp is an expression. All expressions
# will get ';' appended to their result, except for the statement
# sexps. See `STATEMENTS` for a list of sexp names that are
# statements.
#
# @param [Array] sexp the sexp to check
# @return [Boolean]
def expression?(sexp)
!STATEMENTS.include?(sexp.first)
end
# More than one expression in a row will be grouped by the grammar
# into a block sexp. A block sexp just holds any number of other
# sexps.
#
# s(:block, s(:str, "hey"), s(:int, 42))
#
# A block can actually be empty. As opal requires real values to
# be returned (to appease javascript values), a nil sexp
# s(:nil) will be generated if the block is empty.
#
# @return [String]
def process_block(sexp, level)
return process s(:nil) if sexp.empty?
result = []
join = (@scope.class_scope? ? "\n\n#@indent" : "\n#@indent")
sexp.each do |stmt|
result << f(join, sexp) unless result.empty?
# find any inline yield statements
if yasgn = find_inline_yield(stmt)
result << process(yasgn, level) << f(";", yasgn)
end
expr = expression?(stmt) and LEVEL.index(level) < LEVEL.index(:list)
result << process(stmt, level)
result << f(";", stmt) if expr
end
result
end
# When a block sexp gets generated, any inline yields (i.e. yield
# statements that are not direct members of the block) need to be
# generated as a top level member. This is because if a yield
# is returned by a break statement, then the method must return.
#
# As inline expressions in javascript cannot return, the block
# must be rewritten.
#
# For example, a yield inside an array:
#
# [1, 2, 3, yield(4)]
#
# Must be rewitten into:
#
# tmp = yield 4
# [1, 2, 3, tmp]
#
# This rewriting happens on sexps directly.
#
# @param [Sexp] stmt sexps to (maybe) rewrite
# @return [Sexp]
def find_inline_yield(stmt)
found = nil
case stmt.first
when :js_return
if found = find_inline_yield(stmt[1])
found = found[2]
end
when :array
stmt[1..-1].each_with_index do |el, idx|
if el.first == :yield
found = el
stmt[idx+1] = s(:js_tmp, '$yielded')
end
end
when :call
arglist = stmt[3]
arglist[1..-1].each_with_index do |el, idx|
if el.first == :yield
found = el
arglist[idx+1] = s(:js_tmp, '$yielded')
end
end
end
if found
@scope.add_temp '$yielded' unless @scope.has_temp? '$yielded'
s(:yasgn, '$yielded', found)
end
end
def process_scope(sexp, level)
stmt = sexp[0] || s(:nil)
stmt = returns stmt unless @scope.class_scope?
process stmt, :stmt
end
# s(:js_return, sexp)
def process_js_return(sexp, level)
[f("return ", sexp), process(sexp[0])]
end
# s(:js_tmp, str)
def process_js_tmp(sexp, level)
f(sexp[0].to_s, sexp)
end
def js_block_given(sexp, level)
@scope.uses_block!
if @scope.block_name
f("(#{@scope.block_name} !== nil)", sexp)
elsif scope = @scope.find_parent_def and scope.block_name
f("(#{scope.block_name} !== nil)", sexp)
else
f("false", sexp)
end
end
def handle_block_given(sexp, reverse = false)
@scope.uses_block!
name = @scope.block_name
f((reverse ? "#{ name } === nil" : "#{ name } !== nil"), sexp)
end
def process_sym(sexp, level)
f(sexp[0].to_s.inspect, sexp)
end
# Process integers. Wrap in parens if a receiver of method call
def process_int(sexp, level)
f((level == :recv ? "(#{sexp[0]})" : sexp[0].to_s), sexp)
end
# Floats generated just like integers
alias_method :process_float, :process_int
# Regexp literals. Convert to empty js regexp if empty (not compatible)
def process_regexp(sexp, level)
val = sexp[0]
f((val == // ? /^/.inspect : val.inspect), sexp)
end
# Dynamic regexps with interpolation
# s(:dregx, parts...) => new Regexp("...")
def process_dregx(sexp, level)
result = []
sexp.each do |part|
result << f(" + ", sexp) unless result.empty?
if String === part
result << f(part.inspect, sexp)
elsif part[0] == :str
result << process(part)
else
result << process(part[1])
end
end
[f("(new RegExp(", sexp), result, f("))", sexp)]
end
# Exclusive range, uses opal __range helper.
# s(:dot3, start, end) => __range(start, end, false)
def process_dot2(sexp, level)
@helpers[:range] = true
[f("$range(", sexp), process(sexp[0]), f(", ", sexp), process(sexp[1]), f(", false)", sexp)]
end
# Inclusive range, uses __range helper
# s(:dot3, start, end) => __range(start, end, true)
def process_dot3(sexp, level)
@helpers[:range] = true
[f("$range(", sexp), process(sexp[0]), f(", ", sexp), process(sexp[1]), f(", true)", sexp)]
end
# Simple strings, no interpolation.
# s(:str, "string") => "string"
def process_str(sexp, level)
f sexp[0].inspect, sexp
end
# defined?(x) => various
def process_defined(sexp, level)
part = sexp[0]
case part[0]
when :self
f("'self'", sexp)
when :nil
f("'nil'", sexp)
when :true
f("'true'", sexp)
when :false
f("'false'", sexp)
when :call
mid = mid_to_jsid part[2].to_s
recv = part[1] ? process(part[1]) : f(current_self, sexp)
[f("(", sexp), recv, f("#{mid} ? 'method' : nil)", sexp)]
when :xstr, :dxstr
[f("(typeof(", sexp), process(part), f(") !== 'undefined')", sexp)]
when :const
f("($scope.#{part[1].to_s} != null)", sexp)
when :cvar
f("($opal.cvars[#{part[1].to_s.inspect}] != null ? 'class-variable' : nil)", sexp)
when :colon2
f("false", sexp)
when :colon3
f("($opal.Object._scope.#{sexp[0][1]} == null ? nil : 'constant')", sexp)
when :ivar
ivar_name = part[1].to_s[1..-1]
with_temp do |t|
f("((#{t} = #{current_self}[#{ivar_name.inspect}], #{t} != null && #{t} !== nil) ? 'instance-variable' : nil)", sexp)
end
when :lvar
f("local-variable", sexp)
else
raise "bad defined? part: #{part[0]}"
end
end
# not keyword or '!' operand
# s(:not, value) => (tmp = value, (tmp === nil || tmp === false))
def process_not(sexp, level)
with_temp do |tmp|
expr = sexp[0]
[f("(#{tmp} = ", sexp), process(expr), f(", (#{tmp} === nil || #{tmp} === false))", sexp)]
end
end
# A block pass '&foo' syntax
# s(:block_pass, value) => value.$to_proc()
def process_block_pass(exp, level)
process s(:call, exp[0], :to_proc, s(:arglist))
end
# A block/iter with embeded call. Compiles into function
# s(:iter, block_args [, body) => (function() { ... })
def process_iter(sexp, level)
args, body = sexp
body ||= s(:nil)
body = returns body
code = []
params = nil
scope_name = nil
identity = nil
to_vars = nil
args = nil if Fixnum === args # argh
args ||= s(:masgn, s(:array))
args = args.first == :lasgn ? s(:array, args) : args[1]
# opt args are last, if present, and are a [:block]
if args.last.is_a?(Array) and args.last[0] == :block
opt_args = args.pop
opt_args.shift
end
if args.last.is_a?(Array) and args.last[0] == :block_pass
block_arg = args.pop
block_arg = block_arg[1][1].to_sym
end
if args.last.is_a?(Array) and args.last[0] == :splat
splat = args.last[1][1]
args.pop
len = args.length
end
indent do
in_scope(:iter) do
identity = @scope.identify!
@scope.add_temp "#{current_self} = #{identity}._s || this"
params = js_block_args(args[1..-1])
args[1..-1].each_with_index do |arg, idx|
if arg[0] == :lasgn
arg = arg[1]
arg = "#{arg}$" if RESERVED.include? arg.to_s
if opt_args and current_opt = opt_args.find { |s| s[1] == arg.to_sym }
code << [f("if (#{arg} == null) #{arg} = ", sexp), process(current_opt[2]), f(";\n#{@indent}", sexp)]
else
code << f("if (#{arg} == null) #{arg} = nil;\n#{@indent}", sexp)
end
elsif arg[0] == :array
arg[1..-1].each_with_index do |arg, midx|
arg = arg[1]
arg = "#{arg}$" if RESERVED.include? arg.to_s
code << f("#{arg} = #{params[idx]}[#{midx}];\n#{@indent}")
end
else
raise "Bad block_arg type: #{arg[0]}"
end
end
if splat
@scope.add_arg splat
params << splat
code << f("#{splat} = $slice.call(arguments, #{len - 1});", sexp)
end
if block_arg
@scope.block_name = block_arg
@scope.add_temp block_arg
scope_name = @scope.identify!
blk = []
blk << f("\n#@indent#{block_arg} = #{scope_name}._p || nil, #{scope_name}._p = null;\n#@indent", sexp)
code.unshift blk
end
code << f("\n#@indent", sexp)
code << process(body, :stmt)
if @scope.defines_defn
@scope.add_temp "def = ((#{current_self}._isClass) ? #{current_self}._proto : #{current_self})"
end
to_vars = [f("\n#@indent", sexp), @scope.to_vars, f("\n#@indent", sexp)]
end
end
itercode = [f("function(#{params.join ', '}) {\n", sexp), to_vars, code, f("\n#@indent}", sexp)]
itercode.unshift f("(#{identity} = ", sexp)
itercode << f(", #{identity}._s = #{current_self}, #{identity})", sexp)
itercode
end
# Maps block args into array of jsid. Adds $ suffix to invalid js
# identifiers.
#
# s(:args, parts...) => ["a", "b", "break$"]
def js_block_args(sexp)
result = []
sexp.each do |arg|
if arg[0] == :lasgn
ref = lvar_to_js(arg[1])
@scope.add_arg ref
result << ref
elsif arg[0] == :array
result << @scope.next_temp
else
raise "Bad js_block_arg: #{arg[0]}"
end
end
result
end
##
# recv.mid = rhs
#
# s(recv, :mid=, s(:arglist, rhs))
def process_attrasgn(exp, level)
recv, mid, arglist = exp
process s(:call, recv, mid, arglist), level
end
# s(:call, recv, :mid, s(:arglist))
# s(:call, nil, :mid, s(:arglist))
def process_call(sexp, level)
recv, meth, arglist, iter = sexp
mid = mid_to_jsid meth.to_s
@method_calls[meth.to_sym] = true
# we are trying to access a lvar in irb mode
if @irb_vars and @scope.top? and arglist == s(:arglist) and recv == nil and iter == nil
return with_temp { |t|
lvar = meth.intern
lvar = "#{lvar}$" if RESERVED.include? lvar
call = s(:call, s(:self), meth.intern, s(:arglist))
[f("((#{t} = $opal.irb_vars.#{lvar}) == null ? ", sexp), process(call), f(" : #{t})", sexp)]
}
end
case meth
when :block_given?
return js_block_given(sexp, level)
end
splat = arglist[1..-1].any? { |a| a.first == :splat }
if Array === arglist.last and arglist.last.first == :block_pass
block = process(arglist.pop)
elsif iter
block = process(iter)
end
recv ||= s(:self)
if block
tmpfunc = @scope.new_temp
end
tmprecv = @scope.new_temp if splat || tmpfunc
args = ""
recv_code = process recv, :recv
call_recv = s(:js_tmp, tmprecv || recv_code)
arglist.insert 1, call_recv if tmpfunc and !splat
args = process arglist
dispatch = if tmprecv
[f("(#{tmprecv} = "), recv_code, f(")#{mid}")]
else
[recv_code, f(mid)]
end
if tmpfunc
dispatch.unshift f("(#{tmpfunc} = ")
dispatch << f(", #{tmpfunc}._p = ")
dispatch << block
dispatch << f(", #{tmpfunc})")
end
result = if splat
[dispatch, f(".apply("), (tmprecv ? f(tmprecv) : recv_code),
f(", "), args, f(")")]
elsif tmpfunc
[dispatch, f(".call("), args, f(")")]
else
[dispatch, f("("), args, f(")")]
end
@scope.queue_temp tmpfunc if tmpfunc
result
end
# s(:arglist, [arg [, arg ..]])
def process_arglist(sexp, level)
code, work = [], []
sexp.each do |current|
splat = current.first == :splat
arg = process current
if splat
if work.empty?
if code.empty?
code << f("[].concat(", sexp)
code << arg
code << f(")")
else
code += ".concat(#{arg})"
end
else
if code.empty?
code << [f("["), work, f("]")]
else
code << [f(".concat(["), work, f("])")]
end
code << [f(".concat("), arg, f(")")]
end
work = []
else
work << f(", ") unless work.empty?
work << arg
end
end
unless work.empty?
join = work
if code.empty?
code = join
else
code << f(".concat(") << join << f(")")
end
end
code
end
# s(:splat, sexp)
def process_splat(sexp, level)
if sexp.first == [:nil]
[f("[]")]
elsif sexp.first.first == :sym
[f("["), process(sexp[0]), f("]")]
else
process sexp.first, :recv
end
end
# s(:class, cid, super, body)
def process_class(sexp, level)
cid, sup, body = sexp
body[1] = s(:nil) unless body[1]
code = []
@helpers[:klass] = true
if Symbol === cid or String === cid
base = process s(:self)
name = cid.to_s
elsif cid[0] == :colon2
base = process(cid[1])
name = cid[2].to_s
elsif cid[0] == :colon3
base = process(s(:js_tmp, '$opal.Object'))
name = cid[1].to_s
else
raise "Bad receiver in class"
end
sup = sup ? process(sup) : process(s(:js_tmp, 'null'))
indent do
in_scope(:class) do
@scope.name = name
@scope.add_temp "#{ @scope.proto } = #{name}._proto", "$scope = #{name}._scope"
body = process(returns(body), :stmt)