Class: Opal::Compiler

Inherits:
Object
  • Object
show all
Includes:
Nodes::Closure::CompilerSupport
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

Methods included from Nodes::Closure::CompilerSupport

#closure_stack

Constructor Details

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

Returns a new instance of Compiler.



287
288
289
290
291
292
293
294
295
296
297
298
# File 'opal/lib/opal/compiler.rb', line 287

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

Instance Attribute Details

#case_stmtObject (readonly)

Current case_stmt



266
267
268
# File 'opal/lib/opal/compiler.rb', line 266

def case_stmt
  @case_stmt
end

#commentsObject (readonly)

Comments from the source code



272
273
274
# File 'opal/lib/opal/compiler.rb', line 272

def comments
  @comments
end

#dynamic_cache_resultObject

Set if some rewritter caused a dynamic cache result, meaning it's not fit to be cached



285
286
287
# File 'opal/lib/opal/compiler.rb', line 285

def dynamic_cache_result
  @dynamic_cache_result
end

#eof_contentObject (readonly)

Any content in END special construct



269
270
271
# File 'opal/lib/opal/compiler.rb', line 269

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



257
258
259
# File 'opal/lib/opal/compiler.rb', line 257

def fragments
  @fragments
end

#magic_commentsObject (readonly)

Magic comment flags extracted from the leading comments



278
279
280
# File 'opal/lib/opal/compiler.rb', line 278

def magic_comments
  @magic_comments
end

#method_callsObject (readonly)

Method calls made in this file



275
276
277
# File 'opal/lib/opal/compiler.rb', line 275

def method_calls
  @method_calls
end

#resultString (readonly)

Returns The compiled ruby code.

Returns:

  • (String)

    The compiled ruby code



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

def result
  @result
end

#scopeObject

Current scope



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

def scope
  @scope
end

#sourceObject (readonly)

Access the source code currently processed



281
282
283
# File 'opal/lib/opal/compiler.rb', line 281

def source
  @source
end

#top_scopeObject

Top scope



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

def top_scope
  @top_scope
end

Class Method Details

.compiler_option(name, config = {}) ⇒ Object

Defines a compiler option.

Parameters:

  • as: (Hash)

    a customizable set of options

  • default: (Hash)

    a customizable set of options

  • magic_comment: (Hash)

    a customizable set of options



72
73
74
75
# File 'opal/lib/opal/compiler.rb', line 72

def self.compiler_option(name, config = {})
  method_name = config.fetch(:as, name)
  define_method(method_name) { option_value(name, config) }
end

.module_name(path) ⇒ Object



63
64
65
66
# File 'opal/lib/opal/compiler.rb', line 63

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)


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

compiler_option :arity_check, default: false, as: :arity_check?

#async_awaitObject

Enable async/await support and optionally enable auto-await.

Use either true, false, an Array of Symbols, a String containing names to auto-await separated by a comma or a Regexp.

Auto-await awaits provided methods by default as if .await was added to them automatically.

By default, the support is disabled (set to false).

If the config value is not set to false, any calls to #await will be translated to ES8 await keyword which makes the scope return a Promise and a containing scope will be async (instead of a value, it will return a Promise).

If the config value is an array, or a String separated by a comma, auto-await is also enabled.

A member of this collection can contain a wildcard character * in which case all methods containing a given substring will be awaited.

It can be used as a magic comment, examples:

# await: true
# await: *await*
# await: *await*, sleep, gets


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

compiler_option :await, default: false, as: :async_await, magic_comment: true

#async_await_before_typecastingObject

Enable async/await support and optionally enable auto-await.

Use either true, false, an Array of Symbols, a String containing names to auto-await separated by a comma or a Regexp.

Auto-await awaits provided methods by default as if .await was added to them automatically.

By default, the support is disabled (set to false).

If the config value is not set to false, any calls to #await will be translated to ES8 await keyword which makes the scope return a Promise and a containing scope will be async (instead of a value, it will return a Promise).

If the config value is an array, or a String separated by a comma, auto-await is also enabled.

A member of this collection can contain a wildcard character * in which case all methods containing a given substring will be awaited.

It can be used as a magic comment, examples:

# await: true
# await: *await*
# await: *await*, sleep, gets


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

compiler_option :await, default: false, as: :async_await, magic_comment: true

#async_await_set_to_regexp(set) ⇒ Object



383
384
385
386
387
# File 'opal/lib/opal/compiler.rb', line 383

