1 คะแนน โดย GN⁺ 2 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • 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% ที่คอมไพล์แบบสแตติกได้
  • วิธีรันถูกแบ่งอย่างชัดเจนเป็นสามขั้น
    1. การคอมไพล์แบบสแตติก: เป็นโหมดพื้นฐาน โดยแปลงเป็นโค้ดเนทีฟโดยไม่ใช้เอนจิน JavaScript
    2. การรันแบบไดนามิก: เมื่อระบุ --dynamic จะรวม quickjs-ng ขนาดประมาณ 620KB เพื่อรัน JavaScript ของแพ็กเกจ npm และโค้ดชนิด any
      • ตรวจสอบค่าทุกค่าที่ข้ามมายังโค้ดสแตติกใน runtime
      • หากค่ากับชนิดที่ประกาศไม่ตรงกัน จะ throw TypeError ที่จับได้โดยไม่ทำให้หน่วยความจำเสียหาย
    3. การปฏิเสธ: โค้ดที่จัดการไม่ได้จะให้ 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 ที่รองรับ typed catch

Node และ Web API

  • Node API รองรับ fs, path, process, child_process, os, crypto, url/URL, zlib, timer และ signal handler
    • fs มีทั้ง synchronous API และ Promise API
    • child_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 ของระบบ

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 โดยเปรียบเทียบ double 1 ล้านค่ากับ Node
    • server ทดสอบด้วยการเชื่อมต่อ client driver จริงเข้ากับทั้งสอง implementation
  • รันชุดทดสอบทั้งหมดอีกครั้งภายใต้ 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

โครงสร้างคอมไพเลอร์

  • ขั้นตอนการประมวลผลเป็นลำดับ 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 build
    • pnpm test รันชุด differential testing และ diagnostic snapshot
    • SCRIPTC_SAN=1 pnpm test รันชุดทดสอบเดียวกันภายใต้ ASan และการ audit จำนวน reference
    • pnpm scriptc build x.ts --emit-ir เก็บ C ที่สร้างขึ้นและ x.ir.json ไว้
  • ทุกฟีเจอร์ถูกเพิ่มพร้อม differential test และจะ merge ได้ก็ต่อเมื่อผ่านทั้งการทดสอบทั่วไปและการทดสอบความปลอดภัยของหน่วยความจำ

