Implementasi Fitur

Fitur menembus setiap tahap pipeline: lexer, parser, semantik, IR, dan codegen.

Daftar isi

Implementasi fitur for-loop rentang menembus kelima tahap pipeline, yaitu lexer, parser, semantik, codegen, dan VM. Topik ini menunjukkan tambahan kode pada tiap tahap secara ringkas, lalu menyajikan berkas implementasi utuh yang menyatukan semua tahap dan dapat dijalankan. Berkas implementasi_for.py di samping topik ini berisi compiler lengkap dari lexer hingga VM, serta dua demo kecil. Setelah membaca topik ini, pembaca dapat menelusuri satu fitur dari token hingga eksekusi bytecode dalam satu berkas yang memenuhi kontrak spesifikasi_for.md.

Penambahan pada tiap tahap pipeline#

Setiap tahap pipeline menerima tambahan kode yang spesifik untuk fitur for-loop. Pada lexer, tiga token baru ditambahkan, yaitu FOR, IN, dan DOTDOT. Token DOTDOT dikenali lewat maximal munch pada dua titik berturut, sebelum titik tunggal, sebab titik tunggal tidak dipakai pada v1. Tanpa maximal munch, lexer akan salah membagi .. menjadi dua titik tunggal yang tidak dikenal.

python tahap1_lexer.py
"""Demo tahap 1: lexer for-loop.

Berkas ini berdiri sendiri dan mengeksekusi fragmen lexer untuk fitur
for-loop, yaitu pengenalan token FOR, IN, dan DOTDOT melalui maximal
munch. Keluaran menampilkan deret token dari kode sumber kecil yang
memuat for-loop rentang.

Pemakaian:
    python3 tahap1_lexer.py
"""


class Token:
    __slots__ = ("kind", "value", "pos")

    def __init__(self, kind, value, pos):
        self.kind = kind
        self.value = value
        self.pos = pos

    def __repr__(self):
        return f"Token({self.kind}, {self.value!r}, pos={self.pos})"


# Fragmen lexer for-loop (diambil dari implementasi_for.py)
KEYWORDS = {"let": "LET", "print": "PRINT", "for": "FOR", "in": "IN"}
SINGLE = {"=": "EQ", "+": "PLUS", "-": "MINUS", "*": "STAR",
          "(": "LPAREN", ")": "RPAREN", "{": "LBRACE", "}": "RBRACE"}


def lex(src):
    """Ubah kode sumber menjadi deret token, diakhiri token EOF."""
    tokens = []
    i = 0
    n = len(src)
    while i < n:
        c = src[i]
        if c in " \t\r\n":
            i += 1
            continue
        if c.isdigit():
            j = i
            while j < n and src[j].isdigit():
                j += 1
            tokens.append(Token("INT", int(src[i:j]), i))
            i = j
            continue
        if c.isalpha() or c == "_":
            j = i
            while j < n and (src[j].isalnum() or src[j] == "_"):
                j += 1
            word = src[i:j]
            kind = KEYWORDS.get(word, "IDENT")
            tokens.append(Token(kind, word, i))
            i = j
            continue
        # Fragmen inti: maximal munch pada dua titik berturut.
        # Token DOTDOT dikenali lewat maximal munch pada dua titik berturut,
        # sebelum titik tunggal, sebab titik tunggal tidak dipakai pada v1.
        if c == "." and i + 1 < n and src[i + 1] == ".":
            tokens.append(Token("DOTDOT", "..", i))
            i += 2
            continue
        if c in SINGLE:
            tokens.append(Token(SINGLE[c], c, i))
            i += 1
            continue
        raise ValueError(f"lexer: karakter tidak dikenal {c!r} pada posisi {i}")
    tokens.append(Token("EOF", None, i))
    return tokens


def main():
    src = "for i in 1..3 { print i }"
    print(f"Sumber: {src!r}")
    print()
    print("Deret token:")
    for t in lex(src):
        print(f"  {t!r}")


if __name__ == "__main__":
    main()

Keluaran berikut diperoleh dengan menjalankan python3 tahap1_lexer.py. Token DOTDOT dihasilkan dari dua titik berturut, bukan dua titik tunggal terpisah, sebagaimana yang dijanjikan fragmen maximal munch.

text Keluaran
Sumber: 'for i in 1..3 { print i }'

Deret token:
  Token(FOR, 'for', pos=0)
  Token(IDENT, 'i', pos=4)
  Token(IN, 'in', pos=6)
  Token(INT, 1, pos=9)
  Token(DOTDOT, '..', pos=10)
  Token(INT, 3, pos=12)
  Token(LBRACE, '{', pos=14)
  Token(PRINT, 'print', pos=16)
  Token(IDENT, 'i', pos=22)
  Token(RBRACE, '}', pos=24)
  Token(EOF, None, pos=25)

Pada parser, satu produksi tata bahasa baru ditambahkan, yaitu for_stmt := "for" IDENT "in" range_expr block. Produksi range_expr menangani operator .. dengan presedensi lebih rendah daripada aritmetika, sehingga 1 + 2 .. 5 * 2 dikelompokkan sebagai (1 + 2) .. (5 * 2). Node AST For menyimpan variabel loop, ekspresi rentang, dan badan. Node Range dibangun hanya pada parse_range, sehingga munculnya Range di luar for-in dapat ditolak oleh semantik.

python tahap2_parser.py
"""Demo tahap 2: parser for-loop.

Berkas ini berdiri sendiri dan mengeksekusi fragmen parser untuk fitur
for-loop, yaitu produksi `parse_for` dan `parse_range`. Keluaran
menampilkan AST yang dihasilkan dari kode sumber kecil yang memuat
for-loop rentang.

Pemakaian:
    python3 tahap2_parser.py
"""

