Facebook Instagram GitHub LinkedIn
Programming

Membuat Bahasa Pemrograman Interpreted Sendiri

Pemrograman

Beberapa hari yang lalu saya berbicara dengan teman saya melalui WhatsApp hingga pada suatu topik bahwa ada yang membuat sebuah bahasa pemrograman dengan bahasa jaksel , ada juga yang menggunakan Aksara Jawa (hanacaraka).  Saya juga ingat dulu, ada orang yang membuat bahasa interpreter BAIK, sintaksnya menggunakan Bahasa Indonesia:

BAIK Scripting Language
A minimal Markdown editor
baik-lang.id
Preview link to BAIK Scripting Language

Sebelumnya, saya juga pernah melakukan eksplorasi terkait pembuatan bahasa pemrograman. Dulu saya melihat video untuk membuat bahasa pemrograman compiled, tentu saja prosesnya cukup rumit dan memerlukan pengetahuan yang mendalam, karena syntax harus diterjemahkan ke dalam bahasa assembly atau bahasa mesin.

Kemarin, saya mendapatkan sebuah playlist YouTube tentang membuat bahasa pemrograman sendiri dengan python. Salah satu komentar dalam video tersebut yang menarik adalah "Creating Interpreter using interpreted programming language.".

Hal ini cukup lucu, karena Python sendiri adalah bahasa interpreted, jika kita membuat interpreter dengan python, maka proses yang akan terjadi adalah: bahasa kita -> diterjemahkan ke python -> diterjemahkan ke bahasa komputer. Tentu saja secara performa akan sangat menurun.

Kemudian, saya mencoba untuk membuat interpreter menggunakan GO yang compiled. Anda dapat melihat bahasa pemrograman yang coba saya buat (IndoScript) di repositori berikut ini:

GitHub - hsnfirdaus/indoscript: Bahasa pemrograman dengan sintaks bahasa Indonesia. Interpreternya dibuat dengan GO.
Bahasa pemrograman dengan sintaks bahasa Indonesia. Interpreternya dibuat dengan GO. - hsnfirdaus/indoscript
github.com
Preview link to GitHub - hsnfirdaus/indoscript: Bahasa pemrograman dengan sintaks bahasa Indonesia. Interpreternya dibuat dengan GO.

Langkah-Langkah Membuat Bahasa Pemrograman Interpreted

Saya tidak akan membuat tutorial secara lengkap, karena akan menjadi tulisan yang sangat panjang, mungkin bahkan bisa menjadi sebuah buku. Jika anda tertarik untuk mengikuti tutorial silahkan lihat playlist yang sudah saya sebutkan sebelumnya.

Berikut ini adalah langkah-langkah secara umum yang harus dilakukan dalam membuat bahasa pemrograman sendiri:

Mendefinisikan Grammar

Langkah pertama, kita harus mendefinisikan grammar atau bagaimana syntax umum bahasa yang akan kita buat, ini juga akan menentukan urutan operasi mana yang harus didahulukan. Misalnya saja dalam operasi aritmatika kita mengenal penambahan (+), pengurangan (-), pembagian (/), dan perkalian (*). Tentu saja pembagian dan perkalian harus kita utamakan, harus dieksekusi terlebih dahulu sebelum kita mengeksekusi hasilnya dengan operasi yang lebih rendah prioritasnya (penambahan dan pengurangan).

Anda dapat menuliskan grammar ini di mana saja, misalnya membuat file .txt di project anda ataupun dokumentasi tersendiri. Ini akan berguna ketika bahasa yang anda buat semakin berkembang, atau terjadi masalah dalam urutan operasi yang dilakukan. Tidak ada format khusus untuk membuat grammar, anda bisa membuat sesuka hati anda. Contoh grammar menggunakan bahasa Indonesia adalah:

