Slide 1

Slide 1 text

Vladimir Dementyev Evil Martians The why’s and how’s of transpiling Ruby

Slide 2

Slide 2 text

palkan_tula palkan RubyKaigi‘20 Ruby 2

Slide 3

Slide 3 text

palkan_tula palkan RubyKaigi‘20 Transpiler 3 Illustration from Ruby Weekly #477 Source-to-source compiler

Slide 4

Slide 4 text

palkan_tula palkan RubyKaigi‘20 The most popular transpiler 4

Slide 5

Slide 5 text

palkan_tula palkan RubyKaigi‘20 Yet another transpiler 5

Slide 6

Slide 6 text

palkan_tula palkan RubyKaigi‘20 6 Ruby vs. Transpiling

Slide 7

Slide 7 text

palkan_tula palkan RubyKaigi‘20 Ruby 3.02.8 7

Slide 8

Slide 8 text

palkan_tula palkan RubyKaigi‘20 Ruby is evolving faster than ever 8

Slide 9

Slide 9 text

palkan_tula palkan RubyKaigi‘20 Ruby 2.8 9 def greet(val) = case val in hello: hello if hello =~ /human/i '' in hello: 'martian' '' end greet(hello: 'martian') => greeting puts greeting # =>

Slide 10

Slide 10 text

palkan_tula palkan RubyKaigi‘20 Ruby 2.8 10 def greet(val) = case val in hello: hello if hello =~ /human/i '' in hello: 'martian' '' end greet(hello: 'martian') => greeting puts greeting # => Pattern matching (2.7)

Slide 11

Slide 11 text

palkan_tula palkan RubyKaigi‘20 Ruby 2.8 11 def greet(val) = case val in hello: hello if hello =~ /human/i '' in hello: 'martian' '' end greet(hello: 'martian') => greeting puts greeting # => Endless method (2.8)

Slide 12

Slide 12 text

palkan_tula palkan RubyKaigi‘20 Ruby 2.8 12 def greet(val) = case val in hello: hello if hello =~ /human/i '' in hello: 'martian' '' end greet(hello: 'martian') => greeting puts greeting # => Rightward assignment (2.8)

Slide 13

Slide 13 text

palkan_tula palkan RubyKaigi‘20 How to use upcoming features today? 13

Slide 14

Slide 14 text

palkan_tula palkan RubyKaigi‘20 Transpiler for Ruby 14

Slide 15

Slide 15 text

palkan_tula palkan RubyKaigi‘20 $ ruby -v
 2.5.3p105
 $ ruby -ruby-next -e "
 case {hello: 'こんにちは'}
 in hello: hello if hello =~ /[\p{Hiragana}]+/
 puts '私はウラジミールです'
 in hello: hello if hello =~ /[a-z]+/i
 puts 'My name is Vladimir'
 end"
 私はウラジミールです 15 tl;dr

Slide 16

Slide 16 text

palkan_tula palkan RubyKaigi‘20 About me 16 github.com/palkan

Slide 17

Slide 17 text

palkan_tula palkan RubyKaigi‘20 evilmartians.com 17

Slide 18

Slide 18 text

palkan_tula palkan RubyKaigi‘20 18 evilmartians.com

Slide 19

Slide 19 text

palkan_tula palkan RubyKaigi‘20 evilmartians.com 19 Ruby Next Transpiler for Ruby

Slide 20

Slide 20 text

palkan_tula palkan RubyKaigi‘20 evl.ms/blog 20

Slide 21

Slide 21 text

palkan_tula palkan RubyKaigi‘20 Why transpiling Ruby? 21

Slide 22

Slide 22 text

palkan_tula palkan RubyKaigi‘20 Backporting Why transpiling? 22

Slide 23

Slide 23 text

palkan_tula palkan RubyKaigi‘20 stats.rubygems.org 23

Slide 24

Slide 24 text

palkan_tula palkan RubyKaigi‘20 Gem authors should stick to older versions 24

Slide 25

Slide 25 text

palkan_tula palkan RubyKaigi‘20 Story: Hanami::API 25

Slide 26

Slide 26 text

palkan_tula palkan RubyKaigi‘20 26 Story: Hanami::API

Slide 27

Slide 27 text

palkan_tula palkan RubyKaigi‘20 Backporting Interoperability Why transpiling? 27

Slide 28

Slide 28 text

palkan_tula palkan RubyKaigi‘20 JRuby 9.3 — 2.6 TruffleRuby — 2.6 mruby ~ 2.7 (no pattern matching) Opal, RubyMotion, Artichoke — ??? Syntax support 28

Slide 29

Slide 29 text

palkan_tula palkan RubyKaigi‘20 Backporting Interoperability ... Why transpiling? 29

Slide 30

Slide 30 text

palkan_tula palkan RubyKaigi‘20 How to transpile Ruby? 30

Slide 31

Slide 31 text

palkan_tula palkan RubyKaigi‘20 Transpiler is a source-to- source compiler 31

Slide 32

Slide 32 text