import os
import sys
sys.path.insert(0, os.getcwd())

from tahap1_lexer import Token, lex


# --- AST node ---

class Range:
    def __init__(self, lo, hi, pos):
        self.lo = lo
        self.hi = hi
        self.pos = pos

    def __repr__(self):
        return f"Range({self.lo!r}, {self.hi!r})"


class IntLit:
    def __init__(self, value, pos):
        self.value = value
        self.pos = pos

    def __repr__(self):
        return f"IntLit({self.value})"


class Var:
    def __init__(self, name, pos):
        self.name = name
        self.pos = pos

    def __repr__(self):
        return f"Var({self.name!r})"


class Print:
    def __init__(self, expr, pos):
        self.expr = expr
        self.pos = pos

    def __repr__(self):
        return f"Print({self.expr!r})"


class Block:
    def __init__(self, stmts, pos):
        self.stmts = stmts
        self.pos = pos

    def __repr__(self):
        return f"Block({self.stmts!r})"


class For:
    def __init__(self, var, expr, body, pos):
        self.var = var
        self.expr = expr
        self.body = body
        self.pos = pos

    def __repr__(self):
        return f"For(var={self.var!r}, expr={self.expr!r}, body={self.body!r})"


# --- Parser ---

class Parser:
    def __init__(self, tokens):
        self.tokens = tokens
        self.i = 0

    def peek(self):
        return self.tokens[self.i]

    def advance(self):
        t = self.tokens[self.i]
        self.i += 1
        return t

    def expect(self, kind):
        t = self.tokens[self.i]
        if t.kind != kind:
            raise SyntaxError(f"mengharapkan {kind}, mendapat {t.kind}")
        self.i += 1
        return t

    # Fragmen inti: produksi for_stmt dan range_expr
    def parse_for(self):
        k = self.expect("FOR")
        var = self.expect("IDENT").value
        self.expect("IN")
        expr = self.parse_expr()      # expr bisa berupa Range atau bukan
        body = self.parse_block()
        return For(var, expr, body, k.pos)

    def parse_range(self, lo):
        if self.peek().kind == "DOTDOT":
            pos = self.advance().pos
            hi = self.parse_arith()
            return Range(lo, hi, pos)   # hanya di sini Range dibangun
        return lo

    def parse_arith(self):
        t = self.peek()
        if t.kind == "INT":
            self.advance()
            return IntLit(t.value, t.pos)
        if t.kind == "IDENT":
            self.advance()
            return Var(t.value, t.pos)
        raise SyntaxError(f"mengharapkan INT/IDENT, mendapat {t.kind}")

    def parse_expr(self):
        lo = self.parse_arith()
        return self.parse_range(lo)

    def parse_block(self):
        lb = self.expect("LBRACE")
        stmts = []
        while self.peek().kind != "RBRACE":
            stmts.append(self.parse_stmt())
        rb = self.expect("RBRACE")
        return Block(stmts, lb.pos)

    def parse_stmt(self):
        t = self.peek()
        if t.kind == "PRINT":
            self.advance()
            e = self.parse_expr()
            return Print(e, t.pos)
        if t.kind == "IDENT":
            self.advance()
            return Var(t.value, t.pos)
        raise SyntaxError(f"stmt tak didukung pada {t.kind}")


def main():
    src = "for i in 1..3 { print i }"
    print(f"Sumber: {src!r}")
    print()
    tokens = lex(src)
    parser = Parser(tokens)
    ast = parser.parse_for()
    print("AST yang dihasilkan parse_for:")
    print(f"  {ast!r}")


if __name__ == "__main__":
    main()

Keluaran berikut diperoleh dengan menjalankan python3 tahap2_parser.py. Node Range hanya dibangun di parse_range, sehingga munculnya Range di luar for-in dapat ditolak oleh semantik.

text Keluaran
Sumber: 'for i in 1..3 { print i }'

AST yang dihasilkan parse_for:
  For(var='i', expr=Range(IntLit(1), IntLit(3)), body=Block([Print(Var('i'))]))

Pada semantik, for-loop memeriksa tiga hal. Ekspresi setelah in harus berupa Range, endpoint harus int, dan variabel loop dideklarasikan dalam scope blok loop. Scope didorong sebelum badan diperiksa dan dimunculkan setelahnya, sehingga variabel loop keluar dari scope sesuai spesifikasi. Semantik juga menolak Range yang muncul di luar for-in, sebab check_expr memperlakukan Range sebagai node yang hanya sah pada konteks for-in.

python tahap3_semantik.py
"""Demo tahap 3: semantik for-loop.

Berkas ini berdiri sendiri dan mengeksekusi fragmen semantik untuk
fitur for-loop, yaitu pemeriksaan bahwa ekspresi setelah `in` adalah
Range, endpoint adalah int, dan variabel loop dideklarasikan dalam
scope blok loop. Keluaran menampilkan jejak pemeriksaan pada program
kecil yang memuat for-loop rentang.

Pemakaian:
    python3 tahap3_semantik.py
"""

import os
import sys
sys.path.insert(0, os.getcwd())

from tahap2_parser import Parser, For, Range, IntLit, Var, Print, Block
from tahap1_lexer import lex


class CompilerError(Exception):
    pass