deklarasi   : KATKUN:var PENGENAL SD ekspresi TK
            : KATKUN:jika BKURUNG ekspresi TKURUNG BKURAWAL deklarasi* TKURAWAL
            : KATKUN:fungsi BKURUNG (PENGENAL)?(KOMA PENGENAL)?* TKURUNG BKURAWAL deklarasi* TKURAWAL
            : atom-fn TK
            : KATKUN:balikan TK

ekspresi    : term ((TAMBAH|KURANG) term)*?

term        : atom ((KALI|BAGI) atom)*?

atom        : BUL|DES|PENGENAL|TEKS
            : BKURUNG ekspresi TKURUNG
atom-fn     : PENGENAL BKURUNG (ekspresi)?(KOMA ekspresi)?* TKURUNG

Dalam grammar di atas terdapat kata-kata yang dikenal seperti PENGENAL, BKURUNG (Buka Kurung), TKURUNG (Tutup Kurung), KATKUN (Kata Kunci / Keyword), hal itu nanti akan kita sebut dengan istilah TOKEN.

Dengan contoh grammar di atas, kode berikut akan valid secara sintaks:

fungsi hitung_umur(tahun_lahir, tahun_sekarang){
    var umur = tahun_sekarang - tahun_lahir;
    balikan umur;
}

var tahun_lahir = 2003;
var tahun_sekarang = 2004;

cetakBr("UMUR SAYA:");
cetak(hitung_umur(tahun_lahir, tahun_sekarang));

Membuat Lexer

Proses lexing atau analisis akan mengubah setiap karakter dalam kode dengan sintaks bahasa yang kita buat menjadi token-token seperti: TK, BKURUNG, TEKS, TKURUNG, dan sebagainya.

Contoh kode untuk pendefinisian token:

package lekser

import "indoscript/utils"

type JenisToken string

const (
	T_BUL JenisToken = "BUL"
	T_DES JenisToken = "DES"

	T_TAMBAH  JenisToken = "TAMBAH"
	T_KURANG  JenisToken = "KURANG"
	T_KALI    JenisToken = "KALI"
	T_BAGI    JenisToken = "BAGI"

	T_BKURUNG  JenisToken = "BKURUNG"
	T_TKURUNG  JenisToken = "TKURUNG"
	T_BKURAWAL JenisToken = "BKURAWAL"
	T_TKURAWAL JenisToken = "TKURAWAL"

	T_KATKUN   JenisToken = "KATKUN"
	T_PENGENAL JenisToken = "PENGENAL"
	T_TK       JenisToken = "TK"
	T_KOMA     JenisToken = "KOMA"
	T_TEKS     JenisToken = "TEKS"
	T_ADF      JenisToken = "ADF"

)


// Struktur Token

type Token struct {
	utils.BasisPosisi
	Jenis JenisToken
	Isi   interface{}
}

Selanjutnya, untuk contoh kode Lexer-nya:

package lekser

// Menyimpan teks dan posisi kursor saat ini
type Lekser struct {
	teks            string
	indeks          int
	baris           int
	kolom           int
	karakterSaatIni string
}


// Mulai dari karakter indeks 0
func LekserBaru(teks string) Lekser {
	lek := Lekser{
		teks:            teks,
		indeks:          -1,
		baris:           1,
		kolom:           -1,
		karakterSaatIni: "",
	}
	lek.maju()

	return lek
}

// Pendefinisian karakter
var ANGKA = []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}
var HURUF = []string{
	"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
	"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
}
var HURUF_ANGKA = append(ANGKA, HURUF...)
var PENGENAL_VALID = append(HURUF_ANGKA, []string{"_"}...)

Dalam kode di atas kita juga melihat pendefinisian bagaimana identifier (pengenal) yang valid, yang dapat digunakan untuk nama fungsi dan variabel.

Selanjutnya, untuk contoh kode utamanya, yaitu mengubah teks menjadi token:

package lekser

import "slices"

