{"id":13747270,"url":"https://github.com/tenderlove/tenderjit","last_synced_at":"2025-04-05T08:09:19.456Z","repository":{"id":43062758,"uuid":"292371505","full_name":"tenderlove/tenderjit","owner":"tenderlove","description":"JIT for Ruby that is written in Ruby","archived":false,"fork":false,"pushed_at":"2024-01-12T18:19:57.000Z","size":1106,"stargazers_count":421,"open_issues_count":8,"forks_count":27,"subscribers_count":14,"default_branch":"main","last_synced_at":"2025-03-29T07:09:07.984Z","etag":null,"topics":["jit","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tenderlove.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-09-02T19:07:49.000Z","updated_at":"2025-01-14T04:58:39.000Z","dependencies_parsed_at":"2024-01-13T00:03:04.165Z","dependency_job_id":"131ae454-1eda-4f1c-a068-1747af585dca","html_url":"https://github.com/tenderlove/tenderjit","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tenderlove%2Ftenderjit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tenderlove%2Ftenderjit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tenderlove%2Ftenderjit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tenderlove%2Ftenderjit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tenderlove","download_url":"https://codeload.github.com/tenderlove/tenderjit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247305935,"owners_count":20917208,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["jit","ruby"],"created_at":"2024-08-03T06:01:23.430Z","updated_at":"2025-04-05T08:09:19.438Z","avatar_url":"https://github.com/tenderlove.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# TenderJIT\n\nTenderJIT is an experimental JIT compiler for Ruby written in Ruby.\nIts design is mostly based off [YJIT](https://github.com/shopify/yjit).\n\n## Getting Started with TenderJIT\n\nTenderJIT isn't available as a gem (yet).  To start using it, clone the\nrepository and run the following commands:\n\n```\n$ bundle install\n$ bundle exec rake test\n```\n\nIf the tests pass, then you're ready to go!\n\nTenderJIT currently requires Ruby 3.0.2 or the edge version of Ruby.  It *may*\nwork on 3.0.X, but I haven't tested older versions.\n\n## Running JIT code\n\nRight now, TenderJIT doesn't automatically compile methods.  You must manually\ntell TenderJIT to compile a method.\n\nLet's look at an example:\n\n```ruby\nrequire \"tenderjit\"\n\ndef fib n\n  if n \u003c 3\n    1\n  else\n    fib(n - 1) + fib(n - 2)\n  end\nend\n\njit = TenderJIT.new\njit.compile(method(:fib)) # Compile the `fib` method\n\n# Run the `fib` method with the JIT enabled\njit.enable!\nfib 8\njit.disable!\n```\n\nEventually TenderJIT will compile code automatically, but today it doesn't.\n\nTenderJIT only supports Ruby 3.0.2 and up!\n\n## How does TenderJIT work?\n\nTenderJIT reads each YARV instruction in the target method, then converts that\ninstruction to machine code.\n\nLet's look at an example of this in action.  Say we have a function like this:\n\n```ruby\ndef add a, b\n  a + b\nend\n```\n\nIf we disassemble the method using `RubyVM::InstructionSequence`, we can see\nthe instructions that YARV uses to implement the add method:\n\n```\n$ cat x.rb\ndef add a, b\n  a + b\nend\n\n$ ruby --dump=insns x.rb\n== disasm: #\u003cISeq:\u003cmain\u003e@x.rb:1 (1,0)-(3,3)\u003e (catch: FALSE)\n0000 definemethod                           :add, add                 (   1)[Li]\n0003 putobject                              :add\n0005 leave\n\n== disasm: #\u003cISeq:add@x.rb:1 (1,0)-(3,3)\u003e (catch: FALSE)\nlocal table (size: 2, argc: 2 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])\n[ 2] a@0\u003cArg\u003e   [ 1] b@1\u003cArg\u003e\n0000 getlocal_WC_0                          a@0                       (   2)[LiCa]\n0002 getlocal_WC_0                          b@1\n0004 opt_plus                               \u003ccalldata!mid:+, argc:1, ARGS_SIMPLE\u003e[CcCr]\n0006 leave                                                            (   3)[Re]\n```\n\nThe `add` method calls 4 instructions, 3 of them are unique:\n\n* `getlocal_WC_0`\n* `opt_plus`\n* `leave`\n\nThe YARV virtual machine works by pushing and popping values on a stack.  The\nfirst two calls to `getlocal_WC_0` take one parameter, 0, and 1 respectively.\nThis means \"get the local at index 0 and push it on the stack\", and \"get the\nlocal at index 1 and push it on the stack\".\n\nAfter these two instructions have executed, the stack should have two values on\nit.  The `opt_plus` instructions pops two values from the stack, adds them,\nthen pushes the summed value on the stack.  This leaves 1 value on the stack.\n\nFinally the `leave` instruction pops one value from the stack and returns that\nvalue to the calling method.\n\nTenderJIT works by examining each of these instructions, then converts them to\nmachine code at runtime.  If a machine code version of the method is available\nat run-time, then YARV will call the machine code version rather than the\nYARV byte code version.\n\n## Hacking on TenderJIT\n\nYou should only need Ruby 3.0.0 or up to get started hacking on TenderJIT.\nHowever, I highly recommend installing a debugger like lldb or gdb as well.\n\nThe main compiler object is the `TenderJIT::ISEQCompiler` class which can be\nfound in [`lib/tenderjit/iseq_compiler.rb`](lib/tenderjit/iseq_compiler.rb).\n\nEach instruction sequence object (method, block, etc) gets its own instance of\nan `ISEQCompiler` object.\n\nEach YARV instruction has a corresponding `handle_*` method in the\n`ISEQCompiler` class.  The example above used `getlocal_WC_0`, `opt_plus`, and\n`leave`.  Each of these instructions have corresponding `handle_getlocal_WC_0`,\n`handle_opt_plus`, and `handle_leave` methods in the `ISEQCompiler` class.\n\nWhen a request is made to compile an instruction sequence (iseq), the compiler\nchecks to see if there is already an ISEQCompiler object associated with the\niseq.  If not, it allocates one, then calls `compile` on the object.\n\nThe compiler will compile as many instructions in a row as it can, then will\nquit compilation.  Depending on the instructions that were compiled, it may\nresume later on.\n\nNot all instructions have corresponding `handle_*` methods.  This just means\nthey are not implemented yet!  If you find an instruction you'd like to implement,\nplease do it!\n\nWhen no corresponding handler function is found, the compiler will generate an\n\"exit\" and the machine code will pass control back to YARV.  YARV will resume\nwhere the compiler left off, so even partially compiled instruction sequences\nwill work.\n\nYARV has a few data structures that you need to be aware of when hacking on\nTenderJIT.  First is the \"control frame pointer\" or CFP.  The CFP represents\na stack frame.  Each time we call a method, an new stack frame is created.\n\nThe CFP points to the iseq it's executing.  It also points to the Program\nCounter, or PC.  The PC indicates which instruction is going to execute next.\nThe other crucial thing the CFP points to is the Stack Pointer, or SP.  The SP\nindicates where the *top* of the stack is, and it points at the \"next empty slot\"\nin the stack.\n\nWhen a function is called, a new CFP is created.  The CFP is initialized with\nthe first instruction in the iseq set as the PC, and an empty slot in the SP.\nWhen `getlocal_WC_0` executes, first it advances the PC to point at the *next*\ninstruction.  Then `getlocal_WC_0` fetches the local value, writes it to the\nempty SP slot, then pushes the SP slot up by one.\n\nTenderJIT gains speed by eliminating PC and SP advancement.  This means that\nas TenderJIT machine code executes, the values on the CFP may not reflect\nreality!  In order to hand control back to YARV, TenderJIT must write accurate\nvalues back to the CFP before returning control.\n\n## Lazy compilation\n\nTenderJIT is a lazy compiler.  It (very poorly) implements a version of [Lazy\nBasic Block Versioning](https://arxiv.org/abs/1411.0352).  TenderJIT will only\ncompile one basic block at a time.  This means that TenderJIT will stop compiling\nany time it finds an instruction that might jump somewhere else.\n\nFor example:\n\n```ruby\ndef add a, b\n  puts \"hi\"\n\n  if a \u003e 0\n    b - a\n  else\n    a + b\n  end\nend\n```\n\nTenderJIT will compile the method calls as well as the comparison, but when it\nsees there is a conditional, it will stop compiling.  At that point, it inserts\na \"stub\" which is just a way to resume compilation at that point.  These \"stubs\"\ncall back in to the compiler and ask it to resume compilation from that point.\n\nRuntime compilation methods start with `compile_*` rather than `handle_*`.\n\nAs a practical example, lets look at how the compiler handles the following code:\n\n```ruby\ndef get_a_const\n  Foo\nend\n```\n\nThe instructions for this method are as follows:\n\n```\n== disasm: #\u003cISeq:get_a_const@x.rb:1 (1,0)-(3,3)\u003e (catch: FALSE)\n0000 opt_getinlinecache                     9, \u003cis:0\u003e                 (   2)[LiCa]\n0003 putobject                              true\n0005 getconstant                            :Foo\n0007 opt_setinlinecache                     \u003cis:0\u003e\n0009 leave                                                            (   3)[Re]\n```\n\nIf we check [the implementation of `opt_getinlinecache` in YARV](https://github.com/ruby/ruby/blob/9770bf23b7a273246b9a6b084e79a8fb6fc1af11/insns.def#L1005-L1020), we see that it will check a cache.\nIf the cache is valid it will jump to the destination instruction, in this case\nthe instruction at position 9 (you can see that 9 is a parameter on the right of `opt_getinlinecache`).\nSince this function can jump, we consider it the end of a basic block.\nAt compile time, TenderJIT doesn't know the machine address where it would have to jump.\nSo it inserts a \"stub\" which calls the method `compile_opt_getinlinecache`, but\n*at runtime* rather than compile time.\n\nThe runtime function will examine the cache.  If the cache is valid, it patches\nthe calling jump instruction *in the generated machine code* to just jump to\nthe destination.\n\nThe next time the machine code is run, it no longer calls in to the lazy compile\nmethod, but jumps directly where it needs to go.\n\n## Why TenderJIT?\n\nI built this JIT for several reasons.  The first, main reason, is that I'm helping\nto build a more production ready actually-fast-and-good JIT at work called [YJIT](https://github.com/shopify/yjit).\nI was not confident in my skills to build a JIT whatsoever, so I wanted to try\nmy hand at building one, but in pure Ruby.\n\nThe second reason is that I wanted to see if it was possible to write a JIT for\nRuby in pure Ruby (apparently it is).\n\nMy ultimate goal is to be able to ship a gem, and people can just require the gem\nand their code is suddenly faster.\n\nI picked the name \"TenderJIT\" because I thought it was silly.  If this project\ncan become a serious JIT contender then I'll probably consider renaming it to\nsomething that sounds more serious like \"SeriousJIT\" or \"AdequateCodeGenerator\".\n\n## How can I help?\n\nIf you'd like a low friction way to mess around with a JIT compiler, please\nhelp contribute!\n\nYou can contribute by adding missing instructions or adding tests, or whatever\nyou want to do!\n\nLots of TenderJIT internals just look like x86-64 assembly, and I'd like to\nget away from that.  So I've been working on a DSL to hide the assembly language\naway from developers.  I need help developing that and converting the existing\n\"assembly-like\" code to use the runtime class.\n\nYou can find the DSL in [`lib/tenderjit/runtime.rb`](lib/tenderjit/runtime.rb).\n\nThanks for reading!  If you want to help out, please ping me on Twitter or\nopen an issue!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftenderlove%2Ftenderjit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftenderlove%2Ftenderjit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftenderlove%2Ftenderjit/lists"}