Pemeriksaan Tipe

Pemeriksaan tipe statis, inference ringan, dan error tipe sebagai data terstruktur.

Daftar isi

Topik ini membahas pemeriksaan tipe statis untuk bahasa mini v1, yaitu pemeriksaan yang mengetahui tipe tiap ekspresi tanpa menjalankan program. Pembaca memperoleh class TypeChecker yang berjalan di atas AST dari topik desain-ast dan SymbolTable dari topik tabel-simbol, melakukan inference ringan untuk Literal, Binary, Unary, dan Assign, serta menghasilkan list[TypeErrorInfo] sebagai data terstruktur. Antarmuka TypeErrorInfo dipakai ulang oleh topik error-semantik untuk merender diagnostik yang membantu.

Apa itu pemeriksaan tipe statis#

Pemeriksaan tipe statis adalah pemeriksaan yang dilakukan sebelum program dijalankan, dengan menelusuri AST dan menghitung tipe tiap ekspresi. Bila sebuah operator menerima operand dengan tipe yang tidak cocok, pemeriksaan melaporkan error tanpa peduli nilai operand saat runtime. Sebagai contoh, 1 + "a" ditolak karena + mengharapkan int atau float (atau string pada kedua sisi), dan string di salah satu sisi melanggar aturan itu. Kebalikannya adalah pemeriksaan dinamis yang baru mengetahui ketidakcocokan saat baris itu dieksekusi.

Pemeriksaan statis berdiri di atas resolusi scope. Pemeriksa tipe menggunakan SymbolTable.resolve untuk mengetahui tipe variabel dari field type_hint pada Symbol. Bila nama tidak terdefinisi, resolver sudah melaporkannya pada tahap sebelumnya, tetapi pemeriksa tipe tetap menjalankan pemeriksaan sendiri agar tetap mandiri bila dipakai tanpa resolver. Pada pipeline utama, kedua tahap dijalankan berurutan, dan pemeriksa tipe hanya melaporkan error tipe, bukan error resolusi.

Tipe dan inference#

Bahasa mini v1 mengenal lima tipe dasar, ditambah satu tipe sentinel untuk inferensi yang gagal. Tipe int, float, string, bool, dan nil dipakai untuk literal dan hasil inferensi, sedangkan unknown dipakai bila pemeriksaan tidak dapat menentukan tipe (misalnya operand variabel yang tidak terdefinisi, atau operand dari ekspresi yang sudah ber-error). Penggunaan unknown membuat pemeriksaan dapat berlanjut: error tidak menyebar ke seluruh pohon, hanya ke cabang yang gagal.

Inference ringan dilakukan dengan aturan sintaks-tetap. Pada Literal, tipe ditentukan oleh tipe Python nilai (int, float, str, bool, None). Pada Binary, tipe ditentukan oleh operator dan tipe kedua operand. Pada Unary, tipe ditentukan oleh operator dan tipe operand. Pada Assign, tipe kembali adalah tipe variabel sasaran, dan tipe nilai harus kompatibel. Pada Grouping, tipe adalah tipe ekspresi di dalam kurung. Aturan-aturan ini ditulis sebagai tabel keputusan, bukan algoritma kompleks, sehingga pembaca dapat memverifikasi tiap cabang.

Aturan biner dan unary#

Operator biner dibagi menjadi tiga kelompok. Aritmetika (+, -, *, /) mengharapkan kedua operand numerik (int atau float), dengan promosi int ke float bila salah satu operand adalah float. Operator + juga menerima string + string untuk concatenation. Bila aturan tidak terpenuhi, error binop dilaporkan dengan operator, tipe kiri, dan tipe kanan. Pembandingan (<, <=, >, >=) mengharapkan kedua operand numerik, atau keduanya string (untuk urutan leksikografis), dan menghasilkan bool. Kesetaraan (==, !=) mengharapkan kedua operand bertipe sama (atau keduanya numerik untuk perbandingan int-float), dan menghasilkan bool.

