├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------