Error Semantik

Kualitas diagnostik semantik: pesan yang membantu pembaca memperbaiki program.

Daftar isi

Topik ini membahas kualitas diagnostik semantik, yaitu cara merender error dari pemeriksa tipe dan resolver menjadi pesan yang membantu pembaca memperbaiki program. Pembaca memperoleh class Renderer dan Diagnostic yang menyebut baris dan kolom, mengutip baris sumber, dan memberi tanda panah pada kolom yang tepat. Kontrak Diagnostic dirancang untuk menerima TypeErrorInfo dari topik pemeriksaan-tipe dan ResolutionError dari topik resolusi-scope, sehingga satu perender cukup untuk seluruh front end compiler.

Apa itu diagnostik yang baik#

Diagnostik yang baik adalah pesan yang membuat pembaca tahu tiga hal sekaligus: apa yang salah, di mana letaknya pada sumber, dan bagaimana cara memperbaikinya. Pesan “type error” tanpa konteks memaksa pembaca membaca seluruh berkas. Pesan “type error pada 1:11” sudah membantu, tetapi tetap perlu pembaca membuka baris itu. Pesan yang mengutip baris sumber dan menunjuk kolom dengan tanda panah membuat pembaca langsung melihat masalahnya. Tiga lapis informasi ini menjadi standar pada compiler produksi sejak Clang dan Rust mempopulerkannya.

Bentuk kanonik diagnostik pada topik ini terdiri atas lima baris. Baris pertama menyebut severitas (error atau warning) dan pesan singkat. Baris kedua menyebut lokasi line:column. Baris ketiga adalah baris gutter kosong yang menyelaraskan pipa. Baris keempat mengutip baris sumber dengan nomor baris pada gutter. Baris kelima menempatkan tanda ^ pada kolom yang tepat. Catatan tambahan bila perlu menyertakan penjelasan lebih lanjut, seperti tipe yang diharapkan atau saran perbaikan. Format ini bukan tiruan persis dari Rust atau Clang, melainkan adaptasi ringkas yang mempertahankan inti tiga lapis informasi.

Struktur Diagnostic dan Renderer#

Class Diagnostic menyimpan satu error yang akan dirender. Field line dan column menyebut posisi sumber, severity membedakan error dari warning, kind menyebut jenis error (mis. binop, undefined), message menyimpan pesan utama, dan notes menyimpan daftar catatan tambahan. Diagnostic bersifat umum, tidak terikat pada TypeErrorInfo atau ResolutionError, sehingga satu perender cukup untuk semua sumber error pada front end.

Class Renderer menerima kode sumber dan menyimpan baris-barisnya. Metode render(diag) mengembalikan string multi-baris yang sudah disusun sesuai bentuk kanonik di atas. Gutter memiliki lebar tetap (default 4 karakter), nomor baris dirata-kanan pada gutter, pipa | dipakai sebagai pemisah gutter dan konten, dan tanda ^ ditempatkan pada kolom yang sesuai dengan diag.column. Lebar gutter yang tetap menjaga agar pipa sejajar antar baris meski nomor baris mencapai tiga atau empat digit.

Konversi dari TypeErrorInfo dan ResolutionError#

Dua fungsi bantu mengubah error dari pemeriksa tipe dan resolver menjadi Diagnostic. Fungsi from_type_error menerima TypeErrorInfo dan menyusun catatan tambahan berdasarkan kind: binop menghasilkan catatan tentang operator dan tipe, unop menghasilkan catatan tentang operator unary, assign menghasilkan catatan tentang tipe variabel sasaran dan nilai, cond menghasilkan catatan tentang persyaratan bool, dan logical menghasilkan catatan tentang operand bool. Fungsi from_resolution_error menerima ResolutionError dan menambahkan catatan yang menyebut tindakan perbaikan, seperti “tambahkan deklarasi ’let’ untuk nama itu” untuk undefined, atau “pilih nama lain, atau bungkus deklarasi sebelumnya dalam blok” untuk redeclare.

Pemisahan produksi error (pemeriksa tipe dan resolver) dari rendering (Renderer) mengikuti pola yang dipakai Rust dan Clang. Kedua compiler menyimpan error sebagai Diagnostic selama pemeriksaan, lalu merendernya pada akhir kompilasi. Pola ini memungkinkan satu pemeriksaan menghasilkan banyak error, urutan error diatur terpisah, dan rendering dapat diubah (mis. warna terminal, bahasa pesan) tanpa menyentuh pemeriksa. Pada bahasa mini v1, dua fungsi bantu adalah padanan sederhana dari pola itu.

Perbandingan dengan compiler produksi#