def async_await_set_to_regexp(set)
  set = set.map { |name| Regexp.escape(name.to_s).gsub('\*', '.*?') }
  set = set.join('|')
  /^(#{set})$/
end

#autoloadsObject

An array of things (requires, trees) which don't need to success in loading compile-time.



546
547
548
# File 'opal/lib/opal/compiler.rb', line 546

def autoloads
  @autoloads ||= []
end

#backtick_javascript?Object

Allows use of a backtick operator (and %x{}) to embed verbatim JavaScript. If false, backtick operator will



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

compiler_option :backtick_javascript, default: nil, as: :backtick_javascript?, magic_comment: true

#backtick_javascript_or_warn?Boolean

Warn about impending compatibility break

Returns:

  • (Boolean)


204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'opal/lib/opal/compiler.rb', line 204

def backtick_javascript_or_warn?
  case backtick_javascript?
  when true
    true
  when nil
    @backtick_javascript_warned ||= begin
      warning 'Backtick operator usage interpreted as intent to embed JavaScript; this code will ' \
              'break in Opal 2.0; add a magic comment: `# backtick_javascript: true`'
      true
    end

    true
  when false
    false
  end
end

#compileString

Compile some ruby code to a string.

Returns:

  • (String)

    javascript code



303
304
305
306
307
308
309
310
# File 'opal/lib/opal/compiler.rb', line 303

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

#directory?Object

Builds a JavaScript file that is aimed to reside as part of a directory for an import map build or something similar.



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

compiler_option :directory, default: false, as: :directory?

#dynamic_require_severityObject

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



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

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

#enable_file_source_embed?Object

Embeds source code along compiled files



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

compiler_option :enable_file_source_embed, default: false, as: :enable_file_source_embed?

#enable_source_location?Object

Adds source_location for every method definition



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

compiler_option :enable_source_location, default: 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.



392
393
394
395
396
# File 'opal/lib/opal/compiler.rb', line 392

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

#esm?Object

Encourage ESM semantics, eg. exporting run result



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

compiler_option :esm, default: false, as: :esm?

#fileString

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

Returns:

  • (String)


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

compiler_option :file, default: '(file)'

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



429
430
431
# File 'opal/lib/opal/compiler.rb', line 429

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

#freezing?Boolean

Deprecated.

stubs out #freeze and #frozen?

Returns:

  • (Boolean)


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

compiler_option :freezing, default: true, as: :freezing?

#handle_block_given_call(sexp) ⇒ Object



635
636
637
638
639
640
641
642
643
644
# File 'opal/lib/opal/compiler.rb', line 635

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



529
530
531
# File 'opal/lib/opal/compiler.rb', line 529

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

#helper(name) ⇒ Object

Use the given helper



464
465
466
# File 'opal/lib/opal/compiler.rb', line 464

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>)


353
354
355
356
357
# File 'opal/lib/opal/compiler.rb', line 353

def helpers
  @helpers ||= Set.new(
    magic_comments[:helpers].to_s.split(',').map { |h| h.strip.to_sym }
  )
end

#in_caseObject



503
504
505
506
507
508
509
# File 'opal/lib/opal/compiler.rb', line 503

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.



495
496
497
498
499
500
501
# File 'opal/lib/opal/compiler.rb', line 495

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

#in_while?Boolean

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

Returns:

  • (Boolean)


513
514
515
# File 'opal/lib/opal/compiler.rb', line 513

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.



471
472
473
474
475
476
477
478
479
# File 'opal/lib/opal/compiler.rb', line 471

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



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

compiler_option :inline_operators, default: true, as: :inline_operators?

#irb?Object

compile top level local vars with support for irb style vars



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

compiler_option :irb, default: false, as: :irb?

#load?Object

Instantly load a requirable module



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

compiler_option :load, default: false, as: :load?

#marshal_dumpObject

Marshalling for cache shortpath



652
653
654
655
656
# File 'opal/lib/opal/compiler.rb', line 652

def marshal_dump
  [@options, @option_values, @source_map ||= source_map.cache,
   @magic_comments, @result,
   @required_trees, @requires, @autoloads]
end

#marshal_load(src) ⇒ Object



658
659
660
661
662
# File 'opal/lib/opal/compiler.rb', line 658

def marshal_load(src)
  @options, @option_values, @source_map,
  @magic_comments, @result,
  @required_trees, @requires, @autoloads = src
end

#method_missing?Boolean