class Semantik:
    def __init__(self):
        self.scopes = [{}]

    def declare(self, name, pos=None):
        if name in self.scopes[-1]:
            raise CompilerError(f"semantik: redeclare {name!r} pada scope yang sama")
        self.scopes[-1][name] = "int"

    def check_expr(self, e):
        if isinstance(e, IntLit):
            return "int"
        if isinstance(e, Var):
            for s in reversed(self.scopes):
                if e.name in s:
                    return s[e.name]
            raise CompilerError(f"semantik: variabel {e.name!r} belum dideklarasikan")
        if isinstance(e, Range):
            lo_t = self.check_expr(e.lo)
            hi_t = self.check_expr(e.hi)
            if lo_t != "int" or hi_t != "int":
                raise CompilerError("semantik: endpoint rentang harus int")
            return "range"
        raise CompilerError(f"semantik: ekspresi tak didukung {type(e).__name__}")

    # Fragmen inti: cek For, scope loop, dan tipe rentang.
    def check(self, node):
        # elif isinstance(node, For):  -- di topik ini, hanya For yang ditangani
        if isinstance(node, For):
            if not isinstance(node.expr, Range):
                raise CompilerError("semantik: for-in mengharapkan rentang (a..b)", node.pos)
            self.check_expr(node.expr.lo)
            self.check_expr(node.expr.hi)
            self.scopes.append({})          # scope blok loop
            self.declare(node.var, node.pos)
            for stmt in node.body.stmts:
                self.check_stmt(stmt)
            self.scopes.pop()               # variabel loop keluar dari scope
            return
        raise CompilerError(f"semantik: node tak didukung {type(node).__name__}")

    def check_stmt(self, stmt):
        if isinstance(stmt, Print):
            self.check_expr(stmt.expr)
            return
        if isinstance(stmt, Var):
            for s in reversed(self.scopes):
                if stmt.name in s:
                    return
            raise CompilerError(f"semantik: variabel {stmt.name!r} belum dideklarasikan")
        raise CompilerError(f"semantik: stmt tak didukung {type(stmt).__name__}")


def main():
    src = "for i in 1..3 { print i }"
    print(f"Sumber: {src!r}")
    print()
    tokens = lex(src)
    parser = Parser(tokens)
    ast = parser.parse_for()
    print("AST:", repr(ast))
    print()
    print("Jejak pemeriksaan semantik:")
    sem = Semantik()
    sem.check(ast)
    print(f"  For: ekspresi adalah Range, OK")
    print(f"  For: endpoint lo dan hi bertipe int, OK")
    print(f"  For: scope blok loop didorong, variabel 'i' dideklarasikan")
    print(f"  For: body diperiksa dalam scope loop")
    print(f"  For: scope blok loop dimunculkan, variabel 'i' keluar dari scope")
    print(f"  Setelah pemeriksaan: scope terdalam = {sem.scopes[-1]}")

    # Verifikasi bahwa variabel loop keluar dari scope setelah blok
    print()
    print("Pemeriksaan bahwa 'i' tidak dapat dipakai setelah loop:")
    sem2 = Semantik()
    sem2.scopes = [{}]
    try:
        sem2.check_expr(Var("i", 0))
    except CompilerError as e:
        print(f"  CompilerError ditemukan: {e}")


if __name__ == "__main__":
    main()

Keluaran berikut diperoleh dengan menjalankan python3 tahap3_semantik.py. Setelah blok loop selesai diperiksa, scope terdalam kembali kosong, dan variabel i tidak dapat dipakai di luar loop.

text Keluaran
Sumber: 'for i in 1..3 { print i }'

AST: For(var='i', expr=Range(IntLit(1), IntLit(3)), body=Block([Print(Var('i'))]))

Jejak pemeriksaan semantik:
  For: ekspresi adalah Range, OK
  For: endpoint lo dan hi bertipe int, OK
  For: scope blok loop didorong, variabel 'i' dideklarasikan
  For: body diperiksa dalam scope loop
  For: scope blok loop dimunculkan, variabel 'i' keluar dari scope
  Setelah pemeriksaan: scope terdalam = {}

Pemeriksaan bahwa 'i' tidak dapat dipakai setelah loop:
  CompilerError ditemukan: semantik: variabel 'i' belum dideklarasikan

Pada codegen, for-loop dibangkitkan sebagai pola jump klasik. Endpoint kanan disimpan ke variabel sementara, variabel loop diinisialisasi dengan endpoint kiri, lalu label kondisi membandingkan variabel loop dengan endpoint kanan. Bila kondisi salah, lompat ke label akhir. Bila benar, jalankan badan, naikkan variabel loop satu per satu, lalu lompat kembali ke kondisi. Scope didorong sebelum loop dan dimunculkan setelah loop, sehingga variabel loop dan sementara hilang setelah loop selesai.

python tahap4_codegen.py
"""Demo tahap 4: codegen for-loop.

Berkas ini berdiri sendiri dan mengeksekusi fragmen codegen untuk
fitur for-loop, yaitu pembangkitan pola jump klasik dari AST For
menjadi deret instruksi bytecode. Keluaran menampilkan bytecode yang
dihasilkan untuk for-loop sederhana.

Pemakaian:
    python3 tahap4_codegen.py
"""

import os
import sys
sys.path.insert(0, os.getcwd())

from tahap2_parser import Parser, For, Range, IntLit, Var, Print, Block
from tahap1_lexer import lex