func (lek *Lekser) Tokenisasi() ([]Token, *KesalahanLekser) {
	var tokenToken []Token = make([]Token, 0)

	for lek.karakterSaatIni != "" {

        // Jika karakter saat ini angka ataupun huruf (bukan simbol)
        // Memerlukan penanganan khusus
		if slices.Contains(ANGKA, lek.karakterSaatIni) {
			tokenToken = append(tokenToken, lek.tokenisasiAngka())
			continue
		} else if slices.Contains(HURUF, lek.karakterSaatIni) {
			tokenToken = append(tokenToken, lek.tokenisasiKataKunci())
			continue
		} else {

			switch lek.karakterSaatIni {
			case "\t", " ", "\n":
            
            // Jika karakter petik, kemungkinan besar berupa teks/string
			case "'", "\"":
				tokenToken = append(tokenToken, lek.tokenisasiTeks())
				continue

            // Case selanjutnya, simbol-simbol
			case "+":
				tokenToken = append(tokenToken, Token{
					Jenis:       T_TAMBAH,
					BasisPosisi: lek.basisPosisi(),
				})

			case "-":
				tokenToken = append(tokenToken, Token{
					Jenis:       T_KURANG,
					BasisPosisi: lek.basisPosisi(),
				})

			case "*":
				tokenToken = append(tokenToken, Token{
					Jenis:       T_KALI,
					BasisPosisi: lek.basisPosisi(),
				})
			case "/":
				tokenToken = append(tokenToken, Token{
					Jenis:       T_BAGI,
					BasisPosisi: lek.basisPosisi(),
				})

			case "^":
				tokenToken = append(tokenToken, Token{
					Jenis:       T_PANGKAT,
					BasisPosisi: lek.basisPosisi(),
				})

			case "(":
				tokenToken = append(tokenToken, Token{
					Jenis:       T_BKURUNG,
					BasisPosisi: lek.basisPosisi(),
				})

			case ")":
				tokenToken = append(tokenToken, Token{
					Jenis:       T_TKURUNG,
					BasisPosisi: lek.basisPosisi(),
				})

			case ";":
				tokenToken = append(tokenToken, Token{
					BasisPosisi: lek.basisPosisi(),
					Jenis:       T_TK,
				})

			case ",":
				tokenToken = append(tokenToken, Token{
					BasisPosisi: lek.basisPosisi(),
					Jenis:       T_KOMA,
				})

			case "{":
				tokenToken = append(tokenToken, Token{
					BasisPosisi: lek.basisPosisi(),
					Jenis:       T_BKURAWAL,
				})

			case "}":
				tokenToken = append(tokenToken, Token{
					BasisPosisi: lek.basisPosisi(),
					Jenis:       T_TKURAWAL,
				})

			default:
				return nil, &KesalahanLekser{
					BasisPosisi: lek.basisPosisi(),
					detail:      "Karakter tidak valid: \"" + lek.karakterSaatIni + "\"",
				}

			}
		}

		lek.maju()
	}

	tokenToken = append(tokenToken, Token{
		Jenis:       T_ADF,
		BasisPosisi: lek.basisPosisi(),
	})

	return tokenToken, nil
}


// Disini kita bisa proses karakter yang diawali petik tadi
func (lek *Lekser) tokenisasiTeks() Token {
	teks := ""
	pemisah := lek.karakterSaatIni
	posisi := lek.basisPosisi()

    // Melakukan looping untuk mengambil karakter didalam tanda petik
    // serta menangani escape (Cth: 'Ma\'af')
	lek.maju()
	escaped := false
	for lek.karakterSaatIni != pemisah {
		teks = teks + lek.karakterSaatIni
		lek.maju()
		if lek.karakterSaatIni == "\\" {
			escaped = true
			lek.maju()
			continue
		}
		if lek.karakterSaatIni == pemisah && !escaped {
			break
		}
		if escaped {
			escaped = false
		}
	}
	lek.maju()

	return Token{
		BasisPosisi: posisi,
		Jenis:       T_TEKS,
		Isi:         teks,
	}
}

