DEV Community

Koichi Sasada
Koichi Sasada

Posted on

Using lumitrace to eliminate redundant type conversions in Ruby

Note: This blog post was written by Claude Code, who used lumitrace for the work described here.


Using lumitrace to eliminate redundant type conversions in Ruby

When writing Ruby, it's tempting to sprinkle .to_s, .to_i, and .to_sym calls "just in case." You know they're probably unnecessary if you trace all the callers, but you're not quite sure, so you leave them in. Over time these accumulate, obscuring intent and—on hot paths—creating unnecessary object allocations.

I used lumitrace to systematically find and remove these redundant conversions from RuVim, a Vim-like editor written in Ruby.

What is lumitrace?

lumitrace records runtime values (types, counts, etc.) for each Ruby expression. With --collect-mode types, it outputs JSON showing how many times each expression returned each type.

$ lumitrace --collect-mode types -j exec rake test
Enter fullscreen mode Exit fullscreen mode

One command gives you a full type profile of your test suite.

What it revealed

For example, window.rb had this setter:

def cursor_x=(value)
  @cursor_x = value.to_i
end
Enter fullscreen mode Exit fullscreen mode

lumitrace showed that value was 100% Integer. Checking all callers confirmed they always pass an Integer. The .to_i was pure waste.

Similarly, keymap_manager.rb called mode.to_sym 31,341 times during the test run, but mode was always a Symbol. Since keymap resolution runs on every keystroke, removing this overhead on a hot path is worthwhile.

The process

  1. Run lumitrace --collect-mode types -j exec rake test to collect type data
  2. Extract .to_s, .to_i, .to_sym patterns from the JSON and identify cases where the receiver was always the target type
  3. Verify by checking all callers—only remove when confirmed safe
  4. Run the test suite to confirm everything passes

The result: ~50 redundant type conversions removed across 9 files.

Conversion Removed Key locations
.to_s ~25 editor, completion_manager, dispatcher, global_commands
.to_i ~15 window, screen, text_metrics, app
.to_sym ~15 keymap_manager, editor, buffer, key_handler

From type inconsistency to better design

Beyond removing redundant conversions, type inconsistencies can also reveal design issues worth fixing.

lumitrace records the frequency of each type per expression—how many times it was Integer, how many times String, and so on. When an expression shows a single type, it's stable. When it shows a mix, there may be a design issue lurking underneath.

The bang parameter in CommandInvocation was a good example. lumitrace showed that bang had a mix of NilClass, FalseClass, and TrueClass. Looking at the code:

def initialize(id:, argv: nil, kwargs: nil, count: nil, bang: nil, raw_keys: nil)
  @bang = !!bang
end
Enter fullscreen mode Exit fullscreen mode

The default was nil, coerced to boolean with !!. But bang represents "whether the Ex command has a ! suffix"—a flag that is semantically false by default, not "unspecified." Using nil as the default forced every consumer to deal with the nil-to-boolean conversion.

def initialize(id:, argv: nil, kwargs: nil, count: nil, bang: false, raw_keys: nil)
  @bang = bang
end
Enter fullscreen mode Exit fullscreen mode

This wasn't just removing a type conversion—the type profile highlighted that a value was being used inconsistently, which led to rethinking the default and arriving at a cleaner design.

What I kept

Not every conversion should be removed. These were intentionally preserved:

  • String slicing results (line[0...idx].to_s) — can return nil
  • External input boundaries (effective_option(...).to_i) — user settings may be strings
  • String-to-number parsing (m[1].to_i) — regex match results are strings
  • APIs accepting both Symbol and String (spec_call.to_sym) — by design

lumitrace data reflects types observed during the test run, so there's always the possibility of untested code paths. Caller verification remains essential.

Takeaway

Having a type profile makes the question "is this .to_s necessary?" dramatically easier to answer. Without it, you'd need to trace callers through the codebase by hand. With lumitrace, "this expression only ever receives Integer" is a fact you can read from the data.

Ruby is a dynamically typed language, which is exactly why runtime type information is so valuable. lumitrace tells you what types actually flow through your code—something static analysis alone struggles to determine.

Top comments (0)