Operator unary mengikuti aturan yang lebih sederhana. Negasi ! mengharapkan bool dan menghasilkan bool. Negasi numerik - mengharapkan int atau float dan menghasilkan tipe yang sama. Operator logika and dan or (di node Logical, bukan Binary) mengharapkan kedua operand bool dan menghasilkan bool. Bila aturan tidak terpenuhi, error unop atau logical dilaporkan. Aturan cond dilaporkan bila kondisi if atau while bukan bool.

Let dan Assign#

Pernyataan Let menyatakan tipe variabel dari tipe initializer. Karena bahasa mini v1 tidak memiliki anotasi tipe eksplisit, tipe let x = e adalah tipe e setelah inferensi. Tipe itu disimpan pada field type_hint dari Symbol di tabel simbol, sehingga pemeriksaan ekspresi berikutnya yang menggunakan x dapat melihat tipe tersebut lewat resolve.

Penetapan Assign(name, value) memeriksa dua hal. Pertama, name harus terdefinisi pada scope yang terlihat. Bila tidak, error assign-undefined dilaporkan. Kedua, tipe value harus kompatibel dengan type_hint dari name. Kompatibel berarti tipe sama, atau int ke float (promosi). Penetapan float = int diperbolehkan, sedangkan int = float ditolak agar tidak kehilangan presisi tanpa anotasi eksplisit. Bila tipe tidak kompatibel, error assign dilaporkan.

Error tipe sebagai data#

Error tipe dimodelkan sebagai TypeErrorInfo (dataclass), bukan sebagai Exception yang dilempar. Pemisahan ini memungkinkan pemeriksa tipe melaporkan banyak error dalam satu kali jalan, dan memungkinkan topik error-semantik merender tiap error dengan format yang konsisten. Field kind menyebut jenis error (binop, unop, logical, assign, cond, undefined), op menyebut operator terkait, expected dan got menyimpan tipe yang diharapkan dan ditemukan, dan message menyimpan pesan yang dapat ditampilkan. Posisi sumber line dan column dilampirkan dari atribut sampingan pada AST, sebagaimana topik resolusi-scope.

Pendekatan “error sebagai data” lazim dipakai compiler produksi. Rust menyebutnya Diagnostic, Clang menyebutnya DiagnosticEntry. Keduanya memisahkan produksi error dari rendering, sehingga satu pemeriksaan dapat menghasilkan banyak error dan rendering dapat diatur terpisah (misalnya, dengan atau tanpa kode warna, bahasa yang berbeda untuk pesan). Pada bahasa mini v1, TypeErrorInfo adalah padanan sederhana dari pola itu.

Berkas type_checker.py#

Berkas berikut berisi class TypeChecker plus salinan ringkas Token, Lexer, AST, Parser, dan SymbolTable agar berdiri sendiri. Demo main() menjalankan pemeriksaan pada program yang memuat dua error tipe (1 + "a" dan true + 1) dan dua deklarasi yang sehat. Setelah pemeriksaan, tabel simbol ditampilkan untuk memperlihatkan tipe yang diinferensi: a dan b menjadi unknown karena initializer-nya ber-error, sedangkan c dan d menjadi int.

python type_checker.py
"""Pemeriksa tipe statis untuk bahasa mini v1.

Antarmuka publik:
    TypeChecker().check(program) -> list[TypeErrorInfo]

Tipe yang dikenali: "int", "float", "string", "bool", "nil", "unknown".
Token, Lexer, AST, dan Parser disertakan ulang dari modul 1 dan 2 agar
berkas berdiri sendiri. SymbolTable dari topik tabel-simbol dipakai
untuk menyimpan tipe tiap variabel pada field type_hint.

Inference ringan dilakukan pada Literal, Binary, Unary, Logical,
Grouping, Variable, Assign, dan Let. Bila terdeteksi ketidakcocokan
tipe, error dimasukkan sebagai data terstruktur TypeErrorInfo, dan
inferensi kembali ke tipe "unknown" agar pemeriksaan dapat berlanjut.
"""
from __future__ import annotations
from collections import namedtuple
from dataclasses import dataclass, field
from typing import Any


