Topik ini memperkenalkan struktur runtime untuk pemanggilan fungsi, yaitu CallFrame sebagai snapshot pemanggilan dan CallStack sebagai wadah frame. Pembaca memperoleh kelas CallFrame yang menyimpan argumen, variabel lokal, return address, dan dynamic link ke frame pemanggil, kelas CallStack dengan operasi push dan pop, serta demo pemanggilan rekursif factorial(3) yang menampilkan kondisi stack pada setiap level. Implementasi pada topik ini berdiri sendiri dan tidak terintegrasi dengan StackVM pada modul 5, sebab bahasa mini v1 memang belum memuat fungsi. Tujuannya adalah menunjukkan struktur data yang dibutuhkan runtime ketika pemanggilan fungsi ditambahkan. Setelah membaca topik ini, pembaca dapat menjelaskan isi sebuah frame, menjelaskan perbedaan antara dynamic link dan static link, dan menjelaskan bagaimana nilai balik dikembalikan ke pemanggil.
Frame sebagai snapshot pemanggilan#
Setiap pemanggilan fungsi membentuk satu CallFrame yang menyimpan lima hal. Pertama, nama fungsi yang dipanggil, digunakan untuk pelacakan dan pesan error. Kedua, daftar argumen yang diterima, yaitu nilai yang sudah dievaluasi pada pemanggil dan diikat ke parameter pada frame. Ketiga, variabel lokal yang dibangun selama eksekusi badan fungsi. Keempat, return address, yaitu lokasi instruksi tempat kontrol kembali setelah fungsi selesai. Kelima, dynamic link, yaitu rujukan ke frame pemanggil, sehingga setelah fungsi selesai, frame aktif dapat dikembalikan ke frame yang berada di bawahnya pada stack.
Return address perlu disimpan secara eksplisit karena satu fungsi dapat dipanggil dari banyak titik, sehingga setiap pemanggilan perlu mencatat titik kembalinya sendiri. Pada kode mesin nyata, return address biasanya berisi alamat instruksi setelah instruksi call. Pada interpreter berbasis indeks seperti StackVM pada modul 5, return address berupa indeks instruksi pada bytecode pemanggil. Tanpa penyimpanan ini, setelah fungsi selesai kontrol tidak tahu harus melanjutkan dari mana, sehingga pemanggilan rekursif menjadi mustahil. Penyimpanan return address inilah yang menjadikan pemanggilan dapat berlapis, sebab setiap frame menyimpan titik kembali masing-masing.
Dynamic link berbeda dari static link, meskipun keduanya berbentuk rujukan ke frame lain. Dynamic link menunjuk ke frame pemanggil, yaitu frame yang berada di bawah frame saat ini pada call stack. Static link, bila digunakan, menunjuk ke frame yang berisi lingkungan leksikal dari fungsi yang sedang berjalan, yaitu scope tempat fungsi itu didefinisikan. Pada bahasa dengan fungsi tingkat atas dan tanpa closure bersarang, dynamic link dan static link sering kali menunjuk ke frame yang sama. Pada bahasa dengan closure bersarang, static link mungkin menunjuk ke frame yang berada bukan tepat di bawah, sehingga keduanya perlu dibedakan secara eksplisit.
Call stack dan pertumbuhan frame#
CallStack adalah wadah frame dengan disiplin LIFO, yaitu frame terakhir yang dimasukkan adalah pertama yang dikeluarkan. Operasi push menambahkan satu frame sebagai frame aktif, sedangkan pop menghapus frame aktif dan mengembalikan frame pemanggil sebagai aktif. Pada setiap saat, hanya frame puncak yang sedang dieksekusi, sedangkan frame di bawahnya menunggu hingga pemanggilan di atasnya selesai. Kedalaman stack pada saat tertentu menyatakan jumlah pemanggilan yang sedang berjalan, dan kedalaman inilah yang digunakan untuk membatasi rekursi tak terhingga pada implementasi produksi.
Pada pemanggilan rekursif, stack tumbuh sebelum menyusut. Setiap pemanggilan rekursif menambahkan satu frame, sehingga kedalaman stack sama dengan jumlah pemanggilan yang sedang aktif. Setelah kasus dasar tercapai, frame puncak mulai di-pop satu per satu, dan nilai balik dihitung dengan menggabungkan hasil dari pemanggilan di bawahnya. Pada demo factorial(3), stack tumbuh hingga kedalaman tiga saat factorial(1) dijalankan, lalu menyusut kembali ke nol setelah seluruh pemanggilan selesai. Pola pertumbuhan dan penyusutan ini disebut stack discipline, dan merupakan alasan mengapa pemanggilan rekursif yang tidak memiliki kasus dasar akan menghabiskan memori stack.
Implementasi referensi#
Berkas call_frame.py berisi kelas CallFrame, kelas CallStack, dan fungsi factorial yang menggunakan keduanya secara eksplisit. Fungsi factorial membangun frame baru pada setiap pemanggilan, memasukkannya ke stack, lalu melakukan rekursi atau mengembalikan nilai dasar, lalu memutakhirkan locals["result"] sebelum mem-pop frame. Penempatan result pada locals sebelum pop penting karena setelah pop, frame tidak lagi menjadi frame aktif, sehingga penulisan setelah pop akan menulis ke frame yang salah.
"""Frame pemanggilan pada call stack.
Topik ini memperkenalkan struktur runtime untuk pemanggilan fungsi,
yaitu CallFrame sebagai snapshot pemanggilan dan CallStack sebagai
wadah frame. Implementasi ini berdiri sendiri dan tidak terintegrasi
dengan VM modul 5.
Setiap CallFrame menyimpan:
- function: nama fungsi yang sedang dipanggil
- args: daftar argumen yang diterima
- locals: variabel lokal pada frame ini
- return_address: indeks instruksi tempat kontrol kembali
- dynamic_link: rujukan ke frame pemanggil (caller)
Demo menjalankan factorial(3) dengan rekursi sederhana, lalu
menampilkan kondisi call stack pada setiap level.
"""
from typing import Any
class CallFrame:
"""Snapshot satu pemanggilan fungsi."""
def __init__(self, function: str, args: list[Any],
return_address: int, dynamic_link: "CallFrame | None") -> None:
self.function = function
self.args = args
self.locals: dict[str, Any] = {}
self.return_address = return_address
self.dynamic_link = dynamic_link
def describe(self) -> str:
args_str = ", ".join(repr(a) for a in self.args)
locals_str = ", ".join(f"{k}={v!r}" for k, v in self.locals.items())
return (f"Frame[{self.function}] args=({args_str}) "
f"locals={{{locals_str}}} ret={self.return_address}")
class CallStack:
"""Call stack dengan push/pop frame."""
def __init__(self) -> None:
self.frames: list[CallFrame] = []
def push(self, frame: CallFrame) -> None:
self.frames.append(frame)
def pop(self) -> CallFrame:
return self.frames.pop()
def top(self) -> CallFrame:
return self.frames[-1]
def depth(self) -> int:
return len(self.frames)
def dump(self) -> list[str]:
return [f" [{i}] {f.describe()}" for i, f in enumerate(self.frames)]
def factorial(stack: CallStack, n: int, return_address: int) -> int:
"""Implementasi factorial yang menggunakan call stack eksplisit.
Setiap pemanggilan menampilkan kondisi stack sehingga pembaca dapat
melihat pertumbuhan frame pada rekursi.
"""
frame = CallFrame("factorial", [n], return_address, dynamic_link=stack.top() if stack.depth() else None)
stack.push(frame)
print(f"-- masuk factorial({n}), kedalaman stack = {stack.depth()}")
for line in stack.dump():
print(line)
if n <= 1:
result = 1
else:
# panggilan rekursif: kembali ke "baris 100" setelah selesai
sub = factorial(stack, n - 1, return_address=100)
result = n * sub
frame.locals["result"] = result
print(f"-- keluar factorial({n}) = {result}, kembali ke ret={frame.return_address}")
stack.pop()
return result
def main() -> None:
print("=== Pemanggilan factorial(3) ===")
stack = CallStack()
hasil = factorial(stack, n=3, return_address=0)
print()
print(f"hasil akhir = {hasil}")
print(f"kedalaman stack akhir = {stack.depth()} (harus 0)")
if __name__ == "__main__":
main()Demo memanggil factorial(3) dari titik masuk dengan return_address=0. Pemanggilan rekursif di dalam badan fungsi menggunakan return_address=100, sehingga keluaran memperlihatkan perbedaan titik kembali antara panggilan terluar dan panggilan rekursif. Kondisi stack ditampilkan pada saat masuk setiap pemanggilan, dan pesan keluar menampilkan nilai balik serta titik kembali. Keluaran berikut diperoleh dengan menjalankan python3 call_frame.py.
=== Pemanggilan factorial(3) ===
-- masuk factorial(3), kedalaman stack = 1
[0] Frame[factorial] args=(3) locals={} ret=0
-- masuk factorial(2), kedalaman stack = 2
[0] Frame[factorial] args=(3) locals={} ret=0
[1] Frame[factorial] args=(2) locals={} ret=100
-- masuk factorial(1), kedalaman stack = 3
[0] Frame[factorial] args=(3) locals={} ret=0
[1] Frame[factorial] args=(2) locals={} ret=100
[2] Frame[factorial] args=(1) locals={} ret=100
-- keluar factorial(1) = 1, kembali ke ret=100
-- keluar factorial(2) = 2, kembali ke ret=100
-- keluar factorial(3) = 6, kembali ke ret=0
hasil akhir = 6
kedalaman stack akhir = 0 (harus 0)Keluaran memperlihatkan tiga sifat call stack. Pertama, kedalaman stack naik dari satu ke tiga saat rekursi bertumpuk, lalu turun kembali ke nol setelah seluruh pemanggilan selesai. Kedua, return_address pada frame terluar adalah 0, sedangkan frame rekursif menggunakan 100, sehingga perbedaan titik kembali terlihat secara langsung. Ketiga, nilai balik akhir adalah 6, yaitu 3 * 2 * 1, dan stack kembali kosong, sesuai dengan semantik pemanggilan rekursif yang sehat.
Kesalahan umum#
Kesalahan paling sering adalah menulis ke frame setelah pop. Setelah frame di-pop, frame aktif adalah frame pemanggil, sehingga penugasan ke locals akan menulis ke frame pemanggil, bukan ke frame yang baru saja selesai. Penyebabnya biasanya adalah urutan pop sebelum locals["result"] = result. Perbaikannya adalah memutakhirkan locals sebelum pop, atau menyimpan nilai balik pada variabel lokal Python sebelum pop.
Kesalahan kedua adalah lupa menyimpan return address. Bila pemanggilan rekursif tidak mencatat titik kembali, semua pemanggilan akan kembali ke titik yang sama, sehingga eksekusi setelah rekursi menjadi kacau. Perbaikannya adalah selalu mengisi return_address pada saat CallFrame dibangun, dan menempatkannya sebagai parameter wajib, bukan sebagai field yang diisi setelahnya.
Ringkasan#
- CallFrame menyimpan lima hal, yaitu nama fungsi, argumen, variabel lokal, return address, dan dynamic link ke frame pemanggil.
- Dynamic link menunjuk ke frame pemanggil pada call stack, sedangkan static link menunjuk ke frame lingkungan leksikal. Keduanya berbeda pada bahasa dengan closure bersarang.
- CallStack bersifat LIFO. Pada rekursi, stack tumbuh hingga kasus dasar tercapai, lalu menyusut saat setiap pemanggilan mengembalikan nilai balik.
- Nilai balik harus ditulis ke
localssebelumpop, sebab setelahpopframe aktif adalah frame pemanggil.
Referensi#
- Call stack — Wikipedia — Struktur call stack, peran frame, dan perbedaan dynamic link dengan static link.
- Crafting Interpreters — Robert Nystrom — Bab 25 membahas pemanggilan fungsi pada VM berbasis bytecode.
- Stack-based memory allocation — Wikipedia — Penjelasan alokasi memori otomatis berbasis stack dan keterkaitannya dengan call frame.