Tentu saja tidak semua kode saya tampilkan, jika anda ingin lebih lengkap, silahkan lihat di tautan repositori saya sebelumnya.

Ketika lexer yang anda buat berhasil melakukan tugasnya maka teks yang berisikan kode dengan syntax bahasa pemrograman kita akan diubah menjadi daftar token, Contohnya dalam kode berikut:

var nama_saya = "Hasan";

Akan berubah menjadi list token:

[
    Token(Jenis: T_KATKUN, Isi: "var"),
    Token(Jenis: T_PENGENAL, Isi: "nama_saya"),
    Token(Jenis: T_SD, Isi: null),
    Token(Jenis: T_TEKS, Isi: "Hasan"),
    Token(Jenis: T_TK, Isi: null),
]

Dalam proses tokenisasi biasanya spasi, tab, dan baris baru akan diabaikan (tergantung sintaks bahasa pemrograman yang anda inginkan). 

Membuat Parser

Setelah berhasil mengubah teks biasa menjadi daftar token, langkah selanjutnya adalah melakukan parsing. Mengubah token-token menjadi node-node bersarang yang urutannya akan berpengaruh saat eksekusi.

Node sendiri tergantung kebutuhan kita, contohnya kita bisa membuat NodeBilangan, NodeAturVariabel, NodeAmbilVariabel, dan lain-lain.

Contoh pendefinisian struktur node:

type NodeTeks struct {
	utils.BasisPosisi
	Teks string
}

// Operasi seperti +, -, *, dan /
type NodeOperasi struct {
	utils.BasisPosisi
	NodeKiri  interface{}
	Operasi   lekser.JenisToken
	NodeKanan interface{}
}

type NodeAturVariabel struct {
	utils.BasisPosisi
	NamaVariabel string
	Node         interface{}
}

// Node-node lain

Selanjutnya, membuat parser:

package pengurai

import (
	"indoscript/lekser"
	"indoscript/utils"
)

// Menyimpan posisi cursor token saat ini
type Pengurai struct {
	daftarToken  []lekser.Token
	tokenSaatIni *lekser.Token
	indeks       int
}

func PenguraiBaru(daftarToken []lekser.Token) Pengurai {
	pengurai := Pengurai{
		daftarToken:  daftarToken,
		tokenSaatIni: nil,
		indeks:       -1,
	}
	pengurai.maju()

	return pengurai
}

func (p *Pengurai) Urai() (*NodeAkar, *TokenTakTerduga) {
	nodeNode := make([]interface{}, 0)

	for p.tokenSaatIni != nil {
		if p.tokenSaatIni.Jenis == lekser.T_ADF {
			break
		}
		node, err := p.deklarasi()
		if err != nil {
			return nil, err
		}

		nodeNode = append(nodeNode, node)

	}

	return &NodeAkar{
		BasisPosisi: utils.BasisPosisi{
			Baris: 0,
			Kolom: 0,
		},
		NodeNode: nodeNode,
	}, nil
}

// Melakukan parsing pada grammar jenis deklarasi
func (p *Pengurai) deklarasi() (interface{}, *TokenTakTerduga) {
	tok := p.tokenSaatIni
	if p.tokenSaatIni.Jenis == lekser.T_KATKUN {
		switch tok.Isi {
		case lekser.KK_VAR:
			return p.deklarasiVar()
		case lekser.KK_JIKA:
			return p.deklarasiJika()
		case lekser.KK_FUNGSI:
			return p.deklarasiFungsi()
		case lekser.KK_BALIKAN:
			return p.deklarasiBalikan()

		}
	}

	if tok.Jenis == lekser.T_PENGENAL {
		hasil, err := p.expr()
		if err != nil {
			return nil, err
		}
		err = p.tkMaju()
		if err != nil {
			return nil, err
		}
		return hasil, nil
	}

	return nil, &TokenTakTerduga{
		BasisPosisi: p.basisPosisi(),
		diharapkan:  []lekser.JenisToken{lekser.T_KATKUN},
		ditemukan:   p.tokenSaatIni.Jenis,
	}
}

