Ruby has lots of DSLs (method calls that mutate what is defined by the code). We need a flexible way of handling this type of code or else we will not be able to represent all of the richness that Rubyists can express.
In other attempts, like the Ruby LSP indexer or Spoom plugins, we used Ruby's own dynamic nature to load custom logic that can tailor the analysis. With a Rust backend, these options aren't really viable as we want to make the most out of parallelism without hitting the GVL and keeping the core logic agnostic from the Ruby VM.
Our primary idea now is shifting Rubydex to use an intermediate representation (IR), where we compile the code into fundamental type-level operations, like enter_class, leave, define_method and so on. This would allow us to expose a Ruby API for registering how to compile specific method call DSLs. Since everything is a simple type and easily shareable with Rust, it would satisfy our performance needs to continue making the most out of parallelism.
Additionally, by compiling the DSLs into resolution-level instructions, we make sure that any DSLs that impact resolution results are actually taken into account. For example, a method call that creates a new class, which is then used as a parent for another class (which impacts ancestor linearization and therefore constant resolution inside of that lexical scope).
This is roughly what the idea is:
graph.register_compiler(
owner: "Module",
method_name: "new",
instructions: [
[:enter_module, lexical_scope: false],
]
)
graph.register_compiler(
owner: "Struct",
method_name: "new",
instructions: [
[:enter_class, lexical_scope: false, parent: "Struct"],
[:define_method, :initialize, ...],
[:for_each_param, :define_attr_reader, :param]
]
)
Ruby has lots of DSLs (method calls that mutate what is defined by the code). We need a flexible way of handling this type of code or else we will not be able to represent all of the richness that Rubyists can express.
In other attempts, like the Ruby LSP indexer or Spoom plugins, we used Ruby's own dynamic nature to load custom logic that can tailor the analysis. With a Rust backend, these options aren't really viable as we want to make the most out of parallelism without hitting the GVL and keeping the core logic agnostic from the Ruby VM.
Our primary idea now is shifting Rubydex to use an intermediate representation (IR), where we compile the code into fundamental type-level operations, like
enter_class,leave,define_methodand so on. This would allow us to expose a Ruby API for registering how to compile specific method call DSLs. Since everything is a simple type and easily shareable with Rust, it would satisfy our performance needs to continue making the most out of parallelism.Additionally, by compiling the DSLs into resolution-level instructions, we make sure that any DSLs that impact resolution results are actually taken into account. For example, a method call that creates a new class, which is then used as a parent for another class (which impacts ancestor linearization and therefore constant resolution inside of that lexical scope).
This is roughly what the idea is: