├── EN ├── 1_culture.md ├── 2_mri_structure.md ├── 3_practice.md ├── 4_bug.md ├── 5_performance.md └── task_ideas.md ├── JA ├── 1_culture.md ├── 2_mri_structure.md ├── 3_practice.md ├── 4_bug.md ├── 5_performance.md ├── 6_coverage.md ├── img │ ├── 6_coverage_fig1.png │ └── 6_coverage_fig2.png └── tasks.md ├── README.md ├── array_second ├── array_second.c └── extconf.rb ├── bib.md ├── docker └── Dockerfile.bionic ├── events ├── rhc_1.md ├── rhc_2.md ├── rhc_3.md ├── rhc_4.md ├── rhc_5.md └── rhc_kosenconf2018.md └── yasm ├── README.md ├── asm ├── answers.rb ├── asmfib.rb ├── fastfib.rb ├── fib.rb └── task.rb ├── ast2iseq ├── ast2iseq.rb ├── ast2iseq_ans_composite.rb ├── ast2iseq_ans_func.rb ├── ast2iseq_ans_visitor.rb ├── ast2iseq_composite.rb ├── ast2iseq_func.rb ├── ast2iseq_visitor.rb ├── ruby2ast.rb ├── ruby_nodes.rb └── test.rb ├── try.rb └── yasm.rb /EN/1_culture.md: -------------------------------------------------------------------------------- 1 | # (1) Introduction of MRI development culture 2 | 3 | ## About this document 4 | 5 | This document introduces the development culture of MRI. MRI: Matz's Ruby Interpreter (a.k.a. the ruby command you are using) is OSS: Open Source Software. Development of MRI started in 1993 (published in Dec. 1995). Because MRI is OSS, anyone can join in its development. However if you don't know anything about MRI development, it is hard to join. So, this article introduces how we develop MRI. 6 | 7 | This document contains: 8 | 9 | * How to develop MRI (development flow and support tools) 10 | * How to manage bug tickets 11 | * How to learn about MRI internals and how to get information about the community 12 | * What kind of unsolved issues are there in MRI? 13 | 14 | ## MRI development flow 15 | 16 | Ruby is OSS so that anyone can participate. 17 | 18 | "Ruby development" has two meanings: 19 | 20 | * Ruby language specification. 21 | * MRI implementation which is one of Ruby implementation. 22 | 23 | MRI is the reference implementation of the Ruby language, so that approved Ruby features are implemented on MRI. If we fix a bug in MRI, it means that we also change Ruby's specification. Therefore, when deciding whether to fix bugs, care must be taken to consider loss of compatibility. To be precise, several MRI-specific features exist, such as virtual machine instructions, so not all changes to MRI result in a change to Ruby's specification. 24 | 25 | ### Repository and Ruby committers 26 | 27 | Ruby's primary repository uses Git for source control . Some people have a right to modify this repository. We call them "Ruby committers". Now we have about 100 Ruby committers all over the world (but the number of active members is much smaller. If you become a Ruby committer, you can't throw away the title of "Ruby committer"). 28 | 29 | Committers can modify any of the source code of MRI. However, each committer has an area of responsibility. If a committer wants to modify another area, he/she is expected to ask and respect the advice of the responsible committers. For example, ko1 is a VM developer, so that if someone wants to change the VM drastically, he wants to be consulted before any changes are committed. 30 | 31 | There is no formal code review system. We may check committed patches and point out issues that we notice. We use `git bisect` (or similar techniques) to investigate problems (e.g. bug reports). If we have a big change, we ask other committers for a review. 32 | 33 | BTW, there is a mirror of the repository on GitHub . 34 | 35 | ## Ticket management 36 | 37 | We use [Redmine](https://bugs.ruby-lang.org/issues/), a bug tracker, to discuss specification changes, bug reports, and so on. A ticket is filed for each issue. 38 | Notifications of new tickets and any changes about tickets are delivered to the Ruby mailing lists. 39 | There are mailing lists in English (ruby-core) and Japanese (ruby-dev). 40 | 41 | 42 | Most important issues/proposals are filed in English. 43 | Tickets written in Japanese are also acceptable for easy and small issues. 44 | 45 | Tickets can be divided into two categories: "Feature request" and "Bug report". 46 | 47 | * Feature requests 48 | * Requests for additions or changes to the Ruby language itself. 49 | * incidentally, Redmine's URL contains the word "bugs" :) 50 | * Bug reports 51 | * Strange behaviour, performance issues and so on. Everything except for changes to the specification. 52 | 53 | When you submit a ticket, you will be asked to choose a description language (English or Japanese). The forwarded mailing list is chosen by this specified language (ruby-core or ruby-dev). 54 | 55 | Good bug reports should contain the following: 56 | 57 | * Summary 58 | * Reproducible code and environment information to reproduce it (the result of ruby -v, OS, compiler's version and so on) 59 | * Expected behavior. 60 | * Actual behaviour exhibited. 61 | * A patch which solves the issue (if possible). 62 | 63 | For details, please check (English) 64 | or Japanese version . 65 | 66 | Good feature request should contain the following: 67 | 68 | * Abstract (short summary) 69 | * Background (What is the issue you want to solve and why. Use-cases) 70 | * Proposal 71 | * Implementation (if possible) 72 | * Evaluation 73 | * Discussion 74 | * Summary 75 | 76 | For example, if you submit a "Feature request", then you will certainly be asked "what are your actual use cases?" 77 | This is an extreme example: if you proposed that "this feature should be changed because of inconsistency, but I don't use this feature any more", then your proposal will be rejected (fixing inconsistency is not more important than compatibility). 78 | 79 | For further information, please check . 80 | 81 | Issues or Pull Requests on GitHub are checked occasionally. In other words, sometimes they are ignored. 82 | I recommend you to make a new ticket on Redmine and link to your Issue or Pull Request on GitHub. 83 | Or, you can try to contact to a Ruby committer directly. 84 | 85 | ## CI on MRI 86 | 87 | MRI is a big and complex piece of software, so it is necessary to use automated testing for Quality Assurance (QA). We have about 450,000 lines of tests across some 5,000 files. 88 | 89 | We also need to prepare a variety of environments to run our tests. For example, well-known OSes such as Linux, macOS, Windows, as well as lesser known OSes \*BSD, Solaris and so on. 90 | Usually we use Intel x86/64 CPU or ARM processors, but there are other processors that we try to test on. 91 | The list of Ruby's supported platforms can be found at: . 92 | 93 | Because MRI is used in many enviroments, it is preferable to run tests on as wide a variety of environments as possible. 94 | It's common practice to use Continuous Integration (CI) to run automated tests. Ruby is no exception. 95 | 96 | In addition to using the popular Travis-CI service, we also run the site to collect the results of tests performed on a wider variety of environments. Typically, a CI system uses its own computing resources. However, our resources are limited. So, instead of preparing and managing the computers for the multitude of environments we need, we gather the results from tests run by volunteers in the community who run tests on their own computing resources. The tool [chkbuild](https://github.com/ruby/chkbuild) builds Ruby, runs tests, generates results, and performs a diff on the output so that we can determine which versions of Ruby have particular bugs. 97 | 98 | `chkbuild` is good test/CI framework but, for various reasons (for example, chkbuild downloads source code each time) it can be quite slow (typically tens of minutes). To overcome this limitation, we use another CI system that can reuse previous builds, and can build/test in parallel, reducing the time required for testing to the order of 2-3 minutes. This allows us to run our tests hundreds of times every day, which can be helpful for revealing hard-to-reproduce bugs (e.g. timing bugs). 99 | 100 | Ruby committers are expected to run tests on their own machines. 101 | If a Ruby committer accidentally adds a commit that doesn't pass the tests, the error should be detected on and hopefully committers will be alerted. 102 | 103 | ## Unresolved issues on MRI 104 | 105 | Ruby / MRI has many unresolved issues. The following issues are examples of them. 106 | 107 | * Specification 108 | * Ruby 2.6, ... 109 | * Ruby 3 110 | * JIT compilation (only for performance? drop backward compatibility?) 111 | * Static checking 112 | * Concurrent execution 113 | * Performance 114 | * Benchmarking framework and benchmarks 115 | * Performance improvements 116 | * Documentation 117 | * Bug fixes 118 | 119 | The following issues are internal problems I (ko1) want to fix: 120 | 121 | * Improve performance and quality of bytecode serializer. 122 | * Improve method dispatch mechanism. 123 | * Inlining code. 124 | * Increase generational GC supported objects (especially on `T_DATA`). 125 | * Provide CI service to tests gems on trunk. 126 | 127 | ## Information about Ruby development 128 | 129 | ### How to hack MRI internals 130 | 131 | Please refer to the [Bibliography](../bib.md). 132 | 133 | If you want to hack deeply into MRI, you need to know the C language. 134 | 135 | ### Communication channels 136 | 137 | * Ruby's Redmine: https://bugs.ruby-lang.org/projects/ruby/ 138 | * Ticket 139 | * Wiki 140 | * Mailing lists 141 | * https://www.ruby-lang.org/en/community/mailing-lists/ (En) https://www.ruby-lang.org/ja/community/mailing-lists/ (Ja) 142 | * ruby-core (English) 143 | * ruby-dev (Japanese) 144 | * Conferences, meetups 145 | * RubyConf and other international conferences 146 | * Japan domestic 147 | * RubyKaigi 148 | * RegionalRubyKaigi 149 | * Asakusa.rb, *.rb 150 | * Ruby developers meeting (Monthly meeting at Tokyo) 151 | * Contact individually 152 | * Twitter 153 | * @yukihiro_matz 154 | * ... 155 | * Gitter 156 | 157 | ## Important note 158 | 159 | This article introduces several rather pedantic "rules". However, what we interpreter developers value the most is "Hacking". 160 | If you contribute a great patch, we will support your contribution, even if you don't strictly abide by the above rules. 161 | 162 | Write code, and have fun! 163 | -------------------------------------------------------------------------------- /EN/2_mri_structure.md: -------------------------------------------------------------------------------- 1 | # (2) MRI source code structure 2 | 3 | ## About this document 4 | 5 | This document introduces the structure of the MRI source code. 6 | It also introduces the minimum required knowledge for hacking on MRI. 7 | 8 | There are the following topics: 9 | 10 | * Exercise: Clone the MRI source code. 11 | * Exercise: Build MRI and install built binaries. 12 | * Exercise: Execute Ruby programs with built Ruby. 13 | * MRI source code structures. 14 | * Exercise: The 1st hack. Change the version description. 15 | 16 | ## Assumptions 17 | 18 | The following commands assume an Unix-like environment, such as Linux, macOS, etc. If you're using a Windows environment, you will need to refer to other resources. 19 | 20 | NOTE: We provide an experimental docker image: `docker pull koichisasada/rhc`. Use `rubydev` account with `su rubydev` and enjoy hacking. 21 | 22 | We assume the use of the following directory structure: 23 | 24 | * `workdir/` 25 | * `ruby/` <- git cloned directory 26 | * `build/` <- build directory (`*.o` files and other compilation artifacts are stored here) 27 | * `install/` <- install directory (`workdir/install/bin/ruby` is the installed binary) 28 | 29 | The commands `git`, `ruby`, `autoconf`, `gcc` (or `clang`, etc), and `make` are required. 30 | Standard Ruby extensions (such as zlib, openssl, etc.) will be built if the libraries they depend on are available. 31 | 32 | If you use `apt-get` (or `apt`) for package management in your environment, then you can get all dependencies with the following command: 33 | 34 | ``` 35 | $ sudo apt-get install git ruby autoconf gcc make zlib1g-dev libffi-dev libreadline-dev libgdbm-dev libssl-dev libyaml-dev 36 | ``` 37 | 38 | If you would like to install other than `apt-get`, see for example [Home · rbenv/ruby\-build Wiki](https://github.com/rbenv/ruby-build/wiki) 39 | ## Exercise: Clone the MRI source code 40 | 41 | Use the following commands: 42 | 43 | 1. `$ mkdir workdir` 44 | 2. `$ cd workdir` 45 | 3. `$ git clone https://github.com/ruby/ruby.git` # The cloned source code will be available in `workdir/ruby` 46 | 47 | Due to limited network bandwidth at the venue, please clone the source code at home. 48 | 49 | ## Exercise: Build MRI and install built binaries 50 | 51 | 1. Check the required commands described above. 52 | 2. `$ cd workdir/` # Move to `workdir` 53 | 3. `$ cd ruby` # Move to `workdir/ruby` 54 | 4. `$ ./autogen.sh` 55 | 5. `$ cd ..` 56 | 6. `$ mkdir build` 57 | 7. `$ cd build` 58 | 8. `$ ../ruby/configure --prefix=$PWD/../install --enable-shared` 59 | * the `prefix` option specifies an install directory. You can specify the directory of your choice by supplying the full absolute path (in this case, `workdir/install` is specified). 60 | * users of `Homebrew` will need to add the following options `--with-openssl-dir="$(brew --prefix openssl)" --with-readline-dir="$(brew --prefix readline)" --disable-libedit` 61 | 9. `$ make -j` # Run build. `-j` specifies *parallel build*. 62 | 10. `$ make install` # Tip: for a faster install, instead run `make install-nodoc` to install ruby without rdoc. 63 | 11. `$ ../install/bin/ruby -v` will show the version description of your installed ruby command. 64 | 65 | NOTE: Running `make` with the `V=1` option (i.e. `make V=1 -j`, etc.) will output the full commands that are executed during the build. By default, `V=0` is specified and detailed output is suppressed. 66 | 67 | ## Exercise: Execute Ruby programs with the Ruby you built 68 | 69 | There are several ways to run Ruby scripts on the Ruby you built. 70 | 71 | The simplest way is to launch the installed Ruby directly, i.e. invoke `workdir/install/bin/ruby`. This is the same as invoking a pre-built Ruby binary. However, this means you will need to run `make install` every time you make a change to the Ruby source code, which can be rather time-consuming. 72 | 73 | Here we introduce a few convenient ways to launch our version of Ruby without installing. 74 | 75 | ### Use miniruby 76 | 77 | After building Ruby, the `miniruby` command is available in `workdir/build`. `miniruby` is a limited version of Ruby for building Ruby itself. The limitations of `miniruby`, however, are minimal: it is unable to load extension libraries and limited encodings are available. You can try most of Ruby's syntax using `miniruby`. 78 | 79 | `miniruby` is built during the first phase of the Ruby build process. Thus, `miniruby` is useful for a early verification of modifications made to MRI. 80 | 81 | The following development loop is very efficient: 82 | 83 | 1. Modify MRI 84 | 2. Run `make miniruby` to build `miniruby` (this is faster than `make` or `make all`) 85 | 3. Run a Ruby script in `miniruby` to test the correctness of your modifications. 86 | 87 | To support this development loop, we provide a `make run` rule in the Makefile. This rule does the following: 88 | 89 | 1. Build `miniruby` 90 | 2. Run `workdir/ruby/test.rb` (`test.rb` in source directory) with the built miniruby. 91 | 92 | Using `make run`, you can test your modifications with the following steps. 93 | 94 | 1. Write a test for your modifications in `ruby/test.rb`. Note that you can't require gems or extension libraries in `test.rb`. 95 | 2. Modify MRI. 96 | 3. Invoke `$ make run` in the build directory 97 | 98 | ### Use fully-featured Ruby (not miniruby) 99 | 100 | If you want to run the "normal" Ruby, which can load extension libraries, you can use `make runruby`. This allows you to run Ruby without the `make install` step, which should save you some time. 101 | 102 | 1. Write in `ruby/test.rb` what you want to check. 103 | 2. Modify MRI. 104 | 3. Invoke `$ make runruby` in the build directory. 105 | 106 | ### Debug with gdb 107 | 108 | NOTE: Running `gdb` on macOS can be quite difficult. The following commands assume a Linux environment. 109 | 110 | When modifying the MRI source code, you can easily introduce critical problems that result in a SEGV. To debug such problems, we provide Makefile rules to support debugging with gdb. Of course, you can also debug with break points. 111 | 112 | 1. Write in `ruby/test.rb` what you want to check. Note that you can't use gems or extension libraries in `test.rb`. 113 | 3. Invoke `$ make gdb` to run miniruby with gdb. If there are no problems, gdb finishes silently. 114 | 115 | `make gdb` uses `./miniruby`. If you want to debug with `./ruby`, use `make gdb-ruby` instead. 116 | 117 | If you want to use break points, modify the `run.gdb` file generated by the `make gdb` command. 118 | For example, the `b func_name` gdb command inserts a break point at the beginning of the `func_name` function. 119 | 120 | There is a similar rule for [lldb](https://lldb.llvm.org/), `$ make lldb`, for using lldb instead of gdb (but Koichi doesn't know the details because he doesn't use lldb). It may be useful if you use macOS. 121 | 122 | ### Run Ruby tests 123 | 124 | 1. `$ make btest` # run bootstrap tests in `ruby/bootstraptest/` 125 | 2. `$ make test-all` # run test-unit tests in `ruby/test/` 126 | 3. `$ make test-spec` # run tests provided in `ruby/spec` 127 | 128 | These three tests have different purposes and characteristics. 129 | 130 | ## MRI source code structures 131 | 132 | ### Interpreter 133 | 134 | At a glance, the following directory structure you can observe: 135 | 136 | * `ruby/*.c` MRI core files 137 | * VM cores 138 | * VM 139 | * `vm*.[ch]`: VM implementation 140 | * `vm_core.h`: definitions of VM data structure 141 | * `insns.def`: definitions of VM instructions 142 | * `compile.c, iseq.[ch]`: instruction sequence (bytecode) 143 | * `gc.c`: GC and memory management 144 | * `thread*.[ch]`: thread management 145 | * `variable.c`: variable management 146 | * `dln*.c`: dll management for extension libraries 147 | * `main.c`, `ruby.c`: the entry point of MRI 148 | * `st.c`: Hash algorithm implementation (see https://blog.heroku.com/ruby-2-4-features-hashes-integers-rounding) 149 | * Embedded classes 150 | * `string.c`: String class 151 | * `array.c`: Array class 152 | * ... (file names show class names, such as time.c for Time class) 153 | * `ruby/*.h`: internal definitions. C-extension libraries can't use them. 154 | * `ruby/include/ruby/*`: external definitions. C-extension libraries can use them. 155 | * `ruby/enc/`: encoding information. 156 | * `ruby/defs/`: various definitions. 157 | * `ruby/tool/`: tools to build MRI. 158 | * `ruby/missing/`: implementations for features that are missing in some OSes 159 | * `ruby/cygwin/`, `ruby/nacl/`, `ruby/win32`, ...: OS/system dependent code. 160 | 161 | ### Libraries 162 | 163 | There are two kinds of libraries. 164 | 165 | * `ruby/lib/`: Standard libraries written in Ruby. 166 | * `ruby/ext/`: Bundled extension libraries written in C. 167 | 168 | ### Tests 169 | 170 | * `ruby/basictest/`: place of old test 171 | * `ruby/bootstraptest/`: bootstrap test 172 | * `ruby/test/`: tests written in test-unit notation 173 | * `ruby/spec/`: tests written in RSpec notation 174 | 175 | ### misc 176 | 177 | * `ruby/doc/`, `ruby/man/`: documentation 178 | 179 | ## Ruby's build process 180 | 181 | the Ruby build process is composed of several phases involving source code generation and so on. Several tools are written in Ruby, so the Ruby build process requires the Ruby interpreter. Release tarballs contain generated source code so that installing Ruby with a release tarball does not require the Ruby interpreter (and other development tools such as autoconf). 182 | 183 | If you want to build MRI with source code fetched by Subversion or Git repository, you need a Ruby interpreter. 184 | 185 | The following steps describe the build and install process: 186 | 187 | 1. Build miniruby 188 | 1. parse.y -> parse.c: Compile syntax rules into C code with lrama 189 | 2. insns.def -> vm.inc: Compile VM instructions into C code with ruby (`BASERUBY`) 190 | 3. `*.c` -> `*.o` (`*.obj` on Windows): Compile C code into object files. 191 | 4. link object files into miniruby 192 | 2. Build encodings 193 | 1. translate enc/... to appropriate C code with `miniruby` 194 | 2. compile C code 195 | 3. Build C-extension libraries 196 | 1. Generate `Makefile` from `extconf.rb` with `mkmf.rb` and `miniruby` 197 | 2. Run `make` using generated `Makefile`. 198 | 4. Build `ruby` command 199 | 5. Generate documentation (`rdoc`, `ri`) 200 | 6. Install MRI (to the install directory specified by the `configure --prefix` option) 201 | 202 | There are actually many more steps in the process. It is difficult, however, to comprehensively list all the steps (even I don't know all of them!), so the above is an abbreviated sequence of steps. If you are curious, you can see all the rules in `common.mk` and related files. 203 | 204 | ## Exercise: the 1st hack. Change the version description 205 | 206 | Let's start modifying MRI. We assume that all source code is placed at `workdir/ruby/`. 207 | 208 | For your first exercise, let's modify the version description which is displayed with `ruby -v` (or `./miniruby -v`) to display it as your own Ruby (for example, show a version description with your name included). 209 | 210 | 1. Open `version.c` in your editor. 211 | 2. Briefly skim over the entirety of `version.c`. 212 | 3. The function `ruby_show_version()` seems like what we're looking for 213 | 4. `fflush()` is a C function that flushes the output buffer, so we can guess that adding some printing code just before `fflush()` call could work. 214 | 5. Add the line `printf("...\n");` (Replace `...` with a string of your choice) 215 | 6. `$ make miniruby` and build (don't forget to move to the build directory) 216 | 7. run `$ ./miniruby -v` and check the result. 217 | 8. `$ make install` and install build ruby. 218 | 9. run `$ ../install/bin/ruby -v` and check the result with the installed ruby. 219 | 220 | Finally, instead of just inserting a `printf(...)` statement, try replacing the entire `ruby ...` description with something else (such as `perl ...` and so on) would be interesting ;p 221 | 222 | -------------------------------------------------------------------------------- /EN/3_practice.md: -------------------------------------------------------------------------------- 1 | # (3) Exercise: Add methods to Ruby 2 | 3 | ## About this document 4 | 5 | Let's try adding some new methods to MRI. This document shows you how to add a new method, step by step. Please follow along in your own environment. 6 | 7 | ## `Array#second` 8 | 9 | Let's add a `Array#second` method. `Array#first` returns the first element of an Array. 10 | `Array#second` will return the second element of an Array. 11 | 12 | Here is a definition in Ruby: 13 | 14 | ```ruby 15 | # specification written in Ruby 16 | class Array 17 | def second 18 | self[1] 19 | end 20 | end 21 | ``` 22 | 23 | Steps: 24 | 25 | 1. Open `array.c` in your editor. 26 | 2. Add a `ary_second()` function definition into `array.c`. A good place to add it is before `Init_Array()`. 27 | 3. Add the statement `rb_define_method(rb_cArray, "second", ary_second, 0);` to the body of the `Init_Array()` function. 28 | 4. Write some sample code to try your new method in `ruby/test.rb`, then build and run with `make run`. 29 | 5. Add a test in `ruby/test/ruby/test_array.rb`. These tests are written in the minitest format. 30 | 6. `$ make test-all` will run the test code you wrote. However, it runs a tremendous number of ruby tests, so you may want to run only the Array-related tests. 31 | * `$ make test-all TESTS='ruby/test_array.rb'` will test only `ruby/test/ruby/test_array.rb`. 32 | * `$ make test-all TESTS='-j8'` will run in parallel with 8 processes. 33 | 7. Add rdoc documentation of `Array#second` by referencing the documentation of other methods in `array.c`. 34 | 35 | 36 | One possible implementation of `ary_second()` is shown below. Line numbers may differ because `array.c` is likely to have changed since this document was written. 37 | 38 | ```diff 39 | diff --git a/array.c b/array.c 40 | index bd24216af3..79c1c1d334 100644 41 | --- a/array.c 42 | +++ b/array.c 43 | @@ -6131,6 +6131,12 @@ rb_ary_sum(int argc, VALUE *argv, VALUE ary) 44 | * 45 | */ 46 | 47 | +static VALUE 48 | +ary_second(VALUE self) 49 | +{ 50 | + return rb_ary_entry(self, 1); 51 | +} 52 | + 53 | void 54 | Init_Array(void) 55 | { 56 | @@ -6251,6 +6257,8 @@ Init_Array(void) 57 | rb_define_method(rb_cArray, "dig", rb_ary_dig, -1); 58 | rb_define_method(rb_cArray, "sum", rb_ary_sum, -1); 59 | 60 | + rb_define_method(rb_cArray, "second", ary_second, 0); 61 | + 62 | id_cmp = rb_intern("<=>"); 63 | id_random = rb_intern("random"); 64 | id_div = rb_intern("div"); 65 | ``` 66 | 67 | A brief explanation follows: 68 | 69 | * `ary_second()` is the implementation of the method. 70 | * `VALUE` represents a type of Ruby object in C, and `self` is the method's receiver (i.e. for `ary.second`, the receiver is `ary`). All Ruby methods return a Ruby object, so the type of the return value should also be `VALUE`. 71 | * `rb_ary_entry(self, n)` does the same thing as `self[n]` in Ruby. Therefore, `rb_ary_entry(self, 1)` returns the second element (note: C uses 0-based index). 72 | * The function `Init_Array` is invoked by the interpreter at launch-time. 73 | * The statement `rb_define_method(rb_cArray, "second", ary_second, 0);` defines the `second` method on the `Array` class. 74 | * `rb_cArray` points to the `Array` class object. The `rb_` prefix is used to indicate it is something Ruby-related, and the `c` means "Class". Therefore, we can infer that `rb_cArray` is Ruby's Array class object. BTW, the module object prefix is `m` (e.g. `rb_mEnumerable` == `Enumerable` module object) and the error class prefix is `e` (e.g. `rb_eArgError` == `ArgumentError` object). 75 | * `rb_define_method` is a function that defines instance methods. 76 | * This statement can be read as: "Define an instance method `second` on `rb_cArray`. When `Array#second` is called, then call the `ary_second` C function. This method accepts 0 arguments". 77 | 78 | ## `String#palindrome?` 79 | 80 | Let's define a method `String#palindrome?` that checks if the string is a palindrome or not. 81 | 82 | The following code is a sample Ruby implementation of `String#palindrome?` along with some tests. 83 | 84 | 85 | ```ruby 86 | class String 87 | def palindrome? 88 | chars = self.gsub(/[^A-z0-9\p{hiragana}\p{katakana}]/, '').downcase 89 | # p chars 90 | !chars.empty? && chars == chars.reverse 91 | end 92 | end 93 | 94 | # Small sample program 95 | # Sample palindrome from https://en.wikipedia.org/wiki/Palindrome 96 | [# OK 97 | "Sator Arepo Tenet Opera Rotas", 98 | "A man, a plan, a canal - Panama!", 99 | "Madam, I'm Adam", 100 | "NisiOisiN", 101 | "わかみかものとかなかとのもかみかわ", 102 | "アニマルマニア", 103 | # NG 104 | "", 105 | "ab", 106 | ].each{|str| 107 | p [str, str.palindrome?] 108 | } 109 | ``` 110 | 111 | Translate the above Ruby code into C code. 112 | Please recall the procedure for implementing `Array#second`, and use this procedure to implement `String#palindrome?` in MRI. 113 | 114 | Below is one possible solution for implementing `String#palindrome?`. 115 | 116 | ```diff 117 | diff --git a/string.c b/string.c 118 | index c140148778..0f170bd20b 100644 119 | --- a/string.c 120 | +++ b/string.c 121 | @@ -10062,6 +10062,18 @@ rb_to_symbol(VALUE name) 122 | return rb_str_intern(name); 123 | } 124 | 125 | +static VALUE 126 | +str_palindrome_p(VALUE self) 127 | +{ 128 | + const char *pat = "[^A-z0-9\\p{hiragana}\\p{katakana}]"; 129 | + VALUE argv[2] = {rb_reg_regcomp(rb_utf8_str_new_cstr(pat)), 130 | + rb_str_new_cstr("")}; 131 | + VALUE filtered_str = rb_str_downcase(0, NULL, str_gsub(2, argv, self, FALSE)); 132 | + return rb_str_empty(filtered_str) ? Qfalse : 133 | + rb_str_equal(filtered_str, rb_str_reverse(filtered_str)); 134 | + 135 | +} 136 | + 137 | /* 138 | * A String object holds and manipulates an arbitrary sequence of 139 | * bytes, typically representing characters. String objects may be created 140 | @@ -10223,6 +10235,8 @@ Init_String(void) 141 | rb_define_method(rb_cString, "valid_encoding?", rb_str_valid_encoding_p, 0); 142 | rb_define_method(rb_cString, "ascii_only?", rb_str_is_ascii_only_p, 0); 143 | 144 | + rb_define_method(rb_cString, "palindrome?", str_palindrome_p, 0); 145 | + 146 | rb_fs = Qnil; 147 | rb_define_hooked_variable("$;", &rb_fs, 0, rb_fs_setter); 148 | rb_define_hooked_variable("$-F", &rb_fs, 0, rb_fs_setter); 149 | ``` 150 | 151 | Explanation: 152 | 153 | * The suffix `_p` indicates a predicate method that returns true or false. 154 | * `rb_reg_regcomp(pat)` compiles the `pat` C string into a RegExp object. 155 | * `rb_str_new_cstr("")` generates an empty Ruby string. 156 | * `str_gsub()` does the same replacement as `String#gsub`. 157 | * `rb_str_downcase()` does the same replacement as `String#downcase`. 158 | * `rb_str_empty()` does the same checking as `String#empty?`. 159 | * `rb_str_reverse()` does the same reordering as `String#reverse`. 160 | * `rb_str_equal()` does the same comparison as `String#==`. 161 | 162 | Hopefully, you can see how the C implementation corresponds to the Ruby implementation. 163 | 164 | ## `Integer#add(n)` 165 | 166 | Add a method `Integer#add(n)` which returns the result when `n` is added. 167 | 168 | Ruby example definition: 169 | 170 | ```ruby 171 | class Integer 172 | def add n 173 | self + n 174 | end 175 | end 176 | 177 | p 1.add(3) #=> 4 178 | p 1.add(4.5) #=> 5.5 179 | ``` 180 | 181 | Below is one possible solution for implementing `Integer#add`: 182 | 183 | ```diff 184 | Index: numeric.c 185 | =================================================================== 186 | --- numeric.c (Revision 59647) 187 | +++ numeric.c (Working copy) 188 | @@ -5238,6 +5238,12 @@ 189 | } 190 | } 191 | 192 | +static VALUE 193 | +int_add(VALUE self, VALUE n) 194 | +{ 195 | + return rb_int_plus(self, n); 196 | +} 197 | + 198 | /* 199 | * Document-class: ZeroDivisionError 200 | * 201 | @@ -5449,6 +5455,8 @@ 202 | rb_define_method(rb_cInteger, "bit_length", rb_int_bit_length, 0); 203 | rb_define_method(rb_cInteger, "digits", rb_int_digits, -1); 204 | 205 | + rb_define_method(rb_cInteger, "add", int_add, 1); 206 | + 207 | #ifndef RUBY_INTEGER_UNIFICATION 208 | rb_cFixnum = rb_cInteger; 209 | #endif 210 | ``` 211 | 212 | This method should accept 1 argument, so the last argument of `rb_define_method()` is `1` and the definition of `int_add()` accepts one parameter with `VALUE n`. 213 | 214 | The actual addition is performed in `rb_int_plus()` so we don't need to write any complex code. 215 | 216 | Let's try to modify this code to use our own implementation of addition if a given parameter is a `Fixnum` (numbers represented by `Fixnum` are small and can be easily translated both to and from a C `int`). 217 | 218 | Note that Ruby 2.4 removed the `Fixnum` and `Bignum` classes. They are now unified into a single `Integer` class. However, MRI still uses Fixnum and Bignum as internal data structures for performance reasons. For example, `FIXNUM_P(bignum)` returns false. 219 | 220 | ```diff 221 | Index: numeric.c 222 | =================================================================== 223 | --- numeric.c (Revision 59647) 224 | +++ numeric.c (Working copy) 225 | @@ -5238,6 +5238,22 @@ 226 | } 227 | } 228 | 229 | +static VALUE 230 | +int_add(VALUE self, VALUE n) 231 | +{ 232 | + if (FIXNUM_P(self) && FIXNUM_P(n)) { 233 | + /* c = a + b */ 234 | + int a = FIX2INT(self); 235 | + int b = FIX2INT(n); 236 | + int c = a + b; 237 | + VALUE result = INT2NUM(c); 238 | + return result; 239 | + } 240 | + else { 241 | + return rb_int_plus(self, n); 242 | + } 243 | +} 244 | + 245 | /* 246 | * Document-class: ZeroDivisionError 247 | * 248 | ``` 249 | 250 | `FIXNUM_P(self) && FIXNUM_P(n)` checks to see if `self` and `n` are both `Fixnum`. 251 | If they are `Fixnum`, they are converted into C `int` values with `FIX2INT()`, and then addition is performed using C `int` values. The result is then converted from a C integer value back into Ruby's Integer value with `FIX2NUM()`. 252 | 253 | Note: This definition has a bug. See the next document. 254 | 255 | ## `Time#day_before(n=1)` 256 | 257 | Add a method to the Time class to return the time from `n` days ago (with a default value for `n` of 1). 258 | 259 | Here is an example definition in Ruby. It returns a result with time reduced by the number of seconds in 24 hours * `n`. This is not a complete solution because it will occasionally be incorrect (e.g. when there are leap seconds, daylight saving time, etc). We'll ignore these problems here because this is simply an illustrative example. 260 | 261 | ```ruby 262 | class Time 263 | def day_before n = 1 264 | Time.at(self.to_i - (24 * 60 * 60 * n)) 265 | end 266 | end 267 | 268 | p Time.now #=> 2017-08-24 14:48:44 +0900 269 | p Time.now.day_before #=> 2017-08-23 14:48:44 +0900 270 | p Time.now.day_before(3) #=> 2017-08-21 14:48:44 +0900 271 | ``` 272 | 273 | Here is a definition written in C: 274 | 275 | ```diff 276 | Index: time.c 277 | =================================================================== 278 | --- time.c (Revision 59647) 279 | +++ time.c (Working copy) 280 | @@ -4717,6 +4717,22 @@ 281 | return time; 282 | } 283 | 284 | +static VALUE 285 | +time_day_before(int argc, VALUE *argv, VALUE self) 286 | +{ 287 | + VALUE nth; 288 | + int n, sec, day_before_sec; 289 | + 290 | + rb_scan_args(argc, argv, "01", &nth); 291 | + if (nth == Qnil) nth = INT2FIX(1); 292 | + n = NUM2INT(nth); 293 | + 294 | + sec = NUM2INT(time_to_i(self)); 295 | + day_before_sec = sec - (60 * 60 * 24 * n); 296 | + 297 | + return rb_funcall(rb_cTime, rb_intern("at"), 1, INT2NUM(day_before_sec)); 298 | +} 299 | + 300 | /* 301 | * Time is an abstraction of dates and times. Time is stored internally as 302 | * the number of seconds with fraction since the _Epoch_, January 1, 1970 303 | @@ -4896,6 +4912,8 @@ 304 | 305 | rb_define_method(rb_cTime, "strftime", time_strftime, 1); 306 | 307 | + rb_define_method(rb_cTime, "day_before", time_day_before, -1); 308 | + 309 | /* methods for marshaling */ 310 | rb_define_private_method(rb_cTime, "_dump", time_dump, -1); 311 | rb_define_private_method(rb_singleton_class(rb_cTime), "_load", time_load, 1); 312 | ``` 313 | 314 | Explanation: 315 | 316 | * To define a method that accepts optional arguments, `-1` is specified as the last argument of `rb_define_method()`. This means this function does not know how many methods it will receive until it is called. 317 | * The function `time_day_before(int argc, VALUE *argv, VALUE self)` is the definition of the method. `argc` is the number of arguments given when it was called, and `argv` is a pointer to a C array of size `argc` objects of type `VALUE`. 318 | * `rb_scan_args()` is called to check the method arguments. `"01"` means that the number of required parameters is 0 and optional parameters is 1. This means that this method accepts 0 or 1 parameters. If 1 argument is passed, then it is stored in `nth`. If there are no arguments, then `nth` will contain `Qnil` (the C representation of Ruby's `nil`). 319 | * To call Ruby's method `Time.at()`, `rb_funcall(recv, mid, argc, ...)` is used. 320 | * The first argument is the method's receiver (`recv` in `recv.mid(...)`). In the case of `Time.at`, the receiver is `Time`. 321 | * The name of the method called by `rb_funcall` is specified by its `ID`, a Symbol. To generate the `ID` in C, we use `rb_intern("...")`. An `ID` is a unique value for a C string in a Ruby process. In Ruby it is called a Symbol, and in Java it is an `intern`ed string. 322 | * We want to call `Time.at` with 1 argument, so we specify `1` and pass the actual argument `INT2NUM(day_before_sec)` as the final parameter. 323 | 324 | There are a number of problems with this implementation. Try comparing it with Ruby's actual implementation and see if you can understand the differences. 325 | 326 | ## Extension libraries 327 | 328 | C extension libraries allow us to extend the functionality of MRI without modifying MRI itself. 329 | We can make C extension libraries using almost the same process as we use to hack on MRI internals. 330 | 331 | For example, let's make an extension library to add the `Array#second` method instead of modifying MRI itself. 332 | 333 | Steps to make an `.so` extension library file (or `.bundle` in MacOS): 334 | 335 | 1. Make a directory named `array_second/`. 336 | 2. Make a file named `array_second/extconf.rb`. 337 | * In this file, `require 'mkmf'` to enable the mkmf library. We can use mkmf to generate a Makefile and perform any configuration needed for the library. 338 | * After adding configuration (in this case, we don't have any configuration), call `create_makefile('array_second')`. This method creates a Makefile. 339 | 3. Make a file named `array_second.c`. 340 | * Add the line `#include ` to the top of the file to enable the MRI C-API. 341 | * This file should contain (1) method body and (2) code that adds the method into `Array`. 342 | * (1) is the same as `ary_second()` written earlier. 343 | * (2) should be the `Init_array_second()` function, which calls `rb_define_method()`. The name `Init_array_second` is inferred from the argument passed to `create_makefile` in `extconf.rb`. 344 | 4. Run `$ ruby extconf.rb` to generate the Makefile. 345 | 5. Run `$ make` to build `array_second.so` (or `array_second.bundle` in MacOS). You will then be able to `require` this file. Example: `$ ruby -r ./array_second -e 'p [1, 2].second'` will show `2`. In MacOS: `$ ruby -r ./array_second/array_second.bundle -e 'p [1, 2].second'` 346 | 6. `$ make install` installs .so file into install directory. 347 | 348 | A sample `array_second` directory is available in this repository for you to reference. 349 | 350 | Except for the `extconf.rb` and the installation steps, the Ruby extensions are defined in exactly the same way as Ruby's embedded methods and classes. 351 | 352 | To distribute extension libraries, the minimum requirement is to create a package with the files made in step 2 and 3. It's probably more convenient for your users if you package your extension as a RubyGem. 353 | 354 | ## Tips: Debugging 355 | 356 | Please refer to https://docs.ruby-lang.org/en/2.5.0/extension_rdoc.html for a detailed explanation of writing Ruby extensions. 357 | 358 | Browse through the MRI source code to find methods which perform functions that are similar to what you want to add. 359 | 360 | When you write Ruby programs, you probably already use `p(obj)` to inspect objects. In C, you can use `rb_p(obj)` to perform the equivalent function. 361 | 362 | If you can use gdb, breakpoints will help you. 363 | If you add the line `#include "vm_debug.h"`, you will be able to use the `bp()` macro to set a breakpoint. `make gdb` will stop on this macro, similar to when you use `binding.pry` or `binding.irb`. 364 | 365 | gdb allows you to use `p expr` to show the value of `expr` (for example, you can see a value of a variable `foo` with `p foo`). The type `VALUE` is just an integer value in C, so it may be difficult to determine what kind of Object it is and what data it represents. The special command `rp` for gdb (defined in `ruby/.gdbinit`) is provided to give a human-readable representation for VALUE-type data. 366 | 367 | 368 | ## Advanced Exercises 369 | 370 | Try solving the following challenges. `grep` will help you to find similar implementations in the source code of MRI. 371 | 372 | * Implement `Integer#sub(n)` which subtracts n from an integer value. 373 | * `Array#second` returns `nil` if there is no second element. This is because `rb_ary_entry()` returns `nil` when the specified index exceeds the size of an array. Instead, raise an exception when there is no second element. Use `rb_raise()` function to raise an error. 374 | * `String#palindrome?` is an inefficient implementation. Identify which part is inefficient and consider how to resolve the inefficiency. Try implementing a solution to improve its performance. 375 | * `Time#day_before` is an awkward name. Think of a better method name. 376 | * Let's play a trick on MRI. For example, change the behaviour of `Integer#+` to perform subtraction instead. This hack will break your ruby build, so make a new git branch and experiment to see what happens. 377 | * Use your imagination and try to add an interesting new method. 378 | 379 | The following topics are discussed in the next chapter, but try to explore them yourself before proceeding: 380 | 381 | * I described that `Integer#add(n)` had a bug. 382 | * Write a test which fails due to this bug. 383 | * Solve the issue and make the test pass. 384 | * What is a problem with our implementation of `Time#day_before`? There is a similar problem in `Integer#add(n)`. 385 | -------------------------------------------------------------------------------- /EN/5_performance.md: -------------------------------------------------------------------------------- 1 | 2 | # (5) Performance improvements 3 | 4 | ## About this article 5 | 6 | This document will present several ideas and tips for performance improvements that can be used in MRI. 7 | 8 | ## What are performance improvements 9 | 10 | When hearing "performance improvement" we most of the time think of increasing the speed of a program, which means usually shorting the execution time, don't you think? 11 | Of course, speed is usually the most important performance factor, but there are others, depending on the given use-case. 12 | 13 | There are many performance indicators, I'm writing down those that currently come to mind, but there are probably more. 14 | 15 | * time related indicators 16 | * execution time (throughput) 17 | * real-time properties (latency) 18 | * start-up time 19 | 20 | * resource related indicators 21 | * memory consumption 22 | * count of life objects 23 | * real memory usage to virtual memory usage gap size (e.g. TLB misses) 24 | * copy on write affinity of process forking 25 | * CPU usage (for the same execution time, less usage is better) 26 | * CPU utilisation probability (does performance improve when adding more CPU power/cores) 27 | * file descriptors needed for I/O 28 | * disc space usage(binary size etc.) 29 | 30 | Some of these indicators depend on each other, in correlation or in inverse correlation. 31 | 32 | Examples of correlations: 33 | 34 | * by dropping memory consumption, cache misses also drop, which improves throughput. 35 | * by reducing the amount of created objects, throughput increases 36 | 37 | Examples of inverse correlations: 38 | 39 | * to improve the real-time rate, a real-time GC is needed → throughput declines 40 | * having special code paths for often occurring values might increase memory or disc usage 41 | 42 | It's important to be aware which of these indicators need to improve, what influence that will have on the other indicators and if those changes are still in acceptable limits. 43 | Furthermore, as computers are constantly evolving, it might be important to reevaluate one's choices that were appropriate in the past. 44 | 45 | For instance, a long time ago, memory was the main restriction, but the amount of memory we can use has constantly grown over time. But this time we might want to use cloud computing, where memory usage equals money, so memory consumption might be relevant yet again. 46 | 47 | It's important to decide what needs improvement right now. 48 | 49 | ## Performance improvement mindset 50 | 51 | ### Measuring first 52 | 53 | I know this is always said, but it is absolutely important to properly measure the current performance before trying to implement any improvements. 54 | 55 | In case of throughput improvement, it is vital to find the correct places inside the program that are slow. 56 | To find out which parts of MRI need improvement, you can use stackprof and the like for ruby-level analysis and Linux' perf command to analyze the details. 57 | 58 | (By the way, these tools don't always tell the truth. In the end, tools produced by humans can't be trusted 100% (especially because it's a difficult and hard to debug field). Sometimes when you have strange outputs you should doubt the tools. To clear your doubts, it's important to understand how these tools work and produce their results.) 59 | 60 | ### Algorithms and data structures 61 | 62 | It's probably common sense that for performance optimization, exchanging the underlying algorithms can have the biggest impact. 63 | 64 | For example, let's consider the problem of optimizing the computation of the n-th Fibonacci number. When sticking close to the mathematical definition, we write some code like this: 65 | 66 | ``` 67 | def fib(n) 68 | if n < 2 69 | 1 70 | else 71 | fib(n-2) + fib(n-1) 72 | end 73 | end 74 | 75 | n = Integer(ARGV.shift || 35) 76 | puts "fib(#{n}) = #{fib(n)}" 77 | ``` 78 | A general improvement would be to develop a JIT compiler (Just-in-Time compiler, compiling at run time), which would ease the workload by substituting the recursive method calls with jump instructions. But we would get a performance improvement of several times to hundred times at best (well, you could argue that that much of an improvement would be fine on its on). 79 | 80 | Of course, there's a faster algorithm to compute the Fibonacci numbers. 81 | 82 | ``` 83 | def fib n 84 | a, b = 1, 1 85 | n.times{ 86 | a, b = b, a+b 87 | } 88 | a 89 | end 90 | 91 | n = Integer(ARGV.shift || 35) 92 | puts "fib(#{n}) = #{fib(n)}" 93 | ``` 94 | For algorithm classification, we replaced an O(k^n) algorithm with an O(n) one. Depending on n, this algorithm can be much faster than only hundred times compared to the original algorithm. 95 | (By the way, there is an algorithm that computes Fibonacci numbers with O(log n).) 96 | 97 | In that sense, it is very important to evaluate whether it is possible to replace the given algorithm. 98 | 99 | That is pretty base-level performance improvement knowledge, let me just mention a little bit more. 100 | 101 | For instance if we have an algorithm to improve, but we know that the given n is small enough, there might be another algorithm that is more effective in this case (like linear search vs binary search). 102 | Also it can be better to use a simpler and slower algorithm instead of a complex and fast one, because we get it to work earlier and with less bugs. It might not be so fast, but who would want to use an algorithm with (potentially more) bugs inside. 103 | Also, consider we could write a program that runs for 3 seconds and needs 5 minutes development time, or we could write an improved version that runs in only 0.3 seconds, but which would need an hour development time, and let's say we need to run it only once. I guess you'd agree we'd choose the former. 104 | 105 | Depending on the problem at hand, we have to be flexible with our methods. 106 | 107 | ## Adjusting MRI 108 | 109 | These are places of MRI that can be improved 110 | 111 | * Built-in class methods 112 | * See if we can improve their algorithms (not only for class methods though). Sometimes we are using ineffective algorithms out of convenience (which are simpler and have less bugs though). 113 | * Consider specializations for common values. We should optimize for the most used cases. For example, in `Array#[]` we have usually function arguments with small integers (index). 114 | * Standard Extension Library(`lib/`) 115 | * In Ruby, reducing the allocation of unneeded objects can be effective. is should be useful. 116 | * Object management, garbage collector 117 | * Evaluating the GC algorithm. Yet another time? 118 | * Inspecting the object memory layout. 119 | * VM 120 | * Review the instruction set. 121 | * Consider JIT compilation. 122 | * Consider optimizations of the compiler such as instruction replacement, inlining, etc. 123 | 124 | And I think there are yet many other places. 125 | 126 | (to be continued, maybe) 127 | -------------------------------------------------------------------------------- /EN/task_ideas.md: -------------------------------------------------------------------------------- 1 | # Task ideas 2 | 3 | Here are some ideas for the Ruby Hack Challenge. 4 | Feel free to choose any of them or try your own hacks. 5 | 6 | Before your hack, please file your hack topic as a Github issue. 7 | This issue should contain: 8 | 9 | * Hack topic summary 10 | * Your name (or names. Group work is also welcome) 11 | 12 | After your hack, close this ticket with your achievement summary. 13 | 14 | ## Run tests on your specific environment 15 | 16 | Ruby has test suites (explained in lecture materials). 17 | We run tests on some environments (you can check https://rubyci.org) periodically, but not all environments. 18 | 19 | Please try and run the test suite on your environment, and if you have any trouble: 20 | 21 | (1) write a report about it. 22 | (2) fix the issues. 23 | 24 | ## Libraries 25 | 26 | `ruby/lib/` contains libraries written in Ruby. 27 | You can modify them without any C knowledge. 28 | Find out your issue and try to fix them. 29 | 30 | ## Documentation 31 | 32 | Ruby's documentation is written in RDoc. 33 | As you can see, RDoc documentation is written in .rb and .c files. 34 | 35 | Please add information such as examples and so on. 36 | 37 | ## Check unresolved issues 38 | 39 | We file all issues on Redmine and there are many unresolved tickets. 40 | 41 | (1) Bug tickets 42 | 43 | We need to fix many bugs but there we don't have enough resources. 44 | Please help us. 45 | 46 | (1-1) Check if the bug is reproducible or not 47 | 48 | There are resolved tickets but not closed because of many reasons (for example, forgot to close the issue, resolved accidentally with other issues, and so on). 49 | 50 | Please check the bug is reproducible on new versions and report them. 51 | It should be very important information. 52 | 53 | (1-2) Make small reproducible code 54 | 55 | If a bug report contains a long explanation and reproducible process (for example, install xxx and yyy, run it with zzz and wait for several minutes...), it is very difficult to debug it. 56 | 57 | Please try and find out how to reproduce with a small example. 58 | Reproducible code with `make run` is excellent. 59 | 60 | (1-3) Try to fix a bug. 61 | 62 | Of course, a patch is very welcome. 63 | 64 | (1') report new bug ticket you found 65 | 66 | And do (1-1) to (1-3). 67 | 68 | (2) Feature tickets 69 | 70 | There are several feature-request tickets. 71 | 72 | (2-1) Consider the feature request 73 | 74 | Please consider new features and report your thoughts. 75 | Concrete use-cases based on your experiences will help us. 76 | Conversely, thoughts on just your feelings are not usually enough. 77 | Fact is preferable (for example, consistency with other features, other languages and so on). 78 | 79 | (2-2) Try to implement a feature request 80 | 81 | Some tickets contains patches to implement it. Try it and report how you like it or not. 82 | 83 | (2-3) Implement feature requests 84 | 85 | Some tickets do not contain patches. 86 | Please implement them. 87 | 88 | For example, ko1's ticket is very easy to implement. 89 | 90 | (2') Make your own feature request 91 | 92 | And do (2-1) to (2-3). 93 | 94 | For example, there are many methods in ActiveSupport but not in Ruby builtin/standard libary. 95 | Porting them is one topic (but you need to persuade Matz to introduce it). 96 | 97 | ## Visualization 98 | 99 | Now you can modify MRI by yourself! 100 | You can insert any "printf" into MRI c source code to see the behavior. 101 | This is a very simple *Visualization*. 102 | 103 | Add your original visualization feature on terminal, GUI, sound or something cool. 104 | 105 | ## Add your performance counter 106 | 107 | ruby/debug_counter.[ch] provides a feature to add/show performance counters. 108 | Read implementation and try them. 109 | Try to set `USE_DEBUG_COUNTER` to 1 and run your script. You can see performance counters information. 110 | 111 | After that, feel free to add your performance counters to check the *true* behaviour. 112 | -------------------------------------------------------------------------------- /JA/1_culture.md: -------------------------------------------------------------------------------- 1 | # (1) MRI 開発文化の紹介 2 | 3 | ## この資料について 4 | 5 | MRI の開発文化について紹介します。MRI: Matz Ruby Interpreter (要するに ruby コマンド)はオープンソースとして、1993 年から開発が行われてきました(実際に、ソースコードが公開されたのは 1995 年 12 月です)。オープンソースソフトウェアなので、誰でも開発に参加して良い、ということになっていますが、何も知らないと参加は困難です。そこで、本稿ではまず、MRI の開発がどのように行われているか、笹田が把握している範囲で紹介します。 6 | 7 | 本稿で扱う内容: 8 | 9 | * どのように MRI は開発されているか(開発フローや利用しているツールの紹介) 10 | * (バグなどの)チケットはどのように管理されているか 11 | * どのように MRI の中身についての情報を知るのか、それから開発コミュニティの情報を得るのか 12 | * MRI には、どのような未解決問題が残っているのか 13 | 14 | ## Ruby の開発 15 | 16 | Ruby はオープンソースで開発されているため、誰でも開発に参加できます。この文章を読んでいる皆さんを歓迎します! 17 | 18 | Ruby の開発と一言で言っても、次の二つを指します。 19 | 20 | * (1) プログラミング言語 Ruby の仕様策定 21 | * (2) MRI という Ruby インタプリタの実装 22 | 23 | ### (1) プログラミング言語 Ruby の仕様策定 24 | 25 | プログラミング言語は、なんとなくずっと変らない、という印象があるかもしれませんが、結構変ります。具体的には、(a) 新機能の追加、(b) イマイチだった仕様の変更の 2 点になります。 26 | 27 | 我々は、Ruby をさらに魅力的にするために、この仕様変更を行っています。例えば、もっと便利に、もっと書きやすく、といった観点から Ruby 言語に変更を加えています(ちなみに、言語処理系の高速化のために言語を変更(多くの場合、言語の制限の追加)を行うことも、たまにありますが、Ruby には「速度よりも書きやすさ」というポリシーがあるため、あまり推奨されません)。 28 | 29 | ただ、プログラミング言語利用者としては、今まで習得した内容が変っていくことになるので、負担になります。(a) 新機能は、新しく覚えることが増えるのは、まだマシかもしれません。(b) では、覚えたことを、覚え直さねばならないからです。さらに、複数の Ruby のバージョンで動作するプログラムを書かないとならない人にとっては、バージョン間の違いを適切に把握する必要があります。 30 | 31 | 例えば、Ruby 1.8 以前では、`?x` という記法は、ASCII コードの 0x78 という数値を返していました。しかし、Ruby 1.9 からは、複数エンコーディング対応のため、`"x"` という1文字の文字列が返ることになりました(各エンコーディングでのコードポイントの数値を返す、という案もありますが、その場合エンコーディング情報が失われてしまう、という問題があります)。これまで、`?x` は数値が返ると思っていた人にとっては、覚え直しが発生しましたし、それまで `?x` を使っていた箇所をすべて書き換えるという作業が発生しました。仕様としては整理され、良いものになりましたが(だから変えたんですが)、互換性という観点から、多くの人に負担をかけることになりました。 32 | 33 | 「よりよいものを目指す」という観点と、「過去との互換性を保つ」という観点をなるべく両立させられるように、互換性への影響はなるべく少なくなるように、新しい仕様を検討しています。 34 | 35 | もし、必要があって互換性を壊す変更が必要な場合は、(影響範囲にもよりますが)互換性が崩れるという警告を出す、マイグレーションパス(このように変更すれば良い、という手順)を設ける、といったことを気を付けて行っています。 36 | 37 | 互換性に影響しない変更というと、これまで動かなかったコードを動くようにする、という修正があります。動かなかったコードを書く人は(多分)居ないからです。例えば、Ruby 2.6 で導入される `a..` といった endless-range は、これまでは文法エラーで記述できなかったので、誰も書いたことがない、ということが期待できるため、さっと入りました(が、実はやはり互換性に絡む問題が隠れていて、議論になりました [[Misc #14769]](https://bugs.ruby-lang.org/issues/14769))。 38 | 39 | 変更は、MRI のバージョン更新のタイミングで導入されます。Ruby のバージョンは x.y.z (例えば、Ruby 2.5.1 など)で表されます。それぞれ、x: メジャー、y: マイナー、z: ティーニーと呼びます。 40 | 41 | * ティーニーの変更は、バグ修正のみであり、ティーニーの変更では、アプリケーションの互換性が崩れることが無いように気を付けています。 42 | * マイナーの更新時は、(なるべく互換性を崩さない)機能の修正、追加が行われます。 43 | * メジャーの更新時は、結構大きな変更があってもしょうがない、かもしれません。 44 | 45 | なお、このあたりの判断と努力は、個々の開発者によって行われており、何か保障があるわけではありません。 46 | 47 | ### (2) MRI という Ruby インタプリタの実装 48 | 49 | MRI は Ruby 言語のリファレンス実装、つまり「Ruby 言語の仕様を満たす言語処理系」として扱われているため、採用が決定された Ruby の仕様は、MRI へ搭載されます。 50 | 51 | リファレンス実装であるため、仕様の提案が行われるときは、MRI での実装(パッチ)があると、議論されやすいです。単なる提案や要望は、後回しにされがちです。仕様を検討しているとき、とりあえず MRI 上で実装して試す、といったこともよく行われます。MRI に入っていない、「Ruby 言語の仕様」というものは、おそらくありません。 52 | 53 | MRI のバグが修正されたとMRI の仕様が変わった場合は、Ruby の仕様が変更された、ということなります。そのため、バグ修正においても、互換性が無くなることに対する検討が慎重に行われています(多分)。 54 | 55 | ただ、MRI 特有の変更も多いため、MRI の変更 == Ruby 言語の変更、ということではありません。例えば、性能向上のための変更などがそれにあたります。また、MRI の GC やバイトコード特有の操作を行うメソッドなどは、Ruby 言語ではなく、MRI 特有の仕様と考えられます。例えば、`GC.disable` というメソッドは、GC を起こさないようにする、というものですが、JVM 上で作っていて GC をいじれない場合は、利用することはできません。 56 | 57 | ## リポジトリと Ruby コミッタ 58 | 59 | Ruby のプライマリリポジトリは Git で管理されています 。ある人々がこのリポジトリを修正することができ、その人々のことを「コミッタ」と呼んでいます。現在、全世界で100人程度のコミッタがいらっしゃいます(ただし、現在アクティブに活動している人数は、もっと少ないです。過去にコミッタになると、(今のところ)コミッタを抜けることはできません。怖いですね)。 60 | 61 | コミッタは Ruby の全ソースコードを修正することができますが、(なんとなく)担当範囲が決まっているため、担当範囲外の修正を行う場合は、担当のコミッタの意見を尊重することが求められています。例えば、笹田は現在 Ruby で利用している VM の開発者なので、VM に大きな変更を加える場合は笹田に修正を相談して欲しい、と思っています(現実的には、されないことも多いですが)。 62 | 63 | コミッタ間でのコードレビュー体制はなく、コミットされた修正を眺めて気づいたら指摘したり、問題(バグ報告等)があったときに bisect する、といった体制で開発は行われています。大きな修正では、コミッタ間でレビューを求め合ったりします。 64 | 65 | なお、 という GitHub のリポジトリにミラーがあります。 66 | 67 | ## チケット管理 68 | 69 | 仕様変更、バグ修正など、すべての議論は Redmine にチケットとしてまとめられます(されるべきです)。チケットの登録やコメントなどは、メーリングリストに配信されます。メーリングリストには日本語向けの ruby-dev と、英語向けの ruby-core があります 。 70 | 71 | 仕様変更といった大事なチケットは、英語で起票し、世界中に周知し、広く議論することを強く推奨されています。軽微な修正は、日本語でも受け付けています。日本語で議論を始めたけれど、「仕様の議論だから英語のチケットにしよう」といったことも行われています。 72 | 73 | チケットには、大きく分けて、「機能追加要求(Feature request)」と「バグ報告(Bug report)」があります。 74 | 75 | * Feature requests 76 | * Ruby 仕様の追加や修正(Ruby 言語への要求だったり、MRI への要求だったり)。 77 | * ところで、Redmine の URL は `bugs` で始まってますね。これは、仕様変更のための修正(いや、どんな修正にも、かな?)にはバグが混入するだろうから、`bugs` でいいんだとか。 78 | * Bug reports 79 | * おかしな挙動や、性能の問題といった、仕様変更以外のすべてが含まれます。 80 | 81 | Ruby が利用している Redmine では、チケットを登録する際、チケットに記述する言語を英語か日本語かでドロップダウンメニューから選びます。選択した言語によって、チケットの内容を配信するメーリングリスト(ruby-core か ruby-dev)が決定されます。 82 | 83 | 良いバグ報告には、次のような内容が含まれていることが期待されます。 84 | 85 | * Summary(問題の短いまとめ) 86 | * 再現コードと再現環境(ruby -v の結果(必須です)、OS、コンパイラなどのバージョン、その他) 87 | * 期待する挙動 88 | * 実際に得られた挙動 89 | * (可能なら)その問題を修正するためのパッチ 90 | 91 | 詳細は、英語 (English) もしくは日本語 のドキュメントがありますのでご参照ください。 92 | 93 | 良い Feature request (機能追加要求)には、次のような内容が含まれていることが期待されます。 94 | 95 | * Abstract(提案の短いまとめ) 96 | * Background(背景:現在、何が問題なのか、実際に困っているのは何であるか、実際のユースケースは何か) 97 | * Proposal(提案) 98 | * Implementation (実装:実装があれば、その提案が実現可能であるかを判断する強い証拠になります) 99 | * Evaluation(評価:提案によって、何がどのように良くなったのか、実装があれば、その性能は十分であるか、など) 100 | * Discussion (議論:検討するべき内容、他のアプローチとの比較など) 101 | * Summary(まとめ) 102 | 103 | 機能追加要求では「実際にどんなユースケースがあるのか」という点が(最近だととくに)よく求められます。例えば、「使わないけど、整合性のためにはこういう機能があるほうがいいのではないか」という提案は、あまり通らないことが多いです(この場合、整合性よりも互換性のほうが優先されます)。 104 | 105 | さらなる詳細は (英語)をご参照ください。 106 | 107 | GitHub への issue および Pull request は運が良ければ対応されますが、運が悪ければ放置されます。それらを作成した場合、Redmine への issue を別途作成し、Github の当該 URL を示す、というのが良いと思います(もしくは、誰かコミッタに連絡すれば、対応してくれるかもしれません)。 108 | 109 | ## MRI における CI 110 | 111 | MRI は大きく複雑なソフトウェアなので、テストによる品質保証が必要です。そのため、約 5,000ファイル、45万行程度のテスト用スクリプトが存在します(その話は次の資料で)。 112 | 113 | また、プログラムが大きいだけで無く、様々な環境で Ruby を動作させるので、テスト実行環境も沢山用意する必要があります。 114 | 例えば OS だけでも、Linux、Mac OSX、Windows が有名ですが、それ以外にも、*BSD、Solaris など、色々あります(今はそんなにないのかな?)。 115 | ハードウェア/CPU も、多くの場合、Intel x86/64 CPU、もしくは ARM だと思いますが、それ以外のプロセッサを利用していることがあります。 116 | なお、Ruby がサポートするプラットフォーム一覧はこちら:。 117 | 118 | このように、MRI は様々な場所で利用されていますので、いろいろな組み合わせでテストすることが望ましいです。 119 | テストを自動化するためには、CI を用いることが一般的になっています。我々も、CI 環境を用意しています。 120 | 121 | Ruby では、有名どころである Travis-CI も用いていますが、さまざまな環境で幅広いテストを行うために という、いろいろな環境でテストした結果をまとめるサービスを作って利用しています。 122 | 123 | 通常、CI は自分が管理する計算リソースを利用してテスト等を行いますが、我々は多くの環境を持ってはいません(最近は AWS を利用しているので、管理していると言えなくもないですが)。 124 | 自分たちですべての計算機リソースを用意する代わりに、計算機リソースを持っている人に定期的にテストを実行してもらい、その結果を収集する、という方法にしています。 125 | テストに利用するのは chkbuild というフレームワークで、Ruby のビルド・テスト実行を行い、結果を生成します。 126 | テスト結果は、前回の結果の diff として表示できるため、どのバージョンでバグがあったか見ることができます。 127 | 128 | chkbuild は良いテストフレームワークなのですが、いくつかの理由(例えば、毎回ソースコードのダウンロードから始める)から、実行に時間がかかります(たいてい、数十分)。 129 | そこで という環境を用意し、簡易ですが高速にテストを回していく環境を用意しています。 130 | 前回のビルド結果を(可能なら)再利用したり、並列ビルド・テストを利用する、といった仕組みで、短いもので 2、3分で結果が出ます。例えば、コミットにミスがあったとき、(ci.rvm.jp を監視していれば)すぐに間違いに気づくことができます。 131 | また、2、3 分で済むテストを繰り返しているため、一日に数百回のテストを行うことで、「時々しか出ないバグ」を(時々運良く)発現させることができます。 132 | 133 | コミッタは、基本的には手元でテストを通してからコミットすることが求められます。 134 | うっかりテストの通っていないコミットをしてしまった時は、まず でエラーが検出されます(コミッタの集まる slack にアラートが流れます)。 135 | また、手元の環境(例えば Linux)では動いているが、別の環境(例えば Mac OSX)では動かない、ということもよくあります。その場合、 を見ることでほかの環境の実行結果を確認することができます。 136 | 137 | ## MRI における未解決問題 138 | 139 | Ruby / MRI には、まだまだ手を付けたい問題がたくさんあります。下記にいくつかあげておきます。 140 | 141 | * 仕様の議論 142 | * Ruby 2.x (2.6, ...) 143 | * Ruby 3 144 | * JIT compilation (only for performance? drop backward compatibility?) 145 | * Static checking 146 | * Concurrent execution 147 | * 性能改善 148 | * ベンチマークの整備 149 | * 性能改善 150 | * ドキュメンテーション 151 | * バグ修正 152 | 153 | 最近笹田がなんとかしたいと思っている問題(インターナルが多い)もあげておきます。 154 | 155 | * バイトコードシリアライザの性能・品質向上 156 | * メソッド呼び出しの仕組み変更による高速化 157 | * コードのインライン化の対応 158 | * 世代別 GC 対応オブジェクトを増やして性能向上 159 | * `Time` オブジェクト 160 | * これに関してのサーベイ 161 | * 世間の gem を Ruby の CI でテストする仕組みの用意 162 | * 同じく、ベンチマークする仕組み 163 | 164 | ## Ruby 開発の情報 165 | 166 | ### MRI インターナルをハックするための情報 167 | 168 | [参考文献](../bib.md) をご参照ください。 169 | また、深くハックする場合は、C 言語の知識が必要になります。 170 | 171 | ### コミュニケーションチャンネル 172 | 173 | * Ruby's redmine: https://bugs.ruby-lang.org/projects/ruby/ 174 | * Ticket 175 | * Wiki 176 | * Mailing list 177 | * https://www.ruby-lang.org/en/community/mailing-lists/ (En) https://www.ruby-lang.org/ja/community/mailing-lists/ (Ja) 178 | * ruby-core (English) 179 | * ruby-dev (Japanese) 180 | * Conference, meetup 181 | * RubyConf and other international conferences(ほぼ、英語で議論しています) 182 | * 日本国内 183 | * RubyKaigi 184 | * RegionalRubyKaigi 185 | * Asakusa.rb, *.rb 186 | * Ruby 開発者会議 187 | * 毎月、東京のどこかで行っています。 188 | * 個人へのコンタクト 189 | * Twitter 190 | * @yukihiro_matz 191 | * ... 192 | * Gitter 193 | * これを機に作りました。 194 | 195 | ## 大切なこと 196 | 197 | 本稿では、いくつか面倒そうなルール的なことを書きましたが、我々 Ruby インタプリタ開発者が最も重視しているのは「ハッキング」です。 198 | もし、偉大なパッチを寄贈してくださるのであれば、多少ルールからそれても、全力でサポートします(もしくは、全力で議論します)。 199 | 200 | コードを書きましょう。 201 | -------------------------------------------------------------------------------- /JA/2_mri_structure.md: -------------------------------------------------------------------------------- 1 | # (2) MRI ソースコードの構造 2 | 3 | ## この資料について 4 | 5 | MRI のソースコードの構造について紹介します。また、Ruby のソースコードをハックする最低限の知識を紹介します。 6 | 7 | * 演習: MRI のソースコードを clone 8 | * 演習: MRI のビルド、およびインストール 9 | * MRI の構造の紹介 10 | * 演習: ビルドした Ruby でプログラムを実行 11 | * 演習: バージョン表記を変更してみよう 12 | 13 | ## 本稿で前提とするディレクトリ構造 14 | 15 | 下記のコマンドは、Linux や Mac OSX などを前提としています。Windows 等を使う場合は、別途頑張ってください。 16 | 17 | > Note: docker 環境(Ubuntu 18.04 base)を作ってみました。 `docker pull koichisasada/rhc` で試してみてください。`su rubydev` でアカウントを rubydev でご利用ください。 18 | 19 | 前提とするディレクトリ構造: 20 | 21 | * `workdir/` 22 | * `ruby/` <- git clone するディレクトリ 23 | * `build/` <- ビルドディレクトリ(ここに、コンパイルした `*.o` などが入る) 24 | * `install/` <- インストールディレクトリ (`workdir/install/bin/ruby` がインストールされたディレクトリになります) 25 | 26 | 前提とするコマンド: 27 | 28 | git、ruby、autoconf、gcc (or clang, etc)、make が必須です。その他、依存ライブラリがあれば、拡張ライブラリが作成されます。 29 | 30 | `apt-get` が使える環境では、下記のようなコマンドでインストールされます。 31 | 32 | ``` 33 | $ sudo apt-get install git ruby autoconf gcc make zlib1g-dev libffi-dev libreadline-dev libgdbm-dev libssl-dev libyaml-dev 34 | ``` 35 | 36 | `apt-get` 以外でインストールしたい場合は、例えば [Home · rbenv/ruby\-build Wiki](https://github.com/rbenv/ruby-build/wiki) を参照してみてください。 37 | 38 | ## 演習: MRI のソースコードを clone 39 | 40 | 1. `$ mkdir workdir` 41 | 2. `$ cd workdir` 42 | 3. `$ git clone https://github.com/ruby/ruby.git` # workdir/ruby にソースコードが clone されます 43 | 44 | (ネットワーク帯域の問題があるので、できれば家などで行ってきてください) 45 | 46 | ## 演習: MRI のビルド、およびインストール 47 | 48 | 1. 上記「前提とするコマンド」を確認 49 | 2. `$ cd workdir/` # workdir に移動します 50 | 3. `$ cd ruby` # workdir/ruby に移動します 51 | 4. `$ ./autogen.sh` 52 | 5. `$ cd ..` 53 | 6. `$ mkdir build` # `workdir/build` を作成します 54 | 7. `$ cd build` 55 | 8. `$ ../ruby/configure --prefix=$PWD/../install --enable-shared` 56 | * `prefix` は、インストールする先のディレクトリです。絶対パスで、好きな場所を指定してください(この例では `workdir/install`) 57 | * Homebrew で諸々インストールしている場合は、 ```--with-openssl-dir=`brew --prefix openssl` --with-readline-dir=`brew --prefix readline` --disable-libedit``` を付けてください。 58 | 9. `$ make -j` # ビルドします。`-j` は並列にコンパイルなどを行うオプションです。 59 | * この時点で、`ruby` コマンドと `miniruby` コマンドが `workdir/build` にできているはずです。 60 | * また、`.ext/` に拡張ライブラリが格納されています。 61 | 10. `$ make install` 62 | * この時点で、`../install`、つまり `workdir/install` に諸々インストールされます。実際に何が入っているか確認してみましょう。 63 | * > tips: `make install-nodoc` とすると、rdoc/ri ドキュメントのインストールをスキップします 64 | 11. `$ ../install/bin/ruby -v` で、Ruby がインストールされたことを確認してください(`ruby -v` はバージョンを出力して終了します) 65 | 66 | > NOTE: `make V=1` とすると、`make` コマンドが具体的にどのようなコマンドを実行しているかを表示します。デフォルト(`V=0`)では、これらの表示を抑制しています。 67 | 68 | > NOTE: `make -j` とすると、コンパイルなどのプロセスが並列に実行され、高速に終了する可能性があります。`make -j4` など、数字を置くことで、並列に実行するプロセス数を抑えることができます。 69 | 70 | 上記手順では、主に次のことをしています。 71 | 72 | * `autoreconf` による `configure` スクリプトの生成 73 | * `configure` による `Makefile` の生成 74 | * `make` による `./ruby` の生成(`make` 単体での実行は `make all` の意味になります)。これは、いくつかの生成が含まれています。 75 | * `make miniruby` による `./miniruby` の生成 76 | * `make encs` によるエンコーディング関連拡張ライブラリの生成 77 | * `make exts` による拡張ライブラリの生成 78 | * `make ruby` による `./ruby` の生成 79 | * `make docs` による rdoc の生成 80 | * `make install` によるインストールディレクトリの生成 81 | 82 | なお、この 2 回の `make` については、`make all install` とすると、1回の呼び出しで終わります。 83 | 84 | ### 久しぶりに実行したビルドでエラーが起こる場合 85 | 86 | 以前にRubyを上記の方法でビルドしたことがある場合、 `make` コマンドが失敗する可能性があります。 87 | その場合は、 88 | 89 | ``` 90 | make clean 91 | ``` 92 | 93 | を実行して古いファイル・ディレクトリを削除してから再度 `make` コマンドを実行してみてください。 94 | 95 | それでも失敗する場合は、 96 | 97 | ``` 98 | make distclean 99 | ``` 100 | 101 | でconfigureからやり直すとうまくいく可能性があります。 102 | 103 | ## 演習:ビルドした Ruby でプログラムを実行してみよう 104 | 105 | ビルドした Ruby で実際に Ruby スクリプトを実行する方法はいくつかあります。 106 | 107 | 一番わかりやすい方法は、上記手順でインストールまで終わらせ、インストールした Ruby を利用して実行することです(この例では、`workdir/install/bin/ruby`)。「いつも Ruby を使っている方法」と全く同じです。ですが、Ruby を修正するたびに Ruby のインストールまで行うと、若干時間がかかります(マシンによりますが、`make install` が終わるまでに数十秒かかります)。 108 | 109 | ここでは、それ以外の、Ruby を修正・確認するときに便利な実行方法を紹介します。 110 | 111 | ### miniruby で実行しよう 112 | 113 | Ruby のビルドが終わると、ビルドディレクトリ(`workdir/build`)に、`miniruby` という実行ファイルが生成されます。`miniruby` は、Ruby のビルドするために作られる、機能制限版の Ruby インタプリタです。ただ、制限といっても、拡張ライブラリを読み込むことができない、エンコーディングに制約がある、といったものであり、Ruby の機能のほとんどをサポートしています。 114 | 115 | `miniruby` は、Ruby のビルドの初期段階で生成されるため、MRIの修正を行い、その結果を確認するためには、`miniruby` を実行して修正結果を確認するのが良いです。つまり、 116 | 117 | 1. MRI のソースコードを修正する 118 | 2. `make miniruby` として、`./miniruby` を生成する(すべてビルドしてインストールするよりも速く終わる) 119 | 3. 修正に関係あるスクリプト `workdir/build/script.rb` を `./miniruby script.rb` で実行する 120 | 121 | という流れで開発を進めると効率的です。 122 | 123 | この流れを行うために、`make run` という make のルールがあります。これを行うと `miniruby` をビルドし、`workdir/ruby/test.rb` (ソースディレクトリであることに注意)に書かれた内容を実行します。 124 | 125 | つまり、下記のように進められます。 126 | 127 | 1. Ruby のソースコードを修正する。 128 | 2. `ruby/test.rb` に、修正に関係した Ruby スクリプトを記述する(`miniruby` では、gem や拡張ライブラリは使えないので注意)。 129 | 3. ビルドディレクトリ(`workdir/build`)で `$ make run` を実行する。 130 | 131 | `make miniruby` で `./miniruby` を生成した後、`./miniruby ../ruby/test.rb` を実行してくれます。 132 | いちいち、`./miniruby ...` などと入力しなくて良いのが便利なところです。 133 | また、拡張ライブラリのビルドなどを行わない、というのも、実行時間の短縮に寄与しています。 134 | つまり、ちょっと修正しては試す、というサイクルをささっと回しやすい、ということです。 135 | 136 | もし、修正が失敗しており、コンパイルエラーなどが起こると、このプロセスは途中で止まります(`make` の機能ですね)。 137 | 138 | なお、新しい修正をするとき、`test.rb` の内容を書き換える必要があります。このとき、全てを消すよりは、すでに書いてあるスクリプトの前に `__END__` と書くようにすると、前のスクリプトを残したまま新しいスクリプトを追加できるので便利です。 139 | 140 | 141 | ``` 142 | # 新しいテストスクリプト 143 | 144 | __END__ 145 | 146 | # 前のスクリプト 147 | ``` 148 | 149 | 笹田の `test.rb` を見てみると、4000行ありました(時々消すので、あんまり大きくないです)。 150 | 151 | ### miniruby ではない、フルセットの ruby で実行しよう 152 | 153 | 拡張ライブラリを含む「普通の」Rubyを実行したい時は、`make run` の代わりに `make runruby` を使います。`make install` しないで実行できるため、若干早く開発が進められます。 154 | 155 | 1. `ruby/test.rb` に実行したい Ruby スクリプトを表示する(gem は使えないので注意)。また、Ruby のソースコードを修正する。 156 | 2. ビルドディレクトリ(`workdir/build`)で `$ make runruby` を実行する 157 | 158 | ### gdb を用いてデバッグしよう 159 | 160 | > NOTE: Mac OSX で gdb を動かすのは難しいようです。下記は、Linux 等を念頭に解説しています。笹田は使わないのでよく知らないのですが、`$ make lldb` もあるようです。 161 | 162 | Ruby のソースコードを修正すると、C プログラムなので容易に SEGV といったクリティカルな問題を簡単に発生させることができます(発生しちゃいます)。そこで、gdb を使ってデバッグするための方法を用意しています。もちろん、ブレイクポイントを用いたデバッグなどでも利用可能です。 163 | 164 | 1. `ruby/test.rb` にテストしたい Ruby スクリプトを記述する 165 | 2. ビルドディレクトリ(`workdir/build`)で `$ make gdb` を実行する(問題が起こらなければ、何事もなく終了します) 166 | 167 | このとき、利用するのは `./miniruby` になります。`./ruby` を用いたい場合は `make gdb-ruby` としてください。 168 | 169 | もし、ブレイクポイントを挿入したい場合は、`make gdb` コマンドでビルドディレクトリに生成される `run.gdb` というファイルに、例えば `b func_name` といったブレイクポイント指定を書いてください。 170 | 171 | ### Ruby のテストを実行しよう 172 | 173 | 1. `$ make btest` # run bootstrap tests in `ruby/bootstraptest/` 174 | 2. `$ make test-all` # run test-unit tests in `ruby/test/` 175 | 3. `$ make test-spec` # run tests provided in `ruby/spec` 176 | 177 | これらの三つは、それぞれ別々の目的・特徴をもって開発されています。 178 | 179 | * `ruby/bootstraptest/`: メソッド呼び出しができるか、など最低限のテスト。各テストは別プロセスで実行される。minitest っぽい独自形式で書かれている。 180 | * `ruby/test/`: Ruby の全機能(が目標)のテスト。minitest 形式で書かれている。 181 | * `ruby/spec/`: Ruby の仕様を記述しようという rubyspec というプロジェクトによるテスト。rspec っぽい独自形式で書かれている。 182 | 183 | なお、`make check` とすると、これら全てのテストをまとめて実行します。 184 | 185 | ## MRI のソースコードの構造の紹介 186 | 187 | ### インタプリタ 188 | 189 | 大雑把に、下記のようなディレクトリ構造になっています。 190 | 191 | * `ruby/*.c` MRI core files 192 | * VM cores 193 | * VM 194 | * `vm*.[ch]`: VM の実装 195 | * `vm_core.h`: VM データ構造の定義 196 | * `insns.def`: VM の命令定義 197 | * `compile.c, iseq.[ch]`: 命令列関係の処理 198 | * `gc.c`: GC とメモリ管理 199 | * `thread*.[ch]`: スレッド管理 200 | * `variable.c`: 変数管理 201 | * `dln*.c`: C拡張のためのダイナミックリンクライブラリ管理 202 | * `main.c`, `ruby.c`: MRI のエントリーポイント 203 | * `st.c`: ハッシュテーブルアルゴリズムの実装 (参考: https://blog.heroku.com/ruby-2-4-features-hashes-integers-rounding) 204 | * 組み込みクラス 205 | * `string.c`: String class 206 | * `array.c`: Array class 207 | * ... (だいたい、クラス名に対応するファイル名に定義が格納されています) 208 | * `ruby/*.h`: 内部定義。拡張ライブラリは基本的に使えません 209 | * `ruby/include/ruby/*`: 外部定義。拡張ライブラリで参照できます 210 | * `ruby/enc/`: エンコーディングのためのソースコードや情報 211 | * `ruby/defs/`: 各種定義 212 | * `ruby/tool/`: MRI をビルド・実行するためのツール 213 | * `ruby/missing/`: いくつかの OS で足りないものの実装 214 | * `ruby/cygwin/`, `ruby/nacl/`, `ruby/win32`, ...: OS/system 依存のソースコード 215 | 216 | ### ライブラリ 217 | 218 | ライブラリは 2 種類あります。 219 | 220 | * `ruby/lib/`: 標準添付のライブラリ(Ruby で記述されたライブラリ) 221 | * `ruby/ext/`: 標準添付の拡張ライブラリ(C で記述されたライブラリ) 222 | 223 | ### テスト 224 | 225 | * `ruby/basictest/`: place of old test 226 | * `ruby/bootstraptest/`: bootstrap test 227 | * `ruby/test/`: tests written by test-unit notation 228 | * `ruby/spec/`: tests written by RSpec notation 229 | 230 | ### misc 231 | 232 | * `ruby/doc/`, `ruby/man/`: ドキュメント 233 | 234 | ## Ruby のビルドプロセス 235 | 236 | Ruby のビルドでは、ソースコードを生成しながらビルドを進めていきます。ソースコードを生成するいくつかのツールは Ruby を用いるため、Ruby のビルドには Ruby が必要になります。ソースコード配布用の tar ball には、これら生成されたソースコードもあわせて配布しているので、tar ball を用いるのであれば、Ruby のビルドに Ruby (や、その他 autoconf などの外部ツール)は不要です。 237 | 238 | 逆に言うと、Subversion や Git リポジトリからソースコードを取得した場合は、Ruby インタプリタ(や、autoconf などの外部ツール)が必要になります。 239 | 240 | ビルド・インストールは、次のように進みます(要するに、`make all` がやっていること)。 241 | 242 | 1. miniruby のビルド 243 | 1. parse.y -> parse.c: Compile syntax rules to C code by lrama 244 | 2. insns.def -> vm.inc: Compile VM instructions to C code by ruby (`BASERUBY`) 245 | 3. `*.c` -> `*.o` (`*.obj` on Windows): Compile C codes to object files. 246 | 4. link object files into miniruby 247 | 2. エンコーディングのビルド 248 | 1. translate enc/... to appropriate C code by `miniruby` 249 | 2. compile C code 250 | 3. C 拡張ライブラリのビルド 251 | 1. Make `Makefile` from `extconf.rb` by `mkmf.rb` and `miniruby` 252 | 2. Run `make` using generated `Makefile`. 253 | 4. `ruby` コマンドのビルド 254 | 5. `rdoc`, `ri` ドキュメントの生成 255 | 6. 生成されたファイルのインストール(インストール先は `configure` の `--prefix` で指定したもの) 256 | 257 | 実は、本当はもっと色々やっているのですが、書き切れないし、私も把握していないので、省略しています。`common.mk` といった make 用のルール集に、いろいろなファイルが入っています。 258 | 259 | ## 演習:バージョン表記の修正(改造) 260 | 261 | では、実際に Ruby を修正してみましょう。ソースコードはすべて `workdir/ruby/` にあると仮定します。 262 | 263 | まずは、`ruby -v`(もしくは `./miniruby -v`)を実行したときに、自分の Ruby だとわかるように、何か表示を変えてみましょう。 264 | 265 | 1. バージョン表記を行うコードは `version.c` にあるので、これを開きます。 266 | 2. 少し、ソースコード全体を眺めてみましょう。 267 | 3. `ruby_show_version()` という関数が怪しそうです(関数名見れば自明?)。 268 | 4. `fflush()` が、出力を確定する(出力バッファを吐き出す) C の関数なので、この前に何らかの出力をすれば良いと推測。 269 | 5. `printf("...\n");` (`...` の部分には、好きな文字列)を記入。 270 | 6. `$ make miniruby` でビルド(ビルドディレクトリに移動しておく)。 271 | 7. `$ ./miniruby -v` で結果を確認。 272 | 8. `$ make install` でインストール。 273 | 9. `$ ../install/bin/ruby -v` でインストールされた ruby コマンドにも変更が反映されたことを確認。 274 | 275 | 最後に `printf(...)` を挟むだけではなく、`ruby ...` と書かれた行を変更しても面白いかもしれませんね。`perl` と出力してみるとか。 276 | -------------------------------------------------------------------------------- /JA/3_practice.md: -------------------------------------------------------------------------------- 1 | # (3) 演習:メソッドの追加 2 | 3 | ## この資料について 4 | 5 | 実際に、MRI にメソッドを追加してみましょう。修正する例を書いているので、実際に手を動かして追加してみてください。 6 | 7 | なお、git リポジトリを使っていると思うので、各修正ごとに commit するか、ブランチを作るようにしておいてください。 8 | 9 | ## `Array#second` 10 | 11 | `Array#second` メソッドを追加してみましょう。 12 | `Array#first` は最初の要素を返します。`Array#second` は二つ目の要素を返すメソッドです。 13 | 14 | Ruby で定義するとこんな感じです。 15 | 16 | ```ruby 17 | # specification written in Ruby 18 | class Array 19 | def second 20 | self[1] 21 | end 22 | end 23 | ``` 24 | 25 | 1. `array.c` を開きましょう。 26 | 2. `ary_second()` という関数を追加しましょう。`Init_Array()` の前が良いと思います。 27 | 3. `rb_define_method(rb_cArray, "second", ary_second, 0);` という行を `Init_Array()` 関数に追加しましょう。 28 | 4. ビルドし、`ruby/test.rb` にサンプルコードを記述して、`make run` で動くか試してみましょう。 29 | 5. テストを `ruby/test/ruby/test_array.rb` に記入しましょう。minitest フォーマットです。 30 | 6. `$ make test-all` と実行すると、書いたテストが実行されます。ただし、数万のテストが走ってしまうので、Array のテストだけに絞りましょう。 31 | * `$ make test-all TESTS='ruby/test_array.rb'` とすることで、`ruby/test/ruby/test_array.rb` だけテストします。 32 | * `$ make test-all TESTS='ruby/test_array.rb -n test_xxx'` とすることで、`ruby/test_array.rb` にある `test_xxx` にマッチするテストのみ走らせます。 33 | * `$ make test-all TESTS='-j8'` とすることで、8 並列でテストを走らせます。 34 | 7. ほかのメソッドを参考に、`Array#second` に rdoc ドキュメントを記入してみましょう。 35 | 36 | C での定義はこんな感じになります(下記 diff を取ってから時間がたっているので、行番号は、ずれていると思います)。 37 | 38 | ```diff 39 | diff --git a/array.c b/array.c 40 | index bd24216af3..79c1c1d334 100644 41 | --- a/array.c 42 | +++ b/array.c 43 | @@ -6131,6 +6131,12 @@ rb_ary_sum(int argc, VALUE *argv, VALUE ary) 44 | * 45 | */ 46 | 47 | +static VALUE 48 | +ary_second(VALUE self) 49 | +{ 50 | + return rb_ary_entry(self, 1); 51 | +} 52 | + 53 | void 54 | Init_Array(void) 55 | { 56 | @@ -6251,6 +6257,8 @@ Init_Array(void) 57 | rb_define_method(rb_cArray, "dig", rb_ary_dig, -1); 58 | rb_define_method(rb_cArray, "sum", rb_ary_sum, -1); 59 | 60 | + rb_define_method(rb_cArray, "second", ary_second, 0); 61 | + 62 | id_cmp = rb_intern("<=>"); 63 | id_random = rb_intern("random"); 64 | id_div = rb_intern("div"); 65 | ``` 66 | 67 | 少し解説しておきます。 68 | 69 | * `ary_second()` が実装です。 70 | * `VALUE` は Ruby のオブジェクトであり、`self` はメソッド呼び出しでのレシーバ(`ary.second` の時の `ary`)です。すべてのメソッド呼び出しは、Ruby の配列を返すので、返値も `VALUE` となります。 71 | * `rb_ary_entry(self, n)` が `self[n]` の意味であり、`n = 1` なので、2番目(0 origin なので)を返します。 72 | * `Init_Array` という関数が、MRI 起動時に実行されます。 73 | * `rb_define_method(rb_cArray, "second", ary_second, 0);` で、`Array` クラスに `second` メソッドを定義しています。 74 | * `rb_cArray` が Array クラスのオブジェクトです。`rb_` が Ruby の何か、`c` がクラスであることを意味するため、`rb_cArray` が Ruby の `Array` クラスであることがわかります。ちなみに、モジュールの場合は `m`(例えば、`rb_mEnumerable`、エラークラスの場合は `e`(例えば、`rb_eArgError`)。 75 | * `rb_define_method` がインスタンスメソッドを定義する関数です。 76 | * 「`rb_cArray` に、`"second"` という名前のメソッドを定義しろ。メソッドが呼ばれたら `ary_second` を呼び出せ。なお、引数の数は 0 である」という意味になります。 77 | 78 | ポイントは、(1) メソッドの実体は C の 1 関数である (2) その関数を `rb_define_method()` で Ruby のメソッドとして登録する、ということです。Ruby のメソッドは、ほぼこんな感じで実装されています。 79 | 80 | ## `String#palindrome?` 81 | 82 | 回文判定メソッド `String#palindrome?` を定義してみましょう。 83 | 84 | 次のコードは Ruby で書いたもの、および、ちょっとしたテストです。 85 | 86 | ```ruby 87 | class String 88 | def palindrome? 89 | chars = self.gsub(/[^A-z0-9\p{hiragana}\p{katakana}]/, '').downcase 90 | # p chars 91 | !chars.empty? && chars == chars.reverse 92 | end 93 | end 94 | 95 | # Small sample program 96 | # Sample palindrome from https://en.wikipedia.org/wiki/Palindrome 97 | [# OK 98 | "Sator Arepo Tenet Opera Rotas", 99 | "A man, a plan, a canal - Panama!", 100 | "Madam, I'm Adam", 101 | "NisiOisiN", 102 | "わかみかものとかなかとのもかみかわ", 103 | "アニマルマニア", 104 | # NG 105 | "", 106 | "ab", 107 | ].each{|str| 108 | p [str, str.palindrome?] 109 | } 110 | ``` 111 | 112 | Ruby コードを、C のコードに直接的に変換してみます。 113 | `Array#second` での手順を参考に、下記を変更してみてください。 114 | 115 | ```diff 116 | diff --git a/string.c b/string.c 117 | index c140148778..0f170bd20b 100644 118 | --- a/string.c 119 | +++ b/string.c 120 | @@ -10062,6 +10062,18 @@ rb_to_symbol(VALUE name) 121 | return rb_str_intern(name); 122 | } 123 | 124 | +static VALUE 125 | +str_palindrome_p(VALUE self) 126 | +{ 127 | + const char *pat = "[^A-z0-9\\p{hiragana}\\p{katakana}]"; 128 | + VALUE argv[2] = {rb_reg_regcomp(rb_utf8_str_new_cstr(pat)), 129 | + rb_str_new_cstr("")}; 130 | + VALUE filtered_str = rb_str_downcase(0, NULL, str_gsub(2, argv, self, FALSE)); 131 | + return rb_str_empty(filtered_str) ? Qfalse : 132 | + rb_str_equal(filtered_str, rb_str_reverse(filtered_str)); 133 | + 134 | +} 135 | + 136 | /* 137 | * A String object holds and manipulates an arbitrary sequence of 138 | * bytes, typically representing characters. String objects may be created 139 | @@ -10223,6 +10235,8 @@ Init_String(void) 140 | rb_define_method(rb_cString, "valid_encoding?", rb_str_valid_encoding_p, 0); 141 | rb_define_method(rb_cString, "ascii_only?", rb_str_is_ascii_only_p, 0); 142 | 143 | + rb_define_method(rb_cString, "palindrome?", str_palindrome_p, 0); 144 | + 145 | rb_fs = Qnil; 146 | rb_define_hooked_variable("$;", &rb_fs, 0, rb_fs_setter); 147 | rb_define_hooked_variable("$-F", &rb_fs, 0, rb_fs_setter); 148 | ``` 149 | 150 | 解説します。 151 | 152 | * `rb_reg_regcomp(pat)` によって、`pat` という C の文字列を正規表現オブジェクトとしてコンパイルします。 153 | * `rb_str_new_cstr("")` で、空の Ruby 文字列を生成します(C の空文字列を、Ruby の空文字列に変換しています)。 154 | * `str_gsub()` で、`String#gsub` 相当の処理を行います。ここでは、正規表現を使って、扱う文字以外を削っています。 155 | * `rb_str_downcase()` で、その結果を小文字にそろえます。 156 | * `rb_str_empty()` で、フィルタ結果が空文字列であるかどうかをチェックします。 157 | * `rb_str_reverse()` で、文字列の順序の逆転をしています。 158 | * `rb_str_equal()` で、文字列の比較をしています。 159 | 160 | なんとなく、Ruby のコードと一対一に対応しているのがわかるでしょうか。 161 | 162 | ちなみに、Ruby 版と仕様が決定的に異なる点が1つあります。それは、`String` クラスのメソッド(例えば `String#empty?`)を書き換えた時の挙動です。Ruby 版では、書き換えた処理が呼ばれますが、`rb_str_empty()` は、`String#empty?` とは無関係なので呼ばれません(`String#empty?` が `rb_str_empty()` を利用している)。興味があれば、実際に試してみて下さい。 163 | 164 | ## `Integer#add(n)` 165 | 166 | `Integer` クラスに、`n` 足すメソッドを作りましょう。 167 | 168 | Ruby で書くと、こんな感じです。 169 | 170 | ```ruby 171 | class Integer 172 | def add n 173 | self + n 174 | end 175 | end 176 | 177 | p 1.add(3) #=> 4 178 | p 1.add(4.5) #=> 5.5 179 | ``` 180 | 181 | ```diff 182 | Index: numeric.c 183 | =================================================================== 184 | --- numeric.c (リビジョン 59647) 185 | +++ numeric.c (作業コピー) 186 | @@ -5238,6 +5238,12 @@ 187 | } 188 | } 189 | 190 | +static VALUE 191 | +int_add(VALUE self, VALUE n) 192 | +{ 193 | + return rb_int_plus(self, n); 194 | +} 195 | + 196 | /* 197 | * Document-class: ZeroDivisionError 198 | * 199 | @@ -5449,6 +5455,8 @@ 200 | rb_define_method(rb_cInteger, "bit_length", rb_int_bit_length, 0); 201 | rb_define_method(rb_cInteger, "digits", rb_int_digits, -1); 202 | 203 | + rb_define_method(rb_cInteger, "add", int_add, 1); 204 | + 205 | #ifndef RUBY_INTEGER_UNIFICATION 206 | rb_cFixnum = rb_cInteger; 207 | #endif 208 | ``` 209 | 210 | 1引数が必須なので、`rb_define_method()` の最後の引数が `1` になっており、`int_add()` の引数に `VALUE n` が追加されています。 211 | 212 | 実際に、足し算を行う処理は `rb_int_plus()` が行っています。そのため、難しい処理は書いていません。ただ、`self` と `n` が `Fixnum`(ある一定の小さな数値、C の `int` への変換、`int` からの変換が容易)である場合だけ、C で足し算をしてみましょう。 213 | 214 | > Tips: Ruby 2.3 までは、整数値は `Fixnum` クラスと `Bignum` クラスに分かれていましたが、Ruby 2.4 からは `Integer` に統合されました。ただし、MRI 内部では、(性能上の観点から)それらを区別して管理しています(例えば、`FIXNUM_P(bignum)` とすると偽が返ります)。 215 | 216 | ```diff 217 | Index: numeric.c 218 | =================================================================== 219 | --- numeric.c (リビジョン 59647) 220 | +++ numeric.c (作業コピー) 221 | @@ -5238,6 +5238,22 @@ 222 | } 223 | } 224 | 225 | +static VALUE 226 | +int_add(VALUE self, VALUE n) 227 | +{ 228 | + if (FIXNUM_P(self) && FIXNUM_P(n)) { 229 | + /* c = a + b */ 230 | + int a = FIX2INT(self); 231 | + int b = FIX2INT(n); 232 | + int c = a + b; 233 | + VALUE result = INT2NUM(c); 234 | + return result; 235 | + } 236 | + else { 237 | + return rb_int_plus(self, n); 238 | + } 239 | +} 240 | + 241 | /* 242 | * Document-class: ZeroDivisionError 243 | * 244 | ``` 245 | 246 | `FIXNUM_P(self) && FIXNUM_P(n)` によって、`self` と `n` が `Fixnum` であるかどうかをチェックしています。 247 | もしそうであれば、`FIX2INT()` によって、`int` に変換できるので、変換し計算しています。計算結果を `INT2NUM()` によって、VALUE 型(つまり、Ruby の `Integer` クラスのオブジェクト)へ変換し、それを返り値として返します。 248 | もし `self` と `n` のどちらかが `Fixnum` でなければ、`rb_int_plus` に処理を任せています。 249 | 250 | ※注意:実は、この修正版のプログラムにはバグがあります。 251 | 252 | ## `Time#day_before(n=1)` 253 | 254 | `Time` クラスに n 日前の値(引数が無ければ1日前)を返すメソッドを加えてみましょう。 255 | 256 | Ruby で書くとこんな感じです。24時間 * n の秒数を減らしています。厳密には、この方法で n 日前を計算するということは出来ません(閏秒とか、サマータイムとか。そもそも n 日前とは?)。が、今回はサンプルなので、あまり細かいことを考えないようにしようと思います。 257 | 258 | ```ruby 259 | class Time 260 | def day_before n = 1 261 | Time.at(self.to_i - (24 * 60 * 60 * n)) 262 | end 263 | end 264 | 265 | p Time.now #=> 2017-08-24 14:48:44 +0900 266 | p Time.now.day_before #=> 2017-08-23 14:48:44 +0900 267 | p Time.now.day_before(3) #=> 2017-08-21 14:48:44 +0900 268 | ``` 269 | 270 | C で書いてみると、こんな感じです。 271 | 272 | ```diff 273 | Index: time.c 274 | =================================================================== 275 | --- time.c (リビジョン 59647) 276 | +++ time.c (作業コピー) 277 | @@ -4717,6 +4717,22 @@ 278 | return time; 279 | } 280 | 281 | +static VALUE 282 | +time_day_before(int argc, VALUE *argv, VALUE self) 283 | +{ 284 | + VALUE nth; 285 | + int n, sec, day_before_sec; 286 | + 287 | + rb_scan_args(argc, argv, "01", &nth); 288 | + if (nth == Qnil) nth = INT2FIX(1); 289 | + n = NUM2INT(nth); 290 | + 291 | + sec = NUM2INT(time_to_i(self)); 292 | + day_before_sec = sec - (60 * 60 * 24 * n); 293 | + 294 | + return rb_funcall(rb_cTime, rb_intern("at"), 1, INT2NUM(day_before_sec)); 295 | +} 296 | + 297 | /* 298 | * Time is an abstraction of dates and times. Time is stored internally as 299 | * the number of seconds with fraction since the _Epoch_, January 1, 1970 300 | @@ -4896,6 +4912,8 @@ 301 | 302 | rb_define_method(rb_cTime, "strftime", time_strftime, 1); 303 | 304 | + rb_define_method(rb_cTime, "day_before", time_day_before, -1); 305 | + 306 | /* methods for marshaling */ 307 | rb_define_private_method(rb_cTime, "_dump", time_dump, -1); 308 | rb_define_private_method(rb_singleton_class(rb_cTime), "_load", time_load, 1); 309 | ``` 310 | 311 | ポイントを説明します。 312 | 313 | * 可変長引数にするために、`rb_define_method()` で `-1` を指定しています。何個来るかわかりませんよ、という意味になります。 314 | * `time_day_before(int argc, VALUE *argv, VALUE self)` という関数でメソッドの実体を定義しています。`argc` に引数の数、`argv` に長さ `argc` VALUE の配列へのポインタが格納されています。 315 | * `rb_scan_args()` を使い、引数をチェックしています。`"01"` というのは、必須引数が 0 個、オプショナル引数が 1 個、という意味になります。つまり、0 or 1 個の引数を取る、ということになり、もし 1 個引数を取っていれば、`nth` に格納されます。もし、引数が 0 個の場合(つまり、引数が無い場合)は、`nth` には `Qnil` (Ruby での `nil` を、C ではこのように表現している)が格納されます。 316 | * `Time.at()` を実現するために、`rb_funcall(recv, mid, argc, ...)` を利用しています。 317 | * 第一引数はレシーバ、つまり `recv.mid(...)` の時の `recv` になります。`Time.at` では、レシーバは `Time` クラスオブジェクト、ということになります。 318 | * メソッド名の指定は、C の文字列リテラルでは無く、ID で行います。ID を生成するためには、`rb_intern("...")` を利用します。ID は、ある文字列に対して、MRI プロセス中で一意な値のことです。Ruby でいう Symbol、Java でいう "intern" した文字列です。 319 | * 1 引数なので、1 と指定し、その後で実際の引数を指定します。 320 | 321 | なお、この実装には色々と問題があります。Ruby 実装と何が違うのか、検討してみてください。 322 | 323 | ## 拡張ライブラリ 324 | 325 | MRI を後から機能拡張するための、C 拡張ライブラリは、ほぼ同じような流儀で作ることができます。 326 | 327 | 例えば、`Array#second` を MRI に直接組み込むのではなく、拡張ライブラリで提供することを考えます。 328 | 329 | 次の手順で `.so` を作ります(MacOS だと、`.bundle` になります)。 330 | 331 | 1. ディレクトリ `array_second/` を作成する(どこでも良いです)。 332 | 2. `array_second/extconf.rb` を作成する 333 | * `require 'mkmf'` として、mkmf ライブラリを使えるようにする。mkmf ライブラリは、Makefile を生成するためのライブラリで、各種設定(たとえば、OS によって利用するライブラリを変えるなど)を行います。今回は、とくに設定はない。 334 | * 設定後(今回はない)、`create_makefile('array_second')` と書いておく。 335 | 3. `array_second.c` を作成する 336 | * 最初に `#include ` を記載する。 337 | * このファイルには、(1) メソッドの実体と、(2) `Array` クラスへの初期化を書いておく。 338 | * (1) は、上記 `ary_second()` 関数とまったく同じ。 339 | * (2) は、`Init_array_second()` 関数内で、`rb_define_method()` を利用する。`Init_array_second` という名前は、`create_makefile` で指定した名前から自動的に決まる。 340 | 4. `$ ruby extconf.rb` を実行して、Makefile を生成する。 341 | 5. `$ make` を実行し、`array_second.so` をビルドする。できれば、これを `require` で使うことができる。例えば、`$ ruby -r ./array_second -e 'p [1, 2].second'` は 2 を出力する。 342 | 6. `$ make install` とすれば、インストールディレクトリに .so がコピーされる。 343 | 344 | 4 で起動した ruby 用の拡張ライブラリを作成します。前節までで拡張していた Ruby 用に拡張ライブラリを作成するためには、`workdir/install/bin/ruby extconf.rb` のように、インストールした Ruby インタプリタを指定してください。 345 | 346 | `array_second` は、このリポジトリにも存在するので参照してください。 347 | 348 | `extconf.rb` や別途ビルド・インストールのくだりを除けば Ruby の組み込みメソッドは拡張ライブラリと記述方法がまったく同じです。 349 | 350 | 拡張ライブラリを配布するためには 2, 3 で作成したファイルをまとめて配布します。ただ、RubyGems パッケージとして配布する方が利用者には便利でしょう。 351 | 352 | ## Tips: 開発のヒント 353 | 354 | https://docs.ruby-lang.org/en/2.4.0/extension_ja_rdoc.html に詳細説明があるのでチェックしましょう。 355 | 356 | MRI のソースコードを検索し、似たようなことをやっているメソッドを探しましょう。 357 | 358 | Ruby プログラムを書くときは、`p(obj)` メソッドを利用することがあると思います。 359 | C では `rb_p(obj)` とすることで、同様に出力することができます。 360 | 361 | gdb が使えるようでしたら、ブレイクポイントを指定して `$ make gdb` を使って実行すると、処理を確認することができます(`./miniruby $(srcdir)/test.rb` を gdb 上で実行します)。このとき、C のファイル中で `#include "vm_debug.h"` とすることで、`bp()` というマクロが使えるようになります。この `bp()` が埋め込まれたところはブレイクポイントとして最初から登録されているため、気になるところに `bp()` を置くと便利かもしれません(つまり、`binding.pry` のように使えます)。 362 | 363 | gdb では、`p expr` とすることで、`expr` の値を示すことができます(例えば、変数 `foo` の値を表示したいときは、`p foo`)。`VALUE obj` の値を表示すると、`obj` のクラスにかかわらず、数字が表示されます。これは見づらいので、`rp` という gdb 用のコマンドが定義されています(`ruby/.gdbinit` で定義)。このコマンドを使うと、見やすく整形して出力してくれます。 364 | 365 | ## 発展演習 366 | 367 | 次のトピックを、実際に解決してみてください。似たような実装を MRI のソースコードを grep して探してみてください。 368 | 369 | * 引き算を行う `Integer#sub(n)` を実装してみてください。 370 | * `Array#second` は、要素数が 1 個以下の場合は `nil` を返します。というのも、`rb_ary_entry()` は、存在しない要素インデックスが指定されると `nil`(`Qnil`)を返すためです。そこで、2要素目がない場合は例外を発生するようにしてみてください。`rb_raise()` という関数を利用します。 371 | * `String#palindrome?` は、非効率な実装になっています。どこが非効率であり、どのように解決できるか検討してみてください。また、可能なら性能を改善するように実装を変更してみてください。 372 | * `Time#day_before` は名前が微妙です。良い名前を考えてみてください。 373 | * MRI にいたずらしてみましょう。 374 | * `p` メソッドの出力を、ちょっと変えてみましょう。例えば、`p true` の結果を `p> true` としてみるのはどうでしょうか。 375 | * 起動したら、すぐに終了するようにしてみましょう。 376 | * GC の処理が行われたら、何か表示するようにしてみましょう。 377 | * `Integer#+` の結果を、足し算ではなく、引き算した結果になるようにしてください。git の新しいブランチで実行するといいですよ。 378 | * 実は(高速化のための複雑化のために)とっても難しいです。 379 | * `numeric.c` に加えて、`insns.def` というファイルも見て下さい。 380 | * 想像力をたくましくして、好きなメソッドを追加してみましょう。 381 | 382 | 次の章で扱いますが、 383 | 384 | * `Time#day_before` の実装の問題点を `Integer#add(n)` と同様に考えてみてください。 385 | * `Integer#add(n)` にはバグがあると言いました。どのようなバグがあるでしょうか。また、どのように解決できるでしょうか。 386 | * まずは失敗するテストを書きましょう。 387 | * 問題を解決し、テストが通ることを確認しましょう。 388 | -------------------------------------------------------------------------------- /JA/4_bug.md: -------------------------------------------------------------------------------- 1 | # (4) バグの修正 2 | 3 | ## この資料について 4 | 5 | MRI の変更の大部分は、バグの修正になります。 6 | 本稿では、MRI のバグ修正をどのように行っていくのか、仮想のバグ報告をもとに、ご紹介していきます。 7 | バグ報告を受ける方、バグ報告をする方の2つの視点から説明します。 8 | 9 | ## `Kernel#hello(name)`(他の人のバグ報告を見る場合) 10 | 11 | ### `Kernel#hello(name)` の実装 12 | 13 | まずは、MRI にメソッドを追加する復習です。`hello` という関数っぽいメソッドを定義してみましょう。 14 | `p` メソッドのように `Kernel` に定義し、private メソッドにしておきましょう。 15 | 16 | このメソッドは、`"Hello #{name}\n"` という文字列を出力します。 17 | 18 | Ruby で実装すると、こんな感じです。 19 | 20 | ```ruby 21 | def hello name 22 | puts "Hello #{name}" 23 | end 24 | 25 | hello 'ko1' #=> "Hello ko1" と出力 26 | ``` 27 | 28 | これを、C で書き直し、MRI に埋め込みましょう。 29 | `rb_define_global_function()` を使うことで、`Kernel#hello` という private メソッドを作ることができます。 30 | 31 | ```diff 32 | Index: io.c 33 | =================================================================== 34 | --- io.c (リビジョン 59647) 35 | +++ io.c (作業コピー) 36 | @@ -12327,6 +12327,14 @@ 37 | } 38 | } 39 | 40 | +static VALUE 41 | +f_hello(VALUE self, VALUE name) 42 | +{ 43 | + const char *name_ptr = RSTRING_PTR(name); 44 | + fprintf(stdout, "Hello %s\n", name_ptr); 45 | + return Qnil; 46 | +} 47 | + 48 | /* 49 | * Document-class: IOError 50 | * 51 | @@ -12530,6 +12538,8 @@ 52 | rb_define_global_function("p", rb_f_p, -1); 53 | rb_define_method(rb_mKernel, "display", rb_obj_display, -1); 54 | 55 | + rb_define_global_function("hello", f_hello, 1); 56 | + 57 | rb_cIO = rb_define_class("IO", rb_cObject); 58 | rb_include_module(rb_cIO, rb_mEnumerable); 59 | 60 | ``` 61 | 62 | ポイントは `RSTRING_PTR(name)` で、文字列オブジェクトから C 文字列のポインタを得ることができます。 63 | 64 | `test.rb` にサンプルコードを記述し、`$ make run` を用いて実行してみましょう。ちゃんと動きましたか? 多分、動いてるんじゃないかと思います。 65 | 66 | ### バグ報告 67 | 68 | `hello()` メソッドを含めて Ruby の最新バージョン(例えば、Ruby 2.4.0)がリリースされたと考えてください。 69 | このメソッドが世界中で大人気になり、多くのユーザーが `hello()` を使ったとします。 70 | 多くのユーザーが使っていると、不具合も見つかるもので、ある日 Redmine のほうに、次のようなバグ報告がきました。 71 | 72 | ``` 73 | My script causes SEGV. 74 | 75 | See attached log for details. 76 | ``` 77 | 78 | 添付されていたログには次のように書かれていました。 79 | 80 | ``` 81 | ../../trunk/test.rb:2: [BUG] Segmentation fault at 0x0000000000000008 82 | ruby 2.5.0dev (2017-08-23 trunk 59647) [x86_64-linux] 83 | 84 | -- Control frame information ----------------------------------------------- 85 | c:0003 p:---- s:0011 e:000010 CFUNC :hello 86 | c:0002 p:0007 s:0006 e:000005 EVAL ../../trunk/test.rb:2 [FINISH] 87 | c:0001 p:0000 s:0003 E:000b00 (none) [FINISH] 88 | 89 | -- Ruby level backtrace information ---------------------------------------- 90 | ../../trunk/test.rb:2:in `
' 91 | ../../trunk/test.rb:2:in `hello' 92 | 93 | -- Machine register context ------------------------------------------------ 94 | RIP: 0x00000000004c17f4 RBP: 0x0000000000df5430 RSP: 0x00007fff031d4680 95 | RAX: 0x0000000000000000 RBX: 0x00002ba4beccefb0 RCX: 0x00002ba4bebcf048 96 | RDX: 0x00000000004c17f0 RDI: 0x0000000000e562d0 RSI: 0x0000000000000008 97 | R8: 0x00002ba4bebcf068 R9: 0x00002ba4beccef80 R10: 0x0000000000000000 98 | R11: 0x0000000000000001 R12: 0x00002ba4beccefb0 R13: 0x0000000000e1d4f8 99 | R14: 0x0000000000ec78f0 R15: 0x0000000000e562d0 EFL: 0x0000000000010202 100 | 101 | -- C level backtrace information ------------------------------------------- 102 | /mnt/sdb1/ruby/build/trunk/miniruby(rb_vm_bugreport+0x528) [0x61a088] ../../trunk/vm_dump.c:671 103 | /mnt/sdb1/ruby/build/trunk/miniruby(rb_bug_context+0xd0) [0x4939c0] ../../trunk/error.c:539 104 | /mnt/sdb1/ruby/build/trunk/miniruby(sigsegv+0x42) [0x58a622] ../../trunk/signal.c:932 105 | /lib/x86_64-linux-gnu/libpthread.so.0 [0x2ba4bedc7330] 106 | /mnt/sdb1/ruby/build/trunk/miniruby(f_hello+0x4) [0x4c17f4] ../../trunk/io.c:12332 107 | /mnt/sdb1/ruby/build/trunk/miniruby(vm_call_cfunc+0x12d) [0x5fbe6d] ../../trunk/vm_insnhelper.c:1903 108 | /mnt/sdb1/ruby/build/trunk/miniruby(vm_call_method+0xf7) [0x60d417] ../../trunk/vm_insnhelper.c:2364 109 | /mnt/sdb1/ruby/build/trunk/miniruby(vm_exec_core+0x2051) [0x607a01] ../../trunk/insns.def:854 110 | /mnt/sdb1/ruby/build/trunk/miniruby(vm_exec+0x98) [0x60b7f8] ../../trunk/vm.c:1793 111 | /mnt/sdb1/ruby/build/trunk/miniruby(ruby_exec_internal+0xb2) [0x499a02] ../../trunk/eval.c:246 112 | /mnt/sdb1/ruby/build/trunk/miniruby(ruby_exec_node+0x1d) [0x49b78d] ../../trunk/eval.c:310 113 | /mnt/sdb1/ruby/build/trunk/miniruby(ruby_run_node+0x1c) [0x49dffc] ../../trunk/eval.c:302 114 | /mnt/sdb1/ruby/build/trunk/miniruby(main+0x5f) [0x41a61f] ../../trunk/main.c:42 115 | 116 | -- Other runtime information ----------------------------------------------- 117 | 118 | * Loaded script: ../../trunk/test.rb 119 | 120 | * Loaded features: 121 | 122 | 0 enumerator.so 123 | 1 thread.rb 124 | 2 rational.so 125 | 3 complex.so 126 | 127 | * Process memory map: 128 | 129 | 00400000-006f7000 r-xp 00000000 08:11 262436 /mnt/sdb1/ruby/build/trunk/miniruby 130 | 008f6000-008fb000 r--p 002f6000 08:11 262436 /mnt/sdb1/ruby/build/trunk/miniruby 131 | 008fb000-008fc000 rw-p 002fb000 08:11 262436 /mnt/sdb1/ruby/build/trunk/miniruby 132 | 008fc000-0090e000 rw-p 00000000 00:00 0 133 | 00df4000-00f2c000 rw-p 00000000 00:00 0 [heap] 134 | 2ba4beb92000-2ba4bebb5000 r-xp 00000000 08:01 310550 /lib/x86_64-linux-gnu/ld-2.19.so 135 | 2ba4bebb5000-2ba4bebb7000 rw-p 00000000 00:00 0 136 | 2ba4bebb7000-2ba4bebb8000 ---p 00000000 00:00 0 137 | 2ba4bebb8000-2ba4bebbb000 rw-p 00000000 00:00 0 [stack:13848] 138 | 2ba4bebbb000-2ba4bebc2000 r--s 00000000 08:01 216019 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache 139 | 2ba4bebc2000-2ba4bebc3000 rw-p 00000000 00:00 0 140 | 2ba4bebcb000-2ba4becd0000 rw-p 00000000 00:00 0 141 | 2ba4becd0000-2ba4becf3000 r--s 00000000 08:01 309830 /lib/x86_64-linux-gnu/libpthread-2.19.so 142 | 2ba4becf3000-2ba4bed9a000 r--s 00000000 08:01 6641 /usr/lib/debug/lib/x86_64-linux-gnu/libpthread-2.19.so 143 | 2ba4bedb4000-2ba4bedb5000 r--p 00022000 08:01 310550 /lib/x86_64-linux-gnu/ld-2.19.so 144 | 2ba4bedb5000-2ba4bedb6000 rw-p 00023000 08:01 310550 /lib/x86_64-linux-gnu/ld-2.19.so 145 | 2ba4bedb6000-2ba4bedb7000 rw-p 00000000 00:00 0 146 | 2ba4bedb7000-2ba4bedd0000 r-xp 00000000 08:01 309830 /lib/x86_64-linux-gnu/libpthread-2.19.so 147 | 2ba4bedd0000-2ba4befcf000 ---p 00019000 08:01 309830 /lib/x86_64-linux-gnu/libpthread-2.19.so 148 | 2ba4befcf000-2ba4befd0000 r--p 00018000 08:01 309830 /lib/x86_64-linux-gnu/libpthread-2.19.so 149 | 2ba4befd0000-2ba4befd1000 rw-p 00019000 08:01 309830 /lib/x86_64-linux-gnu/libpthread-2.19.so 150 | 2ba4befd1000-2ba4befd5000 rw-p 00000000 00:00 0 151 | 2ba4befd5000-2ba4befd8000 r-xp 00000000 08:01 309835 /lib/x86_64-linux-gnu/libdl-2.19.so 152 | 2ba4befd8000-2ba4bf1d7000 ---p 00003000 08:01 309835 /lib/x86_64-linux-gnu/libdl-2.19.so 153 | 2ba4bf1d7000-2ba4bf1d8000 r--p 00002000 08:01 309835 /lib/x86_64-linux-gnu/libdl-2.19.so 154 | 2ba4bf1d8000-2ba4bf1d9000 rw-p 00003000 08:01 309835 /lib/x86_64-linux-gnu/libdl-2.19.so 155 | 2ba4bf1d9000-2ba4bf1e2000 r-xp 00000000 08:01 309837 /lib/x86_64-linux-gnu/libcrypt-2.19.so 156 | 2ba4bf1e2000-2ba4bf3e2000 ---p 00009000 08:01 309837 /lib/x86_64-linux-gnu/libcrypt-2.19.so 157 | 2ba4bf3e2000-2ba4bf3e3000 r--p 00009000 08:01 309837 /lib/x86_64-linux-gnu/libcrypt-2.19.so 158 | 2ba4bf3e3000-2ba4bf3e4000 rw-p 0000a000 08:01 309837 /lib/x86_64-linux-gnu/libcrypt-2.19.so 159 | 2ba4bf3e4000-2ba4bf412000 rw-p 00000000 00:00 0 160 | 2ba4bf412000-2ba4bf517000 r-xp 00000000 08:01 309816 /lib/x86_64-linux-gnu/libm-2.19.so 161 | 2ba4bf517000-2ba4bf716000 ---p 00105000 08:01 309816 /lib/x86_64-linux-gnu/libm-2.19.so 162 | 2ba4bf716000-2ba4bf717000 r--p 00104000 08:01 309816 /lib/x86_64-linux-gnu/libm-2.19.so 163 | 2ba4bf717000-2ba4bf718000 rw-p 00105000 08:01 309816 /lib/x86_64-linux-gnu/libm-2.19.so 164 | 2ba4bf718000-2ba4bf8d6000 r-xp 00000000 08:01 309818 /lib/x86_64-linux-gnu/libc-2.19.so 165 | 2ba4bf8d6000-2ba4bfad6000 ---p 001be000 08:01 309818 /lib/x86_64-linux-gnu/libc-2.19.so 166 | 2ba4bfad6000-2ba4bfada000 r--p 001be000 08:01 309818 /lib/x86_64-linux-gnu/libc-2.19.so 167 | 2ba4bfada000-2ba4bfadc000 rw-p 001c2000 08:01 309818 /lib/x86_64-linux-gnu/libc-2.19.so 168 | 2ba4bfadc000-2ba4bfae1000 rw-p 00000000 00:00 0 169 | 2ba4bfae1000-2ba4bfdaa000 r--p 00000000 08:01 376 /usr/lib/locale/locale-archive 170 | 2ba4bfdaa000-2ba4bfdc0000 r-xp 00000000 08:01 266058 /lib/x86_64-linux-gnu/libgcc_s.so.1 171 | 2ba4bfdc0000-2ba4bffbf000 ---p 00016000 08:01 266058 /lib/x86_64-linux-gnu/libgcc_s.so.1 172 | 2ba4bffbf000-2ba4bffc0000 rw-p 00015000 08:01 266058 /lib/x86_64-linux-gnu/libgcc_s.so.1 173 | 2ba4bffc0000-2ba4c0f1c000 r--s 00000000 08:11 262436 /mnt/sdb1/ruby/build/trunk/miniruby 174 | 2ba4c0f1c000-2ba4c10e2000 r--s 00000000 08:01 309818 /lib/x86_64-linux-gnu/libc-2.19.so 175 | 7fff031b5000-7fff031d6000 rw-p 00000000 00:00 0 176 | 7fff031e5000-7fff031e7000 r-xp 00000000 00:00 0 [vdso] 177 | ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] 178 | 179 | 180 | [NOTE] 181 | You may have encountered a bug in the Ruby interpreter or extension libraries. 182 | Bug reports are welcome. 183 | For details: http://www.ruby-lang.org/bugreport.html 184 | 185 | make: *** [run] Aborted (core dumped) 186 | ``` 187 | 188 | このバグ報告には次の点が欠けています。 189 | 190 | * 再現コード 191 | * 実行環境 192 | 193 | ただ、添付されたログファイルを見ると、`ruby 2.5.0dev (2017-08-23 trunk 59647) [x86_64-linux]` と書いているので、Linux 環境で Ruby 2.5.0dev (開発版)を使っているのだな、ということがわかります。 194 | 195 | 再現コードがないのはよくないので、「再現コードをください」と返事をすることにしました(この添付のログを読むと、実は手がかりがたくさんあるので、この程度だったらすぐに治るのですが、今回はわからなかったとします)。 196 | 197 | ``` 198 | Please send us your reproducible code. Small code is awesome. 199 | ``` 200 | 201 | 問題が起きたプログラムが社外秘の Rails アプリケーションなどですと、そのソースコードをそのまま送るわけにはいきません。 202 | また、「時々落ちる」といった問題は、再現コードを作るのは難しいです。 203 | 204 | 今回も、実は大きなアプリケーションの一部だったと仮定し、相手からも「Sorry we can't make such repro」といった返事が来たとします。 205 | 206 | そこで、しょうが無いので自分でデバッグを開始することにしました。 207 | 208 | ### `[BUG]` の見方 209 | 210 | `[BUG]` は、MRI に何か問題が起こったときに生じます。基本的には、インタプリタのバグになります。 211 | 212 | ``` 213 | ../../trunk/test.rb:2: [BUG] Segmentation fault at 0x0000000000000008 214 | ``` 215 | 216 | まず、行頭ですが、これは `../../trunk/test.rb:2` という場所を実行中に何か問題が生じた、ということを示しています。 217 | 次に、`Segmentation fault at 0x0000000000000008` は、`[BUG]` の原因です。この場合、0x0000000000000008 番地へのメモリの読み書きで、Segmentation fault が発生した、という意味になります。一般的には、読み書き禁止領域に対する読み書きにおいて生じます。「せぐぶ」とか、「せぐふぉ」とか略されるもので、C プログラムでバグがあると、比較的多く見ることができます。 218 | 219 | 次の `ruby 2.5.0dev (2017-08-23 trunk 59647) [x86_64-linux]` で、`ruby -v` で得られるバージョン番号(および実行環境)が書いてあります。 220 | 221 | ``` 222 | -- Control frame information ----------------------------------------------- 223 | c:0003 p:---- s:0011 e:000010 CFUNC :hello 224 | c:0002 p:0007 s:0006 e:000005 EVAL ../../trunk/test.rb:2 [FINISH] 225 | c:0001 p:0000 s:0003 E:000b00 (none) [FINISH] 226 | ``` 227 | 228 | このブロックでは、「Control frame information」と書いてあるとおり、Ruby の VM の制御フレーム情報が記述されています。 229 | ここで表示される内容は、VM の内部構造に強く依存するため、VM デバッグ以外では使いませんが、各行には次の内容が含まれています。 230 | 231 | * `c`: フレーム番号(cf インデックス) 232 | * `p`: プログラムカウンタ 233 | * `s`: スタックの深さ 234 | * `e`: 環境ポインタ(ep)の値(スタックの深さ、もしくは heap に確保した環境のアドレス) 235 | * フレームタイプ。`EVAL` は `eval` で積んだフレーム、`CFUNC` は C で実装されたメソッド 236 | * フレームの場所。Ruby レベルならファイル名と行番号、C 関数ならメソッド名 237 | 238 | 最後のカラムを見ると、いわゆる普通のバックトレース情報が書いてあります。 239 | 240 | ``` 241 | -- Ruby level backtrace information ---------------------------------------- 242 | ../../trunk/test.rb:2:in `
' 243 | ../../trunk/test.rb:2:in `hello' 244 | ``` 245 | 246 | このブロックは、「Ruby level backtrace information」、つまり Ruby で通常得ることができるバックトレースの情報です。 247 | 248 | ``` 249 | -- Machine register context ------------------------------------------------ 250 | RIP: 0x00000000004c17f4 RBP: 0x0000000000df5430 RSP: 0x00007fff031d4680 251 | RAX: 0x0000000000000000 RBX: 0x00002ba4beccefb0 RCX: 0x00002ba4bebcf048 252 | RDX: 0x00000000004c17f0 RDI: 0x0000000000e562d0 RSI: 0x0000000000000008 253 | R8: 0x00002ba4bebcf068 R9: 0x00002ba4beccef80 R10: 0x0000000000000000 254 | R11: 0x0000000000000001 R12: 0x00002ba4beccefb0 R13: 0x0000000000e1d4f8 255 | R14: 0x0000000000ec78f0 R15: 0x0000000000e562d0 EFL: 0x0000000000010202 256 | ``` 257 | 258 | このあたりから環境に依存した話になってきますが、「Machine register context」つまり CPU レジスタの情報です。 259 | 260 | ``` 261 | -- C level backtrace information ------------------------------------------- 262 | /mnt/sdb1/ruby/build/trunk/miniruby(rb_vm_bugreport+0x528) [0x61a088] ../../trunk/vm_dump.c:671 263 | /mnt/sdb1/ruby/build/trunk/miniruby(rb_bug_context+0xd0) [0x4939c0] ../../trunk/error.c:539 264 | /mnt/sdb1/ruby/build/trunk/miniruby(sigsegv+0x42) [0x58a622] ../../trunk/signal.c:932 265 | /lib/x86_64-linux-gnu/libpthread.so.0 [0x2ba4bedc7330] 266 | /mnt/sdb1/ruby/build/trunk/miniruby(f_hello+0x4) [0x4c17f4] ../../trunk/io.c:12332 267 | /mnt/sdb1/ruby/build/trunk/miniruby(vm_call_cfunc+0x12d) [0x5fbe6d] ../../trunk/vm_insnhelper.c:1903 268 | /mnt/sdb1/ruby/build/trunk/miniruby(vm_call_method+0xf7) [0x60d417] ../../trunk/vm_insnhelper.c:2364 269 | /mnt/sdb1/ruby/build/trunk/miniruby(vm_exec_core+0x2051) [0x607a01] ../../trunk/insns.def:854 270 | /mnt/sdb1/ruby/build/trunk/miniruby(vm_exec+0x98) [0x60b7f8] ../../trunk/vm.c:1793 271 | /mnt/sdb1/ruby/build/trunk/miniruby(ruby_exec_internal+0xb2) [0x499a02] ../../trunk/eval.c:246 272 | /mnt/sdb1/ruby/build/trunk/miniruby(ruby_exec_node+0x1d) [0x49b78d] ../../trunk/eval.c:310 273 | /mnt/sdb1/ruby/build/trunk/miniruby(ruby_run_node+0x1c) [0x49dffc] ../../trunk/eval.c:302 274 | /mnt/sdb1/ruby/build/trunk/miniruby(main+0x5f) [0x41a61f] ../../trunk/main.c:42 275 | ``` 276 | 277 | は、C レベルでのバックトレースです。OS などによって、とれたり取れなかったり、別のファイルに保存されていることを示すメッセージを表示することがあります。 278 | 279 | ``` 280 | * Loaded script: ../../trunk/test.rb 281 | ``` 282 | 283 | この行は、どのファイルを ruby コマンドに渡したかを示しています。 284 | 285 | ``` 286 | * Loaded features: 287 | 288 | 0 enumerator.so 289 | 1 thread.rb 290 | 2 rational.so 291 | 3 complex.so 292 | ``` 293 | 294 | では、どのファイルを `require` などでロードしているかを示しています(`$LOADED_FEATURES` の内容です)。 295 | この例では、ファイルがほとんどありませんが、Ruby on Rails アプリケーションなどではたくさんの Gem を利用しているため、この行が数千行になることがあります。 296 | 297 | ``` 298 | * Process memory map: 299 | 300 | 00400000-006f7000 r-xp 00000000 08:11 262436 /mnt/sdb1/ruby/build/trunk/miniruby 301 | 008f6000-008fb000 r--p 002f6000 08:11 262436 /mnt/sdb1/ruby/build/trunk/miniruby 302 | 008fb000-008fc000 rw-p 002fb000 08:11 262436 /mnt/sdb1/ruby/build/trunk/miniruby 303 | 008fc000-0090e000 rw-p 00000000 00:00 0 304 | 00df4000-00f2c000 rw-p 00000000 00:00 0 [heap] 305 | 2ba4beb92000-2ba4bebb5000 r-xp 00000000 08:01 310550 /lib/x86_64-linux-gnu/ld-2.19.so 306 | 2ba4bebb5000-2ba4bebb7000 rw-p 00000000 00:00 0 307 | 2ba4bebb7000-2ba4bebb8000 ---p 00000000 00:00 0 308 | 2ba4bebb8000-2ba4bebbb000 rw-p 00000000 00:00 0 [stack:13848] 309 | 2ba4bebbb000-2ba4bebc2000 r--s 00000000 08:01 216019 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache 310 | 2ba4bebc2000-2ba4bebc3000 rw-p 00000000 00:00 0 311 | 2ba4bebcb000-2ba4becd0000 rw-p 00000000 00:00 0 312 | 2ba4becd0000-2ba4becf3000 r--s 00000000 08:01 309830 /lib/x86_64-linux-gnu/libpthread-2.19.so 313 | 2ba4becf3000-2ba4bed9a000 r--s 00000000 08:01 6641 /usr/lib/debug/lib/x86_64-linux-gnu/libpthread-2.19.so 314 | 2ba4bedb4000-2ba4bedb5000 r--p 00022000 08:01 310550 /lib/x86_64-linux-gnu/ld-2.19.so 315 | 2ba4bedb5000-2ba4bedb6000 rw-p 00023000 08:01 310550 /lib/x86_64-linux-gnu/ld-2.19.so 316 | 2ba4bedb6000-2ba4bedb7000 rw-p 00000000 00:00 0 317 | 2ba4bedb7000-2ba4bedd0000 r-xp 00000000 08:01 309830 /lib/x86_64-linux-gnu/libpthread-2.19.so 318 | 2ba4bedd0000-2ba4befcf000 ---p 00019000 08:01 309830 /lib/x86_64-linux-gnu/libpthread-2.19.so 319 | 2ba4befcf000-2ba4befd0000 r--p 00018000 08:01 309830 /lib/x86_64-linux-gnu/libpthread-2.19.so 320 | 2ba4befd0000-2ba4befd1000 rw-p 00019000 08:01 309830 /lib/x86_64-linux-gnu/libpthread-2.19.so 321 | 2ba4befd1000-2ba4befd5000 rw-p 00000000 00:00 0 322 | 2ba4befd5000-2ba4befd8000 r-xp 00000000 08:01 309835 /lib/x86_64-linux-gnu/libdl-2.19.so 323 | 2ba4befd8000-2ba4bf1d7000 ---p 00003000 08:01 309835 /lib/x86_64-linux-gnu/libdl-2.19.so 324 | 2ba4bf1d7000-2ba4bf1d8000 r--p 00002000 08:01 309835 /lib/x86_64-linux-gnu/libdl-2.19.so 325 | 2ba4bf1d8000-2ba4bf1d9000 rw-p 00003000 08:01 309835 /lib/x86_64-linux-gnu/libdl-2.19.so 326 | 2ba4bf1d9000-2ba4bf1e2000 r-xp 00000000 08:01 309837 /lib/x86_64-linux-gnu/libcrypt-2.19.so 327 | 2ba4bf1e2000-2ba4bf3e2000 ---p 00009000 08:01 309837 /lib/x86_64-linux-gnu/libcrypt-2.19.so 328 | 2ba4bf3e2000-2ba4bf3e3000 r--p 00009000 08:01 309837 /lib/x86_64-linux-gnu/libcrypt-2.19.so 329 | 2ba4bf3e3000-2ba4bf3e4000 rw-p 0000a000 08:01 309837 /lib/x86_64-linux-gnu/libcrypt-2.19.so 330 | 2ba4bf3e4000-2ba4bf412000 rw-p 00000000 00:00 0 331 | 2ba4bf412000-2ba4bf517000 r-xp 00000000 08:01 309816 /lib/x86_64-linux-gnu/libm-2.19.so 332 | 2ba4bf517000-2ba4bf716000 ---p 00105000 08:01 309816 /lib/x86_64-linux-gnu/libm-2.19.so 333 | 2ba4bf716000-2ba4bf717000 r--p 00104000 08:01 309816 /lib/x86_64-linux-gnu/libm-2.19.so 334 | 2ba4bf717000-2ba4bf718000 rw-p 00105000 08:01 309816 /lib/x86_64-linux-gnu/libm-2.19.so 335 | 2ba4bf718000-2ba4bf8d6000 r-xp 00000000 08:01 309818 /lib/x86_64-linux-gnu/libc-2.19.so 336 | 2ba4bf8d6000-2ba4bfad6000 ---p 001be000 08:01 309818 /lib/x86_64-linux-gnu/libc-2.19.so 337 | 2ba4bfad6000-2ba4bfada000 r--p 001be000 08:01 309818 /lib/x86_64-linux-gnu/libc-2.19.so 338 | 2ba4bfada000-2ba4bfadc000 rw-p 001c2000 08:01 309818 /lib/x86_64-linux-gnu/libc-2.19.so 339 | 2ba4bfadc000-2ba4bfae1000 rw-p 00000000 00:00 0 340 | 2ba4bfae1000-2ba4bfdaa000 r--p 00000000 08:01 376 /usr/lib/locale/locale-archive 341 | 2ba4bfdaa000-2ba4bfdc0000 r-xp 00000000 08:01 266058 /lib/x86_64-linux-gnu/libgcc_s.so.1 342 | 2ba4bfdc0000-2ba4bffbf000 ---p 00016000 08:01 266058 /lib/x86_64-linux-gnu/libgcc_s.so.1 343 | 2ba4bffbf000-2ba4bffc0000 rw-p 00015000 08:01 266058 /lib/x86_64-linux-gnu/libgcc_s.so.1 344 | 2ba4bffc0000-2ba4c0f1c000 r--s 00000000 08:11 262436 /mnt/sdb1/ruby/build/trunk/miniruby 345 | 2ba4c0f1c000-2ba4c10e2000 r--s 00000000 08:01 309818 /lib/x86_64-linux-gnu/libc-2.19.so 346 | 7fff031b5000-7fff031d6000 rw-p 00000000 00:00 0 347 | 7fff031e5000-7fff031e7000 r-xp 00000000 00:00 0 [vdso] 348 | ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] 349 | ``` 350 | 351 | これは、多分 Linux 上だけじゃないかと思いますが、OS が管理するプロセスのメモリマップを示しています。`/proc/self/maps` で出てくる内容と同じです。 352 | 353 | デバッグするとき、特に注目するべきはバックトレース情報です。どうやら、Control frame information によると、`hello` メソッド実行時に問題が起こっていることがわかります。 354 | 355 | そこで、`hello` の実装を、もう一度じっと見直してみましょう。 356 | 357 | > NOTE: 問題箇所がバックトレースに現れるバグは、比較的簡単なバグです。難しいバグになると、例えばある箇所でバグによりデータが壊れ、プログラムがある程度進んだ後にその情報を参照した箇所で `[BUG]` になる、といったケースですと、バグの箇所がわからない、ということが起きます。VM や GC のバグなんかは、こういうのが多いです(困ります)。 358 | 359 | ### `f_hello()` 関数の見直し 360 | 361 | `hello` メソッドの実体は次の C 関数でした。 362 | 363 | ``` 364 | static VALUE 365 | f_hello(VALUE self, VALUE name) 366 | { 367 | const char *name_ptr = RSTRING_PTR(name); 368 | fprintf(stdout, "Hello %s\n", name_ptr); 369 | return Qnil; 370 | } 371 | ``` 372 | 373 | じっと見ますと、`name` で渡ってきた引数に対して `RSTRING_PTR()` を使っています。 374 | `RSTRING_PTR()` は、文字列オブジェクト(`T_STRING` と型付けされたオブジェクト)にのみ有効なマクロであり、その他のオブジェクトには対応しません(何が起きるか保証されていません)。 375 | 多分、これが原因なんじゃないでしょうか。 376 | 377 | > NOTE: 「じっと見る」ことで問題がわかるかは、MRI の内部構造をどの程度把握しているかによります。今回は規模が小さいため簡単ですが、通常はもっと難しいです。 378 | 379 | では、仮説を検証するために、`hello(nil)` とでもしてみましょう。同様の `[BUG]` が出力されたのではないかと思います。 380 | 381 | そこで、記録のために、チケットに再現コードをコメントしておきましょう。 382 | 383 | ``` 384 | The following code can reproduce this issue: 385 | 386 | hello(nil) 387 | ``` 388 | 389 | これだけ小さな再現コードがあれば、あとは得意な人に任せても問題ないと思います。 390 | 今回は、せっかくなので、パッチの作成まで行いましょう。 391 | 392 | ### gdb を用いたデバッグ 393 | 394 | `test.rb` に `hello(nil)` と記入し、`make gdb` と実行しましょう(lldb を用いる場合は、 `make lldb` と実行した後に、 `run` を実行します)。 395 | 396 | ``` 397 | Program received signal SIGSEGV, Segmentation fault. 398 | f_hello (self=9904840, name=8) at ../../trunk/io.c:12333 399 | 12333 const char *name_ptr = RSTRING_PTR(name); 400 | ``` 401 | 402 | と出力されれば成功です。これは、`SEGV` シグナルをうけたため、`gdb` がデバッグ対象プログラムを一時停止した、という意味です。 403 | 404 | ちょっと、`name` に何が入っているか確認してみましょう。 405 | 406 | ``` 407 | (gdb) p name 408 | $1 = 8 409 | (gdb) rp name 410 | nil 411 | ``` 412 | 413 | `p name` によって、`name` の値(数値)が 8 であることがわかります。が、8 だけだとよくわかりません。 414 | `rp name` によって、その 8 という値が `nil` であることがわかります。 415 | 416 | SEGV によって停止した場所は io.c:12333 であり、`const char *name_ptr = RSTRING_PTR(name);` という行で起こっていることがわかります。 417 | どうやら、「`RSTRING_PTR()`が問題だ」という仮説は正しかったようです。 418 | 419 | この問題を解決するためには、`name` が `String` オブジェクトであることをチェックしなければなりません。 420 | 違う場合はどうしましょうか。型が合わない、と例外をあげてもいいですね。 421 | ただ、Ruby の場合、`to_str` を持っていれば、これを呼んで文字列オブジェクトに変換し、それを扱うという慣習があります。そこもサポートしておきたいところです。 422 | ただ(まだあるのか)、`to_str` の結果が `String` オブジェクトじゃなかったらどうしましょう。この場合は例外でいいですね。 423 | 424 | いちいちこのような処理を書くのは面倒ですし、頻出する操作なので、MRI は、これらのチェック(や、必要なら変換)を行うマクロ `StringValueCStr()` が用意されています。 425 | 426 | あるオブジェクトから C の文字列ポインタをとるには、`StringValueCStr()` を使います。 427 | 必要なら `to_str` を実行し、文字列に変換してから C の文字列ポインタに変換します。 428 | 問題が生じれば、きちんとエラーを発生させます。 429 | 430 | 早速使って修正してみましょう。 431 | 432 | ``` 433 | const char *name_ptr = StringValueCStr(name); 434 | ``` 435 | 436 | このように修正し、もう一度 `$ make gdb` を実行してみましょう。 437 | 438 | ``` 439 | ko1@u64:~/ruby/build/trunk$ make gdb 440 | compiling ../../trunk/io.c 441 | linking miniruby 442 | gdb -x run.gdb --quiet --args ./miniruby -I../../trunk/lib -I. -I.ext/common ../../trunk/test.rb 443 | Reading symbols from ./miniruby...done. 444 | Breakpoint 1 at 0x474900: file ../../trunk/debug.c, line 127. 445 | warning: ../../trunk/breakpoints.gdb: No such file or directory 446 | [Thread debugging using libthread_db enabled] 447 | Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". 448 | [New Thread 0x2aaaaaad5700 (LWP 17714)] 449 | Traceback (most recent call last): 450 | 1: from ../../trunk/test.rb:2:in `
' 451 | ../../trunk/test.rb:2:in `hello': no implicit conversion of nil into String (TypeError) 452 | [Thread 0x2aaaaaad5700 (LWP 17714) exited] 453 | [Inferior 1 (process 17710) exited with code 01] 454 | ``` 455 | 456 | まず、修正したので `io.c` を再コンパイルし、修正を反映した `miniruby` を生成します。 457 | 次に、gdb 上で `test.rb` を実行しています。 458 | 459 | 実行結果は、`nil.to_str` がないため、`TypeError` が発生し、終了します。Ruby 的には例外発生で終了しましたが、MRI 的には、そのように例外発生による終了することが正常(わかりづらいですね)なので、問題ないと判断し、gdb を終了させます。 460 | 461 | では、治ったので、`f_hello()` 関数に行った修正を、チケットに反映させましょう。修正方法を伝えるのは、どのような方法でもかまいません。 462 | 463 | よくあるのは次のような方法です。 464 | 465 | * Redmine チケットに、diff を含めコメントする(diff が長ければファイルを添付する) 466 | * github で pull request を作り、その URL をチケットにコメントする 467 | 468 | やりやすい方法をお選び下さい。 469 | 470 | ### チケットその後 471 | 472 | #### そのまま放っておかれた場合 473 | 474 | チケットに修正まで提案しても、Ruby コミッタがコミットするまで、バグは修正されません。 475 | 多くの場合、中田さん(nobu)などの手の早いコミッタによって、バグ修正はすぐに取り込まれることが多いですが、いくつかの理由で取り込まれないことがあります。 476 | 477 | * 優先度が低い場合 → `hello()` なんて誰も使ってないよね、とコミッタが判断すると、面倒くさがって後回しにされることがあります。 478 | * コミッタが忙しい場合 → Ruby の品質管理に法的責任をもってあたっている人物はいないため、ほかのことに忙しければ後回しになります。 479 | * 修正が微妙な場合 → 担当コミッタが、さらに修正しなければなりません。当該人物が忙しければ、後回しになります。 480 | * 再現ができない場合 → 再現コードがない、もしくは実行してもコミッタの環境では問題が再現できない場合、修正の確認が難しく、放置しがちになります。 481 | 482 | 状況がわからない場合、チケットにコメントで催促してみましょう。また、もし Ruby コミッタに知り合いがいれば、聞いてみると話が早いかもしれません。 483 | SNS をやっている人も多いので(例えば、まつもとゆきひろさんの twitter アカウントは https://twitter.com/yukihiro_matz です)、そういう場所で聞いてもいいかもしれません。 484 | 485 | 最近毎月行っている Ruby 開発者会議で議題に挙げる方法もあります。https://bugs.ruby-lang.org/issues/14770 からたどれる(かもしれない)次回の会議のアジェンダに、議論して欲しいチケットを、その理由を含めてコメントしておいて下さい。 486 | 487 | #### バックポート 488 | 489 | さて、最新開発版(例えば、ruby 2.5.0dev)では、バグ修正が取り込まれたとします。 490 | しかし、すでにリリース済みの安定版ブランチ、例えば、Ruby 2.4.0 にバグ入りの `hello()` メソッドがあれば、Ruby 2.4.1 で修正して欲しいと思うのが人情です。 491 | 492 | このように、すでにリリース済みの安定版ブランチに修正を反映して欲しい時は redmine のチケットに、コメントでその旨伝えましょう。 493 | 494 | 安定版ブランチメンテナは Backport 欄を適切に設定して backport の必要な変更を管理します。開発版で修正が行われると、チケットは Close になります。安定版ブランチメンテナは Closed のチケットを検索して backport 対象を探すので Close のままにしておいてください。問題なければ、バックポートが行われ、安定版のバグフィックスリリースのタイミングで修正された MRI が公開されます。 495 | 496 | ## `Integer#add(n)` (バグを自分で発見してしまった場合) 497 | 498 | 先ほど実装した `Integer#add(n)` には問題があると述べました。この問題ありのコードが、Ruby の安定版としてリリースされてしまったとします(実際にも、こういう例が結構あります)。 499 | 500 | あなたは `Integer#+` よりも、`Integer#add` のほうが cool だと思ったので、たくさん使ってみたとします。ただ、`Integer#+` では起きなかった例外が、`Integer#add` では起こるようになってしまいました。 501 | 502 | ``` 503 | a = 3_000_000_000 504 | b = 2_000_000_000 505 | p a+b #=> 5000000000 506 | p a.add(b) #=> `add': integer 3000000000 too big to convert to `int' (RangeError) 507 | ``` 508 | 509 | ### バグ報告 510 | 511 | ここまでで、 512 | 513 | * どのような挙動を期待するか(`Integer#+` と同じ挙動になって欲しい) 514 | * 実際はどのようになったか(`RangeError` となった) 515 | * 小さな再現可能なコード(4 行なので十分小さい) 516 | 517 | のような情報が出揃っているため、バグ報告するのに十分な情報が集まりました。バグ報告をしましょう。 518 | 519 | ただ、その前に、同様のバグ報告がないかどうか、チェックしましょう。Redmine の検索機能を使って、例えば `Integer#add` や `RangeError` といったキーワードを用いて検索してみましょう。 520 | 521 | 検索した結果、どうやらまだ報告されていないことがわかったので、チケットを作成することにしましょう。 522 | 523 | 1. https://bugs.ruby-lang.org/projects/ruby-trunk/issues でバグ登録しますが、Redmine にアカウントがない場合は、アカウントを登録しましょう。その後、ログインします。 524 | 2.「新しいチケット」ボタンをクリックし、チケット登録画面へ映ります。 525 | 3. 「トラッカー」は "Bug" で良いです。 526 | 4. 題名は、ぱっと見てわかる名前が良いので、「Integer#add causes RangeError unexpectedly」にしましょう。日本語で登録する場合は「Integer#add が RangeError を返す」などでもいいでしょう。 527 | 5. 説明には、症状を書きましょう。後述します。 528 | 6. ステータス、担当者、対象バージョンは変えないでよいです(わかる人が付けてくれます)。 529 | 7. ruby -v には、再現した Ruby の ruby -v の結果を入力しましょう。 530 | 8. 報告先は、日本語で議論したい場合、「ruby-dev in Japanese(日本語)」、英語の場合「ruby-core in English」を選びましょう。 531 | 9. 今回は長いログなどはないため、添付ファイルは無しです。 532 | 533 | 説明部には、 534 | 535 | * Summary(問題の短いまとめ) 536 | * 再現コードと再現環境(ruby -v の結果(必須です)、OS、コンパイラなどのバージョン、その他) 537 | * 期待する挙動 538 | * 実際に得られた挙動 539 | * (可能なら)その問題を修正するためのパッチ 540 | 541 | を書くのでした。下記をコピペして使いましょう。Markdown で記述します。 542 | 543 | ~~~markdown 544 | # Summary(問題の短いまとめ) 545 | 546 | `Integer#+` で意図しない `UnexpectedError` が発生する。 547 | 548 | # 再現コードと再現環境(ruby -v の結果(必須です)、OS、コンパイラなどのバージョン、その他) 549 | 550 | 次のコードで再現します。 551 | 552 | ``` 553 | a = 3_000_000_000 554 | b = 2_000_000_000 555 | p a+b #=> 5000000000 556 | p a.add(b) #=> `add': integer 3000000000 too big to convert to `int' (RangeError) 557 | ``` 558 | 559 | 実行したのは下記の環境です。 560 | 561 | ``` 562 | $ uname -a 563 | Linux u64 3.13.0-126-generic #175-Ubuntu SMP Thu Jul 20 17:33:56 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux 564 | ``` 565 | 566 | # 期待する挙動 567 | 568 | `Integer#+` と同じ、つまり上記例では `p a+b #=> 5000000000` と同じ挙動を期待しました。 569 | 570 | # 実際に得られた挙動 571 | 572 | `p a.add(b) #=> `add': integer 3000000000 too big to convert to `int' (RangeError)` のように、`RangeError` が発生しました。 573 | ~~~ 574 | 575 | さて、ここまでくれば OK です。「作成」ボタンを押して、チケットを作成しましょう。 576 | 577 | ### 問題のある値の絞り込み 578 | 579 | チケットを登録しましたが、誰かも返信がつきません。この機能、誰も使ってないんでしょうか...(よくあります)。 580 | そのため、自分でデバッグしようと決心しました。 581 | 582 | 原因究明のためには、どのような値でエラーが出るのか確認するのが先決です。 583 | 584 | 今回は、上記コードでは `a` の値を色々いじってみましょう。どうやら、2B(20億)では OK で、3B ではエラーになるようです。 585 | そこで、2B から 3B まで、値をずらしてみましょう。 586 | 587 | ``` 588 | def trial low, high 589 | result = false 590 | while result == false && low != high 591 | break if yield low 592 | low += 1 593 | end 594 | low 595 | end 596 | 597 | point_value = trial 2_000_000_000, 3_000_000_000 do |n| 598 | # p n 599 | begin 600 | a = n 601 | b = 2_000_000_000 602 | a.add(b) 603 | rescue RangeError 604 | true 605 | else 606 | false 607 | end 608 | end 609 | 610 | p point_value 611 | ``` 612 | 613 | `trial` メソッドは、値を low から high まであげていって、そのカウンタを渡して起動したブロックの返値が false -> true に変更されたタイミングを探ります。 614 | そこで、`Integer#add()` でエラーで無ければ false、エラーなら true になるようにしたブロックを使って、エラーになる境界値を探ってみましょう。 615 | 616 | 実行してみると、どうやら、2,147,483,648 という値で false -> true になったようです。 617 | 618 | なお、この実行を行うために、手元のコンピューターでは 13 秒かかりました。繰り返しを 147,483,648 回行ったからですね(つまり、13sec / 147... = 8.8e-08 ということで、だいたい 90ns / iteration ということになります)。 619 | 今回は、たまたま 2B の近くで値を見つけることができましたが、3B に近いところに境界値があると、何倍もの時間がかることになります。 620 | 621 | そこで、二分探索を使うように `trial` を修正してみます。二分探索は、探索範囲を半分ずつに減らしていく方法です(前出の線形探索は、1/n ずつ減らしていく方法)。 622 | 623 | ``` 624 | def linear_trial low, high 625 | result = false 626 | while result == false && low != high 627 | break if yield low 628 | low += 1 629 | end 630 | low 631 | end 632 | 633 | def trial low, high, &b 634 | # binary search 635 | while high - low > 2 636 | mid = (low + high)/2 637 | result = yield mid 638 | if result 639 | high = mid 640 | else 641 | low = mid 642 | end 643 | end 644 | linear_trial mid-2, mid+1, &b # 実装で楽をするためです 645 | end 646 | ``` 647 | 648 | これを利用すると、2,147,483,648 で変わった、ということがわかり、0.20秒で実行が終了しました。計算量でいうと、O(n) が O(log n) に減ったと表現します。 649 | 650 | さて、これによって、2,147,483,647 では平気でしたが、2,147,483,648 ではエラーがでる、ということがわかります。ここまでわかったので、報告しておきましょう。 651 | 652 | ``` 653 | 調べてみると、 654 | 655 | a が 656 | 2_147_483_647 ではエラーが出ませんが、 657 | 2_147_483_648 ではエラーがでるようです。 658 | ``` 659 | 660 | これを見たあるコミッタは、すぐに状況を把握できたので、バグを修正してくれました。めでたしめでたし(いつもそうだといいですね)。 661 | 662 | ### 答え合わせ 663 | 664 | 2,147,483,648 の値を見てピンとくる人はくると思いますが、2 の 31 乗(2^31)が 2,147,483,648 です。つまり、a < 2^31 のとき、問題ない、ということがわかります。 665 | 666 | 元々のエラーメッセージは `integer 3000000000 too big to convert to `int' (RangeError)` でした。 667 | 変換しようとしている数値が大きすぎて、C の `int` 型の値に収まらないよ、と言っています。 668 | C 言語での int の範囲は -2^31 ~ 2^31-1 になりますので、この範囲を超えた、というエラーですね。 669 | 670 | 実装を確認してみましょう。 671 | 672 | ``` 673 | static VALUE 674 | int_add(VALUE self, VALUE n) 675 | { 676 | if (FIXNUM_P(self) && FIXNUM_P(n)) { 677 | /* c = a + b */ 678 | int a = FIX2INT(self); 679 | int b = FIX2INT(n); 680 | int c = a + b; 681 | VALUE result = INT2NUM(c); 682 | return result; 683 | } 684 | else { 685 | return rb_int_plus(self, n); 686 | } 687 | } 688 | ``` 689 | 690 | `FIX2INT()` をしているところがありますね。ここで、2^31 以上の大きな値が渡ってきたからエラーが出たわけですね。そこで、`long`(と、`FIX2LONG`)を使うようにしましょう。 691 | 692 | ```C 693 | static VALUE 694 | int_add(VALUE self, VALUE n) 695 | { 696 | if (FIXNUM_P(self) && FIXNUM_P(n)) { 697 | /* c = a + b */ 698 | long a = FIX2LONG(self); 699 | long b = FIX2LONG(n); 700 | long c = a + b; 701 | VALUE result = LONG2NUM(c); 702 | return result; 703 | } 704 | else { 705 | return rb_int_plus(self, n); 706 | } 707 | } 708 | ``` 709 | 710 | これで、どんなに大きな数を渡しても、`RangeError` が起きることは無くなりました。めでたしめでたし。 711 | 712 | Ruby では、メモリがある限り大きな数を(ほかの整数と変わらずに)扱うことができます。 713 | しかし、C やほかの多くのプログラミング言語では、整数値に値の上限(と下限)があることが一般的です。 714 | Ruby に限りませんが、プログラミングにおいて、その違いを意識することは大事です。 715 | 716 | ## デバッグ Tips 717 | 718 | 何か問題があったプログラムが大きい場合、どのように再現コードを見つければ良いでしょうか。 719 | 720 | 可能であれば、プログラムをどんどん削ってしまいましょう。二分探索の要領で、(消せるなら)コードを削っていくと小さくなることがあります。 721 | 多くの場合、バージョン管理システムを使っていると思うので、元に戻すのは(多分)簡単です。思い切って削りましょう。 722 | 723 | GC バグやスレッドバグ、VM バグなど、なんかよくわからないんだけどランダムに発生する、というケースが時々あります(笹田の場合、とてもよくあります)。 724 | 725 | MRI には、ソースコード中にいろいろな制約チェックを行う仕組みが入っています。これを利用すると性能低下してしまうため、普段はオフになっていますが、これをオンにして実行すれば、何かわかるかもしれません。 726 | 727 | `gc.c` の `RGENGC_CHECK_MODE` を 2 に、`vm_core.h` の `VM_CHECK_MODE` を 1 に変更すると、この仕組みをオンにできます。 728 | `Makefile` で C コンパイラに渡すオプションを変更する場合は、`-DRGENGC_CHECK_MODE=2 -DVM_CHECK_MODE=1` みたいに追加すると良いです(ただし、一度 clean する必要があります)。 729 | 730 | バグがどうしても取れないときは、上記のようなチェックコードを沢山挟むことでバグがみつかりやすくなることがあります。 731 | 原因を検討し、関係する正しい条件でアサーションを入れてみましょう。 732 | 733 | ## おまけの練習問題 734 | 735 | バグを仕込んだブランチを作ってみました。どこがバグか探してみましょう。答えは diff にあるので、解いている間は diff を見ないように注意して下さい。 736 | 737 | > Note: なお、普通は diff や blame で過去の履歴を見ながら問題を解決していくので、今回の話は、あくまでそういうゲームとしてお楽しみ下さい。 738 | 739 | ### (1) なんか異常終了する 740 | 741 | * `$ git remote add ko1 https://github.com/ko1/ruby.git` として、笹田の GitHub 上の Ruby リポジトリを、リモートリポジトリとして登録して下さい。 742 | * fetch して、`rhc_fail1` ブランチを checkout して下さい。 743 | * 具体的には、 `$git fetch ko1 && git checkout -b rhc_fail1 ko1/rhc_fail1` とします。 744 | * このとき、diff は見ないように注意して下さい。 745 | * `$ make miniruby` として、`miniruby` を作って下さい。 746 | * `$ ./miniruby -e ...` など、`./miniruby` で何か動かしてみて下さい。きっと、派手なエラーが出てくると思います。くれぐれも diff を見ないように、ご注意下さい。 747 | 748 | > Note: `make gdb` などが役に立つでしょう。 749 | 750 | ### (2) なんか変な表示が出る 751 | 752 | * 今度は、`rhc_fail2` を checkout して下さい。 753 | * 生成した `./miniruby` で、何か動かしてみて下さい。いつもと違うメッセージが出てくるようです。どうやら、デバッグメッセージを消し忘れているようですが、どこにあるか探して、直してみましょう。 754 | 755 | > Note: `git grep` などが役に立つでしょう。 756 | 757 | ### (3) なんか変な値が出る 758 | 759 | * 次は、`rhc_fail3` を checkout して下さい。 760 | * `make test-all TESTS='ruby/test_string.rb'` として、文字列のテストを実行すると、沢山エラーが出ます。なぜエラーが出るのか、調べてみましょう。 761 | 762 | > Note: 問題のテストコードを見て、最小再現コードを探してみましょう。 763 | 764 | ### (4) なんかテストが失敗する 765 | 766 | * 最後に `rhc_fail4` を checkout して下さい。 767 | * 今度は配列のテストがおかしいようです。`make test-all TESTS='ruby/test_array.rb` を実行してみると、エラーが出ます。なぜエラーが出るのか、調べてみましょう。 768 | -------------------------------------------------------------------------------- /JA/5_performance.md: -------------------------------------------------------------------------------- 1 | # (5) 性能向上 2 | 3 | ## この資料について 4 | 5 | この資料では、MRI の性能向上に関するいくつかの考え方、Tips について紹介します。 6 | 7 | ## 性能向上とは 8 | 9 | ソフトウェアの「性能向上」という言葉を聞くと、多くの場合「高速化」、つまりプログラムの実行時間の短縮を思い浮かべることが多いのではないでしょうか。 10 | もちろん高速化は性能向上のもっとも重要なものの一つですが、ほかにも向上するべき性能指標があり、そのどれが重要であるかはアプリケーションの特徴、利用用途によります。 11 | 12 | 性能指標について、いくつかあげてみます。思いつくままに書いているので、きっとほかにもあると思います。 13 | 14 | * 時間に関する指標 15 | * 実行時間(スループット) 16 | * リアルタイム性(レイテンシ) 17 | * 起動時間 18 | 19 | * 計算資源に関する指標 20 | * メモリ消費量 21 | * 生きているオブジェクトの数 22 | * 実メモリ消費量、仮想メモリ空間の広さ(e.g. TLB ミス) 23 | * プロセス fork 時の Copy on Write の親和性 24 | * CPU の利用率(同じ実行時間なら、少ない利用率の方が良い) 25 | * CPU の活用可能性(多くのCPU資源を利用することで性能改善できるかどうか) 26 | * I/O のための fd 消費量 27 | * ディスク利用量(バイナリサイズ等) 28 | 29 | いくつかの性能指標は互いに相関しているものもあれば、逆相関であるものもあります。 30 | 31 | 相関している例: 32 | 33 | * メモリ消費量を減らすことでキャッシュミスが減り、スループットが向上する 34 | * オブジェクト生成を減らすことで、スループットが向上する 35 | 36 | 逆相関の例: 37 | 38 | * リアルタイム性を向上するためにリアルタイム GC を導入 → スループットは低下 39 | * ある頻出する特定の値の場合のコードを生成することで高速化 → メモリやディスクをより消費 40 | 41 | 自分が求める性能向上がどのようなものであるか意識し、またほかの指標に対してどのような影響を与えるか、それが許容範囲であるかを意識することは重要です。 42 | また、計算機環境はどんどん変化していくので、昔は有効だった性能改善手法も、今では逆効果、というものもあります。 43 | 44 | 例えば、大昔はメモリはとても重要な資源でしたが、利用可能なメモリはどんどん増えていきました。 45 | しかし、クラウド上の計算機環境を利用することになり、時間課金になると、今度はメモリ資源は貴重なものとして認識されるようになりました。 46 | 47 | 今、何が課題であるかを適切に把握することが大事です。 48 | 49 | ## 性能向上の考え方 50 | 51 | ### まず測ろう 52 | 53 | まず、よく言われていることですが、実装する前に、適切に計測することが必要です。 54 | 55 | 目的がスループットの改善の場合、プログラムの、どの箇所が遅いのかを見極めるのが大事です。 56 | Ruby レベルで stackprof 等のツールを用いて適切に把握し、さらにその箇所を Linux の perf コマンドを用いて詳細に分析することで、MRI のどこを改良すれば良いかわかります。 57 | 58 | (ちなみに、これらツールは「嘘」をつくことがあります。あくまで人間の作ったツールなので、100% 信じることはできません(だって、難しいし、デバッグしづらい分野ですから)。何か妙な結果かな、と思ったら、ツールを疑うことも必要になります。何か変な評価結果が出てきてしまった場合、疑い、解決するためには、それらのツールがどのように作られているかを知っておくことが大事です) 59 | 60 | ### アルゴリズムとデータ構造 61 | 62 | CS の基礎ですが、性能改善に一番効果があるのは、アルゴリズムを変更することです。 63 | 64 | 例えば、n 番目のフィボナッチ数を求める、よくあるプログラムを高速化してください、というお題があったとします。定義通りに書くと、このような感じでしょうか。 65 | 66 | ``` 67 | def fib(n) 68 | if n < 2 69 | 1 70 | else 71 | fib(n-2) + fib(n-1) 72 | end 73 | end 74 | 75 | n = Integer(ARGV.shift || 35) 76 | puts "fib(#{n}) = #{fib(n)}" 77 | ``` 78 | 79 | 全体的な高速化のために、JIT コンパイラ(Just-in-Time コンパイラ、実行時コンパイラ)を開発する、再帰呼び出しを軽量化するために、再帰メソッド呼び出しを、単なるジャンプへ変換する、といった工夫が考えられます。が、それらの工夫では、たかだか数倍から数百倍の高速化しか見込めません(それだけ速くなればいいだろ、という話もありますが)。 80 | 81 | もちろん、フィボナッチ数を求めるプログラムは、より高速なアルゴリズムで書き下すことができます。 82 | 83 | ``` 84 | def fib n 85 | a, b = 1, 1 86 | n.times{ 87 | a, b = b, a+b 88 | } 89 | a 90 | end 91 | 92 | n = Integer(ARGV.shift || 35) 93 | puts "fib(#{n}) = #{fib(n)}" 94 | ``` 95 | 96 | 計算量でいうと、O(k^n) だったものが O(n) になりました。n によっては、数百倍などでは効かず、もっとえげつない高速化が達成できます(ちなみに、フィボナッチ数を求めるプログラムは O(log n) で実現できることが知られています)。 97 | 98 | というわけで、まずはアルゴリズムを根本的に変更できないか、検討することが大事です。 99 | 100 | というのが、よくある性能向上に関する議論なのですが、もうちょっとだけ。 101 | 102 | 例えば計算量が改善するアルゴリズムがあったとき、与えられた n が十分に小さければ、実はほかの計算量が多いアルゴリズムの方が効率的、ということがあります(例えば、リニアサーチとバイナリサーチ)。 103 | また、計算量が優れているアルゴリズムで実装するよりも、計算量について劣っているが、シンプルなアルゴリズムのほうが、すぐに実装できてバグが少ないプログラムになることがあります。 104 | 速いかもしれないけど、バグがある(かもしれない)プログラムは、そもそも使いたくないですよね。 105 | また、実行に3秒かかるプログラム(開発に5分)と、それを改善して 0.3 秒で動くプログラム(開発に1時間)がある時、実は一度しか実行しないのであれば、圧倒的に前者を選択するべきです。 106 | 107 | 解くべき問題に応じて、執るべき手段を柔軟に選択していきましょう。 108 | 109 | ## MRI のいじりどころ 110 | 111 | 高速化のために、MRI でいじるべきところを見ていきます。 112 | 113 | * 組み込みクラス・メソッド 114 | * (ほかのものも同じだが)アルゴリズムの改善を検討する。時々、面倒なので非効率(だけど簡単でバグが少ない)なアルゴリズムを使っていることがある。 115 | * 値の特殊化を検討する。よくあるケースを速くできないか検討する。例えば、`Array#[]` は、小さな整数値(インデックス)を引数に取ることが多い。 116 | * 標準添付ライブラリ(`lib/`) 117 | * Ruby で書かれているため、不要なオブジェクト生成を抑制する、といった工夫が有効な場合がある。 こういうのを使うと便利。 118 | * オブジェクト管理、ガーベージコレクタ 119 | * GC アルゴリズムを検討する。でも、まだやることあるかな? 120 | * オブジェクトのメモリレイアウトを検討する。 121 | * VM 122 | * 命令セットを検討する。 123 | * JIT コンパイルを検討する。 124 | * 命令置き換え、インライン化等、コンパイラによる最適化を検討する。 125 | 126 | ほかにも色々あると思われます。 127 | 128 | ## 実際に計ってみよう:実行時間 129 | 130 | では、実際に計ってみましょう。 131 | 132 | まずは、あるプログラムの実行がどの程度時間がかかるか、その確認です。 133 | 134 | ### Time を使う 135 | 136 | プログラムの実行時間を計るには、実行前の時間(t1)と実行後の時間(t2)を取り、その差(t2-t1)を出せば実行時間になります。こんな感じ。 137 | 138 | ```ruby 139 | t1 = Time.now 140 | sleep 0.5 # 適当な負荷です。 141 | t2 = Time.now 142 | p t2 - t1 143 | ``` 144 | 145 | 簡単ですね。 146 | 147 | ### benchmark を使う 148 | 149 | ただ、毎回 `Time.now` などと書いていると大変なので、同じことを簡単に行うことができる benchmark ライブラリを使ってみます。 150 | 151 | ```ruby 152 | require 'benchmark' 153 | 154 | Benchmark.bm{|x| 155 | x.report{ 156 | sleep 0.5 157 | } 158 | } 159 | 160 | #=> 出力: 161 | # user system total real 162 | # 0.000000 0.000000 0.000000 ( 0.500101) 163 | ``` 164 | 165 | `TIme.now` を書くより長くなっちゃってますが、それっぽくユーザ時間、システム時間、合計時間、および実際の時間を表示してくれます。`x.report` で複数ベンチマークとることもできます。 166 | 167 | 結果を出力しなくない、という場合、`Benchmark.measure` で、結果だけを取り出すことができます。 168 | 169 | ```ruby 170 | require 'benchmark' 171 | 172 | p Benchmark.measure{ 173 | sleep 0.5 174 | } 175 | 176 | #=> 出力 177 | # # 178 | ``` 179 | 180 | 結果は `Benchmark::Tms` という構造で返ります。詳しくはドキュメントを読んでみて下さい。 181 | 182 | ### 実行時間が短いプログラムの時間を計る:その1 183 | 184 | このあたりから難しくなってきます。実行時間が短いプログラムの正確な実行時間を取るにはどうすればいいでしょうか。 185 | 186 | 例えば、`s1 = "str1"; s2 = "str2"` という二つの文字列 `s1`、`s2` があるとき、この文字列を連結するためには `s1 + s2` と `#{s1}#{s2}` ではどっちが速いか、ということを調べたくなったとします。 187 | 188 | 素直に書くとこんな感じでしょうか。 189 | 190 | ```ruby 191 | require 'benchmark' 192 | 193 | s1 = "str1" 194 | s2 = "str2" 195 | 196 | Benchmark.bm{|x| 197 | x.report{ 198 | _a = s1 + s2 199 | } 200 | x.report{ 201 | _b = "#{s1}#{s2}" 202 | } 203 | } 204 | ``` 205 | 206 | 手許での出力結果ははこんな感じでした。 207 | 208 | ``` 209 | user system total real 210 | 0.000005 0.000001 0.000006 ( 0.000002) 211 | 0.000003 0.000000 0.000003 ( 0.000002) 212 | ``` 213 | 214 | これを見ると、実際の時間は変んないらしい、ということがわかります。 215 | 216 | ただ、実はいろんな理由から、短いプログラムの実行時間を正確に計るのは難しい、ということが知られています。そこで、同じプログラムを繰り返し N 回実行し、後で N を割れば正しい実行時間が出来る気がします。 217 | 218 | ただ、今度は繰り返しの数 N をどのようにすれば良いかを考えなければなりません。小さすぎれば、やはりまた計れませんし、大きすぎれば、無駄に計測時間を消費してしまいます。 219 | 220 | そこで、`benchmark/ips` ライブラリがよく利用されています。 221 | 222 | `benchmark/ips` は、100ms で何回ループが回るか確認し(M)、その測定結果から、1秒間実行するためには N をどのように決めれば良いか計算します。そして、実際に N 回ループで繰り返した時間を計測することで、ips(iteration per seconds、つまり1秒に何回繰り返しを行うことができたかという指標)を計算します。 223 | 224 | 実際にやってみましょう。 225 | 226 | ```ruby 227 | require 'benchmark/ips' 228 | 229 | s1 = "str1" 230 | s2 = "str2" 231 | 232 | Benchmark.ips do |x| 233 | x.report { _a = s1 + s2 } 234 | x.report { _b = "#{s1}#{s2}" } 235 | end 236 | ``` 237 | 238 | 出力はこんなふうになりました。 239 | 240 | ``` 241 | Warming up -------------------------------------- 242 | 357.924k i/100ms 243 | 263.015k i/100ms 244 | Calculating ------------------------------------- 245 | 7.642M (± 8.3%) i/s - 37.940M in 5.007196s 246 | 4.616M (±11.1%) i/s - 22.882M in 5.032705s 247 | ``` 248 | 249 | この結果では、7.6M ips と 4.6M ips という結果が出て、1.65 倍、`_a = s1 + s2` のほうが速い、ということがわかりました。便利ですね。 250 | 251 | ### 実行時間が短いプログラムの時間を計る:その2 252 | 253 | `benchmark/ips` で、うまいこと実行時間を計ることができることがわかりました。ただ、`benchmark/ips` では各繰り返しにブロックの起動という、比較的処理時間の大きなオーバヘッドを含んでいるため、正しい結果にならない可能性があります。 254 | 255 | そこで、繰り返しの最も軽量なものである `while` ループを使ったプログラムを自動的に生成するのが `benchmark-driver` です。このライブラリでは、ベンチマーク中の様々な擾乱を排除するように設計されています。計測したいプログラムをブロックでは無く、プログラム文字列を渡すことで、後から計測対象プログラムを生成し、計測します。 256 | 257 | 具体的にはベンチマークはこのように記述します。 258 | 259 | ```ruby 260 | require 'benchmark_driver' 261 | 262 | Benchmark.driver do |x| 263 | x.prelude <<~RUBY 264 | s1 = "str1" 265 | s2 = "str2" 266 | RUBY 267 | 268 | x.report %q{ _a = s1 + s2 } 269 | x.report %q{ _b = "#{s1}#{s2}" } 270 | end 271 | ``` 272 | 273 | `benchmark/ips` とソコソコ似ていますが、プログラムのコード片がブロックでは無く文字列で渡されているのが違います。 274 | 275 | 結果はこんな感じでした。 276 | 277 | ``` 278 | Warming up -------------------------------------- 279 | _a = s1 + s2 15.368M i/s - 15.652M times in 1.018515s (65.07ns/i, 188clocks/i) 280 | _b = "#{s1}#{s2}" 9.331M i/s - 9.465M times in 1.014296s (107.16ns/i, 311clocks/i) 281 | Calculating ------------------------------------- 282 | _a = s1 + s2 20.664M i/s - 46.103M times in 2.231049s (48.39ns/i, 140clocks/i) 283 | _b = "#{s1}#{s2}" 10.639M i/s - 27.994M times in 2.631268s (93.99ns/i, 272clocks/i) 284 | 285 | Comparison: 286 | _a = s1 + s2 : 20664061.0 i/s 287 | _b = "#{s1}#{s2}" : 10639110.2 i/s - 1.94x slower 288 | ``` 289 | 290 | これを見ると、前者の方が後者より 1.94 倍 速いようです。前の二つの方法よりも、だいぶ数字が違いますね。だんだん、精度が良くなっている、ということがわかると思います。 291 | 292 | > Note: なんで文字列埋め込みのほうが遅いんでしょうか? さらなる調査は、Linux の `perf` コマンドなどで、より詳細に(C 言語レベルで)行うことができます。 293 | 294 | > Note: 結果中、`48.39ns/i` とあるのは、1回の繰り返しに 48.39ns(ナノセカンド)かかった(全体の実行時間(2.231049s = 約 2.2 秒)を繰り返しの回数(46.103M回 = 約4600万回)で割った)値です。大分小さな値ですね。 295 | 296 | なお、`benchmark-driver` は、YAML の形でベンチマークファイルを入力可能なので、YAML でまとめて書いておくと良いかもしれません。 297 | 298 | ```yaml 299 | prelude: | 300 | s1 = "str1" 301 | s2 = "str2" 302 | benchmark: 303 | - _a = s1 + s2 304 | - _b = "#{s1}#{s2}" 305 | ``` 306 | 307 | これを、`benchmark-driver` コマンドで実行すると、下記の結果が得られます。 308 | 309 | ``` 310 | Warming up -------------------------------------- 311 | _a = s1 + s2 16.794M i/s - 16.930M times in 1.008076s (59.54ns/i, 172clocks/i) 312 | _b = "#{s1}#{s2}" 9.850M i/s - 9.956M times in 1.010824s (101.53ns/i, 294clocks/i) 313 | Calculating ------------------------------------- 314 | _a = s1 + s2 19.793M i/s - 50.383M times in 2.545463s (50.52ns/i, 146clocks/i) 315 | _b = "#{s1}#{s2}" 10.178M i/s - 29.549M times in 2.903267s (98.25ns/i, 285clocks/i) 316 | 317 | Comparison: 318 | _a = s1 + s2: 19793357.8 i/s 319 | _b = "#{s1}#{s2}": 10177734.1 i/s - 1.94x slower 320 | ``` 321 | 322 | ほぼ同じですね。 323 | 324 | > Note: 例えば、`s1`、`s2` はそれぞれ4文字の文字列でしたが、これが100文字の文字列だったら、結果はどう変わるでしょうか。 325 | 326 | これを、Ruby の各メソッド用に用意したのが 327 | 328 | でして、これは で定期的に最新の Ruby で計測されています。 329 | 330 | (細かい YAML の書き方は、上記リポジトリのサンプルを参照して下さい) 331 | 332 | さて、この辺についても、いろいろ小難しい理屈はあるのですが、細かいことはおいといて、いろいろ調べてみましょう。 333 | 334 | 複数のメソッドを、`self` や引数を変えながら調べてみると、例えば「1個だけ他のメソッドに比べて遅い」とか、「あるデータを渡すとなぜか遅い」といったことが見つかるかもしれません。見つかれば、それを改善すれば、Ruby の性能改善になります。 335 | 336 | このように改善していった @watson1978 さんによる記録が ["How to optimize Ruby internal."](https://rubykaigi.org/2017/presentations/watson1978.html) にまとまっていますので参考にしてみて下さい。 337 | 338 | ---- 339 | 340 | (続く、かもしれない) 341 | -------------------------------------------------------------------------------- /JA/6_coverage.md: -------------------------------------------------------------------------------- 1 | # (6) コードカバレッジを用いた MRI の品質向上 2 | 3 | ## この資料について 4 | 5 | この資料では、MRI のコードカバレッジを測定し、テストを追加する方法を紹介します。 6 | 7 | まずコードカバレッジについてごく簡単に説明し、それから MRI のコードカバレッジのとり方を説明します。 8 | 最後に、MRI でコードカバレッジを参考にテストを拡充していく方法を説明します。 9 | 10 | ## コードカバレッジとは 11 | 12 | [コードカバレッジ](https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%BC%E3%83%89%E7%B6%B2%E7%BE%85%E7%8E%87)とは、テストを評価・分析するための指標です。プログラムを関数、行、分岐などの「単位」で区切り、テスト実行中にそれぞれの「単位」が実行されたかどうかを記録したデータのことで、これを分析することで、プログラムのどの「単位」がテストされていないか、どのあたりのモジュールのテストが手薄か、などを把握する材料になります(コードカバレッジの定義をより正確に言うと、「全単位中、いくつの単位が実行されたか」を表す割合の数値なのですが、ここでは「各単位が実行されたかどうかのデータ」を指すことにします)。 13 | 14 | コードカバレッジについてもっと知りたければ、[wikipedia の記事](https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%BC%E3%83%89%E7%B6%B2%E7%BE%85%E7%8E%87)や、[遠藤の RubyKaigi 2017 での発表資料](https://www.slideshare.net/mametter/an-introduction-and-future-of-ruby-coverage-library)、よりきちんと知りたければ教科書([『ソフトウェア・テストの技法』](https://www.amazon.co.jp/dp/4764903296)や ["Advanced Software Testing"](https://www.amazon.co.jp/dp/B00V7B1NYI/) 、オンラインで読めるドキュメントなら ["Code Coverage Analysis"](http://www.bullseye.com/coverage.html) など)を参照してください。 15 | 16 | カバレッジの可視化例を見てみましょう。次の URL を開いてください。 17 | 18 | https://rubyci.org/coverage 19 | 20 | ここには、MRI のカバレッジ測定・可視化を定期実行した最新の結果が置かれています。たとえば、"ruby" というディレクトリを辿り、その中の ["array.c"](https://rubyci.s3.amazonaws.com/debian9-coverage/ruby-master/lcov/ruby/array.c.gcov.html) というファイルを開いてみて下さい。 21 | 22 | 各行のコードのすぐ右に書かれている数値が実行回数です。実行回数が 1 回以上の行は青く、0 回の行は赤く表示されています(白い行は、空行やコメントや記号だけなど、実行の意味を持たない行)。ほとんどの行は白か青で、ときどき赤い行があるのが確認できると思います。ごく単純に言うと、これを見て赤い行を青くするように、テストを拡充していきます(あとで述べますが、実際には、もうちょっと慎重な検討をするべきです)。このように、行単位で計測したカバレッジを行カバレッジ(ラインカバレッジ、ステートメントカバレッジなどとも)と呼びます。 23 | 24 | また、同じページで、実行回数よりも右に青や赤で色づいた `[ + - ]` のような表示は、分岐のカバレッジを表しています。両方向に実行が進んでいたら青だけで `[ + + ]` 、片方だけの場合は青と赤で `[ + - ]` 、その分岐に到達していなかったら赤だけで `[ - - ]` となります。 25 | 26 | それから、画面一番上の ["functions" というリンク](https://rubyci.s3.amazonaws.com/debian9-coverage/ruby-master/lcov/ruby/array.c.func-sort-c.html)をたどると、関数カバレッジが確認できます。これは、各関数が何回呼ばれたかを昇順に並べたものです。 27 | 28 | ## MRI のコードカバレッジ 29 | 30 | ### MRI のコードカバレッジとは 31 | 32 | ひとくちに MRI のコードカバレッジと言っても、2 種類あることを意識する必要があります。 33 | すなわち、C 言語で書かれた部分(コアと、いくつかの拡張ライブラリ)のコードカバレッジと、Ruby で書かれた部分(拡張ライブラリ以外の標準添付ライブラリ)のコードカバレッジです。 34 | 前者は C 言語用のカバレッジ測定ツールを用いて測定し、後者は Ruby 用のカバレッジ測定ツールを用いて測定します。 35 | 36 | MRI の Makefile には、それぞれのカバレッジを測定し、合算した上で可視化する機能が備わっています。この機能の使い方を説明していきます。 37 | 38 | ### C 言語部分のコードカバレッジ測定 39 | 40 | MRI の Makefile に用意されているコードカバレッジ測定・可視化の仕組みは、gcov と lcov を利用しています。gcov は gcc に付属するコードカバレッジ測定ツールで、lcov は gcov の測定結果を HTML で可視化するツールです。現時点では clang でのコードカバレッジ測定は未対応です(貢献チャンス)。 41 | 42 | gcov は通常、gcc とあわせてインストールされますが、lcov は別途インストールする必要があります。apt-get が使える環境では、下記のようなコマンドでインストールできます。 43 | 44 | ``` 45 | $ sudo apt-get install lcov 46 | ``` 47 | 48 | これで環境設定が完了です。つぎに、実際に測定する方法を説明します。 49 | 50 | コアと、いくつかの拡張ライブラリなど、C 言語で書かれたコードのカバレッジを測定するには、`configure` に `--enable-gcov` オプションを与えてビルドする必要があります。なお、`configure` の `--enable-shared` オプションは `--enable-gcov` と相性が悪いので、いまのところ与えないでください。 51 | 52 | ``` 53 | $ ../ruby/configure --prefix=$PWD/../install --enable-gcov 54 | $ make -j 55 | $ make test-all 56 | ``` 57 | 58 | `make btest` や `make test-spec` もお好みで実行してください。 59 | 60 | ここまで実行すると、拡張子が `.gcda` や `.gcno` のファイルがたくさんできているはずです。`.gcda` はカバレッジ測定単位ごとのカウンタ、すなわち実行回数を記録したもので、`.gcno` はカバレッジ測定単位とコード中の位置の対応付けを持つファイルです。 61 | 62 | これを人間に理解できるようにするには、`make lcov` コマンドを実行します。 63 | 64 | ``` 65 | $ make lcov 66 | ``` 67 | 68 | これで lcov-c-out/index.html に HTML が生成されました。これを開けばカバレッジが見えるはず。 69 | 70 | ### Ruby 部分のコードカバレッジ測定 71 | 72 | 標準ライブラリのコードカバレッジを測定するには、まず simplecov をインストールする必要があります。これは `make update-coverage` で勝手にやってくれます。また、測定自体は `make test-all` に `COVERAGE=true` というオプションを与えます。なお、test-all 以外のテストで Ruby 部分のコードカバレッジを測定することは未対応です(貢献チャンス)。 73 | まとめると、次のように実行します。 74 | 75 | ``` 76 | $ ../ruby/configure --prefix=$PWD/../install 77 | $ make update-coverage 78 | $ make -j 79 | $ make test-all COVERAGE=true 80 | ``` 81 | 82 | 測定結果は、coverage/index.html に HTML が生成されています。 83 | 84 | このとき、Ruby 部分のコードカバレッジ情報は test-coverage.dat というファイルにも保存されています。これを lcov で可視化することもできます。C 言語のときと同じ `make lcov` を実行してください。 85 | 86 | ``` 87 | $ make lcov 88 | ``` 89 | 90 | これで lcov-rb-out/index.html が生成されるはずです。見方は C 言語と同じです。 91 | 92 | ### C 言語部分と Ruby 部分のコードカバレッジを同時に測定 93 | 94 | 単純に、`configure` に `--enable-gcov` を与えつつ `make test-all` に `COVERAGE=true` を与えれば、両方あわせて測定できます。 95 | 96 | ``` 97 | $ ../ruby/configure --prefix=$PWD/../install --enable-gcov 98 | $ make -j 99 | $ make test-all COVERAGE=true 100 | ... 101 | 102 | Finished tests in 774.821741s, 25.3052 tests/s, 2936.0586 assertions/s. 103 | 19607 tests, 2274922 assertions, 0 failures, 1 errors, 96 skips 104 | 105 | ruby -v: ruby 2.6.0dev (2018-04-04 trunk 63088) [x86_64-linux] 106 | Coverage report generated for Ruby's `make test-all` to /home/mame/work/ruby.build/coverage. 3581 / 3944 LOC (90.8%) covered. 107 | ``` 108 | 109 | また、Ruby 部分のコードカバレッジは、HTML だけでなく `test-coverage.dat` というファイルにも保存されています。`make lcov` は `test-coverage.dat` があったら、gcov で測定した結果と合算した上で可視化するようになっています。 110 | 111 | ``` 112 | $ make lcov 113 | ``` 114 | 115 | これで、lcov-out/index.html が生成されます。C 言語のコードと Ruby のコードのカバレッジが両方含まれているのがわかると思います。冒頭で紹介した [CI でのコードカバレッジ測定結果](https://rubyci.s3.amazonaws.com/debian9-coverage/ruby-master/lcov/index.html) は、この結果をアップロードしています。 116 | 117 | 118 | ## コードカバレッジを見てテストを改善する 119 | 120 | 手元でカバレッジを確認できるようになったので、次はコードカバレッジを上げてみましょう。基本的なやり方は次のとおりです。 121 | 122 | 1. コードカバレッジを見て、実行されていないコード(赤い行)を見つける 123 | 2. そのコードを実行するようなテストを書いて、実行されるようにする(青い行にする) 124 | 125 | ただ、あまり安易にやりすぎると、意図がわからないテストになったり、環境依存なテストになったり、CI に悪影響があったりします。極端な例では、メモリ不足に動くコードをテストするために、実際にメモリを使い尽くすようなテストは、現状の `make test-all` の中に入れることは想定されていません。そのようなテストに意味がないわけではありませんが、`make test-all` 以外の仕組みを作って行うべきでしょう。コードカバレッジを上げることは目的ではなく、テストの質を高めることが目的であることを忘れないようにしましょう。 126 | 127 | また、「なぜテストされていないのか」を考えることも重要です。最近新しく実装されたコードであれば、単純にテストが不足している可能性が高いです。しかし、昔からあるコードなのにテストされていない場合は、何か事情があるかもしれません。典型的なのは、特定のプラットフォームでしか使われないコードです(この場合、テストを書くのは難しいでしょう)。条件分岐が冗長になっていたり、すでに使われないコードになっていたりするなど、コードの方に問題がある場合もあります(この場合、テストを書く以外に、リファクタリングも検討すべきです)。 128 | 129 | ### `Array#second` のコードカバレッジを測定する 130 | 131 | さて、3 章で紹介した `Array#second` を実装した状態で、コードカバレッジを測定してみましょう。 132 | 133 | ```diff 134 | diff --git a/array.c b/array.c 135 | index bd24216af3..79c1c1d334 100644 136 | --- a/array.c 137 | +++ b/array.c 138 | @@ -6131,6 +6131,12 @@ rb_ary_sum(int argc, VALUE *argv, VALUE ary) 139 | * 140 | */ 141 | 142 | +static VALUE 143 | +ary_second(VALUE self) 144 | +{ 145 | + return rb_ary_entry(self, 1); 146 | +} 147 | + 148 | void 149 | Init_Array(void) 150 | { 151 | @@ -6251,6 +6257,8 @@ Init_Array(void) 152 | rb_define_method(rb_cArray, "dig", rb_ary_dig, -1); 153 | rb_define_method(rb_cArray, "sum", rb_ary_sum, -1); 154 | 155 | + rb_define_method(rb_cArray, "second", ary_second, 0); 156 | + 157 | id_cmp = rb_intern("<=>"); 158 | id_random = rb_intern("random"); 159 | id_div = rb_intern("div"); 160 | ``` 161 | 162 | ↑のパッチをあてて、C 言語部分のコードカバレッジを測定してみましょう。 163 | 164 | ``` 165 | $ ../ruby/configure --prefix=$PWD/../install --enable-gcov 166 | $ make -j 167 | $ make test-all 168 | $ make lcov 169 | ``` 170 | 171 | ... を開いて、ruby/ ディレクトリ→ array.c のカバレッジ、と進み、`ary_second` のところを探します。 172 | 173 | ![図1](img/6_coverage_fig1.png) 174 | 175 | このように、赤くなっています。まだ `Array#second` のテストを書いてないのだから、当たり前です。 176 | 177 | ### Ruby のテストの構成 178 | 179 | 2 章で触れたとおり、Ruby には 3 種類のテストがあります。それぞれ簡単に説明します。 180 | 181 | 1 つめは `make btest` で実行されるものです。Ruby の VM をテストすることに特化しています。失敗したら SEGV のように致命的な問題が起きるテストが書かれています。1 つのテストを実行するたびにインタプリタを立ち上げ直すことで、そのような問題を扱いやすくする工夫がなされています。 182 | 183 | 2 つめは `make test-all` で実行されるものです。組み込みクラスから標準ライブラリまでを幅広くテスト対象としています。テストファイルは ruby/test 以下に置かれています。特に、コア本体に対するテストは ruby/test/ruby にあります。テストは minitest という、比較的シンプルなテストフレームワークを使って記述されています。 184 | 185 | 3 つめは、`make test-spec` で実行されるものです。これは元々 rubyspec と呼ばれていたプロジェクトで、複数の Ruby インタプリタ間の互換性を検証する目的で作られています。rubyspec は https://github.com/ruby/spec という別リポジトリが upstream となっています。 186 | 187 | ### `Array#second` のテストを追加する 188 | 189 | それでは、`Array#second` のテストを書いてみましょう。`Array#second` は VM の機能ではないので、`make test-all` か `make test-spec` のどちらかに追加します。今回は、`make test-all` の方に追加することにします(執筆者が test-spec の流儀にあまり詳しくないので)。 190 | 191 | テストを追加するときは、似たようなテストを探すところから始めるとよいでしょう。今回は、`Array#first` のテストを探します。組み込みクラスのテストは ruby/test/ruby 以下にあるので、そこから git grep してみます。 192 | 193 | ``` 194 | $ git grep -w first test/ruby 195 | ... 196 | test/ruby/test_array.rb: assert_equal(1, x.first) 197 | test/ruby/test_array.rb: assert_equal([1], x.first(1)) 198 | test/ruby/test_array.rb: assert_equal([1, 2, 3], x.first(3)) 199 | test/ruby/test_array.rb: assert_equal(3, @cls[3, 4, 5].first) 200 | test/ruby/test_array.rb: assert_equal(nil, @cls[].first) 201 | ... 202 | ``` 203 | 204 | `-w` オプションは word 単位の検索です。"first" の前後が記号や空白などで、単語区切りになっている場合のみヒットします。"headfirst" にはヒットしません。 205 | 206 | どうやら `test/ruby/test_array.rb` が Array クラスのテストのようです。ファイルを開いて見てみます。 207 | 208 | ``` 209 | def test_first 210 | assert_equal(3, @cls[3, 4, 5].first) 211 | assert_equal(nil, @cls[].first) 212 | end 213 | ``` 214 | 215 | かんたんですね。これを真似して書き足しましょう。次のテストを `test_first` メソッドの後ろあたりに書きます。 216 | 217 | ``` 218 | def test_second 219 | assert_equal(4, @cls[3, 4, 5].second) 220 | assert_equal(nil, @cls[].second) 221 | end 222 | ``` 223 | 224 | その上で、再度テストを実行します。`test/ruby/test_array.rb` だけ実行すればよいので、次のように実行します。 225 | 226 | ``` 227 | $ make test-all TESTS='ruby/test_array.rb' 228 | Run options: "--ruby=./miniruby -I../ruby/lib -I. -I.ext/common ../ruby/tool/runruby.rb --extout=.ext -- --disable-gems" --excludes-dir=../ruby/test/excludes --name=!/memory_leak/ 229 | 230 | # Running tests: 231 | 232 | Finished tests in 3.994787s, 47.0613 tests/s, 3109.0522 assertions/s. 233 | 188 tests, 12420 assertions, 0 failures, 0 errors, 0 skips 234 | 235 | ruby -v: ruby 2.6.0dev (2018-04-05 trunk 63097) [x86_64-linux] 236 | ``` 237 | 238 | 無事テストは成功しました。lcov でカバレッジを確かめてみましょう。 239 | 240 | ``` 241 | $ make lcov 242 | ``` 243 | 244 | coverage/index.html を開き、ruby → array.c のカバレッジを開いて、`ary_second` 関数を確認します。 245 | 246 | ![図2](img/6_coverage_fig2.png) 247 | 248 | 無事青くなりました。きちんとテストできていることが確認できて、安心できますね。 249 | 250 | ### 注意点 251 | 252 | #### コンパイル後に C 言語コードを変更した場合、すべての再コンパイルが必要です。 253 | 254 | 本文で示した例は、Ruby でテストコードを書き足しただけだったので、問題なく再実行できました。 255 | 256 | しかし、C 言語コードを変更してテスト実行すると、再コンパイルが起きます。そうすると、実行不回数を保持する .gcda ファイルと、ソースコードの対応をつなぐ .gcno ファイルに一貫性がなくなってしまい、gcov や lcov がうまく動かない場合があります。 257 | 258 | たとえば array.c を編集した場合は、array.gcdea と array.gcno を削除した上で `make` と `make test-all` を実行することで、array.c に関してゼロから測定し直すことができるようです(ただし、array.c が `#include` している別ソースコードの測定は異常になる可能性があります)。それでもうまくいかない場合は、すべてを消してビルドし直すのが(少し面倒ですが)確実です。 259 | 260 | #### 試行錯誤はカバレッジ測定なしでやるとよいです。 261 | 262 | 現代では、MRI のテストの品質はわりとよいです(遠藤がテストを書き足したり、ruby/spec というプロジェクトが取り込まれたりしたため)。 263 | 264 | よって、いまだに未テストの箇所を実行するテストを書くのは、それほど容易ではないかもしれません。printf などを挟みながら、いろいろな入力を試行錯誤して未テストの箇所に到達していく必要があるでしょう(デバッガを使えば良いのですが、遠藤は gcov と gdb を組み合わせて使ったことがないのでよくわかりません)。 265 | 266 | これをカバレッジ測定下でやるためには、.gcda や .gcno の削除をする必要があり、トラブルが生じやすいです。遠藤のベストプラクティスとしては、別ディレクトリで `--enable-gcov` なしでビルドしたディレクトリでテスト作成を行い、最終的にまたカバレッジ測定を使ってその箇所が実行されることを確認するという方法です。より良い方法があれば、ぜひ共有してください。 267 | 268 | また、C 言語コード部分のテストは入念に行われていますが、一部の標準添付ライブラリについては十分にテストされていないものもあります。そちらの方をターゲットとして取り組んでみるのもよいでしょう。 269 | -------------------------------------------------------------------------------- /JA/img/6_coverage_fig1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ko1/rubyhackchallenge/da5e6fc6ee6e360f4d0639ead1a8558204db3431/JA/img/6_coverage_fig1.png -------------------------------------------------------------------------------- /JA/img/6_coverage_fig2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ko1/rubyhackchallenge/da5e6fc6ee6e360f4d0639ead1a8558204db3431/JA/img/6_coverage_fig2.png -------------------------------------------------------------------------------- /JA/tasks.md: -------------------------------------------------------------------------------- 1 | # 発展課題 2 | 3 | 発展課題の案を示します。これ以外にも、興味のあるテーマを自分で見つけてくれる分には大歓迎です。 4 | 5 | ## バグの再現、コメント、修正 6 | 7 | Redmine で未解決のバグチケットを見つけ、調査してみましょう。自分が興味のある、もしくは自分が困っているチケットですと、やる気が出ると思います。 8 | 9 | 再現手順が曖昧なチケットでは、再現手順を見つけるだけでも十分に助かります(再現手順が見つかれば、もう半分以上、解けたようなものです)。ぜひ挑戦してみて下さい。 10 | 11 | ## 機能の追加 12 | 13 | Ruby に足りないと思っている機能を、これを機会に追加してみましょう。 14 | 15 | 好きな文法を加えてみても、面白いと思います。 16 | 17 | ## ドキュメンテーション 18 | 19 | MRI にはドキュメントが不足しています。自分が知りたいと思い、そしてその資料がないもの(例えば拡張ライブラリのための API ドキュメント)などがあれば、 20 | ソースコードなどを解読し、ドキュメントを追加してみましょう。 21 | 22 | 例えば [【緩募】Rubyリファレンス・マニュアル=るりまで不足中のサンプルコード8,000件を一緒に作りませんか?](http://tbpgr.hatenablog.com/entry/2017/09/17/232557) などが参考になります。 23 | 24 | ## テストの拡充や環境の整備 25 | 26 | ### テストの追加 27 | 28 | 6章を参考に、C レベル、Ruby レベルでカバレッジを計測してみましょう。 29 | そして、カバレッジが足りないところにテストを追加してみましょう。 30 | 31 | ### -w での警告削除 32 | 33 | `ruby -w` で実行すると、普段は出ない警告が出るようになります。例えば、 34 | 35 | ``` 36 | def foo 37 | end 38 | ``` 39 | 40 | のように、インデントがずれていると警告が出ます。おそらく、あまり好ましくない自体が起こっているのでは無いかと思います。 41 | 42 | そこで、`-w` で `make test-all` を実行してみて、警告が出るかどうか試してみましょう。そして、警告が出たら、でなくなるようにテストを改善してみましょう。 43 | 44 | ### 新しい警告の追加 45 | 46 | 前項の続きです。自分が「こんな場面では警告を出して欲しいな」という例を考え(例えば、Refinements は使うべきでは無いので、利用時に警告を出すとか)、警告を出すように Ruby を改造してみましょう。 47 | 48 | 激しい警告は Rubocop などのツールの役目かもしれません。 49 | 50 | ### ビルドした Ruby での Gem のテスト 51 | 52 | 現在、ビルドした Ruby で Gem など、外部ライブラリを、ビルドした Ruby で、他の影響を与えないで適切にテストする方法がありません。 53 | というのも、Gem の場合、Gem のテストのために他の Gem を利用したりする必要があるためです。 54 | 区切られた環境(サンドボックス)を用いることで 55 | 56 | ### テストマシンの整備 57 | 58 | のテスト環境の現状を調べ、足りない機能を検討してみましょう。また、できれば整備を手伝ってください。 59 | 60 | ## 可視化 61 | 62 | VM をいじることで、外部から実行状況を知る方法を検討してみましょう。 63 | 64 | 例えば、Ruby の VM はスタックマシンなので、そのスタックの状況を図で表示するとか、特定のタイミングで音を出すようにするとか。 65 | 66 | ## 性能改善 67 | 68 | ### 性能測定 69 | 70 | (5) で紹介したりしなかったりしたツールを使って、自分がよく使う Ruby アプリケーションの性能測定をしてみましょう。 71 | 可能なら、性能改善のための方法を実装してみましょう。 72 | 73 | ### GC チューニングパラメータの評価 74 | 75 | 実行時に GC パラメータを変更するパッチが提案されているのですが 実際にこれを使うと良いことがあるかどうかがわかりません。 76 | まずは Ruby の GC パラメータにどのようなものがあるかを確認し、実際にお持ちのアプリケーションで、実行時の GC パラメータチューニングが役に立つことがあるか、調べてコメントしてみてください。 77 | 78 | ### ベンチマークセットの拡充 79 | 80 | 5章を参考に、ベンチマークを追加してみましょう。 81 | そして、他のメソッドに比べて遅いな? という場所を探して、改善するコードを書いてみましょう。 82 | 83 | ### デバッグカウンタを追加してみよう 84 | 85 | `ruby/debug_counter.[ch]` に、デバッグカウンタという仕組みが入っています。これは、ある処理が何回実行されたか、調べるための仕組みです(DTrace のしょぼい版です)。 86 | カウンタは、実行時では無く、コンパイル時に埋め込まれます。デフォルトではオフになっています。 87 | 88 | 好きなカウンタを作って、MRI の挙動を調べてみましょう。 89 | 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruby Hack Challenge (RHC) 2 | 3 | ## Upcoming events 4 | 5 | ## About RHC 6 | 7 | "Ruby Hack Challenge" (RHC) is a short guide to hack MRI (Matz Ruby Interpreter) internals. 8 | 9 | Periodically we hold an event "RHC" at Cookpad or other places. 10 | 11 | * [Matz, Koichi and Mame celebrate “Ruby Week” at Cookpad, Bristol](https://sourcediving.com/matz-koichi-and-mame-celebrate-ruby-week-at-cookpad-bristol-b1efbde8007) 12 | 13 | If you want to hold a RHC event, please ask us. 14 | 15 | We provide lecture materials: 16 | 17 | * [(1) Introduction of MRI development culture](EN/1_culture.md) 18 | * [(2) MRI source code structure](EN/2_mri_structure.md) 19 | * [(3) Exercise: Add methods to Ruby](EN/3_practice.md) 20 | * [(4) Fixing bugs](EN/4_bug.md) 21 | * [(5) Performance improvements](EN/5_performance.md) 22 | * [Task ideas](EN/task_ideas.md) 23 | 24 | Feel free to ask questions you have at Gitter channel . 25 | 26 | ## (Japanese Guide) RHC とは 27 | 28 | "Ruby Hack Challenge" (RHC) とは、MRI (Matz Ruby Interpreter) の内部をハックするためのガイドです。 29 | 30 | 定期的に RHC イベントをクックパッドやその他の場所で開催しています。 31 | 32 | * [Cookpad Ruby Hack Challenge 開催報告](https://techlife.cookpad.com/entry/2017/09/29/224024) 33 | * [Rubyのなかを覗いてみよう!池澤あやかが「Cookpad Ruby Hack Challenge」に参加してみた](https://next.rikunabi.com/journal/20180601_c11/) 34 | 35 | 「うちでもイベントを開催したい」といったお話があればご一報ください。 36 | 37 | 教材を用意しています。 38 | 39 | * [(1) MRI 開発文化の紹介](JA/1_culture.md) 40 | * [(2) MRI ソースコードの構造](JA/2_mri_structure.md) 41 | * [(3) 演習:メソッドの追加](JA/3_practice.md) 42 | * [(4) バグの修正](JA/4_bug.md) 43 | * [(5) 性能向上](JA/5_performance.md) 44 | * [(6) コードカバレッジを用いた MRI の品質向上](JA/6_coverage.md) 45 | * [発展課題](JA/tasks.md) 46 | 47 | もし質問などがあれば、お気軽に Gitter チャンネル でお問い合わせください。 48 | -------------------------------------------------------------------------------- /array_second/array_second.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | static VALUE 5 | ary_second(VALUE self) 6 | { 7 | return rb_ary_entry(self, 1); 8 | } 9 | 10 | void 11 | Init_array_second(void) 12 | { 13 | rb_define_method(rb_cArray, "second", ary_second, 0); 14 | } 15 | -------------------------------------------------------------------------------- /array_second/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | create_makefile('array_second') 3 | -------------------------------------------------------------------------------- /bib.md: -------------------------------------------------------------------------------- 1 | # MRI Internal bibliography / 参考文献 2 | 3 | ## English 4 | 5 | * Redmine wiki 6 | * https://github.com/ruby/ruby/wiki 7 | * https://github.com/ruby/ruby/wiki/Developer-How-To 8 | * [Ruby Hacking Guide](https://ruby-hacking-guide.github.io/) by @minero-aoki 9 | * MRI Internal complete guide for Ruby 1.8 era. 10 | * Part 1, 2 (GC, Parser) are valuable yet. 11 | * [Ruby Under a Microscope](http://patshaughnessy.net/ruby-under-a-microscope) by Pat Shaughnessy 12 | * MRI Internal brief guide for Ruby 2.0. 13 | * Not completed but it is useful to understand internals for not experts. 14 | * [The Ruby Bibliography](http://rubybib.org/) 15 | 16 | ## Japanese 17 | 18 | * Redmine wiki 19 | * https://github.com/ruby/ruby/wiki 20 | * https://github.com/ruby/ruby/wiki/Developer-How-To 21 | * [Rubyの拡張ライブラリの作り方](https://docs.ruby-lang.org/en/2.4.0/extension_ja_rdoc.html) 22 | * Ruby (MRI) の拡張ライブラリの作り方と、詳細な API の情報がまとまっています。 23 | * [Ruby Hacking Guide](http://i.loveruby.net/ja/rhg/) by @minero-aoki 24 | * 通称 RHG。完全ガイドだけど 1.8 向け。 25 | * 1, 2 部(GC やパーサー)は今でも十分に役に立ちます。 26 | * [Rubyのしくみ Ruby Under a Microscope](http://magazine.rubyist.net/?0049-BookRUM_ja) by Pat Shaughnessy 27 | * Ruby 2.0 向けの話。RHG よりも新しい(1.9 以降の VM の概要が書いてあります)。 28 | * 完全網羅ではないが、ガチ勢じゃなければそこそこ役に立ちます。 29 | 30 | -------------------------------------------------------------------------------- /docker/Dockerfile.bionic: -------------------------------------------------------------------------------- 1 | FROM ubuntu:bionic 2 | MAINTAINER Koichi Sasada 3 | 4 | RUN apt update && \ 5 | apt dist-upgrade -y && \ 6 | apt install -y gcc && \ 7 | apt install -y ruby subversion autoconf make git && \ 8 | apt install -y libgmp-dev libssl-dev zlib1g-dev libffi-dev libreadline-dev libgdbm-dev && \ 9 | apt install -y vim-tiny gdb && \ 10 | apt install -y sudo && \ 11 | DEBIAN_FRONTEND=noninteractive apt install -y tzdata 12 | 13 | # RUN rm -rf /var/lib/apt/lists/* 14 | 15 | # add user (rubydev, password is also rubydev) 16 | RUN mkdir /home/rubydev && \ 17 | groupadd -g 1000 dev && \ 18 | useradd -g dev -G sudo -s /bin/bash rubydev && \ 19 | echo 'rubydev:rubydev' | chpasswd && \ 20 | chown rubydev /home/rubydev 21 | 22 | # prepare build 23 | RUN sudo -u rubydev sh -c "cd /home/rubydev && mkdir workdir && mkdir workdir/build/ && mkdir workdir/install" 24 | #RUN sudo -u rubydev sh -c "cd /home/rubydev/workdir && git clone https://github.com/ruby/ruby.git" 25 | #RUN sudo -u rubydev sh -c "cd /home/rubydev/workdir/ruby && autoconf" 26 | 27 | -------------------------------------------------------------------------------- /events/rhc_1.md: -------------------------------------------------------------------------------- 1 | # Cookpad Ruby Hack Challenge 2 | 3 | * 2017/08/30, 31 @ クックパッド株式会社 4 | * 募集ページ: https://cookpad.connpass.com/event/60450/ 5 | * hashtag: #cookpad_rhc 6 | 7 | # ゴール 8 | 9 | ## 共通課題(1日目)のゴール 10 | 11 | * Ruby のソースコードの構造を知る 12 | * Ruby のビルドができるようになる 13 | * Ruby の中身を弄ることができるようになる 14 | 15 | ## 発展課題(2日目)のゴール(できれば) 16 | 17 | * 未解決問題を解決する 18 | * 実際に Ruby インタプリタへの貢献を体験する 19 | * 開発コミュニティへの参加を体験する 20 | 21 | # スケジュール 22 | 23 | ## 2017/08/30 (水) 一日目 24 | 25 | * 10:00 オープニング 26 | * 10:30 ハックに必要となる事前知識の講義 27 | * 12:00 ランチ(各自でお取りください) 28 | * 隣に三越があり、地下にお弁当等豊富です) 29 | * お店もそこそこあります 30 | * 13:00 共通課題 31 | * 16:00 発展課題の紹介 32 | 33 | ## 2017/08/31 (木) 二日目 34 | 35 | * 10:00 発展課題の開始 36 | * 11:30 まつもとゆきひろ氏 特別講演 37 | * 12:00 Ruby開発者を交えてのランチ 38 | * 13:00 Ruby開発者との Q&A セッション 39 | * 14:00 発展課題の再開 40 | * 18:30 打ち上げパーティー 41 | * 可能なら、取り組んだ発展課題を紹介してください。 42 | 43 | # コミュニケーション方法 44 | 45 | * 参加者間: https://gitter.im/rubyhackchallenge/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link 46 | * 有識者(Rubyコミッタ)に相談: https://gitter.im/ruby/ruby 47 | 48 | -------------------------------------------------------------------------------- /events/rhc_2.md: -------------------------------------------------------------------------------- 1 | # Cookpad Ruby Hack Challenge #2 2 | 3 | * 2018/01/29 @ Cookpad Tokyo office 4 | * Event page: https://cookpad.connpass.com/event/74231/ 5 | * hashtag: #cookpad_rhc 6 | 7 | # About the Cookpad Ruby Hack Challenge 8 | 9 | Hack the Ruby interpreter at Cookpad! 10 | 11 | The Programming language Ruby is widely used on web application development. Of course, programs written in Ruby run on Ruby interpreter. 12 | 13 | Cookpad Ruby Hack Challenge (RHC) is a one-day event where we'll teach you how to extend Ruby features, fix bugs, and to improve performance of Ruby. 14 | 15 | The first Ruby Hack Challenge was a great success, but it was offered only in Japanese. This time, the Ruby Hack Challenge will be offered in English (English lecture materials and English announcements) as well as Japanese. 16 | 17 | Lecture materials: https://github.com/ko1/rubyhackchallenge (currently being translating into English!) 18 | 19 | Note that we offer support for English at the event (English lecture materials, announcements and English hacking supports) but we don't restrict our support to English. Japanese language users are also welcome. (in Japanese: イベント自体は英語で行いますが、日本語話者も歓迎します) 20 | 21 | One more note: Koichi and another supporter Endoh-san are not native English speakers. 22 | 23 | # Details 24 | 25 | This event held over 1 day. 26 | 27 | You can choose several options what you want to do. 28 | 29 | * (1) Read the lecture materials at the event and try hacks along with texts. 30 | * (2) Read the lecture materials in advance, and ask us if you have any questions. 31 | * (3) Read the lecture materials in advance, and try advanced hacking with our support. 32 | 33 | Unlike the first RHC, for RHC#2 we don't offer any lessons. We only offer lecture materials and assistants to support attendees. 34 | 35 | 36 | ## Requirements 37 | 38 | * You can attend the whole day 39 | * You bring your own laptop which is capable of building Ruby interpreters. 40 | 41 | ## Goal with lecture materials 42 | 43 | * You can understand Ruby's source code structures. 44 | * You can build the Ruby interpreter. 45 | * You can modify Ruby interpreter internals. 46 | 47 | ## Goal with advanced hacks 48 | 49 | * You can solve unsolved problems. 50 | * You can experience contributing to the Ruby interpreter. 51 | * You can experience participation in the Ruby development community. 52 | 53 | # Schedule 54 | 55 | ## 1/29 (Mon) 56 | 57 | * 10:00 Opening 58 | * 10:30 Hack: start 59 | * 12:00 Lunch 60 | * 13:00 Hack: continue 61 | * 18:00 Closing 62 | * 18:30 After party / networking 63 | 64 | ## Communication channel 65 | 66 | Feel free to join gitter channel: https://gitter.im/rubyhackchallenge/LobbyEn (English) and https://gitter.im/rubyhackchallenge/Lobby (in Japanese, past RHC attendees are there) 67 | 68 | -------------------------------------------------------------------------------- /events/rhc_3.md: -------------------------------------------------------------------------------- 1 | # Cookpad Ruby Hack Challenge #3 2 | 3 | * 2018/03/21 @ Cookpad Bristol, UK [[map]](https://goo.gl/maps/HCdSstcXPwm) 4 | * hashtag: #cookpad_rhc 5 | 6 | # About the Cookpad Ruby Hack Challenge 7 | 8 | Hack the Ruby interpreter at Cookpad! 9 | 10 | The Ruby Programming language is widely used for web application development. Of course, programs written in Ruby run on the Ruby interpreter. 11 | 12 | Cookpad Ruby Hack Challenge (RHC) is a one-day event where we'll teach you how to extend Ruby features, fix bugs, and to improve performance of Ruby. 13 | 14 | The first two Ruby Hack Challenge's were great successes, but were offered only in Japan. This time, the Ruby Hack Challenge will be offered in the UK! 15 | 16 | Lecture materials: https://github.com/ko1/rubyhackchallenge/tree/master/EN 17 | 18 | Note: Koichi and Endoh-san are not native English speakers. 19 | 20 | # Details 21 | 22 | This event will held over 1 day on March 21st, 2018 23 | 24 | You can choose what you want to do eg. 25 | 26 | * (1) Read the lecture materials at the event and try hacks along with the documentation. 27 | * (2) Read the lecture materials in advance, and ask us if you have any questions. 28 | * (3) Read the lecture materials in advance, and try advanced hacking with our support. 29 | 30 | We offer these lecture materials in English, and will be on hand to provide assistance to support attendees. 31 | 32 | ## Requirements 33 | 34 | * You are able to attend the whole day 35 | * You will bring your own laptop which is capable of building Ruby interpreters. 36 | 37 | ## Goal with lecture materials 38 | 39 | * You can understand Ruby's source code structure. 40 | * You can build the Ruby interpreter. 41 | * You can modify Ruby interpreter internals. 42 | 43 | ## Goal with advanced hacks 44 | 45 | * You can solve unsolved problems. 46 | * You can experience contributing to the Ruby interpreter. 47 | * You can experience participation in the Ruby development community. 48 | 49 | # Schedule 50 | 51 | ## 3/21 (Wed) 52 | 53 | * 09:30 Doors open, coffee 54 | * 10:00 Introduction with Matz 55 | * 10:30 Hack: start 56 | * 12:00 Lunch 57 | * 13:00 Hack: continue 58 | * 17:30 Close 59 | 60 | ## Communication channel 61 | 62 | Feel free to join gitter channel: https://gitter.im/rubyhackchallenge/LobbyEn (English) and https://gitter.im/rubyhackchallenge/Lobby (in Japanese, past RHC attendees are there) 63 | 64 | ## Hosts 65 | 66 | * Yukihiro Matsumoto 67 | * Koichi Sasada 68 | * Yusuke Endoh 69 | 70 | ## Attendees 71 | 72 | * TBC 73 | 74 | 75 | ---- 76 | ### Previous Events 77 | 78 | #### Cookpad Ruby Hack Challenge #2 79 | 80 | * 2018/01/29 @ Cookpad Tokyo office 81 | * Event page: https://cookpad.connpass.com/event/74231/ 82 | * hashtag: #cookpad_rhc 83 | -------------------------------------------------------------------------------- /events/rhc_4.md: -------------------------------------------------------------------------------- 1 | # Cookpad Ruby Hack Challenge #4 カバレッジ特別回 2 | 3 | # Cookpad Ruby Hack Challenge とは 4 | 5 | クックパッドで Ruby インタプリタを Hack しよう! 6 | 7 | クックパッドをはじめ、多くのウェブアプリケーション開発でプログラミング言語 Ruby が利用されています。Ruby で書かれたプログラムを動かすときは Ruby インタプリタで実行します。 8 | 9 | Cookpad Ruby Hack Challenge は、この Ruby インタプリタを Hack する方法をお伝えするイベントです。 10 | 11 | 今回は、Ruby インタプリタのカバレッジをお題としたハックイベントになります。 12 | カバレッジ環境を整えた Ruby コミッタの遠藤が講師を行います。 13 | ハックネタは思いつかないけど、何かしら Ruby インタプリタを弄ってみたい、パッチを送ってみたい、そんな人にオススメです。 14 | 15 | # イベント概要 16 | 17 | 4 回目となる今回は、「カバレッジ特別回」と題して、未テストのコードを探す指標「コードカバレッジ」の情報を頼りに Ruby インタプリタのテストを書くというテーマで開催します。Ruby のコードカバレッジの読み方、自分でコードカバレッジを測定する方法、Ruby のテストの書き方などを扱い、書いたテストでコードカバレッジが上昇することを確認するところまでやりましょう。 18 | 19 | 当日は 1 時間程度講義した後、実習とする予定です。これまでの RHC の講義資料([Cookpad Ruby Hack Challenge](https://github.com/ko1/rubyhackchallenge))に基づいて説明するので、この資料に目を通しておくことが望まれますが、必須ではありません。なお、講義資料([(6) コードカバレッジを用いた MRI の品質向上](https://github.com/ko1/rubyhackchallenge/blob/master/JA/6_coverage.md))も、Ruby のコードカバレッジを定期実行で測定したもの([LCOV - code coverage report](https://rubyci.s3.amazonaws.com/debian8-coverage/ruby-trunk/lcov/index.html))も既に公開しています。 20 | 21 | また、RHC の資料を読んで(カバレッジ以外の)ハックをしたい方も歓迎します(VM に詳しい Ruby コミッタの笹田に質問もできます)。 22 | 23 | ## こんな方に来てほしい 24 | 25 | * Ruby インタプリタの開発に貢献をしてみたい方 26 | * Ruby インタプリタのソースコードを読み書きするきっかけ・手がかりが欲しい方 27 | 28 | ## 参加要件 29 | 30 | * Ruby インタプリタのビルドが可能なノートパソコンを当日持ち込める方(弊社からのノートパソコンの貸与は行ないません) 31 | 32 | ## スケジュール 33 | 34 | 注意:時間等は変わる可能性があります。 35 | 36 | ### 開催前 37 | 38 | * Gitter を用いたオンライン予習サポート(希望者) 39 | * https://gitter.im/rubyhackchallenge/Lobby 40 | 41 | ### 当日(6/23 土) 42 | 43 | * 13:00 オープニング 44 | * 13:10 講義(コードカバレッジの見方、測定方法、テストの書き方など) 45 | * 14:00 実習 46 | * 16:30 成果発表とまとめ 47 | * 17:00 解散 48 | -------------------------------------------------------------------------------- /events/rhc_5.md: -------------------------------------------------------------------------------- 1 | # Cookpad Ruby Hack Challenge #5 [二日間開催] 2 | 3 | * Date: 2018/09/10 (Mon), 2018/09/13 (Thu) 4 | * Location: Cookpad Tokyo Office 5 | * CFA page: https://cookpad.connpass.com/event/95127/ 6 | * Language: japanese 7 | 8 | # Cookpad Ruby Hack Challenge とは 9 | 10 | クックパッドで Ruby インタプリタを Hack しよう! 11 | 12 | クックパッドをはじめ、多くのウェブアプリケーション開発でプログラミング言語 Ruby が利用されています。Ruby で書かれたプログラムを動かすときは Ruby インタプリタで実行します。 13 | 14 | Cookpad Ruby Hack Challenge は、この Ruby インタプリタに対して機能を追加したり、改良したり、性能向上させたりする方法、つまり Ruby インタプリタを Hack する方法を、二日間かけてお伝えするイベントです。 15 | 16 | # イベント概要 17 | 18 | 9/10 (月) と 9/13 (木) の二日間をかけて、Ruby インタプリタをハックします。一日目(9/10 月)は共通課題として、用意する資料を手順どおりに進めて頂きます。二日目(9/13 木)は発展課題として、Ruby インタプリタに残る未解決問題に取り組んで頂きます。両日とも、講師は Ruby コミッタの笹田および遠藤が務めます。 19 | 20 | また、二日目には特別企画として、クックパッド以外の Ruby 開発者とも交流できる場を用意したいと思っています。Ruby という言語や、Ruby インタプリタに対する疑問や意見がある人は、この機会にぜひ議論してもらえればと思います。 21 | 22 | 去年行ったときの記録です。 23 | 24 | * [Cookpad Ruby Hack Challenge 開催報告](https://techlife.cookpad.com/entry/2017/09/29/224024) 25 | * [Rubyのなかを覗いてみよう!池澤あやかが「Cookpad Ruby Hack Challenge」に参加してみた](https://next.rikunabi.com/journal/20180601_c11/) 26 | 27 | 28 | ## こんな方に来てほしい 29 | 30 | * Ruby インタプリタの Hack がしてみたい方 31 | * Ruby プログラムは書けるけど、どうやって動いているのか知りたい方 32 | * プログラミング言語 Ruby および Ruby インタプリタを普段開発しているような人達が、何を考えているのか知りたい方 33 | * 難易度の高いプログラミングに挑戦してみたい方 34 | * 夏休みの思い出が欲しい方 35 | 36 | ## 参加要件 37 | 38 | * 全日程に参加可能な方 39 | * Ruby インタプリタのビルドが可能なノートパソコンを当日持ち込める方(弊社からのノートパソコンの貸与は行ないません) 40 | * [「演習:バージョン表記の修正(改造)」](https://github.com/ko1/rubyhackchallenge/blob/master/JA/2_mri_structure.md#%E6%BC%94%E7%BF%92%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E8%A1%A8%E8%A8%98%E3%81%AE%E4%BF%AE%E6%AD%A3%E6%94%B9%E9%80%A0) を申込時までに出来た方 41 | * このドキュメントを一通りお読み頂き、Ruby のビルド、および最初の Hack を楽しんでください。 42 | * 申込時のアンケートに、実際に改造した結果のバージョン表記を入力してください。 43 | * もし、わからないことがあれば、下記 gitter (チャット)などでお問い合わせください。 44 | 45 | 学生さんをある程度優先します。Rubyインタプリタ開発入門というイベントという趣旨に照らして、抽選とありますが、学生の参加希望者の方を中心にしつつも、社会人の方も参加いただけるように参加者の選考をさせていただきます。今回参加いただけない方にも、また別途機会を設けることで、より多くの方にも参加いただけるようにしていきたいと存じますので、よろしくお願いいたします。 46 | 47 | ## 共通課題(1日目)のゴール 48 | 49 | * Ruby のソースコードの構造を知る 50 | * Ruby のビルドができるようになる 51 | * Ruby の中身を弄ることができるようになる 52 | 53 | ## 発展課題(2日目)のゴール(できれば) 54 | 55 | * 未解決問題を解決する 56 | * 実際に Ruby インタプリタへの貢献を体験する 57 | * 開発コミュニティへの参加を体験する 58 | 59 | ## スケジュール 60 | 61 | 注意:時間等は変わる可能性があります。 62 | 63 | ### 開催前 64 | 65 | * 8/13 (月) 募集締め切り 66 | * 8/15 (水) 参加者抽選決定 67 | * Gitter (下記)を用いたオンライン予習サポート(希望者) 68 | 69 | ### 9/10 (月) 一日目 70 | 71 | * 10:00 オープニング 72 | * 10:30 ハックに必要となる事前知識の講義 73 | * 12:00 ランチ 74 | * 13:00 共通課題 75 | * 16:00 発展課題の紹介 76 | * 17:00 解散 77 | 78 | ### 9/13 (木) 二日目 79 | 80 | * 10:00 発展課題の開始 81 | * 12:00 Ruby開発者を交えてのランチ 82 | * 13:00 まつもとゆきひろ氏 特別講演 83 | * 13:30 まつもとゆきひろ氏&Ruby開発者含めての Q&A 84 | * 14:00 発展課題の再開 85 | * 18:30 打ち上げパーティー(参加者と主催者、まつもとゆきひろ氏を含むRuby開発者) 86 | 87 | # コミュニケーションチャンネル 88 | 89 | 下記 Gitter のチャンネルを用意しています。予習での疑問や質問等はこちらまでお気軽にお寄せください。 90 | https://gitter.im/rubyhackchallenge/Lobby 91 | -------------------------------------------------------------------------------- /events/rhc_kosenconf2018.md: -------------------------------------------------------------------------------- 1 | # 高専カンファレンス Ruby Hack Challenge - Rails寺子屋特別編 2 | 3 | Site: https://rails-terakoya.connpass.com/event/88342/ 4 | 5 | ## 当日 7/16(祝月) のスケジュール 6 | 7 | - 9:30 開場 8 | - 当日は休日のため、会場のpixivさんのビルは施錠されています。裏口からスタッフがご案内しますので、裏口付近でお待ちください。裏口はビル正面入口の横にある駐車場のバーの奥にあります。(ローソンとは逆側です) 9 | - また、スタッフの人数も限られておりますため、ご案内の時間を9:30,9:40,9:50,10:00とさせていただきます。その後いらした方はスタッフまでtwitterでメンションください。ご協力をお願いいたします。(スタッフtwitterアカウント @igaiga555 @alitaso346 ) 10 | - 10:00 開始 11 | - (ランチはお弁当を提供予定です) 12 | - 17:00 終了 13 | - 軽くお茶会懇親会 14 | - 18:00 完全撤収 15 | 16 | ## 高専カンファレンス Ruby Hack Challenge とは 17 | Ruby インタプリタを Hack しよう! 18 | 19 | 多くのウェブアプリケーション開発でプログラミング言語 Ruby が利用されています。Ruby で書かれたプログラムを動かすときは Ruby インタプリタで実行します。 20 | 21 | Ruby Hack Challenge は、この Ruby インタプリタに対して機能を追加したり、改良したり、性能向上させたりする方法、つまり Ruby インタプリタを Hack する方法をお伝えするイベントです。 22 | 23 | 初級としては表示されるバージョン番号を変えてみたり、上級としてはRubyの未解決問題に挑戦したり、参加者のみなさんのレベルに応じて取り組める課題を用意しています。 24 | 25 | ## イベント概要 26 | Ruby インタプリタをハックします。共通課題として、用意する資料を手順どおりに進めて頂きます。発展課題として、Ruby インタプリタに残る未解決問題に取り組んで頂きます。 27 | 28 | 本イベントは、過去に行われているRuby Hack Challengeを1日開催用に再構成したものです。 29 | 30 | 本イベントは前日に開催される高専カンファレンス in Tokyo 2018とは独立した別のイベントです。高専カンファレンス in Tokyo 2018を応援しています。また、Railsは特に関係ありませんが、Rails寺子屋の特別編でもあります。 31 | 32 | - 高専カンファレンス in Tokyo 2018 [https://kosenconf.tokyo](https://kosenconf.tokyo) 33 | 34 | ## 講師 35 | 36 | 笹田耕一 (クックパッド株式会社) 37 | 38 | Rubyコミッタ。YARV(Ruby実行処理系VM)、世代別GCなどRubyの根幹を支える部分の開発者。現在はRuby3.0へ向けた新しい並列実行モデル"Guild"を開発中。日本Rubyの会理事。未踏ユース スーパークリエータ。 39 | 40 | ## こんな方に来てほしい 41 | - Ruby インタプリタの Hack がしてみたい方 42 | - Ruby プログラムは書けるけど、どうやって動いているのか知りたい方 43 | - プログラミング言語 Ruby および Ruby インタプリタを普段開発しているような人達が、何を考えているのか知りたい方 44 | - 難易度の高いプログラミングに挑戦してみたい方 45 | - 前日にある高専カンファレンスに来たのでついでにRubyをHackしてやるぞという方 46 | - 夏休みの思い出が欲しい方 47 | 48 | ## 参加要件 49 | Ruby インタプリタのビルドが可能なノートパソコンを当日持ち込める方 50 | 51 | ## 申し込み〆切と選考 52 | - 今回は、高専およびその他の学生の参加希望者の方を中心に参加枠を作っております。 53 | - 各枠の人数は変動することもあります。 54 | - 空席がある限り、先着で申し込みできます。 55 | 56 | ## 当日の進め方 57 | 58 | - あらかじめ、セットアップまで終わらせておくのがお勧めです 59 | - 資料 60 | - https://docs.google.com/document/d/1Za9HvW2Oo-QMjC5NLYioZ5V6Bm-a99cP02oLr8ZZVxw/ 61 | - https://github.com/ko1/rubyhackchallenge/tree/master/JA 62 | 63 | 64 | ## ゴール(いけるところまで) 65 | - Ruby のソースコードの構造を知る 66 | - Ruby のビルドができるようになる 67 | - Ruby の中身を弄ることができるようになる 68 | - 未解決問題を解決する 69 | - 実際に Ruby インタプリタへの貢献を体験する 70 | - 開発コミュニティへの参加を体験する 71 | 72 | ## 参考ページ 73 | - Rubyのなかを覗いてみよう!池澤あやかが「Cookpad Ruby Hack Challenge」に参加してみた 74 | - https://next.rikunabi.com/journal/20180601_c11/ 75 | - Cookpad Ruby Hack Challenge 開催報告 76 | - http://techlife.cookpad.com/entry/2017/09/29/224024 77 | - Ruby Hack Challenge の良さについて書いてみる 78 | - http://at284km.hatenablog.com/entry/2018/06/26/101928 -------------------------------------------------------------------------------- /yasm/README.md: -------------------------------------------------------------------------------- 1 | "Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう" で利用したソースコード群です。 2 | 資料は https://www.slideshare.net/KoichiSasada/cookpad-17-day-tech-internship-2017-ruby にあります。 3 | -------------------------------------------------------------------------------- /yasm/asm/answers.rb: -------------------------------------------------------------------------------- 1 | require_relative '../yasm' 2 | 3 | def assert iseq, expected 4 | r = iseq.eval 5 | if r == expected 6 | puts "==> OK: #{iseq.label}@#{iseq.path} success." 7 | else 8 | puts "!" * 70 9 | puts "==> NG: #{iseq.label}@#{iseq.path} fails (epxected: #{expected.inspect}, actual: #{r.inspect})." 10 | end 11 | end 12 | 13 | # 1 14 | iseq = YASM.asm label: 'integer:1' do 15 | putobject 1 16 | leave 17 | end 18 | 19 | assert iseq, 1 20 | 21 | # 1_000_000 22 | iseq = YASM.asm label: 'integer:1_000_000' do 23 | putobject 1_000_000 24 | leave 25 | end 26 | 27 | assert iseq, 1_000_000 28 | 29 | # :ok 30 | iseq = YASM.asm label: 'symbol:ok' do 31 | putobject :ok 32 | leave 33 | end 34 | 35 | assert iseq, :ok 36 | 37 | # :ng 38 | iseq = YASM.asm label: 'symbol:ng' do 39 | putobject :ng 40 | leave 41 | end 42 | 43 | assert iseq, :ng 44 | 45 | # "hello" 46 | iseq = YASM.asm label: 'string:hello' do 47 | putstring "hello" 48 | leave 49 | end 50 | 51 | assert iseq, "hello" 52 | 53 | # a = 1; a 54 | iseq = YASM.asm label: 'local_variables' do 55 | putobject 1 56 | setlocal :a 57 | getlocal :a 58 | leave 59 | end 60 | 61 | assert iseq, 1 62 | 63 | # self 64 | iseq = YASM.asm label: 'self' do 65 | putself 66 | leave 67 | end 68 | 69 | assert iseq, self 70 | 71 | # nil 72 | iseq = YASM.asm label: 'nil' do 73 | putnil 74 | leave 75 | end 76 | 77 | assert iseq, nil 78 | 79 | # method call: 1 < 10 #=> true ( 1.<(10) #=> true ) 80 | iseq = YASM.asm label: '1.<(10)' do 81 | putobject 1 82 | putobject 10 83 | send :<, 1 84 | leave 85 | end 86 | 87 | assert iseq, true 88 | 89 | # method call: p(1) #=> 1 90 | iseq = YASM.asm label: 'p(1)' do 91 | putself 92 | putobject 1 93 | send :p, 1, YASM::FCALL 94 | leave 95 | end 96 | 97 | assert iseq, 1 98 | 99 | # combination: 1 - 2 * 3 #=> -5 100 | iseq = YASM.asm label: '1 - 2 * 3' do 101 | putobject 1 102 | putobject 2 103 | putobject 3 104 | send :*, 1 105 | send :-, 1 106 | leave 107 | end 108 | 109 | assert iseq, -5 110 | 111 | # combination: a = 1; b = 2; c = 3; a - b * c #=> -5 112 | iseq = YASM.asm label: 'a = 1; b = 2; c = 3; a - b * c' do 113 | putobject 1 114 | setlocal :a 115 | putobject 2 116 | setlocal :b 117 | putobject 3 118 | setlocal :c 119 | 120 | getlocal :a 121 | getlocal :b 122 | getlocal :c 123 | send :*, 1 124 | send :-, 1 125 | leave 126 | end 127 | 128 | assert iseq, -5 129 | 130 | # combination: a = 10; p(a > 1) #=> true 131 | iseq = YASM.asm label: 'a = 10; p(a > 1)' do 132 | # a = 10 133 | putobject 10 134 | setlocal :a 135 | 136 | putself # receiver of p() 137 | 138 | # a > 1 139 | getlocal :a 140 | putobject 1 141 | send :>, 1 142 | 143 | # p(result) 144 | send :p, 1, YASM::FCALL 145 | 146 | leave 147 | end 148 | 149 | assert iseq, true 150 | 151 | # combination: p(‘foo’.upcase) #=> 'FOO' 152 | iseq = YASM.asm label: 'a = 1; b = 2; c = 3; a + b * c' do 153 | putself 154 | putstring 'foo' 155 | send(:upcase, 0) 156 | send(:p, 1, YASM::FCALL) 157 | leave 158 | end 159 | 160 | assert iseq, 'FOO' 161 | 162 | # if statement 163 | iseq = YASM.asm label: 'if' do 164 | # a = 10 165 | putobject 10 166 | setlocal :a 167 | 168 | # a > 1 169 | getlocal :a 170 | putobject 1 171 | send :>, 1 172 | 173 | # conditional jump 174 | branchunless :if_else 175 | 176 | # p(:ok) 177 | putself 178 | putobject :ok 179 | send :p, 1, YASM::FCALL 180 | 181 | # jump to end 182 | jump :if_end 183 | 184 | label(:if_else) 185 | # p(:ng) 186 | putself 187 | putobject :ng 188 | send :p, 1, YASM::FCALL 189 | 190 | label(:if_end) 191 | leave 192 | end 193 | 194 | assert iseq, :ok 195 | 196 | # if statement without else (1) 197 | iseq = YASM.asm label: 'if_without_else1' do 198 | # a = 10 199 | putobject 10 200 | setlocal :a 201 | 202 | # a > 1 203 | getlocal :a 204 | putobject 1 205 | send :>, 1 206 | 207 | # conditional jump 208 | branchunless :if_else 209 | 210 | # p(:ok) 211 | putself 212 | putobject :ok 213 | send :p, 1, YASM::FCALL 214 | 215 | # jump to end 216 | jump :if_end 217 | 218 | label(:if_else) 219 | # nil 220 | putnil 221 | 222 | label(:if_end) 223 | leave 224 | end 225 | 226 | assert iseq, :ok 227 | 228 | # if statement without else (2) 229 | iseq = YASM.asm label: 'if_without_else2' do 230 | # a = 10 231 | putobject 10 232 | setlocal :a 233 | 234 | # a > 1 235 | getlocal :a 236 | putobject 1 237 | send :<, 1 238 | 239 | # conditional jump 240 | branchunless :if_else 241 | 242 | # p(:ok) 243 | putself 244 | putobject :ok 245 | send :p, 1, YASM::FCALL 246 | 247 | # jump to end 248 | jump :if_end 249 | 250 | label(:if_else) 251 | # nil 252 | putnil 253 | 254 | label(:if_end) 255 | leave 256 | end 257 | 258 | assert iseq, nil 259 | 260 | # while 261 | iseq = YASM.asm label: 'while' do 262 | putobject 0 263 | setlocal :a 264 | 265 | label :when_cond 266 | # condition 267 | getlocal :a 268 | putobject 10 269 | send :<, 1 270 | 271 | # conditional jump 272 | branchunless :when_end 273 | 274 | # p a 275 | putself 276 | getlocal :a 277 | send :p, 1, YASM::FCALL 278 | pop 279 | 280 | # a += 1 #=> a = a.+(1) 281 | 282 | # a.+(1) 283 | getlocal :a 284 | putobject 1 285 | send :+, 1 286 | 287 | # a = ... 288 | setlocal :a 289 | 290 | jump :when_cond 291 | 292 | label :when_end 293 | getlocal :a 294 | leave 295 | end 296 | 297 | assert iseq, 10 298 | 299 | # def foo(); end 300 | iseq = YASM.asm label: 'def:foo()' do 301 | define_method_macro :foo do 302 | putnil 303 | leave 304 | end 305 | 306 | leave 307 | end 308 | 309 | assert iseq, :foo 310 | 311 | # def foo(a); a; end; foo(100) 312 | iseq = YASM.asm label: 'def:foo(a)' do 313 | define_method_macro :foo, parameters: [:a] do 314 | getlocal :a # as usual local variables 315 | leave 316 | end 317 | pop 318 | 319 | putself 320 | putobject 100 321 | send :foo, 1, YASM::FCALL 322 | 323 | leave 324 | end 325 | 326 | assert iseq, 100 327 | 328 | # def fib 329 | iseq = YASM.asm label: 'fib' do 330 | define_method_macro :fib, parameters: [:n] do 331 | # n < 2 332 | getlocal :n 333 | putobject 2 334 | send :<, 1 335 | 336 | branchunless :if_else 337 | 338 | # 1 339 | putobject 1 340 | jump :if_end 341 | 342 | label :if_else 343 | # fib(n-2) 344 | putself 345 | 346 | # n-2 => n.-(2) 347 | getlocal :n 348 | putobject 2 349 | send :-, 1 350 | 351 | # fib(n-2) 352 | send :fib, 1, YASM::FCALL 353 | 354 | # fib(n-1) 355 | putself 356 | 357 | # n-1 => n.-(1) 358 | getlocal :n 359 | putobject 1 360 | send :-, 1 361 | 362 | # fib(n-1) 363 | send :fib, 1, YASM::FCALL 364 | 365 | # fib() + fib() 366 | send :+, 1 367 | 368 | label :if_end 369 | leave 370 | end 371 | pop 372 | 373 | # fib(10) 374 | putself 375 | putobject 10 376 | send :fib, 1, YASM::FCALL 377 | 378 | leave 379 | end 380 | 381 | assert iseq, 89 382 | -------------------------------------------------------------------------------- /yasm/asm/asmfib.rb: -------------------------------------------------------------------------------- 1 | require_relative '../yasm' 2 | 3 | # def fib 4 | iseq = YASM.asm label: 'define-fib' do 5 | # copy your fib definition here 6 | end 7 | 8 | iseq.eval 9 | 10 | n = Integer(ARGV.shift || 35) 11 | puts "fib(#{n}) = #{fib(n)}" 12 | -------------------------------------------------------------------------------- /yasm/asm/fastfib.rb: -------------------------------------------------------------------------------- 1 | def fib n 2 | a, b = 1, 1 3 | n.times{ 4 | a, b = b, a+b 5 | } 6 | a 7 | end 8 | 9 | n = Integer(ARGV.shift || 35) 10 | puts "fib(#{n}) = #{fib(n)}" 11 | -------------------------------------------------------------------------------- /yasm/asm/fib.rb: -------------------------------------------------------------------------------- 1 | def fib(n) 2 | if n < 2 3 | 1 4 | else 5 | fib(n-2) + fib(n-1) 6 | end 7 | end 8 | 9 | n = Integer(ARGV.shift || 35) 10 | puts "fib(#{n}) = #{fib(n)}" 11 | -------------------------------------------------------------------------------- /yasm/asm/task.rb: -------------------------------------------------------------------------------- 1 | require_relative '../yasm' 2 | 3 | def assert iseq, expected 4 | r = iseq.eval 5 | if r == expected 6 | puts "==> OK: #{iseq.label}@#{iseq.path} success." 7 | else 8 | puts "!" * 70 9 | puts "==> NG: #{iseq.label}@#{iseq.path} fails (epxected: #{expected.inspect}, actual: #{r.inspect})." 10 | exit 1 11 | end 12 | end 13 | 14 | # 1 15 | iseq = YASM.asm label: 'A-1: integer:1' do 16 | putobject :replace_me 17 | leave 18 | end 19 | 20 | assert iseq, 1 21 | 22 | # 1_000_000 23 | iseq = YASM.asm label: 'A-1: integer:1_000_000' do 24 | putobject :replace_me 25 | leave 26 | end 27 | 28 | assert iseq, 1_000_000 29 | 30 | # :ok 31 | iseq = YASM.asm label: "A-1': symbol:ok" do 32 | putobject :replace_me 33 | leave 34 | end 35 | 36 | assert iseq, :ok 37 | 38 | # :ng 39 | iseq = YASM.asm label: "A-1': symbol:ng" do 40 | putobject :replace_me 41 | leave 42 | end 43 | 44 | assert iseq, :ng 45 | 46 | # "hello" 47 | iseq = YASM.asm label: "A-1'': string:hello" do 48 | putobject :replace_me 49 | leave 50 | end 51 | 52 | assert iseq, "hello" 53 | 54 | # a = 1; a 55 | iseq = YASM.asm label: 'A-2: local_variables' do 56 | putobject :replace_me 57 | leave 58 | end 59 | 60 | assert iseq, 1 61 | 62 | # self 63 | iseq = YASM.asm label: 'A-3: self' do 64 | putobject :replace_me 65 | leave 66 | end 67 | 68 | assert iseq, self 69 | 70 | # nil 71 | iseq = YASM.asm label: 'A-3: nil' do 72 | putobject :replace_me 73 | leave 74 | end 75 | 76 | assert iseq, nil 77 | 78 | # method call: 1 < 10 #=> true ( 1.<(10) #=> true ) 79 | iseq = YASM.asm label: 'A-4: 1.<(10)' do 80 | putobject :replace_me 81 | leave 82 | end 83 | 84 | assert iseq, true 85 | 86 | # method call: p(1) #=> 1 87 | iseq = YASM.asm label: 'A-4: p(1)' do 88 | putobject :replace_me 89 | leave 90 | end 91 | 92 | assert iseq, 1 93 | 94 | # combination: 1 - 2 * 3 #=> -5 95 | iseq = YASM.asm label: "A-4': 1 - 2 * 3" do 96 | putobject :replace_me 97 | leave 98 | end 99 | 100 | assert iseq, -5 101 | 102 | # combination: a = 10; p(a > 1) #=> true 103 | iseq = YASM.asm label: "A-4': a = 10; p(a > 1)" do 104 | putobject :replace_me 105 | leave 106 | end 107 | 108 | assert iseq, true 109 | 110 | # combination: a = 1; b = 2; c = 3; a - b * c #=> -5 111 | iseq = YASM.asm label: "A-4': a = 1; b = 2; c = 3; a - b * c" do 112 | putobject :replace_me 113 | leave 114 | end 115 | 116 | assert iseq, -5 117 | 118 | # combination: p('foo'.upcase) #=> 'FOO' 119 | iseq = YASM.asm label: "A-4': p('foo'.upcase)" do 120 | putobject :replace_me 121 | leave 122 | end 123 | 124 | assert iseq, 'FOO' 125 | 126 | # if statement 127 | iseq = YASM.asm label: 'A-5: if' do 128 | putobject :replace_me 129 | leave 130 | end 131 | 132 | assert iseq, :ok 133 | 134 | # if statement without else (1) 135 | iseq = YASM.asm label: "A-5': if_without_else1" do 136 | putobject :replace_me 137 | leave 138 | end 139 | 140 | assert iseq, :ok 141 | 142 | # if statement without else (2) 143 | iseq = YASM.asm label: "A-5': if_without_else2" do 144 | putobject :replace_me 145 | leave 146 | end 147 | 148 | assert iseq, nil 149 | 150 | # while 151 | iseq = YASM.asm label: "A-6: while" do 152 | putobject :replace_me 153 | leave 154 | end 155 | 156 | assert iseq, 10 157 | 158 | # def foo(); end 159 | iseq = YASM.asm label: "A-7: def:foo()" do 160 | putobject :replace_me 161 | leave 162 | end 163 | 164 | assert iseq, :foo 165 | 166 | # def foo(a); a; end; foo(100) 167 | iseq = YASM.asm label: 'A-7: def:foo(a)' do 168 | putobject :replace_me 169 | leave 170 | end 171 | 172 | assert iseq, 100 173 | 174 | # def fib 175 | iseq = YASM.asm label: 'A-7: fib' do 176 | putobject :replace_me 177 | leave 178 | end 179 | 180 | assert iseq, 89 181 | -------------------------------------------------------------------------------- /yasm/ast2iseq/ast2iseq.rb: -------------------------------------------------------------------------------- 1 | require 'pp' 2 | require_relative './ruby2ast' 3 | require_relative '../yasm' 4 | 5 | # recursive function version 6 | # require_relative './ast2iseq_ans_func' 7 | # See ./ast2iseq_func.rb for skelton 8 | 9 | # composite pattern version 10 | # require_relative './ast2iseq_ans_composite' 11 | # See ./ast2iseq_composite.rb for skelton 12 | 13 | # visitor pattern version 14 | # require_relative './ast2iseq_ans_visitor' 15 | # See ./ast2iseq_visitor.rb for skelton 16 | 17 | # if you want to modify ast2iseq_visitor.rb, remove `#' of the following line. 18 | require_relative 'ast2iseq_visitor' 19 | 20 | def ast2iseq ast 21 | # define your ast2iseq method here 22 | raise "not implemented yet" 23 | end unless defined?(ast2iseq) 24 | 25 | if $0 == __FILE__ 26 | script = <<-EOS 27 | # fill your script here 28 | 29 | 1 30 | 31 | ####################### 32 | EOS 33 | ast = Ruby2AST.to_ast(script) 34 | pp ast 35 | iseq = ast2iseq(ast) 36 | puts iseq.disasm 37 | p iseq.eval 38 | end 39 | -------------------------------------------------------------------------------- /yasm/ast2iseq/ast2iseq_ans_composite.rb: -------------------------------------------------------------------------------- 1 | $label_no = 0 2 | def gen_label 3 | :"label_#{$label_no+=1}" 4 | end 5 | 6 | class ProgramNode 7 | def process yasm 8 | seq_node.process yasm 9 | yasm.leave 10 | end 11 | end 12 | 13 | class SequenceNode 14 | def process yasm 15 | nodes = self.nodes.dup 16 | last_node = nodes.pop 17 | nodes.each{|n| 18 | n.process yasm 19 | yasm.pop 20 | } 21 | if last_node 22 | last_node.process yasm 23 | else 24 | yasm.putnil 25 | end 26 | end 27 | end 28 | 29 | class NilNode 30 | def process yasm 31 | yasm.putnil 32 | end 33 | end 34 | 35 | class SelfNode 36 | def process yasm 37 | yasm.putself 38 | end 39 | end 40 | 41 | class LiteralNode 42 | def process yasm 43 | yasm.putobject self.obj 44 | end 45 | end 46 | 47 | class StringLiteralNode 48 | def process yasm 49 | yasm.putstring self.obj 50 | end 51 | end 52 | 53 | class LvarAssignNode 54 | def process yasm 55 | value_node.process yasm 56 | yasm.dup 57 | yasm.setlocal self.lvar_id 58 | end 59 | end 60 | 61 | class LvarNode 62 | def process yasm 63 | yasm.getlocal self.lvar_id 64 | end 65 | end 66 | 67 | class SendNode 68 | def process yasm 69 | self.receiver_node.process yasm 70 | self.argument_nodes.each{|e| e.process yasm} 71 | if self.type == :fcall 72 | yasm.send self.method_id, self.argument_nodes.size, YASM::FCALL 73 | else 74 | yasm.send self.method_id, self.argument_nodes.size 75 | end 76 | end 77 | end 78 | 79 | class IfNode 80 | def process yasm 81 | else_label = gen_label 82 | end_label = gen_label 83 | 84 | self.cond_node.process yasm 85 | yasm.branchunless else_label 86 | self.body_node.process yasm 87 | yasm.jump end_label 88 | yasm.label else_label 89 | self.else_node.process yasm 90 | yasm.label end_label 91 | end 92 | end 93 | 94 | class WhileNode 95 | def process yasm 96 | begin_label = gen_label 97 | end_label = gen_label 98 | 99 | yasm.label begin_label 100 | self.cond_node.process yasm 101 | yasm.branchunless end_label 102 | self.body_node.process yasm 103 | yasm.pop 104 | yasm.jump begin_label 105 | yasm.label end_label 106 | yasm.putnil 107 | end 108 | end 109 | 110 | class DefNode 111 | def process yasm 112 | method_iseq = ast2iseq(self) 113 | yasm.putspecialobject 1 114 | yasm.putobject self.name 115 | yasm.putiseq method_iseq.to_a 116 | yasm.send :"core#define_method", 2 117 | end 118 | end 119 | 120 | def ast2iseq node 121 | if DefNode === node 122 | yasm = YASM.new label: node.name.to_s, type: :method, parameters: node.parameters 123 | node = node.body 124 | else 125 | yasm = YASM.new 126 | end 127 | 128 | node.process yasm 129 | yasm.to_iseq 130 | end 131 | -------------------------------------------------------------------------------- /yasm/ast2iseq/ast2iseq_ans_func.rb: -------------------------------------------------------------------------------- 1 | $label_no = 0 2 | def gen_label 3 | :"label_#{$label_no+=1}" 4 | end 5 | 6 | # function version 7 | def ast2iseq_rec node, yasm 8 | case node 9 | when ProgramNode 10 | ast2iseq_rec node.seq_node, yasm 11 | yasm.leave 12 | 13 | when SequenceNode 14 | nodes = node.nodes.dup 15 | last_node = nodes.pop 16 | nodes.each{|n| 17 | ast2iseq_rec n, yasm 18 | yasm.pop 19 | } 20 | if last_node 21 | ast2iseq_rec last_node, yasm 22 | else 23 | yasm.putnil 24 | end 25 | 26 | when SendNode 27 | ast2iseq_rec node.receiver_node, yasm 28 | node.argument_nodes.each{|arg| 29 | ast2iseq_rec arg, yasm 30 | } 31 | if node.type == :fcall 32 | yasm.send node.method_id, node.argument_nodes.size, YASM::FCALL 33 | else 34 | yasm.send node.method_id, node.argument_nodes.size 35 | end 36 | 37 | when SelfNode 38 | yasm.putself 39 | 40 | when LiteralNode 41 | yasm.putobject node.obj 42 | 43 | when NilNode 44 | yasm.putnil 45 | 46 | when IfNode 47 | else_label = gen_label() 48 | end_label = gen_label() 49 | 50 | ast2iseq_rec node.cond_node, yasm 51 | yasm.branchunless else_label 52 | ast2iseq_rec node.body_node, yasm 53 | yasm.jump end_label 54 | yasm.label else_label 55 | ast2iseq_rec node.else_node, yasm 56 | yasm.label end_label 57 | # end 58 | 59 | when WhileNode 60 | begin_label = gen_label() 61 | end_label = gen_label() 62 | 63 | yasm.label begin_label 64 | ast2iseq_rec node.cond_node, yasm 65 | yasm.branchunless end_label 66 | ast2iseq_rec node.body_node, yasm 67 | yasm.pop 68 | yasm.jump begin_label 69 | yasm.label end_label 70 | yasm.putnil 71 | 72 | when DefNode 73 | method_iseq = ast2iseq(node) 74 | 75 | yasm.putspecialobject 1 76 | yasm.putobject node.name 77 | yasm.putiseq method_iseq.to_a 78 | yasm.send :"core#define_method", 2 79 | 80 | when LvarAssignNode 81 | ast2iseq_rec node.value_node, yasm 82 | yasm.dup 83 | yasm.setlocal node.lvar_id 84 | 85 | when LvarNode 86 | yasm.getlocal node.lvar_id 87 | 88 | else 89 | raise "unsupported: #{node.class}" 90 | end 91 | end 92 | 93 | def ast2iseq node 94 | if DefNode === node 95 | yasm = YASM.new label: node.name.to_s, type: :method, parameters: node.parameters 96 | node = node.body 97 | else 98 | yasm = YASM.new 99 | end 100 | 101 | ast2iseq_rec node, yasm 102 | yasm.to_iseq 103 | end 104 | -------------------------------------------------------------------------------- /yasm/ast2iseq/ast2iseq_ans_visitor.rb: -------------------------------------------------------------------------------- 1 | $label_no = 0 2 | def gen_label 3 | :"label_#{$label_no+=1}" 4 | end 5 | 6 | class NodeVisitor 7 | def initialize yasm 8 | @yasm = yasm 9 | end 10 | 11 | def to_iseq 12 | @yasm.to_iseq 13 | end 14 | 15 | def visit node 16 | node_type = node.class.name.to_s.sub(/Node/, '').downcase 17 | send("process_#{node_type}", node) 18 | end 19 | 20 | def process_program node 21 | visit node.seq_node 22 | @yasm.leave 23 | end 24 | 25 | def process_sequence node 26 | nodes = node.nodes.dup 27 | last_node = nodes.pop 28 | nodes.each{|n| 29 | visit n 30 | @yasm.pop 31 | } 32 | 33 | if last_node 34 | visit last_node 35 | else 36 | @yasm.putnil 37 | end 38 | end 39 | 40 | def process_send node 41 | visit node.receiver_node 42 | node.argument_nodes.each{|arg| 43 | visit arg 44 | } 45 | if node.type == :fcall 46 | @yasm.send node.method_id, node.argument_nodes.size, YASM::FCALL 47 | else 48 | @yasm.send node.method_id, node.argument_nodes.size 49 | end 50 | end 51 | 52 | def process_self node 53 | @yasm.putself 54 | end 55 | 56 | def process_literal node 57 | @yasm.putobject node.obj 58 | end 59 | 60 | def process_stringliteral node 61 | @yasm.putstring node.obj 62 | end 63 | 64 | def process_nil node 65 | @yasm.putnil 66 | end 67 | 68 | def process_if node 69 | else_label = gen_label() 70 | end_label = gen_label() 71 | 72 | visit node.cond_node 73 | @yasm.branchunless else_label 74 | visit node.body_node 75 | @yasm.jump end_label 76 | @yasm.label else_label 77 | visit node.else_node 78 | @yasm.label end_label 79 | # end 80 | end 81 | 82 | def process_while node 83 | begin_label = gen_label() 84 | end_label = gen_label() 85 | 86 | @yasm.label begin_label 87 | visit node.cond_node 88 | @yasm.branchunless end_label 89 | visit node.body_node 90 | @yasm.pop 91 | @yasm.jump begin_label 92 | @yasm.label end_label 93 | @yasm.putnil 94 | end 95 | 96 | def process_def node 97 | method_iseq = ast2iseq(node) 98 | @yasm.putspecialobject 1 99 | @yasm.putobject node.name 100 | @yasm.putiseq method_iseq.to_a 101 | @yasm.send :"core#define_method", 2 102 | end 103 | 104 | def process_lvarassign node 105 | visit node.value_node 106 | @yasm.dup 107 | @yasm.setlocal node.lvar_id 108 | end 109 | 110 | def process_lvar node 111 | @yasm.getlocal node.lvar_id 112 | end 113 | end 114 | 115 | def ast2iseq node 116 | if DefNode === node 117 | yasm = YASM.new label: node.name.to_s, type: :method, parameters: node.parameters 118 | node = node.body 119 | else 120 | yasm = YASM.new 121 | end 122 | visitor = NodeVisitor.new(yasm) 123 | visitor.visit node 124 | visitor.to_iseq 125 | end 126 | -------------------------------------------------------------------------------- /yasm/ast2iseq/ast2iseq_composite.rb: -------------------------------------------------------------------------------- 1 | $label_no = 0 2 | def gen_label 3 | :"label_#{$label_no+=1}" 4 | end 5 | 6 | class ProgramNode 7 | def process yasm 8 | raise "not supported (#{self.class})" 9 | end 10 | end 11 | 12 | class SequenceNode 13 | def process yasm 14 | raise "not supported (#{self.class})" 15 | end 16 | end 17 | 18 | class NilNode 19 | def process yasm 20 | raise "not supported (#{self.class})" 21 | end 22 | end 23 | 24 | class SelfNode 25 | def process yasm 26 | raise "not supported (#{self.class})" 27 | end 28 | end 29 | 30 | class LiteralNode 31 | def process yasm 32 | raise "not supported (#{self.class})" 33 | end 34 | end 35 | 36 | class StringLiteralNode 37 | def process yasm 38 | raise "not supported (#{self.class})" 39 | end 40 | end 41 | 42 | class LvarAssignNode 43 | def process yasm 44 | raise "not supported (#{self.class})" 45 | end 46 | end 47 | 48 | class LvarNode 49 | def process yasm 50 | raise "not supported (#{self.class})" 51 | end 52 | end 53 | 54 | class SendNode 55 | def process yasm 56 | raise "not supported (#{self.class})" 57 | end 58 | end 59 | 60 | class IfNode 61 | def process yasm 62 | raise "not supported (#{self.class})" 63 | end 64 | end 65 | 66 | class WhileNode 67 | def process yasm 68 | raise "not supported (#{self.class})" 69 | end 70 | end 71 | 72 | class DefNode 73 | def process yasm 74 | raise "not supported (#{self.class})" 75 | end 76 | end 77 | 78 | def ast2iseq node 79 | if DefNode === node 80 | yasm = YASM.new label: node.name.to_s, type: :method, parameters: node.parameters 81 | node = node.body 82 | else 83 | yasm = YASM.new 84 | end 85 | 86 | node.process yasm 87 | yasm.to_iseq 88 | end 89 | -------------------------------------------------------------------------------- /yasm/ast2iseq/ast2iseq_func.rb: -------------------------------------------------------------------------------- 1 | $label_no = 0 2 | def gen_label 3 | :"label_#{$label_no+=1}" 4 | end 5 | 6 | # function version 7 | def ast2iseq_rec node, yasm 8 | case node 9 | when ProgramNode 10 | raise "not supported (#{node.class})" 11 | 12 | when SequenceNode 13 | raise "not supported (#{node.class})" 14 | 15 | when SendNode 16 | raise "not supported (#{node.class})" 17 | 18 | when SelfNode 19 | raise "not supported (#{node.class})" 20 | 21 | when LiteralNode 22 | raise "not supported (#{node.class})" 23 | 24 | when NilNode 25 | raise "not supported (#{node.class})" 26 | 27 | when IfNode 28 | raise "not supported (#{node.class})" 29 | 30 | when WhileNode 31 | raise "not supported (#{node.class})" 32 | 33 | when DefNode 34 | method_iseq = ast2iseq(node) 35 | # call "core#define_method" explicitly 36 | # see define_method_macro at yasm.rb. 37 | raise "not supported (#{node.class})" 38 | 39 | when LvarAssignNode 40 | raise "not supported (#{node.class})" 41 | 42 | when LvarNode 43 | raise "not supported (#{node.class})" 44 | 45 | else 46 | raise "unsupported: #{node.class}" 47 | end 48 | end 49 | 50 | def ast2iseq node 51 | if DefNode === node 52 | yasm = YASM.new label: node.name.to_s, type: :method, parameters: node.parameters 53 | node = node.body 54 | else 55 | yasm = YASM.new 56 | end 57 | 58 | ast2iseq_rec node, yasm 59 | yasm.to_iseq 60 | end 61 | -------------------------------------------------------------------------------- /yasm/ast2iseq/ast2iseq_visitor.rb: -------------------------------------------------------------------------------- 1 | $label_no = 0 2 | def gen_label 3 | :"label_#{$label_no+=1}" 4 | end 5 | 6 | class NodeVisitor 7 | def initialize yasm 8 | @yasm = yasm 9 | end 10 | 11 | def to_iseq 12 | @yasm.to_iseq 13 | end 14 | 15 | def visit node 16 | node_type = node.class.name.to_s.sub(/Node/, '').downcase 17 | send("process_#{node_type}", node) 18 | end 19 | 20 | def process_program node 21 | visit(node.seq_node) 22 | @yasm.leave 23 | end 24 | 25 | def process_sequence node 26 | node.nodes.each{|n| 27 | visit(n) 28 | } 29 | end 30 | 31 | def process_send node 32 | raise "not implemented yet: #{node}" 33 | end 34 | 35 | def process_self node 36 | raise "not implemented yet: #{node}" 37 | end 38 | 39 | def process_literal node 40 | obj = node.obj 41 | @yasm.putobject obj 42 | end 43 | 44 | def process_stringliteral node 45 | raise "not implemented yet: #{node}" 46 | end 47 | 48 | def process_nil node 49 | raise "not implemented yet: #{node}" 50 | end 51 | 52 | def process_if node 53 | raise "not implemented yet: #{node}" 54 | end 55 | 56 | def process_while node 57 | raise "not implemented yet: #{node}" 58 | end 59 | 60 | def process_def node 61 | method_iseq = ast2iseq(node) 62 | 63 | # call "core#define_method" explicitly 64 | # see define_method_macro at yasm.rb. 65 | raise "not implemented yet: #{node}" 66 | end 67 | 68 | def process_lvarassign node 69 | raise "not implemented yet: #{node}" 70 | end 71 | 72 | def process_lvar node 73 | raise "not implemented yet: #{node}" 74 | end 75 | end 76 | 77 | def ast2iseq node 78 | if DefNode === node 79 | yasm = YASM.new label: node.name.to_s, type: :method, parameters: node.parameters 80 | node = node.body 81 | else 82 | yasm = YASM.new 83 | end 84 | visitor = NodeVisitor.new(yasm) 85 | visitor.visit node 86 | visitor.to_iseq 87 | end 88 | -------------------------------------------------------------------------------- /yasm/ast2iseq/ruby2ast.rb: -------------------------------------------------------------------------------- 1 | require 'ripper' 2 | require 'pp' 3 | require_relative './ruby_nodes' 4 | 5 | module Ruby2AST 6 | class << self 7 | def to_ast(script) 8 | sexp = Ripper.sexp(script) 9 | sexp2ast sexp 10 | end 11 | 12 | def seq2ary node 13 | if SequenceNode === node 14 | node.nodes 15 | else 16 | [node] 17 | end 18 | end 19 | 20 | def seq nodes 21 | if nodes.nil? 22 | SequenceNode.new [] 23 | else 24 | SequenceNode.new nodes.map{|node| 25 | sexp2ast node 26 | } 27 | end 28 | end 29 | 30 | def sexp2ast node 31 | type, *data = node 32 | 33 | case type 34 | when :program 35 | ProgramNode.new seq(data[0]) 36 | when :void_stmt 37 | NilNode.new 38 | 39 | when :assign 40 | lhs = data[0] 41 | rhs = data[1] 42 | 43 | case lhs[0] 44 | when :var_field 45 | lvar_id = lhs[1][1].to_sym 46 | LvarAssignNode.new lvar_id, sexp2ast(rhs) 47 | else 48 | raise "unsupported #{lhs.inspect}" 49 | end 50 | 51 | when :opassign 52 | lhs, op, rhs = data 53 | lvar_name = lhs[1][1].to_sym 54 | optype = op[1].to_sym 55 | case optype 56 | when :'+=' 57 | op = :+ 58 | when :'-=' 59 | op = :- 60 | else 61 | raise "unsupported opassign #{optype}" 62 | end 63 | 64 | # foo += exp 65 | # => 66 | # foo = foo + exp 67 | args = seq2ary(sexp2ast(rhs)) 68 | LvarAssignNode.new(lvar_name, 69 | SendNode.new(:call, 70 | LvarNode.new(lvar_name), 71 | op, 72 | *args)) 73 | # 74 | when :binary 75 | receiver = data[0] 76 | method_id = data[1] 77 | arg = data[2] 78 | 79 | SendNode.new(:call, 80 | sexp2ast(receiver), 81 | method_id, 82 | sexp2ast(arg)) 83 | 84 | when :method_add_arg 85 | call_info = data[0] 86 | call_type = call_info[0] 87 | 88 | args_info = data[1] 89 | args = seq2ary(sexp2ast(args_info)) 90 | 91 | case call_type 92 | when :fcall 93 | method_id = call_info[1][1].to_sym 94 | args = seq2ary(sexp2ast(args_info)) 95 | SendNode.new :fcall, SelfNode.new, method_id, *args 96 | when :call 97 | receiver = sexp2ast call_info[1] 98 | method_id = call_info[3][1].to_sym 99 | SendNode.new :call, receiver, method_id, *args 100 | end 101 | 102 | when :command 103 | method_name = data[0][1].to_sym 104 | args_info = data[1] 105 | SendNode.new :fcall, SelfNode.new, method_name, *seq2ary(sexp2ast(args_info)) 106 | 107 | when :call 108 | receiver, _, (_, mid, _) = data 109 | SendNode.new :call, sexp2ast(receiver), mid.to_sym 110 | 111 | when :vcall 112 | method_name = data[0][1].to_sym 113 | SendNode.new :fcall, SelfNode.new, method_name 114 | 115 | when :arg_paren 116 | if data[0] 117 | sexp2ast data[0] 118 | else 119 | SequenceNode.new [] 120 | end 121 | 122 | when :args_add_block 123 | seq data[0] 124 | 125 | when :var_ref 126 | vtype, vname, = data[0] 127 | case vtype 128 | when :@ident 129 | lvar = node[1][1].to_sym 130 | LvarNode.new lvar 131 | when :@kw 132 | case vname 133 | when 'self' 134 | SelfNode.new 135 | when 'nil' 136 | NilNode.new 137 | when 'true' 138 | LiteralNode.new true 139 | when 'false' 140 | LiteralNode.new false 141 | else 142 | raise "unsupported kw #{vname.inspect}" 143 | end 144 | else 145 | raise "unsupported: unkwnon node #{node[1][0]}" 146 | end 147 | 148 | when :if 149 | cond, body, (_, else_body) = data 150 | IfNode.new sexp2ast(cond), seq(body), seq(else_body) 151 | 152 | when :while 153 | cond, body = data 154 | WhileNode.new sexp2ast(cond), seq(body) 155 | 156 | when :def 157 | (_, name, _) = data[0] 158 | param_info = data[1] 159 | param_info = param_info[1] if param_info[0] 160 | (_, parameters, ) = param_info 161 | if parameters 162 | parameters = parameters.map{|e| e[1].to_sym} 163 | else 164 | parameters = [] 165 | end 166 | body = data[2][1] 167 | 168 | DefNode.new name.to_sym, parameters || [], ProgramNode.new(seq(body)) 169 | 170 | when :paren 171 | seq data[0] 172 | 173 | when :@int 174 | LiteralNode.new data.first.to_i 175 | when :symbol_literal 176 | LiteralNode.new data.first[1][1].to_sym 177 | when :string_literal 178 | StringLiteralNode.new data.first[1][1] 179 | else 180 | raise "unsuppoted: #{node.inspect}" 181 | end 182 | end 183 | end 184 | end 185 | 186 | if $0 == __FILE__ 187 | script = <<-EOS 188 | 189 | def fib(n) 190 | if n < 2 191 | 1 192 | else 193 | fib(n-2) + fib(n-1) 194 | end 195 | end 196 | fib(10) 197 | 198 | __END__ 199 | EOS 200 | ast = Ruby2AST.to_ast(script) 201 | pp ast 202 | end 203 | -------------------------------------------------------------------------------- /yasm/ast2iseq/ruby_nodes.rb: -------------------------------------------------------------------------------- 1 | # Ruby nodes 2 | 3 | class Node 4 | def pretty_print q 5 | name = self.class.name.to_s.sub(/\A.+::/, '') 6 | q.breakable 7 | q.group 2, "#<#{name}" do 8 | instance_variables.each{|e| 9 | q.breakable 10 | q.text "#{e} => " 11 | q.pp instance_variable_get(e) 12 | } 13 | end 14 | q.text ">" 15 | # exit 16 | end 17 | 18 | def accept visitor 19 | visitor.visit(self) 20 | end 21 | end 22 | 23 | class ProgramNode < Node 24 | attr_reader :seq_node 25 | def initialize seq_node 26 | @seq_node = seq_node 27 | end 28 | end 29 | 30 | class SequenceNode < Node 31 | attr_reader :nodes 32 | def initialize nodes 33 | @nodes = nodes # Array 34 | end 35 | end 36 | 37 | class NilNode < Node 38 | end 39 | 40 | class SelfNode < Node 41 | end 42 | 43 | class LiteralNode < Node 44 | attr_reader :obj 45 | 46 | def initialize obj 47 | @obj = obj 48 | end 49 | end 50 | 51 | class StringLiteralNode < LiteralNode 52 | end 53 | 54 | class LvarAssignNode < Node 55 | attr_reader :lvar_id, :value_node 56 | def initialize lvar_id, value_node 57 | @lvar_id = lvar_id 58 | @value_node = value_node 59 | end 60 | end 61 | 62 | class LvarNode < Node 63 | attr_reader :lvar_id 64 | def initialize lvar_id 65 | @lvar_id = lvar_id 66 | end 67 | end 68 | 69 | class SendNode < Node 70 | attr_reader :receiver_node, :method_id, :argument_nodes, :type 71 | def initialize type, receiver_node, method_id, *argument_nodes 72 | @type = type # :call or :fcall 73 | @receiver_node = receiver_node 74 | @method_id = method_id 75 | @argument_nodes = argument_nodes # Array 76 | end 77 | end 78 | 79 | class IfNode < Node 80 | attr_reader :cond_node, :body_node, :else_node 81 | def initialize cond_node, body_node, else_node 82 | @cond_node = cond_node 83 | @body_node = body_node 84 | @else_node = else_node 85 | end 86 | end 87 | 88 | class WhileNode < Node 89 | attr_reader :cond_node, :body_node 90 | def initialize cond_node, body_node 91 | @cond_node = cond_node 92 | @body_node = body_node 93 | end 94 | end 95 | 96 | class DefNode 97 | attr_reader :name, :parameters, :body 98 | def initialize name, parameters, body 99 | @name = name 100 | @parameters = parameters 101 | @body = body 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /yasm/ast2iseq/test.rb: -------------------------------------------------------------------------------- 1 | require_relative './ruby2ast' 2 | require_relative './ast2iseq' 3 | 4 | # [[program, expected_result], ...] 5 | [['1', 1], 6 | ['1_000_000', 1_000_000], 7 | [':ok', :ok], 8 | [':ng', :ng], 9 | ['"hello"', "hello"], 10 | ['a=1; a', 1], 11 | ['self', self], 12 | ['nil', nil], 13 | ['p(1)', 1], 14 | ['1 < 10', true], 15 | ['1 - 2 * 3', -5], 16 | ['a = 1; b = 2; c = 3; a - b * c', -5], 17 | ['a = 10; p(a > 1)', true], 18 | ['p("foo".upcase)', 'FOO'], 19 | [%q{ 20 | a = 10 21 | if a > 1 22 | p :ok 23 | else 24 | p :ng 25 | end 26 | }, :ok], 27 | [%q{ 28 | a = 10 29 | if a > 1 30 | p :ok 31 | end 32 | }, :ok], 33 | [%q{ 34 | a = 10 35 | if a < 1 36 | p :ok 37 | end 38 | }, nil], 39 | [%q{ 40 | a = 0 41 | while(a < 10) 42 | p a 43 | a += 1 #=> a = a.+(1) 44 | end 45 | a #=> 10 46 | }, 10], 47 | [%q{ 48 | def foo() 49 | end 50 | }, :foo], 51 | [%q{ 52 | def foo(a) 53 | a 54 | end 55 | foo(100) 56 | }, 100], 57 | [%q{ 58 | def fib(n) 59 | if n < 2 60 | 1 61 | else 62 | fib(n-2) + fib(n-1) 63 | end 64 | end 65 | fib(10) 66 | }, 89], 67 | ].each do |script, expected| 68 | begin 69 | # puts script; STDOUT.flush 70 | 71 | ast = Ruby2AST.to_ast(script) 72 | iseq = ast2iseq(ast) 73 | actual = iseq.eval 74 | 75 | if actual == expected 76 | puts "==> OK" 77 | else 78 | puts "==> NG: the following code doesn't show correct answer (expected: #{expected.inspect}, actual: #{actual.inspect})" 79 | puts script 80 | end 81 | rescue Exception => e 82 | puts "==> Error: #{e.message} (#{e.class})" 83 | puts "script: #{script.inspect}" 84 | puts e.backtrace.join("\n") 85 | exit 1 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /yasm/try.rb: -------------------------------------------------------------------------------- 1 | require_relative 'yasm' 2 | 3 | script = <" 36 | @path = path || "" 37 | end 38 | 39 | def label label 40 | @seq << label 41 | end 42 | 43 | def newline line 44 | @seq << line 45 | end 46 | 47 | def add insn, *operands 48 | @seq << [insn, *operands] 49 | end 50 | 51 | RubyVM::INSTRUCTION_NAMES.each{|insn| 52 | eval <<-EOS 53 | def #{insn}(*ops) 54 | add(:#{insn}, *ops) 55 | end 56 | EOS 57 | } 58 | undef send 59 | undef getlocal 60 | undef setlocal 61 | 62 | FCALL = 4 63 | VCALL = 8 64 | def send mid, argc, flag = 0 65 | add :send, {mid: mid, orig_argc: argc, flag: flag}, false, nil 66 | end 67 | 68 | def getlocal lid, level = 0 69 | register_lvar lid 70 | add :getlocal, lid, level 71 | end 72 | 73 | def setlocal lid, level = 0 74 | register_lvar lid 75 | add :setlocal, lid, level 76 | end 77 | 78 | def define_method_macro mid, *args, **kw, &b 79 | iseq = YASM.asm(*args, label: mid.to_s, type: :method, **kw, &b) 80 | 81 | self.putspecialobject 1 82 | self.putobject mid.to_sym 83 | self.putiseq iseq.to_a 84 | send :"core#define_method", 2 85 | end 86 | 87 | def asm &b 88 | self.instance_eval(&b) 89 | self 90 | end 91 | 92 | private 93 | 94 | def register_lvar lvar 95 | @local_variables[lvar] ||= @local_variables.size 96 | end 97 | 98 | def each_insn 99 | @seq.each.with_index do |item, idx| 100 | case item 101 | when Integer 102 | # ok (lineno) 103 | when Symbol 104 | # ok (label) 105 | when Array 106 | r = yield(*item) 107 | @seq[idx] = r if r 108 | else 109 | raise "unsupported seq entry: #{item.inspect}" 110 | end 111 | end 112 | end 113 | 114 | def seq2bytecode seq 115 | each_insn do |insn, *operands| 116 | case insn 117 | when :getlocal, :setlocal 118 | l, * = operands 119 | operands[0] = 2 + (@local_variables.size - @local_variables[l]) 120 | [insn, *operands] # replace 121 | else 122 | nil 123 | end 124 | end 125 | 126 | # pp @seq 127 | end 128 | 129 | SIG, MAJOR, MINOR, FORMAT_TYPE, = RubyVM::InstructionSequence.compile('').to_a 130 | 131 | public 132 | 133 | def to_iseq 134 | bytecode = seq2bytecode(@seq) 135 | params = {} 136 | params[:lead_num] = @parameters.size unless @parameters.empty? 137 | ary = [SIG, MAJOR, MINOR, FORMAT_TYPE, 138 | { arg_size: @parameters.size, 139 | local_size: @local_variables.size, 140 | stack_max: @seq.size, 141 | }, 142 | @label, 143 | @path, 144 | nil, 145 | 1, 146 | @type, 147 | @local_variables.keys, 148 | params, 149 | [], # catch_table 150 | bytecode, 151 | ] 152 | RubyVM::InstructionSequence.load(ary) 153 | end 154 | 155 | def disasm 156 | to_iseq.disasm 157 | end 158 | 159 | def eval 160 | to_iseq.eval 161 | end 162 | 163 | def self.asm(**kw, &b) 164 | kw[:path] ||= caller(1).first 165 | self.new(**kw).asm(&b).to_iseq 166 | end 167 | 168 | def self.compile_and_disasm(script) 169 | puts RubyVM::InstructionSequence.compile(script).disasm 170 | end 171 | 172 | def self.compile_and_to_ary(script) 173 | pp RubyVM::InstructionSequence.compile(script).to_a 174 | end 175 | end 176 | 177 | if __FILE__ == $0 178 | 179 | #### Your assmelber 180 | 181 | iseq = YASM.asm do 182 | #################### 183 | # fill your asm here 184 | putobject :foo 185 | leave 186 | end 187 | 188 | # show your asm 189 | puts iseq.disasm 190 | # pp iseq.to_a 191 | puts "== result " + "="*62 192 | p iseq.eval 193 | 194 | end 195 | --------------------------------------------------------------------------------