class Codegen:
    def __init__(self):
        self.instrs = []
        self._n_tmp = 0
        self._n_lbl = 0

    def new_tmp(self):
        self._n_tmp += 1
        return f"t{self._n_tmp}"

    def new_label(self):
        self._n_lbl += 1
        return f"L{self._n_lbl}"

    def emit(self, op, *args):
        self.instrs.append((op,) + args)

    def gen_expr(self, e):
        if isinstance(e, IntLit):
            self.emit("PUSH", e.value)
        elif isinstance(e, Var):
            self.emit("LOAD", e.name)
        else:
            raise ValueError(f"codegen: ekspresi tak didukung {type(e).__name__}")

    def gen_stmt(self, stmt):
        if isinstance(stmt, Print):
            self.gen_expr(stmt.expr)
            self.emit("PRINT")
        elif isinstance(stmt, Var):
            self.gen_expr(stmt)
        else:
            raise ValueError(f"codegen: stmt tak didukung {type(stmt).__name__}")

    # Fragmen inti: pola jump untuk For.
    def gen_for(self, node):
        # elif isinstance(node, For):
        end = self.new_tmp()
        self.emit("PUSH_SCOPE")
        self.gen_expr(node.expr.hi); self.emit("STORE", end)
        self.gen_expr(node.expr.lo); self.emit("STORE", node.var)
        lbl_cond = self.new_label(); lbl_end = self.new_label()
        self.emit("LABEL", lbl_cond)
        self.emit("LOAD", node.var); self.emit("LOAD", end); self.emit("LT")
        self.emit("JMP_IF_FALSE", lbl_end)
        for stmt in node.body.stmts:
            self.gen_stmt(stmt)
        self.emit("INC", node.var); self.emit("JMP", lbl_cond)
        self.emit("LABEL", lbl_end)
        self.emit("POP_SCOPE")

    def generate(self, for_node):
        self.gen_for(for_node)
        self.emit("HALT")
        return self.instrs


def main():
    src = "for i in 1..3 { print i }"
    print(f"Sumber: {src!r}")
    print()
    tokens = lex(src)
    parser = Parser(tokens)
    ast = parser.parse_for()
    print("AST:", repr(ast))
    print()
    cg = Codegen()
    bytecode = cg.generate(ast)
    print("Bytecode yang dibangkitkan codegen untuk For:")
    for instr in bytecode:
        print(f"  {instr}")


if __name__ == "__main__":
    main()

Keluaran berikut diperoleh dengan menjalankan python3 tahap4_codegen.py. Pola jump klasik terlihat: LABEL L1 (kondisi), JMP_IF_FALSE L2 (keluar), INC i, JMP L1 (ulang), LABEL L2 (akhir).

text Keluaran
Sumber: 'for i in 1..3 { print i }'

AST: For(var='i', expr=Range(IntLit(1), IntLit(3)), body=Block([Print(Var('i'))]))

Bytecode yang dibangkitkan codegen untuk For:
  ('PUSH_SCOPE',)
  ('PUSH', 3)
  ('STORE', 't1')
  ('PUSH', 1)
  ('STORE', 'i')
  ('LABEL', 'L1')
  ('LOAD', 'i')
  ('LOAD', 't1')
  ('LT',)
  ('JMP_IF_FALSE', 'L2')
  ('LOAD', 'i')
  ('PRINT',)
  ('INC', 'i')
  ('JMP', 'L1')
  ('LABEL', 'L2')
  ('POP_SCOPE',)
  ('HALT',)

Pada VM, dispatch instruksi jump dan scope bertingkat menjadi titik utama. JMP mengganti program counter dengan alamat label, sedangkan JMP_IF_FALSE melakukan hal yang sama hanya bila kondisi bernilai salah. PUSH_SCOPE dan POP_SCOPE mengelola tumpukan dict pada VM, sehingga variabel loop hidup selama loop dan hilang setelahnya. Pencarian variabel menyusuri scope dari terdalam ke terluar, sehingga shadowing bekerja.

python tahap5_vm.py
"""Demo tahap 5: VM dispatch jump dan scope.

Berkas ini berdiri sendiri dan mengeksekusi fragmen VM untuk fitur
for-loop, yaitu dispatch instruksi JMP, JMP_IF_FALSE, PUSH_SCOPE,
dan POP_SCOPE. Keluaran menampilkan hasil eksekusi bytecode yang
dihasilkan codegen tahap 4 untuk for-loop rentang.

Pemakaian:
    python3 tahap5_vm.py
"""

import os
import sys
sys.path.insert(0, os.getcwd())

from tahap4_codegen import Codegen
from tahap2_parser import Parser, For, Range, IntLit, Var, Print, Block
from tahap1_lexer import lex


class VM:
    def __init__(self):
        self.env = [{}]   # tumpukan dict scope, terdalam di akhir
        self.stack = []
        self.output = []

    def find(self, name):
        for s in reversed(self.env):
            if name in s:
                return s
        raise KeyError(f"variabel {name!r} tidak terlihat pada scope mana pun")

    def run(self, bytecode):
        # Pass 1: bangun pemetaan label -> indeks
        labels = {}
        for idx, instr in enumerate(bytecode):
            if instr[0] == "LABEL":
                labels[instr[1]] = idx
        # Pass 2: eksekusi
        pc = 0
        n = len(bytecode)
        while pc < n:
            instr = bytecode[pc]
            op = instr[0]
            if op == "HALT":
                break
            if op == "PUSH":
                self.stack.append(instr[1])
                pc += 1
            elif op == "STORE":
                self.env[-1][instr[1]] = self.stack.pop()
                pc += 1
            elif op == "LOAD":
                scope = self.find(instr[1])
                self.stack.append(scope[instr[1]])
                pc += 1
            elif op == "LT":
                b = self.stack.pop()
                a = self.stack.pop()
                self.stack.append(1 if a < b else 0)
                pc += 1
            elif op == "PRINT":
                v = self.stack.pop()
                self.output.append(v)
                pc += 1
            elif op == "INC":
                scope = self.find(instr[1])
                scope[instr[1]] += 1
                pc += 1
            # Fragmen inti: dispatch jump dan scope.
            elif op == "JMP":
                pc = labels[instr[1]]
            elif op == "JMP_IF_FALSE":
                cond = self.stack.pop()
                pc = labels[instr[1]] if not cond else pc + 1
            elif op == "PUSH_SCOPE":
                self.env.append({})
                pc += 1
            elif op == "POP_SCOPE":
                self.env.pop()
                pc += 1
            elif op == "LABEL":
                pc += 1
            else:
                raise ValueError(f"VM: opcode tak dikenal {op!r}")
        return self.output