# ----- Token (salinan ringkas dari modul 1) -----
Token = namedtuple("Token", ["type", "lexeme", "line", "column"])

KEYWORDS = frozenset({
    "let", "print", "if", "else", "while",
    "true", "false", "nil", "and", "or",
})


# ----- Lexer ringkas (salinan dari modul 1) -----
DUA_KARAKTER = {"<=", ">=", "==", "!="}
SATU_KARAKTER = set("+-*/<>!=")
PUNCT = set("(){}")


class Lexer:
    def __init__(self, source: str):
        self.src = source
        self.pos = 0
        self.line = 1
        self.column = 1

    def _peek(self, offset: int = 0) -> str:
        idx = self.pos + offset
        if idx >= len(self.src):
            return ""
        return self.src[idx]

    def _advance(self) -> str:
        ch = self.src[self.pos]
        self.pos += 1
        if ch == "\n":
            self.line += 1
            self.column = 1
        else:
            self.column += 1
        return ch

    def _at_end(self) -> bool:
        return self.pos >= len(self.src)

    def tokens(self) -> list[Token]:
        out: list[Token] = []
        while not self._at_end():
            ch = self._peek()
            if ch in " \t\r\n":
                self._advance()
                continue
            if ch == "/" and self._peek(1) == "/":
                while not self._at_end() and self._peek() != "\n":
                    self._advance()
                continue
            if ch.isalpha() or ch == "_":
                out.append(self._identifier())
                continue
            if ch.isdigit():
                out.append(self._angka())
                continue
            if ch == '"':
                out.append(self._string())
                continue
            if ch in PUNCT:
                line, col = self.line, self.column
                self._advance()
                out.append(Token("PUNCT", ch, line, col))
                continue
            if ch in SATU_KARAKTER:
                out.append(self._operator())
                continue
            self._advance()
        out.append(Token("EOF", "", self.line, self.column))
        return out

    def _identifier(self) -> Token:
        line, col = self.line, self.column
        start = self.pos
        while not self._at_end() and (self._peek().isalnum() or self._peek() == "_"):
            self._advance()
        lex = self.src[start:self.pos]
        kelas = "KEYWORD" if lex in KEYWORDS else "IDENTIFIER"
        return Token(kelas, lex, line, col)

    def _angka(self) -> Token:
        line, col = self.line, self.column
        start = self.pos
        while not self._at_end() and self._peek().isdigit():
            self._advance()
        if self._peek() == "." and self._peek(1).isdigit():
            self._advance()
            while not self._at_end() and self._peek().isdigit():
                self._advance()
        return Token("NUMBER", self.src[start:self.pos], line, col)

    def _string(self) -> Token:
        line, col = self.line, self.column
        self._advance()
        start = self.pos
        while not self._at_end() and self._peek() != '"':
            self._advance()
        lex = '"' + self.src[start:self.pos] + '"'
        if not self._at_end():
            self._advance()
        return Token("STRING", lex, line, col)

    def _operator(self) -> Token:
        line, col = self.line, self.column
        dua = self._peek() + self._peek(1)
        if dua in DUA_KARAKTER:
            self._advance()
            self._advance()
            return Token("OPERATOR", dua, line, col)
        ch = self._advance()
        return Token("OPERATOR", ch, line, col)


# ----- AST (salinan ringkas dari topik desain-ast) -----
@dataclass
class Expr:
    pass


@dataclass
class Binary(Expr):
    left: Expr
    op: str
    right: Expr


@dataclass
class Unary(Expr):
    op: str
    operand: Expr


@dataclass
class Literal(Expr):
    value: Any


@dataclass
class Variable(Expr):
    name: str


@dataclass
class Assign(Expr):
    name: str
    value: Expr


@dataclass
class Logical(Expr):
    left: Expr
    op: str
    right: Expr


@dataclass
class Grouping(Expr):
    expression: Expr