palkan_tula palkan RubyKaigi‘20 Compilers 32 @pgurtovaya

Slide 33

Slide 33 text

palkan_tula palkan RubyKaigi‘20 Parse Analyze/optimize Generate Transpiling 33

Slide 34

Slide 34 text

palkan_tula palkan RubyKaigi‘20 Transpiling 34 source code new source code AST new AST

Slide 35

Slide 35 text

palkan_tula palkan RubyKaigi‘20 Ripper RubyVM::AbstractSyntaxTree Parser Source to AST 35

Slide 36

Slide 36 text

palkan_tula palkan RubyKaigi‘20 Written in pure Ruby Version-independent Bullet-proofed (e.g., by RuboCop) Parser 36

Slide 37

Slide 37 text

palkan_tula palkan RubyKaigi‘20 github.com/whitequark/parser 37

Slide 38

Slide 38 text

palkan_tula palkan RubyKaigi‘20 def transpile(source) rewriters.inject(source) do |src, rewriter| buffer = Parser ::Source ::Buffer.new("") buffer.source = src rewriter.new.rewrite(buffer, parse(src)) end end 38

Slide 39

Slide 39 text

palkan_tula palkan RubyKaigi‘20 module Rewriters class ArgsForward < Base def on_forward_args(node) replace(node.loc.expression, "(* __rest __, & __block __") end def on_send(node) return super(node) unless node.children[2]&.type == :forwarded_args replace(node.children[2].loc.expression, "* __rest __, & __block __") end end end 39 Rewriter

Slide 40

Slide 40 text

palkan_tula palkan RubyKaigi‘20 Ruby Next Transpiling 40 source code new source code AST in-place rewrite unparse bits of AST

Slide 41

Slide 41 text

palkan_tula palkan RubyKaigi‘20 41 def fizzbuzz(num) case [num % 3, num % 5] in [0, 0] then 'FizzBuzz' in [0, _] then 'Fizz' in [_, 0] then 'Buzz' end end Example

Slide 42

Slide 42 text

palkan_tula palkan RubyKaigi‘20 42 def fizzbuzz(num) rems = [num % 3, num % 5] if rems == [0, 0] 'FizzBuzz' elsif rems[0] == 0 'Fizz' elsif rems[1] == 1 'Buzz' else raise "NoMatchingPattern" end end def fizzbuzz(num) case [num % 3, num % 5] in [0, 0] then 'FizzBuzz' in [0, _] then 'Fizz' in [_, 0] then 'Buzz' end end 2.7 2.5 "Transpile" by hand

Slide 43

Slide 43 text

palkan_tula palkan RubyKaigi‘20 43 def fizzbuzz(num) case; when ( __m __ = [(num % 3), (num % 5)]) && false when ( __p_1 __ = ( __m __.respond_to?(:deconstruct) && ((( __m_arr __ = __m __.deconstruct) || true) && ((Array === __m_arr __) || Kernel.raise(TypeError, "#deconstruct must return Array"))))) && (( __p_2 __ = (2 == __m_arr __.size)) && ((0 === __m_arr __[0]) && (0 === __m_arr __[1]))) then 'FizzBuzz' when __p_1 __ && ( __p_2 __ && (0 === __m_arr __[0])) then 'Fizz' when __p_1 __ && ( __p_2 __ && (0 === __m_arr __[1])) then 'Buzz'; else; Kernel.raise(NoMatchingPatternError, __m __.inspect) end end def fizzbuzz(num) case [num % 3, num % 5] in [0, 0] then 'FizzBuzz' in [0, _] then 'Fizz' in [_, 0] then 'Buzz' end end For humans (2.7) For machines (2.5) Transpile with Ruby Next

Slide 44

Slide 44 text

palkan_tula palkan RubyKaigi‘20 Parse Analyze/optimize Generate Transpiling 44

Slide 45

Slide 45 text

palkan_tula palkan RubyKaigi‘20 module Rewriters class PatternMatching < Base def on_case_match(node) # ~800 LOC end end end 45 Pattern matching

Slide 46

Slide 46 text

palkan_tula palkan RubyKaigi‘20 def call(val) status, headers, body = 200, {}, "" case val in [String => body] [status, headers, [body]] in [Integer => status] [status, headers, [body]] in [Integer, String] => response [response[0], headers, [response[1]]] in [Integer, Hash, String] => response headers.merge!(response[1]) [response[0], headers, [response[2]]] end end 46 Comparison: transpiled (last pattern): 1162200.7 i/s baseline (last pattern): 799739.5 i/s - 1.45x slower Transpiled != Slow

Slide 47

Slide 47 text

palkan_tula palkan RubyKaigi‘20 47

Slide 48

Slide 48 text

palkan_tula palkan RubyKaigi‘20 How to integrate a transpiler into an interpreted language? 48

Slide 49

Slide 49 text

palkan_tula palkan RubyKaigi‘20 Gems: transpile at “build”/ release time Apps/Scripts: transpile at runtime 49

Slide 50