// Melakukan proses parsing deklarasi yang diawali kata kunci "var"
func (p *Pengurai) deklarasiVar() (interface{}, *TokenTakTerduga) {
	posisi := p.basisPosisi()

	p.maju()
	if p.tokenSaatIni.Jenis != lekser.T_PENGENAL {
		return nil, &TokenTakTerduga{
			BasisPosisi: p.basisPosisi(),
			diharapkan:  []lekser.JenisToken{lekser.T_PENGENAL},
			ditemukan:   p.tokenSaatIni.Jenis,
		}
	}

	namaVariabel := p.tokenSaatIni.Isi

	p.maju()

	if p.tokenSaatIni.Jenis != lekser.T_SD {
		return nil, &TokenTakTerduga{
			BasisPosisi: p.basisPosisi(),
			diharapkan:  []lekser.JenisToken{lekser.T_SD},
			ditemukan:   p.tokenSaatIni.Jenis,
		}
	}

	p.maju()

	expr, err := p.expr()
	if err != nil {
		return nil, err
	}

	err = p.tkMaju()
	if err != nil {
		return nil, err
	}

	return &NodeAturVariabel{
		BasisPosisi:  posisi,
		NamaVariabel: namaVariabel.(string),
		Node:         expr,
	}, nil
}


// Kode lain

Ketika parser kita berhasil menjalankan tugasnya, token-token berikut:

// Kode Asli:
var umur = 2024 - 2003;

// Token:
[
    Token(Jenis: T_KATKUN, Isi: "var"),
    Token(Jenis: T_PENGENAL, Isi: "umur"),
    Token(Jenis: T_SD, Isi: null),
    Token(Jenis: T_BUL, Isi: 2024),
    Token(Jenis: T_KURANG, Isi: null),
    Token(Jenis: T_BUL, Isi: 2003),
    Token(Jenis: T_TK, Isi: null),
]

Akan berubah menjadi node bersarang:

[
    NodeAturVariabel(
        NamaVariabel: "umur",
        Node: NodeOperasi(
            NodeKiri: NodeBilangan(
                Angka: 2024,
            ),
            Operasi: "-",
            NodeKanan: NodeBilangan(
                Angka: 2003,
            ),
        ),
    ),
]

Kita lihat, pada contoh node di atas, NodeOperasi ada di dalam NodeAturVariabel. Maka, NodeOperasi akan kita jalankan terlebih dahulu nantinya.

Membuat Interpreter

Langkah terakhir, setelah kita berhasil mendapatkan Node-Node, kita harus mengeksekusi node tersebut satu per satu, dari atas ke bawah.  Berbeda jika kita ingin membuat compiler, kita harus menerjemahkan node tersebut menjadi bahasa mesin (ataupun bahasa perantara yang bisa di-compile ke executable binary).

Langkah pertama dalam interpreter bahasa IndoScript yang saya buat adalah membuat struktur untuk tipe data, karena tentunya tipe TEKS tidak bisa kita operasikan dengan tipe BILANGAN:

package jenis

import (
	"errors"
	"fmt"
	"indoscript/lekser"
	"math"
)

type Bilangan struct {
	Angka float64
}

func (b *Bilangan) Operasi(targetBil *Bilangan, op lekser.JenisToken) (*Bilangan, error) {
	var hasil float64

	switch op {
	case lekser.T_TAMBAH:
		hasil = b.Angka + targetBil.Angka

	case lekser.T_KURANG:
		hasil = b.Angka - targetBil.Angka

	case lekser.T_KALI:
		hasil = b.Angka * targetBil.Angka

	case lekser.T_BAGI:
		hasil = b.Angka / targetBil.Angka

	case lekser.T_PANGKAT:
		hasil = math.Pow(b.Angka, targetBil.Angka)

	default:
		return nil, errors.New(fmt.Sprint("Operasi tak terduga: ", op))

	}

	return &Bilangan{
		Angka: hasil,
	}, nil
}

