- scriptc คอมไพล์ TypeScript ปกติให้เป็นไบนารีเนทีฟขนาดเล็กที่รันได้โดยไม่ต้องใช้ Node, V8 หรือเอนจิน JavaScript ขณะเดียวกันยังคงการตรวจสอบชนิดของคอมไพเลอร์ TypeScript จริงและความเข้ากันได้กับพฤติกรรมของ Node
- ตัดสินตามโครงสร้างโค้ดว่าสามารถคอมไพล์แบบสแตติกได้หรือไม่ โดยค่าเริ่มต้นจะสร้างเป็นโค้ดเนทีฟ และจะใช้ quickjs-ng เพื่อรัน JavaScript ของแพ็กเกจ npm และโค้ดชนิด
anyเฉพาะเมื่อเลือก--dynamicเท่านั้น - รองรับตั้งแต่คลาส, generic,
async/await, exception, regular expression ไปจนถึง Node server API,fetchและ dependency ของ npm ส่วน syntax ที่ยังไม่รองรับจะถูกปฏิเสธพร้อม error code, code frame และคำแนะนำในการแก้ไข - รันโปรแกรมมากกว่า 800 รายการทั้งบน Node และไบนารีเนทีฟเพื่อเปรียบเทียบ output และ exit code พร้อมตรวจสอบ memory error ด้วย AddressSanitizer และการ audit จำนวน reference
- จากการวัดบน Apple M series เวลาเริ่มต้นอยู่ที่ประมาณ 2.4ms, ไบนารีแบบสแตติกมีขนาด 170–200KB, RSS ทั่วไปอยู่ที่ 1–4MB และเมื่อรวม dynamic mode กับ dependency ที่ฝังมา ไบนารีจะมีขนาดประมาณ 3MB
โมเดลการคอมไพล์แบบสแตติก
- ใช้ TypeScript เดิมโดยไม่ต้องมี dialect หรือ annotation แยกต่างหาก และใช้ระดับความเข้มงวดในการตรวจของ
tsconfig.jsonรวมถึงไลบรารีes2025จริงของ TypeScript- หากโปรเจกต์มี
@types/nodeก็จะนำมาตรวจสอบชนิดร่วมด้วย - โค้ดที่เข้าถึงได้แต่ไม่มีการ lowering จะรายงาน diagnostic ที่ถูกต้องและหยุดคอมไพล์
- หากโปรเจกต์มี
scriptc coverageแสดงจำนวน statement ที่วิเคราะห์ สัดส่วนที่คอมไพล์แบบสแตติกได้ รวมถึงปัจจัยที่บล็อกและ error code- ในตัวอย่าง จาก 4,481 statement มี 4,451 statement หรือ 99% ที่คอมไพล์แบบสแตติกได้
- วิธีรันถูกแบ่งอย่างชัดเจนเป็นสามขั้น
- การคอมไพล์แบบสแตติก: เป็นโหมดพื้นฐาน โดยแปลงเป็นโค้ดเนทีฟโดยไม่ใช้เอนจิน JavaScript
- การรันแบบไดนามิก: เมื่อระบุ
--dynamicจะรวม quickjs-ng ขนาดประมาณ 620KB เพื่อรัน JavaScript ของแพ็กเกจ npm และโค้ดชนิดany- ตรวจสอบค่าทุกค่าที่ข้ามมายังโค้ดสแตติกใน runtime
- หากค่ากับชนิดที่ประกาศไม่ตรงกัน จะ throw
TypeErrorที่จับได้โดยไม่ทำให้หน่วยความจำเสียหาย
- การปฏิเสธ: โค้ดที่จัดการไม่ได้จะให้ error code, code frame และมักมีคำแนะนำในการแก้ไข โดยจะไม่คอมไพล์ผิดแบบเงียบ ๆ
TypeScript และ standard library ที่รองรับ
- ฟีเจอร์ภาษา รองรับคลาสแบบ single inheritance พร้อม dynamic dispatch, closure, การทำ generic monomorphization, discriminated union, destructuring, spread, template literal, getter/setter และ iterator
- หากพิสูจน์ความปลอดภัยได้ จะทำ devirtualization ของ dynamic dispatch
- discriminated union จัดการด้วย tag value โดยใช้ narrowing ของ TypeScript
async/awaitทำงานด้วย stackful fiber และ scheduling ที่ปรับให้เข้ากับ JavaScript- รองรับ exception และ
finallyรวมถึงพารามิเตอร์แบบ optional, default และ rest
- Regular expression ใช้ bytecode interpreter ที่เข้ากันได้กับ ECMAScript ตัวเดียวกับที่ QuickJS ใช้ และจะลิงก์เฉพาะในไบนารีที่ใช้ regular expression
- Standard library รวม string ที่รักษา semantics แบบ UTF-16, array, Map และ Set ที่มีกฎลำดับและ identity เหมือน JavaScript
- การแปลงชนิดของ
JSONผ่านการตรวจสอบใน runtime - ยังมี
Math, typed array,Bufferและลำดับชั้นErrorที่รองรับ typedcatch
- การแปลงชนิดของ
Node และ Web API
- Node API รองรับ
fs,path,process,child_process,os,crypto,url/URL,zlib, timer และ signal handlerfsมีทั้ง synchronous API และ Promise APIchild_processรองรับ pipe stream- event loop ไม่มี dependency ภายนอก
- server stack รวม
net,http,https,tls,dgram,dns,fs.watch,readlineและสามารถคอมไพล์ proxy server จริงได้- TLS ใช้ mbedTLS ที่รวมมาให้
- การทำงานบางส่วนของ WHATWG Web API เช่น
fetch, stream,Headers,AbortSignalอยู่บน native network และ TLS stack เดียวกัน- รองรับ redirect, gzip,
AbortSignal.timeoutและ error cause ในรูปแบบ Node - ไม่ใช้ libcurl หรือ dependency HTTP ของระบบ
- รองรับ redirect, gzip,
dependency ของ npm และการรันแบบไดนามิก
- ใน
--dynamicใช้วิธี resolve module ของ Node และตรวจสอบชนิดโดยอิงจาก.d.tsที่แพ็กเกจให้มา - JavaScript ของแพ็กเกจ npm จะถูกฝังในไบนารีตอน build ดังนั้นขณะรันจะไม่อ่าน
node_modules scriptc coverage --dynamicแสดงว่าแต่ละ statement รันในพื้นที่สแตติกหรือไดนามิก และแสดงปัจจัยที่ยังบล็อกอยู่- เอนจิน JavaScript จะถูกรวมเฉพาะเมื่อเลือก dynamic mode อย่างชัดเจน จึงไม่ทำให้ขนาดไบนารีเพิ่มขึ้นแบบเงียบ ๆ
ความถูกต้องและความปลอดภัยของหน่วยความจำ
- Differential testing รันโปรแกรมมากกว่า 800 รายการแยกกันบน Node และไบนารีเนทีฟ เพื่อเปรียบเทียบ stdout, stderr และ exit code แบบ byte-by-byte
- output ตัวเลขใช้รูปแบบ shortest round-trip และตรวจสอบด้วย fuzzing โดยเปรียบเทียบ
double1 ล้านค่ากับ Node - server ทดสอบด้วยการเชื่อมต่อ client driver จริงเข้ากับทั้งสอง implementation
- output ตัวเลขใช้รูปแบบ shortest round-trip และตรวจสอบด้วย fuzzing โดยเปรียบเทียบ
- รันชุดทดสอบทั้งหมดอีกครั้งภายใต้ AddressSanitizer และการ audit จำนวน reference หากมี leak หรือ use-after-free จะทำให้ build ล้มเหลว
- พฤติกรรมที่ตั้งใจให้ต่างจาก Node มีอยู่หลักสิบกรณี และส่วนใหญ่เกี่ยวกับ implementation ภายในด้าน timing และ property ของ error object
- ความแตกต่างแต่ละรายการมีการจัดทำเอกสารและกำหนดหมายเลขไว้ และไม่อนุญาตให้มีความแตกต่างแอบแฝง
ลักษณะด้านประสิทธิภาพ
- วัดบน Apple M series โดยเทียบกับ Node, Go, Rust และ Zig ด้วยงานเดียวกันและ output ที่เหมือนกันแบบ byte-by-byte
- เวลาเริ่มต้นประมาณ 2.4ms สั้นกว่า Node ที่ประมาณ 47ms ใกล้เคียงกับ Zig และนำหน้า Go กับ Rust
- ขนาดไบนารีแบบสแตติกอยู่ที่ 170–200KB และเมื่อรวม
--dynamicกับ dependency ที่ฝังมาจะอยู่ที่ประมาณ 3MB- ไบนารี Go ที่ยกมาเปรียบเทียบอยู่ที่ประมาณ 2MB ส่วน Node SEA อยู่ที่ 60–100MB
- การใช้หน่วยความจำทั่วไปอยู่ที่ RSS 1–4MB ขณะที่ Node อยู่ที่ 67–116MB
- runtime ยังคง semantics แบบ
f64ที่เหมาะกับ JavaScript พร้อมแข่งขันกับภาษาระบบได้ในงานส่วนใหญ่- integer inference และ ownership analysis อยู่ใน roadmap
ทางออกที่ระบุชัดเจน
comptime(() => ...)รัน TypeScript ณ เวลา build ภายใน VM ที่แยกอยู่ในคอมไพเลอร์ และใส่ผลลัพธ์เป็น literal ลงในไบนารี--ffiเชื่อม declaration ของ TypeScript ที่มีเฉพาะ signature เข้ากับการเรียก C ABI โดยตรง และลิงก์ archive, object และ system library ที่ประกาศไว้ใน manifest- boundary ระบุไว้อย่างชัดเจนและมีข้อมูลความยาวรวมอยู่ด้วย
- ดูรายละเอียดได้ใน Native FFI guide
- checked type assertion เช่น
JSON.parse(...) as Configจะแทรกโค้ดตรวจสอบใน runtime- เมื่อการตรวจสอบล้มเหลว จะ throw exception ที่มี path ที่ผิดพลาดและชนิดที่คาดหวัง/ชนิดจริง เช่น
expected number at $.port, got string
- เมื่อการตรวจสอบล้มเหลว จะ throw exception ที่มี path ที่ผิดพลาดและชนิดที่คาดหวัง/ชนิดจริง เช่น
โครงสร้างคอมไพเลอร์
- ขั้นตอนการประมวลผลเป็นลำดับ
TypeScript → tsc parsing/type checking → lowering → typed IR → C → clang → native executable packages/compilerรวม frontend ที่อิง tsc API, การตรวจสอบและ serialization ของ IR รวมถึง backend แบบ LLVM และ C- ใช้ IR เท่านั้นเป็น interface ระหว่าง frontend กับ backend
- LLVM เป็น code generator เริ่มต้น และใช้ fallback path ที่โปร่งใสสำหรับโปรแกรมที่อยู่นอกขอบเขตการรองรับ
- C ถูกคงไว้เป็น reference backend ถาวร และ
--backend cจะสร้างผลลัพธ์ที่อ่านได้พร้อมข้อมูลบรรทัดของ source
packages/runtimeทำงานกับ value แบบ reference counting กับ cycle collector, stackful fiber, event loop แบบkqueue, server stack และ output ตัวเลขที่เข้ากันได้กับ JavaScript- ใช้วิธีลิงก์ตามฟีเจอร์ ทำให้มีเฉพาะฟีเจอร์ที่ใช้งานจริงในไบนารี
packages/cliให้คำสั่งscriptc build,scriptc run,scriptc coverage
การติดตั้งและการพัฒนา
- ติดตั้งด้วย
npm install -g scriptcและต้องใช้ clang - แพลตฟอร์มหลักคือ macOS arm64 ส่วนไบนารี Linux และ Windows ทำด้วย cross-compilation
- แต่ละแพลตฟอร์มถูกตรวจสอบด้วยเส้นทาง differential testing แยกกัน
- เริ่มพัฒนาด้วย
pnpm install && pnpm buildpnpm testรันชุด differential testing และ diagnostic snapshotSCRIPTC_SAN=1 pnpm testรันชุดทดสอบเดียวกันภายใต้ ASan และการ audit จำนวน referencepnpm scriptc build x.ts --emit-irเก็บ C ที่สร้างขึ้นและx.ir.jsonไว้
- ทุกฟีเจอร์ถูกเพิ่มพร้อม differential test และจะ merge ได้ก็ต่อเมื่อผ่านทั้งการทดสอบทั่วไปและการทดสอบความปลอดภัยของหน่วยความจำ
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ดูเหมือนว่า Vercel จะปล่อยโปรเจกต์ที่เป็นกระแสออกมาประมาณเดือนละครั้ง เพื่อรักษาความน่าเชื่อถือและตัวตนในวงการไว้ ไม่น่าจะมีบริษัทหรือโปรเจกต์จริงจังไหนใช้ scriptc
เคารพผู้ร่วมพัฒนานะ แต่โค้ดดูมีกลิ่นว่า generate ด้วย Claude ชัดมาก และยิ่งน่าสงสัยเพราะ Claude ไม่ได้ถูกระบุเป็นผู้ร่วมพัฒนา
โปรเจกต์ / โพสต์ HN ที่เกี่ยวข้อง
Porffor ไล่ตามเป้าหมายเดียวกันนี้มาสักพักแล้ว นักพัฒนา CanadaHonk เก่งมาก แต่โปรเจกต์ยังผ่าน Test262 ได้เพียงประมาณ 68%
ถ้าไม่ได้เข้าใจขอบเขตโปรเจกต์ผิดไป วิธีที่ Vercel ทำความคืบหน้าได้เร็วขนาดนี้ก็น่าสงสัยมาก
เป็นโปรเจกต์สไตล์ Vercel แบบคลาสสิก เปิดตัวมา 5 วัน ทั้งหมดเป็น vibe coding ได้ดาว 1,500 ดวงแบบไร้เหตุผล แต่ไม่ได้แก้ปัญหาของใคร และอย่างมากอีกไม่กี่เดือนก็คงหยุดดูแล
แทนที่จะเอาแต่วิจารณ์ ผมลองเอาไปใช้กับหลายโปรเจกต์ในเครื่องแล้ว แต่ทั้งหมดเจอ ข้อผิดพลาดหลายร้อยรายการ ตอนวิเคราะห์ขอบเขตโค้ด จนแทบใช้ไม่ได้จริง
ถ้าเขียนขึ้นใหม่ตั้งแต่ต้นโดยไม่ใช้ไลบรารีภายนอก ก็คงคอมไพล์เป็นไบนารีได้ แต่ถ้าอย่างนั้นก็ไม่มีเหตุผลที่จะไม่ใช้ Rust, Go, Zig, D, C, V, Ada, C++, Nim, Swift, Kotlin Native, Haskell ซึ่งเป็นภาษาที่ออกแบบมาตั้งแต่แรกเพื่อการคอมไพล์อย่างจริงจัง
จุดแข็งของ TypeScript ไม่ใช่แค่ความสามารถในการสื่อความ แต่ยังรวมถึง ความเข้ากันได้กับ ecosystem npm ขนาดใหญ่ ด้วย แพ็กเกจส่วนใหญ่ประกาศอินเทอร์เฟซด้วย type declaration เท่านั้น ส่วนโค้ดจริงเผยแพร่เป็น JavaScript ดังนั้นถ้าจะใช้แพ็กเกจเหล่านี้ ในทางปฏิบัติก็ต้องมี JavaScript engine
ถ้าจะเริ่มใหม่ตั้งแต่ต้นและไม่มีแผนใช้แพ็กเกจ npm เลย ใช้ AssemblyScript น่าจะดีกว่า Node แนะนำอย่างชัดเจนว่าไม่ควรเผยแพร่แพ็กเกจเป็น TypeScript เพราะ TypeScript ไม่ได้ backward compatible แม้ระหว่าง minor version และการตั้งค่าคอมไพเลอร์ก็ไม่ portable ระหว่างแพ็กเกจ
โปรเจกต์แบบนี้สักอันก็ทำให้ติดหน้าแรกของบริการอย่าง HN ได้เรื่อย ๆ แล้ว เป็น กลยุทธ์การเติบโต ที่ลงทุนด้วย token เพื่อสร้างโปรเจกต์ดูดีแต่ไม่มีใครต้องการ แล้วเปิดซอร์สเพื่อขยาย reach จากนั้นก็ทำซ้ำ
อีก 12 เดือนข้างหน้า 90% ของโปรเจกต์โอเพนซอร์สอาจเป็นผลลัพธ์จาก vibe coding ที่แค่ดูน่าสนใจแต่ไม่มีผู้ใช้จริงก็ได้ ตอนนี้แม้แต่คอมไพเลอร์เต็มรูปแบบก็สร้างได้ง่าย แต่หัวใจคือการดูแลระยะยาวกับชุมชน ชื่อเรื่องสะดุดตาอย่างเดียวไม่ทำให้ผู้ใช้ยังอยู่
ถ้า Vercel จริงจัง ก็ควรรับต้นทุนและความเสี่ยงจริงด้วยการนำไปใช้เป็น runtime เชิงทดลอง ของตัวเอง
เป็นพื้นที่ปัญหาที่ยอดเยี่ยมมาก ผมทำงานคล้ายกันคือใช้ AI สร้างคอมไพเลอร์ที่ปรับ runtime code ให้เหมาะสม โดยนำไปใช้กับ Zod: zod-compiler
มันคอมไพล์ Zod schema ตอน build time ให้เป็นเชนของ boolean operation แบบง่าย ๆ ทำให้เร็วขึ้น 2–74 เท่า โดยไม่ต้องแก้โค้ด และปลั๊กอินจะแทนที่การเรียก Zod ด้วย parsing ที่คอมไพล์แล้ว การ optimize ส่วนใหญ่ Claude เขียนขึ้นจากการทำซ้ำมากกว่า 100 รอบ
เหมือนกับ scriptc ตรงที่สามารถเทียบผลกับ Zod จริงได้ จึงไม่ต้องตัดสินความถูกต้องแบบอัตวิสัย วิธีนี้ใช้ได้กับคอมไพเลอร์ เครื่องมือ serialization, formatter, query planner ฯลฯ ที่มี implementation อ้างอิงและ benchmark
ผมใช้ Claude รัน benchmark ระหว่าง scriptc กับ Node แม้ในผลลัพธ์แบบ byte array ที่เข้าทางที่สุด scriptc หลัง optimize เฉพาะทางก็ยัง ช้ากว่า Node 24 ประมาณ 7.5 เท่า
แต่การเริ่ม executable เร็วกว่าถึง 12 เท่า (1.5ms เทียบกับ 18.6ms), ใช้หน่วยความจำน้อยกว่า 72 เท่า (2.5MiB เทียบกับ 181MiB) และได้เป็น executable เดี่ยวขนาด 370KB ที่ไม่มี dependency ตอน runtime
ดีที่ยอมรับว่ามีความต้องการ executable แบบ native ที่เล็กและเร็ว แต่เมื่อดูเส้นทางที่ Java ผ่านมาตลอดหลายสิบปี ก็ยังสงสัยเรื่องความใช้งานได้จริง ในยุค 1990 GCJ ถือว่าโอเคในเชิงเทคนิค แต่ไม่มีการสนับสนุนจาก ecosystem
ต่อมา GraalVM Native จัดการปัญหาอย่างครอบคลุมกว่า ทำให้ไลบรารีและเฟรมเวิร์กหลัก ๆ เริ่มทำเรื่อง compatibility แต่ถึงตอนนี้ แม้แต่แอปเดิมแบบง่าย ๆ ก็ยังรันเป็น native ได้สมบูรณ์ยากมาก ความพยายามอย่าง scriptc น่ายินดี แต่เส้นทางสู่การใช้งานจริงคงยาวและลำบาก
หนึ่งในเหตุผลที่ Excelsior หายไปอาจเป็นเพราะ GraalVM และ OpenJ9 มีให้ใช้ฟรี ส่วน PTC กับ Aicas ยังดำเนินงานได้ดีเพราะมีกลุ่มลูกค้า embedded/real-time ที่ไม่ค่อยได้รับความสนใจ
ถ้าพัฒนาต่อเนื่อง ก็มีศักยภาพจะเป็นความสำเร็จใหญ่ระดับ .NET AOT ได้ เพิ่งเปิดตัวมาไม่กี่วัน ตอนนี้จึงแค่ลองเล่นเบา ๆ แต่ถ้าไม่ถูกทิ้งและพัฒนาต่อ ก็อาจช่วย ecosystem ได้มาก
โค้ดที่ AI สร้างก็มีช่วงคุณภาพกว้างเหมือนโค้ดที่มนุษย์เขียน ถ้าเป็นซอฟต์แวร์สำคัญ ก็ควรสร้างด้วยมาตรฐานเดียวกับตอนเขียนเองและตรวจโค้ดทั้งหมด ถ้าใช้แบบนั้นก็เป็นวิธีที่ยอดเยี่ยม โปรเจกต์ที่ตรวจทานไม่พอมีแนวโน้มว่าคุณภาพและความรับผิดชอบของผู้พัฒนาจะต่ำ ทำให้นำไปใช้ยากขึ้น
การถกเถียงออนไลน์มักไหลไปสุดขั้วระหว่าง “สร้างด้วย AI ทั้งหมด” กับ “ไม่ใช้ AI เด็ดขาด” แต่ในความเป็นจริง จุดกึ่งกลางที่เร่งการตัดสินใจอย่างรอบคอบ นั้นสมเหตุสมผล ซอฟต์แวร์ที่ไม่เป็นแบบนั้นทำให้ลังเลที่จะใช้ เพราะเสี่ยงคุณภาพต่ำหรือถูกปล่อยทิ้ง