Mesin byte untuk benteng (dan tidak hanya) di Native American (bagian 4)

Fort Byte Car (dan banyak lagi) Penduduk Asli Amerika

Dan lagi saya melebih-lebihkan volume artikel! Saya merencanakan bahwa ini akan menjadi artikel terakhir, di mana kami akan membuat kompiler dan melakukan pengujian. Tetapi volumenya ternyata besar, dan saya memutuskan untuk membagi artikel menjadi dua.

Pada artikel ini, kita akan melakukan hampir semua fungsi dasar dari kompiler. Ini akan hidup kembali, dan dimungkinkan untuk menulis, menyusun, dan mengeksekusi kode yang cukup serius. Dan kami akan melakukan pengujian di bagian selanjutnya. (Omong-omong, bagian sebelumnya: satu , dua , tiga ).

Saya menulis untuk pertama kalinya di HabrΓ©, mungkin itu tidak selalu baik-baik saja. Menurut pendapat saya, artikel 2, 3 ternyata agak kering, banyak kode, sedikit deskripsi. Kali ini saya akan mencoba melakukan sesuatu yang berbeda, fokus pada deskripsi ide-ide itu sendiri. Nah, kodenya ... kodenya, tentu saja akan! Siapa yang ingin mengerti secara menyeluruh, kesempatan seperti itu akan terjadi. Dalam banyak kasus, saya akan meletakkan kode di bawah spoiler. Dan, tentu saja, Anda selalu dapat melihat sumber lengkap di github.

Kompiler akan terus menulis untuk beberapa waktu di assembler, tetapi kemudian pergi ke benteng dan terus menulis kompilator pada diri kita sendiri. Ini akan menyerupai Baron Munchausen, yang menarik rambutnya dari rawa. Tapi, sebagai permulaan, saya akan menguraikan bagaimana kompiler di benteng bekerja. Selamat datang di kucing!

Bagaimana cara kerja kompiler?


Memori di benteng terdiri dari sebuah fragmen yang berkelanjutan di mana entri kamus disusun secara berurutan. Setelah selesai mereka diikuti oleh area memori bebas. Byte gratis pertama ditunjukkan oleh variabel h. Ada juga kata yang sering digunakan di sini, yang mendorong alamat byte bebas pertama pada stack, ditentukan dengan sangat sederhana:

: here h @ ; 



Perlu disebutkan kata allot, yang menyimpan jumlah byte yang ditentukan dengan menggerakkan pointer h. Kata allot dapat didefinisikan sebagai berikut:

 : allot h +! ; 

Bahkan, kompiler menggunakan mode juru bahasa khusus ditambah beberapa kata-kata khusus. Jadi, dengan satu kalimat, Anda bisa menggambarkan seluruh prinsip penyusun di benteng. Mode apa yang digunakan penerjemah ditentukan oleh variabel status. Jika nol, maka mode eksekusi diatur, jika tidak - mode kompilasi. Kita sudah terbiasa dengan mode eksekusi, di dalamnya kata-kata dari buffer input dieksekusi satu demi satu. Tetapi dalam mode kompilasi mereka tidak dieksekusi, tetapi dikompilasi ke dalam memori oleh pointer h. Dengan demikian, penunjuk bergerak maju.

Di benteng klasik, kata "," digunakan untuk mengkompilasi nilai integer, kata "c," digunakan untuk mengkompilasi byte. Sistem kami menggunakan nilai kedalaman bit yang berbeda (8, 16, 32, 64), oleh karena itu, kami juga akan membuat kata "w," dan "i,". Kami juga membuat kata "str," yang akan mengkompilasi string, mengambil dua nilai dari stack - alamat dan panjang string.

Kata-kata kompiler khusus digunakan untuk membentuk struktur kontrol. Ini adalah kata-kata jika, kemudian, lakukan, loop, dan lainnya. Kata-kata ini dieksekusi bahkan dalam mode kompilasi. Misalnya, kata jika mengkompilasi perintah byte cabang bersyarat (? Nbranch) saat eksekusi. Agar sistem tahu kata-kata apa yang perlu dieksekusi dalam mode kompilasi, dan tidak dikompilasi, flag langsung (tanda) digunakan. Kami sudah memilikinya di bidang bendera entri kamus. Dalam kode sumber assembler, itu disebut f_immediate. Untuk mengatur tanda ini, gunakan kata langsung. Tidak memiliki parameter, bendera langsung ditetapkan pada kata terakhir dalam kamus.

Sekarang mari kita beralih dari teori ke praktek!

Persiapan


Pada awalnya, kita perlu melakukan beberapa perintah byte sederhana dalam bahasa assembly yang kita butuhkan. Inilah mereka: memindahkan (menyalin area memori), mengisi (mengisi area memori), operasi bit (dan, atau, xor, membalikkan), perintah bit shift (rshift, lshift). Mari kita lakukan rpick yang sama (ini sama dengan pick, ini hanya bekerja dengan stack kembali, bukan tumpukan data).

Perintah-perintah ini sangat sederhana, ini adalah kodenya
 b_move = 0x66 bcmd_move: pop rcx pop rdi pop rsi repz movsb jmp _next b_fill = 0x67 bcmd_fill: pop rax pop rcx pop rdi repz stosb jmp _next b_rpick = 0x63 bcmd_rpick: pop rcx push [rbp + rcx * 8] jmp _next b_and = 0x58 bcmd_and: pop rax and [rsp], rax jmp _next b_or = 0x59 bcmd_or: pop rax or [rsp], rax jmp _next b_xor = 0x5A bcmd_xor: pop rax xor [rsp], rax jmp _next b_invert = 0x5B bcmd_invert: notq [rsp] jmp _next b_rshift = 0x5C bcmd_rshift: pop rcx or rcx, rcx jz _next 1: shrq [rsp] dec rcx jnz 1b jmp _next b_lshift = 0x5D bcmd_lshift: pop rcx or rcx, rcx jz _next 1: shlq [rsp] dec rcx jnz 1b jmp _next 

Masih perlu membuat kata kata. Ini sama dengan blword, tetapi pembatas khusus ditunjukkan pada stack. Saya tidak memberikan kode, itu dapat ditemukan di sumbernya. Saya membuat copy / paste kata-kata blworld dan mengganti perintah perbandingan.

Sebagai kesimpulan, kami membuat kata syscall. Dengan itu, akan dimungkinkan untuk melakukan operasi sistem yang hilang, misalnya, bekerja dengan file. Solusi semacam itu tidak akan berfungsi jika independensi platform diperlukan. Tetapi sistem ini sekarang digunakan untuk pengujian, jadi biarkanlah untuk sekarang. Jika perlu, semua operasi dapat dikonversi ke perintah byte, sama sekali tidak sulit. Perintah syscall akan menerima 6 parameter untuk panggilan sistem dan nomor panggilan dari tumpukan. Ini akan mengembalikan satu parameter. Penugasan parameter dan nilai pengembalian ditentukan oleh nomor panggilan sistem.

 b_syscall = 0xFF bcmd_syscall: sub rbp, 8 mov [rbp], r8 pop rax pop r9 pop r8 pop r10 pop rdx pop rsi pop rdi syscall push rax mov r8, [rbp] add rbp, 8 jmp _next 

Dan sekarang mari kita lanjutkan langsung ke kompiler.

Kompiler


Mari kita buat variabel h, semuanya sederhana di sini.

  item h h: .byte b_var0 .quad 0 
Kami akan menulis inisialisasi di baris awal:
 # forth last_item context @ ! h dup 8 + swap ! quit start: .byte b_call16 .word forth - . - 2 .byte b_call16 .word last_item - . - 2 .byte b_call16 .word context - . - 2 .byte b_get .byte b_set .byte b_call16 .word h - . - 2 .byte b_dup, b_num8, b_add, b_swap, b_set .byte b_quit 

Mari kita buat kata di sini:

  item here .byte b_call8, h - . - 1 .byte b_get .byte b_exit 

Dan juga kata-kata untuk menyusun nilai: "membagikan" dan "c,", "w,", "i,", ",", "str,"
 # : allot h +! ; item allot allot: .byte b_call8, h - . - 1, b_setp, b_exit # : , here ! 8 allot ; item "," .byte b_call8, here - . - 1, b_set, b_num8, b_call8, allot - . - 1, b_exit # : i, here i! 4 allot ; item "i," .byte b_call8, here - . - 1, b_set32, b_num4, b_call8, allot - . - 1, b_exit # : w, here w! 2 allot ; item "w," .byte b_call8, here - . - 1, b_set16, b_num2, b_call8, allot - . - 1, b_exit # : c, here c! 1 allot ; item "c," .byte b_call8, here - . - 1, b_set8, b_num1, b_call8, allot - . - 1, b_exit # : str, dup -rot dup c, here swap move 1+ h +!; item "str," c_str: .byte b_dup, b_mrot, b_dup callb c_8 callb here .byte b_swap, b_move callb h .byte b_setp .byte b_exit 

Sekarang mari kita membuat variabel keadaan dan dua kata untuk mengontrol nilainya: "[" dan "]". Biasanya kata-kata ini digunakan untuk melakukan sesuatu pada saat kompilasi. Karenanya, kata "[" mematikan mode kompilasi, dan kata "]" menyalakannya. Tetapi tidak ada yang mencegah mereka digunakan dalam kasus lain ketika perlu untuk mengaktifkan atau menonaktifkan mode kompilasi. Kata "[" akan menjadi kata pertama kami dengan tanda langsung. Jika tidak, ia tidak akan dapat mematikan mode kompilasi, karena akan dikompilasi, tidak dieksekusi.

  item state .byte b_var0 .quad 0 item "]" .byte b_num1 callb state .byte b_set, b_exit item "[", f_immediate .byte b_num0 callb state .byte b_set, b_exit 

Gilirannya datang untuk kata $ compile. Ini akan mengambil alamat entri kamus dari tumpukan dan mengkompilasi kata yang ditentukan. Untuk mengkompilasi kata dalam implementasi Fort biasa, cukup untuk menerapkan kata "," ke alamat eksekusi. Semuanya jauh lebih rumit di sini. Pertama, ada dua jenis kata - bytecode dan kode mesin. Yang pertama dikompilasi oleh byte, dan yang terakhir oleh perintah byte panggilan. Dan kedua - kami memiliki sebanyak empat varian dari perintah panggilan: call8, call16, call32 dan call64. Empat? Tidak! Ketika saya menulis kompiler, saya menambahkan 16 lagi ke empat ini! :)

Bagaimana ini bisa terjadi? Kita harus melakukan penyimpangan kecil.

Meningkatkan perintah panggilan


Ketika kompiler mulai bekerja, saya menemukan bahwa dalam banyak kasus (tetapi tidak semua) perintah call8 sudah cukup. Ini adalah ketika kata yang dipanggil adalah dalam 128 byte. Saya pikir - dan bagaimana memastikan bahwa ini terjadi di hampir semua kasus? Bagaimana cara menempatkan lebih dari 256 nilai dalam satu byte?
Poin pertama yang saya perhatikan adalah bahwa di benteng panggilan selalu menuju ke alamat yang lebih rendah. Ini berarti bahwa Anda dapat mengulang perintah panggilan sedemikian rupa sehingga hanya dapat memanggil alamat yang lebih rendah, tetapi untuk 256 byte, bukan 128. Itu lebih baik.

Tetapi jika Anda menaruh beberapa bit di suatu tempat ... Ternyata ada di mana! Kami memiliki dua byte: satu byte adalah perintah, yang kedua adalah offset. Tetapi tidak ada yang mencegah bit perintah yang lebih rendah dari menempatkan bit parameter yang tinggi (offset). Untuk mesin byte, sepertinya bukan satu perintah panggilan, ada beberapa. Ya, dengan cara ini kita menempati beberapa sel tabel kode byte-perintah dengan satu perintah, tetapi kadang-kadang ada baiknya melakukannya. Perintah panggilan adalah salah satu perintah yang paling sering digunakan, jadi saya memutuskan untuk memasukkan 4 bit offset dalam perintah. Dengan demikian, Anda bisa melakukan panggilan pada jarak 4.095 byte! Ini berarti bahwa perintah panggilan singkat seperti itu akan digunakan hampir selalu. Saya menempatkan perintah ini dengan kode 0xA0 dan baris berikut muncul di tabel perintah:

 .quad bcmd_call8b0, bcmd_call8b1, bcmd_call8b2, bcmd_call8b3, bcmd_call8b4, bcmd_call8b5, bcmd_call8b6, bcmd_call8b7 # 0xA0 .quad bcmd_call8b8, bcmd_call8b9, bcmd_call8b10, bcmd_call8b11, bcmd_call8b12, bcmd_call8b13, bcmd_call8b14, bcmd_call8b15 

Perintah byte pertama ini hanya membuat panggilan ke arah alamat yang lebih rendah pada offset yang ditentukan dalam parameter (hingga 255). Sisanya menambahkan offset yang sesuai ke parameter. bcmd_call8b1 menambahkan 256, bcmd_call8b2 menambahkan 512, dan seterusnya. Saya membuat perintah panggilan pertama secara terpisah, sisanya dengan makro.

Perintah pertama:

 b_call8b0 = 0xA0 bcmd_call8b0: movzx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 sub r8, rax jmp _next 

Makro dan membuat sisa perintah panggilan

 .macro call8b N b_call8b\N = 0xA\N bcmd_call8b\N: movzx rax, byte ptr [r8] sub rbp, 8 inc r8 add rax, \N * 256 mov [rbp], r8 sub r8, rax jmp _next .endm call8b 1 call8b 2 call8b 3 call8b 4 call8b 5 call8b 6 call8b 7 call8b 8 call8b 9 call8b 10 call8b 11 call8b 12 call8b 13 call8b 14 call8b 15 

Yah, saya redid perintah call8 lama untuk memanggil maju, karena kita sudah memiliki 16 tim membuat panggilan kembali. Apa pun kebingungannya, saya menamainya b_call8f:

 b_call8f = 0x0C bcmd_call8f: movzx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 add r8, rax jmp _next 

Ngomong-ngomong, untuk kenyamanan, saya membuat makro yang di assembler secara otomatis mengkompilasi panggilan yang terkait kembali dalam 4095. Dan kemudian saya tidak pernah perlu :)

 .macro callb adr .if \adr > . .error "callb do not for forward!" .endif .byte b_call8b0 + (. - \adr + 1) >> 8 .byte (. - \adr + 1) & 255 .endm 

Dan sekarang ...

Kompilasi tim


Jadi, kami mendapatkan algoritma kompilasi perintah yang agak rumit. Jika ini adalah perintah byte, kompilasi hanya byte (kode perintah byte). Dan jika kata ini sudah ditulis dalam bytecode, Anda perlu mengkompilasi panggilannya dengan perintah panggilan, memilih salah satu dari dua puluh. Lebih tepatnya 19, jadi kami tidak memiliki panggilan ke depan, dan call8f tidak akan digunakan untuk benteng.

Jadi pilihannya adalah ini. Jika offset terletak dalam 0 ...- 4095, pilih perintah bcmd_call8b dengan kode 0xA0, menempatkan empat bit offset paling signifikan dalam bit perintah yang paling tidak signifikan. Pada saat yang sama, untuk mesin byte, kode untuk salah satu dari perintah bcmd_call8b0 adalah bcmd_call8b15.

Jika offset mundur lebih besar dari atau sama dengan 4095, maka kami menentukan dimensi offset mana yang ditempatkan dan menggunakan perintah yang sesuai dari call16 / 32/64. Harus diingat bahwa offset untuk tim-tim ini ditandatangani. Mereka dapat menyebabkan maju dan mundur. Misalnya, panggilan16 dapat memanggil jarak 32767 di kedua arah.

Inilah implementasinya sebagai hasilnya:

$ kompilasi

Kompilasi sebuah kata. Sebagai parameter, ambil alamat entri kamus dari kata yang dikompilasi. Bahkan, ia memeriksa flag f_code, menghitung alamat kode (cfa), dan memanggil compile_b atau compile_c (jika flag diatur).

compile_c

Mengkompilasi perintah byte. Kata paling sederhana di sini dijelaskan di benteng seperti ini:

 : compile_c c@ c, ; 

compile_b
Dibutuhkan alamat bytecode pada stack dan mengkompilasi panggilannya.

test_bv

Dibutuhkan offset dari tumpukan (dengan tanda) dan menentukan kedalaman bit mana yang digunakan (1, 2, 4, atau 8 byte). Mengembalikan nilai 0, 1, 2, atau 3. Dengan menggunakan kata ini, Anda dapat menentukan mana yang akan digunakan dari perintah call16 / 32/64. Kata ini akan berguna ketika menyusun angka (pilihan dari lit8 / 16/32/64).

Omong-omong, Anda dapat memulai sistem dan "bermain-main" di konsol benteng dengan kata-kata ini. Sebagai contoh:

 $ ./forth ( 0 ): > 222 test_bv ( 2 ): 222 1 > drop drop ( 0 ): > 1000000 test_bv ( 2 ): 1000000 2 > drop drop ( 0 ): > -33 test_bv ( 2 ): -33 0 > 

test_bvc

Dibutuhkan offset (dengan tanda) dari tumpukan dan menentukan perintah panggilan mana yang digunakan. Bahkan, ia memeriksa untuk melihat apakah offset terletak dalam 0 ... -4095, dan mengembalikan 0. Dalam hal ini, jika tidak ada hit dalam interval ini, ia memanggil test_bv.