Clang mempopulerkan format diagnostik dengan kutipan baris sumber dan tanda panah kolom sejak versi awal LLVM. Format itu kemudian diadopsi oleh Rust, yang menambahkan saran perbaikan otomatis (mis. “did you mean foo?”) dan tanda multi-baris untuk span yang lebih dari satu kolom. Kedua compiler menampilkan pesan yang ringkas tetapi padat: severitas, pesan, lokasi, baris sumber, tanda span, dan catatan, sering kali dengan kode warna pada terminal yang mendukung. Bahasa mini v1 pada topik ini tidak meniru persis, melainkan mempertahankan inti tiga lapis informasi (pesan, lokasi, kutipan dengan tanda) dan menyederhanakan sisanya.

Satu perbedaan penting adalah dukungan multi-baris. Clang dan Rust menampilkan span yang melintasi beberapa baris dengan tepi kiri dan kanan, sehingga error pada blok yang panjang tetap terlihat jelas. Renderer pada topik ini hanya menangani span satu kolom, karena error semantik pada bahasa mini v1 (binop, undefined, redeclare) cukup ditunjuk dengan satu kolom. Bila kelak fitur bahasa menuntut span multi-baris (mis. pesan tentang blok yang tidak terjangkau), renderer dapat diperluas dengan menambahkan field end_line dan end_column pada Diagnostic, tanpa mengubah TypeErrorInfo atau ResolutionError.

Berkas diagnostics.py#

Berkas berikut berisi class Renderer, class Diagnostic, dan dua fungsi bantu from_type_error serta from_resolution_error. Demo main() merender empat kasus error berbeda: error tipe biner pada 1 + "a", nama tak terdefinisi pada print x, redeclarasi pada let y = 0; let y = 1, serta error tipe biner pada b + 1 dengan b bertipe bool. Untuk berdiri sendiri, demo menyertakan salinan ringkas TypeErrorInfo dan ResolutionError sebagai dataclass lokal, dengan field yang persis sama dengan kontrak pada topik pemeriksaan-tipe dan resolusi-scope.

python diagnostics.py
"""Diagnostik semantik untuk bahasa mini v1.

Antarmuka publik:
    Renderer(source).render(diag) -> str

Mengubah error dari pemeriksa tipe dan resolver menjadi pesan yang
menyebut baris/kolom, mengutip baris sumber, dan memberi tanda panah
pada kolom yang tepat. Bentuknya mengikuti konvensi compiler produksi
yang baik (Rust, Clang) tanpa meniru persis.
"""
from __future__ import annotations
from dataclasses import dataclass, field


@dataclass
class Diagnostic:
    """Bentuk umum error yang akan dirender."""
    line: int
    column: int
    severity: str              # "error" atau "warning"
    kind: str                  # mis. "binop", "undefined", "assign"
    message: str
    notes: list[str] = field(default_factory=list)


class Renderer:
    """Render Diagnostic menjadi pesan multi-baris dengan penunjuk kolom."""

    def __init__(self, source: str, gutter_width: int = 4) -> None:
        self.lines = source.split("\n")
        self.gutter_width = gutter_width

    def render(self, diag: Diagnostic) -> str:
        out: list[str] = []
        out.append(f"{diag.severity}: {diag.message}")
        out.append(f"  --> {diag.line}:{diag.column}")
        # Gutter kosong dengan pipa sejajar dengan pipa baris sumber.
        out.append(f"{' ' * self.gutter_width} |")
        # Baris sumber dengan nomor baris pada gutter.
        if 0 <= diag.line - 1 < len(self.lines):
            src = self.lines[diag.line - 1]
        else:
            src = "<baris tidak ada>"
        out.append(f"{diag.line:>{self.gutter_width}} | {src}")
        # Tanda panah pada kolom yang tepat.
        col = max(1, diag.column)
        pointer = f"{' ' * self.gutter_width} | " + " " * (col - 1) + "^"
        out.append(pointer)
        for note in diag.notes:
            out.append(f"  = catatan: {note}")
        return "\n".join(out)


# ----- Konversi dari TypeErrorInfo / ResolutionError ke Diagnostic -----
def from_type_error(te) -> Diagnostic:
    return Diagnostic(
        line=te.line, column=te.column,
        severity="error", kind=te.kind,
        message=te.message,
        notes=_notes_for(te))


def from_resolution_error(re_) -> Diagnostic:
    note: str | None = None
    if re_.kind == "use-before-def":
        note = "deklarasikan nama sebelum memakainya, atau pindah ke modus longgar."
    elif re_.kind == "undefined":
        note = "tambahkan deklarasi 'let' untuk nama itu sebelum memakainya."
    elif re_.kind == "assign-undefined":
        note = "penetapan memerlukan variabel yang sudah dideklarasikan."
    elif re_.kind == "redeclare":
        note = "pilih nama lain, atau bungkus deklarasi sebelumnya dalam blok."
    notes = [note] if note else []
    return Diagnostic(
        line=re_.line, column=re_.column,
        severity="error", kind=re_.kind,
        message=re_.message,
        notes=notes)


