Skip to content

kozy4324/logicuit

Repository files navigation

Logicuit

                *******       *******       *******
               *********     *********     *********
              ***********   ***********   ***********
               *********     *********     *********
                *******       *******       *******

+-----------------------------------------------+       OUT 0111
|                                               |       ADD A,0001
+--->|rg_a|(0111)----->|   |                    |       JNC 0001
|    |1000|            |   |                    |       ADD A,0001
|                      |   |                    |     > JNC 0011
+--->|rg_b|(0000)----->|   |----------->|   |---+       OUT 0110
|    |0000|            |   |            |   |           ADD A,0001
|                      |SEL|            |ALU|           JNC 0110
+--->| out|  |  in|--->|   |            |   |           ADD A,0001
|    |0111|  |0000|    |   |  |  im|--->|   |--(0)      JNC 1000
|                      |   |  |0011|                    OUT 0000
+--->|  pc|  (0000)--->|   |                            OUT 0100
     |0100|                                             ADD A,0001
                                                        JNC 1010
                                                        OUT 1000
                                                        JMP 1111

tick: 48
input: in0,in1,in2,in3?

From logic circuit to Logicuit — a playful portmanteau.

A Ruby-based logic circuit simulator featuring an internal DSL for building circuits.

Table of Contents

Installation

Install the gem and add to the application's Gemfile by executing:

bundle add logicuit

If bundler is not being used to manage dependencies, install the gem by executing:

gem install logicuit

DSL

This library provides an internal DSL for defining logic circuits in a declarative and readable way. You can define inputs, outputs, and even a visual diagram — all within a Ruby class.

Here is an example of a simple 2-input AND gate:

# rbs_inline: enabled

require "logicuit"

class MyAndGate < Logicuit::DSL
  attr_reader :a, :b, :y #: Logicuit::Signals::Signal

  inputs :a, :b

  outputs y: -> { a & b }

  diagram <<~DIAGRAM
    (A)-|   |
        |AND|-(Y)
    (B)-|   |
  DIAGRAM
end

MyAndGate.run

This defines:

  • two inputs (a and b),
  • one output (y) that returns the logical AND of the inputs,
  • and an ASCII diagram that shows the structure of the gate.

About signals and logical operations

Inputs and outputs in Logicuit are not plain booleans — they are instances of the Logicuit::Signals::Signal class. The Signal class provides methods like &, |, and ! to represent logical AND, OR, and NOT operations, respectively.

For example:

outputs y: -> { (a & b) | !a }

This allows logic expressions to look natural and circuit-like while supporting chaining and composition.

Interactive execution

When you call run, the simulator enters an interactive mode.

At first, the circuit is evaluated with all inputs set to 0, and drawn as an ASCII diagram:

(0)-|   |
    |AND|-(0)
(0)-|   |

input: a,b?

To interact with the circuit, just type the name of an input — for example, a — and press Enter. That input will toggle its value (0 → 1 or 1 → 0), and the diagram will be redrawn to reflect the new state. You can keep toggling inputs this way to observe how the circuit reacts in real time.

To exit the simulator, simply press Ctrl+C.

Assembling circuits

In addition to defining simple gates declaratively, Logicuit also lets you assemble circuits from reusable components using the assembling block.

This approach gives you more control and expressiveness when building complex circuits.

Here's an example of a 2-to-1 multiplexer:

# rbs_inline: enabled

require "logicuit"

class MyMultiplexer < Logicuit::DSL
  attr_reader :c0, :c1, :a, :y #: Logicuit::Signals::Signal

  inputs :c0, :c1, :a

  outputs :y

  assembling do
    and_gate1 = Logicuit::Gates::And.new
    and_gate2 = Logicuit::Gates::And.new
    not_gate = Logicuit::Gates::Not.new
    or_gate = Logicuit::Gates::Or.new

    c0 >> and_gate1.a
    a >> not_gate.a
    not_gate.y >> and_gate1.b

    c1 >> and_gate2.a
    a >> and_gate2.b

    and_gate1.y >> or_gate.a
    and_gate2.y >> or_gate.b
    or_gate.y >> y
  end

  diagram <<~DIAGRAM
    (C0)---------|   |
                 |AND|--+
         +-|NOT|-|   |  +--|  |
         |                 |OR|--(Y)
    (C1)---------|   |  +--|  |
         |       |AND|--+
    (A)--+-------|   |
  DIAGRAM
end

MyMultiplexer.run

Connection syntax

The >> operator is used to connect outputs to inputs, mimicking the direction of signal flow.

This allows the code to resemble the actual structure of the circuit, making it more intuitive to follow.

