TypeScript Ternyata Cukup Oke untuk Implementasi Kompiler
(matklad.github.io)- Bahasa implementasi untuk alat bergaya kompiler secara tradisional terbagi antara OCaml dan C++, tetapi untuk eksperimen bahasa kecil, TypeScript bisa menjadi pilihan yang ringan seperti bahasa keluarga ML
- Rust menggabungkan kelebihan ML dan C++ serta menyediakan multithreading aman, tetapi karena perlu memodelkan tata letak fisik data, ia bisa terasa berat untuk prototipe kecil
- Deno membantu memulai eksperimen bahasa dengan TypeScript dengan cepat berkat biner tunggal, linting dan formatting bawaan, tanpa tahap kompilasi, serta task runner dan mode watch
- Type checker contoh menggabungkan AST generik, tagged union, visitor, dan transform bottom-up untuk mengubah
Expr<void>menjadiExpr<Type>, serta memasukkanTypeErrorsebagai nilai tipe guna mengurangi error berantai - TypeScript bisa produktif sebagai alat untuk eksperimen bahasa kecil berkat autocompletion, sistem tipe yang fleksibel, dan karakteristik runtime yang memungkinkan kita beralih ke pendekatan dinamis saat diperlukan
Pola lama dalam memilih bahasa implementasi
- Saat membuat alat berbentuk kompiler, secara umum ada dua arus besar dalam pemilihan bahasa implementasi
- Untuk pekerjaan yang berpusat pada bahasa seperti spesifikasi formal atau bahasa mainan hobi, OCaml sangat cocok
- Contohnya ada plzoo dan WebAssembly reference interpreter
- Untuk pekerjaan yang berpusat pada implementasi dan perlu kesiapan produksi, C++ sering dipilih
- LLVM, clang, v8, dan HotSpot semuanya berbasis C++
- Rust secara langsung dipengaruhi oleh ML dan C++, menggabungkan kelebihan keduanya, dan juga menawarkan kekuatan khasnya sendiri seperti multithreading yang aman
- Namun, dalam spektrum ini Rust lebih condong ke sisi kesiapan produksi
- Sistem build yang “langsung jalan” memang membantu untuk prototyping, tetapi ikut membawa kompleksitas tambahan karena kita harus memodelkan tata letak fisik data
Kelebihan pendekatan indeks di Rust dan bebannya untuk kode kecil
- Saran umum saat membuat kompiler dengan Rust adalah menghindari pointer dan menggunakan indeks
- Indeks memiliki berbagai kelebihan pada codebase besar
- Side table bisa ditempatkan di dalam modul terkait sehingga coupling lebih rendah
- Indeks berupa
u32dan mendorong tata letak struct-of-arrays yang menguntungkan untuk performa - Mudah diserialisasi atau dihubungkan ke framework incremental compilation sehingga strategi komputasi lebih fleksibel
- Namun, dalam pemrograman skala kecil, pendekatan indeks itu sendiri bisa merepotkan, dan untuk eksperimen hobi beban ini bisa menjadi fatal
- OCaml masih terasa agak kuno, dan dalam konteks ini dipertimbangkan apakah TypeScript dapat dipakai sebagai alternatif yang setara dengan ML
Lingkungan eksperimen cepat dari Deno dan TypeScript
- deno digunakan sebagai lingkungan awal
- Ia memberikan pengalaman langsung pakai untuk TypeScript
- Di OCaml bagian ini merupakan titik sakit, dan meski Rust lebih baik daripada OCaml atau C++, Deno tetap memberi pengalaman yang lebih sederhana daripada Rust
- Pengalaman pengembangan di Deno sangat cocok untuk hacking PLT skala kecil
- Biner tunggal
- Linting dan formatting bawaan
- Tidak ada tahap kompilasi terpisah
- Ada task runner dan mode watch bawaan
- TypeScript sendiri menyediakan sistem tipe yang cukup fleksibel sambil tetap ringan dari sisi beban sintaks
Pola TypeScript yang terlihat dari contoh type checker kecil
- AST dimulai dari ekspresi yang memiliki informasi lokasi file
Locationmemilikifile,line,column- Di TypeScript, string tetap hanya
string, angka tetap hanyanumber, jadi tidak perlu memikirkan pembedaan sepertiusizedanu32
- Ekspresi dipisahkan antara lokasi dan kind, lalu belakangan data terkait digenerikkan menjadi bentuk
Expr<T>- Ekspresi tepat setelah parsing memiliki data
void - Ekspresi yang telah diproses type checker memiliki data
Type - Fungsi inferensi tipe menerima
ast.Expr<void>dan mengembalikanast.Expr<Type>
- Ekspresi tepat setelah parsing memiliki data
- Karena TypeScript tidak otomatis menambahkan perilaku runtime, untuk melakukan matching pada union type kita perlu memasukkan informasi tipe runtime sendiri
- Field seperti
tag: "binary",tag: "if"menjalankan peran ini tag: "binary"berarti pada runtime nilai tersebut hanya bisa berupa string"binary"
- Field seperti
- Literal boolean dan literal int hampir berbentuk sama sehingga diabstraksikan sebagai
ExprLiteral<T, V, Tag>ExprBool<T>adalahExprLiteral<T, boolean, "bool">ExprInt<T>adalahExprLiteral<T, number, "int">
- Nilai tipe dipisah menjadi
TypeBool,TypeInt, dan nilai singleton juga disediakan dengan nama yang sama- TypeScript menghapus tipe sepenuhnya, sehingga nama terkait tipe dan nama terkait nilai berada di namespace terpisah
- Karakteristik ini memungkinkan kita mendefinisikan tipe dan nilai dengan nama yang sama
Visitor, transform, tipe error, desugaring
switchdi TypeScript adalah statement, bukan expression, jadi untuk menangani kind ekspresi dengan nyaman didefinisikan visitor- Metode
bool,int,binary,ifmasing-masing menangani kind terkait - Autocompletion editor membantu baik untuk case
switchmaupun implementasi visitor
- Metode
transform<U, V>adalah fungsi traversal umum yang mengubahExpr<U>menjadiExpr<V>- Transformasi dijalankan secara bottom-up
- Saat mengunjungi node internal, subekspresi sudah lebih dulu ditransformasi, sehingga tipe visitor menjadi
Visitor<V, V>, bukanVisitor<U, V>
- Dengan memanfaatkan sifat TypeScript sebagai bahasa bertipe dinamis, kita juga bisa membuat traversal yang lebih umum dengan
Object.keys- Bahkan dalam kasus ini, signature fungsi statis tetap bisa dipertahankan
- Pada contoh hal ini tidak wajib, tetapi bila perlu ada ruang untuk turun ke pendekatan dinamis
- Error tipe tidak dikumpulkan ke array sebagai efek samping, melainkan direpresentasikan dengan tipe
TypeErrorTypemenjadiTypeBool | TypeInt | TypeErrorTypeErrormemilikitag: "Error",location,messagetype_equalmengembalikan true jika salah satu sisi adalahErroruntuk mencegah kegagalan berantai
- Type checker akhir memeriksa binary dan if
- Jika tipe operand kiri dan kanan pada ekspresi binary berbeda, ia mengembalikan error
"binary expression operands have different types" - Jika kondisi pada ekspresi if bukan boolean, ia mengembalikan error
"if condition is not a boolean" - Jika tipe branch then dan else berbeda, ia mengembalikan error
"if branches have different types"
- Jika tipe operand kiri dan kanan pada ekspresi binary berbeda, ia mengembalikan error
- Hasil akhirnya memang tetap memerlukan sedikit penulisan tipe, tetapi sangat dibantu oleh autocompletion, terasa lebih sedikit “berkelahi dengan bahasa”, dan cocok secara alami dengan bentuk masalahnya
- Alasan TypeScript produktif sebagai alat untuk eksperimen bahasa kecil diringkas menjadi tiga hal
- Deno adalah runtime scripting yang kecil, tertutup, kuat, dan dioptimalkan untuk workflow pengembangan yang efektif
- Tooling TypeScript membuat IDE berguna dan produktif, dan berkat Deno tidak memerlukan konfigurasi
- Bahasanya kuat baik di runtime maupun compile time, mampu mengekspresikan hal yang cukup canggih lewat tipe, tetapi tetap memungkinkan beralih ke pendekatan dinamis bila perlu
- Sebagai ide tambahan, banyak syntactic sugar juga bisa di-desugar dengan aman secara tipe
ExprdanExprKinddiparameterkan secara rekursif terhadap keseluruhanExprKind, bukan terhadap data terkaitExprKindCoremerepresentasikan himpunan ekspresi dasarExprKindSugarmencakup ekspresi dasar atau ekspresi yang bisa di-desugar ke ekspresi dasardesugar(expr: ExprSugar): ExprCoremereduksi ekspresi syntactic sugar menjadi ekspresi intidesugar_one(expr: ExprKindSugar<ExprCore>): ExprKindCore<ExprCore>melakukan transformasi satu langkah saat subekspresi sudah berada dalam keadaan ter-desugar
1 komentar
Opini Hacker News
TypeScript secara keseluruhan adalah bahasa yang bagus, dan fakta bahwa fungsi adalah objek yang bisa memiliki properti/metode masih kurang diapresiasi
Anda bisa menjalankan array fungsi seperti perintah, lalu belakangan menambahkan deskripsi seperti
help, atau menambahkan state dengan closure/partial application, sehingga tidak perlu buru-buru mendefinisikan kelas sepertiCommandDalam pemrograman berorientasi objek, memberi nama terlalu dini sering menimbulkan banyak konflik, dan menurut saya lebih natural mengoper nilai yang diperlukan ke fungsi daripada struktur seperti
VideoCompressor#compress()Bahasa lain yang mendukung objek yang bertindak seperti fungsi dirangkum di https://en.wikipedia.org/wiki/Function_object
Seperti handler di
net/http, sebuah struct bisa mengimplementasikan metodeserve, atau sebuah fungsi handler bisa memanggil dirinya sendiri untuk memenuhi interfaceDi Clojure, metadata bisa ditempelkan pada
varfungsi untuk menangani hal serupa, dan karena ini Lisp, hampir apa pun bisa dilakukan dengan makroSelain itu, kanal CSP di
core/asyncmemisahkan eksekusi dan komunikasi sehingga membantu menghindari masalah warna fungsi seperti callback/promise/async/await, dan perintah dapat bertindak seperti produsen yang mengirim hasil ke kanalApa pun nama metodenya, ia bisa digunakan dalam konteks fungsi, dan tidak perlu merujuk nama metode konkretnya
Namun, saya tidak suka jika fungsi memiliki properti untuk memuat state sehingga perilakunya bisa berubah meski dipanggil dengan argumen yang sama. Menurut saya salah satu keunggulan besar pemrograman fungsional adalah menjauh dari state ala OOP
Func, dan fungsi tanpa nilai balik denganAction, dengan cara yang miripEkspresi lambda JavaScript cukup mirip dengan C#, dan signature fungsi TypeScript dan C# juga cukup serupa
Ada juga repositori kecil yang menunjukkan kemiripan JavaScript, TypeScript, dan C#: https://github.com/CharlieDigital/js-ts-csharp
Screenshot yang menampilkan logika yang sama dalam JS/TS/C# berdampingan: https://github.com/CharlieDigital/js-ts-csharp/blob/main/js-...
Tidak terlalu mengejutkan. Menurut saya TypeScript pada akhirnya juga merupakan satu lagi bahasa yang dengan susah payah mengadopsi cukup banyak fitur keluarga ML
Karena tidak ada pattern matching sungguhan, ia lebih merepotkan daripada OCaml, tetapi jika dibandingkan dengan kelompok seperti C#, Swift, Dart, dan Kotlin, levelnya masih cukup lumayan
TypeScript punya sistem tipe yang kuat, tetapi standard library dasarnya dan bahasanya sendiri terasa kurang memuaskan, dan tidak ada pattern matching/switch expression
Dart memiliki model objek yang tertutup sehingga kebebasan dinamisnya rendah, sistem tipe dan ekspresinya juga lebih lemah, dan hampir tidak ada fasilitas metaprogramming, sehingga harus bergantung pada boilerplate gaya Java dan generator kode
C# adalah yang paling dekat dengan fitur ML di antara bahasa yang disebutkan, tetapi tidak seperti TypeScript, ia tidak memiliki sum type, sehingga banyak hal menjadi lebih merepotkan
Misalnya, parser yang mengenali
{ ... }bisa dikombinasikan dengan parser lain, dan statement dapat didefinisikan sebagai salah satu dari control flow/deklarasi/assignmentPemrosesan list dan pattern matching ala ML sangat ekspresif saat menangani representasi antara
Di artikel utama pun, karena
switchbukan expression, mereka harus memutar lewat visitor pattern, dan dukungan iterator JavaScript juga anehnya kurang.map()memang ada, tetapi hanya bekerja pada array dan tidak bisa langsung diterapkan pada iterator umumAda banyak perbedaan, tetapi transisinya tidak sulit, dan secara pribadi saya lebih menyukai Kotlin, meski keduanya bisa digunakan
Saya bertanya-tanya bagaimana jadinya jika TypeScript lepas dari kompatibilitas JavaScript dan dikompilasi ke WASM. Kotlin sedang menambahkan compiler WASM dan sudah memiliki transpiler JS, tetapi kode yang sama lebih kecil dan lebih cepat dimuat di WASM
JavaScript di browser bukan target kompilasi yang baik, dan karena makin banyak proyek baru dimulai langsung dengan TypeScript sejak awal, alasan bahwa harus mudah bermigrasi dari JavaScript lama juga makin melemah
Saat berpindah-pindah antara Rust dan TypeScript, sangat jelas terasa betapa hilangnya fitur seperti tagged enum
Proposal enum ADT tampaknya berhenti, jadi saya penasaran apakah ada upaya lain: https://github.com/Jack-Works/proposal-enum/discussions/19
Sistem tipe TypeScript memang menarik, tetapi jadi penasaran seberapa jauh lebih cepat jika compilernya ditulis dalam bahasa yang dikompilasi
Tentu saja perlu prasyarat besar berupa “implementasi yang baik”
swcdanesbuildbukan pembanding yang bagus. Sebagian besar peningkatan kecepatannya berasal dari menghapus sintaks khusus TypeScript untuk menghasilkan JavaScripttsccenderung lambat hanya pada eksekusi pertama, dan jika memakai flagincrementalatau mode watch--transpile-only, waktu kompilasi biasanya turun di bawah 100 ms sehingga hampir tidak terasa berbeda dari SWC atau ESBuildIntinya, jika pemeriksaan tipe pada program berukuran menengah memakan waktu 20 detik, biasanya itu bukan karena JS, melainkan karena tipenya menyebabkan ledakan kombinatorial
Runtime lain bisa memberi keuntungan dalam paralelisme atau waktu startup, tetapi katanya mereka belum melihat benchmark berpusat CPU yang mendukung peningkatan kecepatan total 20 kali lipat
Lalu setelah seharian men-debug 5 baris logika tipe, kita akan bertanya-tanya bagaimana bisa sampai sejauh ini
Ini proyek yang ditulis dalam Rust oleh pengembang utama SWC; SWC mengompilasi TS ke JS, sedangkan STC memeriksa tipe TS
isolated declaration mode yang akan datang disebut bisa memangkas waktu kompilasi hingga 75%: https://github.com/microsoft/TypeScript/pull/53463#issuecomm...
Untuk orang yang baru mulai belajar compiler, saya merekomendasikan buku ini: https://keleshev.com/compiling-to-assembly-from-scratch/
Penulisnya menjelaskan cara membuat compiler assembly ARM 32-bit dengan subset TypeScript, dan bentuknya hampir seperti pseudocode sehingga mudah didekati
Bukunya terbagi menjadi dua bagian; bagian pertama membuat interpreter bahasa dengan Java, dan bagian kedua mengompilasi bahasa yang sama ke bytecode lalu mengimplementasikan VM bytecode dalam C
Setiap baris kode implementasinya dirujuk di dalam buku
Jika ingin menghindari visitor pattern, bisa memakai switch bergaya IIFE dengan fungsi utilitas
runCaranya, gunakan
switchdi dalam fungsi yang langsung dieksekusi dan biarkan tipe kembaliannya diinferensiJika ingin menghindari IIFE karena tidak suka
()di akhir, definisikan saja fungsinya secara terpisah lalu panggilSaya sedang menulis compiler dengan TypeScript, dan setuju bahwa ini tidak seburuk yang dibayangkan
Awalnya saya mulai dengan Deno seperti penulis, tetapi akhirnya pindah ke Bun; meski masih ada bagian yang kasar, saya lebih puas dibanding Deno dan performanya sangat cepat
Untuk frontend parser generator standar, Ohm-js cukup nyaman: https://ohmjs.org/
Compiler resmi
tscterlalu besar sehingga tidak saya sarankan untuk dibaca; untuk melihat cara kerjatsc, mini-typescript lebih bagus: https://github.com/sandersn/mini-typescript/Khususnya branch
centi-typescriptcukup membantu: https://github.com/sandersn/mini-typescript/tree/centi-types...Saya menantikan GC dan akses DOM tersedia di WASM
Saya kira TypeScript akan punya overhead tambahan karena interface, jadi ini cukup di luar dugaan
Jadi penasaran apakah bisa diterapkan ke bidang lain juga, misalnya apakah cocok untuk parsing bahasa
Interface sepenuhnya hilang pada proses kompilasi
Hasilnya tidak terlalu mengejutkan. Karena compiler TypeScript sendiri ditulis dalam TypeScript
TypeScript sebagai ML sudah divalidasi setiap hari di lingkungan produksi yang berat
Saya pernah menulis compiler dengan C#, dan yang tampak istimewa di sini kira-kira hanya union type
Secara pribadi saya memilih menghindari verbositas visitor pattern, dan sedang menunggu fitur enum tertutup untuk pemeriksaan kelengkapan pada waktu kompilasi
Alternatifnya biasanya canggung. Entah memakai kelas untuk merepresentasikan satu sum type dengan N properti nullable dan mengandalkan syarat dalam dokumentasi bahwa “selalu hanya satu yang non-null”, atau membuat beberapa kelas mewarisi kelas bersama; keduanya terasa berat
Untuk membuat beberapa union type yang saling tumpang tindih, kedua cara itu membutuhkan duplikasi atau kombinasi yang cerdik