Back to features

Ruby DSL

Design hardware using Ruby's expressive syntax and metaprogramming. Familiar language, powerful abstractions for digital circuit design.

Ruby Metaprogramming DSL HDL

Why Ruby for Hardware?

Traditional HDLs like Verilog and VHDL were designed decades ago with verbose, rigid syntax. Ruby brings a modern, expressive language to hardware design — one that engineers already know and love. RHDL leverages Ruby's metaprogramming to create a DSL that reads like a hardware specification but executes as real Ruby code.

AspectVerilog / VHDLRHDL (Ruby)
Syntax verbosityHigh — explicit begin/end, wire declarationsMinimal — Ruby blocks and conventions
ParameterizationLimited generate/generic supportFull Ruby metaprogramming, loops, conditionals
Type systemRigid, error-prone width mismatchesDynamic with automatic width inference
TestbenchSeparate testbench files, $displayRSpec integration, Ruby assertions
Code reuseCopy-paste or limited includesModules, mixins, inheritance, gems
ToolingProprietary vendor toolsOpen source, standard Ruby ecosystem

A simple counter in Verilog requires explicit boilerplate for ports, wire types, and sensitivity lists.

counter.v (Verilog)
module counter #(
  parameter WIDTH = 8
)(
  input  wire clk,
  input  wire rst,
  input  wire en,
  output reg [WIDTH-1:0] count
);
  always @(posedge clk or
          posedge rst) begin
    if (rst)
      count <= 0;
    else if (en)
      count <= count + 1;
  end
endmodule

The same counter in RHDL is concise, readable, and parameterized naturally with Ruby.

counter.rb (RHDL)
class Counter < RHDL::Sim::SequentialComponent
  input  :clk, :rst, :en
  output :count, width: 8

  behavior clock: :clk,
           reset: :rst do
    if en
      count <= count + 1
    end
  end
end

DSL Syntax Overview

RHDL components are defined as Ruby classes that inherit from base component types. Inputs, outputs, and behavior blocks map directly to hardware concepts while remaining idiomatic Ruby.

Port declarations use Ruby symbols and keyword arguments. Width defaults to 1 (single bit) when omitted. Multiple ports can share a single declaration line.

port_declarations.rb
class ALU < RHDL::Sim::Component
  # Single-bit inputs (width: 1 is default)
  input  :carry_in

  # Multi-bit inputs with explicit width
  input  :a, width: 8
  input  :b, width: 8
  input  :op, width: 4

  # Multiple outputs on one line
  output :result, width: 8
  output :carry, :zero, :overflow

  # Combinational behavior block
  behavior do
    result <= case_select(op, {
      0 => a + b + carry_in,
      1 => a - b,
      2 => a & b,
      3 => a | b,
      4 => a ^ b,
    })
    zero  <= result.eq?(0)
    carry <= result[8]
  end
end

Sequential components use clock and reset signals. State machines are first-class citizens with declarative transition rules.

traffic_light.rb
class TrafficLight < RHDL::Sim::SequentialComponent
  input  :clk, :rst
  input  :sensor
  output :red, :yellow, :green

  state_machine :state,
    clock: :clk, reset: :rst,
    states: [:idle, :go,
             :slow, :stop] do

    transition :idle => :go,
      when: sensor
    transition :go => :slow,
      after: 30
    transition :slow => :stop,
      after: 5
    transition :stop => :go,
      after: 20
  end

  behavior do
    red    <= state.eq?(:stop)
    yellow <= state.eq?(:slow)
    green  <= state.eq?(:go)
  end
end

Components are composed by instantiation. Wiring is explicit and type-checked, catching width mismatches at elaboration time.

cpu_top.rb
class CPU < RHDL::Sim::Component
  input  :clk, :rst
  input  :data_in, width: 8
  output :data_out, width: 8
  output :addr, width: 16

  # Instantiate sub-components
  component :alu,
    ALU.new
  component :decoder,
    Decoder.new
  component :regfile,
    RegisterFile.new(
      num_regs: 8
    )

  # Wire components together
  behavior do
    decoder.opcode <= data_in
    alu.a   <= regfile.rd1
    alu.b   <= regfile.rd2
    alu.op  <= decoder.alu_op
    regfile.wd <= alu.result
  end
end

Metaprogramming Power

Ruby's metaprogramming capabilities enable hardware patterns that are impossible or extremely verbose in traditional HDLs. Generate parameterized components, create pipeline stages dynamically, and build entire architectures from configuration hashes.

Parameterized components use standard Ruby to generate hardware. No special generate syntax needed — loops, conditionals, and methods just work.

parameterized_fifo.rb
class FIFO < RHDL::Sim::SequentialComponent
  def initialize(depth: 16, width: 8)
    input  :clk, :rst
    input  :wr_en, :rd_en
    input  :din,  width: width
    output :dout, width: width
    output :full, :empty

    # Generate storage registers
    addr_bits = Math.log2(depth).ceil
    register :wr_ptr, width: addr_bits
    register :rd_ptr, width: addr_bits
    memory   :mem,
      depth: depth, width: width
  end

  behavior clock: :clk,
           reset: :rst do
    if wr_en && !full
      mem[wr_ptr] <= din
      wr_ptr <= wr_ptr + 1
    end
    if rd_en && !empty
      dout <= mem[rd_ptr]
      rd_ptr <= rd_ptr + 1
    end
  end
end

# Instantiate with different configs
small = FIFO.new(depth: 4,  width: 8)
large = FIFO.new(depth: 64, width: 32)

Dynamic generation lets you build entire bus architectures, pipeline stages, and interconnects from configuration data.

pipeline_generator.rb
# Generate an N-stage pipeline dynamically
def build_pipeline(stages:, width:)
  Class.new(RHDL::Sim::SequentialComponent) do
    input  :clk, :rst
    input  :din,  width: width
    output :dout, width: width
    output :valid

    # Create pipeline registers
    stages.times do |i|
      register :"stage_#{i}",
        width: width
      register :"valid_#{i}"
    end

    behavior clock: :clk,
             reset: :rst do
      # Chain stages together
      send(:stage_0) <= din
      (1...stages).each do |i|
        send(:"stage_#{i}") <=
          send(:"stage_#{i-1}")
      end
      dout <= send(:"stage_#{stages-1}")
    end
  end
end

# Create pipelines at elaboration time
Pipe3 = build_pipeline(stages: 3, width: 16)
Pipe7 = build_pipeline(stages: 7, width: 32)

Mixins and modules let you share behavior across components — a common pattern for adding debug interfaces, protocol support, or standard bus connectivity to any design.

mixins.rb
module Debuggable
  def self.included(base)
    base.input  :debug_en
    base.output :debug_data, width: 32
    base.output :debug_valid
  end

  def emit_debug(data)
    if debug_en
      debug_data  <= data
      debug_valid <= 1
    end
  end
end

class MyProcessor < RHDL::Sim::SequentialComponent
  include Debuggable
  # debug_en, debug_data, debug_valid ports are
  # automatically added by the mixin
end