diskusi.tech (beta) Community

loading...
Cover image for Berkenalan dengan ANTLR untuk Membuat Bahasa Bot Engine Sederhana
DeepTechID

Berkenalan dengan ANTLR untuk Membuat Bahasa Bot Engine Sederhana

mimindeeptech profile image Mimin Deep Tech ・6 min read

Penulis: Aditya Purwa

Bismillahirrahmanirrahim.

ANTLR (Another Tool for Language Recognition) adalah satu alat yang bisa kita gunakan untuk membuat lexer dan parser secara otomatis cukup dengan menggunakan file grammar.

Tentang Lexer dan Parser

Lexer adalah suatu komponen yang berfungsi untuk mendeteksi bagian terkecil (token) dari suatu bahasa. Proses pendeteksian ini disebut lexing. Biasanya regular expressions digunakan untuk melakukan proses lexing karena bentuk token biasanya sangat sederhana.

Sedangkan parser berfungsi sebagai komponen yang mendeteksi struktur dari beberapa token. Proses pendeteksian ini disebut parsing. Proses parsing lebih rumit daripada lexing, sehingga regular expression kurang cocok untuk digunakan di sini.

Karena proses pembuatan lexer dan parser cukup melelahkan jika dilakukan secara manual. Dibuatlah ANTLR oleh Terence Parr untuk memudahkan proses pembuatan lexer dan parser secara otomatis.

Membuat Bahasa Bot Engine

Kita akan mencoba menggunakan ANTLR untuk membuat bahasa yang digunakan untuk mendefinisikan perilaku sebuah chatbot. Sebenarnya yang kita bahas adalah contoh yang pernah saya bikin setahun yang lalu. Tulisan ini sekaligus menjadi penyegar ingatan saya tentang ANTLR.

Kunjungi situs resmi ANTLR untuk panduan installasi dan setup ANTLR.

Arubick
Tahun lalu sempat membuat sebuah bahasa chatbot sederhana yang saya beri nama Arubick, terinspirasi dari nama hero di DoTA 2. Kalian bisa mengaksesnya di https://github.com/adityapurwa/Arubick.

PT

Arubick adalah sebuah bahasa sederhana untuk membuat chatbot, project ini merupakan tempat saya belajar tentang ANTLR dan cara membuat interpreter. Arubick didesain untuk berjalan di atas Node.js, dan menggunakan TypeScript sebagai bahasa pemrogramannya.

Jadi bagaimana bentuk bahasa yang akan kita buat? Contoh sederhana yang ada di readme repository Arubick adalah sebagai berikut:

RandomBot:0.0.1 # Baris pertama adalah nama dan versi bot yang dibuat

def ext/random = extern("./js/random.js") # Digunakan untuk mengimpor sebuah fungsi JavaScript

# Intent di Arubick adalah pencarian string sederhana, jika ada string "random" di sebuah kalimat, berarti intentnya query
def intent/query = [
    "random"
]
def intent/greet = [
    "hi",
    "hello"
]