def main():
    src = "for i in 1..3 { print i }"
    print(f"Sumber: {src!r}")
    print()
    tokens = lex(src)
    ast = Parser(tokens).parse_for()
    bytecode = Codegen().generate(ast)
    print("Bytecode yang akan dieksekusi VM:")
    for instr in bytecode:
        print(f"  {instr}")
    print()
    vm = VM()
    output = vm.run(bytecode)
    print("Hasil eksekusi VM (output print i selama i = 1..3):")
    for v in output:
        print(f"  {v}")


if __name__ == "__main__":
    main()

Keluaran berikut diperoleh dengan menjalankan python3 tahap5_vm.py. Rentang [1, 3) eksklusif ujung kanan menghasilkan 1 dan 2, sebagaimana yang dispesifikasi pada spesifikasi_for.md.

text Keluaran
Sumber: 'for i in 1..3 { print i }'

Bytecode yang akan dieksekusi VM:
  ('PUSH_SCOPE',)
  ('PUSH', 3)
  ('STORE', 't1')
  ('PUSH', 1)
  ('STORE', 'i')
  ('LABEL', 'L1')
  ('LOAD', 'i')
  ('LOAD', 't1')
  ('LT',)
  ('JMP_IF_FALSE', 'L2')
  ('LOAD', 'i')
  ('PRINT',)
  ('INC', 'i')
  ('JMP', 'L1')
  ('LABEL', 'L2')
  ('POP_SCOPE',)
  ('HALT',)

Hasil eksekusi VM (output print i selama i = 1..3):
  1
  2

Berkas implementasi utuh#

Kelima tahap di atas disatukan dalam satu berkas implementasi_for.py di samping topik ini. Berkas ini berisi kelas Token dan fungsi lex untuk lexer, kelas AST dan Parser untuk parser, kelas Semantik untuk pemeriksaan tipe dan scope, kelas Codegen untuk pembangkitan bytecode, kelas VM untuk eksekusi stack machine, serta fungsi compile_src dan run_src yang merangkai semua tahap. Berkas dapat dijalankan langsung untuk menjalankan dua demo kecil.

python implementasi_for.py
"""Implementasi fitur for-loop rentang yang menembus setiap tahap pipeline.

Fitur: for IDENT in a..b { body }
  - a dan b dievaluasi sekali sebelum loop dimulai
  - variabel loop dilingkupkan ke blok loop
  - rentang [a, b) eksklusif ujung kanan

Tahap pipeline yang disentuh:
  lexer    -> token FOR, IN, DOTDOT (maximal munch pada dua titik berturut)
  parser   -> produksi for_stmt, node AST For dengan badan Block
  semantik -> deklarasi variabel loop di scope blok, cek tipe endpoint
  codegen  -> emit PUSH_SCOPE/LOAD/STORE/LT/JMP_IF_FALSE/INC/POP_SCOPE
  VM       -> dispatch jump, increment, dan scope lookup bertingkat

Berkas ini bersifat mandiri: jalankan `python3 implementasi_for.py` untuk
menjalankan dua demo kecil yang mencetak keluaran ke layar.
"""


class CompilerError(Exception):
    """Error lexer, parser, semantik, atau runtime dengan pesan dan posisi."""

    def __init__(self, msg, pos=None):
        super().__init__(msg)
        self.msg = msg
        self.pos = pos

    def __str__(self):
        if self.pos is not None:
            return f"{self.msg} (posisi {self.pos})"
        return self.msg


# ---------------------------------------------------------------------------
# Tahap 1: Lexer
# ---------------------------------------------------------------------------

KEYWORDS = {"let": "LET", "print": "PRINT", "for": "FOR", "in": "IN"}
SINGLE = {"=": "EQ", "+": "PLUS", "-": "MINUS", "*": "STAR",
          "(": "LPAREN", ")": "RPAREN", "{": "LBRACE", "}": "RBRACE"}


class Token:
    __slots__ = ("kind", "value", "pos")

    def __init__(self, kind, value, pos):
        self.kind = kind
        self.value = value
        self.pos = pos

    def __repr__(self):
        return f"Token({self.kind}, {self.value!r}, pos={self.pos})"


def lex(src):
    """Ubah kode sumber menjadi deret token, diakhiri token EOF."""
    tokens = []
    i = 0
    n = len(src)
    while i < n:
        c = src[i]
        if c in " \t\r\n":
            i += 1
            continue
        if c.isdigit():
            j = i
            while j < n and src[j].isdigit():
                j += 1
            tokens.append(Token("INT", int(src[i:j]), i))
            i = j
            continue
        if c.isalpha() or c == "_":
            j = i
            while j < n and (src[j].isalnum() or src[j] == "_"):
                j += 1
            word = src[i:j]
            kind = KEYWORDS.get(word, "IDENT")
            tokens.append(Token(kind, word, i))
            i = j
            continue
        if c == "." and i + 1 < n and src[i + 1] == ".":
            tokens.append(Token("DOTDOT", "..", i))
            i += 2
            continue
        if c in SINGLE:
            tokens.append(Token(SINGLE[c], c, i))
            i += 1
            continue
        raise CompilerError(f"lexer: karakter tidak dikenal {c!r}", i)
    tokens.append(Token("EOF", None, i))
    return tokens


# ---------------------------------------------------------------------------
# Tahap 2: Parser (AST + recursive descent)
# ---------------------------------------------------------------------------

class Program:
    def __init__(self, stmts):
        self.stmts = stmts


