tl;dr

  1. Numbered block params (_1, _2, etc.) are reserved and read-only, while it (the new block param in Ruby 3.4) has different mutation rules for compatibility.
  2. Using combined assignment operators (e.g. +=, &&=), numbered block params could be overwritten without error due to a discrepancy between parse.y and Prism (Ruby’s default parser since 3.3).
  3. A patch has been applied to Prism in Ruby 3.4.2 to explicitly check for writes to numbered params via combined assignment operators.

The Bug

In Ruby, block-local numbered parameters (_1, _2, etc.) provide a concise way to reference block arguments without explicit naming. They have received renewed attention recently with the introduction of the it parameter in Ruby 3.4.

Though they serve a similar purpose, numbered parameters are designed to be read-only, while it is intentionally mutable for backward compatibility reasons.

In Bug #21117, radarek (Radosław Bułat) identified inconsistencies in how Ruby handles assignments to _1 and it. Most concerning was that _1 could be modified in specific cases, violating its intended immutability.

Direct assignment correctly raises SyntaxError:

irb(main):001> [1, 2, 3].each { _1 = _1 + 1; p _1 }
<internal:kernel>:168:in 'Kernel#loop': (irb):1: syntax error found (SyntaxError)
> 1  | [1, 2, 3].each { _1 = _1 + 1; p _1 }
     |                  ^~ Can't assign to numbered parameter _1

However, combined assignment operators work without error:

irb(main):002> [1, 2, 3].each { _1 += 1; p _1 }
2
3
4
=> [1, 2, 3]

The root cause was an implementation difference between Prism (Ruby’s default parser since 3.3) and parse.y. Prism failed to enforce numbered parameter immutability for combined assignment operations, while the legacy parse.y parser correctly rejected these operations:

# $ ruby --parser 'parse.y' -e 'binding.irb'

irb(main):001> [1, 2, 3].each { _1 += 1; p _1 }
<internal:kernel>:168:in 'Kernel#loop': _1 is reserved for numbered parameter (SyntaxError)

The Fix