Hanya itu yang diperlukan untuk mengkompilasi perintah.
 # : test_bvc dup 0 >= over FFF <= and if 0 exit else ... item test_bvc test_bvc: .byte b_dup, b_neg .byte b_num0 .byte b_gteq .byte b_over, b_neg .byte b_lit16 .word 0xFFF .byte b_lteq .byte b_and .byte b_qnbranch8, 1f - . .byte b_num0 .byte b_exit item test_bv test_bv: .byte b_dup, b_lit8, 0x80, b_gteq, b_over, b_lit8, 0x7f, b_lteq, b_and, b_qnbranch8, 1f - ., b_num0 .byte b_exit 1: .byte b_dup .byte b_lit16 .word 0x8001 .byte b_gteq .byte b_over .byte b_lit16 .word 0x7ffe .byte b_lteq, b_and, b_qnbranch8, 2f - ., b_num1, b_exit 2: .byte b_dup .byte b_lit32 .int 0x80000002 .byte b_gteq .byte b_over .byte b_lit32 .int 0x7ffffffd .byte b_lteq, b_and, b_qnbranch8, 3f - ., b_num2, b_exit 3: .byte b_num3 .byte b_exit #  - item compile_c compile_c: .byte b_get8 callb c_8 .byte b_exit #   - item compile_b compile_b: callb here .byte b_num2, b_add .byte b_sub callb test_bvc .byte b_dup .byte b_zeq .byte b_qnbranch8, 1f - . .byte b_drop .byte b_neg .byte b_dup .byte b_lit8, 8 .byte b_rshift .byte b_lit8, b_call8b0 .byte b_or callb c_8 callb c_8 .byte b_exit 1: .byte b_dup, b_num1, b_eq, b_qnbranch8, 2f - ., b_drop, b_lit8, b_call16 callb c_8 .byte b_wm callb c_16 .byte b_exit 2: .byte b_num2, b_eq, b_qnbranch8, 3f - ., b_lit8, b_call32 callb c_8 .byte b_num3, b_sub callb c_32 .byte b_exit 3: .byte b_lit8, b_call64 callb c_8 .byte b_lit8, 7, b_sub callb c_64 .byte b_exit #: $compile dup c@ 0x80 and if cfa compile_c else cfa compile_b then ; item "$compile" _compile: .byte b_dup, b_get8, b_lit8, 0x80, b_and, b_qnbranch8, 1f - ., b_cfa callb compile_c .byte b_exit 1: .byte b_cfa callb compile_b .byte b_exit 


Sekarang kita perlu mengkompilasi nomornya.

Menyusun angka (literal)


Menulis seluruh subtitle, siap untuk secara khusus menggambarkan kompilasi dari literal, tetapi ternyata tidak ada yang istimewa untuk dijelaskan :)

Kami telah melakukan separuh pekerjaan dalam kata test_bv. Tetap hanya untuk memanggil test_bv, dan, tergantung pada hasilnya, kompilasi lit8 / 16/32/64, dan kemudian nilai yang sesuai dari ukuran 1, 2, 4 atau 8 byte.

Kami melakukan ini dengan mendefinisikan kata compile_n
 #   item compile_n compile_n: callb test_bv .byte b_dup .byte b_zeq .byte b_qnbranch8, 1f - . .byte b_drop, b_lit8, b_lit8 callb c_8 callb c_8 .byte b_exit 1: .byte b_dup, b_num1, b_eq, b_qnbranch8, 2f - ., b_drop, b_lit8, b_lit16 callb c_8 callb c_16 .byte b_exit 2: .byte b_num2, b_eq, b_qnbranch8, 3f - ., b_lit8, b_lit32 callb c_8 callb c_32 .byte b_exit 3: .byte b_lit8, b_lit64 callb c_8 callb c_64 .byte b_exit 

Ubah penerjemah


Semuanya siap mengkompilasi perintah dan literal. Sekarang perlu dibangun ke dalam interpreter. Modifikasi ini sederhana. Di mana perintah dieksekusi, tambahkan cek negara. Jika status bukan nol dan kata itu tidak berisi flag langsung, alih-alih eksekusi Anda harus memanggil $ compile. Dan tentang hal yang sama dilakukan di mana nomor tersebut diperoleh dari input stream. Jika statusnya nol, tinggalkan saja nomornya di tumpukan, dan jika tidak, panggil compile_n.

Ini penerjemahnya
  item interpret interpret: .byte b_blword .byte b_dup .byte b_qnbranch8 .byte 0f - . .byte b_over .byte b_over .byte b_find .byte b_dup .byte b_qnbranch8 .byte 1f - . .byte b_mrot .byte b_drop .byte b_drop callb state .byte b_get .byte b_qnbranch8, irpt_execute - . #  0,    .byte b_dup, b_get8, b_lit8, f_immediate, b_and #  immediate    .byte b_qbranch8, irpt_execute - . #    -   #   ! callb _compile .byte b_branch8, 2f - . irpt_execute: .byte b_cfa #  ,    (state = 0  immediate  ) .byte b_execute .byte b_branch8, 2f - . 1: .byte b_drop .byte b_over, b_over .byte b_numberq # ,    .byte b_qbranch8, 3f - . #     0, ,      3 .byte b_type #    .byte b_strp #   .byte 19 #     .ascii " : word not found!\n" .byte b_quit #    3: .byte b_nip, b_nip #  ,     ( b_over, b_over) #   -   callb state # ,    .byte b_get .byte b_qnbranch8, 2f - . #   -     ;   -   #   callb compile_n 2: #       .byte b_depth #    .byte b_zlt # ,   0 ( 0<) .byte b_qnbranch8, interpret_ok - . #   ,    ,   .byte b_strp #    .byte 14 .ascii "\nstack fault!\n" .byte b_quit #    interpret_ok: .byte b_branch8 .byte interpret - . 0: .byte b_drop .byte b_exit 

Sekarang kita selangkah lagi dari kompiler ...

Definisi kata-kata baru (kata ":")


Sekarang, jika kita mengatur variabel status ke nilai bukan nol, proses kompilasi akan dimulai. Tetapi hasilnya akan sia-sia, kita tidak bisa memenuhinya, atau bahkan menemukannya di memori. Untuk memungkinkannya melakukan semua ini, perlu untuk memformat hasil kompilasi dalam bentuk artikel kamus. Untuk melakukan ini, sebelum mengaktifkan mode kompilasi, Anda perlu membuat judul untuk kata tersebut.

Header harus berisi bendera, bidang komunikasi, dan nama. Di sini kita memiliki kisah yang akrab - bidang komunikasi dapat 1, 2, 4, atau 8 byte. Mari kita buat kata compile_1248, yang akan membantu kita membentuk bidang komunikasi semacam itu. Dibutuhkan dua angka pada stack - offset dan nilai yang dihasilkan oleh perintah test_bv.

compile_1248
 #    , ,     #     ,  test_dv item compile_1248 compile_1248: .byte b_dup .byte b_zeq .byte b_qnbranch8, 1f - . .byte b_drop callb c_8 .byte b_exit 1: .byte b_dup, b_num1, b_eq, b_qnbranch8, 2f - . .byte b_drop callb c_16 .byte b_exit 2: .byte b_num2, b_eq, b_qnbranch8, 3f - . callb c_32 .byte b_exit 3: callb c_64 .byte b_exit 

