feature image

2026年3月26日 | ブログ記事

組み合わせ回路シミュレーション言語を作ってみた

この記事は、2026年3月に部内向けに書いた記事を外部公開向けに書き直したものです。

はじめに

こんにちは。koukawa_pp です。

さて、皆さんは論理回路というものをご存じでしょうか。
端的に言えば0と1のデジタルな値だけで演算を行う回路のことを言います。
基本的には AND, OR, NOT の三つを使って、デジタルな入力を受け取ってデジタルな出力を行うといった回路になっています。
難しそうに聞こえるかもしれませんが、分かるとこれがかなり楽しいです。高校数学の集合の知識で理解できます。あとで説明するのでご覧ください。
and, or, not と言えばプログラミングでも使われます。AND, OR, NOT の使い方はそれと同じです。

身近なところで言えば、Minecraft というゲームをご存じでしょうか。
Minecraft ではレッドストーン回路と呼ばれる仕組みが備わっているわけですが、そのレッドストーン回路は実質論理回路のようなものです。
NOT や OR はともかく AND を表現するのは少々大変なのですが[1]、OR と NOT を組み合わせることで AND は表現できる[2]ので、回路が複雑になることに目をつぶれば、基本的にはどんな回路でも表現できます。
そんな単純だけど奥深い論理回路の中でも、今回は組み合わせ回路についてシミュレーション言語を作ってみたということになります[3]

組み合わせ回路とは、Wikipedia によると「現在の入力のみで出力が決まる回路」のことだそうです[4]
なのでシミュレーションも簡単だし理解も容易です。
本当は時系列の入力にも対応できる言語を作りたかったのですが、一旦はここでご勘弁いただきまして、いよいよ本題に入っていきましょう。

なお、シミュレーション言語は Langium というフレームワークを使用して作成しています。
こちらの GitHub リポジトリ にファイルがございますので、よろしければご覧ください。

組み合わせ回路

まず組み合わせ回路についてもう一度説明いたします。
組み合わせ回路は、AND, OR, NOT といった論理ゲートで構成され、ある時点での0または1の入力のみで出力が決まる回路を言います。
今回はこれに合わせて NAND, NOR, XOR, XNOR についても説明します。

高校数学の集合の知識を使いますが、集合の知識といっても単にベン図を使うといったくらいのものです。円内にあれば1,なければ0と思ってもらえればOKです[5]

以降入力をA, B(, C)として、出力をXとします。
これによって書かれる表は真理値表と呼ばれ、与えられた入力の組み合わせに対しその回路がどのような出力を行うのかを簡単に可視化できます。

NOT

まずNOTについて説明します。
NOTは、入力された値を反転させます。すなわち0が入力されれば1を出力し、1が入力されれば0を出力します。

A X
0 1
1 0

ベン図で表すと次のような感じです。

not

AND

次にANDについて説明します。
ANDは、全ての入力値が1であれば1を出力し、そうでなければ0を出力します。

A B X
0 0 0
0 1 0
1 0 0
1 1 1

ベン図で表すと次のような感じです。

and2

そして、先ほど「全ての入力値が」とお伝えしました。したがってこの AND ゲートは入力をいくらでも受け取ることができる、とします[6]
例えば3変数の場合は次のような感じです。

A B C X
0 0 0 0
0 0 1 0
0 1 0 0
0 1 1 0
1 0 0 0
1 0 1 0
1 1 0 0
1 1 1 1

ベン図で表すと次のような感じです。

and3

OR

次にORについて説明します。
ORは、少なくとも1つの入力値が1であれば1を出力し、そうでなければ0を出力します。

A B X
0 0 0
0 1 1
1 0 1
1 1 1

ベン図で表すと次のような感じです。

or2

そして、こちらも先ほど「少なくとも一つの入力値が」とお伝えしました。したがってこの OR ゲートも入力をいくらでも受け取ることができる、とします[6:1]
例えば3変数の場合は次のような感じです。

A B C X
0 0 0 0
0 0 1 1
0 1 0 1
0 1 1 1
1 0 0 1
1 0 1 1
1 1 0 1
1 1 1 1

ベン図で表すと次のような感じです。

or3

NAND

次にNANDについて説明します。
NANDは、AND + NOT です。したがってすべての入力が1であれば0を出力し、そうでなければ1を出力します。

A B X
0 0 1
0 1 1
1 0 1
1 1 0

ベン図で表すと次のような感じです。

nand2

そして、AND が複数の入力を受け取れることと、NAND は AND + NOT であることから、NAND ゲートも入力をいくらでも受け取ることができる、とします。
例えば3変数の場合は次のような感じです。

A B C X
0 0 0 1
0 0 1 1
0 1 0 1
0 1 1 1
1 0 0 1
1 0 1 1
1 1 0 1
1 1 1 0

ベン図で表すと次のような感じです。

nand3

一般には AND, OR, NOT で組み合わせ回路は構成されるみたいですが、NANDは実際に現物とするとき、他のゲートを使うよりもコスパがよいらしくよく使われる[7]みたいなので、組み込みゲートとして定義しました。

NOR

