Class: Opal::Compiler

Inherits:
Object
  • Object
show all
Defined in:
opal/lib/opal/compiler.rb

Overview

Compiler is the main class used to compile ruby to javascript code. This class uses Parser to gather the sexp syntax tree for the ruby code, and then uses Node to step through the sexp to generate valid javascript.

Examples:

Opal::Compiler.new("ruby code").compile
# => "javascript code"

Accessing result

compiler = Opal::Compiler.new("ruby_code")
compiler.compile
compiler.result # => "javascript code"

Source Maps

compiler = Opal::Compiler.new("")
compiler.compile
compiler.source_map # => #<SourceMap:>

Constant Summary collapse

INDENT =

Generated code gets indented with two spaces on each scope

'  '
COMPARE =

All compare method nodes - used to optimize performance of math comparisons

%w[< > <= >=].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source, options = {}) ⇒ Compiler

Returns a new instance of Compiler.



160
161
162
163
164
165
166
167
# File 'opal/lib/opal/compiler.rb', line 160

def initialize(source, options = {})
  @source = source
  @indent = ''
  @unique = 0
  @options = options
  @comments = Hash.new([])
  @case_stmt = nil
end

Instance Attribute Details

#case_stmtObject (readonly)

Current case_stmt



152
153
154
# File 'opal/lib/opal/compiler.rb', line 152

def case_stmt
  @case_stmt
end

#commentsObject (readonly)

Comments from the source code



158
159
160
# File 'opal/lib/opal/compiler.rb', line 158

def comments
  @comments
end

#eof_contentObject (readonly)

Any content in END special construct



155
156
157
# File 'opal/lib/opal/compiler.rb', line 155

def eof_content
  @eof_content
end

#fragmentsArray (readonly)

Returns all [Opal::Fragment] used to produce result.

Returns:

  • (Array)

    all [Opal::Fragment] used to produce result



146
147
148
# File 'opal/lib/opal/compiler.rb', line 146

def fragments
  @fragments
end

#resultString (readonly)

Returns The compiled ruby code.

Returns:

  • (String)

    The compiled ruby code



143
144
145
# File 'opal/lib/opal/compiler.rb', line 143

def result
  @result
end

#scopeObject

Current scope



149
150
151
# File 'opal/lib/opal/compiler.rb', line 149

def scope
  @scope
end

Class Method Details

.compiler_option(name, default_value, options = {}) ⇒ Object

defines a compiler option, also creating method of form 'name?'



61
62
63
64
65
66
67
68
69
70
71
72
# File 'opal/lib/opal/compiler.rb', line 61

def self.compiler_option(name, default_value, options = {})
  mid          = options[:as]
  valid_values = options[:valid_values]
  define_method(mid || name) do
    value = @options.fetch(name) { default_value }
    if valid_values && !valid_values.include?(value)
      raise ArgumentError, "invalid value #{value.inspect} for option #{name.inspect} " \
                           "(valid values: #{valid_values.inspect})"
    end
    value
  end
end

.module_name(path) ⇒ Object



55
56
57
58
# File 'opal/lib/opal/compiler.rb', line 55

def self.module_name(path)
  path = File.join(File.dirname(path), File.basename(path).split('.').first)
  Pathname(path).cleanpath.to_s
end

Instance Method Details

#arity_check?Boolean

adds an arity check to every method definition

Returns:

  • (Boolean)


94
# File 'opal/lib/opal/compiler.rb', line 94

compiler_option :arity_check, false, as: :arity_check?

#compileString

Compile some ruby code to a string.

Returns:

  • (String)

    javascript code



172
173
174
175
176
177
178
179
# File 'opal/lib/opal/compiler.rb', line 172

def compile
  parse

  @fragments = re_raise_with_location { process(@sexp).flatten }
  @fragments << fragment("\n", nil, s(:newline)) unless @fragments.last.code.end_with?("\n")

  @result = @fragments.map(&:code).join('')