@dataclass
class Stmt:
    pass


@dataclass
class Let(Stmt):
    name: str
    initializer: Expr


@dataclass
class Print(Stmt):
    expression: Expr


@dataclass
class If(Stmt):
    condition: Expr
    then_branch: Stmt
    else_branch: Stmt | None = None


@dataclass
class While(Stmt):
    condition: Expr
    body: Stmt


@dataclass
class Block(Stmt):
    statements: list[Stmt] = field(default_factory=list)


@dataclass
class Expression(Stmt):
    expression: Expr


@dataclass
class Program:
    statements: list[Stmt] = field(default_factory=list)


# ----- Parser (ringkas, dengan penanda posisi) -----
class ParseError(Exception):
    pass


def _tag(node, tok):
    node._line = tok.line
    node._column = tok.column
    return node


class Parser:
    def __init__(self, tokens: list[Token]):
        self.tokens = tokens
        self.pos = 0

    def parse(self) -> Program:
        statements: list[Stmt] = []
        while not self._is_at_end():
            statements.append(self.statement())
        return Program(statements)

    def _peek(self) -> Token:
        return self.tokens[self.pos]

    def _previous(self) -> Token:
        return self.tokens[self.pos - 1]

    def _is_at_end(self) -> bool:
        return self._peek().type == "EOF"

    def _advance(self) -> Token:
        if not self._is_at_end():
            self.pos += 1
        return self._previous()

    def _check(self, type_, lexeme=None) -> bool:
        tok = self._peek()
        if tok.type != type_:
            return False
        return lexeme is None or tok.lexeme == lexeme

    def _match(self, type_, lexeme=None) -> bool:
        if self._check(type_, lexeme):
            self._advance()
            return True
        return False

    def _match_any(self, type_, lexemes) -> bool:
        tok = self._peek()
        if tok.type == type_ and tok.lexeme in lexemes:
            self._advance()
            return True
        return False

    def _consume(self, type_, lexeme, message) -> Token:
        if self._check(type_, lexeme):
            return self._advance()
        raise self._error(self._peek(), message)

    def _error(self, tok, message) -> ParseError:
        return ParseError(f"pada {tok.line}:{tok.column}: {message}")

    def statement(self) -> Stmt:
        if self._check("KEYWORD", "let"):
            return self.let_statement()
        if self._check("KEYWORD", "print"):
            return self.print_statement()
        if self._check("KEYWORD", "if"):
            return self.if_statement()
        if self._check("KEYWORD", "while"):
            return self.while_statement()
        if self._check("PUNCT", "{"):
            return self.block()
        return self.expression_statement()

    def let_statement(self) -> Let:
        self._advance()
        name_tok = self._consume("IDENTIFIER", None,
                                 "identifier diperlukan setelah 'let'")
        self._consume("OPERATOR", "=",
                      "'=' diperlukan setelah identifier pada pernyataan let")
        value = self.expression()
        return _tag(Let(name_tok.lexeme, value), name_tok)

    def print_statement(self) -> Print:
        kw = self._advance()
        value = self.expression()
        node = Print(value)
        node._line = kw.line
        node._column = kw.column
        return node

    def if_statement(self) -> If:
        self._advance()
        cond = self.expression()
        then_branch = self.block()
        else_branch = None
        if self._match("KEYWORD", "else"):
            else_branch = self.block()
        return If(cond, then_branch, else_branch)

    def while_statement(self) -> While:
        self._advance()
        cond = self.expression()
        body = self.block()
        return While(cond, body)

    def block(self) -> Block:
        self._consume("PUNCT", "{", "'{' diperlukan untuk memulai blok")
        statements: list[Stmt] = []
        while not self._check("PUNCT", "}") and not self._is_at_end():
            statements.append(self.statement())
        self._consume("PUNCT", "}", "'}' diperlukan untuk menutup blok")
        return Block(statements)

    def expression_statement(self) -> Expression:
        expr = self.expression()
        return Expression(expr)

    def expression(self) -> Expr:
        return self.assignment()

    def assignment(self) -> Expr:
        expr = self.logic_or()
        if self._match("OPERATOR", "="):
            value = self.assignment()
            if isinstance(expr, Variable):
                return _tag(Assign(expr.name, value), expr)
            raise self._error(self._previous(), "target penetapan tidak valid")
        return expr

    def logic_or(self) -> Expr:
        expr = self.logic_and()
        while self._match("KEYWORD", "or"):
            op_tok = self._previous()
            right = self.logic_and()
            expr = _tag(Logical(expr, "or", right), op_tok)
        return expr

    def logic_and(self) -> Expr:
        expr = self.equality()
        while self._match("KEYWORD", "and"):
            op_tok = self._previous()
            right = self.equality()
            expr = _tag(Logical(expr, "and", right), op_tok)
        return expr

    def equality(self) -> Expr:
        expr = self.comparison()
        while self._match_any("OPERATOR", {"==", "!="}):
            op_tok = self._previous()
            right = self.comparison()
            expr = _tag(Binary(expr, op_tok.lexeme, right), op_tok)
        return expr

    def comparison(self) -> Expr:
        expr = self.term()
        while self._match_any("OPERATOR", {"<", "<=", ">", ">="}):
            op_tok = self._previous()
            right = self.term()
            expr = _tag(Binary(expr, op_tok.lexeme, right), op_tok)
        return expr

    def term(self) -> Expr:
        expr = self.factor()
        while self._match_any("OPERATOR", {"+", "-"}):
            op_tok = self._previous()
            right = self.factor()
            expr = _tag(Binary(expr, op_tok.lexeme, right), op_tok)
        return expr

    def factor(self) -> Expr:
        expr = self.unary()
        while self._match_any("OPERATOR", {"*", "/"}):
            op_tok = self._previous()
            right = self.unary()
            expr = _tag(Binary(expr, op_tok.lexeme, right), op_tok)
        return expr

    def unary(self) -> Expr:
        if self._match_any("OPERATOR", {"-", "!"}):
            op_tok = self._previous()
            operand = self.unary()
            return _tag(Unary(op_tok.lexeme, operand), op_tok)
        return self.primary()

    def primary(self) -> Expr:
        if self._match("KEYWORD", "true"):
            return Literal(True)
        if self._match("KEYWORD", "false"):
            return Literal(False)
        if self._match("KEYWORD", "nil"):
            return Literal(None)
        if self._match("NUMBER", None):
            tok = self._previous()
            if "." in tok.lexeme:
                return Literal(float(tok.lexeme))
            return Literal(int(tok.lexeme))
        if self._match("STRING", None):
            tok = self._previous()
            return Literal(tok.lexeme[1:-1])
        if self._match("IDENTIFIER", None):
            tok = self._previous()
            return _tag(Variable(tok.lexeme), tok)
        if self._match("PUNCT", "("):
            inner = self.expression()
            self._consume("PUNCT", ")", "')' diperlukan setelah ekspresi")
            return Grouping(inner)
        raise self._error(self._peek(), "ekspresi diperlukan")