Sekarang buat kata $ create. Ini akan bermanfaat bagi kita lebih dari sekali. Anda dapat menggunakannya kapan pun Anda perlu membuat judul untuk entri kamus. Ini akan mengambil dua nilai dari tumpukan - alamat nama kata yang dibuat dan panjangnya. Setelah mengeksekusi kata ini, alamat entri kamus yang dibuat akan muncul di tumpukan.

$ buat
 # : $create here current @ @ here - test_bv dup c, compile_1248 -rot str, current @ ! ' var0 here c!; item "$create" create: callb here callb current .byte b_get, b_get callb here .byte b_sub callb test_bv .byte b_dup callb c_8 callb compile_1248 .byte b_mrot callb c_str #       callb current .byte b_get, b_set #     - var0,      here #   ,    -    ,    #     ,     #    1 allot   ,   .byte b_lit8, b_var0 callb here .byte b_set8 .byte b_exit 

Kata berikutnya akan mengambil nama kata baru dari aliran input menggunakan kata blword dan memanggil $ create, membuat kata baru dengan nama yang ditentukan.

buat_in
  item "create_in" create_in: .byte b_blword .byte b_dup .byte b_qbranch8 .byte 1f - . .byte b_strp #   (     ) .byte 3f - 2f #     2: .ascii "\ncreate_in - name not found!\n" 3: .byte b_quit 1: callb create .byte b_exit 

Dan akhirnya, buatlah kata ":". Ini akan membuat kata baru menggunakan create_in dan mengatur mode kompilasi, itu tidak diinstal. Dan jika dipasang, itu memberikan kesalahan. Kata ":" akan memiliki tanda langsung.

kata:
 # : : create_in 1 state dup @ if ." : - no execute state!" then ! 110 ; immediate item ":", f_immediate colon: callb create_in .byte b_num1 callb state .byte b_dup .byte b_get .byte b_qnbranch8, 2f - . .byte b_strp #   (     ) .byte 4f - 3f #     3: .ascii "\n: - no execute state!\n" 4: .byte b_quit 2: .byte b_set .byte b_lit8, 110 .byte b_exit 

Jika seseorang melihat kode tersebut, maka dia melihat bahwa kata ini melakukan sesuatu yang lain :)

Dan ini 110 ???

Ya, kata ini juga mendorong angka 110 ke tumpukan, dan itulah sebabnya. Ketika dikompilasi, berbagai konstruksi harus menjadi satu kesatuan. Misalnya, setelah jika harus maka. Dan kata yang dibuat menggunakan ":" harus diakhiri dengan ";". Untuk memeriksa kondisi ini, kata-kata khusus dari kompiler meletakkan nilai-nilai tertentu pada tumpukan dan memeriksa keberadaannya. Misalnya, kata ":" memberi nilai 110, dan kata ";" memeriksa apakah 110 berada di atas tumpukan. Jika ini bukan masalahnya, maka ini merupakan kesalahan. Jadi, struktur kontrol tidak berpasangan.

Pemeriksaan semacam itu dilakukan dalam semua kata-kata seperti kompiler, oleh karena itu, kami akan membuat kata khusus untuk ini - "Pasangan?" Ini akan mengambil dua nilai dari tumpukan, dan melemparkan kesalahan jika tidak sama.

Juga, dengan kata-kata seperti itu, Anda sering harus memeriksa bahwa mode kompilasi diatur. Mari kita membuat kata "Negara" untuk ini.

"pasangan" negara
 #: ?pairs = ifnot exit then ." \nerror: no pairs operators" quit then ; item "?pairs" .byte b_eq, b_qbranch8, 1f - . .byte b_strp .byte 3f - 2f 2: .ascii "\nerror: no pairs operators" 3: .byte b_quit 1: .byte b_exit #: ?state state @ 0= if abort" error: no compile state" then ; item "?state" callb state .byte b_get, b_zeq, b_qnbranch8, 1f - . .byte b_strp .byte 3f - 2f 2: .ascii "\nerror: no compile state" 3: .byte b_quit 1: .byte b_exit 

Itu saja! Kami tidak akan mengkompilasi apa pun di assembler secara manual :)

Tetapi sampai akhir, kompiler belum ditulis, jadi pada awalnya Anda harus menggunakan beberapa metode yang tidak biasa ...

Mari kita siap mengkompilasi kompiler yang dibuat dengan kompiler yang dibuat


Untuk memulai, Anda dapat memeriksa bagaimana kata ":" bekerja dengan menyusun sesuatu yang sederhana. Mari kita buat, misalnya, kata:

 : ^2 dup * ; 