adds method stubs for all used methods in file

Returns:

  • (Boolean)


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

compiler_option :method_missing, default: true, as: :method_missing?

#no_export?Object

Don't export this compile, even if ESM mode is enabled. We use this internally in CLI, so that even if ESM output is desired, we would only have one default export.



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

compiler_option :no_export, default: false, as: :no_export?

#option_value(name, config) ⇒ Object

Fetches and memoizes the value for an option.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'opal/lib/opal/compiler.rb', line 78

def option_value(name, config)
  return @option_values[name] if @option_values.key? name

  default_value = config[:default]
  valid_values  = config[:valid_values]
  magic_comment = config[:magic_comment]

  value = @options.fetch(name, default_value)

  if magic_comment && @magic_comments.key?(name)
    value = @magic_comments.fetch(name)
  end

  if valid_values && !valid_values.include?(value)
    raise(
      ArgumentError,
      "invalid value #{value.inspect} for option #{name.inspect} " \
      "(valid values: #{valid_values.inspect})"
    )
  end

  @option_values[name] = value
end

#parseObject



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

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) }

  kind = case
         when requirable?
           :require
         when eval?
           :eval
         else
           :main
         end

  @sexp = sexp.tap { |i| i.meta[:kind] = kind }

  first_node = sexp.children.first if sexp.children.first.location

  @comments = ::Parser::Source::Comment.associate_locations(first_node, comments)
  @magic_comments = MagicComments.parse(first_node, comments)
  @eof_content = EofContent.new(tokens, @source).eof
end

#parse_comments?Object

Adds comments for every method definition



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

compiler_option :parse_comments, default: 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.



420
421
422
# File 'opal/lib/opal/compiler.rb', line 420

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.



519
520
521
522
523
524
525
526
527
# File 'opal/lib/opal/compiler.rb', line 519

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



398
399
400
401
402
403
404
405
406
407
408
# File 'opal/lib/opal/compiler.rb', line 398

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

#record_method_call(mid) ⇒ Object



359
360
361
# File 'opal/lib/opal/compiler.rb', line 359

def record_method_call(mid)
  @method_calls << mid
end

#requirable?Object

Prepare the code for future requires



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

compiler_option :requirable, default: false, as: :requirable?

#required_treesObject

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



540
541
542
# File 'opal/lib/opal/compiler.rb', line 540

def required_trees
  @required_trees ||= []
end

#requiresObject

An array of requires used in this file



534
535
536
# File 'opal/lib/opal/compiler.rb', line 534

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.



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

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

  case sexp.type
  when :undef
    # undef :method_name always returns nil
    returns sexp.updated(:begin, [sexp, s(:nil)])
  when :break, :next, :redo, :retry
    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])
    sexp.updated(: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
    if backtick_javascript_or_warn?
      sexp.updated(nil, [s(:js_return, *sexp.children)])
    else
      sexp
    end
  when :if
    cond, true_body, false_body = *sexp
    sexp.updated(
      nil, [
        cond,
        returns(true_body),
        returns(false_body)
      ]
    ).tap { |s| s.meta[:returning] = true }
  else
    if sexp.type == :send && sexp.children[1] == :debugger
      # debugger is a statement, so it doesn't return a value
      # and returning it is invalid. Therefore we update it
      # to do `debugger; return nil`.
      sexp.updated(:begin, [sexp, s(:js_return, s(:nil))])
    else
      sexp.updated(:js_return, [sexp])
    end
  end
end

#s(type, *children) ⇒ Object

Create a new sexp using the given parts.



425
426
427
# File 'opal/lib/opal/compiler.rb', line 425

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:



343
344
345
346
# File 'opal/lib/opal/compiler.rb', line 343

def source_map
  # We only use @source_map if compiler is cached.
  @source_map || ::Opal::SourceMap::File.new(@fragments, file, @source, @result)
end

#track_require(mod) ⇒ Object

Track a module as required, so that builder will know to process it



647
648
649
# File 'opal/lib/opal/compiler.rb', line 647

def track_require(mod)
  requires << mod
end

#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.



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

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

#use_strict?Object

Enables JavaScript's strict mode (i.e., adds 'use strict'; statement)



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

compiler_option :use_strict, default: false, as: :use_strict?, magic_comment: true

#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.



413
414
415
# File 'opal/lib/opal/compiler.rb', line 413

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.



486
487
488
489
490
491
# File 'opal/lib/opal/compiler.rb', line 486

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