end

#dynamic_require_severityObject

how to handle dynamic requires (:error, :warning, :ignore)



118
# File 'opal/lib/opal/compiler.rb', line 118

compiler_option :dynamic_require_severity, :ignore, valid_values: %i[error warning ignore]

#enable_source_location?Object

Adds source_location for every method definition



135
# File 'opal/lib/opal/compiler.rb', line 135

compiler_option :enable_source_location, false, as: :enable_source_location?

#error(msg, line = nil) ⇒ Object

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.



225
226
227
228
229
# File 'opal/lib/opal/compiler.rb', line 225

def error(msg, line = nil)
  error = ::Opal::SyntaxError.new(msg)
  error.location = Opal::OpalBacktraceLocation.new(file, line)
  raise error
end

#fileString

The filename to use for compiling this code. Used for FILE directives as well as finding relative require()

Returns:

  • (String)


80
# File 'opal/lib/opal/compiler.rb', line 80

compiler_option :file, '(file)'

#fragment(str, scope, sexp = nil) ⇒ Object



264
265
266
# File 'opal/lib/opal/compiler.rb', line 264

def fragment(str, scope, sexp = nil)
  Fragment.new(str, scope, sexp)
end

#freezing?Boolean

Deprecated.

stubs out #freeze and #frozen?

Returns:

  • (Boolean)


102
# File 'opal/lib/opal/compiler.rb', line 102

compiler_option :freezing, true, as: :freezing?

#handle_block_given_call(sexp) ⇒ Object



457
458
459
460
461
462
463
464
465
466
# File 'opal/lib/opal/compiler.rb', line 457

def handle_block_given_call(sexp)
  @scope.uses_block!
  if @scope.block_name
    fragment("(#{@scope.block_name} !== nil)", scope, sexp)
  elsif (scope = @scope.find_parent_def) && scope.block_name
    fragment("(#{scope.block_name} !== nil)", scope, sexp)
  else
    fragment('false', scope, sexp)
  end
end

#handlersObject



364
365
366
# File 'opal/lib/opal/compiler.rb', line 364

def handlers
  @handlers ||= Opal::Nodes::Base.handlers
end

#helper(name) ⇒ Object

Use the given helper



299
300
301
# File 'opal/lib/opal/compiler.rb', line 299

def helper(name)
  helpers << name
end

#helpersSet<Symbol>

Any helpers required by this file. Used by Nodes::Top to reference runtime helpers that are needed. These are used to minify resulting javascript by keeping a reference to helpers used.

Returns:

  • (Set<Symbol>)


208
209
210
# File 'opal/lib/opal/compiler.rb', line 208

def helpers
  @helpers ||= Set.new(%i[breaker slice])
end

#in_caseObject



338
339
340
341
342
343
344
# File 'opal/lib/opal/compiler.rb', line 338

def in_case
  return unless block_given?
  old = @case_stmt
  @case_stmt = {}
  yield
  @case_stmt = old
end

#in_whileObject

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.



330
331
332
333
334
335
336
# File 'opal/lib/opal/compiler.rb', line 330

def in_while
  return unless block_given?
  @while_loop = @scope.push_while
  result = indent { yield }
  @scope.pop_while
  result
end

#in_while?Boolean

Returns true if the parser is curently handling a while sexp, false otherwise.

Returns:

  • (Boolean)


348
349
350
# File 'opal/lib/opal/compiler.rb', line 348

def in_while?
  @scope.in_while?
end

#indentObject

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.



306
307
308
309
310
311
312
313
314
# File 'opal/lib/opal/compiler.rb', line 306

def indent
  indent = @indent
  @indent += INDENT
  @space = "\n#{@indent}"
  res = yield
  @indent = indent
  @space = "\n#{@indent}"
  res
end

#inline_operators?Object

are operators compiled inline