# ----- Symbol (salinan ringkas dari topik tabel-simbol) -----
@dataclass
class Symbol:
    name: str
    kind: str
    declared_line: int
    declared_column: int
    type_hint: str | None = None


class SemanticError(Exception):
    pass


class Scope:
    def __init__(self, parent: "Scope | None" = None) -> None:
        self.parent = parent
        self.symbols: dict[str, Symbol] = {}


class SymbolTable:
    def __init__(self) -> None:
        self.root = Scope()
        self.current = self.root
        self.depth = 0

    def push(self) -> Scope:
        self.current = Scope(parent=self.current)
        self.depth += 1
        return self.current

    def pop(self) -> None:
        if self.current.parent is None:
            raise SemanticError("tidak dapat menutup scope global")
        self.current = self.current.parent
        self.depth -= 1

    def define(self, name: str, symbol: Symbol) -> Symbol:
        if name in self.current.symbols:
            existing = self.current.symbols[name]
            raise SemanticError(
                f"nama {name!r} sudah dideklarasikan pada "
                f"{existing.declared_line}:{existing.declared_column}"
            )
        self.current.symbols[name] = symbol
        return symbol

    def resolve(self, name: str) -> Symbol | None:
        scope: Scope | None = self.current
        while scope is not None:
            if name in scope.symbols:
                return scope.symbols[name]
            scope = scope.parent
        return None