For example:

a >> not_gate.a
not_gate.y >> and_gate1.b

can be read as:

"Connect input a to the NOT gate's input. Then connect the NOT gate's output to one of the AND gate's inputs."

Built-in gates

Logicuit includes several built-in logic gates, which you can use as components:

  • Logicuit::Gates::And
  • Logicuit::Gates::Or
  • Logicuit::Gates::Not
  • Logicuit::Gates::Nand
  • Logicuit::Gates::Xor

These gates expose their input and output pins as attributes (a, b, y, etc.), which can be freely connected using >>.

Signal groups

When building larger circuits, it's common to connect one output to multiple inputs, or to connect multiple outputs to multiple inputs.

Logicuit provides a convenient way to express these kinds of connections using signal groups.

One-to-many connections

These two lines:

a >> xor_gate.a
a >> and_gate.a

can be written more concisely as:

a >> [xor_gate.a, and_gate.a]

The array on the right-hand side is treated as a signal group, and the connection is applied to each element.

Many-to-many connections

You can also connect multiple outputs to multiple inputs at once by using the [] method to access signals by name:

pc.qa >> rom.a0
pc.qb >> rom.a1
pc.qc >> rom.a2
pc.qd >> rom.a3

is equivalent to:

pc[:qa, :qb, :qc, :qd] >> rom[:a0, :a1, :a2, :a3]

This #[](*keys) method returns a SignalGroup object — a Logicuit abstraction that makes it easier to handle groups of signals together.

Note: The number of signals on both sides must match.

Connecting from different sources

What if you want to connect signals from multiple different components as a single group?

You can use Logicuit::ArrayAsSignalGroup, which adds signal group behavior to arrays:

using Logicuit::ArrayAsSignalGroup

assembling do
  [register_a.qa, register_b.qa, in0] >> mux0[:c0, :c1, :c2]
end

This lets you treat a plain Ruby array as a SignalGroup and connect it to another group of inputs in one line.

Sequential circuits

In addition to combinational circuits, Logicuit also supports sequential circuits — circuits whose output depends not only on the current inputs, but also on past inputs.

For example, here’s a D flip-flop:

# rbs_inline: enabled

require "logicuit"

class MyDFlipFlop < Logicuit::DSL
  attr_reader :d, :q #: Logicuit::Signals::Signal

  inputs :d, clock: :ck

  outputs q: -> { d }

  diagram <<~DIAGRAM
    (D)--|   |--(Q)
         |DFF|
    (CK)-|>  |
  DIAGRAM
end

MyDFlipFlop.run

Defining a sequential circuit

A circuit becomes sequential when the inputs declaration includes a keyword argument named clock:. You can assign any name to the clock signal — in the above example, it's :ck — but the presence of the clock: keyword is what tells Logicuit to treat the circuit as sequential.

Once a clock is defined:

  • The outputs lambdas will be evaluated on each clock tick, not continuously.
  • A global singleton clock will drive the timing — you don’t need to define or manage the clock yourself.

Interactive execution with a clock

When a sequential circuit is run interactively, Logicuit enters clock mode. The clock ticks periodically, and the circuit is redrawn after each tick.

(0)--|   |--(0)
     |DFF|
(CK)-|>  |

tick: 2
input: d?

You can still toggle inputs as before (e.g., type d and press Enter), but updates take effect on the next tick.

The number of elapsed ticks is shown as tick: N.

Clock speed

By default, the clock ticks at 1 Hz (once per second). You can change the frequency by passing the hz: option to run:

MyDFlipFlop.run(hz: 10)

This will run the clock at 10 ticks per second — useful when simulating more complex circuits.

If you want full control, you can set hz: 0 to disable automatic ticking.

In this mode, the clock only ticks when you press Enter, allowing you to step through the simulation manually:

MyDFlipFlop.run(hz: 0)

This is useful for debugging or analyzing a circuit’s behavior step by step.

Combining sequential circuits with assembling

You can build sequential circuits out of smaller components using the assembling block, just like with combinational circuits.

Here’s an example of a 4-bit register that stores its input when the load signal ld is not active:

# rbs_inline: enabled

require "logicuit"