Slide 50 text

palkan_tula palkan RubyKaigi‘20 $LOAD_PATH: where to search for “features” $LOADED_FEATURES: required files Two pillars of require 50

Slide 51

Slide 51 text

palkan_tula palkan RubyKaigi‘20 Add transpiled files to releases No additional runtime deps* Nextify gems 51 * polyfills might still be required

Slide 52

Slide 52 text

palkan_tula palkan RubyKaigi‘20 $ ruby-next nextify ./lib -V
 Generated: ./lib/.rbnext/2.7/rubanok/rule.rb
 Generated: ./lib/.rbnext/2.8/rubanok/dsl/matching.rb 52

Slide 53

Slide 53 text

palkan_tula palkan RubyKaigi‘20 # lib/my_gem.rb require "ruby-next/language/setup" RubyNext ::Language.setup_gem_load_path 53

Slide 54

Slide 54 text

palkan_tula palkan RubyKaigi‘20 def setup_gem_load_path lib_dir = File.dirname(caller_locations(1, 1).first.path) current_index = $LOAD_PATH.index(lib_dir) next_dir = File.join(lib_dir, ".rbnext") if File.exist?(next_dir) $LOAD_PATH.insert current_index, next_dir current_index += 1 end end 54

Slide 55

Slide 55 text

palkan_tula palkan RubyKaigi‘20 Hijack Kernel#require & co Reimplement require mechanism Runtime 55

Slide 56

Slide 56 text

palkan_tula palkan RubyKaigi‘20 module Kernel module_function alias_method :require_without_ruby_next, :require def require(path) RubyNext.require(path) rescue => e warn "Failed to require ' #{path}': #{e.message}" require_without_ruby_next(path) end end 56 Kernel#require *Don't try this at home

Slide 57

Slide 57 text

palkan_tula palkan RubyKaigi‘20 module RubyNext module_function def require(path) realpath = resolve_feature_path(path) return false if $LOADED_FEATURES.include?(realpath) File.read(realpath) .then(&Language.:transform).then do |source| RubyVM ::InstructionSequence.compile(code, filepath).then(&:eval) $LOADED_FEATURES << path true end end end 57 RubyNext.require

Slide 58

Slide 58 text

palkan_tula palkan RubyKaigi‘20 How to use a transpiler with compilable Rubies? 58

Slide 59

Slide 59 text

palkan_tula palkan RubyKaigi‘20 Transpile source files before compiling Replace target files with the transpiled versions at build time mruby 59

Slide 60

Slide 60 text

palkan_tula palkan RubyKaigi‘20 desc "transpile source code with ruby-next" task rbnext: [] do Dir.chdir(APP_ROOT) do sh "ruby-next nextify -V" end end Rake ::Task["compile"].enhance [:rbnext] 60 Nextify before compiling

Slide 61

Slide 61 text

palkan_tula palkan RubyKaigi‘20 using(Module.new do refine MRuby ::Gem ::Specification do def setup_ruby_next!(next_dir: ".rbnext") lib_root = File.join(@dir, @mrblib_dir) next_root = Pathname.new(next_dir).absolute? ? next_dir : File.join(lib_root, next_dir) Dir.glob(" #{next_root}/**/*.rb").each do |next_file| orig_file = next_file.sub(next_root, lib_root) index = @rbfiles.index(orig_file) || raise "Source file not found for: #{next_file}" @rbfiles[index] = next_file end end end end) MRuby ::Gem ::Specification.new("acli") do |spec| # ... spec.setup_ruby_next! end 61 Update rbfiles

Slide 62

Slide 62 text

palkan_tula palkan RubyKaigi‘20 Backporting Interoperability Evolution Why transpiling? 62

Slide 63

Slide 63 text

palkan_tula palkan RubyKaigi‘20 Recent Ruby versions added experimental features 63

Slide 64

Slide 64 text

palkan_tula palkan RubyKaigi‘20 We need an experiment 64

Slide 65

Slide 65 text

palkan_tula palkan RubyKaigi‘20 The best way to taste new features is to start using them every day 65

Slide 66

Slide 66 text

palkan_tula palkan RubyKaigi‘20 66 Example: Hash shorthand

Slide 67

Slide 67 text

palkan_tula palkan RubyKaigi‘20 67 Hash shorthand

Slide 68

Slide 68 text

palkan_tula palkan RubyKaigi‘20 $ ruby -v
 2.5.3p105
 $ ruby -ruby-next -e "
 event = "RubyKaigi" year = 2020 p {event, year}
 "
 {event: "RubyKaigi", year: 2020} 68 Ruby Next 0.10.0

Slide 69

Slide 69 text

palkan_tula palkan RubyKaigi‘20 Share your opinion, let's make future Ruby together! 69

Slide 70

Slide 70 text

palkan_tula palkan RubyKaigi‘20 github.com/ruby-next/ruby-next 70

Slide 71

Slide 71 text

Thank you! evilmartians.com github.com/ruby-next evilmartians palkan_tula Vladimir Dementyev