class Opal::Compiler

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

@example

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

@example Accessing result

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

@example Source Maps

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

Constants

COMPARE

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

INDENT

Generated code gets indented with two spaces on each scope

Attributes

case_stmt[R]

Current case_stmt

comments[R]

Comments from the source code

dynamic_cache_result[RW]

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

eof_content[R]

Any content in __END__ special construct

fragments[R]

@return [Array] all [Opal::Fragment] used to produce result

magic_comments[R]

Magic comment flags extracted from the leading comments

method_calls[R]

Method calls made in this file

result[R]

@return [String] The compiled ruby code

scope[RW]

Current scope

source[R]

Access the source code currently processed

top_scope[RW]

Top scope

Public Class Methods

compiler_option(name, config = {}) click to toggle source

Defines a compiler option. @option as: [Symbol] uses a different method name, e.g. with a question mark for booleans @option default: [Object] the default value for the option @option magic_comment: [Bool] allows magic-comments to override the option value

# File 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) click to toggle source
# File 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
new(source, options = {}) click to toggle source
# File lib/opal/compiler.rb, line 281
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

Public Instance Methods

async_await() click to toggle source
# File lib/opal/compiler.rb, line 358
def async_await
  if defined? @async_await
    @async_await
  else
    original = async_await_before_typecasting
    @async_await = case original
                   when String
                     async_await_set_to_regexp(original.split(',').map { |h| h.strip.to_sym })
                   when Array, Set
                     async_await_set_to_regexp(original.to_a.map(&:to_sym))
                   when Regexp, true, false
                     original
                   else
                     raise 'A value of await compiler option can be either ' \
                           'a Set, an Array, a String or a Boolean.'
                   end
  end
end
async_await_before_typecasting()
Alias for: async_await
async_await_set_to_regexp(set) click to toggle source
# File lib/opal/compiler.rb, line 377
def async_await_set_to_regexp(set)
  set = set.map { |name| Regexp.escape(name.to_s).gsub('\*', '.*?') }
  set = set.join('|')
  /^(#{set})$/
end
autoloads() click to toggle source

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

# File lib/opal/compiler.rb, line 540
def autoloads
  @autoloads ||= []
end
backtick_javascript_or_warn?() click to toggle source

Warn about impending compatibility break

# File lib/opal/compiler.rb, line 198
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
compile() click to toggle source

Compile some ruby code to a string.

@return [String] javascript code

# File lib/opal/compiler.rb, line 297
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
error(msg, line = nil) click to toggle source

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.

# File lib/opal/compiler.rb, line 386
def error(msg, line = nil)
  error = ::Opal::SyntaxError.new(msg)
  error.location = Opal::OpalBacktraceLocation.new(file, line)
  raise error
end
fragment(str, scope, sexp = nil) click to toggle source
# File lib/opal/compiler.rb, line 423
def fragment(str, scope, sexp = nil)
  Fragment.new(str, scope, sexp)
end
handle_block_given_call(sexp) click to toggle source
# File lib/opal/compiler.rb, line 629
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
handlers() click to toggle source
# File lib/opal/compiler.rb, line 523
def handlers
  @handlers ||= Opal::Nodes::Base.handlers
end
helper(name) click to toggle source

Use the given helper

# File lib/opal/compiler.rb, line 458
def helper(name)
  helpers << name
end
helpers() click to toggle source

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

@return [Set<Symbol>]

# File lib/opal/compiler.rb, line 347
def helpers
  @helpers ||= Set.new(
    magic_comments[:helpers].to_s.split(',').map { |h| h.strip.to_sym }
  )
end
in_case() { || ... } click to toggle source
# File lib/opal/compiler.rb, line 497
def in_case
  return unless block_given?
  old = @case_stmt
  @case_stmt = {}
  yield
  @case_stmt = old
end
in_while() { || ... } click to toggle source

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.

# File lib/opal/compiler.rb, line 489
def in_while
  return unless block_given?
  @while_loop = @scope.push_while
  result = yield
  @scope.pop_while
  result
end
in_while?() click to toggle source

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

# File lib/opal/compiler.rb, line 507
def in_while?
  @scope.in_while?
end
indent() { || ... } click to toggle source

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.

# File lib/opal/compiler.rb, line 465
def indent
  indent = @indent
  @indent += INDENT
  @space = "\n#{@indent}"
  res = yield
  @indent = indent
  @space = "\n#{@indent}"
  res
end
marshal_dump() click to toggle source

Marshalling for cache shortpath

# File lib/opal/compiler.rb, line 641
def marshal_dump
  [@options, @option_values, @source_map ||= source_map.cache,
   @magic_comments, @result,
   @required_trees, @requires, @autoloads]
end
marshal_load(src) click to toggle source
# File lib/opal/compiler.rb, line 647
def marshal_load(src)
  @options, @option_values, @source_map,
  @magic_comments, @result,
  @required_trees, @requires, @autoloads = src
end
option_value(name, config) click to toggle source

Fetches and memoizes the value for an option.

# File 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
parse() click to toggle source
# File lib/opal/compiler.rb, line 306
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
parser_indent() click to toggle source

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

# File lib/opal/compiler.rb, line 414
def parser_indent
  @indent
end
process(sexp, level = :expr) click to toggle source

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

# File lib/opal/compiler.rb, line 513
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_location() { || ... } click to toggle source
# File lib/opal/compiler.rb, line 392
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) click to toggle source
# File lib/opal/compiler.rb, line 353
def record_method_call(mid)
  @method_calls << mid
end
required_trees() click to toggle source

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

# File lib/opal/compiler.rb, line 534
def required_trees
  @required_trees ||= []
end
requires() click to toggle source

An array of requires used in this file

# File lib/opal/compiler.rb, line 528
def requires
  @requires ||= []
end
returns(sexp) click to toggle source

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.

# File lib/opal/compiler.rb, line 555
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) click to toggle source

Create a new sexp using the given parts.

# File lib/opal/compiler.rb, line 419
def s(type, *children)
  ::Opal::AST::Node.new(type, children)
end
source_map() click to toggle source

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

@param source_file [String] optional source_file to reference ruby source @return [Opal::SourceMap]

# File lib/opal/compiler.rb, line 337
def source_map
  # We only use @source_map if compiler is cached.
  @source_map || ::Opal::SourceMap::File.new(@fragments, file, @source, @result)
end
unique_temp(name) click to toggle source

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

# File lib/opal/compiler.rb, line 429
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) click to toggle source

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.

# File lib/opal/compiler.rb, line 407
def warning(msg, line = nil)
  warn "warning: #{msg} -- #{file}:#{line}"
end
with_temp() { |tmp| ... } click to toggle source

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.

# File lib/opal/compiler.rb, line 480
def with_temp
  tmp = @scope.new_temp
  res = yield tmp
  @scope.queue_temp tmp
  res
end