# Fungsi utama yang akan dijalankan oleh bot engine
main {
    # Arubick adalah chatbot yang berjalan di terminal, sehingga reply berarti menunggu user mengirim pesan
    def message = await reply()

    if message is intent/greet {
        say("Hi! I am random bot, ask me for a random number.")
        # Arubick berjalan sequential, restart akan memulai bot kembali lagi dari awal
        restart()
    } else {        
        if message is intent/query {
            # Kita bisa memanggil fungsi yang diimpor
            def random = ext/random()
            say("Generated random number ${random}")
            restart()
        } else {
            say("Sorry, I don't understand")
            restart()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Bagi kalian yang ingin mendesain sebuah bahasa, saya sarankan membuat contohnya terlebih dahulu daripada aturan-aturannya terlebih dahulu. Contoh lebih mudah untuk diubah-ubah dan kita bisa merasakan seperti apa hasil akhir bahasa yang akan dibuat.

Lalu seperti apa bentuk grammar ANTLR yang harus kita buat untuk memproses bahasa di atas?

Pertama kita harus membuat lexer yang akan memproses token yang ada di Arubick.

grammar Arubick;

// Whitespace, karena Arubick tidak menganggap whitespace penting, maka diberi tanda -> skip
WS
  : [ \t]+ -> skip
  ;

// Sama seperti whitespace
EOL
  : [\r\n]+ -> skip
  ;

// Setiap keyword merupakan sebuah token tersendiri
DEF
  : 'def'
  ;


IF
  : 'if'
  ;


ELSE
  : 'else'
  ;


IS
  : 'is'
  ;


AWAIT
  : 'await'
  ;


AND
  : 'and'
  ;


OR
  : 'or'
  ;

// Begitu juga dengan simbol dan operator yang ada di bahasa Arubick
LBRACKET
  : '['
  ;


RBRACKET
  : ']'
  ;


LPARENT
  : '('
  ;


RPARENT
  : ')'
  ;


LSCOPE
  : '{'
  ;


RSCOPE
  : '}'
  ;


ASSIGN
  : '='
  ;


// String expression juga merupakan token
STRING
  : '"' ('\\"' | ~ ["\\])* '"'
  ;
NUMBER
  : '-'? [0-9]+
  ;
// Identifier juga merupakan token
ID
  : [A-Za-z][A-Za-z0-9]*
  ;
Enter fullscreen mode Exit fullscreen mode

Ketika kita sudah selesai mendefinisikan bagian terkecil dari bahasa kita. Kita tentukan bagaimana bagian-bagian tersebut harus disusun untuk menjadi sebuah struktur grammar.

// Kita tentukan rule/aturan nama storage adalah ID/ID.
storage
  : ID '/' ID
  ;

// Sedangkan aturan untuk pendeklarasian storage adalah dengan token DEF, diikuti nama storage
defStorage
  : DEF storage
  ;

variable
  : ID
  ;

defVariable
  : DEF variable
  ;

// Variable bisa diakses dengan variable.variabel.variabel.dst...
accessVariable
  : variable ('.' variable)+
  ;

// Assignment bisa dilakukan ke storage, deklarasi storage, variable, dan deklarasi variable, diikuti token ASSIGN dan expression
assign
  : storage ASSIGN expression
  | defStorage ASSIGN expression
  | variable ASSIGN expression
  | defVariable ASSIGN expression
  ;

// Parameter method adalah sebua expression yang dipisahkan dengan comma
methodParameters
  : expression (',' expression)*
  ;

// Method/function bisa dipanggil dengan AWAIT (optional), lalu storage/variabel yang menampung method tersebut
// diikuti dengan parenthesis dan parameter di dalamnya
methodCall
  : AWAIT? storage LPARENT methodParameters? RPARENT
  | AWAIT? variable LPARENT methodParameters? RPARENT
  ;

// Hanya ada satu comparison operator di Arubick, yaitu IS.
comparison
  : variable IS (expression|storage)
  ;

// Sedangkan expression bisa berupa string, number, dsb di bawah.
expression
  : STRING
  | NUMBER
  | variable
  | accessVariable
  | methodCall
  | array
  ;

array
  : LBRACKET expression  (',' expression)* RBRACKET
  ;

elseStatement
  : ELSE LSCOPE statement* RSCOPE
  ;

// Aturan if bisa diikuti oleh else (tidak ada elseif)
ifStatement
  : IF comparison ((AND|OR) comparison)* LSCOPE statement* RSCOPE elseStatement?
  ;

// Statement yang valid di Arubick harus berupa assignment, method call, atau if statement
statement
  : assign
  | methodCall
  | ifStatement
  ;

// Ini aturan yang ada di baris paling atas file
botDefinition
  : ID ':' NUMBER ('.' NUMBER)*
  ;
// Main method di Arubick
main
  : 'main' LSCOPE statement* RSCOPE
  ;
// File rbot Arubick yang valid harus berisi bot definition, bisa ada beberapa statement, dan main method
program
  : botDefinition statement* main
  ;
Enter fullscreen mode Exit fullscreen mode

Sekarang kita sudah punya grammar yang dibutuhkan untuk memproses bahasa kita. Kita tinggal meminta ANTLR untuk mengenerate kode yang akan digunakan untuk memproses Arubick.

Karena saya menggunakan antlr4ts, maka saya gunakan perintah antlr4ts untuk menghasilkan kode parser dan lexernya.

$ antlr4ts src/antlr/Arubick.g4 -o src/antlr

Ada 6 jenis file yang akan dihasilkan:

  • Arubick.tokens
  • ArubickLexer.token
  • ArubickLexer.ts
  • ArubickListener.ts
  • ArubickParser.ts
  • ArubickVisitor.ts

Kita tidak perlu mengotak-atik file yang dihasilkan oleh ANTLR, yang kita butuhkan adalah membuat kode yang akan memanfaatkan file tersebut.

public run(): void {
    // Empat baris ini yang bisa kita gunakan untuk memanggil file yang dihasilkan oleh ANTLR
    const charStream = new ANTLRInputStream(this.source);
    const lexer = new ArubickLexer(charStream);
    const tokenStream = new CommonTokenStream(lexer);
    const parser = new ArubickParser(tokenStream);

    RootSolver.workingDirectory = this.sourceDirectory;

    // Kita memanggil rule `program` di parser kita, parser akan mengembalikan struktur program yang ada.
    const program = parser.program();
    const metadata = {
      // Program terdiri dari bot definition, bot definition terdiri dari ID, dan version.
      name: program.botDefinition().ID().text,
      version: program
        .botDefinition()
        .NUMBER()
        .join('.')
    };

    // Anything outside main is only allowed for global storage definition
    // Variable is not allowed and will be ignored

    // Komen di atas dari source code.
    // Di dalam rule program, ada beberapa statement, secara program, hanya boleh ada definisi storage.
    const globalStorage = program.statement().filter(stmt => stmt.assign());
    const storageData = globalStorage.map(stmt => {
      const defStorage = stmt.assign().defStorage();
      if (!defStorage) {
        throw new Error('Only storage definition is allowed');
      }
      const key = defStorage.storage().ID(0).text;
      const subKey = defStorage.storage().ID(1).text;
      return {
        key,
        subKey,
        value: ExpressionSolver.solve(stmt.assign().expression())
      };
    });

    // Dari rule program, kita dapatkan rule `main`.
    const main = program.main();
    RootSolver.main = main;
    // The actual executable code is inside main
    const statements = main.statement();

    const mainScope = {};
    storageData.forEach(storage => {
      mainScope[storage.key + '/' + storage.subKey] = storage.value;
    });
    // A copy of the original state
    RootSolver.mainScope = { ...mainScope };

    while (statements.length !== 0) {
      const stmt = statements.shift();
      // Untuk setiap perintah di main, kita jalankan
      StatementSolver.solve(stmt, statements, mainScope);
    }
  }
Enter fullscreen mode Exit fullscreen mode

Inti dari penggunaan dari parser yang dibuat oleh ANTLR adalah, kita mengambil rule yang sudah kita buat. Lalu memprosesnya. Baik itu rule seperti expression, statement, ataupun variable. Dari rule kita dapatkan rule atau token lainnya yang terkait.

Ada satu hal yang menarik dari Arubick, yaitu async. Karena saya tidak pernah membuat interpreter sebelumnya dan tidak punya acuan referensi tertentu, akhirnya saya mengira-ngira dan mencoba-coba cara untuk implementasi async di Arubick.

if (ctx.AWAIT()) {
        // Kita copy semua yang ada di bawah await
        const copyChildren = children.concat();
        // This is required to prevent the core runner from continuing executing
        // Hapus dari runner utama
        children.splice(0, children.length);
        // Evaluate the method call (for now this is just reply())
        if (this.coreMethods.hasOwnProperty(variable.ID().text)) {
          return this.coreMethods[variable.ID().text].apply(this, [
            answer => {
              // Check whether parent statement want to assign the async call result
              // Kalau ternyata method yg di-await ini akan diberikan ke sebuah variable
              if (parent) {
                const assign = parent.assign();
                if (assign) {
                  const assignVar = assign.variable();
                  const asssignDefVariable = assign.defVariable();
                  const key = assignVar
                    ? assignVar.ID().text
                    : asssignDefVariable.variable().ID().text;
                    // Oper return value dari method ke variabel
                  scope[key] = answer;
                  // Further statement is executed within this scope simulating closure.
                  // Yang tadi dicopy dijalankan lagi, tapi setelah await selesai
                  if (copyChildren) {
                    while (copyChildren.length !== 0) {
                      const stmt = copyChildren.shift();
                      StatementSolver.solve(stmt, copyChildren, scope);
                    }
                  }
                }
              }
            }
          ]);
        }
      }
Enter fullscreen mode Exit fullscreen mode

Di Arubick, tidak ada API untuk pattern async seperti Promise di JavaScript. Sehingga harus dihandle di interpreternya langsung.

Kesimpulan

ANTLR mempermudah proses pembuatan lexer dan parser. Tanpa ANTLR, kita harus menulis sendiri file yang mirip dengan hasil generate dari ANTLR. Isi dari file tersebut repetitif, meskipun bisa diketik secara manual; jauh lebih cepat jika kita menggunakan tool seperti ANTLR untuk melakukan pekerjaan repetitif tersebut.

Sekian dan terima kasih, semoga bermanfaat!

Discussion

pic
Editor guide