Makro

Konstruksi dan inspeksi AST untuk menghasilkan kode pada waktu kompilasi.

Daftar isi

Makro adalah fungsi yang berjalan pada waktu kompilasi, menerima argumennya berupa pohon sintaks atau AST, dan mengembalikan AST baru yang menggantikan pemanggilannya sebelum program disusun. Kekuatan ini berada satu tingkat di bawah template, yaitu template menyusun ulang potongan kode yang sudah berbentuk pohon, sedangkan makro membangun pohonnya sendiri node demi node. Topik ini menginspeksi AST dengan dumpTree, membangun makro pertama dari node dasar, lalu merangkum seluruh alurnya dalam satu diagram.

Program sebagai pohon sintaks#

Sebelum menulis makro, bentuk data yang diolahnya perlu dilihat lebih dulu. Setiap ekspresi Nim tersusun atas node bernomor jenis seperti nnkInfix untuk operator di tengah dan nnkIntLit untuk bilangan, dan prosedur dumpTree dari modul macros mencetak pohon itu tanpa perlu menulis makro sedikit pun.

nim dump_ast.nim
# Menginspeksi AST sebuah ekspresi pada waktu kompilasi
import macros

dumpTree(a + b * 2)
bash
# tampilkan AST ekspresi pada waktu kompilasi
nim r -f --hints:off dump_ast.nim
text Keluaran
Infix
  Ident "+"
  Ident "a"
  Infix
    Ident "*"
    Ident "b"
    IntLit 2

Pohon tersebut menyatakan bahwa a + b * 2 adalah pemanggilan + dengan dua anak, yaitu pengenal a dan pemanggilan * yang membungkus b dan 2, dan struktur ini mencerminkan kaidah presedensi tanpa tanda kurung tambahan. Perhatikan pula bahwa cetak pohon muncul lebih dulu pada keluaran, karena semuanya terjadi saat kompilasi, dan program itu sendiri tidak mencetak apa pun. Opsi -f pada perintah kompilasi memaksa kompilasi ulang penuh, sehingga keluaran waktu kompilasi selalu muncul kembali walaupun cache sudah ada.

Membangun AST dengan newTree#

Makro ditulis seperti proc yang mengembalikan untyped, dan variabel result di dalamnya berisi AST yang dihasilkan. Konstruktor dasarnya adalah newTree yang membentuk node dari jenis dan anak-anaknya, misalnya nnkCall untuk pemanggilan fungsi. Makro pertama membentuk pemanggilan abs(x), dan yang kedua menyusun ekspresi majemuk (a + b) / 2 dengan menyarangkan dua nnkInfix.

nim makro_bangun.nim
# Makro yang membangun AST secara manual
import macros

macro nilaiAbsolut(x: untyped): untyped =
  # membentuk pemanggilan abs(x) sebagai pohon node
  result = nnkCall.newTree(ident("abs"), x)

macro rataRata(a, b: untyped): untyped =
  # membentuk ekspresi (a + b) / 2
  result = nnkInfix.newTree(
    ident("/"),
    nnkInfix.newTree(ident("+"), a, b),
    newLit(2)
  )

echo nilaiAbsolut(-7)
echo nilaiAbsolut(3 - 10)
echo rataRata(3, 8)
echo rataRata(10, 20)
bash
# kompilasi dan jalankan dua makro pembentuk AST
nim r -f --hints:off makro_bangun.nim
text Keluaran
7
7
5.5
15.0

Tiga alat pada program itu menutupi kebutuhan dasar konstruksi AST, yaitu ident membentuk node pengenal, newLit membungkus nilai biasa menjadi node literal, dan newTree menyusun node induk beserta anak-anaknya sekaligus. Pemilihan jenis node menirukan bentuk kode sasarannya, sehingga nnkCall untuk pemanggilan, nnkInfix untuk operator di tengah, dan nnkStmtList untuk kumpulan pernyataan, dan daftar lengkapnya berada di dokumentasi modul macros untuk ditelusuri ketika bentuk yang dibutuhkan bertambah rumit.

