この記事は、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 |
ベン図で表すと次のような感じです。

AND
次にANDについて説明します。
ANDは、全ての入力値が1であれば1を出力し、そうでなければ0を出力します。
| A | B | X |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
ベン図で表すと次のような感じです。

そして、先ほど「全ての入力値が」とお伝えしました。したがってこの 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 |
ベン図で表すと次のような感じです。

OR
次にORについて説明します。
ORは、少なくとも1つの入力値が1であれば1を出力し、そうでなければ0を出力します。
| A | B | X |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
ベン図で表すと次のような感じです。

そして、こちらも先ほど「少なくとも一つの入力値が」とお伝えしました。したがってこの 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 |
ベン図で表すと次のような感じです。

NAND
次にNANDについて説明します。
NANDは、AND + NOT です。したがってすべての入力が1であれば0を出力し、そうでなければ1を出力します。
| A | B | X |
|---|---|---|
| 0 | 0 | 1 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
ベン図で表すと次のような感じです。

そして、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 |
ベン図で表すと次のような感じです。

一般には 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 |
ベン図で表すと次のような感じです。

そして、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 |
ベン図で表すと次のような感じです。

XOR
次にXORについて説明します。
XORは、入力された二つの値が異なるときに1を、同じときに0を出力します。
| A | B | X |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
ベン図で表すと次のような感じです。

XNOR
次にXNORについて説明します。
XNORは、NOT + XOR です。つまり入力された二つの値が同じときに1を、異なるときに0を出力します。
| A | B | X |
|---|---|---|
| 0 | 0 | 1 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
ベン図で表すと次のような感じです。

以上のような論理ゲートを合わせることによって、様々な回路を作成することができます。
回路シミュレーション言語「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 です。0 が false, 1 が true に対応します。
c0 = 0
これで 0(false) が代入された変数 c0 が定義されます。
これまでと同様ですが、一度定義した変数に新しく値を代入することはできません。
関数・モジュールの呼び出し
まずそもそも変数には演算結果を代入したいものです。
このために、関数またはモジュールの計算結果を代入できます。
まず、下記のように組み込み関数(論理ゲート)を呼び出せます。
b0 = and(a0, a1, a2)
これにより、a0, a1, a2 の and を取ったものを代入した変数 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.lctxt の c_res および d1 がそれぞれ c1 および r1 に代入されます。
また、他の関数やモジュール、出力定義文の引数に、直接関数呼び出しやモジュールを渡すことはできません。
必ず変数に代入してから渡してください。
使い方
- 拡張機能をインストールします。
- コードを書きます。
- ファイル右上にある「▶」ボタンを押します。
- 実行完了!
です。これをすると、1 が true, 0 が false と対応して、
全ての場合に関する真理値表が出力されます。
おわりに
今回は Langium を用いたシミュレーション言語開発とプロダクトについてご紹介しました。
最後までお読みいただきありがとうございました。
NOTはレッドストーントーチで、ORはレッドストーンそれ自身で表現できます。 ↩︎
二変数が入力される場合のもっとも単純な方法では、まずとのnotを取り、()、そのORを取ります()。このnotを取ると、ANDになります()。証明はド・モルガンの法則で与えられます。 ↩︎
ちなみにMinecraftに関しては単純な組み合わせ回路のみで表現できる回路はごくわずかです。というのも、例えばレッドストーントーチについてもレッドストーンリピーターについても1tick以上の遅延が存在するためです。このtickという概念が存在する場合、一般には組み合わせ回路で表現することはかなり困難です(一部の例外を除き組み合わせ回路で考えられない回路がほとんどです)。 ↩︎
ベン図(集合)と論理回路を結びつけるのは分かりやすさを優先したと思ってください。今回は真(1)のところに色を塗りましたが、例えば出力が複数ある場合、あと入力が4次元以上の場合は表現が困難であることに注意が必要です。 ↩︎