class Let:
    def __init__(self, name, expr, pos):
        self.name = name
        self.expr = expr
        self.pos = pos


class Print:
    def __init__(self, expr, pos):
        self.expr = expr
        self.pos = pos


class Assign:
    def __init__(self, name, expr, pos):
        self.name = name
        self.expr = expr
        self.pos = pos


class For:
    def __init__(self, var, expr, body, pos):
        self.var = var
        self.expr = expr
        self.body = body
        self.pos = pos


class Block:
    def __init__(self, stmts):
        self.stmts = stmts


class BinOp:
    def __init__(self, op, left, right, pos):
        self.op = op
        self.left = left
        self.right = right
        self.pos = pos


class Range:
    def __init__(self, lo, hi, pos):
        self.lo = lo
        self.hi = hi
        self.pos = pos


class Int:
    def __init__(self, value, pos):
        self.value = value
        self.pos = pos


class Ident:
    def __init__(self, name, pos):
        self.name = name
        self.pos = pos


class Parser:
    def __init__(self, tokens):
        self.toks = tokens
        self.i = 0

    def peek(self):
        return self.toks[self.i]

    def advance(self):
        t = self.toks[self.i]
        self.i += 1
        return t

    def expect(self, kind):
        t = self.peek()
        if t.kind != kind:
            raise CompilerError(
                f"parser: token {kind} diharapkan, mendapat {t.kind}", t.pos)
        return self.advance()

    def parse_program(self):
        stmts = []
        while self.peek().kind != "EOF":
            stmts.append(self.parse_stmt())
        return Program(stmts)

    def parse_stmt(self):
        t = self.peek()
        if t.kind == "LET":
            return self.parse_let()
        if t.kind == "PRINT":
            return self.parse_print()
        if t.kind == "FOR":
            return self.parse_for()
        if t.kind == "LBRACE":
            return self.parse_block()
        if t.kind == "IDENT":
            return self.parse_assign()
        raise CompilerError(
            f"parser: pernyataan tidak dikenal, mendapat {t.kind}", t.pos)

    def parse_assign(self):
        t = self.expect("IDENT")
        self.expect("EQ")
        expr = self.parse_expr()
        return Assign(t.value, expr, t.pos)

    def parse_let(self):
        k = self.expect("LET")
        name = self.expect("IDENT").value
        self.expect("EQ")
        expr = self.parse_expr()
        return Let(name, expr, k.pos)

    def parse_print(self):
        k = self.expect("PRINT")
        expr = self.parse_expr()
        return Print(expr, k.pos)

    def parse_for(self):
        k = self.expect("FOR")
        var = self.expect("IDENT").value
        self.expect("IN")
        expr = self.parse_expr()
        body = self.parse_block()
        return For(var, expr, body, k.pos)

    def parse_block(self):
        self.expect("LBRACE")
        stmts = []
        while self.peek().kind != "RBRACE":
            stmts.append(self.parse_stmt())
        self.expect("RBRACE")
        return Block(stmts)

    def parse_expr(self):
        return self.parse_range()

    def parse_range(self):
        lo = self.parse_arith()
        if self.peek().kind == "DOTDOT":
            pos = self.advance().pos
            hi = self.parse_arith()
            return Range(lo, hi, pos)
        return lo

    def parse_arith(self):
        left = self.parse_term()
        while self.peek().kind in ("PLUS", "MINUS"):
            t = self.advance()
            right = self.parse_term()
            left = BinOp(t.value, left, right, t.pos)
        return left

    def parse_term(self):
        left = self.parse_factor()
        while self.peek().kind == "STAR":
            t = self.advance()
            right = self.parse_factor()
            left = BinOp(t.value, left, right, t.pos)
        return left

    def parse_factor(self):
        t = self.peek()
        if t.kind == "INT":
            self.advance()
            return Int(t.value, t.pos)
        if t.kind == "IDENT":
            self.advance()
            return Ident(t.value, t.pos)
        if t.kind == "LPAREN":
            self.advance()
            e = self.parse_expr()
            self.expect("RPAREN")
            return e
        raise CompilerError(
            f"parser: ekspresi diharapkan, mendapat {t.kind}", t.pos)


# ---------------------------------------------------------------------------
# Tahap 3: Semantik (scope + tipe)
# ---------------------------------------------------------------------------

class Semantik:
    def __init__(self):
        self.scopes = [{}]

    def declare(self, name, pos):
        if name in self.scopes[-1]:
            raise CompilerError(
                f"semantik: nama {name!r} sudah dideklarasikan di scope ini", pos)
        self.scopes[-1][name] = "int"

    def lookup(self, name, pos):
        for s in reversed(self.scopes):
            if name in s:
                return s[name]
        raise CompilerError(
            f"semantik: nama {name!r} tidak dideklarasikan", pos)

    def check(self, node):
        if isinstance(node, Program):
            for s in node.stmts:
                self.check(s)
        elif isinstance(node, Block):
            self.scopes.append({})
            for s in node.stmts:
                self.check(s)
            self.scopes.pop()
        elif isinstance(node, Let):
            self.check_expr(node.expr)
            self.declare(node.name, node.pos)
        elif isinstance(node, Assign):
            self.lookup(node.name, node.pos)
            self.check_expr(node.expr)
        elif isinstance(node, Print):
            self.check_expr(node.expr)
        elif isinstance(node, For):
            if not isinstance(node.expr, Range):
                raise CompilerError(
                    "semantik: for-in mengharapkan rentang (a..b)", node.pos)
            self.check_expr(node.expr.lo)
            self.check_expr(node.expr.hi)
            self.scopes.append({})
            self.declare(node.var, node.pos)
            self.check(node.body)
            self.scopes.pop()
        else:
            raise CompilerError(
                f"semantik: node tidak dikenal {type(node).__name__}")

    def check_expr(self, node):
        if isinstance(node, Int):
            return "int"
        if isinstance(node, Ident):
            return self.lookup(node.name, node.pos)
        if isinstance(node, BinOp):
            lt = self.check_expr(node.left)
            rt = self.check_expr(node.right)
            if lt != "int" or rt != "int":
                raise CompilerError(
                    f"semantik: operand {node.op} harus int", node.pos)
            return "int"
        if isinstance(node, Range):
            raise CompilerError(
                "semantik: rentang hanya boleh muncul pada for-in", node.pos)
        raise CompilerError(
            f"semantik: ekspresi tidak dikenal {type(node).__name__}")


