Bagaimana bahasa pemrograman mendefinisikan fungsi?

28

Bagaimana bahasa pemrograman mendefinisikan dan menyimpan fungsi / metode? Saya membuat bahasa pemrograman yang ditafsirkan dalam Ruby, dan saya mencoba mencari cara untuk mengimplementasikan deklarasi fungsi.

Ide pertama saya adalah menyimpan konten deklarasi di peta. Misalnya, jika saya melakukan sesuatu seperti

def a() {
    callSomething();
    x += 5;
}

Lalu saya akan menambahkan entri ke peta saya:

{
    'a' => 'callSomething(); x += 5;'
}

Masalahnya adalah ini akan menjadi rekursif, karena saya harus memanggil parsemetode saya pada string, yang kemudian akan memanggil parselagi ketika bertemu doSomething, dan kemudian saya akan kehabisan ruang stack pada akhirnya.

Jadi, bagaimana bahasa yang ditafsirkan menangani ini?

Gagang pintu
sumber
Oh, dan ini posting pertama saya di Programmers.SE, jadi tolong beri tahu saya jika saya melakukan sesuatu yang salah atau ini di luar topik. :)
Doorknob
Di masa lalu saya telah menyimpan semuanya semua dalam token saya dan panggilan fungsi hanya melompat ke offset tertentu (seperti label di Majelis). Apakah Anda tokenizing script? Atau parsing string setiap kali?
Simon Whitehead
@SimonWhitehead Saya membagi string menjadi token, dan kemudian mengurai setiap token secara terpisah.
Gagang pintu
3
Jika Anda baru dalam desain dan implementasi bahasa pemrograman, Anda mungkin ingin memeriksa beberapa literatur tentang subjek tersebut. Yang paling populer adalah "Buku Naga": en.wikipedia.org/wiki/… , tetapi ada teks lain yang lebih ringkas yang juga sangat bagus. Misalnya, Menerapkan Bahasa Pemrograman oleh Aarne Ranta dapat diperoleh secara gratis di sini: bit.ly/15CF6gC .
evilcandybag
1
@ddyer, terima kasih! Saya mencari Google untuk juru bahasa dalam berbagai bahasa dan itu sangat membantu. :)
Gagang Pintu

Jawaban:

31

Apakah saya benar dengan menganggap bahwa fungsi "parse" Anda tidak hanya mem-parsing kode tetapi juga menjalankannya pada saat yang sama? Jika Anda ingin melakukannya dengan cara itu, alih-alih menyimpan konten suatu fungsi di peta Anda, simpan lokasi fungsi tersebut.

Tapi ada cara yang lebih baik. Dibutuhkan sedikit usaha di muka, tetapi menghasilkan hasil yang jauh lebih baik seiring meningkatnya kompleksitas: gunakan Pohon Sintaksis Abstrak.

Ide dasarnya adalah bahwa Anda hanya menguraikan kode sekali, selamanya. Kemudian Anda memiliki satu set tipe data yang mewakili operasi dan nilai, dan Anda membuat pohon dari mereka, seperti:

def a() {
    callSomething();
    x += 5;
}

menjadi:

Function Definition: [
   Name: a
   ParamList: []
   Code:[
      Call Operation: [
         Routine: callSomething
         ParamList: []
      ]
      Increment Operation: [
         Operand: x
         Value: 5
      ]
   ]
]

(Ini hanya representasi teks dari struktur AST hipotetis. Pohon yang sebenarnya mungkin tidak akan dalam bentuk teks.) Bagaimanapun, Anda mengurai kode Anda menjadi AST, dan kemudian Anda menjalankan penerjemah melalui AST secara langsung, atau gunakan pass kedua ("pembuatan kode") untuk mengubah AST menjadi beberapa bentuk output.

Dalam hal bahasa Anda, apa yang mungkin Anda lakukan adalah memiliki peta yang memetakan nama fungsi untuk fungsi AST, alih-alih nama fungsi ke string fungsi.

Mason Wheeler
sumber
Oke, tapi masalahnya masih ada: ia menggunakan rekursi. Saya akan kehabisan ruang stack pada akhirnya jika saya melakukan ini.
Gagang pintu
3
@ Doorknob: Apa yang menggunakan rekursi, khususnya? Setiap bahasa pemrograman blok-terstruktur (yang setiap bahasa modern pada tingkat yang lebih tinggi dari ASM) secara inheren berbasis pohon dan dengan demikian bersifat rekursif. Aspek spesifik apa yang Anda khawatirkan tentang menumpuknya tumpukan?
Mason Wheeler
1
@ Doorknob: Ya, itu properti bawaan dari bahasa apa pun, meskipun itu dikompilasi ke kode mesin. (Call stack adalah manifestasi dari perilaku ini.) Saya sebenarnya adalah kontributor untuk sistem skrip yang bekerja dengan cara yang saya jelaskan. Bergabunglah dengan saya di chat di chat.stackexchange.com/rooms/10470/… dan saya akan membahas beberapa teknik untuk interpretasi yang efisien dan meminimalkan dampak pada ukuran tumpukan dengan Anda. :)
Mason Wheeler
2
@ Doorknob: Tidak ada masalah rekursi di sini karena pemanggilan fungsi di AST mereferensikan fungsi dengan nama , itu tidak perlu referensi ke fungsi yang sebenarnya . Jika Anda mengkompilasi ke kode mesin maka pada akhirnya Anda akan memerlukan alamat fungsi, itulah sebabnya sebagian besar kompiler membuat beberapa lintasan. Jika Anda ingin memiliki kompiler satu pass maka Anda perlu "meneruskan deklarasi" dari semua fungsi sehingga kompiler dapat menetapkan alamat sebelumnya. Kompiler Bytecode bahkan tidak repot-repot dengan ini, jitter menangani pencarian nama.
Aaronaught
5
@ Doorknob: Ini memang rekursif. Dan ya, jika tumpukan Anda hanya memiliki 16 entri, Anda akan gagal menguraikan (((((((((((((((( x ))))))))))))))))). Pada kenyataannya, tumpukan bisa jauh lebih besar, dan kompleksitas tata bahasa kode nyata sangat terbatas. Tentu saja jika kode itu harus dapat dibaca manusia.
MSalters
4