# ----- Pemeriksa tipe -----
@dataclass
class TypeErrorInfo:
    """Error tipe sebagai data terstruktur untuk diagnostik."""
    line: int
    column: int
    kind: str               # mis. "binop", "unop", "assign", "cond", "logical"
    op: str                 # operator yang dipakai; "" untuk non-operator
    expected: str           # tipe yang diharapkan atau rangkaian diperbolehkan
    got: str                # tipe yang ditemukan
    message: str


NUMERIC = {"int", "float"}


def _promote(a: str, b: str) -> str | None:
    """Kombinasikan dua tipe numerik dengan promosi int ke float."""
    if a == "int" and b == "int":
        return "int"
    if a in NUMERIC and b in NUMERIC:
        return "float"
    return None


class TypeChecker:
    def __init__(self) -> None:
        self.symbols = SymbolTable()
        self.errors: list[TypeErrorInfo] = []

    def check(self, program: Program) -> list[TypeErrorInfo]:
        self.symbols = SymbolTable()
        self.errors = []
        for s in program.statements:
            self._stmt(s)
        return self.errors

    # -- pernyataan --
    def _stmt(self, s: Stmt) -> None:
        if isinstance(s, Let):
            t = self._expr(s.initializer)
            line = getattr(s, "_line", 0)
            col = getattr(s, "_column", 0)
            try:
                self.symbols.define(s.name, Symbol(
                    s.name, "var", line, col, type_hint=t))
            except SemanticError:
                # Redeclarasi ditangani resolver; type checker mengabaikan.
                pass
        elif isinstance(s, Print):
            self._expr(s.expression)
        elif isinstance(s, Expression):
            self._expr(s.expression)
        elif isinstance(s, If):
            cond_t = self._expr(s.condition)
            if cond_t not in ("bool", "unknown"):
                self.errors.append(TypeErrorInfo(
                    line=getattr(s.condition, "_line", 0),
                    column=getattr(s.condition, "_column", 0),
                    kind="cond", op="",
                    expected="bool", got=cond_t,
                    message=f"kondisi if harus bool, ditemukan {cond_t}"))
            self._stmt_block(s.then_branch)
            if s.else_branch is not None:
                self._stmt_block(s.else_branch)
        elif isinstance(s, While):
            cond_t = self._expr(s.condition)
            if cond_t not in ("bool", "unknown"):
                self.errors.append(TypeErrorInfo(
                    line=getattr(s.condition, "_line", 0),
                    column=getattr(s.condition, "_column", 0),
                    kind="cond", op="",
                    expected="bool", got=cond_t,
                    message=f"kondisi while harus bool, ditemukan {cond_t}"))
            self._stmt_block(s.body)
        elif isinstance(s, Block):
            self.symbols.push()
            for x in s.statements:
                self._stmt(x)
            self.symbols.pop()

    def _stmt_block(self, s: Stmt) -> None:
        if isinstance(s, Block):
            self.symbols.push()
            for x in s.statements:
                self._stmt(x)
            self.symbols.pop()
        else:
            self._stmt(s)

    # -- ekspresi: kembalikan tipe hasil (atau "unknown") --
    def _expr(self, e: Expr) -> str:
        if isinstance(e, Literal):
            v = e.value
            if isinstance(v, bool):
                return "bool"
            if isinstance(v, int):
                return "int"
            if isinstance(v, float):
                return "float"
            if isinstance(v, str):
                return "string"
            if v is None:
                return "nil"
            return "unknown"
        if isinstance(e, Grouping):
            return self._expr(e.expression)
        if isinstance(e, Variable):
            sym = self.symbols.resolve(e.name)
            if sym is None:
                self.errors.append(TypeErrorInfo(
                    line=getattr(e, "_line", 0),
                    column=getattr(e, "_column", 0),
                    kind="undefined", op="",
                    expected="terdefinisi", got="tidak-terdefinisi",
                    message=f"nama {e.name!r} tidak terdefinisi"))
                return "unknown"
            return sym.type_hint or "unknown"
        if isinstance(e, Assign):
            value_t = self._expr(e.value)
            sym = self.symbols.resolve(e.name)
            if sym is None:
                self.errors.append(TypeErrorInfo(
                    line=getattr(e, "_line", 0),
                    column=getattr(e, "_column", 0),
                    kind="assign-undefined", op="=",
                    expected="terdefinisi", got="tidak-terdefinisi",
                    message=f"penetapan ke nama {e.name!r} yang tidak terdefinisi"))
                return "unknown"
            decl_t = sym.type_hint or "unknown"
            if decl_t != "unknown" and value_t != "unknown":
                if not self._assignable(decl_t, value_t):
                    self.errors.append(TypeErrorInfo(
                        line=getattr(e, "_line", 0),
                        column=getattr(e, "_column", 0),
                        kind="assign", op="=",
                        expected=decl_t, got=value_t,
                        message=(f"penetapan ke {e.name!r} bertipe {decl_t} "
                                 f"dengan nilai bertipe {value_t}")))
            return decl_t
        if isinstance(e, Binary):
            return self._binop(e)
        if isinstance(e, Logical):
            lt = self._expr(e.left)
            rt = self._expr(e.right)
            if lt not in ("bool", "unknown") or rt not in ("bool", "unknown"):
                self.errors.append(TypeErrorInfo(
                    line=getattr(e, "_line", 0),
                    column=getattr(e, "_column", 0),
                    kind="logical", op=e.op,
                    expected="bool", got=f"{lt}, {rt}",
                    message=(f"operator {e.op!r} memerlukan bool, "
                             f"ditemukan {lt} dan {rt}")))
                return "unknown"
            return "bool"
        if isinstance(e, Unary):
            return self._unop(e)
        return "unknown"

    def _binop(self, e: Binary) -> str:
        lt = self._expr(e.left)
        rt = self._expr(e.right)
        line = getattr(e, "_line", 0)
        col = getattr(e, "_column", 0)
        op = e.op
        if op in ("+", "-", "*", "/"):
            # Plus memperbolehkan string+string (concatenation).
            if op == "+" and lt == "string" and rt == "string":
                return "string"
            t = _promote(lt, rt)
            if t is not None:
                return t
            self._type_error_binop(line, col, op, lt, rt,
                                   "int/float (atau string+string untuk +)")
            return "unknown"
        if op in ("<", "<=", ">", ">="):
            if lt in NUMERIC and rt in NUMERIC:
                return "bool"
            if lt == "string" and rt == "string":
                return "bool"
            self._type_error_binop(line, col, op, lt, rt,
                                   "int/float, atau string untuk string<string")
            return "unknown"
        if op in ("==", "!="):
            if (lt == "unknown" or rt == "unknown") or (
                    self._comparable(lt, rt)):
                return "bool"
            self._type_error_binop(line, col, op, lt, rt,
                                   "tipe yang sama (int, float, string, bool, nil)")
            return "unknown"
        self._type_error_binop(line, col, op, lt, rt, "tipe terdefinisi")
        return "unknown"

    def _unop(self, e: Unary) -> str:
        t = self._expr(e.operand)
        line = getattr(e, "_line", 0)
        col = getattr(e, "_column", 0)
        if e.op == "!":
            if t in ("bool", "unknown"):
                return "bool"
            self.errors.append(TypeErrorInfo(
                line=line, column=col, kind="unop", op="!",
                expected="bool", got=t,
                message=f"operator '!' memerlukan bool, ditemukan {t}"))
            return "unknown"
        if e.op == "-":
            if t in NUMERIC or t == "unknown":
                return t
            self.errors.append(TypeErrorInfo(
                line=line, column=col, kind="unop", op="-",
                expected="int/float", got=t,
                message=f"operator '-' memerlukan int/float, ditemukan {t}"))
            return "unknown"
        return "unknown"

    def _assignable(self, decl_t: str, value_t: str) -> bool:
        if decl_t == value_t:
            return True
        if decl_t == "float" and value_t == "int":
            return True
        return False

    def _comparable(self, a: str, b: str) -> bool:
        if a == b:
            return True
        if a in NUMERIC and b in NUMERIC:
            return True
        return False

    def _type_error_binop(self, line, col, op, lt, rt, expected) -> None:
        self.errors.append(TypeErrorInfo(
            line=line, column=col, kind="binop", op=op,
            expected=expected, got=f"{lt}, {rt}",
            message=(f"operator {op!r} tidak kompatibel dengan tipe "
                     f"{lt} dan {rt} (diharapkan {expected})")))