class MyRegister4bit < Logicuit::DSL
  attr_reader :a, :b, :c, :d, :ld, :qa, :qb, :qc, :qd #: Logicuit::Signals::Signal

  inputs :a, :b, :c, :d, :ld, clock: :ck

  outputs :qa, :qb, :qc, :qd

  assembling do
    [[a, qa], [b, qb], [c, qc], [d, qd]].each do |input, output|
      dff = Logicuit::Circuits::Sequential::DFlipFlop.new
      mux = Logicuit::Circuits::Combinational::Multiplexer2to1.new
      input >> mux.c0
      dff.q >> mux.c1
      ld    >> mux.a
      mux.y >> dff.d
      dff.q >> output
    end
  end

  diagram <<~DIAGRAM
            +---------------------+
            +-|   |               |
    (A)-------|MUX|-------|DFF|---+---(QA)
          +---|   |   +---|   |
          |           |
          | +---------------------+
          | +-|   |   |           |
    (B)-------|MUX|-------|DFF|---+---(QB)
          +---|   |   +---|   |
          |           |
          | +---------------------+
          | +-|   |   |           |
    (C)-------|MUX|-------|DFF|---+---(QC)
          +---|   |   +---|   |
          |           |
          | +---------------------+
          | +-|   |   |           |
    (D)-------|MUX|-------|DFF|---+---(QD)
          +---|   |   +---|   |
    (LD)--+     (CK)--+
  DIAGRAM
end

MyRegister4bit.run

Sequential detection

If your circuit contains one or more sequential components (such as D flip-flops), Logicuit will treat the entire circuit as sequential, as long as you declare a clock input using the clock: keyword in inputs.

The clock signal is automatically connected to all internal sequential components. You don't need to wire it manually — just declare it at the top level:

inputs ..., clock: :ck

Note: If you forget to declare a clock input, Logicuit won't know it's a sequential circuit — even if you include flip-flops internally. Always include clock: to enable timing.

Truth Table Verification

You can attach a truth table to your circuit class using the #truth_table. The truth table should be written in Markdown table format.

# rbs_inline: enabled

require "logicuit"

class MyAndGate < Logicuit::DSL
  attr_reader :a, :b, :y #: Logicuit::Signals::Signal

  inputs :a, :b

  outputs y: -> { a & b }

  diagram <<~DIAGRAM
    (A)-|   |
        |AND|-(Y)
    (B)-|   |
  DIAGRAM

  truth_table <<~TRUTH_TABLE
    | A | B | Y |
    | - | - | - |
    | 0 | 0 | 0 |
    | 1 | 0 | 0 |
    | 0 | 1 | 0 |
    | 1 | 1 | 1 |
  TRUTH_TABLE
end

MyAndGate.verify_against_truth_table

The #verify_against_truth_table method evaluates the circuit for each row of the truth table and checks whether the outputs match the expected values.

If the behavior of the circuit doesn't match the truth table, you'll see an error like this:

MyAndGate.new(0, 1).y should be 0 (RuntimeError)

This feature is useful for validating your logic circuits against formal truth tables as part of development or testing workflows.

Verifying sequential circuits

Sequential circuits can also be verified using truth tables. When verifying a sequential circuit, the symbol ^ represents the rising edge of the clock.

Here’s an example for a D flip-flop:

# rbs_inline: enabled

class MyDFlipFlop < Logicuit::DSL
  attr_reader :d, :q #: Logicuit::Signals::Signal

  inputs :d, clock: :ck

  outputs q: -> { d }

  diagram <<~DIAGRAM
    (D)--|   |--(Q)
         |DFF|
    (CK)-|>  |
  DIAGRAM

  truth_table <<~TRUTH_TABLE
    | CK | D | Q |
    | -- | - | - |
    |  ^ | 0 | 0 |
    |  ^ | 1 | 1 |
  TRUTH_TABLE
end

MyDFlipFlop.verify_against_truth_table

In this table:

  • The CK column uses ^ to indicate a clock tick.
  • The circuit is initialized with the given inputs, then the clock is ticked once.
  • The outputs are evaluated immediately after the tick and compared against the expected values.

This mechanism makes it easy to specify and verify the behavior of memory elements and registers.

Demo: Ramen Timer

Logicuit comes with a simple demo circuit — a working 4-bit CPU based on the TD4 architecture described in the book CPUの創りかた.

You can try it out by running:

ruby -r logicuit -e 'Logicuit::Circuits::Td4::Cpu.run'

This launches a fully functional CPU simulation that counts down from a programmed value — perfect for timing your instant ramen 🍜

The TD4 CPU is built entirely from logic gates and flip-flops, assembled using Logicuit’s DSL. It’s a great demonstration of how small components can be combined to create a complete digital system.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/kozy4324/logicuit.

License

The gem is available as open source under the terms of the MIT License.

About

A Ruby-based logic circuit simulator featuring an internal DSL for building circuits.

Resources

License

Stars

Watchers

Forks

Packages

No packages published