def _notes_for(te) -> list[str]:
    if te.kind == "binop":
        return [f"operator {te.op!r} mengharapkan {te.expected}; ditemukan {te.got}."]
    if te.kind == "unop":
        return [f"operator {te.op!r} mengharapkan {te.expected}; ditemukan {te.got}."]
    if te.kind == "assign":
        return [f"variabel sasaran bertipe {te.expected}; nilai bertipe {te.got}."]
    if te.kind == "cond":
        return ["kondisi percabangan dan perulangan harus berupa bool."]
    if te.kind == "logical":
        return [f"operator {te.op!r} mengharapkan bool; ditemukan {te.got}."]
    return []


def main() -> None:
    # Untuk berdiri-sendiri, definisi ringkas TypeErrorInfo dan
    # ResolutionError ada di sini sebagai dataclass lokal. Pada pipeline
    # utama, keduanya berasal dari topik pemeriksaan-tipe dan resolusi-scope.
    @dataclass
    class TE:
        line: int
        column: int
        kind: str
        op: str
        expected: str
        got: str
        message: str

    @dataclass
    class RE:
        line: int
        column: int
        name: str
        kind: str
        message: str

    cases = [
        ("let a = 1 + \"a\"\n", [
            TE(1, 11, "binop", "+", "int/float", "int, string",
               "operator '+' tidak kompatibel dengan tipe int dan string"),
        ]),
        ("print x\n", [
            RE(1, 7, "x", "undefined", "nama 'x' tidak terdefinisi"),
        ]),
        ("let y = 0\nlet y = 1\nprint y\n", [
            RE(2, 5, "y", "redeclare", "nama 'y' sudah dideklarasikan pada 1:5"),
        ]),
        ("let b = true\nlet s = b + 1\n", [
            TE(2, 11, "binop", "+", "int/float", "bool, int",
               "operator '+' tidak kompatibel dengan tipe bool dan int"),
        ]),
    ]

    for source, errs in cases:
        print("=== Sumber ===")
        print(source, end="")
        print("=== Diagnostik ===")
        r = Renderer(source)
        for err in errs:
            if isinstance(err, TE):
                diag = from_type_error(err)
            else:
                diag = from_resolution_error(err)
            print(r.render(diag))
            print()


if __name__ == "__main__":
    main()

Keluaran berikut diperoleh dengan menjalankan python3 diagnostics.py. Setiap diagnostik memiliki lima baris inti (severitas, lokasi, gutter kosong, baris sumber, tanda panah) dan diakhiri catatan tambahan. Tanda ^ pada setiap kasus tepat berada di bawah kolom yang disebut pada lokasi, sehingga pembaca langsung melihat operator atau identifier yang bermasalah.

text Keluaran
=== Sumber ===
let a = 1 + "a"
=== Diagnostik ===
error: operator '+' tidak kompatibel dengan tipe int dan string
  --> 1:11
     |
   1 | let a = 1 + "a"
     |           ^
  = catatan: operator '+' mengharapkan int/float; ditemukan int, string.

=== Sumber ===
print x
=== Diagnostik ===
error: nama 'x' tidak terdefinisi
  --> 1:7
     |
   1 | print x
     |       ^
  = catatan: tambahkan deklarasi 'let' untuk nama itu sebelum memakainya.

=== Sumber ===
let y = 0
let y = 1
print y
=== Diagnostik ===
error: nama 'y' sudah dideklarasikan pada 1:5
  --> 2:5
     |
   2 | let y = 1
     |     ^
  = catatan: pilih nama lain, atau bungkus deklarasi sebelumnya dalam blok.

=== Sumber ===
let b = true
let s = b + 1
=== Diagnostik ===
error: operator '+' tidak kompatibel dengan tipe bool dan int
  --> 2:11
     |
   2 | let s = b + 1
     |           ^
  = catatan: operator '+' mengharapkan int/float; ditemukan bool, int.

Ringkasan#

  • Diagnostik yang baik memuat tiga lapis informasi: pesan singkat, lokasi line:column, dan kutipan baris sumber dengan tanda panah pada kolom yang tepat.
  • Diagnostic menyimpan satu error dengan severitas, kind, pesan, dan catatan tambahan. Renderer mengubahnya menjadi string multi-baris dengan gutter dan pipa yang sejajar.
  • Dua fungsi bantu from_type_error dan from_resolution_error mengubah error dari pemeriksa tipe dan resolver menjadi Diagnostic, sehingga satu perender cukup untuk seluruh front end.
  • Pola pemisahan produksi error dari rendering mengikuti compiler produksi seperti Clang dan Rust, yang menyimpan error selama pemeriksaan dan merendernya pada akhir kompilasi.

Referensi#