def parse(source: str) -> Program:
    return Parser(Lexer(source).tokens()).parse()


def main() -> None:
    source = (
        'let a = 1 + "a"\n'
        'let b = true + 1\n'
        'let c = 10\n'
        'let d = c + 1\n'
        'print d\n'
    )
    print("=== Sumber ===")
    print(source, end="")
    program = parse(source)

    print()
    print("=== Pemeriksaan tipe ===")
    tc = TypeChecker()
    errors = tc.check(program)
    if not errors:
        print("(tidak ada error tipe)")
    for e in errors:
        print(f"  {e.line}:{e.column} [{e.kind}] op={e.op!r} "
              f"expected={e.expected} got={e.got}")
        print(f"    {e.message}")

    print()
    print("=== Inference pada scope global ===")
    for name, sym in tc.symbols.root.symbols.items():
        print(f"  {name}: {sym.type_hint}")


if __name__ == "__main__":
    main()

Keluaran berikut diperoleh dengan menjalankan python3 type_checker.py. Dua error binop dilaporkan pada kolom 11 dan 14, yaitu posisi operator + pada tiap baris. Setelah pemeriksaan, tabel simbol menunjukkan a dan b bernilai unknown karena initializer-nya ber-error, sedangkan c dan d bernilai int karena initializer sehat dan inference berhasil.