Kata ini mengkuadratkan. Tetapi kita tidak memiliki kata ";" apa yang harus dilakukan?Kami menulis kata keluar sebagai gantinya, dan itu mengkompilasi. Dan kemudian matikan mode kompilasi dengan kata "[" dan turunkan nilai 110:

 $ ./forth ( 0 ): > : ^2 dup * exit [ drop ( 0 ): > 4 ^2 ( 1 ): 16 > 

Itu berhasil! Mari kita

lanjutkan ...

Karena kita akan terus menulis benteng di benteng, kita perlu memikirkan di mana kode sumber benteng akan, dan kapan harus menyusun. Mari kita buat pilihan termudah. Kode sumber benteng akan ditempatkan dalam kode sumber di assembler, sebagai string teks. Dan agar ia tidak mengambil terlalu banyak ruang, kami akan menempatkannya segera setelah alamat di sini, di area memori bebas. Tentu saja, kita memerlukan area ini untuk kompilasi, tetapi kecepatan "pelarian" interpretasi akan lebih besar daripada kebutuhan akan memori baru. Dengan demikian, kode yang dikompilasi akan mulai menimpa sumber di benteng, mulai dari awal, tetapi kita tidak akan memerlukannya lagi, karena kita telah membaca dan menggunakan bagian ini.

 fcode: .ascii " 2 2 + . quit" 

Tapi, di awal garis ada baiknya menempatkan selusin spasi.

Untuk membuat ini bekerja, kami mengubah bytecode awal sehingga tib, #tib arahkan ke baris ini. Pada akhirnya ada berhenti untuk memasuki baris perintah normal sistem.

Memulai bytecode telah menjadi seperti ini
 start: .byte b_call16 .word forth - . - 2 .byte b_call16 .word last_item - . - 2 .byte b_call16 .word context - . - 2 .byte b_get .byte b_set .byte b_call16 .word vhere - . - 2 .byte b_dup .byte b_call16 .word h - . - 2 .byte b_set .byte b_call16 .word definitions - . - 2 .byte b_call16 .word tib - . - 2 .byte b_set .byte b_lit16 .word fcode_end - fcode .byte b_call16 .word ntib - . - 2 .byte b_set .byte b_call16 .word interpret - . - 2 .byte b_quit 

Luncurkan!

 $ ./forth 4 ( 0 ): > 

Hebat!

Dan sekarang ...

Kompilasi kompiler dengan kompiler


Selanjutnya, kita menulis kode di baris fcode. Hal pertama yang harus dilakukan, tentu saja, adalah kata ";".

 : ; ?state 110 ?pairs lit8 [ blword exit find cfa c@ c, ] c, 0 state ! exit [ current @ @ dup c@ 96 or swap c! drop 

Saya akan membuat beberapa penjelasan.

 ?state 110 ?pairs 

Di sini kita memeriksa bahwa keadaan kompilasi benar-benar diatur, dan 110 ada di stack. Jika tidak, akan ada interupsi karena kesalahan.

 lit8 [ blword exit find cfa c@ c, ] 

Ini kita kompilasi perintah yang menyala dengan bytecode dari perintah keluar. Saya harus masuk ke mode eksekusi, menemukan kata keluar, mendapatkan alamat eksekusi, dan mendapatkan kode perintah dari sana. Semua ini diperlukan karena kami belum memiliki kata kompilasi. Jika ya, alih-alih semua ini, itu akan cukup untuk hanya menulis "kompilasi keluar" :)

 c, 0 state ! 

Ini akan mengkompilasi perintah keluar ketika kata ";" dijalankan, dan kemudian mode interpretasi akan ditetapkan. Kata "[" tidak dapat digunakan di sini, karena memiliki tanda langsung dan dieksekusi sekarang , tetapi kita perlu mengkompilasi perintah tersebut ke dalam kata ";" sehingga mereka mematikan mode kompilasi.

 exit [ 

Kami sudah mengalami ini. Kata keluar dikompilasi dan mode kompilasi dimatikan. Semuanya, kata ";" dikompilasi. Dan apa lagi yang tertulis di sana lebih jauh?

 current @ @ dup c@ 96 or swap c! drop 

Anda perlu mengatur bendera langsung untuk kata baru. Inilah yang dilakukan oleh urutan yang ditunjukkan, kecuali untuk kata drop. Kata drop menghapus 110 yang dilupakan yang menempatkan kata ":" di awal penciptaan.

Sekarang semuanya!

Kami meluncurkan dan mencoba.

 $ ./forth ( 0 ): > : ^3 dup dup * * ; ( 0 ): > 6 ^3 . 216 ( 0 ): > 

Ada! Ini adalah kata pertama yang dikompilasi oleh kompiler kami β€œuntuk nyata”.

Tetapi kita masih tidak memiliki kondisi, tidak ada loop, dan banyak lagi ... Mari kita mulai dengan kata kecil tapi sangat penting untuk membuat kompiler: segera. Ini menetapkan atribut langsung pada kata terakhir yang dibuat:

 : immediate current @ @ dup c@ 96 or swap c! ; 

Urutan yang akrab :) Baru-baru ini, ini ditulis secara manual, ini tidak akan diperlukan lagi.
Sekarang mari kita buat beberapa kata kecil tapi berguna:

 : hex 16 base ! ; : decimal 10 base ! ; : bl 32 ; : tab 9 ; : lf 10 ; 

heks dan desimal mengatur sistem angka yang sesuai. Sisanya adalah konstanta untuk mendapatkan kode karakter yang sesuai.

Kami juga membuat kata untuk menyalin garis dengan counter
:: cmove di atas c @ 1+ move;

Dan sekarang kita akan terlibat dalam kondisi. Secara umum, jika ada kompilasi kata, itu akan terlihat seperti ini:

 : if ?state compile ?nbranch8 here 0 c, 111 ; immediate : then ?state 111 ?pairs dup here swap - swap c! ; immediate 

Semua kata-kata ini di awal memverifikasi bahwa mode kompilasi diatur dan menghasilkan kesalahan jika ini tidak terjadi.

Kata if mengkompilasi cabang kondisional, cadangan byte untuk parameter perintah cabang kondisional, dan mendorong alamat byte tersebut ke stack. Kemudian ia mendorong nilai kontrol 111 ke stack.

Kata itu kemudian memeriksa keberadaan nilai kontrol 111, dan kemudian menulis offset ke alamat di stack.

Dan segera buat kata lain. Itu pada awalnya mengkompilasi perintah lompatan tanpa syarat untuk memotong cabang lain. Dengan cara yang sama seperti jika, offset transisi belum diketahui, ia hanya dicadangkan, dan alamatnya didorong ke stack. Nah, setelah itu, hal yang persis sama dilakukan seperti pada saat itu: alamat transisi tangkapan diatur ke cabang yang lain. Sesuatu lebih sulit untuk dijelaskan daripada kode itu sendiri :) Jika seseorang ingin mengetahuinya secara menyeluruh, lebih baik untuk menguraikan pekerjaan dari kode yang disederhanakan secara maksimal:

 : if compile ?nbranch8 here 0 c, ; immediate : then dup here swap - swap c! ; immediate 

Nah, sekarang kita memprogram kode asli. Karena kami tidak memiliki kompilasi kata, kami menerapkan trik yang sama seperti ketika membuat kata ";":

 : if ?state lit8 [ blword ?nbranch8 find cfa c@ c, ] c, here 0 c, 111 ; immediate : then ?state 111 ?pairs dup here swap - swap c! ; immediate : else ?state 111 ?pairs lit8 [ blword branch8 find cfa c@ c, ] c, here 0 c, swap dup here swap - swap c! 111 ; immediate 