次にNORについて説明します。
NORは、OR + NOT です。したがって少なくとも一つの入力が1であれば0を出力し、そうでなければ1を出力します。

A B X
0 0 1
0 1 0
1 0 0
1 1 0

ベン図で表すと次のような感じです。

nor2

そして、OR が複数の入力を受け取れることと、NOR は OR + NOT であることから、NOR ゲートも入力をいくらでも受け取ることができる、とします。
例えば3変数の場合は次のような感じです。

A B C X
0 0 0 1
0 0 1 0
0 1 0 0
0 1 1 0
1 0 0 0
1 0 1 0
1 1 0 0
1 1 1 0

ベン図で表すと次のような感じです。

nor3

XOR

次にXORについて説明します。
XORは、入力された二つの値が異なるときに1を、同じときに0を出力します。

A B X
0 0 0
0 1 1
1 0 1
1 1 0

ベン図で表すと次のような感じです。

xor2

XNOR

次にXNORについて説明します。
XNORは、NOT + XOR です。つまり入力された二つの値が同じときに1を、異なるときに0を出力します。

A B X
0 0 1
0 1 0
1 0 0
1 1 1

ベン図で表すと次のような感じです。

xnor2

以上のような論理ゲートを合わせることによって、様々な回路を作成することができます。

回路シミュレーション言語「Lctxt」

概要

以上のような組み込みゲートを使いつつ、組み合わせ回路をシミュレーションする言語として、今回は「Lctxt」という仕組みを作成しました。
回路をシミュレーションできる言語は(もちろん順序回路もシミュレーションできるようなものすら)いくつか数があると思いますが、VSCode をターゲットにしたごくごく小規模のシミュレーション言語は逆に珍しいのではないでしょうか。

考えとしては、上記に示した論理ゲートを「組み込み関数」とみなして、Pythonのようなスクリプト言語でシミュレーションしたいという考えから作りました。

サンプル

adder.lctxtinput(a, b, carry)

d0 = xor(a, b)
c0 = and(a, b)

d1 = xor(d0, carry)
c1 = and(d0, carry)

c_res = or(c0, c1)

output(c_res, d1)
sample.lctxtimport adder

input(a3, a2, a1, a0, b3, b2, b1, b0)

c0 = 0

# 2^0 の桁
c1, r1 = adder(a0, b0, c0)

# 2^1 の桁
c2, r2 = adder(a1, b1, c1)

# 2^2 の桁
c3, r3 = adder(a2, b2, c2)

# 2^3 の桁
c4, r4 = adder(a3, b3, c3)

output(c4, r4, r3, r2, r1)

こちらは、2進数で4桁までの足し算を行うためのサンプルプログラムです。
コードは、以下のような文法にしたがって記述してください。

文法

変数

真理値を代入する入れ物です。
一般的な言語における変数と同じものと思っていただいて差し支えないです。
命名規則としては、半角アルファベット、半角数字、アンダーバーを組み合わせることと、最初の文字は半角数字以外にすることです。

なお、この Lctxt の変数については、再定義、再代入禁止です。
一度変数を定義した場合は、もう一度値を定義したり代入することはできません。

コメント

コメントは # から始めることで入力できます。
一行だけ記述でき、改行はできません。
また、複数行のコメントを記述するための文法は現状ありません。

ファイル・モジュール

一つの回路の塊をモジュールと呼称します。
そして、一つのファイルが一つのモジュールと対応します。
ファイルの名前をそのまま呼び出せて、その名称の命名規則は変数と同じです。

なお、以下に示していくコードは全て、一行に一つずつ書いてください。

モジュールのインポート

モジュールは import キーワードでインポートできます。
先述の通り、ファイル名がそのままモジュール名となるので、例えば adder.lctxt で表されるモジュールをインポートするなら

import adder

でインポートできます。

インポートできるモジュールは、同じディレクトリにあるか、そのディレクトリより下層にあるモジュールのみです。
例えば相対パスが gates/submodule.lctxt で与えられるファイルをインポートする場合は、

import gates/submodule

/ でディレクトリとファイル名を区切ってください。

また、例えば複数の同名ファイルがあるとか、分かりづらい名前で保存しているなどの場合、例えば

import sample as adder

とすると、sample.lctxt で定義されているモジュールを adder という名前で使用できます。

入力定義文

各ファイルの定義は各モジュールの定義そのものになります。
モジュールにはもちろん入力および出力が必要であり、入力定義文では入力変数を定義することができます。

input(a, b, carry)

上記の adder.lctxt におけるこの文では、入力変数 a, b, carry を定義できます。
この入力変数とは、ファイル外から値が与えられる変数を指します。
したがって、自分で値の代入をすることはできません。

入力定義文では、これまでに定義された代入を再定義することはできません。
また、同じ入力定義文などで同じ変数を複数回定義することはできません。

また、入力定義文は複数定義することができます。
このとき与えられた入力は、入力変数が定義された順番に代入されます。
ただし、入力定義文を一つも定義しないことはできません。

出力定義文

モジュールには出力が必要であり、出力定義文では出力変数を定義することができます。