text Keluaran
=== Sumber ===
let a = 1 + "a"
let b = true + 1
let c = 10
let d = c + 1
print d

=== Pemeriksaan tipe ===
  1:11 [binop] op='+' expected=int/float (atau string+string untuk +) got=int, string
    operator '+' tidak kompatibel dengan tipe int dan string (diharapkan int/float (atau string+string untuk +))
  2:14 [binop] op='+' expected=int/float (atau string+string untuk +) got=bool, int
    operator '+' tidak kompatibel dengan tipe bool dan int (diharapkan int/float (atau string+string untuk +))

=== Inference pada scope global ===
  a: unknown
  b: unknown
  c: int
  d: int

Ringkasan#

  • Pemeriksaan tipe statis menelusuri AST dan menghitung tipe tiap ekspresi tanpa menjalankan program, dengan inferensi ringan pada Literal, Binary, Unary, Logical, Grouping, Variable, dan Assign.
  • Tipe int, float, string, bool, dan nil dikenali. Tipe unknown dipakai sebagai sentinel bila inferensi gagal, sehingga pemeriksaan dapat berlanjut dan error tidak menyebar ke seluruh pohon.
  • Operator aritmetika memerlukan operand numerik (atau string + string), operator pembanding memerlukan tipe yang sama, dan operator logika memerlukan bool. Penetapan mengikuti kompatibilitas dengan promosi int ke float.
  • Error tipe direpresentasikan sebagai TypeErrorInfo (dataclass), bukan Exception, sehingga banyak error dapat dilaporkan sekaligus dan topik error-semantik dapat merendernya dengan format konsisten.

Referensi#