Sekarang Anda dapat mencoba mengkompilasi kondisi tersebut. Mari kita membuat, misalnya, kata yang mencetak 1000 jika ada 5 di stack, dan 0 dalam kasus lain:

 $ ./forth ( 0 ): > : test 5 = if 1000 . else 0 . then ; ( 0 ): > 22 test 0 ( 0 ): > 3 test 0 ( 0 ): > 5 test 1000 ( 0 ): > 

Jelas bahwa hasil seperti itu tidak langsung bekerja, ada kesalahan, ada debugging. Tetapi pada akhirnya, kondisinya berhasil!

Penyimpangan kecil tentang panjang perintah transisi
, , 127 . . , , . , , . 8 , 40 127 . , ?

. β€” 16 .

. 16 β€” . , , call, . , 11 ( 1023 ). 300 1000 . , . 3 , 8 . : (?nbranch), (?branch) (branch). β€” 24 .

Kami memiliki kondisi, hidup menjadi lebih mudah :)

Mari kita membuat kata. "(Dot-quote). Ini menampilkan teks yang ditentukan ketika dieksekusi. Digunakan dengan cara ini:

 ."    " 

Anda dapat menggunakan kata ini hanya dalam mode kompilasi. Ini akan menjadi jelas setelah kami menganalisis perangkat kata ini:

 : ." ?state 34 word dup if lit8 [ blword (.") find cfa c@ c, ] c, str, else drop then ; immediate 

Kata ini dieksekusi dalam mode kompilasi. Dibutuhkan string dari input stream hingga tanda kutip (34 kata). Jika baris tidak dapat diperoleh, itu tidak menghasilkan apa-apa. Meskipun, di sini akan lebih baik untuk mendapatkan diagnosis. Tetapi untuk keluaran baris, kata ini persis seperti yang kita lakukan :) Jika perlu, maka Anda dapat mendefinisikan kembali kata ini, sudah dengan diagnostik.

Jika dimungkinkan untuk mendapatkan string, perintah byte (. ") Dikompilasi, dan kemudian string diterima. Perintah byte ini (tanda kutip titik-titik dalam tanda kurung), ketika dieksekusi, menampilkan string yang dikompilasi di belakang byte perintah.

Periksa.

 $ ./forth ( 0 ): > : test ."    " ; ( 0 ): > test     ( 0 ): > 

Dan akhirnya, mari kita buat kompilasi kata.

Jelas bahwa dalam mode kompilasi kata ini harus mengambil nama kata berikutnya dari aliran, temukan di kamus. Dan kemudian akan ada opsi: itu bisa berupa perintah byte, atau bisa juga kata yang ditulis dalam kode byte. Kata-kata ini harus dikompilasi dengan berbagai cara. Oleh karena itu, kita akan membuat dua kata tambahan: "(compile_b)" dan "(compile_c)".

(compile_b) akan mengkompilasi perintah panggilan untuk memanggil bytecode. Parameter akan menjadi kata 64-bit - alamat bytecode dipanggil.

(compile_c) akan mengkompilasi perintah byte. Dengan demikian, parameter dari perintah ini akan menjadi satu byte - kode perintah.

Nah, kata kompilasi itu sendiri akan mengkompilasi (compile_b) atau (compile_c) dengan parameter yang sesuai.

Mari kita mulai dengan (compile_c),seperti dengan yang paling sederhana:

 : (compile_c) r> dup c@ swap 1+ >rc, ; 

Terlepas dari kesederhanaannya, pertama-tama kita menulis kata dalam bytecode, yang dengan sendirinya memiliki parameter. Karena itu, saya akan berkomentar. Setelah memasukkan (compile_c), alamat kembali terletak di tumpukan kembali, karena tidak basi. Ini adalah alamat byte berikutnya setelah perintah panggilan. Situasi pada saat panggilan ditunjukkan di bawah ini. A0 - kode perintah panggilan, XX - parameter perintah panggilan - alamat panggilan (offset) dari kode byte kata (compile_c).



Alamat pengirim menunjukkan byte NN. Biasanya ada kode untuk byte perintah berikutnya. Tetapi kata kita memiliki parameter, jadi NN hanyalah parameter dari kata "(compile_c)", yaitu, kode byte dari perintah yang dikompilasi. Anda perlu membaca byte ini dan mengubah alamat pengirim dengan menggerakkannya maju ke perintah byte berikutnya. Ini dilakukan dengan urutan β€œr> dup c @ swap 1+> r”. Urutan ini menarik alamat pengirim dari tumpukan kembali ke tumpukan reguler, mengambil satu byte darinya, menambahkan satu padanya (mengembalikan alamat), dan mengembalikannya kembali ke tumpukan kembali. Perintah yang tersisa "c," mengkompilasi kode perintah byte yang diperoleh dari parameter.

(compile_b) tidak jauh lebih rumit:

 : (compile_b) r> dup @ swap 8 + >r compile_b ; 

Semuanya sama di sini, hanya parameter 64-bit yang dibaca, dan kata compile_b digunakan untuk mengkompilasi kata, yang telah kita buat untuk kompiler.

Dan sekarang kata kompilasi. Seperti yang sudah dibahas, ia membaca nama kata, menemukannya dan mengkompilasi salah satu dari dua perintah sebelumnya. Saya tidak akan berkomentar tentang itu, kami telah menerapkan dan membongkar semua konstruksi yang digunakan.

Kompilasi kata
 : compile blword over over find dup if dup c@ 128 and if cfa c@ (compile_b) [ blword (compile_c) find cfa , ] c, else cfa (compile_b) [ blword (compile_b) find cfa , ] , then drop drop else drop ." compile: " type ." - not found" then ; immediate 

Untuk memeriksa kata yang dibuat, kami membuat, dengan bantuannya, kata ifnot.

 : ifnot ?state compile ?branch8 here 0 c, 111 ; immediate 

Lihat itu!

 $ ./forth ( 0 ): > : test 5 = ifnot 1000 . else 0 . then ; ( 0 ): > 22 test 1000 ( 0 ): > 3 test 1000 ( 0 ): > 5 test 0 ( 0 ): > 

Semuanya baik-baik saja! Dan inilah saatnya untuk melakukan siklus ...

Pada artikel ini kita akan membuat siklus dengan suatu syarat. Benteng memiliki dua opsi untuk satu siklus dengan suatu syarat.

Opsi pertama adalah mulai ... sampai. Kata sampai menghilangkan nilai dari tumpukan, dan jika tidak sama dengan nol, siklus berakhir.

Pilihan kedua adalah mulai ... sementara ... ulangi. Dalam hal ini, pemeriksaan terjadi ketika kata saat dieksekusi. Loop keluar jika nilai pada stack adalah nol.

Siklus di benteng dibuat dengan cara yang sama dengan kondisi - pada transisi bersyarat dan tanpa syarat. Saya membawa kode, komentar, saya pikir, tidak diperlukan.

 : begin ?state here 112 ; immediate : until ?state 112 ?pairs compile ?nbranch8 here - c, ; immediate : while ?state 112 ?pairs compile ?nbranch8 here 0 c, 113 ; immediate : repeat ?state 113 ?pairs swap compile branch8 here - c, dup here swap - swap c! ; immediate 

Hari ini kami selesai dengan kompiler. Hanya ada sedikit yang tersisa. Dari fungsi-fungsi kunci yang belum diimplementasikan hanya siklus dengan penghitung. Dan itu juga layak untuk membuat perintah loop keluar. Kami akan melakukannya lain kali.

Tapi kami tidak mengalami perintah siklus!

Kami melakukan ini dengan menulis kata kata standar. Kami akhirnya harus melihat kamus kami.
Untuk melakukan ini, pada awalnya, kami membuat kata tautan @. Ini akan mengekstrak bidang komunikasi dari entri kamus (diimbangi dengan entri sebelumnya). Seperti yang kita ingat, bidang komunikasi dapat memiliki ukuran yang berbeda: 1, 2, 4 atau 8 byte. Kata ini akan mengambil tumpukan alamat entri kamus, dan mengembalikan dua nilai: alamat bidang nama dan nilai bidang komunikasi.

 : link@ dup c@ 3 and swap 1+ swap dup 0= if drop dup 1+ swap c@ else dup 1 = if drop dup 2 + swap w@ else 2 = if drop dup 4 + swap i@ else drop dup 8 + swap @ then then then ; 

Dan sekarang Anda dapat membuat kata-kata:

 : words context @ @ 0 begin + dup link@ swap count type tab emit dup 0= until drop drop ; 

Meluncurkan ...

 $ ./forth ( 0 ): > words words link@ repeat while until begin ifnot compile (compile_b) (compile_c) ." else then if cmove tab bl decimal hex immediate ; bye ?state ?pairs : str, interpret $compile compile_b compile_n compile_1248 compile_c c, w, i, , allot here h test_bv test_bvc [ ] state .s >in #tib tib . #> #s 60 # hold span holdpoint holdbuf base quit execute cfa find word blword var16 var8 (.") (") count emit expect type lshift rshift invert xor or and >= <= > < = 0> 0< 0= bfind compare syscall fill move rpick r@ r> >r -! +! i! i@ w! w@ c! c@ ! @ depth roll pick over -rot rot swap drop dup abs /mod mod / * - + 1+ 1- exit ?nbranch16 ?nbranch8 ?branch16 ?branch8 branch16 branch8 call8b0 call64 call32 call16 call8f lit64 lit32 lit16 lit8 8 4 3 2 1 0 context definitions current forth ( 0 ): > 

Ini dia, kekayaan kita :)