128
# File 'opal/lib/opal/compiler.rb', line 128

compiler_option :inline_operators, true, as: :inline_operators?

#irb?Object

compile top level local vars with support for irb style vars



113
# File 'opal/lib/opal/compiler.rb', line 113

compiler_option :irb, false, as: :irb?

#method_callsObject

Method calls made in this file



218
219
220
# File 'opal/lib/opal/compiler.rb', line 218

def method_calls
  @method_calls ||= Set.new
end

#method_missing?Boolean

adds method stubs for all used methods in file

Returns:

  • (Boolean)


87
# File 'opal/lib/opal/compiler.rb', line 87

compiler_option :method_missing, true, as: :method_missing?

#operator_helpersObject

Operator helpers



213
214
215
# File 'opal/lib/opal/compiler.rb', line 213

def operator_helpers
  @operator_helpers ||= Set.new
end

#parseObject



181
182
183
184
185
186
187
188
189
190
191
192
# File 'opal/lib/opal/compiler.rb', line 181

def parse
  @buffer = ::Opal::Parser::SourceBuffer.new(file, 1)
  @buffer.source = @source

  @parser = Opal::Parser.default_parser

  sexp, comments, tokens = re_raise_with_location { @parser.tokenize(@buffer) }

  @sexp = s(:top, sexp || s(:nil))
  @comments = ::Parser::Source::Comment.associate_locations(sexp, comments)
  @eof_content = EofContent.new(tokens, @source).eof
end

#parse_comments?Object

Adds comments for every method definition



140
# File 'opal/lib/opal/compiler.rb', line 140

compiler_option :parse_comments, false, as: :parse_comments?

#parser_indentObject

Instances of Scope can use this to determine the current scope indent. The indent is used to keep generated code easily readable.



253
254
255
# File 'opal/lib/opal/compiler.rb', line 253

def parser_indent
  @indent
end

#process(sexp, level = :expr) ⇒ Object

Process the given sexp by creating a node instance, based on its type, and compiling it to fragments.



354
355
356
357
358
359
360
361
362
# File 'opal/lib/opal/compiler.rb', line 354

def process(sexp, level = :expr)
  return fragment('', scope) if sexp.nil?

  if handler = handlers[sexp.type]
    return handler.new(sexp, level, self).compile_to_fragments
  else
    error "Unsupported sexp: #{sexp.type}"
  end
end

#re_raise_with_locationObject



231
232
233
234
235
236
237
238
239
240
241
# File 'opal/lib/opal/compiler.rb', line 231

def re_raise_with_location
  yield
rescue StandardError, ::Opal::SyntaxError => error
  opal_location = ::Opal.opal_location_from_error(error)
  opal_location.path = file
  opal_location.label ||= @source.lines[opal_location.line.to_i - 1].strip
  new_error = ::Opal::SyntaxError.new(error.message)
  new_error.set_backtrace error.backtrace
  ::Opal.add_opal_location_to_error(opal_location, new_error)
  raise new_error
end

#requirable?Object

Prepare the code for future requires



123
# File 'opal/lib/opal/compiler.rb', line 123

compiler_option :requirable, false, as: :requirable?

#required_treesObject