1 ความคิดเห็น

 
GN⁺ 2 시간 전
ความคิดเห็นจาก Hacker News
  • ดูเหมือนว่า Vercel จะปล่อยโปรเจกต์ที่เป็นกระแสออกมาประมาณเดือนละครั้ง เพื่อรักษาความน่าเชื่อถือและตัวตนในวงการไว้ ไม่น่าจะมีบริษัทหรือโปรเจกต์จริงจังไหนใช้ scriptc
    เคารพผู้ร่วมพัฒนานะ แต่โค้ดดูมีกลิ่นว่า generate ด้วย Claude ชัดมาก และยิ่งน่าสงสัยเพราะ Claude ไม่ได้ถูกระบุเป็นผู้ร่วมพัฒนา

    • เป็น contribution ขนาดมหาศาลจริง ๆ ;-) คอมมิต
    • คนที่ขับเคลื่อนด้วย vibe coding คนเดียวกันนี้ยังเป็นผู้นำ zerolang “ภาษาโปรแกรมสำหรับเอเจนต์” ที่ Vercel เปิดตัวอย่างยิ่งใหญ่เมื่อเดือนพฤษภาคมด้วย หลังจากทำไป 1,200 คอมมิต การพัฒนาก็หยุดไปช่วงกลางเดือนมิถุนายน
      โปรเจกต์ / โพสต์ HN ที่เกี่ยวข้อง
    • Simon Willison ดูเหมือนจะ แก้แค่ README และไม่ได้มีส่วนร่วมจริงจังกับโปรเจกต์
    • ดูจากประวัติการมีส่วนร่วมแล้ว เหมือนว่า คนคนเดียวทำ vibe coding ไปประมาณ 99% และก็ดูเหมือนไม่มีพื้นฐานด้านคอมไพเลอร์ด้วย
    • ผลิตภัณฑ์ SaaS จำนวนมากร่วมมือกับ Vercel และในเครื่องมือสำหรับนักพัฒนา Next.js กับ React ถูกถือเป็น SDK ชั้นบนสุด
  • Porffor ไล่ตามเป้าหมายเดียวกันนี้มาสักพักแล้ว นักพัฒนา CanadaHonk เก่งมาก แต่โปรเจกต์ยังผ่าน Test262 ได้เพียงประมาณ 68%
    ถ้าไม่ได้เข้าใจขอบเขตโปรเจกต์ผิดไป วิธีที่ Vercel ทำความคืบหน้าได้เร็วขนาดนี้ก็น่าสงสัยมาก

    • เป็นเพราะ coding agent นั่นแหละ ในเวลาแค่หนึ่งสัปดาห์เพิ่มโค้ดไป 918,000 บรรทัด
  • เป็นโปรเจกต์สไตล์ 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 ระหว่างแพ็กเกจ

    • ดูเหมือนว่า scriptc จะแก้เรื่องนี้ด้วยการใส่ quickjs-ng engine ขนาด 620KB เข้าไปใน bundle แบบเลือกได้ เมื่อต้องรัน dependency เหล่านี้
    • อยากใช้กับ เครื่องมือ command-line ที่มีขอบเขตการใช้งานชัดเจน ซึ่งต้องแชร์โค้ดกับโปรเจกต์ TypeScript ที่ใหญ่กว่า มากกว่าโค้ดที่มี dependency เยอะ
    • นี่เป็นเหตุผลให้ต้องเผยแพร่ไลบรารีที่ไม่มี type ได้ แต่ผมไม่เข้าใจข้อสรุปที่ว่าเหตุผลนั้นมีน้ำหนักมากกว่าคุณค่าของ type
  • โปรเจกต์แบบนี้สักอันก็ทำให้ติดหน้าแรกของบริการอย่าง 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 น่ายินดี แต่เส้นทางสู่การใช้งานจริงคงยาวและลำบาก

    • เท่าที่รู้ ทีม Graal ก็เคยลอง แนวทาง meta-interpreter คล้ายกัน bytecode loading แบบ dynamic หรือ reflection ที่ native execution จัดการไม่ได้ ตั้งใจจะตีความด้วย implementation Java ที่ชื่อ Espresso
    • GCJ นั้นใกล้เคียง prototype มาโดยตลอด ถ้าเป็นผู้ใช้จริงจังคงซื้อ JDK เชิงพาณิชย์ที่มีเครื่องมือ AOT อย่าง Excelsior JET, BEA JRockit
      หนึ่งในเหตุผลที่ Excelsior หายไปอาจเป็นเพราะ GraalVM และ OpenJ9 มีให้ใช้ฟรี ส่วน PTC กับ Aicas ยังดำเนินงานได้ดีเพราะมีกลุ่มลูกค้า embedded/real-time ที่ไม่ค่อยได้รับความสนใจ
  • ถ้าพัฒนาต่อเนื่อง ก็มีศักยภาพจะเป็นความสำเร็จใหญ่ระดับ .NET AOT ได้ เพิ่งเปิดตัวมาไม่กี่วัน ตอนนี้จึงแค่ลองเล่นเบา ๆ แต่ถ้าไม่ถูกทิ้งและพัฒนาต่อ ก็อาจช่วย ecosystem ได้มาก
    โค้ดที่ AI สร้างก็มีช่วงคุณภาพกว้างเหมือนโค้ดที่มนุษย์เขียน ถ้าเป็นซอฟต์แวร์สำคัญ ก็ควรสร้างด้วยมาตรฐานเดียวกับตอนเขียนเองและตรวจโค้ดทั้งหมด ถ้าใช้แบบนั้นก็เป็นวิธีที่ยอดเยี่ยม โปรเจกต์ที่ตรวจทานไม่พอมีแนวโน้มว่าคุณภาพและความรับผิดชอบของผู้พัฒนาจะต่ำ ทำให้นำไปใช้ยากขึ้น
    การถกเถียงออนไลน์มักไหลไปสุดขั้วระหว่าง “สร้างด้วย AI ทั้งหมด” กับ “ไม่ใช้ AI เด็ดขาด” แต่ในความเป็นจริง จุดกึ่งกลางที่เร่งการตัดสินใจอย่างรอบคอบ นั้นสมเหตุสมผล ซอฟต์แวร์ที่ไม่เป็นแบบนั้นทำให้ลังเลที่จะใช้ เพราะเสี่ยงคุณภาพต่ำหรือถูกปล่อยทิ้ง