Saya ingin mengatakan segalanya ... tidak, mari kita tetap memungkinkan untuk menentukan file dengan program benteng untuk kompilasi dan eksekusi sebagai parameter.

Kami membuat perintah syscall untuk membuka, menutup, dan membaca file. Kami mendefinisikan konstanta yang diperlukan untuknya.

 : file_open 0 0 0 2 syscall ; : file_close 0 0 0 0 0 3 syscall ; : file_read 0 0 0 0 syscall ; : file_O_RDONLY 0 ; : file_O_WRONLY 1 ; : file_O_RDWR 3 ; 

Sekarang Anda dapat membuat kata awal _start:

 : _start 0 pick 1 > if 2 pick file_O_RDONLY 0 file_open dup 0< if .\" error: \" . quit then dup here 32 + 32768 file_read dup 0< if .\" error: \" . quit then swap file_close drop #tib ! here 32 + tib ! 0 >in ! interpret then ; 

Kata ini akan dimuat dari file dan menjalankan program benteng apa pun. Lebih tepatnya, penerjemah akan mengeksekusi semua yang ada di file ini. Dan mungkin ada, misalnya, kompilasi kata-kata baru dan eksekusi mereka. Nama file ditunjukkan oleh parameter pertama saat startup. Saya tidak akan merinci, tetapi parameter peluncuran di Linux dilewatkan melalui tumpukan. Kata _start akan mencapainya dengan perintah 0 pick (jumlah parameter) dan 2 pick (pointer ke parameter pertama). Untuk sistem benteng, nilai-nilai ini berada di luar tumpukan, tetapi Anda bisa mendapatkannya dengan perintah pilih. Ukuran file dibatasi hingga 32 KB, sementara tidak ada manajemen memori.

Sekarang tinggal menulis di baris fcode di akhir:

 _start quit 

Buat file test.f dan tulis sesuatu di benteng. Misalnya, algoritma Euclidean untuk menemukan faktor umum terbesar:

 : NOD begin over over <> while over over > if swap over - swap else over - then repeat drop ; 23101 44425 NOD . bye 

Kita mulai.

 $ ./forth test.f 1777 Bye! $ 

Jawabannya benar. Kata itu dikompilasi, kemudian dipenuhi. Hasilnya ditampilkan, maka perintah bye dieksekusi. Jika Anda menghapus dua baris terakhir, kata NOD akan ditambahkan ke kamus dan sistem akan pergi ke baris perintahnya. Anda sudah dapat menulis program :-)

Itu saja.Siapa peduli, Anda dapat mengunduh sumber atau biner siap pakai untuk Linux di x86-64 dari Github: https://github.com/hal9000cc/forth64

Sumber datang dengan lisensi GNU GPL v2 DCH v1 - Do What You Want :-)

Source: https://habr.com/ru/post/id437466/


All Articles