Commit d3fc56d ports Kevin Newton’s (kddnewton) fix from Prism. The update adds explicit checks when parsing combined assignment tokens (PM_TOKEN_#{op}_EQUAL) to reject any attempts to modify numbered parameters.

Simplified code snippet from the fix:

// In prism.c:
switch (token.type) {
  // Cases for all combined assignment operators
  case PM_TOKEN_PIPE_PIPE_EQUAL:
  // ...other operator cases...
  case PM_TOKEN_PLUS_EQUAL:
  case PM_TOKEN_SLASH_EQUAL:
  case PM_TOKEN_STAR_EQUAL:
  case PM_TOKEN_STAR_STAR_EQUAL:
    switch (PM_NODE_TYPE(node)) {
        case PM_LOCAL_VARIABLE_READ_NODE: {
            // Check if we're trying to modify a numbered parameter
            if (pm_token_is_numbered_parameter(node->location.start, node->location.end)) {
                PM_PARSER_ERR_FORMAT(parser, node->location.start, node->location.end,
                                     PM_ERR_PARAMETER_NUMBERED_RESERVED, node->location.start);
                parse_target_implicit_parameter(parser, node);
            }
        }
        // ...other cases...
    }
    // ...
}

What About it?

The it keyword intentionally has different behavior than numbered parameters for backward compatibility. While direct assignment (e.g. tap { it = 2; p it }) is currently allowed to avoid breaking legacy code, Ruby core developers encourage treating it as read-only for consistency with numbered parameters.

In A Life-Saving Checklist, author Atul Gawande describes a patient suffering from an infection introduced by medical equipment. These “line infections” are so common, they’re said to be a routine complication.

In medical practice, a routine complication refers to an adverse event or difficulty that is known to occur with some regularity in association with a particular procedure or treatment. These complications are expected in the sense that they are well-documented and occur with a known frequency, allowing healthcare providers to anticipate and manage them effectively.

In other domains, such as software development, it is useful to identify routine complications inherent to various operations, and understand both their potential for happening and their potential negative impacts.

As an example, during incident response, if standard operating procedure (SOP) includes restarting a web server, routine complications to be understood include: accidentally restarting a healthy server (misdiagnosis), executing a restart incorrectly (human error), cutting off further observations of the issue before it’s fully understood (state evaporation), and encountering unexpected dependencies that cause cascading failures (hidden coupling).

Just because restarting a web server carries with it these potential issues, doesn’t mean we should shy away from the tried-and-true “turn it off and on again” procedure (every operation carries risk after all). Instead, we should manage routine complications by understanding them honestly, preparing for them openly, and being blameless when they happen.

I built an interactive tool for visualizing task progress.

Screenshot of Hill Chart

A hill chart is a visual tool for tracking tasks and projects, using the metaphor of going up and down hill to communicate the different phases of work.

Every piece of work has two phases: an uphill phase where you figure out your approach, and a downhill phase focused on execution.1

I appreciate hill charts because they embrace the fuzziness of task-based work. Unlike rigid progress metrics like “done/not done,” percentage completion, or T-shirt sizes, hill charts offer a more nuanced way to describe progress. If you export the chart each time you update it, you can also detect trends over time.

This tool runs in the browser via Github Pages and is built with plain JavaScript, d3.js, and is styled with TailwindCSS. You can explore the code by right-clicking → View Source or checking out the GitHub repo.2

When you’re interested in testing the actual SQL queries generated by ActiveRecord, rather than just what’s returned from the database, you can capture them using ActiveSupport::Notifications.

module QueryCapturing
  IGNORED_QUERIES = ["SCHEMA", "TRANSACTION"]

  def capture_queries
    captured_queries = []
    subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |event|
      captured_queries << event.payload[:sql] unless IGNORED_QUERIES.include?(event.name)
    end
    yield captured_queries
  ensure ActiveSupport::Notifications.unsubscribe(subscriber)
  end
end
class ServiceMetadata::QueryCommentsTest < Minitest::Test
  include QueryCapturing

  def test_annotates_queries_with_metadata_comments
    capture_queries do |queries|
      ServiceMetadata::QueryComments.comment { Comment.all.load }

      assert queries.all? { |q| q.match?(/\/\*.*?origin_service: my_service.*?\*\//m) }
    end
  end
end
def print_progress(title, total, current_progress, bar_width: 50)
  progress_pct = (current_progress.to_f / total) * bar_width
  printf("\r#{title}: [%-#{bar_width}s ] -- %s", "▤" * progress_pct.round, "#{current_progress}/#{total} ")
end

# Usage
1.upto(10) do |i|
  print_progress("Here we go!", 10, i)
  sleep 0.2
end;print("\n")

# Output Example:
# Here we go!: [▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤▤                ] -- 7/10

I’ve been playing around with the idea of building a checklist app that’s geared towards completeing procedures.

Checklist procedure app prototype

I remember watching a livestream of a rocket launch and seeing the closeout crew go through a checklist using a tablet strapped to their thigh. I thought, “I’m no NASA engineer, but that looks useful!”

I built an interactive jq TUI using fzf!

I was searching for an interactive jq editor when I came across this repo, which had an intriguing suggestion:

echo '' | fzf --print-query --preview "cat *.json | jq {q}" – An fzf hack that turns it into an interactive jq explorer.

This sent me down a rabbit hole, and I discovered just how incredibly configurable fzf is, e.g.:

  • You can bind custom keys to execute non-default behaviors:
    --bind=ctrl-y:execute-silent(jq {q} $tempfile | pbcopy)
    
  • You can start fzf with an initial query:
    --query="."
    
  • You can configure fzf with different layouts:
    --preview-window=top:90%:wrap
    
  • You can add a multi-line header to provide instructions:
    --header=$'ctrl+y : copy JSON\nctrl+f : copy filter\nenter : output\nesc : exit'
    

I wonder how many different TUIs I can create with just fzf?

Checkout the code for ijq here.

Turn your ApplicationRecord models into a Mermaid ERD

Rails.application.eager_load!

# Instead of all ApplicationRecord descendants, you can
# make an Array of only the models you care about
data = ApplicationRecord.descendants.each_with_object({}) do |model, data|
  model_name = model.name.upcase
  data[model_name] = {
    columns: model.columns.map { |column| [column.name, column.sql_type] },
    associations: model.reflect_on_all_associations.each_with_object({}) do |reflection, assoc_data|
      assoc_data[reflection.name] = {
        klass: reflection.class_name.upcase,
        relationship: reflection.macro
      }
    end
  }
end

# Intermediate step to save the data as JSON
File.open("data.json", "w") { |file| file.write(data.to_json) }

mermaid_erd = ["erDiagram"]

data.each do |table, details|
  columns = details[:columns].map { |name, type| "#{name} #{type}" }.join("\n  ")
  mermaid_erd << "#{table} {\n  #{columns}\n}"
end

data.each do |table, details|
  details[:associations]&.each do |association_name, association_details|
    other_table = association_details[:klass]
    case association_details[:relationship]
    when :belongs_to
      mermaid_erd << "#{table} ||--o{ #{other_table} : \"#{association_name}\""
    when :has_one
      mermaid_erd << "#{table} ||--|| #{other_table} : \"#{association_name}\""
    when :has_many
      mermaid_erd << "#{table} ||--o{ #{other_table} : \"#{association_name}\""
    end
  end
end

File.open("out.mermaid", "w") { |file| file.puts mermaid_erd.join("\n") }

Adding Memos

#memo

The thing I love most about social media feeds is the immediacy of capturing and sharing thoughts and ideas. Writing on my blog, being built with Jekyll, creates just enough friction for me to overthink what I’m sharing.

Inspired by Julia Evans’s TIL blog implementation, I’m hoping a dedicated page and a rake task removes enough friction for me to share more often.