output(c3)

このとき、出力として c3 が与えられます。
この引数として与えられるものは、事前定義済みでなければなりません。

また、出力定義文も複数定義することができます。
このときファイル外への出力は、出力変数が定義された順番に行われます。
出力定義文を一つも定義しないことはできません。

代入文

同じ変数を複数回使いたい場合でも、文脈が違うのに同じ変数名を使いまわしているとわけが分からなくなります。
また、外部から変数を代入せず、定数を使いたい場合もあります。
その場合に代入文を使えます。

b1 = a1

これは、変数 a1 の値を代入した変数 b1 が定義されます。
ここで代入できる(右辺の)変数は、すでに定義された変数です。

また、定数は 0 または 1 です。0false, 1true に対応します。

c0 = 0

これで 0(false) が代入された変数 c0 が定義されます。

これまでと同様ですが、一度定義した変数に新しく値を代入することはできません。

関数・モジュールの呼び出し

まずそもそも変数には演算結果を代入したいものです。
このために、関数またはモジュールの計算結果を代入できます。

まず、下記のように組み込み関数(論理ゲート)を呼び出せます。

b0 = and(a0, a1, a2)

これにより、a0, a1, a2and を取ったものを代入した変数 b0 を定義できます。
組み込み関数については、出力の数は必ず1つになります。
また、入力の数は次の通りです。

関数名 入力数
and 可変長
or 可変長
nand 可変長
nor 可変長
not 1
xor 2
xnor 2

これ以外は組み込み関数はありません。

また、モジュールに関しても関数と同様に呼び出せます。
この入力数と出力数については、それぞれ input における引数の数と output における引数の数と一致します。

例えば先ほどの adder.lctxt については、

adder.lctxtinput(a, b, carry)

# 中略

output(c_res, d1)

となっているので、入力3, 出力2 のモジュールになります。

したがって、このモジュールを呼び出すにあたっては、

c1, r1 = adder(a0, b0, c0)

と呼び出せます。
このとき、変数 a0, b0, c0 は事前定義済みである必要があり、c1, r1 はこのタイミングで定義されます。

なお、一般的な言語と異なり、二つ以上の出力をまとめてリストとして一つの変数に代入、といったことはできません。
必ず一つの出力につき一つの変数を定義しなければなりません。

また、この例においては、a0, b0 および c0 はそれぞれ adder.lctxt
a, b, carry に代入されます。
また、adder.lctxtc_res および d1 がそれぞれ c1 および r1 に代入されます。

また、他の関数やモジュール、出力定義文の引数に、直接関数呼び出しやモジュールを渡すことはできません。
必ず変数に代入してから渡してください。

使い方

  1. 拡張機能をインストールします。
  2. コードを書きます。
  3. ファイル右上にある「▶」ボタンを押します。
  4. 実行完了!

です。これをすると、1 が true, 0 が false と対応して、
全ての場合に関する真理値表が出力されます。

おわりに

今回は Langium を用いたシミュレーション言語開発とプロダクトについてご紹介しました。
最後までお読みいただきありがとうございました。


  1. NOTはレッドストーントーチで、ORはレッドストーンそれ自身で表現できます。 ↩︎

  2. 二変数が入力される場合のもっとも単純な方法では、まずのnotを取り、()、そのORを取ります()。このnotを取ると、ANDになります()。証明はド・モルガンの法則で与えられます。 ↩︎

  3. ちなみにMinecraftに関しては単純な組み合わせ回路のみで表現できる回路はごくわずかです。というのも、例えばレッドストーントーチについてもレッドストーンリピーターについても1tick以上の遅延が存在するためです。このtickという概念が存在する場合、一般には組み合わせ回路で表現することはかなり困難です(一部の例外を除き組み合わせ回路で考えられない回路がほとんどです)。 ↩︎

  4. https://ja.wikipedia.org/wiki/論理回路#組み合わせ回路 ↩︎

  5. ベン図(集合)と論理回路を結びつけるのは分かりやすさを優先したと思ってください。今回は真(1)のところに色を塗りましたが、例えば出力が複数ある場合、あと入力が4次元以上の場合は表現が困難であることに注意が必要です。 ↩︎

  6. そうでなくてもゲートに二つずつ入力したものを段々に接続すれば同じことが実現できます。 ↩︎ ↩︎

  7. https://note.com/tona902/n/necd8eb96eb9d ↩︎

koukawa_pp icon
この記事を書いた人
koukawa_pp

20 情報通信系、基本サウンド班所属 ブログはプログラミングに偏りがち

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2022年9月26日
競プロしかシラン人間が web アプリ QK Judge を作った話
tqk icon tqk
2025年11月18日
1-Monthon 8班 Marten Clicker
Suima icon Suima
2024年4月14日
Spotifyのクライアントを自作しよう
d_etteiu8383 icon d_etteiu8383
2024年3月15日
個人開発として2週間でWebサービスを作ってみた話 〜「LABEL」の紹介〜
Natsuki icon Natsuki
2023年9月13日
ブログリレーを支えるリマインダー
H1rono_K icon H1rono_K
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記