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.
"""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.
=== 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: intRingkasan#
- 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, dannildikenali. Tipeunknowndipakai 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 memerlukanbool. Penetapan mengikuti kompatibilitas dengan promosiintkefloat. - Error tipe direpresentasikan sebagai
TypeErrorInfo(dataclass), bukanException, sehingga banyak error dapat dilaporkan sekaligus dan topik error-semantik dapat merendernya dengan format konsisten.
Referensi#
- Type system — Wikipedia — Ringkasan konseptual pemeriksaan tipe statis, inferensi, dan perbedaan dari pemeriksaan dinamis.
- Crafting Interpreters — Bob Nystrom — Bab “Statements and State” dan bab type checker untuk bahasa Lox membahas inferensi ringan dan pelaporan error tipe.
- Hindley-Milner type system — Wikipedia — Inferensi yang lebih kuat dari inference ringan pada topik ini, sebagai pembanding untuk modul lanjutan.