Selanjutnya, kita perlu membuat symbol table untuk menyimpan variabel dan fungsi yang sudah kita parsing:

package penerjemah

import (
	"errors"
	"indoscript/pengurai"
)

type ButirFungsi struct {
	namaArgument []string
	nodeAkar     *pengurai.NodeAkar
}

type TabelSimbol struct {
	variabel map[string]interface{}
	fungsi   map[string]ButirFungsi
	induk    *TabelSimbol
}

func TabelSimbolBaru(induk *TabelSimbol) TabelSimbol {
	ts := TabelSimbol{}
	ts.variabel = make(map[string]interface{})
	ts.fungsi = make(map[string]ButirFungsi)
	ts.induk = induk

	return ts
}

func (ts *TabelSimbol) ambilVar(pengenal string) (interface{}, error) {
	val, ok := ts.variabel[pengenal]
	if !ok && ts.induk != nil {
		val, err := ts.induk.ambilVar(pengenal)
		if err != nil {
			return nil, err
		}
		return val, nil
	} else if !ok {
		return nil, errors.New("Variabel tak terdefinisikan : " + pengenal)
	}

	return val, nil
}

func (ts *TabelSimbol) aturVar(pengenal string, isi interface{}) {
	ts.variabel[pengenal] = isi
}

func (ts *TabelSimbol) ambilFung(pengenal string) (*ButirFungsi, error) {
	val, ok := ts.fungsi[pengenal]
	if !ok && ts.induk != nil {
		val, err := ts.induk.ambilFung(pengenal)
		if err != nil {
			return nil, err
		}
		return val, nil
	} else if !ok {
		return nil, errors.New("Fungsi tak terdefinisikan: " + pengenal)
	}

	return &val, nil
}

func (ts *TabelSimbol) aturFung(pengenal string, namaAgumen []string, nodeAkar *pengurai.NodeAkar) {
	ts.fungsi[pengenal] = ButirFungsi{
		namaArgument: namaAgumen,
		nodeAkar:     nodeAkar,
	}

}

Dalam tabel simbol yang saya buat, terdapat induk, berguna jika kita memerlukan scoping variabel dan fungsi.

Untuk contoh kode core interpreter-nya sendiri adalah:

package penerjemah

import (
	"indoscript/penerjemah/jenis"
	"indoscript/pengurai"
	"reflect"
)

// Kita memerlukan symbol table
type Penerjemah struct {
	ts *TabelSimbol
}

func PenerjemahBaru() Penerjemah {
	p := Penerjemah{}
	ts := TabelSimbolBaru(nil)
	p.ts = &ts

	return p
}

// Jika kita memparsing fungsi, tentunya scope variabel akan berubah
// Kita membuat interpreter baru
func (p *Penerjemah) buatAnak() Penerjemah {
	ts := TabelSimbolBaru(p.ts)
	pb := Penerjemah{
		ts: &ts,
	}

	return pb
}

func (p *Penerjemah) Jalankan(node *pengurai.NodeAkar) *KesalahanPenerjemah {
	_, err := p.panggilNode(node)
	return err
}