Anda seharusnya tidak menelepon parse setelah melihat callSomething()(saya kira Anda maksudkan callSomethingdaripada doSomething). Perbedaan antara adan callSomethingadalah bahwa yang satu adalah definisi metode sedangkan yang lainnya adalah pemanggilan metode.

Saat Anda melihat definisi baru, Anda ingin melakukan pemeriksaan terkait untuk memastikan Anda dapat menambahkan definisi itu, jadi:

  • Periksa apakah fungsi belum ada dengan tanda tangan yang sama
  • Pastikan bahwa deklarasi metode sedang dilakukan dalam cakupan yang tepat (yaitu, apakah metode dapat dideklarasikan di dalam deklarasi metode lain?)

Dengan asumsi lulus pemeriksaan ini, Anda dapat menambahkannya ke peta Anda dan mulai memeriksa konten metode itu.

Ketika Anda menemukan panggilan metode seperti callSomething(), Anda harus melakukan pemeriksaan berikut:

  • Apakah callSomethingada di peta Anda?
  • Apakah itu disebut dengan benar (jumlah argumen cocok dengan tanda tangan yang Anda temukan)?
  • Apakah argumen valid (jika nama variabel digunakan, apakah dideklarasikan? Dapatkah diakses pada lingkup ini?)?
  • Dapatkah callSomething dipanggil dari tempat Anda berada (apakah privat, publik, terlindungi?)?

Jika Anda menemukan itu callSomething()baik-baik saja, maka pada titik ini apa yang ingin Anda lakukan benar-benar tergantung pada bagaimana Anda ingin mendekatinya. Sebenarnya, setelah Anda tahu bahwa panggilan seperti itu tidak masalah pada saat ini, Anda hanya bisa menyimpan nama metode dan argumen tanpa masuk ke perincian lebih lanjut. Ketika Anda menjalankan program Anda, Anda akan memanggil metode dengan argumen yang harus Anda miliki saat runtime.

Jika Anda ingin melangkah lebih jauh, Anda dapat menyimpan tidak hanya string tetapi tautan ke metode aktual. Ini akan lebih efisien, tetapi jika Anda harus mengelola memori, ini bisa membingungkan. Saya akan merekomendasikan Anda hanya memegang string pada awalnya. Nanti Anda bisa mencoba mengoptimalkan.

Perhatikan bahwa ini semua dengan anggapan bahwa Anda telah lexxed program Anda, yang berarti Anda telah mengenali semua token dalam program Anda dan tahu apa itu . Itu bukan untuk mengatakan Anda tahu apakah mereka masuk akal bersama, yang merupakan fase parsing. Jika Anda belum tahu apa token itu, saya sarankan Anda fokus dulu untuk mendapatkan informasi itu terlebih dahulu.

Saya harap itu membantu! Selamat Datang di Programmer SE!

Neil
sumber
2

Membaca posting Anda, saya perhatikan dua pertanyaan dalam pertanyaan Anda. Yang paling penting adalah bagaimana mengurai. Ada banyak jenis parser (misalnya parser keturunan Recursive , LR Parsers , Packrat Parsers ) dan generator parser (mis. GNU bison , ANTLR ) yang dapat Anda gunakan untuk menelusuri program tekstual "secara rekursif" dengan tata bahasa (eksplisit atau implisit).

Pertanyaan kedua adalah tentang format penyimpanan untuk berbagai fungsi. Ketika Anda tidak melakukan terjemahan diarahkan-sintaks , Anda membuat representasi perantara dari program Anda, yang dapat berupa pohon sintaksis abstrak , atau beberapa bahasa perantara kustom, untuk melakukan pemrosesan lebih lanjut dengan itu (mengkompilasi, mengubah, mengeksekusi, menulis di file, dll).

Thiago Silva
sumber
1

Dari sudut pandang generik, definisi fungsi sedikit lebih dari label, atau bookmark, dalam kode. Kebanyakan operator loop, scope, dan conditional lainnya serupa; mereka berdiri untuk perintah "lompat" atau "goto" dasar di tingkat abstraksi yang lebih rendah. Panggilan fungsi pada dasarnya bermuara pada perintah komputer tingkat rendah berikut:

  • Menggabungkan data semua parameter, ditambah pointer ke instruksi selanjutnya dari fungsi saat ini, ke dalam struktur yang dikenal sebagai "bingkai panggilan stack".
  • Dorong bingkai ini ke tumpukan panggilan.
  • Beralih ke offset memori baris pertama kode fungsi.

Pernyataan "kembali" atau serupa akan melakukan hal berikut:

  • Muat nilai yang akan dikembalikan ke register.
  • Muat pointer ke pemanggil ke register.
  • Pop frame stack saat ini.
  • Langsung ke penunjuk penelepon.

Fungsi, oleh karena itu, hanyalah abstraksi dalam spesifikasi bahasa tingkat tinggi, yang memungkinkan manusia untuk mengatur kode dengan cara yang lebih terpelihara dan intuitif. Ketika dikompilasi menjadi bahasa assembly atau intermediate (JIL, MSIL, ILX), dan pasti ketika diterjemahkan sebagai kode mesin, hampir semua abstraksi semacam itu hilang.

KeithS
sumber