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
Current case_stmt
Comments from the source code
Set if some rewritter caused a dynamic cache result, meaning it’s not fit to be cached
Any content in __END__ special construct
@return [Array] all [Opal::Fragment] used to produce result
Magic comment flags extracted from the leading comments
Method calls made in this file
@return [String] The compiled ruby code
Current scope
Access the source code currently processed
Top scope
Public Class Methods
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
# 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
# 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
# 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
# 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
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
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 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
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
# File lib/opal/compiler.rb, line 423 def fragment(str, scope, sexp = nil) Fragment.new(str, scope, sexp) end
# 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
# File lib/opal/compiler.rb, line 523 def handlers @handlers ||= Opal::Nodes::Base.handlers end
Use the given helper
# File lib/opal/compiler.rb, line 458 def helper(name) helpers << name end
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
# File lib/opal/compiler.rb, line 497 def in_case return unless block_given? old = @case_stmt @case_stmt = {} yield @case_stmt = old 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.
# 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
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
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
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
# 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
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
# 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
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 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
# 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
# File lib/opal/compiler.rb, line 353 def record_method_call(mid) @method_calls << mid end
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
An array of requires used in this file
# File lib/opal/compiler.rb, line 528 def requires @requires ||= [] 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.
# 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
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
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
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
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
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