# ---------------------------------------------------------------------------
# Tahap 4 + 5: Codegen + VM (stack machine)
# ---------------------------------------------------------------------------

# Instruksi bytecode:
#   ("PUSH_INT", n)          push int n
#   ("LOAD", name)           push variabel name (cari scope terdalam dulu)
#   ("STORE", name)          pop, simpan ke scope terdalam (pengikatan baru)
#   ("ASSIGN", name)         pop, perbarui binding yang ada di scope mana pun
#   ("ADD"/"SUB"/"MUL")       pop b, pop a, push a op b
#   ("LT")                   pop b, pop a, push 1 bila a < b, 0 bila tidak
#   ("INC", name)            name = name + 1
#   ("PRINT")                pop, catat ke keluaran
#   ("JMP", label)          lompat ke label
#   ("JMP_IF_FALSE", label) pop kondisi, lompat bila 0
#   ("PUSH_SCOPE")          push dict scope baru
#   ("POP_SCOPE")           pop dict scope terdalam
#   ("LABEL", label)        penanda alamat (tidak melakukan apa pun)
#   ("HALT")                 hentikan eksekusi

class Codegen:
    def __init__(self):
        self.code = []
        self.labels = 0
        self.tmps = 0

    def new_label(self):
        self.labels += 1
        return f"L{self.labels}"

    def new_tmp(self):
        self.tmps += 1
        return f"__tmp{self.tmps}"

    def emit(self, *instr):
        self.code.append(instr)

    def gen(self, node):
        if isinstance(node, Program):
            for s in node.stmts:
                self.gen_stmt(s)
            self.emit("HALT")
        else:
            raise CompilerError(
                f"codegen: node tidak dikenal {type(node).__name__}")

    def gen_stmt(self, node):
        if isinstance(node, Let):
            self.gen_expr(node.expr)
            self.emit("STORE", node.name)
        elif isinstance(node, Assign):
            self.gen_expr(node.expr)
            self.emit("ASSIGN", node.name)
        elif isinstance(node, Print):
            self.gen_expr(node.expr)
            self.emit("PRINT")
        elif isinstance(node, Block):
            self.emit("PUSH_SCOPE")
            for s in node.stmts:
                self.gen_stmt(s)
            self.emit("POP_SCOPE")
        elif isinstance(node, For):
            end = self.new_tmp()
            self.emit("PUSH_SCOPE")
            self.gen_expr(node.expr.hi)
            self.emit("STORE", end)
            self.gen_expr(node.expr.lo)
            self.emit("STORE", node.var)
            lbl_cond = self.new_label()
            lbl_end = self.new_label()
            self.emit("LABEL", lbl_cond)
            self.emit("LOAD", node.var)
            self.emit("LOAD", end)
            self.emit("LT")
            self.emit("JMP_IF_FALSE", lbl_end)
            self.gen_stmt(node.body)
            self.emit("INC", node.var)
            self.emit("JMP", lbl_cond)
            self.emit("LABEL", lbl_end)
            self.emit("POP_SCOPE")
        else:
            raise CompilerError(
                f"codegen: pernyataan tidak dikenal {type(node).__name__}")

    def gen_expr(self, node):
        if isinstance(node, Int):
            self.emit("PUSH_INT", node.value)
        elif isinstance(node, Ident):
            self.emit("LOAD", node.name)
        elif isinstance(node, BinOp):
            self.gen_expr(node.left)
            self.gen_expr(node.right)
            self.emit({"+": "ADD", "-": "SUB", "*": "MUL"}[node.op])
        else:
            raise CompilerError(
                f"codegen: ekspresi tidak dikenal {type(node).__name__}")


class VM:
    def __init__(self, code):
        self.code = code
        self.labels = {}
        for i, instr in enumerate(self.code):
            if instr[0] == "LABEL":
                self.labels[instr[1]] = i
        self.stack = []
        self.env = [{}]
        self.out = []

    def load(self, name):
        for s in reversed(self.env):
            if name in s:
                return s[name]
        raise CompilerError(f"vm: variabel {name!r} tidak terdefinisi")

    def store(self, name, val):
        self.env[-1][name] = val

    def assign(self, name, val):
        for s in reversed(self.env):
            if name in s:
                s[name] = val
                return
        self.env[-1][name] = val

    def run(self):
        pc = 0
        while pc < len(self.code):
            instr = self.code[pc]
            op = instr[0]
            if op == "HALT":
                break
            elif op == "PUSH_INT":
                self.stack.append(instr[1]); pc += 1
            elif op == "LOAD":
                self.stack.append(self.load(instr[1])); pc += 1
            elif op == "STORE":
                self.store(instr[1], self.stack.pop()); pc += 1
            elif op == "ASSIGN":
                self.assign(instr[1], self.stack.pop()); pc += 1
            elif op == "ADD":
                b = self.stack.pop(); a = self.stack.pop()
                self.stack.append(a + b); pc += 1
            elif op == "SUB":
                b = self.stack.pop(); a = self.stack.pop()
                self.stack.append(a - b); pc += 1
            elif op == "MUL":
                b = self.stack.pop(); a = self.stack.pop()
                self.stack.append(a * b); pc += 1
            elif op == "LT":
                b = self.stack.pop(); a = self.stack.pop()
                self.stack.append(1 if a < b else 0); pc += 1
            elif op == "INC":
                self.store(instr[1], self.load(instr[1]) + 1); pc += 1
            elif op == "PRINT":
                self.out.append(str(self.stack.pop())); pc += 1
            elif op == "JMP":
                pc = self.labels[instr[1]]
            elif op == "JMP_IF_FALSE":
                cond = self.stack.pop()
                pc = self.labels[instr[1]] if not cond else pc + 1
            elif op == "PUSH_SCOPE":
                self.env.append({}); pc += 1
            elif op == "POP_SCOPE":
                self.env.pop(); pc += 1
            elif op == "LABEL":
                pc += 1
            else:
                raise CompilerError(f"vm: instruksi tidak dikenal {op}")
        return self.out