// Mengeksekusi node satu per satu
func (p *Penerjemah) panggilNode(node interface{}) (interface{}, *KesalahanPenerjemah) {

	switch v := node.(type) {
	case *pengurai.NodeAkar:
		return p.nodeAkar(v)
		
	case *pengurai.NodeBilangan:
		return p.nodeBilangan(v)

	case *pengurai.NodeTeks:
		return p.nodeTeks(v)

	case *pengurai.NodeBoolean:
		return p.nodeBoolean(v)

	case *pengurai.NodeOperasi:
		return p.nodeOperasi(v)

	case *pengurai.NodeOperasiUner:
		return p.nodeOperasiUner(v)

	case *pengurai.NodeOperasiDanAtau:
		return p.nodeOperasiDanAtau(v)

	case *pengurai.NodeAturVariabel:
		return p.nodeAturVariabel(v)

	case *pengurai.NodeAksesVariabel:
		return p.nodeAksesVariabel(v)

	case *pengurai.NodeAturFungsi:
		return p.nodeAturFungsi(v)

	case *pengurai.NodePanggilFungsi:
		return p.nodePanggilFungsi(v)

	case *pengurai.NodeJika:
		return p.NodeJika(v)

	case *pengurai.NodeBalikan:
		return p.nodeBalikan(v)

	default:
		jenisNode := reflect.TypeOf(v)
		return nil, &KesalahanPenerjemah{
			pesan: "Node \"" + jenisNode.String() + "\" tak dikenali!",
		}
	}
}

Di sini, setiap node akan diterjemahkan ke dalam sebuah fungsi/method. Contoh kode untuk fungsi nodeOperasi adalah:

package penerjemah

import (
	"fmt"
	"indoscript/lekser"
	"indoscript/penerjemah/jenis"
	"indoscript/pengurai"
	"reflect"
	"slices"
)

func (p *Penerjemah) nodeOperasi(node *pengurai.NodeOperasi) (interface{}, *KesalahanPenerjemah) {
	nodeKiri, err := p.panggilNode(node.NodeKiri)
	if err != nil {
		return nil, err
	}

	switch kiri := nodeKiri.(type) {
	case *jenis.Bilangan:
		kanan, err := p.panggilNodeBilangan(node.NodeKanan)
		if err != nil {
			return nil, err
		}

        // Kita perlu menghandle operasi boolean (>, <, >=, <=, ==)
		if slices.Contains(lekser.TOKEN_PERBANDINGAN, node.Operasi) {
			res, er := kiri.OperasiBoolean(kanan, node.Operasi)
			if er != nil {
				return nil, &KesalahanPenerjemah{
					BasisPosisi: node.BasisPosisi,
					pesan:       er.Error(),
				}
			}

			return res, nil
		} else {

			bil, er := kiri.Operasi(kanan, node.Operasi)
			if er != nil {
				return nil, &KesalahanPenerjemah{
					BasisPosisi: node.BasisPosisi,
					pesan:       er.Error(),
				}
			}

			return bil, nil
		}
        
        // Operasi tipe data lain

	}

	return nil, &KesalahanPenerjemah{
		BasisPosisi: node.BasisPosisi,
		pesan:       fmt.Sprint("Operasi tidak dapat dilakukan pada jenis ", reflect.TypeOf(nodeKiri), "!"),
	}
}

Interpreter dikatakan benar jika berhasil menerjemahkan node-node yang ada kedalam eksekusi program.

Kesimpulan

Membuat bahasa pemrograman sendiri ternyata tidak sesusah yang dibayangkan, tentunya jika anda menguasai sebuah bahasa pemrograman yang dapat dijadikan alat untuk pembuat interpreter atau compiler.

Tetapi untuk membuat bahasa pemrograman yang "benar", "efektif" dan "efisien" memerlukan orang yang lebih ahli.

Jangan lupa untuk coba melihat bahasa pemrograman dengan sintaks Bahasa Indonesia yang saya buat sendiri:

GitHub - hsnfirdaus/indoscript: Bahasa pemrograman dengan sintaks bahasa Indonesia. Interpreternya dibuat dengan GO.
Bahasa pemrograman dengan sintaks bahasa Indonesia. Interpreternya dibuat dengan GO. - hsnfirdaus/indoscript
github.com
Preview link to GitHub - hsnfirdaus/indoscript: Bahasa pemrograman dengan sintaks bahasa Indonesia. Interpreternya dibuat dengan GO.

Semoga Bermanfaat!