Batas antara makro dan template dapat dirumuskan dari perbedaan kerja keduanya. Template cocok ketika bentuk akhir kodenya sudah tetap dan cukup ditempel, sedangkan makro dibutuhkan begitu bentuk keluaran bergantung pada struktur masukan, misalnya mengubah deklarasi type menjadi serangkaian proc akses yang banyaknya mengikuti jumlah bidangnya, dan pengolahan seperti itu menuntut penelusuran pohon yang hanya dapat dilakukan makro. Argumen makro dipakai langsung sebagai anak, sehingga nilaiAbsolut(3 - 10) menyisipkan seluruh pohon 3 - 10 ke dalam pemanggilan abs, bukan hasilnya yang sudah dihitung, dan penghitungan baru terjadi pada program hasil kompilasi.

Alur makro dalam satu diagram#

Seluruh proses berlangsung di antara dua wajah kompilator yang sama, yaitu sumber diurai menjadi AST, makro menerima AST itu dan mengembalikan AST lain, lalu kompilator melanjutkan pekerjaannya pada hasil makro seolah-olah itulah yang ditulis.

waktu kompilasiKode sumberAST awalMakroAST hasilkompilasiProgramMakro menerima AST dan mengembalikan AST baru sebelum kompilasi berlangsung.
Makro menerima AST hasil penguraian dan mengembalikan AST baru sebelum kompilasi diteruskan

Setiap pemanggilan makro menambah biaya pada waktu kompilasi karena tubuhnya dieksekusi penafsir waktu kompilasi, dan biaya itu wajar dibayar untuk abstraksi yang menghemat ratusan baris kode yang harus ditulis tangan, tetapi tidak untuk pola yang bisa diselesaikan proc biasa. Kaidah praktisnya sederhana, yaitu gunakan proc selama bentuk kodenya tetap, pilih template ketika potongan kode perlu ditempel, dan gunakan makro hanya ketika bentuk kode keluaran bergantung pada struktur masukannya.

Dari alur itu tampak jelas mengapa kesalahan makro terasa berbeda dari kesalahan program biasa, yaitu kesalahan konstruksi AST meledak pada waktu kompilasi sebelum program sempat berjalan, sedangkan logika AST yang keliru diam-diam menghasilkan program yang sah tetapi salah maksud. Karena itu kebiasaan memeriksa hasil makro dengan dumpTree atau expandMacros sejak dini sangat membantu.

Kesalahan umum#

  • Menyisipkan nilai biasa sebagai anak node. Pemanggilan seperti nnkInfix.newTree(ident("+"), 3, 4) ditolak dengan Error: type mismatch pada baris newTree, karena anak newTree wajib bertipe NimNode, dan nilai bilangan harus dibungkus dengan newLit(3) terlebih dahulu.
  • Mengira makro dieksekusi saat program berjalan. Seluruh tubuh makro berjalan pada waktu kompilasi, sehingga echo di dalam makro mencetak saat kompilasi dan keadaan program tidak dapat diakses dari dalamnya, sedangkan yang boleh dikembalikan hanyalah AST.
  • Membaca keluaran waktu kompilasi dari kompilasi bertahap. Kompilator memakai cache, sehingga makro tidak dijalankan ulang pada kompilasi kedua tanpa perubahan, dan keluarannya hilang, karena itu blok yang menyajikan keluaran waktu kompilasi memakai opsi -f untuk memaksa kompilasi ulang.

Ringkasan#

  • Makro adalah fungsi waktu kompilasi yang menerima AST dan mengembalikan AST baru, dan hasilnya menggantikan pemanggilan sebelum program disusun.
  • dumpTree menginspeksi pohon sintaks tanpa perlu menulis makro, sedangkan ident, newLit, dan newTree menyusun pengenal, literal, dan node induk pada konstruksi manual.
  • Kesalahan konstruksi AST muncul sebagai error waktu kompilasi, dan keluaran waktu kompilasi hanya muncul kembali bila kompilasi dipaksa ulang dengan -f.

Referensi#