def compile_src(src):
    tokens = lex(src)
    ast = Parser(tokens).parse_program()
    Semantik().check(ast)
    cg = Codegen()
    cg.gen(ast)
    return cg.code


def run_src(src):
    return VM(compile_src(src)).run()


def main():
    src1 = (
        "let total = 0\n"
        "for i in 1..5 {\n"
        "  total = total + i\n"
        "}\n"
        "print total"
    )
    print("Demo 1: akumulasi rentang 1..5")
    print(src1)
    print("-> " + ", ".join(run_src(src1)))
    print()
    src2 = (
        "for i in 1..4 {\n"
        "  print i\n"
        "}"
    )
    print("Demo 2: cetak iterasi 1..4")
    print(src2)
    print("-> " + ", ".join(run_src(src2)))


if __name__ == "__main__":
    main()
text Keluaran
Demo 1: akumulasi rentang 1..5
let total = 0
for i in 1..5 {
  total = total + i
}
print total
-> 10

Demo 2: cetak iterasi 1..4
for i in 1..4 {
  print i
}
-> 1, 2, 3

Keluaran demo pertama menunjukkan akumulasi rentang 1..5 menghasilkan 10, yaitu penjumlahan 1, 2, 3, 4 sesuai aturan ujung kanan eksklusif pada spesifikasi. Keluaran demo kedua menunjukkan pencetakan iterasi 1..4 menghasilkan 1, 2, 3. Kedua demo ini mereproduksi contoh program valid pada spesifikasi_for.md, sehingga implementasi terbukti memenuhi kontrak yang ditetapkan pada spesifikasi tersebut.

Menelusuri demo lewat pipeline#

Demo pertama, yaitu program yang mengikat total ke nol, mengakumulasi total = total + i untuk tiap i pada rentang 1..5, lalu mencetak total, dapat ditelusuri melalui kelima tahap untuk melihat bagaimana setiap tahap memenuhi kontrak. Pada lexer, kata kunci for dan in dikenali sebagai token FOR dan IN, sedangkan 1..5 dikenali sebagai INT 1, DOTDOT, INT 5 berkat maximal munch. Pada parser, parse_for membangun node For dengan variabel i, ekspresi rentang berupa Range(Int 1, Int 5), dan badan berupa Block yang memuat satu pernyataan Assign.

Pada semantik, ekspresi setelah in diperiksa dan dinyatakan sebagai Range, kedua endpoint diperiksa dan dinyatakan int, lalu scope blok loop didorong dan variabel i dideklarasikan di dalamnya. Variabel total diperiksa pada scope global dan dinyatakan dideklarasikan, sehingga pernyataan Assign di dalam badan sah. Pada codegen, endpoint kanan 5 disimpan ke variabel sementara, variabel loop i diinisialisasi dengan 1, lalu pola kondisi, badan, inkrementasi, dan lompat kembali dibangkitkan sebagai deret instruksi LOAD, LT, JMP_IF_FALSE, INC, dan JMP.

Pada VM, instruksi dijalankan satu per satu. Iterasi pertama menjalankan badan dengan i = 1 sehingga total menjadi 1, iterasi kedua dengan i = 2 menjadikan total 3, dan seterusnya hingga i = 4 menjadikan total 10. Ketika i = 5, komparasi i < 5 menghasilkan salah, sehingga JMP_IF_FALSE melompat ke label akhir dan loop berhenti. Scope blok loop kemudian dimunculkan, sehingga variabel i dan sementara hilang, dan pernyataan print total mencari total pada scope global yang kini bernilai 10. Penelusuran ini menunjukkan bahwa setiap tahap memenuhi satu aturan pada spesifikasi_for.md, sehingga implementasi menjadi bukti bahwa kontrak dapat ditegakkan dari hulu ke hilir.

Ringkasan#

  • Lexer menambah tiga token, yaitu FOR, IN, dan DOTDOT, dengan DOTDOT dikenali lewat maximal munch pada dua titik berturut.
  • Parser menambah produksi for_stmt dan range_expr, dengan node AST For dan Range yang dibangun hanya pada parse_range.
  • Semantik memeriksa tipe rentang, mendeklarasikan variabel loop di scope blok, dan menolak Range di luar for-in.
  • Codegen membangkitkan pola jump klasik, yaitu kondisi, badan, inkrementasi, dan lompat kembali, dengan scope didorong sebelum loop dan dimunculkan setelahnya.
  • VM mendispatch instruksi JMP, JMP_IF_FALSE, PUSH_SCOPE, dan POP_SCOPE, serta menyusuri scope dari terdalam ke terluar untuk pencarian variabel.

Referensi#