An array of trees required in this file (typically by calling #require_tree)



375
376
377
# File 'opal/lib/opal/compiler.rb', line 375

def required_trees
  @required_trees ||= []
end

#requiresObject

An array of requires used in this file



369
370
371
# File 'opal/lib/opal/compiler.rb', line 369

def requires
  @requires ||= []
end

#returns(sexp) ⇒ Object

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.



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
# File 'opal/lib/opal/compiler.rb', line 390

def returns(sexp)
  return returns s(:nil) unless sexp

  case sexp.type
  when :undef
    # undef :method_name always returns nil
    returns s(:begin, sexp, s(:nil))
  when :break, :next, :redo
    sexp
  when :yield
    sexp.updated(:returnable_yield, nil)
  when :when
    *when_sexp, then_sexp = *sexp
    sexp.updated(nil, [*when_sexp, returns(then_sexp)])
  when :rescue
    body_sexp, *resbodies, else_sexp = *sexp

    resbodies = resbodies.map do |resbody|
      returns(resbody)
    end

    if else_sexp
      else_sexp = returns(else_sexp)
    end

    sexp.updated(
      nil, [
        returns(body_sexp),
        *resbodies,
        else_sexp
      ]
    )
  when :resbody
    klass, lvar, body = *sexp
    sexp.updated(nil, [klass, lvar, returns(body)])
  when :ensure
    rescue_sexp, ensure_body = *sexp
    sexp = sexp.updated(nil, [returns(rescue_sexp), ensure_body])
    s(:js_return, sexp)
  when :begin, :kwbegin
    # Wrapping last expression with s(:js_return, ...)
    *rest, last = *sexp
    sexp.updated(nil, [*rest, returns(last)])
  when :while, :until, :while_post, :until_post
    sexp
  when :return, :js_return, :returnable_yield
    sexp
  when :xstr
    sexp.updated(nil, [s(:js_return, *sexp.children)])
  when :if
    cond, true_body, false_body = *sexp
    sexp.updated(
      nil, [
        cond,
        returns(true_body),
        returns(false_body)
      ]
    )
  else
    s(:js_return, sexp).updated(
      nil,
      nil,
      location: sexp.loc,
    )
  end
end

#s(type, *children) ⇒ Object

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.



260
261
262
# File 'opal/lib/opal/compiler.rb', line 260

def s(type, *children)
  ::Opal::AST::Node.new(type, children)
end

#source_mapOpal::SourceMap

Returns a source map that can be used in the browser to map back to original ruby code.

Parameters:

  • source_file (String)

    optional source_file to reference ruby source

Returns:



199
200
201
# File 'opal/lib/opal/compiler.rb', line 199

def source_map
  ::Opal::SourceMap::File.new(@fragments, file, @source)
end

#tainting?Object

Deprecated.

stubs out #taint, #untaint and #tainted?



108
# File 'opal/lib/opal/compiler.rb', line 108

compiler_option :tainting, true, as: :tainting?

#unique_temp(name) ⇒ Object

Used to generate a unique id name per file. These are used mainly to name method bodies for methods that use blocks.



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
# File 'opal/lib/opal/compiler.rb', line 270

def unique_temp(name)
  name = name.to_s
  if name && !name.empty?
    name = name
           .to_s
           .gsub('<=>', '$lt_eq_gt')
           .gsub('===', '$eq_eq_eq')
           .gsub('==', '$eq_eq')
           .gsub('=~', '$eq_tilde')
           .gsub('!~', '$excl_tilde')
           .gsub('!=', '$not_eq')
           .gsub('<=', '$lt_eq')
           .gsub('>=', '$gt_eq')
           .gsub('=', '$eq')
           .gsub('?', '$ques')
           .gsub('!', '$excl')
           .gsub('/', '$slash')
           .gsub('%', '$percent')
           .gsub('+', '$plus')
           .gsub('-', '$minus')
           .gsub('<', '$lt')
           .gsub('>', '$gt')
           .gsub(/[^\w\$]/, '$')
  end
  unique = (@unique += 1)
  "#{'$' unless name.start_with?('$')}#{name}$#{unique}"
end

#warning(msg, line = nil) ⇒ Object

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.



246
247
248
# File 'opal/lib/opal/compiler.rb', line 246

def warning(msg, line = nil)
  warn "warning: #{msg} -- #{file}:#{line}"
end

#with_tempObject

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.



321
322
323
324
325
326
# File 'opal/lib/opal/compiler.rb', line 321

def with_temp
  tmp = @scope.new_temp
  res = yield tmp
  @scope.queue_temp tmp
  res
end